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';
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user