Compare commits
12 commits
29116ce26b
...
936855efab
| Author | SHA1 | Date | |
|---|---|---|---|
| 936855efab | |||
| 7fb0adbea6 | |||
| 503d868e29 | |||
| 88c8a018ad | |||
| 588a3ecb40 | |||
| eccfed2350 | |||
| fcc6ade8f3 | |||
| aaab0e5379 | |||
| dd5f35f507 | |||
| 9029eb5ff8 | |||
| 52a2bd6e6b | |||
| 72f0a6bccb |
14 changed files with 728 additions and 1 deletions
|
|
@ -33,7 +33,7 @@ The structure is flexible and is not limited to reading plans. Attainly can be u
|
|||
|
||||
When creating a schedule, the user selects:
|
||||
|
||||
* The collection or portion they want to complete
|
||||
* The set they want to complete
|
||||
* The level of the collection to schedule
|
||||
* The start date
|
||||
* The target completion date
|
||||
|
|
@ -47,6 +47,10 @@ For example, a user could choose to schedule:
|
|||
* Smaller sections within each chapter
|
||||
* Any other available level in the collection structure
|
||||
|
||||
Schedules always cover a whole set. To schedule only a subset of an existing
|
||||
set, the user creates a separate set containing that subset and gives it its
|
||||
own name.
|
||||
|
||||
## Daily Progress
|
||||
|
||||
Users can view the items scheduled for each day and mark them complete as they finish them.
|
||||
|
|
|
|||
|
|
@ -9,6 +9,9 @@ these rules.
|
|||
- Attainly is in early development.
|
||||
- Attainly helps users break hierarchical goals into scheduled assignments,
|
||||
complete daily work, and track progress toward a target date.
|
||||
- Schedules target whole sets. Users choose an element kind as the assignment
|
||||
granularity. To schedule a subset, create a separate named set containing
|
||||
that subset.
|
||||
- Schedule recalculation must preserve completed work while redistributing
|
||||
unfinished assignments across the remaining dates.
|
||||
- Planned features in `README.md` are future ideas, not authorized scope.
|
||||
|
|
|
|||
15
backend/app/Element/CreateElementDto.php
Normal file
15
backend/app/Element/CreateElementDto.php
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
<?php
|
||||
|
||||
namespace App\Element;
|
||||
|
||||
use App\Set\Set;
|
||||
|
||||
final readonly class CreateElementDto
|
||||
{
|
||||
public function __construct(
|
||||
public Set $set,
|
||||
public string $name,
|
||||
public string $kind,
|
||||
public ?Element $parentElement,
|
||||
) {}
|
||||
}
|
||||
47
backend/app/Element/Element.php
Normal file
47
backend/app/Element/Element.php
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
<?php
|
||||
|
||||
namespace App\Element;
|
||||
|
||||
use App\Set\Set;
|
||||
|
||||
final readonly class Element
|
||||
{
|
||||
public function __construct(
|
||||
private int $id,
|
||||
private string $name,
|
||||
private string $kind,
|
||||
private Set $set,
|
||||
private ?Element $parentElement,
|
||||
private int $position,
|
||||
) {}
|
||||
|
||||
public function getId(): int
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getName(): string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
public function getKind(): string
|
||||
{
|
||||
return $this->kind;
|
||||
}
|
||||
|
||||
public function getSet(): Set
|
||||
{
|
||||
return $this->set;
|
||||
}
|
||||
|
||||
public function getParentElement(): ?Element
|
||||
{
|
||||
return $this->parentElement;
|
||||
}
|
||||
|
||||
public function getPosition(): int
|
||||
{
|
||||
return $this->position;
|
||||
}
|
||||
}
|
||||
47
backend/app/Element/ElementModel.php
Normal file
47
backend/app/Element/ElementModel.php
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
<?php
|
||||
|
||||
namespace App\Element;
|
||||
|
||||
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
/**
|
||||
* @property int $id
|
||||
* @property int $set_id
|
||||
* @property string $name
|
||||
* @property string $kind
|
||||
* @property int|null $parent_element_id
|
||||
* @property int $position
|
||||
*
|
||||
* @method static Builder<static>|ElementModel newModelQuery()
|
||||
* @method static Builder<static>|ElementModel newQuery()
|
||||
* @method static Builder<static>|ElementModel query()
|
||||
*
|
||||
* @mixin \Eloquent
|
||||
*/
|
||||
#[Fillable([
|
||||
'set_id',
|
||||
'name',
|
||||
'kind',
|
||||
'parent_element_id',
|
||||
'position',
|
||||
])]
|
||||
class ElementModel extends Model
|
||||
{
|
||||
protected $table = 'elements';
|
||||
|
||||
public $timestamps = false;
|
||||
|
||||
/**
|
||||
* @return array<string, string>
|
||||
*/
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'set_id' => 'integer',
|
||||
'parent_element_id' => 'integer',
|
||||
'position' => 'integer',
|
||||
];
|
||||
}
|
||||
}
|
||||
26
backend/app/Element/ElementRepository.php
Normal file
26
backend/app/Element/ElementRepository.php
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
<?php
|
||||
|
||||
namespace App\Element;
|
||||
|
||||
use App\Set\Set;
|
||||
use DomainException;
|
||||
|
||||
interface ElementRepository
|
||||
{
|
||||
/**
|
||||
* @throws DomainException
|
||||
*/
|
||||
public function create(CreateElementDto $dto): Element;
|
||||
|
||||
public function find(int $id): ?Element;
|
||||
|
||||
/**
|
||||
* @return list<Element>
|
||||
*/
|
||||
public function findTopLevelBySet(Set $set): array;
|
||||
|
||||
/**
|
||||
* @return list<Element>
|
||||
*/
|
||||
public function findByParentElement(Element $parentElement): array;
|
||||
}
|
||||
171
backend/app/Element/EloquentElementRepository.php
Normal file
171
backend/app/Element/EloquentElementRepository.php
Normal file
|
|
@ -0,0 +1,171 @@
|
|||
<?php
|
||||
|
||||
namespace App\Element;
|
||||
|
||||
use App\Set\Set;
|
||||
use App\Set\SetRepository;
|
||||
use DomainException;
|
||||
use RuntimeException;
|
||||
|
||||
class EloquentElementRepository implements ElementRepository
|
||||
{
|
||||
public function __construct(
|
||||
private SetRepository $setRepository,
|
||||
) {}
|
||||
|
||||
public function create(CreateElementDto $dto): Element
|
||||
{
|
||||
$this->validateParentSet($dto);
|
||||
$position = $this->nextPosition(
|
||||
$dto->set,
|
||||
$dto->parentElement,
|
||||
);
|
||||
$model = ElementModel::create([
|
||||
'set_id' => $dto->set->getId(),
|
||||
'name' => $dto->name,
|
||||
'kind' => $dto->kind,
|
||||
'parent_element_id' => $dto->parentElement?->getId(),
|
||||
'position' => $position,
|
||||
]);
|
||||
|
||||
return new Element(
|
||||
id: $model->id,
|
||||
name: $model->name,
|
||||
kind: $model->kind,
|
||||
set: $dto->set,
|
||||
parentElement: $dto->parentElement,
|
||||
position: $model->position,
|
||||
);
|
||||
}
|
||||
|
||||
public function find(int $id): ?Element
|
||||
{
|
||||
$model = ElementModel::find($id);
|
||||
|
||||
return $model === null ? null : $this->toDomain($model);
|
||||
}
|
||||
|
||||
public function findTopLevelBySet(Set $set): array
|
||||
{
|
||||
$models = ElementModel::query()
|
||||
->where('set_id', $set->getId())
|
||||
->whereNull('parent_element_id')
|
||||
->orderBy('position')
|
||||
->orderBy('id')
|
||||
->get();
|
||||
$elements = [];
|
||||
|
||||
foreach ($models as $model) {
|
||||
$elements[] = $this->toDomainWithRelations(
|
||||
model: $model,
|
||||
set: $set,
|
||||
parentElement: null,
|
||||
);
|
||||
}
|
||||
|
||||
return $elements;
|
||||
}
|
||||
|
||||
public function findByParentElement(Element $parentElement): array
|
||||
{
|
||||
$models = ElementModel::query()
|
||||
->where('set_id', $parentElement->getSet()->getId())
|
||||
->where('parent_element_id', $parentElement->getId())
|
||||
->orderBy('position')
|
||||
->orderBy('id')
|
||||
->get();
|
||||
$elements = [];
|
||||
|
||||
foreach ($models as $model) {
|
||||
$elements[] = $this->toDomainWithRelations(
|
||||
model: $model,
|
||||
set: $parentElement->getSet(),
|
||||
parentElement: $parentElement,
|
||||
);
|
||||
}
|
||||
|
||||
return $elements;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws DomainException
|
||||
*/
|
||||
private function validateParentSet(CreateElementDto $dto): void
|
||||
{
|
||||
$parentElement = $dto->parentElement;
|
||||
if ($parentElement === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($parentElement->getSet()->getId() !== $dto->set->getId()) {
|
||||
throw new DomainException(
|
||||
'parent element must belong to the same set',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private function nextPosition(
|
||||
Set $set,
|
||||
?Element $parentElement,
|
||||
): int {
|
||||
$query = ElementModel::query()
|
||||
->where('set_id', $set->getId());
|
||||
if ($parentElement === null) {
|
||||
$query->whereNull('parent_element_id');
|
||||
} else {
|
||||
$query->where('parent_element_id', $parentElement->getId());
|
||||
}
|
||||
|
||||
$currentMaximum = $query->max('position');
|
||||
if ($currentMaximum === null) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
return (int) $currentMaximum + 1;
|
||||
}
|
||||
|
||||
private function toDomain(ElementModel $model): Element
|
||||
{
|
||||
$set = $this->findSet($model->set_id);
|
||||
|
||||
$parentElement = null;
|
||||
if ($model->parent_element_id !== null) {
|
||||
$parentElement = $this->find($model->parent_element_id);
|
||||
if ($parentElement === null) {
|
||||
throw new RuntimeException('element parent not found');
|
||||
}
|
||||
}
|
||||
|
||||
return $this->toDomainWithRelations(
|
||||
model: $model,
|
||||
set: $set,
|
||||
parentElement: $parentElement,
|
||||
);
|
||||
}
|
||||
|
||||
private function toDomainWithRelations(
|
||||
ElementModel $model,
|
||||
Set $set,
|
||||
?Element $parentElement,
|
||||
): Element {
|
||||
return new Element(
|
||||
id: $model->id,
|
||||
name: $model->name,
|
||||
kind: $model->kind,
|
||||
set: $set,
|
||||
parentElement: $parentElement,
|
||||
position: $model->position,
|
||||
);
|
||||
}
|
||||
|
||||
private function findSet(int $id): Set
|
||||
{
|
||||
foreach ($this->setRepository->all() as $set) {
|
||||
if ($set->getId() === $id) {
|
||||
return $set;
|
||||
}
|
||||
}
|
||||
|
||||
throw new RuntimeException('element set not found');
|
||||
}
|
||||
}
|
||||
|
|
@ -16,6 +16,8 @@ use App\Email\Emailer;
|
|||
use App\Email\EmailFactory;
|
||||
use App\Email\LaravelEmailer;
|
||||
use App\Email\LaravelEmailFactory;
|
||||
use App\Element\ElementRepository;
|
||||
use App\Element\EloquentElementRepository;
|
||||
use App\Set\EloquentSetRepository;
|
||||
use App\Set\SetRepository;
|
||||
use App\User\EloquentUserRepository;
|
||||
|
|
@ -51,6 +53,10 @@ class AppServiceProvider extends ServiceProvider
|
|||
SetRepository::class,
|
||||
EloquentSetRepository::class,
|
||||
);
|
||||
$this->app->bind(
|
||||
ElementRepository::class,
|
||||
EloquentElementRepository::class,
|
||||
);
|
||||
$this->app->bind(PasswordHasher::class, BcryptPasswordHasher::class);
|
||||
$this->app->bind(TokenGenerator::class, RandomTokenGenerator::class);
|
||||
$this->app->bind(Clock::class, SystemClock::class);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,35 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('elements', function (Blueprint $table): void {
|
||||
$table->id();
|
||||
$table->foreignId('set_id')
|
||||
->constrained('sets')
|
||||
->restrictOnDelete();
|
||||
$table->string('name');
|
||||
$table->string('kind');
|
||||
$table->foreignId('parent_element_id')
|
||||
->nullable()
|
||||
->constrained('elements')
|
||||
->restrictOnDelete();
|
||||
$table->unsignedInteger('position');
|
||||
$table->index([
|
||||
'set_id',
|
||||
'parent_element_id',
|
||||
'position',
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('elements');
|
||||
}
|
||||
};
|
||||
147
backend/tests/Fakes/FakeElementRepository.php
Normal file
147
backend/tests/Fakes/FakeElementRepository.php
Normal file
|
|
@ -0,0 +1,147 @@
|
|||
<?php
|
||||
|
||||
namespace Tests\Fakes;
|
||||
|
||||
use App\Element\CreateElementDto;
|
||||
use App\Element\Element;
|
||||
use App\Element\ElementRepository;
|
||||
use App\Set\Set;
|
||||
use DomainException;
|
||||
|
||||
class FakeElementRepository implements ElementRepository
|
||||
{
|
||||
/**
|
||||
* @var array<int, Element>
|
||||
*/
|
||||
private array $elements = [];
|
||||
|
||||
public function create(CreateElementDto $dto): Element
|
||||
{
|
||||
$this->validateParentSet($dto);
|
||||
$id = count($this->elements) + 1;
|
||||
$element = new Element(
|
||||
id: $id,
|
||||
name: $dto->name,
|
||||
kind: $dto->kind,
|
||||
set: $dto->set,
|
||||
parentElement: $dto->parentElement,
|
||||
position: $this->nextPosition($dto),
|
||||
);
|
||||
$this->elements[$id] = $element;
|
||||
|
||||
return $this->copy($element);
|
||||
}
|
||||
|
||||
public function find(int $id): ?Element
|
||||
{
|
||||
$element = $this->elements[$id] ?? null;
|
||||
|
||||
return $element === null ? null : $this->copy($element);
|
||||
}
|
||||
|
||||
public function findTopLevelBySet(Set $set): array
|
||||
{
|
||||
$elements = array_filter(
|
||||
$this->elements,
|
||||
function (Element $element) use ($set): bool {
|
||||
return $element->getSet()->getId() === $set->getId()
|
||||
&& $element->getParentElement() === null;
|
||||
},
|
||||
);
|
||||
|
||||
return $this->orderedCopies($elements);
|
||||
}
|
||||
|
||||
public function findByParentElement(Element $parentElement): array
|
||||
{
|
||||
$elements = array_filter(
|
||||
$this->elements,
|
||||
function (Element $element) use ($parentElement): bool {
|
||||
return $element->getSet()->getId()
|
||||
=== $parentElement->getSet()->getId()
|
||||
&& $element->getParentElement()?->getId()
|
||||
=== $parentElement->getId();
|
||||
},
|
||||
);
|
||||
|
||||
return $this->orderedCopies($elements);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws DomainException
|
||||
*/
|
||||
private function validateParentSet(CreateElementDto $dto): void
|
||||
{
|
||||
$parentElement = $dto->parentElement;
|
||||
if ($parentElement === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($parentElement->getSet()->getId() !== $dto->set->getId()) {
|
||||
throw new DomainException(
|
||||
'parent element must belong to the same set',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private function nextPosition(CreateElementDto $dto): int
|
||||
{
|
||||
$requestedParentId = $dto->parentElement?->getId();
|
||||
$maximumPosition = 0;
|
||||
|
||||
foreach ($this->elements as $element) {
|
||||
if ($element->getSet()->getId() !== $dto->set->getId()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$elementParentId = $element->getParentElement()?->getId();
|
||||
if ($elementParentId !== $requestedParentId) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$maximumPosition = max(
|
||||
$maximumPosition,
|
||||
$element->getPosition(),
|
||||
);
|
||||
}
|
||||
|
||||
return $maximumPosition + 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, Element> $elements
|
||||
* @return list<Element>
|
||||
*/
|
||||
private function orderedCopies(array $elements): array
|
||||
{
|
||||
usort($elements, function (Element $first, Element $second): int {
|
||||
$positionComparison = $first->getPosition()
|
||||
<=> $second->getPosition();
|
||||
if ($positionComparison !== 0) {
|
||||
return $positionComparison;
|
||||
}
|
||||
|
||||
return $first->getId() <=> $second->getId();
|
||||
});
|
||||
|
||||
return array_map(function (Element $element): Element {
|
||||
return $this->copy($element);
|
||||
}, $elements);
|
||||
}
|
||||
|
||||
private function copy(Element $element): Element
|
||||
{
|
||||
$parentElement = $element->getParentElement();
|
||||
|
||||
return new Element(
|
||||
id: $element->getId(),
|
||||
name: $element->getName(),
|
||||
kind: $element->getKind(),
|
||||
set: $element->getSet(),
|
||||
parentElement: $parentElement === null
|
||||
? null
|
||||
: $this->copy($parentElement),
|
||||
position: $element->getPosition(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -3,6 +3,7 @@
|
|||
namespace Tests\Feature\Auth;
|
||||
|
||||
use App\Auth\CreateSessionDto;
|
||||
use App\Auth\Clock;
|
||||
use App\Auth\SessionRepository;
|
||||
use App\Http\Middleware\AuthMiddleware;
|
||||
use App\Shared\ValueObject\EmailAddress;
|
||||
|
|
@ -11,6 +12,7 @@ use App\User\UserRepository;
|
|||
use DateTimeImmutable;
|
||||
use DateTimeZone;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\Fakes\FakeClock;
|
||||
use Tests\TestCase;
|
||||
|
||||
class LogoutEndpointTest extends TestCase
|
||||
|
|
@ -23,6 +25,7 @@ class LogoutEndpointTest extends TestCase
|
|||
'2026-07-31T12:00:00',
|
||||
new DateTimeZone('UTC'),
|
||||
);
|
||||
$this->app->instance(Clock::class, new FakeClock($now));
|
||||
$user = app(UserRepository::class)->create(new CreateUserDto(
|
||||
email: new EmailAddress('user@example.com'),
|
||||
passwordHash: 'hashed-password',
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
namespace Tests\Feature\Auth;
|
||||
|
||||
use App\Auth\CreateSessionDto;
|
||||
use App\Auth\Clock;
|
||||
use App\Auth\SessionRepository;
|
||||
use App\Http\Middleware\AuthMiddleware;
|
||||
use App\Shared\ValueObject\EmailAddress;
|
||||
|
|
@ -11,6 +12,7 @@ use App\User\UserRepository;
|
|||
use DateTimeImmutable;
|
||||
use DateTimeZone;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\Fakes\FakeClock;
|
||||
use Tests\TestCase;
|
||||
|
||||
class MeEndpointTest extends TestCase
|
||||
|
|
@ -23,6 +25,7 @@ class MeEndpointTest extends TestCase
|
|||
'2026-07-31T12:00:00',
|
||||
new DateTimeZone('UTC'),
|
||||
);
|
||||
$this->app->instance(Clock::class, new FakeClock($now));
|
||||
$user = app(UserRepository::class)->create(new CreateUserDto(
|
||||
email: new EmailAddress('user@example.com'),
|
||||
passwordHash: 'hashed-password',
|
||||
|
|
|
|||
171
backend/tests/Feature/Element/EloquentElementRepositoryTest.php
Normal file
171
backend/tests/Feature/Element/EloquentElementRepositoryTest.php
Normal file
|
|
@ -0,0 +1,171 @@
|
|||
<?php
|
||||
|
||||
namespace Tests\Feature\Element;
|
||||
|
||||
use App\Element\CreateElementDto;
|
||||
use App\Element\ElementRepository;
|
||||
use App\Set\CreateSetDto;
|
||||
use App\Set\Set;
|
||||
use App\Set\SetRepository;
|
||||
use App\Shared\ValueObject\EmailAddress;
|
||||
use App\User\CreateUserDto;
|
||||
use App\User\UserRepository;
|
||||
use DomainException;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
class EloquentElementRepositoryTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function testItPersistsNestedElementsInSiblingOrder(): void
|
||||
{
|
||||
$set = $this->createSet('Bible');
|
||||
$repository = app(ElementRepository::class);
|
||||
$genesisBook = $repository->create(new CreateElementDto(
|
||||
set: $set,
|
||||
name: 'Genesis',
|
||||
kind: 'book',
|
||||
parentElement: null,
|
||||
));
|
||||
$exodusBook = $repository->create(new CreateElementDto(
|
||||
set: $set,
|
||||
name: 'Exodus',
|
||||
kind: 'book',
|
||||
parentElement: null,
|
||||
));
|
||||
$genesisPortion = $repository->create(new CreateElementDto(
|
||||
set: $set,
|
||||
name: 'Genesis',
|
||||
kind: 'portion',
|
||||
parentElement: $genesisBook,
|
||||
));
|
||||
$noahPortion = $repository->create(new CreateElementDto(
|
||||
set: $set,
|
||||
name: 'Noah',
|
||||
kind: 'portion',
|
||||
parentElement: $genesisBook,
|
||||
));
|
||||
|
||||
$this->assertSame(1, $genesisBook->getPosition());
|
||||
$this->assertSame(2, $exodusBook->getPosition());
|
||||
$this->assertSame(1, $genesisPortion->getPosition());
|
||||
$this->assertSame(2, $noahPortion->getPosition());
|
||||
$this->assertDatabaseHas('elements', [
|
||||
'id' => $genesisPortion->getId(),
|
||||
'set_id' => $set->getId(),
|
||||
'name' => 'Genesis',
|
||||
'kind' => 'portion',
|
||||
'parent_element_id' => $genesisBook->getId(),
|
||||
'position' => 1,
|
||||
]);
|
||||
|
||||
$foundElement = $repository->find($noahPortion->getId());
|
||||
|
||||
$this->assertNotNull($foundElement);
|
||||
$this->assertSame('Noah', $foundElement->getName());
|
||||
$this->assertSame('portion', $foundElement->getKind());
|
||||
$this->assertSame($set->getId(), $foundElement->getSet()->getId());
|
||||
$this->assertSame(
|
||||
$genesisBook->getId(),
|
||||
$foundElement->getParentElement()?->getId(),
|
||||
);
|
||||
}
|
||||
|
||||
public function testItRejectsAParentFromAnotherSet(): void
|
||||
{
|
||||
$bible = $this->createSet('Bible');
|
||||
$course = $this->createSet('Course');
|
||||
$repository = app(ElementRepository::class);
|
||||
$book = $repository->create(new CreateElementDto(
|
||||
set: $bible,
|
||||
name: 'Genesis',
|
||||
kind: 'book',
|
||||
parentElement: null,
|
||||
));
|
||||
|
||||
$this->expectException(DomainException::class);
|
||||
$this->expectExceptionMessage(
|
||||
'parent element must belong to the same set',
|
||||
);
|
||||
|
||||
$repository->create(new CreateElementDto(
|
||||
set: $course,
|
||||
name: 'Invalid lesson',
|
||||
kind: 'lesson',
|
||||
parentElement: $book,
|
||||
));
|
||||
}
|
||||
|
||||
public function testItListsOrderedElementsWithinTheirTreeScope(): void
|
||||
{
|
||||
$bible = $this->createSet('Bible');
|
||||
$course = $this->createSet('Course');
|
||||
$repository = app(ElementRepository::class);
|
||||
$genesisBook = $repository->create(new CreateElementDto(
|
||||
set: $bible,
|
||||
name: 'Genesis',
|
||||
kind: 'book',
|
||||
parentElement: null,
|
||||
));
|
||||
$exodusBook = $repository->create(new CreateElementDto(
|
||||
set: $bible,
|
||||
name: 'Exodus',
|
||||
kind: 'book',
|
||||
parentElement: null,
|
||||
));
|
||||
$repository->create(new CreateElementDto(
|
||||
set: $course,
|
||||
name: 'Module 1',
|
||||
kind: 'module',
|
||||
parentElement: null,
|
||||
));
|
||||
$repository->create(new CreateElementDto(
|
||||
set: $bible,
|
||||
name: 'Genesis',
|
||||
kind: 'portion',
|
||||
parentElement: $genesisBook,
|
||||
));
|
||||
$repository->create(new CreateElementDto(
|
||||
set: $bible,
|
||||
name: 'Noah',
|
||||
kind: 'portion',
|
||||
parentElement: $genesisBook,
|
||||
));
|
||||
$repository->create(new CreateElementDto(
|
||||
set: $bible,
|
||||
name: 'Shemot',
|
||||
kind: 'portion',
|
||||
parentElement: $exodusBook,
|
||||
));
|
||||
|
||||
$topLevelElements = $repository->findTopLevelBySet($bible);
|
||||
$childElements = $repository->findByParentElement($genesisBook);
|
||||
|
||||
$this->assertSame(
|
||||
['Genesis', 'Exodus'],
|
||||
array_map(function ($element): string {
|
||||
return $element->getName();
|
||||
}, $topLevelElements),
|
||||
);
|
||||
$this->assertSame(
|
||||
['Genesis', 'Noah'],
|
||||
array_map(function ($element): string {
|
||||
return $element->getName();
|
||||
}, $childElements),
|
||||
);
|
||||
}
|
||||
|
||||
private function createSet(string $name): Set
|
||||
{
|
||||
$creator = app(UserRepository::class)->create(new CreateUserDto(
|
||||
email: new EmailAddress(strtolower($name) . '@example.com'),
|
||||
passwordHash: 'hashed-password',
|
||||
));
|
||||
|
||||
return app(SetRepository::class)->create(new CreateSetDto(
|
||||
name: $name,
|
||||
creator: $creator,
|
||||
));
|
||||
}
|
||||
}
|
||||
49
backend/tests/Unit/Element/ElementTest.php
Normal file
49
backend/tests/Unit/Element/ElementTest.php
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
<?php
|
||||
|
||||
namespace Tests\Unit\Element;
|
||||
|
||||
use App\Element\Element;
|
||||
use App\Set\Set;
|
||||
use App\Shared\ValueObject\EmailAddress;
|
||||
use App\User\User;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class ElementTest extends TestCase
|
||||
{
|
||||
public function testItExposesItsHierarchicalIdentity(): void
|
||||
{
|
||||
$creator = new User(
|
||||
id: 7,
|
||||
email: new EmailAddress('creator@example.com'),
|
||||
passwordHash: 'hashed-password',
|
||||
);
|
||||
$set = new Set(
|
||||
id: 11,
|
||||
name: 'Bible',
|
||||
creator: $creator,
|
||||
);
|
||||
$parentElement = new Element(
|
||||
id: 21,
|
||||
name: 'Genesis',
|
||||
kind: 'book',
|
||||
set: $set,
|
||||
parentElement: null,
|
||||
position: 1,
|
||||
);
|
||||
$element = new Element(
|
||||
id: 22,
|
||||
name: 'Genesis',
|
||||
kind: 'portion',
|
||||
set: $set,
|
||||
parentElement: $parentElement,
|
||||
position: 2,
|
||||
);
|
||||
|
||||
$this->assertSame(22, $element->getId());
|
||||
$this->assertSame('Genesis', $element->getName());
|
||||
$this->assertSame('portion', $element->getKind());
|
||||
$this->assertSame($set, $element->getSet());
|
||||
$this->assertSame($parentElement, $element->getParentElement());
|
||||
$this->assertSame(2, $element->getPosition());
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue