test set layout browsing

This commit is contained in:
Yisroel Baum 2026-08-08 23:30:11 +03:00
parent 936855efab
commit eb7ac7d261
Signed by: yisroelbaum
GPG key ID: 0FA60884F75520A9
8 changed files with 619 additions and 3 deletions

View file

@ -3,6 +3,8 @@
namespace Tests\Feature\Database;
use App\Auth\PasswordHasher;
use App\Element\Element;
use App\Element\ElementRepository;
use App\Set\SetRepository;
use App\Shared\ValueObject\EmailAddress;
use App\User\UserRepository;
@ -60,4 +62,60 @@ class DatabaseSeederTest extends TestCase
);
}
}
public function test_it_seeds_elements_for_every_set_idempotently(): void
{
$this->seed();
$this->seed();
$sets = app(SetRepository::class)->all();
$elementRepository = app(ElementRepository::class);
$expectedElements = [
'Bible' => [
'Chapter 1:chapter:Creation',
'Chapter 2:chapter:Creation',
'Creation:portion:Genesis',
'Exodus:book:root',
'Genesis:book:root',
'Noah:portion:Genesis',
'Shemot:portion:Exodus',
],
'Course' => [
'Applied Practice:module:root',
'Core Concepts:lesson:Foundations',
'Final Review:lesson:Applied Practice',
'Foundations:module:root',
'Guided Exercise:lesson:Applied Practice',
'Welcome:lesson:Foundations',
],
'Fitness Program' => [
'Build Phase:phase:root',
'Foundation Phase:phase:root',
'Full Body Circuit:workout:Build Phase',
'Hip Flow:exercise:Mobility Day',
'Mobility Day:workout:Foundation Phase',
'Push-up:exercise:Strength Day',
'Squat:exercise:Strength Day',
'Strength Day:workout:Foundation Phase',
],
];
$this->assertDatabaseCount('elements', 21);
foreach ($sets as $set) {
$signatures = array_map(function (Element $element): string {
$parentName = $element->getParentElement()?->getName()
?? 'root';
return "{$element->getName()}:{$element->getKind()}"
. ":{$parentName}";
}, $elementRepository->findBySet($set));
sort($signatures);
$this->assertSame(
$expectedElements[$set->getName()],
$signatures,
);
}
}
}

View file

