diff --git a/backend/tests/Fakes/FakeElementRepository.php b/backend/tests/Fakes/FakeElementRepository.php index 1e9200d..628a37f 100644 --- a/backend/tests/Fakes/FakeElementRepository.php +++ b/backend/tests/Fakes/FakeElementRepository.php @@ -22,8 +22,7 @@ class FakeElementRepository implements ElementRepository $element = new Element( id: $id, name: $dto->name, - kind: $dto->kind, - set: $dto->set, + level: $dto->level, parentElement: $dto->parentElement, position: $this->nextPosition($dto), ); @@ -88,14 +87,32 @@ class FakeElementRepository implements ElementRepository { $parentElement = $dto->parentElement; if ($parentElement === null) { + if ($dto->level->getDepth() !== 0) { + throw new DomainException( + 'root element must use the first set level', + ); + } + return; } - if ($parentElement->getSet()->getId() !== $dto->set->getId()) { + if ( + $parentElement->getSet()->getId() + !== $dto->level->getSet()->getId() + ) { throw new DomainException( 'parent element must belong to the same set', ); } + + if ( + $dto->level->getDepth() + !== $parentElement->getLevel()->getDepth() + 1 + ) { + throw new DomainException( + 'child element must use the next set level', + ); + } } private function nextPosition(CreateElementDto $dto): int @@ -104,7 +121,10 @@ class FakeElementRepository implements ElementRepository $maximumPosition = 0; foreach ($this->elements as $element) { - if ($element->getSet()->getId() !== $dto->set->getId()) { + if ( + $element->getSet()->getId() + !== $dto->level->getSet()->getId() + ) { continue; } @@ -150,8 +170,7 @@ class FakeElementRepository implements ElementRepository return new Element( id: $element->getId(), name: $element->getName(), - kind: $element->getKind(), - set: $element->getSet(), + level: $element->getLevel(), parentElement: $parentElement === null ? null : $this->copy($parentElement), diff --git a/backend/tests/Fakes/FakeSetLevelRepository.php b/backend/tests/Fakes/FakeSetLevelRepository.php new file mode 100644 index 0000000..0f3f076 --- /dev/null +++ b/backend/tests/Fakes/FakeSetLevelRepository.php @@ -0,0 +1,76 @@ + + */ + private array $levels = []; + + public function create(CreateSetLevelDto $dto): SetLevel + { + foreach ($this->levels as $level) { + if ( + $level->getSet()->getId() === $dto->set->getId() + && $level->getKind() === $dto->kind + ) { + throw new DomainException( + 'level kind must be unique within set', + ); + } + } + + $id = count($this->levels) + 1; + $level = new SetLevel( + id: $id, + set: $dto->set, + kind: $dto->kind, + depth: count($this->findBySet($dto->set)), + ); + $this->levels[$id] = $level; + + return $this->copy($level); + } + + public function find(int $id): ?SetLevel + { + $level = $this->levels[$id] ?? null; + + return $level === null ? null : $this->copy($level); + } + + public function findBySet(Set $set): array + { + $levels = array_filter( + $this->levels, + function (SetLevel $level) use ($set): bool { + return $level->getSet()->getId() === $set->getId(); + }, + ); + usort($levels, function (SetLevel $first, SetLevel $second): int { + return $first->getDepth() <=> $second->getDepth(); + }); + + return array_map(function (SetLevel $level): SetLevel { + return $this->copy($level); + }, $levels); + } + + private function copy(SetLevel $level): SetLevel + { + return new SetLevel( + id: $level->getId(), + set: $level->getSet(), + kind: $level->getKind(), + depth: $level->getDepth(), + ); + } +} diff --git a/backend/tests/Feature/Database/DatabaseSeederTest.php b/backend/tests/Feature/Database/DatabaseSeederTest.php index e6ce46e..59d8be8 100644 --- a/backend/tests/Feature/Database/DatabaseSeederTest.php +++ b/backend/tests/Feature/Database/DatabaseSeederTest.php @@ -6,6 +6,7 @@ use App\Auth\PasswordHasher; use App\Element\Element; use App\Element\ElementRepository; use App\Set\SetRepository; +use App\Set\SetLevelRepository; use App\Shared\ValueObject\EmailAddress; use App\User\UserRepository; use Database\Seeders\UserSeeder; @@ -70,6 +71,12 @@ class DatabaseSeederTest extends TestCase $sets = app(SetRepository::class)->all(); $elementRepository = app(ElementRepository::class); + $setLevelRepository = app(SetLevelRepository::class); + $expectedLevels = [ + 'Bible' => ['book', 'portion', 'chapter'], + 'Course' => ['module', 'lesson'], + 'Fitness Program' => ['phase', 'workout', 'exercise'], + ]; $expectedElements = [ 'Bible' => [ 'Chapter 1:chapter:Creation', @@ -100,9 +107,16 @@ class DatabaseSeederTest extends TestCase ], ]; + $this->assertDatabaseCount('set_levels', 8); $this->assertDatabaseCount('elements', 21); foreach ($sets as $set) { + $this->assertSame( + $expectedLevels[$set->getName()], + array_map(function ($level): string { + return $level->getKind(); + }, $setLevelRepository->findBySet($set)), + ); $signatures = array_map(function (Element $element): string { $parentName = $element->getParentElement()?->getName() ?? 'root'; diff --git a/backend/tests/Feature/Schedule/ScheduleEndpointTest.php b/backend/tests/Feature/Schedule/ScheduleEndpointTest.php index 1093de8..e3aca06 100644 --- a/backend/tests/Feature/Schedule/ScheduleEndpointTest.php +++ b/backend/tests/Feature/Schedule/ScheduleEndpointTest.php @@ -9,7 +9,11 @@ use App\Element\ElementModel; use App\Element\ElementRepository; use App\Http\Middleware\AuthMiddleware; use App\Set\CreateSetDto; +use App\Set\CreateSetLevelDto; use App\Set\Set; +use App\Set\SetLevel; +use App\Set\SetLevelModel; +use App\Set\SetLevelRepository; use App\Set\SetModel; use App\Set\SetRepository; use App\Shared\ValueObject\EmailAddress; @@ -32,42 +36,45 @@ class ScheduleEndpointTest extends TestCase $user = $this->createUser('reader@example.com'); $creator = $this->createUser('creator@example.com'); $set = $this->createSet($creator, 'Bible'); + $bookLevel = $this->createLevel($set, 'book'); + $portionLevel = $this->createLevel($set, 'portion'); + $chapterLevel = $this->createLevel($set, 'chapter'); $repository = app(ElementRepository::class); $genesis = $repository->create(new CreateElementDto( - set: $set, name: 'Genesis', - kind: 'book', + level: $bookLevel, parentElement: null, )); $creation = $repository->create(new CreateElementDto( - set: $set, name: 'Creation', - kind: 'portion', + level: $portionLevel, parentElement: $genesis, )); - $chapterOne = $repository->create(new CreateElementDto( - set: $set, + $repository->create(new CreateElementDto( name: 'Chapter 1', - kind: 'chapter', + level: $chapterLevel, parentElement: $creation, )); $exodus = $repository->create(new CreateElementDto( - set: $set, name: 'Exodus', - kind: 'book', + level: $bookLevel, parentElement: null, )); - $chapterTwo = $repository->create(new CreateElementDto( - set: $set, - name: 'Chapter 1', - kind: 'chapter', + $shemot = $repository->create(new CreateElementDto( + name: 'Shemot', + level: $portionLevel, parentElement: $exodus, )); + $repository->create(new CreateElementDto( + name: 'Chapter 1', + level: $chapterLevel, + parentElement: $shemot, + )); $this->createSession($user, 'valid-token'); $response = $this->credentialedPost('/api/schedules', [ 'setId' => $set->getId(), - 'elementKind' => 'chapter', + 'levelId' => $chapterLevel->getId(), 'startDate' => '2026-08-10', 'targetDate' => '2026-08-12', ]); @@ -114,6 +121,7 @@ class ScheduleEndpointTest extends TestCase 'kind' => 'chapter', 'path' => [ 'Exodus', + 'Shemot', 'Chapter 1', ], ], @@ -144,6 +152,7 @@ class ScheduleEndpointTest extends TestCase ]); $this->assertDatabaseCount('schedule_assignments', 2); $this->assertFalse(Schema::hasColumn('schedules', 'set_id')); + $this->assertFalse(Schema::hasColumn('schedules', 'set_level_id')); $this->assertFalse(Schema::hasColumn( 'schedule_assignments', 'element_id', @@ -159,10 +168,10 @@ class ScheduleEndpointTest extends TestCase $user = $this->createUser('reader@example.com'); $otherUser = $this->createUser('other@example.com'); $set = $this->createSet($user, 'Course'); + $lessonLevel = $this->createLevel($set, 'lesson'); app(ElementRepository::class)->create(new CreateElementDto( - set: $set, name: 'Welcome', - kind: 'lesson', + level: $lessonLevel, parentElement: null, )); $this->createSession($user, 'valid-token'); @@ -170,13 +179,13 @@ class ScheduleEndpointTest extends TestCase $this->credentialedPost('/api/schedules', [ 'setId' => $set->getId(), - 'elementKind' => 'lesson', + 'levelId' => $lessonLevel->getId(), 'startDate' => '2026-08-01', 'targetDate' => '2026-08-01', ])->assertCreated(); $this->credentialedPost('/api/schedules', [ 'setId' => $set->getId(), - 'elementKind' => 'lesson', + 'levelId' => $lessonLevel->getId(), 'startDate' => '2026-09-01', 'targetDate' => '2026-09-01', ])->assertCreated(); @@ -186,7 +195,7 @@ class ScheduleEndpointTest extends TestCase 'other-token', )->postJson('/api/schedules', [ 'setId' => $set->getId(), - 'elementKind' => 'lesson', + 'levelId' => $lessonLevel->getId(), 'startDate' => '2026-10-01', 'targetDate' => '2026-10-01', ])->assertCreated(); @@ -223,23 +232,23 @@ class ScheduleEndpointTest extends TestCase { $user = $this->createUser('reader@example.com'); $set = $this->createSet($user, 'Original set'); + $groupLevel = $this->createLevel($set, 'group'); + $taskLevel = $this->createLevel($set, 'task'); $repository = app(ElementRepository::class); $parent = $repository->create(new CreateElementDto( - set: $set, name: 'Original parent', - kind: 'group', + level: $groupLevel, parentElement: null, )); $element = $repository->create(new CreateElementDto( - set: $set, name: 'Original item', - kind: 'task', + level: $taskLevel, parentElement: $parent, )); $this->createSession($user, 'valid-token'); $createdResponse = $this->credentialedPost('/api/schedules', [ 'setId' => $set->getId(), - 'elementKind' => 'task', + 'levelId' => $taskLevel->getId(), 'startDate' => '2026-08-10', 'targetDate' => '2026-08-10', ])->assertCreated(); @@ -247,12 +256,14 @@ class ScheduleEndpointTest extends TestCase SetModel::query()->whereKey($set->getId())->update([ 'name' => 'Changed set', ]); + SetLevelModel::query()->whereKey($taskLevel->getId())->update([ + 'kind' => 'changed-kind', + ]); ElementModel::query()->whereKey($parent->getId())->update([ 'name' => 'Changed parent', ]); ElementModel::query()->whereKey($element->getId())->update([ 'name' => 'Changed item', - 'kind' => 'changed-kind', ]); $this->credentialedGet('/api/schedules/1') @@ -273,10 +284,10 @@ class ScheduleEndpointTest extends TestCase $owner = $this->createUser('owner@example.com'); $viewer = $this->createUser('viewer@example.com'); $set = $this->createSet($owner, 'Course'); + $lessonLevel = $this->createLevel($set, 'lesson'); app(ElementRepository::class)->create(new CreateElementDto( - set: $set, name: 'Welcome', - kind: 'lesson', + level: $lessonLevel, parentElement: null, )); $this->createSession($owner, 'owner-token'); @@ -288,7 +299,7 @@ class ScheduleEndpointTest extends TestCase 'owner-token', )->postJson('/api/schedules', [ 'setId' => $set->getId(), - 'elementKind' => 'lesson', + 'levelId' => $lessonLevel->getId(), 'startDate' => '2026-08-01', 'targetDate' => '2026-08-01', ])->assertCreated(); @@ -309,7 +320,7 @@ class ScheduleEndpointTest extends TestCase $this->credentialedPost('/api/schedules', [ 'setId' => 999, - 'elementKind' => 'chapter', + 'levelId' => 1, 'startDate' => '2026-08-12', 'targetDate' => '2026-08-10', ])->assertNotFound()->assertExactJson([ @@ -340,6 +351,14 @@ class ScheduleEndpointTest extends TestCase )); } + private function createLevel(Set $set, string $kind): SetLevel + { + return app(SetLevelRepository::class)->create(new CreateSetLevelDto( + set: $set, + kind: $kind, + )); + } + private function createSession(User $user, string $token): void { $createdAt = new DateTimeImmutable( diff --git a/backend/tests/Feature/Set/GetSetLayoutEndpointTest.php b/backend/tests/Feature/Set/GetSetLayoutEndpointTest.php index 3a71638..de3f898 100644 --- a/backend/tests/Feature/Set/GetSetLayoutEndpointTest.php +++ b/backend/tests/Feature/Set/GetSetLayoutEndpointTest.php @@ -8,7 +8,10 @@ use App\Element\CreateElementDto; use App\Element\ElementRepository; use App\Http\Middleware\AuthMiddleware; use App\Set\CreateSetDto; +use App\Set\CreateSetLevelDto; use App\Set\Set; +use App\Set\SetLevel; +use App\Set\SetLevelRepository; use App\Set\SetRepository; use App\Shared\ValueObject\EmailAddress; use App\User\CreateUserDto; @@ -29,28 +32,27 @@ class GetSetLayoutEndpointTest extends TestCase $user = $this->createUser(); $set = $this->createSet($user, 'Bible'); $repository = app(ElementRepository::class); + $bookLevel = $this->createLevel($set, 'book'); + $portionLevel = $this->createLevel($set, 'portion'); + $chapterLevel = $this->createLevel($set, 'chapter'); $genesis = $repository->create(new CreateElementDto( - set: $set, name: 'Genesis', - kind: 'book', + level: $bookLevel, parentElement: null, )); $repository->create(new CreateElementDto( - set: $set, name: 'Exodus', - kind: 'book', + level: $bookLevel, parentElement: null, )); $creation = $repository->create(new CreateElementDto( - set: $set, name: 'Creation', - kind: 'portion', + level: $portionLevel, parentElement: $genesis, )); $chapter = $repository->create(new CreateElementDto( - set: $set, name: 'Chapter 1', - kind: 'chapter', + level: $chapterLevel, parentElement: $creation, )); $this->createSession($user); @@ -62,21 +64,44 @@ class GetSetLayoutEndpointTest extends TestCase 'id' => $set->getId(), 'name' => 'Bible', ], + 'levels' => [ + [ + 'id' => $bookLevel->getId(), + 'kind' => 'book', + 'depth' => 0, + 'elementCount' => 2, + ], + [ + 'id' => $portionLevel->getId(), + 'kind' => 'portion', + 'depth' => 1, + 'elementCount' => 1, + ], + [ + 'id' => $chapterLevel->getId(), + 'kind' => 'chapter', + 'depth' => 2, + 'elementCount' => 1, + ], + ], 'elements' => [ [ 'id' => $genesis->getId(), 'name' => 'Genesis', 'kind' => 'book', + 'levelId' => $bookLevel->getId(), 'children' => [ [ 'id' => $creation->getId(), 'name' => 'Creation', 'kind' => 'portion', + 'levelId' => $portionLevel->getId(), 'children' => [ [ 'id' => $chapter->getId(), 'name' => 'Chapter 1', 'kind' => 'chapter', + 'levelId' => $chapterLevel->getId(), 'children' => [], ], ], @@ -87,6 +112,7 @@ class GetSetLayoutEndpointTest extends TestCase 'id' => $genesis->getId() + 1, 'name' => 'Exodus', 'kind' => 'book', + 'levelId' => $bookLevel->getId(), 'children' => [], ], ], @@ -97,6 +123,7 @@ class GetSetLayoutEndpointTest extends TestCase { $user = $this->createUser(); $set = $this->createSet($user, 'Empty set'); + $taskLevel = $this->createLevel($set, 'task'); $this->createSession($user); $response = $this->credentialedGet("/api/sets/{$set->getId()}"); @@ -106,6 +133,12 @@ class GetSetLayoutEndpointTest extends TestCase 'id' => $set->getId(), 'name' => 'Empty set', ], + 'levels' => [[ + 'id' => $taskLevel->getId(), + 'kind' => 'task', + 'depth' => 0, + 'elementCount' => 0, + ]], 'elements' => [], ]); } @@ -147,6 +180,14 @@ class GetSetLayoutEndpointTest extends TestCase )); } + private function createLevel(Set $set, string $kind): SetLevel + { + return app(SetLevelRepository::class)->create(new CreateSetLevelDto( + set: $set, + kind: $kind, + )); + } + private function createSession(User $user): void { $createdAt = new DateTimeImmutable( diff --git a/backend/tests/Unit/Schedule/UseCases/CreateScheduleTest.php b/backend/tests/Unit/Schedule/UseCases/CreateScheduleTest.php index 3a4e715..02b7a83 100644 --- a/backend/tests/Unit/Schedule/UseCases/CreateScheduleTest.php +++ b/backend/tests/Unit/Schedule/UseCases/CreateScheduleTest.php @@ -8,92 +8,110 @@ use App\Exceptions\NotFoundException; use App\Schedule\UseCases\CreateSchedule\CreateSchedule; use App\Schedule\UseCases\CreateSchedule\CreateScheduleRequest; use App\Set\CreateSetDto; +use App\Set\CreateSetLevelDto; +use App\Set\Set; +use App\Set\SetLevel; use App\Shared\ValueObject\EmailAddress; use App\User\User; use PHPUnit\Framework\TestCase; use Tests\Fakes\FakeElementRepository; use Tests\Fakes\FakeScheduleRepository; use Tests\Fakes\FakeSetRepository; +use Tests\Fakes\FakeSetLevelRepository; class CreateScheduleTest extends TestCase { - public function test_it_schedules_every_matching_element_in_tree_order(): void + public function test_it_schedules_every_element_on_the_selected_level(): void { $user = $this->user(); $setRepository = new FakeSetRepository; + $setLevelRepository = new FakeSetLevelRepository; $elementRepository = new FakeElementRepository; $scheduleRepository = new FakeScheduleRepository; $set = $setRepository->create(new CreateSetDto( name: 'Bible', creator: $user, )); + $bookLevel = $this->createLevel( + $setLevelRepository, + $set, + 'book', + ); + $portionLevel = $this->createLevel( + $setLevelRepository, + $set, + 'portion', + ); + $chapterLevel = $this->createLevel( + $setLevelRepository, + $set, + 'chapter', + ); $genesis = $elementRepository->create(new CreateElementDto( - set: $set, name: 'Genesis', - kind: 'book', + level: $bookLevel, parentElement: null, )); $exodus = $elementRepository->create(new CreateElementDto( - set: $set, name: 'Exodus', - kind: 'book', + level: $bookLevel, parentElement: null, )); - $exodusChapterOne = $elementRepository->create( - new CreateElementDto( - set: $set, - name: 'Exodus 1', - kind: 'chapter', - parentElement: $exodus, - ), - ); $creation = $elementRepository->create(new CreateElementDto( - set: $set, name: 'Creation', - kind: 'portion', + level: $portionLevel, parentElement: $genesis, )); + $shemot = $elementRepository->create(new CreateElementDto( + name: 'Shemot', + level: $portionLevel, + parentElement: $exodus, + )); $genesisChapterOne = $elementRepository->create( new CreateElementDto( - set: $set, name: 'Genesis 1', - kind: 'chapter', + level: $chapterLevel, parentElement: $creation, ), ); $genesisChapterTwo = $elementRepository->create( new CreateElementDto( - set: $set, name: 'Genesis 2', - kind: 'chapter', + level: $chapterLevel, parentElement: $creation, ), ); $genesisChapterThree = $elementRepository->create( new CreateElementDto( - set: $set, name: 'Genesis 3', - kind: 'chapter', + level: $chapterLevel, parentElement: $creation, ), ); + $exodusChapterOne = $elementRepository->create( + new CreateElementDto( + name: 'Exodus 1', + level: $chapterLevel, + parentElement: $shemot, + ), + ); $exodusChapterTwo = $elementRepository->create( new CreateElementDto( - set: $set, name: 'Exodus 2', - kind: 'chapter', - parentElement: $exodus, + level: $chapterLevel, + parentElement: $shemot, ), ); $schedule = (new CreateSchedule( $setRepository, + $setLevelRepository, $elementRepository, $scheduleRepository, ))->execute(new CreateScheduleRequest( user: $user, setId: $set->getId(), - elementKind: 'chapter', + levelId: $chapterLevel->getId(), startDate: '2026-08-10', targetDate: '2026-08-12', )); @@ -139,29 +157,35 @@ class CreateScheduleTest extends TestCase { $user = $this->user(); $setRepository = new FakeSetRepository; + $setLevelRepository = new FakeSetLevelRepository; $elementRepository = new FakeElementRepository; $set = $setRepository->create(new CreateSetDto( name: 'Course', creator: $user, )); + $lessonLevel = $this->createLevel( + $setLevelRepository, + $set, + 'lesson', + ); foreach (['First', 'Second', 'Third'] as $name) { $elementRepository->create(new CreateElementDto( - set: $set, name: $name, - kind: 'lesson', + level: $lessonLevel, parentElement: null, )); } $schedule = (new CreateSchedule( $setRepository, + $setLevelRepository, $elementRepository, new FakeScheduleRepository, ))->execute(new CreateScheduleRequest( user: $user, setId: $set->getId(), - elementKind: 'lesson', + levelId: $lessonLevel->getId(), startDate: '2026-08-10', targetDate: '2026-08-16', )); @@ -178,26 +202,32 @@ class CreateScheduleTest extends TestCase { $user = $this->user(); $setRepository = new FakeSetRepository; + $setLevelRepository = new FakeSetLevelRepository; $elementRepository = new FakeElementRepository; $set = $setRepository->create(new CreateSetDto( name: 'Project', creator: $user, )); + $milestoneLevel = $this->createLevel( + $setLevelRepository, + $set, + 'milestone', + ); $elementRepository->create(new CreateElementDto( - set: $set, name: 'Ship it', - kind: 'milestone', + level: $milestoneLevel, parentElement: null, )); $schedule = (new CreateSchedule( $setRepository, + $setLevelRepository, $elementRepository, new FakeScheduleRepository, ))->execute(new CreateScheduleRequest( user: $user, setId: $set->getId(), - elementKind: 'milestone', + levelId: $milestoneLevel->getId(), startDate: '2020-01-01', targetDate: '2030-01-01', )); @@ -217,18 +247,19 @@ class CreateScheduleTest extends TestCase (new CreateSchedule( new FakeSetRepository, + new FakeSetLevelRepository, new FakeElementRepository, new FakeScheduleRepository, ))->execute(new CreateScheduleRequest( user: $this->user(), setId: 999, - elementKind: 'chapter', + levelId: 1, startDate: '2026-08-10', targetDate: '2026-08-12', )); } - public function test_it_rejects_an_unavailable_element_kind(): void + public function test_it_rejects_a_missing_level(): void { $user = $this->user(); $setRepository = new FakeSetRepository; @@ -238,18 +269,85 @@ class CreateScheduleTest extends TestCase )); $this->expectException(BadRequestException::class); - $this->expectExceptionMessage( - 'elementKind is not available for set', - ); + $this->expectExceptionMessage('levelId is required'); (new CreateSchedule( $setRepository, + new FakeSetLevelRepository, new FakeElementRepository, new FakeScheduleRepository, ))->execute(new CreateScheduleRequest( user: $user, setId: $set->getId(), - elementKind: 'Chapter', + levelId: null, + startDate: '2026-08-10', + targetDate: '2026-08-12', + )); + } + + public function test_it_rejects_a_level_from_another_set(): void + { + $user = $this->user(); + $setRepository = new FakeSetRepository; + $setLevelRepository = new FakeSetLevelRepository; + $bible = $setRepository->create(new CreateSetDto( + name: 'Bible', + creator: $user, + )); + $course = $setRepository->create(new CreateSetDto( + name: 'Course', + creator: $user, + )); + $moduleLevel = $this->createLevel( + $setLevelRepository, + $course, + 'module', + ); + + $this->expectException(BadRequestException::class); + $this->expectExceptionMessage('level is not available for set'); + + (new CreateSchedule( + $setRepository, + $setLevelRepository, + new FakeElementRepository, + new FakeScheduleRepository, + ))->execute(new CreateScheduleRequest( + user: $user, + setId: $bible->getId(), + levelId: $moduleLevel->getId(), + startDate: '2026-08-10', + targetDate: '2026-08-12', + )); + } + + public function test_it_rejects_a_level_without_elements(): void + { + $user = $this->user(); + $setRepository = new FakeSetRepository; + $setLevelRepository = new FakeSetLevelRepository; + $set = $setRepository->create(new CreateSetDto( + name: 'Bible', + creator: $user, + )); + $chapterLevel = $this->createLevel( + $setLevelRepository, + $set, + 'chapter', + ); + + $this->expectException(BadRequestException::class); + $this->expectExceptionMessage('level has no elements'); + + (new CreateSchedule( + $setRepository, + $setLevelRepository, + new FakeElementRepository, + new FakeScheduleRepository, + ))->execute(new CreateScheduleRequest( + user: $user, + setId: $set->getId(), + levelId: $chapterLevel->getId(), startDate: '2026-08-10', targetDate: '2026-08-12', )); @@ -259,22 +357,29 @@ class CreateScheduleTest extends TestCase { $user = $this->user(); $setRepository = new FakeSetRepository; + $setLevelRepository = new FakeSetLevelRepository; $set = $setRepository->create(new CreateSetDto( name: 'Bible', creator: $user, )); + $chapterLevel = $this->createLevel( + $setLevelRepository, + $set, + 'chapter', + ); $this->expectException(BadRequestException::class); $this->expectExceptionMessage('startDate must be a valid date'); (new CreateSchedule( $setRepository, + $setLevelRepository, new FakeElementRepository, new FakeScheduleRepository, ))->execute(new CreateScheduleRequest( user: $user, setId: $set->getId(), - elementKind: 'chapter', + levelId: $chapterLevel->getId(), startDate: '2026-02-30', targetDate: '2026-08-12', )); @@ -284,10 +389,16 @@ class CreateScheduleTest extends TestCase { $user = $this->user(); $setRepository = new FakeSetRepository; + $setLevelRepository = new FakeSetLevelRepository; $set = $setRepository->create(new CreateSetDto( name: 'Bible', creator: $user, )); + $chapterLevel = $this->createLevel( + $setLevelRepository, + $set, + 'chapter', + ); $this->expectException(BadRequestException::class); $this->expectExceptionMessage( @@ -296,17 +407,29 @@ class CreateScheduleTest extends TestCase (new CreateSchedule( $setRepository, + $setLevelRepository, new FakeElementRepository, new FakeScheduleRepository, ))->execute(new CreateScheduleRequest( user: $user, setId: $set->getId(), - elementKind: 'chapter', + levelId: $chapterLevel->getId(), startDate: '2026-08-12', targetDate: '2026-08-10', )); } + private function createLevel( + FakeSetLevelRepository $repository, + Set $set, + string $kind, + ): SetLevel { + return $repository->create(new CreateSetLevelDto( + set: $set, + kind: $kind, + )); + } + private function user(): User { return new User( diff --git a/backend/tests/Unit/Set/UseCases/GetSetLayoutTest.php b/backend/tests/Unit/Set/UseCases/GetSetLayoutTest.php index 69a7714..2b4a7f1 100644 --- a/backend/tests/Unit/Set/UseCases/GetSetLayoutTest.php +++ b/backend/tests/Unit/Set/UseCases/GetSetLayoutTest.php @@ -5,6 +5,7 @@ namespace Tests\Unit\Set\UseCases; use App\Element\CreateElementDto; use App\Exceptions\NotFoundException; use App\Set\CreateSetDto; +use App\Set\CreateSetLevelDto; use App\Set\UseCases\GetSetLayout\ElementLayoutNode; use App\Set\UseCases\GetSetLayout\GetSetLayout; use App\Set\UseCases\GetSetLayout\GetSetLayoutRequest; @@ -13,6 +14,7 @@ use App\User\User; use PHPUnit\Framework\TestCase; use Tests\Fakes\FakeElementRepository; use Tests\Fakes\FakeSetRepository; +use Tests\Fakes\FakeSetLevelRepository; class GetSetLayoutTest extends TestCase { @@ -24,6 +26,7 @@ class GetSetLayoutTest extends TestCase passwordHash: 'hashed-password', ); $setRepository = new FakeSetRepository; + $setLevelRepository = new FakeSetLevelRepository; $elementRepository = new FakeElementRepository; $bible = $setRepository->create(new CreateSetDto( name: 'Bible', @@ -33,51 +36,69 @@ class GetSetLayoutTest extends TestCase name: 'Course', creator: $creator, )); - $genesis = $elementRepository->create(new CreateElementDto( + $bookLevel = $setLevelRepository->create(new CreateSetLevelDto( set: $bible, - name: 'Genesis', kind: 'book', + )); + $portionLevel = $setLevelRepository->create(new CreateSetLevelDto( + set: $bible, + kind: 'portion', + )); + $chapterLevel = $setLevelRepository->create(new CreateSetLevelDto( + set: $bible, + kind: 'chapter', + )); + $moduleLevel = $setLevelRepository->create(new CreateSetLevelDto( + set: $course, + kind: 'module', + )); + $genesis = $elementRepository->create(new CreateElementDto( + name: 'Genesis', + level: $bookLevel, parentElement: null, )); $elementRepository->create(new CreateElementDto( - set: $bible, name: 'Exodus', - kind: 'book', + level: $bookLevel, parentElement: null, )); $creation = $elementRepository->create(new CreateElementDto( - set: $bible, name: 'Creation', - kind: 'portion', + level: $portionLevel, parentElement: $genesis, )); $elementRepository->create(new CreateElementDto( - set: $bible, name: 'Noah', - kind: 'portion', + level: $portionLevel, parentElement: $genesis, )); $elementRepository->create(new CreateElementDto( - set: $bible, name: 'Chapter 1', - kind: 'chapter', + level: $chapterLevel, parentElement: $creation, )); $elementRepository->create(new CreateElementDto( - set: $course, name: 'Foundations', - kind: 'module', + level: $moduleLevel, parentElement: null, )); $layout = (new GetSetLayout( $setRepository, + $setLevelRepository, $elementRepository, ))->execute(new GetSetLayoutRequest( setId: $bible->getId(), )); $this->assertSame('Bible', $layout->getSet()->getName()); + $this->assertSame( + ['book:2', 'portion:2', 'chapter:1'], + array_map(function ($level): string { + return $level->getLevel()->getKind() + . ":{$level->getElementCount()}"; + }, $layout->getLevels()), + ); $this->assertSame( ['Genesis', 'Exodus'], $this->nodeNames($layout->getElements()), @@ -103,19 +124,26 @@ class GetSetLayoutTest extends TestCase passwordHash: 'hashed-password', ); $setRepository = new FakeSetRepository; + $setLevelRepository = new FakeSetLevelRepository; $set = $setRepository->create(new CreateSetDto( name: 'Empty set', creator: $creator, )); + $setLevelRepository->create(new CreateSetLevelDto( + set: $set, + kind: 'task', + )); $layout = (new GetSetLayout( $setRepository, + $setLevelRepository, new FakeElementRepository, ))->execute(new GetSetLayoutRequest( setId: $set->getId(), )); $this->assertSame([], $layout->getElements()); + $this->assertSame(0, $layout->getLevels()[0]->getElementCount()); } public function test_it_rejects_an_unknown_set(): void @@ -125,6 +153,7 @@ class GetSetLayoutTest extends TestCase (new GetSetLayout( new FakeSetRepository, + new FakeSetLevelRepository, new FakeElementRepository, ))->execute(new GetSetLayoutRequest( setId: 999,