Files
Classeo/backend/tests/Unit/Administration/Application/Command/ChangeStudentClass/ChangeStudentClassHandlerTest.php
Mathias STRASSER bda63bd98c fix: Permettre l'affectation de classe pour les élèves sans affectation existante
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.
2026-03-09 11:20:29 +01:00

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,
);
}
}