repository = new InMemoryParentInvitationRepository(); $clock = new class implements Clock { public function now(): DateTimeImmutable { return new DateTimeImmutable('2026-02-08 10:00:00'); } }; $passwordHasher = new class implements PasswordHasher { public function hash(string $plainPassword): string { return 'hashed_' . $plainPassword; } public function verify(string $hashedPassword, string $plainPassword): bool { return $hashedPassword === 'hashed_' . $plainPassword; } }; $this->handler = new ActivateParentInvitationHandler( $this->repository, $passwordHasher, $clock, ); } #[Test] public function itValidatesAndReturnsActivationResult(): void { $invitation = $this->createSentInvitation(); $result = ($this->handler)(new ActivateParentInvitationCommand( code: self::CODE, firstName: 'Jean', lastName: 'Parent', password: 'SecurePass123!', )); self::assertSame((string) $invitation->id, $result->invitationId); self::assertSame((string) $invitation->studentId, $result->studentId); self::assertSame('parent@example.com', $result->parentEmail); self::assertSame('hashed_SecurePass123!', $result->hashedPassword); self::assertSame('Jean', $result->firstName); self::assertSame('Parent', $result->lastName); } #[Test] public function itThrowsWhenCodeNotFound(): void { $this->expectException(ParentInvitationNotFoundException::class); ($this->handler)(new ActivateParentInvitationCommand( code: 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb', firstName: 'Jean', lastName: 'Parent', password: 'SecurePass123!', )); } #[Test] public function itThrowsWhenInvitationNotSent(): void { $this->createPendingInvitation(); $this->expectException(InvitationNonEnvoyeeException::class); ($this->handler)(new ActivateParentInvitationCommand( code: self::CODE, firstName: 'Jean', lastName: 'Parent', password: 'SecurePass123!', )); } #[Test] public function itThrowsWhenInvitationExpired(): void { $this->createExpiredInvitation(); $this->expectException(InvitationExpireeException::class); ($this->handler)(new ActivateParentInvitationCommand( code: self::CODE, firstName: 'Jean', lastName: 'Parent', password: 'SecurePass123!', )); } #[Test] public function itThrowsWhenInvitationAlreadyActivated(): void { $this->createActivatedInvitation(); $this->expectException(InvitationDejaActiveeException::class); ($this->handler)(new ActivateParentInvitationCommand( code: self::CODE, firstName: 'Jean', lastName: 'Parent', password: 'SecurePass123!', )); } private function createSentInvitation(): ParentInvitation { $invitation = ParentInvitation::creer( tenantId: TenantId::fromString(self::TENANT_ID), studentId: UserId::generate(), parentEmail: new Email('parent@example.com'), code: new InvitationCode(self::CODE), createdAt: new DateTimeImmutable('2026-02-07 10:00:00'), createdBy: UserId::generate(), ); $invitation->envoyer(new DateTimeImmutable('2026-02-07 10:00:00')); $invitation->pullDomainEvents(); $this->repository->save($invitation); return $invitation; } private function createPendingInvitation(): ParentInvitation { $invitation = ParentInvitation::creer( tenantId: TenantId::fromString(self::TENANT_ID), studentId: UserId::generate(), parentEmail: new Email('parent@example.com'), code: new InvitationCode(self::CODE), createdAt: new DateTimeImmutable('2026-02-07 10:00:00'), createdBy: UserId::generate(), ); $invitation->pullDomainEvents(); $this->repository->save($invitation); return $invitation; } private function createExpiredInvitation(): ParentInvitation { $invitation = ParentInvitation::creer( tenantId: TenantId::fromString(self::TENANT_ID), studentId: UserId::generate(), parentEmail: new Email('parent@example.com'), code: new InvitationCode(self::CODE), createdAt: new DateTimeImmutable('2026-01-01 10:00:00'), createdBy: UserId::generate(), ); $invitation->envoyer(new DateTimeImmutable('2026-01-01 10:00:00')); $invitation->pullDomainEvents(); $this->repository->save($invitation); return $invitation; } private function createActivatedInvitation(): ParentInvitation { $invitation = ParentInvitation::creer( tenantId: TenantId::fromString(self::TENANT_ID), studentId: UserId::generate(), parentEmail: new Email('parent@example.com'), code: new InvitationCode(self::CODE), createdAt: new DateTimeImmutable('2026-02-07 10:00:00'), createdBy: UserId::generate(), ); $invitation->envoyer(new DateTimeImmutable('2026-02-07 10:00:00')); $invitation->activer(UserId::generate(), new DateTimeImmutable('2026-02-07 12:00:00')); $invitation->pullDomainEvents(); $this->repository->save($invitation); return $invitation; } }