Le handler ChangeStudentClass exigeait une affectation existante pour l'année scolaire en cours avant de pouvoir changer la classe. Un élève créé sans ClassAssignment (import direct, année précédente) provoquait une erreur "Élève non trouvé" au lieu d'être simplement affecté. Le handler crée désormais une nouvelle affectation quand aucune n'existe, et l'erreur de changement de classe s'affiche dans la modale au lieu de la page principale.
206 lines
8.0 KiB
PHP
206 lines
8.0 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Tests\Unit\Administration\Application\Command\ChangeStudentClass;
|
|
|
|
use App\Administration\Application\Command\ChangeStudentClass\ChangeStudentClassCommand;
|
|
use App\Administration\Application\Command\ChangeStudentClass\ChangeStudentClassHandler;
|
|
use App\Administration\Domain\Exception\ClasseNotFoundException;
|
|
use App\Administration\Domain\Model\ClassAssignment\ClassAssignment;
|
|
use App\Administration\Domain\Model\SchoolClass\AcademicYearId;
|
|
use App\Administration\Domain\Model\SchoolClass\ClassId;
|
|
use App\Administration\Domain\Model\SchoolClass\ClassName;
|
|
use App\Administration\Domain\Model\SchoolClass\ClassStatus;
|
|
use App\Administration\Domain\Model\SchoolClass\SchoolClass;
|
|
use App\Administration\Domain\Model\SchoolClass\SchoolId;
|
|
use App\Administration\Domain\Model\SchoolClass\SchoolLevel;
|
|
use App\Administration\Domain\Model\User\UserId;
|
|
use App\Administration\Infrastructure\Persistence\InMemory\InMemoryClassAssignmentRepository;
|
|
use App\Administration\Infrastructure\Persistence\InMemory\InMemoryClassRepository;
|
|
use App\Shared\Domain\Clock;
|
|
use App\Shared\Domain\Tenant\TenantId;
|
|
use DateTimeImmutable;
|
|
use PHPUnit\Framework\Attributes\Test;
|
|
use PHPUnit\Framework\TestCase;
|
|
|
|
final class ChangeStudentClassHandlerTest extends TestCase
|
|
{
|
|
private const string TENANT_ID = '550e8400-e29b-41d4-a716-446655440001';
|
|
private const string OLD_CLASS_ID = '550e8400-e29b-41d4-a716-446655440020';
|
|
private const string NEW_CLASS_ID = '550e8400-e29b-41d4-a716-446655440021';
|
|
private const string ACADEMIC_YEAR_ID = '550e8400-e29b-41d4-a716-446655440040';
|
|
private const string SCHOOL_ID = '550e8400-e29b-41d4-a716-446655440050';
|
|
private const string STUDENT_ID = '550e8400-e29b-41d4-a716-446655440060';
|
|
|
|
private InMemoryClassAssignmentRepository $classAssignmentRepository;
|
|
private InMemoryClassRepository $classRepository;
|
|
private Clock $clock;
|
|
|
|
protected function setUp(): void
|
|
{
|
|
$this->classAssignmentRepository = new InMemoryClassAssignmentRepository();
|
|
$this->classRepository = new InMemoryClassRepository();
|
|
$this->clock = new class implements Clock {
|
|
public function now(): DateTimeImmutable
|
|
{
|
|
return new DateTimeImmutable('2026-02-21 10:00:00');
|
|
}
|
|
};
|
|
|
|
$this->seedTestData();
|
|
}
|
|
|
|
#[Test]
|
|
public function itChangesStudentClass(): void
|
|
{
|
|
$handler = $this->createHandler();
|
|
$command = $this->createCommand();
|
|
|
|
$assignment = $handler($command);
|
|
|
|
self::assertTrue($assignment->classId->equals(ClassId::fromString(self::NEW_CLASS_ID)));
|
|
}
|
|
|
|
#[Test]
|
|
public function itThrowsWhenNewClassDoesNotExist(): void
|
|
{
|
|
$handler = $this->createHandler();
|
|
$command = $this->createCommand(newClassId: '550e8400-e29b-41d4-a716-446655440099');
|
|
|
|
$this->expectException(ClasseNotFoundException::class);
|
|
$handler($command);
|
|
}
|
|
|
|
#[Test]
|
|
public function itCreatesAssignmentWhenNoneExists(): void
|
|
{
|
|
$handler = $this->createHandler();
|
|
$newStudentId = '550e8400-e29b-41d4-a716-446655440070';
|
|
$command = $this->createCommand(studentId: $newStudentId);
|
|
|
|
$assignment = $handler($command);
|
|
|
|
self::assertTrue($assignment->studentId->equals(UserId::fromString($newStudentId)));
|
|
self::assertTrue($assignment->classId->equals(ClassId::fromString(self::NEW_CLASS_ID)));
|
|
self::assertTrue($assignment->academicYearId->equals(AcademicYearId::fromString(self::ACADEMIC_YEAR_ID)));
|
|
}
|
|
|
|
#[Test]
|
|
public function itThrowsWhenClassBelongsToAnotherTenant(): void
|
|
{
|
|
$crossTenantClassId = '550e8400-e29b-41d4-a716-446655440030';
|
|
$this->classRepository->save(SchoolClass::reconstitute(
|
|
id: ClassId::fromString($crossTenantClassId),
|
|
tenantId: TenantId::fromString('550e8400-e29b-41d4-a716-446655440002'),
|
|
schoolId: SchoolId::fromString(self::SCHOOL_ID),
|
|
academicYearId: AcademicYearId::fromString(self::ACADEMIC_YEAR_ID),
|
|
name: new ClassName('5ème B'),
|
|
level: SchoolLevel::CINQUIEME,
|
|
capacity: 30,
|
|
status: ClassStatus::ACTIVE,
|
|
description: null,
|
|
createdAt: new DateTimeImmutable('2026-01-15'),
|
|
updatedAt: new DateTimeImmutable('2026-01-15'),
|
|
deletedAt: null,
|
|
));
|
|
|
|
$handler = $this->createHandler();
|
|
$command = $this->createCommand(newClassId: $crossTenantClassId);
|
|
|
|
$this->expectException(ClasseNotFoundException::class);
|
|
$handler($command);
|
|
}
|
|
|
|
#[Test]
|
|
public function itThrowsWhenClassIsArchived(): void
|
|
{
|
|
$archivedClassId = '550e8400-e29b-41d4-a716-446655440031';
|
|
$this->classRepository->save(SchoolClass::reconstitute(
|
|
id: ClassId::fromString($archivedClassId),
|
|
tenantId: TenantId::fromString(self::TENANT_ID),
|
|
schoolId: SchoolId::fromString(self::SCHOOL_ID),
|
|
academicYearId: AcademicYearId::fromString(self::ACADEMIC_YEAR_ID),
|
|
name: new ClassName('6ème C'),
|
|
level: SchoolLevel::SIXIEME,
|
|
capacity: 30,
|
|
status: ClassStatus::ARCHIVED,
|
|
description: null,
|
|
createdAt: new DateTimeImmutable('2026-01-15'),
|
|
updatedAt: new DateTimeImmutable('2026-01-15'),
|
|
deletedAt: null,
|
|
));
|
|
|
|
$handler = $this->createHandler();
|
|
$command = $this->createCommand(newClassId: $archivedClassId);
|
|
|
|
$this->expectException(ClasseNotFoundException::class);
|
|
$handler($command);
|
|
}
|
|
|
|
private function seedTestData(): void
|
|
{
|
|
$oldClass = SchoolClass::reconstitute(
|
|
id: ClassId::fromString(self::OLD_CLASS_ID),
|
|
tenantId: TenantId::fromString(self::TENANT_ID),
|
|
schoolId: SchoolId::fromString(self::SCHOOL_ID),
|
|
academicYearId: AcademicYearId::fromString(self::ACADEMIC_YEAR_ID),
|
|
name: new ClassName('6ème A'),
|
|
level: SchoolLevel::SIXIEME,
|
|
capacity: 30,
|
|
status: ClassStatus::ACTIVE,
|
|
description: null,
|
|
createdAt: new DateTimeImmutable('2026-01-15'),
|
|
updatedAt: new DateTimeImmutable('2026-01-15'),
|
|
deletedAt: null,
|
|
);
|
|
$this->classRepository->save($oldClass);
|
|
|
|
$newClass = SchoolClass::reconstitute(
|
|
id: ClassId::fromString(self::NEW_CLASS_ID),
|
|
tenantId: TenantId::fromString(self::TENANT_ID),
|
|
schoolId: SchoolId::fromString(self::SCHOOL_ID),
|
|
academicYearId: AcademicYearId::fromString(self::ACADEMIC_YEAR_ID),
|
|
name: new ClassName('6ème B'),
|
|
level: SchoolLevel::SIXIEME,
|
|
capacity: 30,
|
|
status: ClassStatus::ACTIVE,
|
|
description: null,
|
|
createdAt: new DateTimeImmutable('2026-01-15'),
|
|
updatedAt: new DateTimeImmutable('2026-01-15'),
|
|
deletedAt: null,
|
|
);
|
|
$this->classRepository->save($newClass);
|
|
|
|
$assignment = ClassAssignment::affecter(
|
|
tenantId: TenantId::fromString(self::TENANT_ID),
|
|
studentId: UserId::fromString(self::STUDENT_ID),
|
|
classId: ClassId::fromString(self::OLD_CLASS_ID),
|
|
academicYearId: AcademicYearId::fromString(self::ACADEMIC_YEAR_ID),
|
|
assignedAt: new DateTimeImmutable('2026-01-20'),
|
|
);
|
|
$this->classAssignmentRepository->save($assignment);
|
|
}
|
|
|
|
private function createHandler(): ChangeStudentClassHandler
|
|
{
|
|
return new ChangeStudentClassHandler(
|
|
$this->classAssignmentRepository,
|
|
$this->classRepository,
|
|
$this->clock,
|
|
);
|
|
}
|
|
|
|
private function createCommand(
|
|
?string $newClassId = null,
|
|
?string $studentId = null,
|
|
): ChangeStudentClassCommand {
|
|
return new ChangeStudentClassCommand(
|
|
tenantId: self::TENANT_ID,
|
|
studentId: $studentId ?? self::STUDENT_ID,
|
|
newClassId: $newClassId ?? self::NEW_CLASS_ID,
|
|
academicYearId: self::ACADEMIC_YEAR_ID,
|
|
);
|
|
}
|
|
}
|