transport = new MockTransport(); $this->queue = new MailTestQueue(); $this->commandBus = new TestCommandBus(); $this->mailer = new Mailer($this->transport, $this->queue, $this->commandBus); $this->message = new Message( from: new Email('sender@example.com'), subject: 'Test Subject', body: 'Test body', to: new EmailList(new Email('recipient@example.com')) ); }); describe('send method', function () { it('sends email successfully using transport', function () { $result = $this->mailer->send($this->message); expect($result)->toBeTrue(); expect($this->transport->getSentMessageCount())->toBe(1); expect($this->transport->getLastSentMessage()['message'])->toBe($this->message); }); it('returns false when transport fails', function () { $this->transport->setShouldFail(true, 'Transport error'); $result = $this->mailer->send($this->message); expect($result)->toBeFalse(); expect($this->transport->getSentMessageCount())->toBe(0); }); it('does not use queue for synchronous sending', function () { $this->mailer->send($this->message); expect($this->queue->wasUsed())->toBeFalse(); expect($this->commandBus->wasUsed())->toBeFalse(); }); }); describe('queue method', function () { it('dispatches SendEmailCommand with default parameters', function () { $result = $this->mailer->queue($this->message); expect($result)->toBeTrue(); expect($this->commandBus->wasUsed())->toBeTrue(); $command = $this->commandBus->getLastCommand(); expect($command)->toBeInstanceOf(SendEmailCommand::class); expect($command->message)->toBe($this->message); expect($command->maxRetries)->toBe(3); expect($command->delaySeconds)->toBe(0); }); it('dispatches SendEmailCommand with custom parameters', function () { $result = $this->mailer->queue($this->message, 5, 30); expect($result)->toBeTrue(); expect($this->commandBus->wasUsed())->toBeTrue(); $command = $this->commandBus->getLastCommand(); expect($command)->toBeInstanceOf(SendEmailCommand::class); expect($command->message)->toBe($this->message); expect($command->maxRetries)->toBe(5); expect($command->delaySeconds)->toBe(30); }); it('returns false when command dispatch fails', function () { $this->commandBus->setShouldFail(true); $result = $this->mailer->queue($this->message); expect($result)->toBeFalse(); }); it('does not use transport for queued sending', function () { $this->mailer->queue($this->message); expect($this->transport->getSentMessageCount())->toBe(0); }); }); describe('sendBatch method', function () { beforeEach(function () { $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')) ), ]; }); it('sends all messages successfully', function () { $results = $this->mailer->sendBatch($this->messages); expect($results)->toHaveCount(2); expect($results[0])->toBeTrue(); expect($results[1])->toBeTrue(); expect($this->transport->getSentMessageCount())->toBe(2); }); it('handles mixed success and failure', function () { // Make transport fail on second call $transport = new TestTransport(); $transport->failOn(2); $mailer = new Mailer($transport, $this->queue, $this->commandBus); $results = $mailer->sendBatch($this->messages); expect($results)->toHaveCount(2); expect($results[0])->toBeTrue(); expect($results[1])->toBeFalse(); }); it('handles invalid messages in batch', function () { $mixedBatch = [ $this->messages[0], 'invalid-message', $this->messages[1], ]; $results = $this->mailer->sendBatch($mixedBatch); expect($results)->toHaveCount(3); expect($results[0])->toBeTrue(); expect($results[1])->toBeFalse(); expect($results[2])->toBeTrue(); }); }); describe('queueBatch method', function () { beforeEach(function () { $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')) ), ]; }); it('dispatches SendEmailBatchCommand for valid messages', function () { $results = $this->mailer->queueBatch($this->messages); expect($results)->toHaveCount(2); expect($results[0])->toBeTrue(); expect($results[1])->toBeTrue(); expect($this->commandBus->wasUsed())->toBeTrue(); $command = $this->commandBus->getLastCommand(); expect($command)->toBeInstanceOf(SendEmailBatchCommand::class); expect($command->messages)->toBe($this->messages); expect($command->maxRetries)->toBe(3); expect($command->delaySeconds)->toBe(0); }); it('handles mixed valid and invalid messages', function () { $mixedBatch = [ $this->messages[0], 'invalid-message', $this->messages[1], ]; $results = $this->mailer->queueBatch($mixedBatch); expect($results)->toHaveCount(3); expect($results[0])->toBeTrue(); // Valid message expect($results[1])->toBeFalse(); // Invalid message expect($results[2])->toBeTrue(); // Valid message $command = $this->commandBus->getLastCommand(); expect($command->messages)->toHaveCount(2); }); it('returns early for all invalid messages', function () { $invalidBatch = ['invalid1', 'invalid2']; $results = $this->mailer->queueBatch($invalidBatch); expect($results)->toHaveCount(2); expect($results[0])->toBeFalse(); expect($results[1])->toBeFalse(); expect($this->commandBus->wasUsed())->toBeFalse(); }); it('marks all valid messages as failed when batch command fails', function () { $this->commandBus->setShouldFail(true); $results = $this->mailer->queueBatch($this->messages); expect($results)->toHaveCount(2); expect($results[0])->toBeFalse(); expect($results[1])->toBeFalse(); }); it('uses custom retry parameters', function () { $this->mailer->queueBatch($this->messages, 5, 60); $command = $this->commandBus->getLastCommand(); expect($command->maxRetries)->toBe(5); expect($command->delaySeconds)->toBe(60); }); }); }); // Test stubs class MailTestQueue implements Queue { private bool $used = false; private array $jobs = []; public function push(object $job): void { $this->used = true; $this->jobs[] = $job; } public function pop(): ?object { return array_shift($this->jobs); } public function wasUsed(): bool { return $this->used; } } class TestCommandBus implements CommandBus { private bool $used = false; private bool $shouldFail = false; private ?object $lastCommand = null; public function dispatch(object $command): mixed { $this->used = true; $this->lastCommand = $command; if ($this->shouldFail) { throw new Exception('Command dispatch failed'); } return null; } public function wasUsed(): bool { return $this->used; } public function getLastCommand(): ?object { return $this->lastCommand; } public function setShouldFail(bool $shouldFail): void { $this->shouldFail = $shouldFail; } } class TestTransport implements \App\Framework\Mail\TransportInterface { private MockTransport $mockTransport; private int $failOn = 0; private int $callCount = 0; public function __construct() { $this->mockTransport = new MockTransport(); } public function send(Message $message): TransportResult { $this->callCount++; if ($this->failOn > 0 && $this->callCount === $this->failOn) { return TransportResult::failure('Simulated failure'); } 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(); } public function getLastSentMessage(): ?array { return $this->mockTransport->getLastSentMessage(); } public function failOn(int $callNumber): void { $this->failOn = $callNumber; } }