mailer = $this->createMock(MailerInterface::class); $this->twig = $this->createMock(Environment::class); } #[Test] public function sendsResetEmailToUser(): void { $event = $this->createEvent('user@example.com'); $this->twig->method('render')->willReturn('
Reset your password
'); $sentEmail = null; $this->mailer->expects(self::once()) ->method('send') ->with(self::callback(static function (Email $email) use (&$sentEmail): bool { $sentEmail = $email; return true; })); $handler = new SendPasswordResetEmailHandler( $this->mailer, $this->twig, self::APP_URL, self::FROM_EMAIL, ); ($handler)($event); self::assertNotNull($sentEmail); self::assertSame('user@example.com', $sentEmail->getTo()[0]->getAddress()); self::assertSame(self::FROM_EMAIL, $sentEmail->getFrom()[0]->getAddress()); self::assertStringContainsString('initialisation', $sentEmail->getSubject()); } #[Test] public function rendersTemplateWithResetUrl(): void { $event = $this->createEvent('user@example.com'); $this->twig->expects(self::once()) ->method('render') ->with( 'emails/password_reset.html.twig', self::callback(static function (array $vars): bool { return $vars['email'] === 'user@example.com' && $vars['resetUrl'] === 'https://ecole-alpha.classeo.fr/reset-password/' . self::TOKEN_VALUE; }), ) ->willReturn('HTML
'); $this->mailer->method('send'); $handler = new SendPasswordResetEmailHandler( $this->mailer, $this->twig, self::APP_URL, self::FROM_EMAIL, ); ($handler)($event); } #[Test] public function handlesTrailingSlashInAppUrl(): void { $event = $this->createEvent('user@example.com'); $this->twig->expects(self::once()) ->method('render') ->with( self::anything(), self::callback(static function (array $vars): bool { // Should not have double slash return str_contains($vars['resetUrl'], '.fr/reset-password/') && !str_contains($vars['resetUrl'], '.fr//'); }), ) ->willReturn('HTML
'); $this->mailer->method('send'); $handler = new SendPasswordResetEmailHandler( $this->mailer, $this->twig, 'https://ecole-alpha.classeo.fr/', self::FROM_EMAIL, ); ($handler)($event); } #[Test] public function includesTokenValueInResetUrl(): void { $customToken = 'custom-token-value-xyz'; $event = new PasswordResetTokenGenerated( tokenId: PasswordResetTokenId::generate(), tokenValue: $customToken, userId: '550e8400-e29b-41d4-a716-446655440001', email: 'user@example.com', tenantId: TenantId::fromString('a1b2c3d4-e5f6-7890-abcd-ef1234567890'), occurredOn: new DateTimeImmutable('2026-02-15 10:00:00'), ); $this->twig->expects(self::once()) ->method('render') ->with( self::anything(), self::callback(static function (array $vars) use ($customToken): bool { return str_ends_with($vars['resetUrl'], '/reset-password/' . $customToken); }), ) ->willReturn('HTML
'); $this->mailer->method('send'); $handler = new SendPasswordResetEmailHandler( $this->mailer, $this->twig, self::APP_URL, self::FROM_EMAIL, ); ($handler)($event); } #[Test] public function usesDefaultFromEmail(): void { $event = $this->createEvent('user@example.com'); $this->twig->method('render')->willReturn('HTML
'); $sentEmail = null; $this->mailer->expects(self::once()) ->method('send') ->with(self::callback(static function (Email $email) use (&$sentEmail): bool { $sentEmail = $email; return true; })); // Use default fromEmail (constructor default) $handler = new SendPasswordResetEmailHandler( $this->mailer, $this->twig, self::APP_URL, ); ($handler)($event); self::assertSame('noreply@classeo.fr', $sentEmail->getFrom()[0]->getAddress()); } private function createEvent(string $email): PasswordResetTokenGenerated { return new PasswordResetTokenGenerated( tokenId: PasswordResetTokenId::generate(), tokenValue: self::TOKEN_VALUE, userId: '550e8400-e29b-41d4-a716-446655440001', email: $email, tenantId: TenantId::fromString('a1b2c3d4-e5f6-7890-abcd-ef1234567890'), occurredOn: new DateTimeImmutable('2026-02-15 10:00:00'), ); } }