classAssignmentRepository = new InMemoryClassAssignmentRepository(); $this->userRepository = new InMemoryUserRepository(); $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 itAssignsStudentToClass(): void { $handler = $this->createHandler(); $command = $this->createCommand(); $assignment = $handler($command); self::assertTrue($assignment->classId->equals(ClassId::fromString(self::CLASS_ID))); } #[Test] public function itThrowsWhenStudentDoesNotExist(): void { $handler = $this->createHandler(); $command = $this->createCommand(studentId: '550e8400-e29b-41d4-a716-446655440099'); $this->expectException(UserNotFoundException::class); $handler($command); } #[Test] public function itThrowsWhenClassDoesNotExist(): void { $handler = $this->createHandler(); $command = $this->createCommand(classId: '550e8400-e29b-41d4-a716-446655440099'); $this->expectException(ClasseNotFoundException::class); $handler($command); } #[Test] public function itThrowsWhenStudentAlreadyAssigned(): void { $handler = $this->createHandler(); $command = $this->createCommand(); // First assignment succeeds $handler($command); $this->expectException(EleveDejaAffecteException::class); $handler($command); } #[Test] public function itThrowsWhenClassBelongsToAnotherTenant(): void { $otherTenantClassId = '550e8400-e29b-41d4-a716-446655440030'; $classDifferentTenant = SchoolClass::reconstitute( id: ClassId::fromString($otherTenantClassId), tenantId: TenantId::fromString('550e8400-e29b-41d4-a716-446655440002'), 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($classDifferentTenant); $handler = $this->createHandler(); $command = $this->createCommand(classId: $otherTenantClassId); $this->expectException(ClasseNotFoundException::class); $handler($command); } #[Test] public function itThrowsWhenStudentBelongsToAnotherTenant(): void { $otherTenantStudentId = '550e8400-e29b-41d4-a716-446655440070'; $otherTenantStudent = User::reconstitute( id: UserId::fromString($otherTenantStudentId), email: null, roles: [Role::ELEVE], tenantId: TenantId::fromString('550e8400-e29b-41d4-a716-446655440002'), schoolName: 'Autre École', statut: StatutCompte::INSCRIT, dateNaissance: null, createdAt: new DateTimeImmutable('2026-01-15'), hashedPassword: null, activatedAt: null, consentementParental: null, ); $this->userRepository->save($otherTenantStudent); $handler = $this->createHandler(); $command = $this->createCommand(studentId: $otherTenantStudentId); $this->expectException(UserNotFoundException::class); $handler($command); } #[Test] public function itThrowsWhenClassIsArchived(): void { $archivedClassId = '550e8400-e29b-41d4-a716-446655440031'; $archivedClass = 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, ); $this->classRepository->save($archivedClass); $handler = $this->createHandler(); $command = $this->createCommand(classId: $archivedClassId); $this->expectException(ClasseNotFoundException::class); $handler($command); } private function seedTestData(): void { $class = SchoolClass::reconstitute( id: ClassId::fromString(self::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($class); $student = User::reconstitute( id: UserId::fromString(self::STUDENT_ID), email: null, roles: [Role::ELEVE], tenantId: TenantId::fromString(self::TENANT_ID), schoolName: 'École Test', statut: StatutCompte::INSCRIT, dateNaissance: null, createdAt: new DateTimeImmutable('2026-01-15'), hashedPassword: null, activatedAt: null, consentementParental: null, ); $this->userRepository->save($student); } private function createHandler(): AssignStudentToClassHandler { return new AssignStudentToClassHandler( $this->classAssignmentRepository, $this->userRepository, $this->classRepository, $this->clock, ); } private function createCommand( ?string $studentId = null, ?string $classId = null, ): AssignStudentToClassCommand { return new AssignStudentToClassCommand( tenantId: self::TENANT_ID, studentId: $studentId ?? self::STUDENT_ID, classId: $classId ?? self::CLASS_ID, academicYearId: self::ACADEMIC_YEAR_ID, ); } }