createMock(MailerInterface::class); $twig = $this->createMock(Environment::class); $logger = $this->createMock(LoggerInterface::class); $twig->expects($this->once()) ->method('render') ->with('emails/dead_letter_alert.html.twig', $this->callback( static fn (array $params): bool => $params['eventType'] === stdClass::class && $params['retryCount'] === 7 && $params['lastError'] === 'SMTP timeout' && $params['transportName'] === 'async', )) ->willReturn('alert'); $mailer->expects($this->once()) ->method('send') ->with($this->callback( static fn (Email $email): bool => $email->getTo()[0]->getAddress() === 'admin@classeo.fr' && str_contains($email->getSubject() ?? '', 'dead-letter'), )); $handler = new DeadLetterAlertHandler($mailer, $twig, $logger, 'admin@classeo.fr'); $envelope = new Envelope(new stdClass(), [new RedeliveryStamp(7)]); $event = new WorkerMessageFailedEvent($envelope, 'async', new RuntimeException('SMTP timeout')); $handler($event); } #[Test] public function itDoesNotSendAlertWhenMessageWillRetry(): void { $mailer = $this->createMock(MailerInterface::class); $twig = $this->createMock(Environment::class); $logger = $this->createMock(LoggerInterface::class); $mailer->expects($this->never())->method('send'); $twig->expects($this->never())->method('render'); $handler = new DeadLetterAlertHandler($mailer, $twig, $logger, 'admin@classeo.fr'); $envelope = new Envelope(new stdClass(), [new RedeliveryStamp(1)]); $event = new WorkerMessageFailedEvent($envelope, 'async', new RuntimeException('SMTP timeout')); $event->setForRetry(); $handler($event); } #[Test] public function itLogsErrorWhenMailerFailsInsteadOfThrowing(): void { $mailer = $this->createMock(MailerInterface::class); $twig = $this->createMock(Environment::class); $logger = $this->createMock(LoggerInterface::class); $twig->method('render')->willReturn('alert'); $mailer->method('send')->willThrowException(new RuntimeException('SMTP connection refused')); $logger->expects($this->once()) ->method('error') ->with('Failed to send dead-letter alert email.', $this->callback( static fn (array $context): bool => $context['error'] === 'SMTP connection refused' && $context['messageType'] === stdClass::class, )); $handler = new DeadLetterAlertHandler($mailer, $twig, $logger, 'admin@classeo.fr'); $envelope = new Envelope(new stdClass(), [new RedeliveryStamp(7)]); $event = new WorkerMessageFailedEvent($envelope, 'async', new RuntimeException('Original error')); // Should NOT throw - the mailer failure is caught and logged $handler($event); } #[Test] public function itTruncatesLongErrorMessages(): void { $mailer = $this->createMock(MailerInterface::class); $twig = $this->createMock(Environment::class); $logger = $this->createMock(LoggerInterface::class); $longError = str_repeat('x', 1000); $twig->expects($this->once()) ->method('render') ->with('emails/dead_letter_alert.html.twig', $this->callback( static fn (array $params): bool => mb_strlen($params['lastError']) <= 500, )) ->willReturn('alert'); $mailer->method('send'); $handler = new DeadLetterAlertHandler($mailer, $twig, $logger, 'admin@classeo.fr'); $envelope = new Envelope(new stdClass(), [new RedeliveryStamp(7)]); $event = new WorkerMessageFailedEvent($envelope, 'async', new RuntimeException($longError)); $handler($event); } }