transport = new MockTransport(); $this->logger = new MailTestLogger(); $this->handler = new SendEmailCommandHandler($this->transport, $this->logger); $this->message = new Message( from: new Email('sender@example.com'), subject: 'Test Subject', body: 'Test body', to: new EmailList(new Email('recipient@example.com')) ); $this->command = new SendEmailCommand($this->message); }); it('handles successful email sending', function () { $this->handler->handle($this->command); expect($this->transport->getSentMessageCount())->toBe(1); expect($this->transport->getLastSentMessage()['message'])->toBe($this->message); expect($this->logger->hasInfo('Sending email via queue'))->toBeTrue(); expect($this->logger->hasInfo('Email sent successfully'))->toBeTrue(); }); it('logs transport failure and throws SmtpException', function () { $this->transport->setShouldFail(true, 'SMTP connection failed'); expect(fn () => $this->handler->handle($this->command)) ->toThrow(SmtpException::class, 'Failed to send email: SMTP connection failed'); expect($this->logger->hasInfo('Sending email via queue'))->toBeTrue(); expect($this->logger->hasError('Email sending failed'))->toBeTrue(); }); it('logs and wraps unexpected exceptions', function () { // Create a transport that throws unexpected exception $failingTransport = new FailingTransport('Unexpected error'); $handler = new SendEmailCommandHandler($failingTransport, $this->logger); expect(fn () => $handler->handle($this->command)) ->toThrow(SmtpException::class, 'Unexpected error while sending email: Unexpected error'); expect($this->logger->hasInfo('Sending email via queue'))->toBeTrue(); expect($this->logger->hasError('Unexpected exception during email sending'))->toBeTrue(); }); it('re-throws SmtpException for retry logic', function () { // Create a transport that throws SmtpException $failingTransport = new SmtpFailingTransport(); $handler = new SendEmailCommandHandler($failingTransport, $this->logger); expect(fn () => $handler->handle($this->command)) ->toThrow(SmtpException::class, 'Failed to connect to SMTP server localhost:587: Connection refused'); expect($this->logger->hasInfo('Sending email via queue'))->toBeTrue(); expect($this->logger->hasError('SMTP exception during email sending'))->toBeTrue(); }); it('includes transport name in logs', function () { $this->handler->handle($this->command); $logs = $this->logger->getLogs(); expect($logs[0]['context']['transport'])->toBe('Mock Transport'); expect($logs[1]['context']['transport'])->toBe('Mock Transport'); }); it('includes message details in logs', function () { $this->handler->handle($this->command); $logs = $this->logger->getLogs(); expect($logs[0]['context']['to'])->toBe('recipient@example.com'); expect($logs[0]['context']['subject'])->toBe('Test Subject'); expect($logs[1]['context']['to'])->toBe('recipient@example.com'); expect($logs[1]['context']['subject'])->toBe('Test Subject'); expect($logs[1]['context'])->toHaveKey('message_id'); }); it('includes error details in failure logs', function () { $this->transport->setShouldFail(true, 'Custom error message'); expect(fn () => $this->handler->handle($this->command)) ->toThrow(SmtpException::class); $logs = $this->logger->getLogs(); $errorLog = $logs[1]; // Second log should be the error expect($errorLog['context']['error'])->toBe('Custom error message'); expect($errorLog['context']['to'])->toBe('recipient@example.com'); expect($errorLog['context']['subject'])->toBe('Test Subject'); expect($errorLog['context'])->toHaveKey('metadata'); }); }); // Test stub for mail handler class MailTestLogger implements Logger { private array $logs = []; public function emergency(string $message, array $context = []): void { $this->logs[] = ['level' => 'emergency', 'message' => $message, 'context' => $context]; } public function alert(string $message, array $context = []): void { $this->logs[] = ['level' => 'alert', 'message' => $message, 'context' => $context]; } public function critical(string $message, array $context = []): void { $this->logs[] = ['level' => 'critical', 'message' => $message, 'context' => $context]; } public function error(string $message, array $context = []): void { $this->logs[] = ['level' => 'error', 'message' => $message, 'context' => $context]; } public function warning(string $message, array $context = []): void { $this->logs[] = ['level' => 'warning', 'message' => $message, 'context' => $context]; } public function notice(string $message, array $context = []): void { $this->logs[] = ['level' => 'notice', 'message' => $message, 'context' => $context]; } public function info(string $message, array $context = []): void { $this->logs[] = ['level' => 'info', 'message' => $message, 'context' => $context]; } public function debug(string $message, array $context = []): void { $this->logs[] = ['level' => 'debug', 'message' => $message, 'context' => $context]; } public function getLogs(): array { return $this->logs; } public function hasInfo(string $message): bool { return $this->hasLevel('info', $message); } public function hasError(string $message): bool { return $this->hasLevel('error', $message); } public function hasWarning(string $message): bool { return $this->hasLevel('warning', $message); } public function hasDebug(string $message): bool { return $this->hasLevel('debug', $message); } private function hasLevel(string $level, string $message): bool { foreach ($this->logs as $log) { if ($log['level'] === $level && $log['message'] === $message) { return true; } } return false; } } // Test transport classes class FailingTransport implements \App\Framework\Mail\TransportInterface { public function __construct(private string $errorMessage) { } public function send(Message $message): TransportResult { throw new \RuntimeException($this->errorMessage); } public function isAvailable(): bool { return true; } public function getName(): string { return 'Failing Transport'; } } class SmtpFailingTransport implements \App\Framework\Mail\TransportInterface { public function send(Message $message): TransportResult { throw SmtpException::connectionFailed('localhost', 587, 'Connection refused'); } public function isAvailable(): bool { return true; } public function getName(): string { return 'SMTP Failing Transport'; } }