repository = new InMemoryStudentGuardianRepository(); } private function createHandlerWithMockedUsers( ?Role $guardianRole = null, ?Role $studentRole = null, ?string $guardianTenantId = null, ?string $studentTenantId = null, ): LinkParentToStudentHandler { $guardianRole ??= Role::PARENT; $studentRole ??= Role::ELEVE; $now = new DateTimeImmutable('2026-02-10 10:00:00'); $guardianUser = User::creer( email: new Email('guardian@example.com'), role: $guardianRole, tenantId: TenantId::fromString($guardianTenantId ?? self::TENANT_ID), schoolName: 'École Test', dateNaissance: null, createdAt: $now, ); $studentUser = User::creer( email: new Email('student@example.com'), role: $studentRole, tenantId: TenantId::fromString($studentTenantId ?? self::TENANT_ID), schoolName: 'École Test', dateNaissance: null, createdAt: $now, ); $userRepository = $this->createMock(UserRepository::class); $userRepository->method('get')->willReturnCallback( static function (UserId $id) use ($guardianUser, $studentUser): User { if ((string) $id === self::GUARDIAN_ID || (string) $id === self::GUARDIAN_2_ID || (string) $id === self::GUARDIAN_3_ID) { return $guardianUser; } return $studentUser; }, ); $clock = new class implements Clock { public function now(): DateTimeImmutable { return new DateTimeImmutable('2026-02-10 10:00:00'); } }; return new LinkParentToStudentHandler($this->repository, $userRepository, $clock); } #[Test] public function linkParentToStudentSuccessfully(): void { $handler = $this->createHandlerWithMockedUsers(); $command = new LinkParentToStudentCommand( studentId: self::STUDENT_ID, guardianId: self::GUARDIAN_ID, relationshipType: RelationshipType::FATHER->value, tenantId: self::TENANT_ID, createdBy: self::ADMIN_ID, ); $link = ($handler)($command); self::assertInstanceOf(StudentGuardian::class, $link); self::assertSame(RelationshipType::FATHER, $link->relationshipType); self::assertNotNull($link->createdBy); } #[Test] public function linkIsSavedToRepository(): void { $handler = $this->createHandlerWithMockedUsers(); $command = new LinkParentToStudentCommand( studentId: self::STUDENT_ID, guardianId: self::GUARDIAN_ID, relationshipType: RelationshipType::MOTHER->value, tenantId: self::TENANT_ID, ); $link = ($handler)($command); $saved = $this->repository->get($link->id, TenantId::fromString(self::TENANT_ID)); self::assertTrue($saved->id->equals($link->id)); } #[Test] public function throwsWhenLinkAlreadyExists(): void { $handler = $this->createHandlerWithMockedUsers(); $command = new LinkParentToStudentCommand( studentId: self::STUDENT_ID, guardianId: self::GUARDIAN_ID, relationshipType: RelationshipType::FATHER->value, tenantId: self::TENANT_ID, ); ($handler)($command); $this->expectException(LiaisonDejaExistanteException::class); ($handler)($command); } #[Test] public function throwsWhenMaxGuardiansReached(): void { $handler = $this->createHandlerWithMockedUsers(); ($handler)(new LinkParentToStudentCommand( studentId: self::STUDENT_ID, guardianId: self::GUARDIAN_ID, relationshipType: RelationshipType::FATHER->value, tenantId: self::TENANT_ID, )); ($handler)(new LinkParentToStudentCommand( studentId: self::STUDENT_ID, guardianId: self::GUARDIAN_2_ID, relationshipType: RelationshipType::MOTHER->value, tenantId: self::TENANT_ID, )); $this->expectException(MaxGuardiansReachedException::class); ($handler)(new LinkParentToStudentCommand( studentId: self::STUDENT_ID, guardianId: self::GUARDIAN_3_ID, relationshipType: RelationshipType::TUTOR_M->value, tenantId: self::TENANT_ID, )); } #[Test] public function allowsTwoGuardiansForSameStudent(): void { $handler = $this->createHandlerWithMockedUsers(); ($handler)(new LinkParentToStudentCommand( studentId: self::STUDENT_ID, guardianId: self::GUARDIAN_ID, relationshipType: RelationshipType::FATHER->value, tenantId: self::TENANT_ID, )); $link2 = ($handler)(new LinkParentToStudentCommand( studentId: self::STUDENT_ID, guardianId: self::GUARDIAN_2_ID, relationshipType: RelationshipType::MOTHER->value, tenantId: self::TENANT_ID, )); self::assertInstanceOf(StudentGuardian::class, $link2); self::assertSame(2, $this->repository->countGuardiansForStudent( $link2->studentId, TenantId::fromString(self::TENANT_ID), )); } #[Test] public function linkWithoutCreatedByAllowsNull(): void { $handler = $this->createHandlerWithMockedUsers(); $command = new LinkParentToStudentCommand( studentId: self::STUDENT_ID, guardianId: self::GUARDIAN_ID, relationshipType: RelationshipType::FATHER->value, tenantId: self::TENANT_ID, ); $link = ($handler)($command); self::assertNull($link->createdBy); } #[Test] public function throwsWhenGuardianIsNotParent(): void { $handler = $this->createHandlerWithMockedUsers(guardianRole: Role::ELEVE); $this->expectException(InvalidGuardianRoleException::class); ($handler)(new LinkParentToStudentCommand( studentId: self::STUDENT_ID, guardianId: self::GUARDIAN_ID, relationshipType: RelationshipType::FATHER->value, tenantId: self::TENANT_ID, )); } #[Test] public function throwsWhenStudentIsNotEleve(): void { $handler = $this->createHandlerWithMockedUsers(studentRole: Role::PARENT); $this->expectException(InvalidStudentRoleException::class); ($handler)(new LinkParentToStudentCommand( studentId: self::STUDENT_ID, guardianId: self::GUARDIAN_ID, relationshipType: RelationshipType::FATHER->value, tenantId: self::TENANT_ID, )); } #[Test] public function throwsWhenGuardianBelongsToDifferentTenant(): void { $handler = $this->createHandlerWithMockedUsers(guardianTenantId: self::OTHER_TENANT_ID); $this->expectException(TenantMismatchException::class); ($handler)(new LinkParentToStudentCommand( studentId: self::STUDENT_ID, guardianId: self::GUARDIAN_ID, relationshipType: RelationshipType::FATHER->value, tenantId: self::TENANT_ID, )); } #[Test] public function throwsWhenStudentBelongsToDifferentTenant(): void { $handler = $this->createHandlerWithMockedUsers(studentTenantId: self::OTHER_TENANT_ID); $this->expectException(TenantMismatchException::class); ($handler)(new LinkParentToStudentCommand( studentId: self::STUDENT_ID, guardianId: self::GUARDIAN_ID, relationshipType: RelationshipType::FATHER->value, tenantId: self::TENANT_ID, )); } #[Test] public function throwsWhenRelationshipTypeIsInvalid(): void { $handler = $this->createHandlerWithMockedUsers(); $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage('Type de relation invalide'); ($handler)(new LinkParentToStudentCommand( studentId: self::STUDENT_ID, guardianId: self::GUARDIAN_ID, relationshipType: 'invalide', tenantId: self::TENANT_ID, )); } }