@ -156,6 +156,45 @@ class EloquentElementRepositoryTest extends TestCase
);
}
public function testItListsEveryElementForASet(): void
{
$bible = $this->createSet('Bible');
$course = $this->createSet('Course');
$repository = app(ElementRepository::class);
$genesis = $repository->create(new CreateElementDto(
set: $bible,
name: 'Genesis',
kind: 'book',
parentElement: null,
));
$repository->create(new CreateElementDto(
set: $bible,
name: 'Creation',
kind: 'portion',
parentElement: $genesis,
));
$repository->create(new CreateElementDto(
set: $course,
name: 'Foundations',
kind: 'module',
parentElement: null,
));
$elements = $repository->findBySet($bible);
$this->assertSame(
['Genesis', 'Creation'],
array_map(function ($element): string {
return $element->getName();
}, $elements),
);
$this->assertNull($elements[0]->getParentElement());
$this->assertSame(
$genesis->getId(),
$elements[1]->getParentElement()?->getId(),
);
}
private function createSet(string $name): Set
{
$creator = app(UserRepository::class)->create(new CreateUserDto(

View file

@ -66,6 +66,23 @@ class EloquentSetRepositoryTest extends TestCase
);
}
public function test_it_finds_a_set_by_id(): void
{
$creator = $this->createUser('creator@example.com');
$repository = app(SetRepository::class);
$createdSet = $repository->create(new CreateSetDto(
name: 'Bible',
creator: $creator,
));
$foundSet = $repository->find($createdSet->getId());
$this->assertNotNull($foundSet);
$this->assertSame($createdSet->getId(), $foundSet->getId());
$this->assertSame('Bible', $foundSet->getName());
$this->assertNull($repository->find(999));
}
public function test_it_rejects_duplicate_set_names(): void
{
$creator = $this->createUser('creator@example.com');

View file

@ -0,0 +1,171 @@
<?php
namespace Tests\Feature\Set;
use App\Auth\CreateSessionDto;
use App\Auth\SessionRepository;
use App\Element\CreateElementDto;
use App\Element\ElementRepository;
use App\Http\Middleware\AuthMiddleware;
use App\Set\CreateSetDto;
use App\Set\Set;
use App\Set\SetRepository;
use App\Shared\ValueObject\EmailAddress;
use App\User\CreateUserDto;
use App\User\User;
use App\User\UserRepository;
use DateTimeImmutable;
use DateTimeZone;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class GetSetLayoutEndpointTest extends TestCase
{
use RefreshDatabase;
public function test_it_returns_a_sets_ordered_element_layout(): void
{
$user = $this->createUser();
$set = $this->createSet($user, 'Bible');
$repository = app(ElementRepository::class);
$genesis = $repository->create(new CreateElementDto(
set: $set,
name: 'Genesis',
kind: 'book',
parentElement: null,
));
$repository->create(new CreateElementDto(
set: $set,
name: 'Exodus',
kind: 'book',
parentElement: null,
));
$creation = $repository->create(new CreateElementDto(
set: $set,
name: 'Creation',
kind: 'portion',
parentElement: $genesis,
));
$chapter = $repository->create(new CreateElementDto(
set: $set,
name: 'Chapter 1',
kind: 'chapter',
parentElement: $creation,
));
$this->createSession($user);
$response = $this->credentialedGet("/api/sets/{$set->getId()}");
$response->assertOk()->assertExactJson([
'set' => [
'id' => $set->getId(),
'name' => 'Bible',
],
'elements' => [
[
'id' => $genesis->getId(),
'name' => 'Genesis',
'kind' => 'book',
'children' => [
[
'id' => $creation->getId(),
'name' => 'Creation',
'kind' => 'portion',
'children' => [
[
'id' => $chapter->getId(),
'name' => 'Chapter 1',
'kind' => 'chapter',
'children' => [],
],
],
],
],
],
[
'id' => $genesis->getId() + 1,
'name' => 'Exodus',
'kind' => 'book',
'children' => [],
],
],
]);
}
public function test_it_returns_an_empty_element_layout(): void
{
$user = $this->createUser();
$set = $this->createSet($user, 'Empty set');
$this->createSession($user);
$response = $this->credentialedGet("/api/sets/{$set->getId()}");
$response->assertOk()->assertExactJson([
'set' => [
'id' => $set->getId(),
'name' => 'Empty set',
],
'elements' => [],
]);
}
public function test_it_returns_not_found_for_an_unknown_set(): void
{
$user = $this->createUser();
$this->createSession($user);
$response = $this->credentialedGet('/api/sets/999');
$response->assertNotFound()->assertExactJson([
'error' => 'set not found',
]);
}
public function test_it_rejects_an_unauthenticated_request(): void
{
$response = $this->getJson('/api/sets/1');
$response->assertStatus(401)->assertExactJson([
'error' => 'unauthenticated',
]);
}
private function createUser(): User
{
return app(UserRepository::class)->create(new CreateUserDto(
email: new EmailAddress('reader@example.com'),
passwordHash: 'hashed-password',
));
}
private function createSet(User $user, string $name): Set
{
return app(SetRepository::class)->create(new CreateSetDto(
name: $name,
creator: $user,
));
}
private function createSession(User $user): void
{
$createdAt = new DateTimeImmutable(
'2026-08-03T12:00:00',
new DateTimeZone('UTC'),
);
app(SessionRepository::class)->create(new CreateSessionDto(
token: 'valid-token',
user: $user,
createdAt: $createdAt,
expiresAt: $createdAt->modify('+7 days'),
));
}
private function credentialedGet(string $uri): \Illuminate\Testing\TestResponse
{
return $this->withCredentials()
->withUnencryptedCookie(
AuthMiddleware::COOKIE_NAME,
'valid-token',
)->getJson($uri);
}
}

View file

@ -0,0 +1,136 @@
<?php
namespace Tests\Unit\Set\UseCases;
use App\Element\CreateElementDto;
use App\Exceptions\NotFoundException;
use App\Set\CreateSetDto;
use App\Set\UseCases\GetSetLayout\GetSetLayout;
use App\Shared\ValueObject\EmailAddress;
use App\User\User;
use PHPUnit\Framework\TestCase;
use Tests\Fakes\FakeElementRepository;
use Tests\Fakes\FakeSetRepository;
class GetSetLayoutTest extends TestCase
{
public function test_it_builds_an_ordered_element_tree(): void
{
$creator = new User(
id: 7,
email: new EmailAddress('creator@example.com'),
passwordHash: 'hashed-password',
);
$setRepository = new FakeSetRepository;
$elementRepository = new FakeElementRepository;
$bible = $setRepository->create(new CreateSetDto(
name: 'Bible',
creator: $creator,
));
$course = $setRepository->create(new CreateSetDto(
name: 'Course',
creator: $creator,
));
$genesis = $elementRepository->create(new CreateElementDto(
set: $bible,
name: 'Genesis',
kind: 'book',
parentElement: null,
));
$elementRepository->create(new CreateElementDto(
set: $bible,
name: 'Exodus',
kind: 'book',
parentElement: null,
));
$creation = $elementRepository->create(new CreateElementDto(
set: $bible,
name: 'Creation',
kind: 'portion',
parentElement: $genesis,
));
$elementRepository->create(new CreateElementDto(
set: $bible,
name: 'Noah',
kind: 'portion',
parentElement: $genesis,
));
$elementRepository->create(new CreateElementDto(
set: $bible,
name: 'Chapter 1',
kind: 'chapter',
parentElement: $creation,
));
$elementRepository->create(new CreateElementDto(
set: $course,
name: 'Foundations',
kind: 'module',
parentElement: null,
));
$layout = (new GetSetLayout(
$setRepository,
$elementRepository,
))->execute($bible->getId());
$this->assertSame('Bible', $layout->getSet()->getName());
$this->assertSame(
['Genesis', 'Exodus'],
$this->nodeNames($layout->getElements()),
);
$genesisNode = $layout->getElements()[0];
$this->assertSame(
['Creation', 'Noah'],
$this->nodeNames($genesisNode->getChildren()),
);
$this->assertSame(
['Chapter 1'],
$this->nodeNames(
$genesisNode->getChildren()[0]->getChildren(),
),
);
}
public function test_it_returns_an_empty_layout(): void
{
$creator = new User(
id: 7,
email: new EmailAddress('creator@example.com'),
passwordHash: 'hashed-password',
);
$setRepository = new FakeSetRepository;
$set = $setRepository->create(new CreateSetDto(
name: 'Empty set',
creator: $creator,
));
$layout = (new GetSetLayout(
$setRepository,
new FakeElementRepository,
))->execute($set->getId());
$this->assertSame([], $layout->getElements());
}
public function test_it_rejects_an_unknown_set(): void
{
$this->expectException(NotFoundException::class);
$this->expectExceptionMessage('set not found');
(new GetSetLayout(
new FakeSetRepository,
new FakeElementRepository,
))->execute(999);
}
/**
* @param list<\App\Set\UseCases\GetSetLayout\ElementLayoutNode> $nodes
* @return list<string>
*/
private function nodeNames(array $nodes): array
{
return array_map(function ($node): string {
return $node->getElement()->getName();
}, $nodes);
}
}