transport = new MockTransport(); $this->logger = new BatchTestLogger(); $this->handler = new SendEmailBatchCommandHandler($this->transport, $this->logger); $this->messages = [ new Message( from: new Email('sender@example.com'), subject: 'Test 1', body: 'Body 1', to: new EmailList(new Email('recipient1@example.com')) ), new Message( from: new Email('sender@example.com'), subject: 'Test 2', body: 'Body 2', to: new EmailList(new Email('recipient2@example.com')) ), ]; $this->command = new SendEmailBatchCommand($this->messages); }); it('handles successful batch sending', function () { $this->handler->handle($this->command); expect($this->transport->getSentMessageCount())->toBe(2); expect($this->logger->hasInfo('Starting batch email sending'))->toBeTrue(); expect($this->logger->hasDebug('Batch email sent successfully'))->toBeTrue(); expect($this->logger->hasInfo('Batch email sending completed'))->toBeTrue(); }); it('handles partial batch failure (under 50%)', function () { // Create transport that fails on second message only $failingTransport = new PartialFailingTransport(); $handler = new SendEmailBatchCommandHandler($failingTransport, $this->logger); // Should not throw exception for partial failure $handler->handle($this->command); expect($failingTransport->getSentMessageCount())->toBe(1); expect($this->logger->hasInfo('Starting batch email sending'))->toBeTrue(); expect($this->logger->hasDebug('Batch email sent successfully'))->toBeTrue(); // First success expect($this->logger->hasWarning('Batch email failed'))->toBeTrue(); // Second failure expect($this->logger->hasInfo('Batch email sending completed'))->toBeTrue(); }); it('handles high failure rate (over 50%) with warning', function () { $this->transport->setShouldFail(true, 'All messages fail'); expect(fn () => $this->handler->handle($this->command)) ->toThrow(SmtpException::class, 'All 2 emails in batch failed to send'); expect($this->logger->hasInfo('Starting batch email sending'))->toBeTrue(); expect($this->logger->hasWarning('Batch email failed'))->toBeTrue(); // Note: High failure rate warning is not logged when all messages fail (exception is thrown before that) expect($this->logger->hasInfo('Batch email sending completed'))->toBeTrue(); }); it('throws exception when all messages fail', function () { $this->transport->setShouldFail(true, 'Transport unavailable'); expect(fn () => $this->handler->handle($this->command)) ->toThrow(SmtpException::class, 'All 2 emails in batch failed to send'); expect($this->logger->hasInfo('Starting batch email sending'))->toBeTrue(); expect($this->logger->hasWarning('Batch email failed'))->toBeTrue(); // Note: High failure rate warning is not logged when all messages fail (exception is thrown before that) }); it('handles transport exceptions in batch', function () { // Create transport that throws exception $failingTransport = new ExceptionThrowingTransport(); $handler = new SendEmailBatchCommandHandler($failingTransport, $this->logger); // Should not throw for partial failure $handler->handle($this->command); expect($failingTransport->getSentMessageCount())->toBe(1); expect($this->logger->hasInfo('Starting batch email sending'))->toBeTrue(); expect($this->logger->hasDebug('Batch email sent successfully'))->toBeTrue(); // First success expect($this->logger->hasError('Batch email exception'))->toBeTrue(); // Second exception }); it('logs detailed batch statistics', function () { // Mixed success/failure scenario $mixedTransport = new MixedResultTransport(); $handler = new SendEmailBatchCommandHandler($mixedTransport, $this->logger); $handler->handle($this->command); $logs = $this->logger->getLogs(); $completionLog = end($logs); // Last log should be completion expect($completionLog['context']['total_messages'])->toBe(2); expect($completionLog['context']['successful'])->toBe(1); expect($completionLog['context']['failed'])->toBe(1); expect($completionLog['context']['success_rate'])->toBe(50.0); }); it('includes transport name in logs', function () { $this->handler->handle($this->command); $logs = $this->logger->getLogs(); $startLog = $logs[0]; expect($startLog['context']['transport'])->toBe('Mock Transport'); }); it('logs individual message details', function () { $this->handler->handle($this->command); $logs = $this->logger->getLogs(); // Find debug logs (successful sends) $debugLogs = array_filter($logs, fn ($log) => $log['level'] === 'debug'); expect(count($debugLogs))->toBe(2); $firstDebug = array_values($debugLogs)[0]; expect($firstDebug['context']['batch_index'])->toBe(0); expect($firstDebug['context']['to'])->toBe('recipient1@example.com'); expect($firstDebug['context']['subject'])->toBe('Test 1'); $secondDebug = array_values($debugLogs)[1]; expect($secondDebug['context']['batch_index'])->toBe(1); expect($secondDebug['context']['to'])->toBe('recipient2@example.com'); expect($secondDebug['context']['subject'])->toBe('Test 2'); }); it('handles empty batch gracefully', function () { $emptyCommand = new SendEmailBatchCommand([]); $this->handler->handle($emptyCommand); expect($this->transport->getSentMessageCount())->toBe(0); expect($this->logger->hasInfo('Starting batch email sending'))->toBeTrue(); expect($this->logger->hasInfo('Batch email sending completed'))->toBeTrue(); $logs = $this->logger->getLogs(); $completionLog = end($logs); expect($completionLog['context']['total_messages'])->toBe(0); expect($completionLog['context']['successful'])->toBe(0); expect($completionLog['context']['failed'])->toBe(0); expect($completionLog['context']['success_rate'])->toBe(0); }); }); // Test stub for batch handler test class BatchTestLogger implements \App\Framework\Logging\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 for batch handler class PartialFailingTransport implements \App\Framework\Mail\TransportInterface { private MockTransport $mockTransport; private int $callCount = 0; public function __construct() { $this->mockTransport = new MockTransport(); } public function send(Message $message): TransportResult { $this->callCount++; if ($this->callCount === 2) { return TransportResult::failure('Second message failed'); } return $this->mockTransport->send($message); } public function isAvailable(): bool { return $this->mockTransport->isAvailable(); } public function getName(): string { return $this->mockTransport->getName(); } public function getSentMessageCount(): int { return $this->mockTransport->getSentMessageCount(); } } class ExceptionThrowingTransport implements \App\Framework\Mail\TransportInterface { private MockTransport $mockTransport; private int $callCount = 0; public function __construct() { $this->mockTransport = new MockTransport(); } public function send(Message $message): TransportResult { $this->callCount++; if ($this->callCount === 1) { return $this->mockTransport->send($message); // First succeeds } throw new \RuntimeException('Transport exception'); } public function isAvailable(): bool { return $this->mockTransport->isAvailable(); } public function getName(): string { return $this->mockTransport->getName(); } public function getSentMessageCount(): int { return $this->mockTransport->getSentMessageCount(); } } class MixedResultTransport implements \App\Framework\Mail\TransportInterface { private MockTransport $mockTransport; private int $callCount = 0; public function __construct() { $this->mockTransport = new MockTransport(); } public function send(Message $message): TransportResult { $this->callCount++; if ($this->callCount === 1) { return $this->mockTransport->send($message); // Success } return TransportResult::failure('Second fails'); // Failure } public function isAvailable(): bool { return $this->mockTransport->isAvailable(); } public function getName(): string { return $this->mockTransport->getName(); } public function getSentMessageCount(): int { return $this->mockTransport->getSentMessageCount(); } }