Files
Classeo/backend/tests/Unit/Scolarite/Application/Query/GetActiveReplacements/GetActiveReplacementsHandlerTest.php
Mathias STRASSER c856dfdcda feat: Désignation de remplaçants temporaires avec corrections sécurité
Permet aux administrateurs de désigner un enseignant remplaçant pour
un autre enseignant absent, sur des classes et matières précises, pour
une période donnée. Le dashboard enseignant affiche les remplacements
actifs avec les noms de classes/matières au lieu des identifiants bruts.

Inclut les corrections de la code review :
- Requête findActiveByTenant qui excluait les remplacements en cours
  mais incluait les futurs (manquait start_date <= :at)
- Validation tenant et rôle enseignant dans le handler de désignation
  pour empêcher l'affectation cross-tenant ou de non-enseignants
- Validation structurée du payload classes (Assert\Collection + UUID)
  pour éviter les erreurs serveur sur payloads malformés
- API replaced-classes enrichie avec les noms classe/matière
2026-02-16 17:09:12 +01:00

173 lines
6.2 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Tests\Unit\Scolarite\Application\Query\GetActiveReplacements;
use App\Administration\Domain\Model\SchoolClass\ClassId;
use App\Administration\Domain\Model\Subject\SubjectId;
use App\Administration\Domain\Model\User\UserId;
use App\Scolarite\Application\Query\GetActiveReplacements\GetActiveReplacementsHandler;
use App\Scolarite\Application\Query\GetActiveReplacements\GetActiveReplacementsQuery;
use App\Scolarite\Application\Query\GetActiveReplacements\ReplacementDto;
use App\Scolarite\Domain\Model\TeacherReplacement\ClassSubjectPair;
use App\Scolarite\Domain\Model\TeacherReplacement\ReplacementStatus;
use App\Scolarite\Domain\Model\TeacherReplacement\TeacherReplacement;
use App\Scolarite\Infrastructure\Persistence\InMemory\InMemoryTeacherReplacementRepository;
use App\Shared\Domain\Clock;
use App\Shared\Domain\Tenant\TenantId;
use DateTimeImmutable;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;
final class GetActiveReplacementsHandlerTest extends TestCase
{
private const string TENANT_ID = '550e8400-e29b-41d4-a716-446655440001';
private const string OTHER_TENANT_ID = '550e8400-e29b-41d4-a716-446655440002';
private const string REPLACED_TEACHER_ID = '550e8400-e29b-41d4-a716-446655440010';
private const string REPLACEMENT_TEACHER_ID = '550e8400-e29b-41d4-a716-446655440011';
private const string CLASS_ID = '550e8400-e29b-41d4-a716-446655440020';
private const string SUBJECT_ID = '550e8400-e29b-41d4-a716-446655440030';
private const string CREATED_BY_ID = '550e8400-e29b-41d4-a716-446655440099';
private InMemoryTeacherReplacementRepository $repository;
private DateTimeImmutable $now;
protected function setUp(): void
{
$this->repository = new InMemoryTeacherReplacementRepository();
$this->now = new DateTimeImmutable('2026-03-15 10:00:00');
}
#[Test]
public function itReturnsActiveReplacementsForTenant(): void
{
$this->saveActiveReplacement();
$handler = $this->createHandler();
$result = $handler(new GetActiveReplacementsQuery(self::TENANT_ID));
self::assertCount(1, $result);
self::assertContainsOnlyInstancesOf(ReplacementDto::class, $result);
}
#[Test]
public function itReturnsEmptyArrayWhenNoneExist(): void
{
$handler = $this->createHandler();
$result = $handler(new GetActiveReplacementsQuery(self::TENANT_ID));
self::assertSame([], $result);
}
#[Test]
public function itMapsCorrectlyToDtos(): void
{
$replacement = $this->saveActiveReplacement();
$handler = $this->createHandler();
$result = $handler(new GetActiveReplacementsQuery(self::TENANT_ID));
self::assertCount(1, $result);
$dto = $result[0];
self::assertSame((string) $replacement->id, $dto->id);
self::assertSame(self::REPLACED_TEACHER_ID, $dto->replacedTeacherId);
self::assertSame(self::REPLACEMENT_TEACHER_ID, $dto->replacementTeacherId);
self::assertEquals(new DateTimeImmutable('2026-03-01'), $dto->startDate);
self::assertEquals(new DateTimeImmutable('2026-03-31'), $dto->endDate);
self::assertSame(ReplacementStatus::ACTIVE->value, $dto->status);
self::assertSame('Congé maladie', $dto->reason);
self::assertCount(1, $dto->classes);
self::assertSame(self::CLASS_ID, $dto->classes[0]['classId']);
self::assertSame(self::SUBJECT_ID, $dto->classes[0]['subjectId']);
}
#[Test]
public function itFiltersOutExpiredReplacements(): void
{
$this->saveReplacement(
startDate: new DateTimeImmutable('2026-01-01'),
endDate: new DateTimeImmutable('2026-02-01'),
);
$handler = $this->createHandler();
$result = $handler(new GetActiveReplacementsQuery(self::TENANT_ID));
self::assertSame([], $result);
}
#[Test]
public function itFiltersOutEndedReplacements(): void
{
$replacement = $this->saveActiveReplacement();
$replacement->terminer(new DateTimeImmutable('2026-03-10'));
$this->repository->save($replacement);
$handler = $this->createHandler();
$result = $handler(new GetActiveReplacementsQuery(self::TENANT_ID));
self::assertSame([], $result);
}
#[Test]
public function itDoesNotReturnReplacementsFromOtherTenants(): void
{
$this->saveReplacement(tenantId: self::OTHER_TENANT_ID);
$handler = $this->createHandler();
$result = $handler(new GetActiveReplacementsQuery(self::TENANT_ID));
self::assertSame([], $result);
}
private function saveActiveReplacement(): TeacherReplacement
{
return $this->saveReplacement();
}
private function saveReplacement(
?string $tenantId = null,
?DateTimeImmutable $startDate = null,
?DateTimeImmutable $endDate = null,
): TeacherReplacement {
$replacement = TeacherReplacement::designer(
tenantId: TenantId::fromString($tenantId ?? self::TENANT_ID),
replacedTeacherId: UserId::fromString(self::REPLACED_TEACHER_ID),
replacementTeacherId: UserId::fromString(self::REPLACEMENT_TEACHER_ID),
startDate: $startDate ?? new DateTimeImmutable('2026-03-01'),
endDate: $endDate ?? new DateTimeImmutable('2026-03-31'),
classes: [
new ClassSubjectPair(
ClassId::fromString(self::CLASS_ID),
SubjectId::fromString(self::SUBJECT_ID),
),
],
reason: 'Congé maladie',
createdBy: UserId::fromString(self::CREATED_BY_ID),
now: new DateTimeImmutable('2026-02-15 10:00:00'),
);
$this->repository->save($replacement);
return $replacement;
}
private function createHandler(): GetActiveReplacementsHandler
{
$now = $this->now;
$clock = new class($now) implements Clock {
public function __construct(private readonly DateTimeImmutable $now)
{
}
public function now(): DateTimeImmutable
{
return $this->now;
}
};
return new GetActiveReplacementsHandler($this->repository, $clock);
}
}