clock = new class implements Clock { public function now(): DateTimeImmutable { return new DateTimeImmutable('2026-02-07 10:00:00'); } }; } #[Test] public function inviterCreatesUserWithPendingStatusAndRecordsInvitedAt(): void { $invitedAt = new DateTimeImmutable('2026-02-07 10:00:00'); $user = User::inviter( email: new Email('teacher@example.com'), role: Role::PROF, tenantId: TenantId::fromString(self::TENANT_ID), schoolName: self::SCHOOL_NAME, firstName: 'Jean', lastName: 'Dupont', invitedAt: $invitedAt, ); self::assertSame(StatutCompte::EN_ATTENTE, $user->statut); self::assertSame('Jean', $user->firstName); self::assertSame('Dupont', $user->lastName); self::assertEquals($invitedAt, $user->invitedAt); self::assertNull($user->hashedPassword); self::assertNull($user->activatedAt); self::assertNull($user->blockedAt); self::assertNull($user->blockedReason); } #[Test] public function inviterRecordsUtilisateurInviteEvent(): void { $user = $this->inviteUser(); $events = $user->pullDomainEvents(); self::assertCount(1, $events); self::assertInstanceOf(UtilisateurInvite::class, $events[0]); } #[Test] public function renvoyerInvitationUpdatesInvitedAtAndRecordsEvent(): void { $user = $this->inviteUser(); $user->pullDomainEvents(); $newInvitedAt = new DateTimeImmutable('2026-02-14 10:00:00'); $user->renvoyerInvitation($newInvitedAt); self::assertEquals($newInvitedAt, $user->invitedAt); $events = $user->pullDomainEvents(); self::assertCount(1, $events); self::assertInstanceOf(InvitationRenvoyee::class, $events[0]); } #[Test] public function renvoyerInvitationThrowsWhenUserIsActive(): void { $user = $this->inviteUser(); $user->activer( '$argon2id$hashed', new DateTimeImmutable(), new \App\Administration\Domain\Policy\ConsentementParentalPolicy($this->clock), ); $this->expectException(UtilisateurDejaInviteException::class); $user->renvoyerInvitation(new DateTimeImmutable('2026-02-14 10:00:00')); } #[Test] public function bloquerSetsStatusToSuspenduWithReasonAndDate(): void { $user = $this->inviteUser(); $user->activer( '$argon2id$hashed', new DateTimeImmutable(), new \App\Administration\Domain\Policy\ConsentementParentalPolicy($this->clock), ); $user->pullDomainEvents(); $blockedAt = new DateTimeImmutable('2026-02-10 15:00:00'); $user->bloquer('Comportement inapproprié', $blockedAt); self::assertSame(StatutCompte::SUSPENDU, $user->statut); self::assertEquals($blockedAt, $user->blockedAt); self::assertSame('Comportement inapproprié', $user->blockedReason); self::assertFalse($user->peutSeConnecter()); } #[Test] public function bloquerRecordsUtilisateurBloqueEvent(): void { $user = $this->inviteUser(); $user->activer( '$argon2id$hashed', new DateTimeImmutable(), new \App\Administration\Domain\Policy\ConsentementParentalPolicy($this->clock), ); $user->pullDomainEvents(); $user->bloquer('Raison de test', new DateTimeImmutable('2026-02-10 15:00:00')); $events = $user->pullDomainEvents(); self::assertCount(1, $events); self::assertInstanceOf(UtilisateurBloque::class, $events[0]); } #[Test] public function bloquerThrowsWhenAlreadySuspendu(): void { $user = $this->inviteUser(); $user->activer( '$argon2id$hashed', new DateTimeImmutable(), new \App\Administration\Domain\Policy\ConsentementParentalPolicy($this->clock), ); $user->bloquer('Première raison', new DateTimeImmutable('2026-02-10 15:00:00')); $this->expectException(UtilisateurNonBlocableException::class); $user->bloquer('Seconde raison', new DateTimeImmutable('2026-02-11 15:00:00')); } #[Test] public function bloquerThrowsWhenEnAttente(): void { $user = $this->inviteUser(); $this->expectException(UtilisateurNonBlocableException::class); $user->bloquer('Raison de test', new DateTimeImmutable('2026-02-10 15:00:00')); } #[Test] public function bloquerThrowsWhenConsentementRequis(): void { $user = User::reconstitute( id: UserId::generate(), email: new Email('minor@example.com'), role: Role::ELEVE, tenantId: TenantId::fromString(self::TENANT_ID), schoolName: self::SCHOOL_NAME, statut: StatutCompte::CONSENTEMENT_REQUIS, dateNaissance: new DateTimeImmutable('2015-01-01'), createdAt: new DateTimeImmutable('2026-02-01 10:00:00'), hashedPassword: null, activatedAt: null, consentementParental: null, ); $this->expectException(UtilisateurNonBlocableException::class); $user->bloquer('Raison de test', new DateTimeImmutable('2026-02-10 15:00:00')); } #[Test] public function debloquerRestoresActiveStatusAndClearsBlockedInfo(): void { $user = $this->inviteUser(); $user->activer( '$argon2id$hashed', new DateTimeImmutable(), new \App\Administration\Domain\Policy\ConsentementParentalPolicy($this->clock), ); $user->bloquer('Raison de test', new DateTimeImmutable('2026-02-10 15:00:00')); $user->pullDomainEvents(); $user->debloquer(new DateTimeImmutable('2026-02-12 10:00:00')); self::assertSame(StatutCompte::ACTIF, $user->statut); self::assertNull($user->blockedAt); self::assertNull($user->blockedReason); } #[Test] public function debloquerRecordsUtilisateurDebloqueEvent(): void { $user = $this->inviteUser(); $user->activer( '$argon2id$hashed', new DateTimeImmutable(), new \App\Administration\Domain\Policy\ConsentementParentalPolicy($this->clock), ); $user->bloquer('Raison de test', new DateTimeImmutable('2026-02-10 15:00:00')); $user->pullDomainEvents(); $user->debloquer(new DateTimeImmutable('2026-02-12 10:00:00')); $events = $user->pullDomainEvents(); self::assertCount(1, $events); self::assertInstanceOf(UtilisateurDebloque::class, $events[0]); } #[Test] public function debloquerThrowsWhenNotSuspendu(): void { $user = $this->inviteUser(); $user->activer( '$argon2id$hashed', new DateTimeImmutable(), new \App\Administration\Domain\Policy\ConsentementParentalPolicy($this->clock), ); $this->expectException(UtilisateurNonDeblocableException::class); $user->debloquer(new DateTimeImmutable('2026-02-12 10:00:00')); } #[Test] public function debloquerThrowsWhenEnAttente(): void { $user = $this->inviteUser(); $this->expectException(UtilisateurNonDeblocableException::class); $user->debloquer(new DateTimeImmutable('2026-02-12 10:00:00')); } #[Test] public function estInvitationExpireeReturnsTrueAfter7Days(): void { $invitedAt = new DateTimeImmutable('2026-01-30 10:00:00'); $user = User::inviter( email: new Email('teacher@example.com'), role: Role::PROF, tenantId: TenantId::fromString(self::TENANT_ID), schoolName: self::SCHOOL_NAME, firstName: 'Jean', lastName: 'Dupont', invitedAt: $invitedAt, ); // 8 jours après $checkAt = new DateTimeImmutable('2026-02-07 10:00:01'); self::assertTrue($user->estInvitationExpiree($checkAt)); } #[Test] public function estInvitationExpireeReturnsFalseWithin7Days(): void { $invitedAt = new DateTimeImmutable('2026-02-05 10:00:00'); $user = User::inviter( email: new Email('teacher@example.com'), role: Role::PROF, tenantId: TenantId::fromString(self::TENANT_ID), schoolName: self::SCHOOL_NAME, firstName: 'Jean', lastName: 'Dupont', invitedAt: $invitedAt, ); // 2 jours après $checkAt = new DateTimeImmutable('2026-02-07 10:00:00'); self::assertFalse($user->estInvitationExpiree($checkAt)); } #[Test] public function estInvitationExpireeReturnsFalseForActiveUser(): void { $user = $this->inviteUser(); $user->activer( '$argon2id$hashed', new DateTimeImmutable(), new \App\Administration\Domain\Policy\ConsentementParentalPolicy($this->clock), ); // Même longtemps après, un utilisateur actif n'a pas d'invitation expirée $checkAt = new DateTimeImmutable('2027-01-01 10:00:00'); self::assertFalse($user->estInvitationExpiree($checkAt)); } private function inviteUser(): User { return User::inviter( email: new Email('teacher@example.com'), role: Role::PROF, tenantId: TenantId::fromString(self::TENANT_ID), schoolName: self::SCHOOL_NAME, firstName: 'Jean', lastName: 'Dupont', invitedAt: new DateTimeImmutable('2026-02-07 10:00:00'), ); } }