- Move 12 markdown files from root to docs/ subdirectories - Organize documentation by category: • docs/troubleshooting/ (1 file) - Technical troubleshooting guides • docs/deployment/ (4 files) - Deployment and security documentation • docs/guides/ (3 files) - Feature-specific guides • docs/planning/ (4 files) - Planning and improvement proposals Root directory cleanup: - Reduced from 16 to 4 markdown files in root - Only essential project files remain: • CLAUDE.md (AI instructions) • README.md (Main project readme) • CLEANUP_PLAN.md (Current cleanup plan) • SRC_STRUCTURE_IMPROVEMENTS.md (Structure improvements) This improves: ✅ Documentation discoverability ✅ Logical organization by purpose ✅ Clean root directory ✅ Better maintainability
369 lines
11 KiB
PHP
369 lines
11 KiB
PHP
<?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;
|
|
use App\Framework\Queue\ValueObjects\JobPayload;
|
|
|
|
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(JobPayload $payload): void
|
|
{
|
|
$this->used = true;
|
|
$this->jobs[] = $payload;
|
|
}
|
|
|
|
public function pop(): ?JobPayload
|
|
{
|
|
return array_shift($this->jobs);
|
|
}
|
|
|
|
public function peek(): ?JobPayload
|
|
{
|
|
return $this->jobs[0] ?? null;
|
|
}
|
|
|
|
public function size(): int
|
|
{
|
|
return count($this->jobs);
|
|
}
|
|
|
|
public function clear(): int
|
|
{
|
|
$count = count($this->jobs);
|
|
$this->jobs = [];
|
|
return $count;
|
|
}
|
|
|
|
public function getStats(): array
|
|
{
|
|
return ['size' => count($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;
|
|
}
|
|
}
|