Files
michaelschiemer/tests/Framework/Mail/Commands/SendEmailBatchCommandHandlerTest.php
Michael Schiemer 5050c7d73a docs: consolidate documentation into organized structure
- 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
2025-10-05 11:05:04 +02:00

438 lines
14 KiB
PHP

<?php
declare(strict_types=1);
use App\Domain\Common\ValueObject\Email;
use App\Framework\Logging\ChannelLogger;
use App\Framework\Logging\LogChannel;
use App\Framework\Logging\LogLevel;
use App\Framework\Logging\ValueObjects\LogContext;
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, ?LogContext $context = null): void
{
$this->logs[] = ['level' => 'emergency', 'message' => $message, 'context' => $context];
}
public function alert(string $message, ?LogContext $context = null): void
{
$this->logs[] = ['level' => 'alert', 'message' => $message, 'context' => $context];
}
public function critical(string $message, ?LogContext $context = null): void
{
$this->logs[] = ['level' => 'critical', 'message' => $message, 'context' => $context];
}
public function error(string $message, ?LogContext $context = null): void
{
$this->logs[] = ['level' => 'error', 'message' => $message, 'context' => $context];
}
public function warning(string $message, ?LogContext $context = null): void
{
$this->logs[] = ['level' => 'warning', 'message' => $message, 'context' => $context];
}
public function notice(string $message, ?LogContext $context = null): void
{
$this->logs[] = ['level' => 'notice', 'message' => $message, 'context' => $context];
}
public function info(string $message, ?LogContext $context = null): void
{
$this->logs[] = ['level' => 'info', 'message' => $message, 'context' => $context];
}
public function debug(string $message, ?LogContext $context = null): void
{
$this->logs[] = ['level' => 'debug', 'message' => $message, 'context' => $context];
}
public function log(LogLevel $level, string $message, ?LogContext $context = null): void
{
$this->logs[] = ['level' => $level->value, '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;
}
public function logToChannel(LogChannel $channel, LogLevel $level, string $message, ?LogContext $context = null): void
{
$this->logs[] = ['level' => $level->value, 'channel' => $channel->value, 'message' => $message, 'context' => $context];
}
private ?ChannelLogger $mockChannelLogger = null;
public ChannelLogger $security {
get {
return $this->mockChannelLogger ??= $this->createMockChannelLogger();
}
}
public ChannelLogger $cache {
get {
return $this->mockChannelLogger ??= $this->createMockChannelLogger();
}
}
public ChannelLogger $database {
get {
return $this->mockChannelLogger ??= $this->createMockChannelLogger();
}
}
public ChannelLogger $framework {
get {
return $this->mockChannelLogger ??= $this->createMockChannelLogger();
}
}
public ChannelLogger $error {
get {
return $this->mockChannelLogger ??= $this->createMockChannelLogger();
}
}
private function createMockChannelLogger(): ChannelLogger
{
return new class () implements ChannelLogger {
public function emergency(string $message, ?LogContext $context = null): void
{
}
public function alert(string $message, ?LogContext $context = null): void
{
}
public function critical(string $message, ?LogContext $context = null): void
{
}
public function error(string $message, ?LogContext $context = null): void
{
}
public function warning(string $message, ?LogContext $context = null): void
{
}
public function notice(string $message, ?LogContext $context = null): void
{
}
public function info(string $message, ?LogContext $context = null): void
{
}
public function debug(string $message, ?LogContext $context = null): void
{
}
};
}
}
// 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();
}
}