invitationRepository = new InMemoryParentInvitationRepository(); $this->userRepository = new InMemoryUserRepository(); $tenantConfig = new TenantConfig( tenantId: InfraTenantId::fromString(self::TENANT_ID), subdomain: 'ecole-alpha', databaseUrl: 'sqlite:///:memory:', ); $tenantRegistry = $this->createMock(TenantRegistry::class); $tenantRegistry->method('getConfig')->willReturn($tenantConfig); $this->tenantUrlBuilder = new TenantUrlBuilder( $tenantRegistry, 'https://classeo.fr', 'classeo.fr', ); } #[Test] public function itSendsParentInvitationEmailWithStudentName(): void { $student = $this->createAndSaveStudent('Alice', 'Dupont'); $invitation = $this->createAndSaveInvitation($student->id, 'parent@example.com'); $mailer = $this->createMock(MailerInterface::class); $twig = $this->createMock(Environment::class); $twig->expects($this->once()) ->method('render') ->with('emails/parent_invitation.html.twig', $this->callback( static fn (array $params): bool => $params['studentName'] === 'Alice Dupont' && str_contains($params['activationUrl'], 'ecole-alpha.classeo.fr/parent-activate/'), )) ->willReturn('parent invitation'); $mailer->expects($this->once()) ->method('send') ->with($this->callback( static fn (MimeEmail $email): bool => $email->getTo()[0]->getAddress() === 'parent@example.com' && $email->getSubject() === 'Invitation à rejoindre Classeo' && $email->getHtmlBody() === 'parent invitation', )); $handler = new SendParentInvitationEmailHandler( $mailer, $twig, $this->invitationRepository, $this->userRepository, $this->tenantUrlBuilder, self::FROM_EMAIL, ); $event = new InvitationParentEnvoyee( invitationId: $invitation->id, studentId: $student->id, parentEmail: $invitation->parentEmail, tenantId: $invitation->tenantId, occurredOn: new DateTimeImmutable('2026-02-07 10:00:00'), ); ($handler)($event); } #[Test] public function itSendsFromConfiguredEmailAddress(): void { $student = $this->createAndSaveStudent('Bob', 'Martin'); $invitation = $this->createAndSaveInvitation($student->id, 'parent2@example.com'); $mailer = $this->createMock(MailerInterface::class); $twig = $this->createMock(Environment::class); $twig->method('render')->willReturn('invitation'); $customFrom = 'custom@school.fr'; $mailer->expects($this->once()) ->method('send') ->with($this->callback( static fn (MimeEmail $email): bool => $email->getFrom()[0]->getAddress() === $customFrom, )); $handler = new SendParentInvitationEmailHandler( $mailer, $twig, $this->invitationRepository, $this->userRepository, $this->tenantUrlBuilder, $customFrom, ); $event = new InvitationParentEnvoyee( invitationId: $invitation->id, studentId: $student->id, parentEmail: $invitation->parentEmail, tenantId: $invitation->tenantId, occurredOn: new DateTimeImmutable('2026-02-07 10:00:00'), ); ($handler)($event); } #[Test] public function itDoesNothingWhenInvitationNotFound(): void { $student = $this->createAndSaveStudent('Charlie', 'Durand'); $mailer = $this->createMock(MailerInterface::class); $twig = $this->createMock(Environment::class); $mailer->expects($this->never())->method('send'); $handler = new SendParentInvitationEmailHandler( $mailer, $twig, $this->invitationRepository, $this->userRepository, $this->tenantUrlBuilder, self::FROM_EMAIL, ); // Event with a non-existent invitation ID $event = new InvitationParentEnvoyee( invitationId: \App\Administration\Domain\Model\Invitation\ParentInvitationId::generate(), studentId: $student->id, parentEmail: new Email('ghost@example.com'), tenantId: TenantId::fromString(self::TENANT_ID), occurredOn: new DateTimeImmutable('2026-02-07 10:00:00'), ); ($handler)($event); } private function createAndSaveStudent(string $firstName, string $lastName): User { $student = User::inviter( email: new Email($firstName . '@example.com'), role: Role::ELEVE, tenantId: TenantId::fromString(self::TENANT_ID), schoolName: 'École Alpha', firstName: $firstName, lastName: $lastName, invitedAt: new DateTimeImmutable('2026-02-07 10:00:00'), ); $student->pullDomainEvents(); $this->userRepository->save($student); return $student; } private function createAndSaveInvitation(UserId $studentId, string $parentEmail): ParentInvitation { $invitation = ParentInvitation::creer( tenantId: TenantId::fromString(self::TENANT_ID), studentId: $studentId, parentEmail: new Email($parentEmail), code: new InvitationCode(str_repeat('a', 32)), 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->invitationRepository->save($invitation); return $invitation; } }