Les listes admin (utilisateurs, classes, matières, affectations) chargeaient toutes les données d'un coup, ce qui dégradait l'expérience avec un volume croissant. La pagination côté serveur existait dans la config API Platform mais aucun Provider ne l'exploitait. Cette implémentation ajoute la pagination serveur (30 items/page, max 100) avec recherche textuelle sur toutes les sections, des composants frontend réutilisables (Pagination + SearchInput avec debounce), et la synchronisation URL pour le partage de liens filtrés. Les Query valident leurs paramètres (clamp page/limit, trim search) pour éviter les abus. Les affectations utilisent des lookup maps pour résoudre les noms sans N+1 queries. Les pages admin gèrent les race conditions via AbortController.
210 lines
5.9 KiB
PHP
210 lines
5.9 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Tests\Unit\Administration\Application\Query\GetSubjects;
|
|
|
|
use App\Administration\Application\Query\GetSubjects\GetSubjectsHandler;
|
|
use App\Administration\Application\Query\GetSubjects\GetSubjectsQuery;
|
|
use App\Administration\Domain\Model\SchoolClass\SchoolId;
|
|
use App\Administration\Domain\Model\Subject\Subject;
|
|
use App\Administration\Domain\Model\Subject\SubjectCode;
|
|
use App\Administration\Domain\Model\Subject\SubjectColor;
|
|
use App\Administration\Domain\Model\Subject\SubjectName;
|
|
use App\Administration\Infrastructure\Persistence\InMemory\InMemorySubjectRepository;
|
|
use App\Shared\Domain\Tenant\TenantId;
|
|
use DateTimeImmutable;
|
|
use PHPUnit\Framework\Attributes\Test;
|
|
use PHPUnit\Framework\TestCase;
|
|
|
|
final class GetSubjectsHandlerTest extends TestCase
|
|
{
|
|
private const string TENANT_ID = '550e8400-e29b-41d4-a716-446655440002';
|
|
private const string SCHOOL_ID = '550e8400-e29b-41d4-a716-446655440020';
|
|
|
|
private InMemorySubjectRepository $subjectRepository;
|
|
private GetSubjectsHandler $handler;
|
|
|
|
protected function setUp(): void
|
|
{
|
|
$this->subjectRepository = new InMemorySubjectRepository();
|
|
$this->handler = new GetSubjectsHandler($this->subjectRepository);
|
|
}
|
|
|
|
#[Test]
|
|
public function returnsAllActiveSubjectsForTenantAndSchool(): void
|
|
{
|
|
$this->seedSubjects();
|
|
|
|
$query = new GetSubjectsQuery(
|
|
tenantId: self::TENANT_ID,
|
|
schoolId: self::SCHOOL_ID,
|
|
);
|
|
$result = ($this->handler)($query);
|
|
|
|
self::assertCount(3, $result->items);
|
|
self::assertSame(3, $result->total);
|
|
self::assertSame(1, $result->page);
|
|
self::assertSame(30, $result->limit);
|
|
}
|
|
|
|
#[Test]
|
|
public function filtersSubjectsByName(): void
|
|
{
|
|
$this->seedSubjects();
|
|
|
|
$query = new GetSubjectsQuery(
|
|
tenantId: self::TENANT_ID,
|
|
schoolId: self::SCHOOL_ID,
|
|
search: 'Mathématiques',
|
|
);
|
|
$result = ($this->handler)($query);
|
|
|
|
self::assertCount(1, $result->items);
|
|
self::assertSame('Mathématiques', $result->items[0]->name);
|
|
}
|
|
|
|
#[Test]
|
|
public function filtersSubjectsByCode(): void
|
|
{
|
|
$this->seedSubjects();
|
|
|
|
$query = new GetSubjectsQuery(
|
|
tenantId: self::TENANT_ID,
|
|
schoolId: self::SCHOOL_ID,
|
|
search: 'FR',
|
|
);
|
|
$result = ($this->handler)($query);
|
|
|
|
self::assertCount(1, $result->items);
|
|
self::assertSame('FR', $result->items[0]->code);
|
|
}
|
|
|
|
#[Test]
|
|
public function searchIsCaseInsensitive(): void
|
|
{
|
|
$this->seedSubjects();
|
|
|
|
$query = new GetSubjectsQuery(
|
|
tenantId: self::TENANT_ID,
|
|
schoolId: self::SCHOOL_ID,
|
|
search: 'math',
|
|
);
|
|
$result = ($this->handler)($query);
|
|
|
|
self::assertCount(1, $result->items);
|
|
}
|
|
|
|
#[Test]
|
|
public function paginatesResults(): void
|
|
{
|
|
$this->seedSubjects();
|
|
|
|
$query = new GetSubjectsQuery(
|
|
tenantId: self::TENANT_ID,
|
|
schoolId: self::SCHOOL_ID,
|
|
page: 1,
|
|
limit: 2,
|
|
);
|
|
$result = ($this->handler)($query);
|
|
|
|
self::assertCount(2, $result->items);
|
|
self::assertSame(3, $result->total);
|
|
self::assertSame(1, $result->page);
|
|
self::assertSame(2, $result->limit);
|
|
self::assertSame(2, $result->totalPages());
|
|
}
|
|
|
|
#[Test]
|
|
public function returnsSecondPage(): void
|
|
{
|
|
$this->seedSubjects();
|
|
|
|
$query = new GetSubjectsQuery(
|
|
tenantId: self::TENANT_ID,
|
|
schoolId: self::SCHOOL_ID,
|
|
page: 2,
|
|
limit: 2,
|
|
);
|
|
$result = ($this->handler)($query);
|
|
|
|
self::assertCount(1, $result->items);
|
|
self::assertSame(3, $result->total);
|
|
self::assertSame(2, $result->page);
|
|
}
|
|
|
|
#[Test]
|
|
public function returnsEmptyWhenNoMatches(): void
|
|
{
|
|
$this->seedSubjects();
|
|
|
|
$query = new GetSubjectsQuery(
|
|
tenantId: self::TENANT_ID,
|
|
schoolId: self::SCHOOL_ID,
|
|
search: 'nonexistent',
|
|
);
|
|
$result = ($this->handler)($query);
|
|
|
|
self::assertCount(0, $result->items);
|
|
self::assertSame(0, $result->total);
|
|
}
|
|
|
|
#[Test]
|
|
public function clampsInvalidPageToOne(): void
|
|
{
|
|
$query = new GetSubjectsQuery(
|
|
tenantId: self::TENANT_ID,
|
|
schoolId: self::SCHOOL_ID,
|
|
page: -1,
|
|
);
|
|
|
|
self::assertSame(1, $query->page);
|
|
}
|
|
|
|
#[Test]
|
|
public function clampsExcessiveLimitToMax(): void
|
|
{
|
|
$query = new GetSubjectsQuery(
|
|
tenantId: self::TENANT_ID,
|
|
schoolId: self::SCHOOL_ID,
|
|
limit: 999,
|
|
);
|
|
|
|
self::assertSame(100, $query->limit);
|
|
}
|
|
|
|
private function seedSubjects(): void
|
|
{
|
|
$tenantId = TenantId::fromString(self::TENANT_ID);
|
|
$schoolId = SchoolId::fromString(self::SCHOOL_ID);
|
|
$now = new DateTimeImmutable('2026-02-01 10:00:00');
|
|
|
|
$this->subjectRepository->save(Subject::creer(
|
|
tenantId: $tenantId,
|
|
schoolId: $schoolId,
|
|
name: new SubjectName('Mathématiques'),
|
|
code: new SubjectCode('MATH'),
|
|
color: new SubjectColor('#3B82F6'),
|
|
createdAt: $now,
|
|
));
|
|
|
|
$this->subjectRepository->save(Subject::creer(
|
|
tenantId: $tenantId,
|
|
schoolId: $schoolId,
|
|
name: new SubjectName('Français'),
|
|
code: new SubjectCode('FR'),
|
|
color: new SubjectColor('#EF4444'),
|
|
createdAt: $now,
|
|
));
|
|
|
|
$this->subjectRepository->save(Subject::creer(
|
|
tenantId: $tenantId,
|
|
schoolId: $schoolId,
|
|
name: new SubjectName('Histoire-Géo'),
|
|
code: new SubjectCode('HG'),
|
|
color: new SubjectColor('#10B981'),
|
|
createdAt: $now,
|
|
));
|
|
}
|
|
}
|