Enable Discovery debug logging for production troubleshooting
- Add DISCOVERY_LOG_LEVEL=debug - Add DISCOVERY_SHOW_PROGRESS=true - Temporary changes for debugging InitializerProcessor fixes on production
This commit is contained in:
@@ -0,0 +1,354 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Domain\Common\ValueObject\Email;
|
||||
use App\Framework\Mail\Commands\SendEmailBatchCommand;
|
||||
use App\Framework\Mail\Commands\SendEmailBatchCommandHandler;
|
||||
use App\Framework\Mail\EmailList;
|
||||
use App\Framework\Mail\Exceptions\SmtpException;
|
||||
use App\Framework\Mail\Message;
|
||||
use App\Framework\Mail\Testing\MockTransport;
|
||||
use App\Framework\Mail\TransportResult;
|
||||
|
||||
describe('SendEmailBatchCommandHandler', function () {
|
||||
beforeEach(function () {
|
||||
$this->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();
|
||||
}
|
||||
}
|
||||
231
tests/Framework/Mail/Commands/SendEmailCommandHandlerTest.php
Normal file
231
tests/Framework/Mail/Commands/SendEmailCommandHandlerTest.php
Normal file
@@ -0,0 +1,231 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Domain\Common\ValueObject\Email;
|
||||
use App\Framework\Logging\Logger;
|
||||
use App\Framework\Mail\Commands\SendEmailCommand;
|
||||
use App\Framework\Mail\Commands\SendEmailCommandHandler;
|
||||
use App\Framework\Mail\EmailList;
|
||||
use App\Framework\Mail\Exceptions\SmtpException;
|
||||
use App\Framework\Mail\Message;
|
||||
use App\Framework\Mail\Testing\MockTransport;
|
||||
use App\Framework\Mail\TransportResult;
|
||||
|
||||
describe('SendEmailCommandHandler', function () {
|
||||
beforeEach(function () {
|
||||
$this->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';
|
||||
}
|
||||
}
|
||||
175
tests/Framework/Mail/EmailListTest.php
Normal file
175
tests/Framework/Mail/EmailListTest.php
Normal file
@@ -0,0 +1,175 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Domain\Common\ValueObject\Email;
|
||||
use App\Framework\Mail\EmailList;
|
||||
|
||||
describe('EmailList', function () {
|
||||
it('creates empty email list', function () {
|
||||
$list = new EmailList();
|
||||
|
||||
expect($list->isEmpty())->toBeTrue();
|
||||
expect($list->isNotEmpty())->toBeFalse();
|
||||
expect($list->count())->toBe(0);
|
||||
expect($list->toArray())->toBeEmpty();
|
||||
});
|
||||
|
||||
it('creates email list with emails', function () {
|
||||
$email1 = new Email('user1@example.com');
|
||||
$email2 = new Email('user2@example.com');
|
||||
|
||||
$list = new EmailList($email1, $email2);
|
||||
|
||||
expect($list->isEmpty())->toBeFalse();
|
||||
expect($list->isNotEmpty())->toBeTrue();
|
||||
expect($list->count())->toBe(2);
|
||||
expect($list->toArray())->toHaveCount(2);
|
||||
expect($list->toArray()[0]->value)->toBe('user1@example.com');
|
||||
expect($list->toArray()[1]->value)->toBe('user2@example.com');
|
||||
});
|
||||
|
||||
it('creates email list from strings', function () {
|
||||
$list = new EmailList('user1@example.com', 'user2@example.com');
|
||||
|
||||
expect($list->count())->toBe(2);
|
||||
expect($list->toArray()[0]->value)->toBe('user1@example.com');
|
||||
expect($list->toArray()[1]->value)->toBe('user2@example.com');
|
||||
});
|
||||
|
||||
it('creates email list from array using fromArray', function () {
|
||||
$emails = [
|
||||
new Email('user1@example.com'),
|
||||
new Email('user2@example.com'),
|
||||
];
|
||||
|
||||
$list = EmailList::fromArray($emails);
|
||||
|
||||
expect($list->count())->toBe(2);
|
||||
expect($list->toArray())->toBe($emails);
|
||||
});
|
||||
|
||||
it('converts to string representation', function () {
|
||||
$list = new EmailList('user1@example.com', 'user2@example.com');
|
||||
|
||||
expect($list->toString())->toBe('user1@example.com, user2@example.com');
|
||||
});
|
||||
|
||||
it('handles single email in toString', function () {
|
||||
$list = new EmailList('single@example.com');
|
||||
|
||||
expect($list->toString())->toBe('single@example.com');
|
||||
});
|
||||
|
||||
it('handles empty list in toString', function () {
|
||||
$list = new EmailList();
|
||||
|
||||
expect($list->toString())->toBe('');
|
||||
});
|
||||
|
||||
it('adds email to list', function () {
|
||||
$list = new EmailList('existing@example.com');
|
||||
|
||||
$newList = $list->add('new@example.com');
|
||||
|
||||
expect($list->count())->toBe(1); // Original unchanged
|
||||
expect($newList->count())->toBe(2); // New list has both
|
||||
expect($newList->toArray()[1]->value)->toBe('new@example.com');
|
||||
});
|
||||
|
||||
it('merges two email lists', function () {
|
||||
$list1 = new EmailList('user1@example.com');
|
||||
$list2 = new EmailList('user2@example.com');
|
||||
|
||||
$merged = $list1->merge($list2);
|
||||
|
||||
expect($list1->count())->toBe(1); // Original unchanged
|
||||
expect($list2->count())->toBe(1); // Original unchanged
|
||||
expect($merged->count())->toBe(2);
|
||||
expect($merged->toArray()[0]->value)->toBe('user1@example.com');
|
||||
expect($merged->toArray()[1]->value)->toBe('user2@example.com');
|
||||
});
|
||||
|
||||
it('removes duplicates', function () {
|
||||
$list = new EmailList('user@example.com', 'user@example.com', 'other@example.com');
|
||||
$uniqueList = $list->unique();
|
||||
|
||||
expect($list->count())->toBe(3); // Original unchanged
|
||||
expect($uniqueList->count())->toBe(2); // Duplicates removed
|
||||
expect($uniqueList->toArray()[0]->value)->toBe('user@example.com');
|
||||
expect($uniqueList->toArray()[1]->value)->toBe('other@example.com');
|
||||
});
|
||||
|
||||
it('filters emails with callback', function () {
|
||||
$list = new EmailList('user@gmail.com', 'admin@company.com', 'test@gmail.com');
|
||||
|
||||
$gmailList = $list->filter(fn (Email $email) => str_contains($email->value, 'gmail.com'));
|
||||
|
||||
expect($list->count())->toBe(3); // Original unchanged
|
||||
expect($gmailList->count())->toBe(2);
|
||||
expect($gmailList->toArray()[0]->value)->toBe('user@gmail.com');
|
||||
expect($gmailList->toArray()[1]->value)->toBe('test@gmail.com');
|
||||
});
|
||||
|
||||
it('is iterable', function () {
|
||||
$list = new EmailList('user1@example.com', 'user2@example.com');
|
||||
|
||||
$iteratedEmails = [];
|
||||
foreach ($list as $email) {
|
||||
$iteratedEmails[] = $email->value;
|
||||
}
|
||||
|
||||
expect($iteratedEmails)->toBe(['user1@example.com', 'user2@example.com']);
|
||||
});
|
||||
|
||||
it('is countable', function () {
|
||||
$list = new EmailList('user1@example.com', 'user2@example.com');
|
||||
|
||||
expect(count($list))->toBe(2);
|
||||
});
|
||||
|
||||
it('checks if email exists in list', function () {
|
||||
$email1 = new Email('existing@example.com');
|
||||
$list = new EmailList($email1);
|
||||
|
||||
expect($list->contains($email1))->toBeTrue();
|
||||
expect($list->contains('existing@example.com'))->toBeTrue();
|
||||
expect($list->contains('nonexistent@example.com'))->toBeFalse();
|
||||
});
|
||||
|
||||
it('gets first email', function () {
|
||||
$list = new EmailList('first@example.com', 'second@example.com');
|
||||
|
||||
expect($list->first()->value)->toBe('first@example.com');
|
||||
});
|
||||
|
||||
it('returns null for first email in empty list', function () {
|
||||
$list = new EmailList();
|
||||
|
||||
expect($list->first())->toBeNull();
|
||||
});
|
||||
|
||||
it('gets last email', function () {
|
||||
$list = new EmailList('first@example.com', 'last@example.com');
|
||||
|
||||
expect($list->last()->value)->toBe('last@example.com');
|
||||
});
|
||||
|
||||
it('returns null for last email in empty list', function () {
|
||||
$list = new EmailList();
|
||||
|
||||
expect($list->last())->toBeNull();
|
||||
});
|
||||
|
||||
it('converts to string array', function () {
|
||||
$list = new EmailList('user1@example.com', 'user2@example.com');
|
||||
|
||||
expect($list->toStringArray())->toBe(['user1@example.com', 'user2@example.com']);
|
||||
});
|
||||
|
||||
it('supports custom separator in toString', function () {
|
||||
$list = new EmailList('user1@example.com', 'user2@example.com');
|
||||
|
||||
expect($list->toString('; '))->toBe('user1@example.com; user2@example.com');
|
||||
});
|
||||
});
|
||||
345
tests/Framework/Mail/MailerTest.php
Normal file
345
tests/Framework/Mail/MailerTest.php
Normal file
@@ -0,0 +1,345 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Domain\Common\ValueObject\Email;
|
||||
use App\Framework\CommandBus\CommandBus;
|
||||
use App\Framework\Mail\Commands\SendEmailBatchCommand;
|
||||
use App\Framework\Mail\Commands\SendEmailCommand;
|
||||
use App\Framework\Mail\EmailList;
|
||||
use App\Framework\Mail\Mailer;
|
||||
use App\Framework\Mail\Message;
|
||||
use App\Framework\Mail\Testing\MockTransport;
|
||||
use App\Framework\Mail\TransportResult;
|
||||
use App\Framework\Queue\Queue;
|
||||
|
||||
describe('Mailer', function () {
|
||||
beforeEach(function () {
|
||||
$this->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;
|
||||
}
|
||||
}
|
||||
232
tests/Framework/Mail/MessageTest.php
Normal file
232
tests/Framework/Mail/MessageTest.php
Normal file
@@ -0,0 +1,232 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Domain\Common\ValueObject\Email;
|
||||
use App\Framework\Mail\EmailList;
|
||||
use App\Framework\Mail\Message;
|
||||
use App\Framework\Mail\Priority;
|
||||
|
||||
describe('Message', function () {
|
||||
it('creates a basic message with required fields', function () {
|
||||
$from = new Email('sender@example.com');
|
||||
$to = new EmailList(new Email('recipient@example.com'));
|
||||
$subject = 'Test Subject';
|
||||
$body = 'Test body content';
|
||||
|
||||
$message = new Message(
|
||||
from: $from,
|
||||
subject: $subject,
|
||||
body: $body,
|
||||
to: $to
|
||||
);
|
||||
|
||||
expect($message->from)->toBe($from);
|
||||
expect($message->subject)->toBe($subject);
|
||||
expect($message->body)->toBe($body);
|
||||
expect($message->to)->toBe($to);
|
||||
expect($message->priority)->toBe(Priority::NORMAL);
|
||||
expect($message->cc->isEmpty())->toBeTrue();
|
||||
expect($message->bcc->isEmpty())->toBeTrue();
|
||||
expect($message->attachments)->toBeEmpty();
|
||||
expect($message->headers)->toBeEmpty();
|
||||
expect($message->replyTo)->toBeNull();
|
||||
});
|
||||
|
||||
it('creates a message with HTML body', function () {
|
||||
$message = new Message(
|
||||
from: new Email('sender@example.com'),
|
||||
subject: 'Test',
|
||||
htmlBody: '<h1>HTML Content</h1>',
|
||||
to: new EmailList(new Email('recipient@example.com'))
|
||||
);
|
||||
|
||||
expect($message->htmlBody)->toBe('<h1>HTML Content</h1>');
|
||||
expect($message->body)->toBe('');
|
||||
expect($message->hasHtmlBody())->toBeTrue();
|
||||
});
|
||||
|
||||
it('creates a message with both text and HTML body', function () {
|
||||
$message = new Message(
|
||||
from: new Email('sender@example.com'),
|
||||
subject: 'Test',
|
||||
body: 'Plain text',
|
||||
htmlBody: '<p>HTML content</p>',
|
||||
to: new EmailList(new Email('recipient@example.com'))
|
||||
);
|
||||
|
||||
expect($message->body)->toBe('Plain text');
|
||||
expect($message->htmlBody)->toBe('<p>HTML content</p>');
|
||||
expect($message->hasHtmlBody())->toBeTrue();
|
||||
});
|
||||
|
||||
it('throws exception when both body and HTML body are empty', function () {
|
||||
expect(fn () => new Message(
|
||||
from: new Email('sender@example.com'),
|
||||
subject: 'Test',
|
||||
to: new EmailList(new Email('recipient@example.com'))
|
||||
))->toThrow(InvalidArgumentException::class, 'Either body or HTML body is required');
|
||||
});
|
||||
|
||||
it('throws exception when to list is empty', function () {
|
||||
expect(fn () => new Message(
|
||||
from: new Email('sender@example.com'),
|
||||
subject: 'Test',
|
||||
body: 'Test body',
|
||||
to: new EmailList()
|
||||
))->toThrow(InvalidArgumentException::class, 'At least one recipient is required');
|
||||
});
|
||||
|
||||
it('creates message with CC and BCC recipients', function () {
|
||||
$cc = new EmailList(new Email('cc@example.com'));
|
||||
$bcc = new EmailList(new Email('bcc@example.com'));
|
||||
|
||||
$message = new Message(
|
||||
from: new Email('sender@example.com'),
|
||||
subject: 'Test',
|
||||
body: 'Test body',
|
||||
to: new EmailList(new Email('recipient@example.com')),
|
||||
cc: $cc,
|
||||
bcc: $bcc
|
||||
);
|
||||
|
||||
expect($message->cc)->toBe($cc);
|
||||
expect($message->bcc)->toBe($bcc);
|
||||
});
|
||||
|
||||
it('creates message with high priority', function () {
|
||||
$message = new Message(
|
||||
from: new Email('sender@example.com'),
|
||||
subject: 'Urgent',
|
||||
body: 'Urgent message',
|
||||
to: new EmailList(new Email('recipient@example.com')),
|
||||
priority: Priority::HIGH
|
||||
);
|
||||
|
||||
expect($message->priority)->toBe(Priority::HIGH);
|
||||
});
|
||||
|
||||
it('creates message with custom headers', function () {
|
||||
$headers = ['X-Custom-Header' => 'custom-value'];
|
||||
|
||||
$message = new Message(
|
||||
from: new Email('sender@example.com'),
|
||||
subject: 'Test',
|
||||
body: 'Test body',
|
||||
to: new EmailList(new Email('recipient@example.com')),
|
||||
headers: $headers
|
||||
);
|
||||
|
||||
expect($message->headers)->toBe($headers);
|
||||
});
|
||||
|
||||
it('creates message with reply-to address', function () {
|
||||
$replyTo = new Email('noreply@example.com');
|
||||
|
||||
$message = new Message(
|
||||
from: new Email('sender@example.com'),
|
||||
subject: 'Test',
|
||||
body: 'Test body',
|
||||
to: new EmailList(new Email('recipient@example.com')),
|
||||
replyTo: $replyTo
|
||||
);
|
||||
|
||||
expect($message->replyTo)->toBe($replyTo);
|
||||
});
|
||||
|
||||
it('provides fluent withTo method', function () {
|
||||
$message = new Message(
|
||||
from: new Email('sender@example.com'),
|
||||
subject: 'Test',
|
||||
body: 'Test body',
|
||||
to: new EmailList(new Email('original@example.com'))
|
||||
);
|
||||
|
||||
$newMessage = $message->withTo(new Email('new@example.com'));
|
||||
|
||||
expect($newMessage)->not->toBe($message);
|
||||
expect($newMessage->to->toString())->toBe('new@example.com');
|
||||
expect($message->to->toString())->toBe('original@example.com');
|
||||
});
|
||||
|
||||
it('provides fluent withCc method', function () {
|
||||
$message = new Message(
|
||||
from: new Email('sender@example.com'),
|
||||
subject: 'Test',
|
||||
body: 'Test body',
|
||||
to: new EmailList(new Email('recipient@example.com'))
|
||||
);
|
||||
|
||||
$newMessage = $message->withCc(new Email('cc@example.com'));
|
||||
|
||||
expect($newMessage)->not->toBe($message);
|
||||
expect($newMessage->cc->toString())->toBe('cc@example.com');
|
||||
expect($message->cc->isEmpty())->toBeTrue();
|
||||
});
|
||||
|
||||
it('provides fluent withBcc method', function () {
|
||||
$message = new Message(
|
||||
from: new Email('sender@example.com'),
|
||||
subject: 'Test',
|
||||
body: 'Test body',
|
||||
to: new EmailList(new Email('recipient@example.com'))
|
||||
);
|
||||
|
||||
$newMessage = $message->withBcc(new Email('bcc@example.com'));
|
||||
|
||||
expect($newMessage)->not->toBe($message);
|
||||
expect($newMessage->bcc->toString())->toBe('bcc@example.com');
|
||||
expect($message->bcc->isEmpty())->toBeTrue();
|
||||
});
|
||||
|
||||
it('provides fluent withSubject method', function () {
|
||||
$message = new Message(
|
||||
from: new Email('sender@example.com'),
|
||||
subject: 'Original Subject',
|
||||
body: 'Test body',
|
||||
to: new EmailList(new Email('recipient@example.com'))
|
||||
);
|
||||
|
||||
$newMessage = $message->withSubject('New Subject');
|
||||
|
||||
expect($newMessage)->not->toBe($message);
|
||||
expect($newMessage->subject)->toBe('New Subject');
|
||||
expect($message->subject)->toBe('Original Subject');
|
||||
});
|
||||
|
||||
it('gets all recipients (to, cc, bcc)', function () {
|
||||
$to = EmailList::fromArray([new Email('to@example.com')]);
|
||||
$cc = new EmailList(new Email('cc@example.com'));
|
||||
$bcc = new EmailList(new Email('bcc@example.com'));
|
||||
|
||||
$message = new Message(
|
||||
from: new Email('sender@example.com'),
|
||||
subject: 'Test',
|
||||
body: 'Test body',
|
||||
to: $to,
|
||||
cc: $cc,
|
||||
bcc: $bcc
|
||||
);
|
||||
|
||||
$allRecipients = $message->getAllRecipients();
|
||||
|
||||
expect($allRecipients->count())->toBe(3);
|
||||
expect($allRecipients->contains(new Email('to@example.com')))->toBeTrue();
|
||||
expect($allRecipients->contains(new Email('cc@example.com')))->toBeTrue();
|
||||
expect($allRecipients->contains(new Email('bcc@example.com')))->toBeTrue();
|
||||
});
|
||||
|
||||
it('detects if message has attachments', function () {
|
||||
$messageWithoutAttachments = new Message(
|
||||
from: new Email('sender@example.com'),
|
||||
subject: 'Test',
|
||||
body: 'Test body',
|
||||
to: new EmailList(new Email('recipient@example.com'))
|
||||
);
|
||||
|
||||
expect($messageWithoutAttachments->hasAttachments())->toBeFalse();
|
||||
|
||||
// Note: We can't easily test with real attachments without mocking the Storage and file system
|
||||
// This would be covered in integration tests
|
||||
});
|
||||
});
|
||||
272
tests/Framework/Mail/Testing/MockTransportTest.php
Normal file
272
tests/Framework/Mail/Testing/MockTransportTest.php
Normal file
@@ -0,0 +1,272 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Domain\Common\ValueObject\Email;
|
||||
use App\Framework\Mail\EmailList;
|
||||
use App\Framework\Mail\Message;
|
||||
use App\Framework\Mail\Testing\MockTransport;
|
||||
|
||||
describe('MockTransport', function () {
|
||||
beforeEach(function () {
|
||||
$this->transport = new MockTransport();
|
||||
$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('basic functionality', function () {
|
||||
it('sends message successfully by default', function () {
|
||||
$result = $this->transport->send($this->message);
|
||||
|
||||
expect($result->isSuccess())->toBeTrue();
|
||||
expect($result->getMessageId())->toStartWith('mock_');
|
||||
expect($this->transport->getSentMessageCount())->toBe(1);
|
||||
});
|
||||
|
||||
it('is available by default', function () {
|
||||
expect($this->transport->isAvailable())->toBeTrue();
|
||||
});
|
||||
|
||||
it('returns correct name', function () {
|
||||
expect($this->transport->getName())->toBe('Mock Transport');
|
||||
});
|
||||
});
|
||||
|
||||
describe('failure simulation', function () {
|
||||
it('can be configured to fail', function () {
|
||||
$this->transport->setShouldFail(true, 'Custom failure message');
|
||||
|
||||
$result = $this->transport->send($this->message);
|
||||
|
||||
expect($result->isFailure())->toBeTrue();
|
||||
expect($result->getError())->toBe('Custom failure message');
|
||||
expect($this->transport->getSentMessageCount())->toBe(0);
|
||||
});
|
||||
|
||||
it('uses default failure message when none provided', function () {
|
||||
$this->transport->setShouldFail(true);
|
||||
|
||||
$result = $this->transport->send($this->message);
|
||||
|
||||
expect($result->getError())->toBe('Mock transport failure');
|
||||
});
|
||||
|
||||
it('can be reset to success mode', function () {
|
||||
$this->transport->setShouldFail(true);
|
||||
$this->transport->setShouldFail(false);
|
||||
|
||||
$result = $this->transport->send($this->message);
|
||||
|
||||
expect($result->isSuccess())->toBeTrue();
|
||||
});
|
||||
});
|
||||
|
||||
describe('availability simulation', function () {
|
||||
it('can be configured as unavailable', function () {
|
||||
$this->transport->setIsAvailable(false);
|
||||
|
||||
expect($this->transport->isAvailable())->toBeFalse();
|
||||
});
|
||||
|
||||
it('can be reset to available', function () {
|
||||
$this->transport->setIsAvailable(false);
|
||||
$this->transport->setIsAvailable(true);
|
||||
|
||||
expect($this->transport->isAvailable())->toBeTrue();
|
||||
});
|
||||
});
|
||||
|
||||
describe('sent message tracking', function () {
|
||||
it('tracks sent messages', function () {
|
||||
$message1 = $this->message;
|
||||
$message2 = new Message(
|
||||
from: new Email('sender@example.com'),
|
||||
subject: 'Second Message',
|
||||
body: 'Second body',
|
||||
to: new EmailList(new Email('recipient2@example.com'))
|
||||
);
|
||||
|
||||
$this->transport->send($message1);
|
||||
$this->transport->send($message2);
|
||||
|
||||
$sentMessages = $this->transport->getSentMessages();
|
||||
|
||||
expect($sentMessages)->toHaveCount(2);
|
||||
expect($sentMessages[0]['message'])->toBe($message1);
|
||||
expect($sentMessages[1]['message'])->toBe($message2);
|
||||
expect($sentMessages[0]['sent_at'])->toBeInstanceOf(DateTimeImmutable::class);
|
||||
expect($sentMessages[1]['sent_at'])->toBeInstanceOf(DateTimeImmutable::class);
|
||||
});
|
||||
|
||||
it('gets last sent message', function () {
|
||||
$this->transport->send($this->message);
|
||||
|
||||
$lastMessage = $this->transport->getLastSentMessage();
|
||||
|
||||
expect($lastMessage['message'])->toBe($this->message);
|
||||
expect($lastMessage['message_id'])->toStartWith('mock_');
|
||||
});
|
||||
|
||||
it('returns null for last message when none sent', function () {
|
||||
$lastMessage = $this->transport->getLastSentMessage();
|
||||
|
||||
expect($lastMessage)->toBeNull();
|
||||
});
|
||||
|
||||
it('clears sent messages', function () {
|
||||
$this->transport->send($this->message);
|
||||
expect($this->transport->getSentMessageCount())->toBe(1);
|
||||
|
||||
$this->transport->clearSentMessages();
|
||||
|
||||
expect($this->transport->getSentMessageCount())->toBe(0);
|
||||
expect($this->transport->getSentMessages())->toBeEmpty();
|
||||
});
|
||||
});
|
||||
|
||||
describe('recipient checking', function () {
|
||||
it('checks if message was sent to specific email', function () {
|
||||
$this->transport->send($this->message);
|
||||
|
||||
expect($this->transport->wasSentTo('recipient@example.com'))->toBeTrue();
|
||||
expect($this->transport->wasSentTo('other@example.com'))->toBeFalse();
|
||||
});
|
||||
|
||||
it('gets messages sent to specific email', function () {
|
||||
$message1 = new Message(
|
||||
from: new Email('sender@example.com'),
|
||||
subject: 'Message 1',
|
||||
body: 'Body 1',
|
||||
to: new EmailList(new Email('user1@example.com'))
|
||||
);
|
||||
|
||||
$message2 = new Message(
|
||||
from: new Email('sender@example.com'),
|
||||
subject: 'Message 2',
|
||||
body: 'Body 2',
|
||||
to: new EmailList(new Email('user2@example.com'))
|
||||
);
|
||||
|
||||
$message3 = new Message(
|
||||
from: new Email('sender@example.com'),
|
||||
subject: 'Message 3',
|
||||
body: 'Body 3',
|
||||
to: new EmailList(new Email('user1@example.com'))
|
||||
);
|
||||
|
||||
$this->transport->send($message1);
|
||||
$this->transport->send($message2);
|
||||
$this->transport->send($message3);
|
||||
|
||||
$messagesForUser1 = $this->transport->getSentMessagesTo('user1@example.com');
|
||||
|
||||
expect($messagesForUser1)->toHaveCount(2);
|
||||
expect($messagesForUser1[0]['message'])->toBe($message1);
|
||||
expect($messagesForUser1[1]['message'])->toBe($message3);
|
||||
});
|
||||
|
||||
it('handles messages with multiple recipients', function () {
|
||||
$multiRecipientMessage = new Message(
|
||||
from: new Email('sender@example.com'),
|
||||
subject: 'Multi-recipient',
|
||||
body: 'Body',
|
||||
to: EmailList::fromArray([
|
||||
new Email('user1@example.com'),
|
||||
new Email('user2@example.com'),
|
||||
])
|
||||
);
|
||||
|
||||
$this->transport->send($multiRecipientMessage);
|
||||
|
||||
expect($this->transport->wasSentTo('user1@example.com'))->toBeTrue();
|
||||
expect($this->transport->wasSentTo('user2@example.com'))->toBeTrue();
|
||||
expect($this->transport->wasSentTo('user3@example.com'))->toBeFalse();
|
||||
});
|
||||
});
|
||||
|
||||
describe('subject-based searching', function () {
|
||||
it('finds message by subject', function () {
|
||||
$message1 = new Message(
|
||||
from: new Email('sender@example.com'),
|
||||
subject: 'Welcome Email',
|
||||
body: 'Welcome!',
|
||||
to: new EmailList(new Email('user@example.com'))
|
||||
);
|
||||
|
||||
$message2 = new Message(
|
||||
from: new Email('sender@example.com'),
|
||||
subject: 'Password Reset',
|
||||
body: 'Reset your password',
|
||||
to: new EmailList(new Email('user@example.com'))
|
||||
);
|
||||
|
||||
$this->transport->send($message1);
|
||||
$this->transport->send($message2);
|
||||
|
||||
$welcomeMessage = $this->transport->getSentMessageWithSubject('Welcome Email');
|
||||
$resetMessage = $this->transport->getSentMessageWithSubject('Password Reset');
|
||||
$nonExistent = $this->transport->getSentMessageWithSubject('Non-existent');
|
||||
|
||||
expect($welcomeMessage['message'])->toBe($message1);
|
||||
expect($resetMessage['message'])->toBe($message2);
|
||||
expect($nonExistent)->toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('batch sending', function () {
|
||||
it('sends batch of valid messages', function () {
|
||||
$messages = [
|
||||
new Message(
|
||||
from: new Email('sender@example.com'),
|
||||
subject: 'Message 1',
|
||||
body: 'Body 1',
|
||||
to: new EmailList(new Email('user1@example.com'))
|
||||
),
|
||||
new Message(
|
||||
from: new Email('sender@example.com'),
|
||||
subject: 'Message 2',
|
||||
body: 'Body 2',
|
||||
to: new EmailList(new Email('user2@example.com'))
|
||||
),
|
||||
];
|
||||
|
||||
$results = $this->transport->sendBatch($messages);
|
||||
|
||||
expect($results)->toHaveCount(2);
|
||||
expect($results[0]->isSuccess())->toBeTrue();
|
||||
expect($results[1]->isSuccess())->toBeTrue();
|
||||
expect($this->transport->getSentMessageCount())->toBe(2);
|
||||
});
|
||||
|
||||
it('handles invalid messages in batch', function () {
|
||||
$batch = [
|
||||
new Message(
|
||||
from: new Email('sender@example.com'),
|
||||
subject: 'Valid Message',
|
||||
body: 'Body',
|
||||
to: new EmailList(new Email('user@example.com'))
|
||||
),
|
||||
'invalid-message',
|
||||
new Message(
|
||||
from: new Email('sender@example.com'),
|
||||
subject: 'Another Valid Message',
|
||||
body: 'Body',
|
||||
to: new EmailList(new Email('user2@example.com'))
|
||||
),
|
||||
];
|
||||
|
||||
$results = $this->transport->sendBatch($batch);
|
||||
|
||||
expect($results)->toHaveCount(3);
|
||||
expect($results[0]->isSuccess())->toBeTrue();
|
||||
expect($results[1]->isFailure())->toBeTrue();
|
||||
expect($results[1]->getError())->toBe('Invalid message at index 1');
|
||||
expect($results[2]->isSuccess())->toBeTrue();
|
||||
expect($this->transport->getSentMessageCount())->toBe(2);
|
||||
});
|
||||
});
|
||||
});
|
||||
90
tests/Framework/Mail/TransportResultTest.php
Normal file
90
tests/Framework/Mail/TransportResultTest.php
Normal file
@@ -0,0 +1,90 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Framework\Mail\TransportResult;
|
||||
|
||||
describe('TransportResult', function () {
|
||||
it('creates successful result', function () {
|
||||
$messageId = 'msg_123';
|
||||
$result = TransportResult::success($messageId);
|
||||
|
||||
expect($result->isSuccess())->toBeTrue();
|
||||
expect($result->isFailure())->toBeFalse();
|
||||
expect($result->getMessageId())->toBe($messageId);
|
||||
// Cannot call getError() on success result - it throws exception
|
||||
expect($result->getMetadata())->toBeEmpty();
|
||||
});
|
||||
|
||||
it('creates successful result with metadata', function () {
|
||||
$messageId = 'msg_123';
|
||||
$metadata = ['server' => 'smtp.example.com', 'time' => 1.23];
|
||||
$result = TransportResult::success($messageId, $metadata);
|
||||
|
||||
expect($result->isSuccess())->toBeTrue();
|
||||
expect($result->getMessageId())->toBe($messageId);
|
||||
expect($result->getMetadata())->toBe($metadata);
|
||||
});
|
||||
|
||||
it('creates failure result', function () {
|
||||
$error = 'SMTP connection failed';
|
||||
$result = TransportResult::failure($error);
|
||||
|
||||
expect($result->isFailure())->toBeTrue();
|
||||
expect($result->isSuccess())->toBeFalse();
|
||||
expect($result->getError())->toBe($error);
|
||||
// Cannot call getMessageId() on failure result - it throws exception
|
||||
expect($result->getMetadata())->toBeEmpty();
|
||||
});
|
||||
|
||||
it('creates failure result with metadata', function () {
|
||||
$error = 'Authentication failed';
|
||||
$metadata = ['smtp_code' => 535, 'server_response' => 'Invalid credentials'];
|
||||
$result = TransportResult::failure($error, $metadata);
|
||||
|
||||
expect($result->isFailure())->toBeTrue();
|
||||
expect($result->getError())->toBe($error);
|
||||
expect($result->getMetadata())->toBe($metadata);
|
||||
});
|
||||
|
||||
it('handles empty message ID in success result', function () {
|
||||
$result = TransportResult::success('');
|
||||
|
||||
expect($result->isSuccess())->toBeTrue();
|
||||
expect($result->getMessageId())->toBe('');
|
||||
});
|
||||
|
||||
it('handles empty error in failure result', function () {
|
||||
$result = TransportResult::failure('');
|
||||
|
||||
expect($result->isFailure())->toBeTrue();
|
||||
expect($result->getError())->toBe('');
|
||||
});
|
||||
|
||||
it('success and failure are mutually exclusive', function () {
|
||||
$successResult = TransportResult::success('msg_123');
|
||||
$failureResult = TransportResult::failure('error');
|
||||
|
||||
expect($successResult->isSuccess())->toBeTrue();
|
||||
expect($successResult->isFailure())->toBeFalse();
|
||||
|
||||
expect($failureResult->isSuccess())->toBeFalse();
|
||||
expect($failureResult->isFailure())->toBeTrue();
|
||||
});
|
||||
|
||||
it('preserves metadata correctly', function () {
|
||||
$metadata = [
|
||||
'server' => 'smtp.gmail.com',
|
||||
'port' => 587,
|
||||
'encryption' => 'tls',
|
||||
'auth_method' => 'login',
|
||||
'response_time' => 0.456,
|
||||
];
|
||||
|
||||
$successResult = TransportResult::success('msg_123', $metadata);
|
||||
$failureResult = TransportResult::failure('connection failed', $metadata);
|
||||
|
||||
expect($successResult->getMetadata())->toBe($metadata);
|
||||
expect($failureResult->getMetadata())->toBe($metadata);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user