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:
2025-08-11 20:13:26 +02:00
parent 59fd3dd3b1
commit 55a330b223
3683 changed files with 2956207 additions and 16948 deletions

View File

@@ -0,0 +1,354 @@
<?php
declare(strict_types=1);
use App\Framework\Core\ValueObjects\Byte;
use App\Framework\Email\CssInliner;
use App\Framework\Email\EmailContext;
use App\Framework\Email\EmailTemplateRenderer;
use App\Framework\Email\ValueObjects\EmailContent;
use App\Framework\Email\ValueObjects\EmailSubject;
use App\Framework\Template\Parser\DomTemplateParser;
beforeEach(function () {
$this->parser = new DomTemplateParser();
$this->cssInliner = new CssInliner($this->parser);
$this->renderer = new EmailTemplateRenderer($this->parser, $this->cssInliner);
});
describe('CssInliner', function () {
it('inlines CSS from style tags', function () {
$html = '
<html>
<head>
<style>
.button { background-color: red; padding: 10px; }
p { color: blue; }
</style>
</head>
<body>
<p>Hello</p>
<a class="button">Click me</a>
</body>
</html>
';
$result = $this->cssInliner->inline($html);
expect($result)->toContain('style="color: blue"');
expect($result)->toContain('style="background-color: red; padding: 10px"');
});
it('handles multiple selectors', function () {
$html = '
<html>
<head>
<style>
h1, h2, h3 { color: green; }
</style>
</head>
<body>
<h1>Title 1</h1>
<h2>Title 2</h2>
<h3>Title 3</h3>
</body>
</html>
';
$result = $this->cssInliner->inline($html);
expect($result)->toContain('<h1 style="color: green">');
expect($result)->toContain('<h2 style="color: green">');
expect($result)->toContain('<h3 style="color: green">');
});
it('preserves existing inline styles with higher priority', function () {
$html = '
<html>
<head>
<style>
p { color: blue; font-size: 14px; }
</style>
</head>
<body>
<p style="color: red;">Text</p>
</body>
</html>
';
$result = $this->cssInliner->inline($html);
// Existing inline style should win
expect($result)->toContain('color: red');
// But new property should be added
expect($result)->toContain('font-size: 14px');
});
it('removes !important from inline styles', function () {
$html = '
<html>
<head>
<style>
.urgent { color: red !important; }
</style>
</head>
<body>
<p class="urgent">Alert</p>
</body>
</html>
';
$result = $this->cssInliner->inline($html);
expect($result)->toContain('style="color: red"');
expect($result)->not->toContain('!important');
});
});
describe('EmailContext', function () {
it('implements TemplateContext interface', function () {
$context = new EmailContext(
data: ['name' => 'John', 'email' => 'john@example.com']
);
expect($context->getData())->toBe(['name' => 'John', 'email' => 'john@example.com']);
expect($context->get('name'))->toBe('John');
expect($context->get('missing', 'default'))->toBe('default');
expect($context->has('email'))->toBeTrue();
expect($context->has('missing'))->toBeFalse();
});
it('creates context with tracking parameters', function () {
$context = EmailContext::withTracking(
data: ['name' => 'John'],
source: 'newsletter',
campaign: 'welcome'
);
expect($context->utmParams)->toHaveKey('utm_source', 'newsletter');
expect($context->utmParams)->toHaveKey('utm_medium', 'email');
expect($context->utmParams)->toHaveKey('utm_campaign', 'welcome');
expect($context->trackingId)->toStartWith('email_');
});
});
describe('EmailContent', function () {
it('calculates size using Byte value object', function () {
$content = new EmailContent(
html: '<p>Hello World</p>', // 18 bytes
text: 'Hello World' // 11 bytes
);
$size = $content->getSize();
expect($size)->toBeInstanceOf(Byte::class);
expect($size->toBytes())->toBe(29);
});
it('detects multipart content', function () {
$withBoth = new EmailContent(
html: '<p>Hello</p>',
text: 'Hello'
);
$htmlOnly = new EmailContent(
html: '<p>Hello</p>',
text: ''
);
expect($withBoth->hasMultipart())->toBeTrue();
expect($htmlOnly->hasMultipart())->toBeFalse();
});
});
describe('EmailSubject', function () {
it('validates subject length', function () {
$subject = new EmailSubject('Valid subject');
expect($subject->value)->toBe('Valid subject');
expect($subject->isWithinRecommendedLength())->toBeTrue();
});
it('removes line breaks from subject', function () {
$subject = new EmailSubject("Subject with\nnewline");
expect($subject->value)->toBe('Subject with newline');
});
it('throws exception for empty subject', function () {
expect(fn () => new EmailSubject(' '))->toThrow(\InvalidArgumentException::class);
});
it('creates preview with ellipsis', function () {
$longSubject = new EmailSubject('This is a very long subject that needs to be truncated');
expect($longSubject->getPreview(20))->toBe('This is a very lo...');
});
});
describe('EmailTemplateRenderer', function () {
it('renders template with variable replacement', function () {
// Create a test template
$templatePath = __DIR__ . '/test-email.html';
file_put_contents($templatePath, '
<html>
<head>
<title>Test Email</title>
<meta name="email-subject" content="Hello {{name}}!">
</head>
<body>
<p>Dear {{name}},</p>
<p>Your email is {{email}}</p>
</body>
</html>
');
$context = new EmailContext(
data: [
'name' => 'John Doe',
'email' => 'john@example.com',
]
);
$content = $this->renderer->render($templatePath, $context);
expect($content->html)->toContain('Dear John Doe,');
expect($content->html)->toContain('Your email is john@example.com');
expect($content->subject)->toBeInstanceOf(EmailSubject::class);
expect($content->subject->value)->toBe('Hello John Doe!');
expect($content->text)->toContain('Dear John Doe,');
// Clean up
unlink($templatePath);
});
it('adds UTM tracking parameters to links', function () {
$templatePath = __DIR__ . '/test-tracking.html';
file_put_contents($templatePath, '
<html>
<body>
<a href="https://example.com">Link 1</a>
<a href="https://example.com?foo=bar">Link 2</a>
<a href="#anchor">Anchor</a>
<a href="mailto:test@example.com">Email</a>
</body>
</html>
');
$context = EmailContext::withTracking(
data: [],
source: 'email',
campaign: 'test'
);
$content = $this->renderer->render($templatePath, $context);
expect($content->html)->toContain('https://example.com?utm_source=email');
expect($content->html)->toContain('https://example.com?foo=bar&amp;utm_source=email');
expect($content->html)->toContain('href="#anchor"'); // Unchanged
expect($content->html)->toContain('href="mailto:test@example.com"'); // Unchanged
unlink($templatePath);
});
it('generates plain text from HTML', function () {
$templatePath = __DIR__ . '/test-plaintext.html';
file_put_contents($templatePath, '
<html>
<head>
<style>p { color: blue; }</style>
</head>
<body>
<h1>Title</h1>
<p>Paragraph 1</p>
<p>Paragraph 2</p>
<ul>
<li>Item 1</li>
<li>Item 2</li>
</ul>
<a href="https://example.com">Click here</a>
</body>
</html>
');
$context = new EmailContext();
$content = $this->renderer->render($templatePath, $context);
expect($content->text)->not->toContain('<');
expect($content->text)->not->toContain('>');
expect($content->text)->toContain("Title\n\n");
expect($content->text)->toContain("Paragraph 1\n\n");
expect($content->text)->toContain('• Item 1');
expect($content->text)->toContain('• Item 2');
expect($content->text)->toContain('Click here (https://example.com)');
unlink($templatePath);
});
it('adds preheader text', function () {
$templatePath = __DIR__ . '/test-preheader.html';
file_put_contents($templatePath, '
<html>
<body>
<p>Content</p>
</body>
</html>
');
$context = new EmailContext(
data: [],
preheader: 'This is a preview text'
);
$content = $this->renderer->render($templatePath, $context);
expect($content->html)->toContain('<body>');
expect($content->html)->toContain('display:none');
expect($content->html)->toContain('This is a preview text');
unlink($templatePath);
});
});
describe('Integration: Welcome Email Template', function () {
it('renders the welcome email template correctly', function () {
$context = new EmailContext(
data: [
'name' => 'Max Mustermann',
'email' => 'max@example.com',
'company_name' => 'Test Company',
'login_url' => 'https://app.example.com/login',
'current_year' => '2024',
'facebook_url' => 'https://facebook.com/testcompany',
'twitter_url' => 'https://twitter.com/testcompany',
'linkedin_url' => 'https://linkedin.com/company/testcompany',
'unsubscribe_url' => 'https://app.example.com/unsubscribe',
'webview_url' => 'https://app.example.com/email/view/123',
],
preheader: 'Willkommen bei Test Company!'
);
$content = $this->renderer->render('welcome', $context);
// Check HTML content
expect($content->html)->toContain('Hallo Max Mustermann,');
expect($content->html)->toContain('max@example.com');
expect($content->html)->toContain('Test Company');
expect($content->html)->toContain('https://app.example.com/login');
// Check subject extraction
expect($content->subject)->toBeInstanceOf(EmailSubject::class);
expect($content->subject->value)->toBe('Willkommen bei Test Company!');
// Check plain text generation
expect($content->text)->toContain('Hallo Max Mustermann,');
expect($content->text)->not->toContain('<div>');
expect($content->text)->not->toContain('<style>');
// Check CSS was inlined
expect($content->html)->toContain('style=');
});
});

View File

@@ -0,0 +1,526 @@
<?php
declare(strict_types=1);
use App\Domain\Common\ValueObject\Email;
use App\Framework\Core\ValueObjects\Url;
use App\Framework\Email\CssInliner;
use App\Framework\Email\Emails\PasswordResetEmail;
use App\Framework\Email\Emails\WelcomeEmail;
use App\Framework\Email\EmailService;
use App\Framework\Email\EmailTemplateRenderer;
use App\Framework\Logging\Logger;
use App\Framework\Mail\MailerInterface;
use App\Framework\Mail\Priority;
use App\Framework\Template\Parser\DomTemplateParser;
beforeEach(function () {
// Create test templates directory
$templatesDir = __DIR__ . '/../../tmp/email-templates';
if (! is_dir($templatesDir)) {
mkdir($templatesDir, 0755, true);
}
// Create welcome template
$welcomeTemplate = <<<HTML
<!DOCTYPE html>
<html>
<head>
<style>
.header { background: #007bff; color: white; padding: 20px; }
.content { padding: 20px; }
.button { background: #28a745; color: white; padding: 10px 20px; text-decoration: none; }
</style>
<title>Welcome to {{company_name}}</title>
</head>
<body>
<div class="header">
<h1>Willkommen bei {{company_name}}!</h1>
</div>
<div class="content">
<p>Hallo {{name}},</p>
<p>willkommen bei {{company_name}}! Wir freuen uns, dass du dabei bist.</p>
<a href="{{login_url}}" class="button">Jetzt anmelden</a>
<p>&copy; {{current_year}} {{company_name}}</p>
</div>
</body>
</html>
HTML;
file_put_contents($templatesDir . '/welcome.html', $welcomeTemplate);
// Create password reset template
$passwordResetTemplate = <<<HTML
<!DOCTYPE html>
<html>
<head>
<style>
.security { background: #dc3545; color: white; padding: 15px; }
.content { padding: 20px; }
.reset-button { background: #007bff; color: white; padding: 12px 24px; text-decoration: none; }
.warning { background: #fff3cd; padding: 10px; margin: 10px 0; }
</style>
<title>Passwort zurücksetzen - Aktion erforderlich</title>
</head>
<body>
<div class="security">
<h1>Passwort zurücksetzen</h1>
</div>
<div class="content">
<p>Hallo {{name}},</p>
<p>es wurde eine Anfrage zum Zurücksetzen deines Passworts gestellt.</p>
<div class="warning">
<strong>Sicherheitshinweis:</strong> Diese Anfrage kam von IP {{request_ip}}
</div>
<p>Klicke auf den folgenden Link, um dein Passwort zurückzusetzen:</p>
<a href="{{reset_url}}" class="reset-button">Passwort zurücksetzen</a>
<p><strong>Reset-Code:</strong> {{reset_code}}</p>
<p>Dieser Link ist {{expiry_hours}} Stunden gültig.</p>
<p>&copy; {{current_year}} {{company_name}}</p>
</div>
</body>
</html>
HTML;
file_put_contents($templatesDir . '/password-reset.html', $passwordResetTemplate);
$this->templatesDir = $templatesDir;
});
afterEach(function () {
// Cleanup test templates
if (isset($this->templatesDir) && is_dir($this->templatesDir)) {
array_map('unlink', glob($this->templatesDir . '/*'));
rmdir($this->templatesDir);
}
});
describe('WelcomeEmail', function () {
it('creates a welcome email with proper validation', function () {
$email = new WelcomeEmail(
to: new Email('user@example.com'),
userName: 'John Doe',
companyName: 'Test Company',
loginUrl: Url::from('https://example.com/login'),
referralCode: 'REF123'
);
expect($email->to->value)->toBe('user@example.com');
expect($email->userName)->toBe('John Doe');
expect($email->companyName)->toBe('Test Company');
expect($email->loginUrl->toString())->toBe('https://example.com/login');
expect($email->referralCode)->toBe('REF123');
expect($email->priority)->toBe(Priority::HIGH);
});
it('validates required fields', function () {
expect(function () {
new WelcomeEmail(
to: new Email('user@example.com'),
userName: '', // Empty name should fail
companyName: 'Test Company',
loginUrl: Url::from('https://example.com/login')
);
})->toThrow(InvalidArgumentException::class);
expect(function () {
new WelcomeEmail(
to: new Email('user@example.com'),
userName: 'John Doe',
companyName: '', // Empty company should fail
loginUrl: Url::from('https://example.com/login')
);
})->toThrow(InvalidArgumentException::class);
});
it('converts to Mail Message correctly', function () {
$parser = new DomTemplateParser($this->templatesDir);
$cssInliner = new CssInliner($parser);
$renderer = new EmailTemplateRenderer($parser, $cssInliner);
$email = new WelcomeEmail(
to: new Email('user@example.com'),
userName: 'John Doe',
companyName: 'Test Company',
loginUrl: Url::from('https://example.com/login'),
referralCode: 'REF123'
);
$defaultFrom = new Email('noreply@company.com');
$message = $email->toMessage($renderer, $defaultFrom);
expect($message->from->value)->toBe('noreply@company.com');
expect($message->to->toArray()[0]->value)->toBe('user@example.com');
expect($message->subject)->toBe('Willkommen bei Test Company!');
expect($message->priority)->toBe(Priority::HIGH);
expect($message->htmlBody)->toContain('Hallo John Doe');
expect($message->htmlBody)->toContain('Test Company');
expect($message->htmlBody)->toContain('https://example.com/login');
expect($message->htmlBody)->toContain('style='); // CSS should be inlined
expect($message->body)->toContain('John Doe'); // Text version generated
});
it('uses factory method correctly', function () {
$email = WelcomeEmail::for(
to: 'user@example.com',
userName: 'John Doe',
companyName: 'Test Company',
loginUrl: 'https://example.com/login',
referralCode: 'REF123'
);
expect($email->to->value)->toBe('user@example.com');
expect($email->loginUrl->toString())->toBe('https://example.com/login');
});
});
describe('PasswordResetEmail', function () {
it('creates a password reset email with proper validation', function () {
$email = new PasswordResetEmail(
to: new Email('user@example.com'),
userName: 'John Doe',
companyName: 'Test Company',
resetUrl: Url::from('https://example.com/reset?token=abc123'),
resetCode: 'RESET-123-456',
requestIp: '192.168.1.100',
expiryHours: 24
);
expect($email->to->value)->toBe('user@example.com');
expect($email->userName)->toBe('John Doe');
expect($email->resetUrl->toString())->toBe('https://example.com/reset?token=abc123');
expect($email->resetCode)->toBe('RESET-123-456');
expect($email->requestIp)->toBe('192.168.1.100');
expect($email->expiryHours)->toBe(24);
expect($email->priority)->toBe(Priority::HIGHEST);
});
it('validates required fields', function () {
expect(function () {
new PasswordResetEmail(
to: new Email('user@example.com'),
userName: '', // Empty name should fail
companyName: 'Test Company',
resetUrl: Url::from('https://example.com/reset'),
resetCode: 'RESET-123',
requestIp: '192.168.1.100'
);
})->toThrow(InvalidArgumentException::class);
expect(function () {
new PasswordResetEmail(
to: new Email('user@example.com'),
userName: 'John Doe',
companyName: 'Test Company',
resetUrl: Url::from('https://example.com/reset'),
resetCode: '', // Empty code should fail
requestIp: '192.168.1.100'
);
})->toThrow(InvalidArgumentException::class);
});
it('validates expiry hours range', function () {
expect(function () {
new PasswordResetEmail(
to: new Email('user@example.com'),
userName: 'John Doe',
companyName: 'Test Company',
resetUrl: Url::from('https://example.com/reset'),
resetCode: 'RESET-123',
requestIp: '192.168.1.100',
expiryHours: 0 // Should fail
);
})->toThrow(InvalidArgumentException::class);
expect(function () {
new PasswordResetEmail(
to: new Email('user@example.com'),
userName: 'John Doe',
companyName: 'Test Company',
resetUrl: Url::from('https://example.com/reset'),
resetCode: 'RESET-123',
requestIp: '192.168.1.100',
expiryHours: 73 // Should fail (max 72)
);
})->toThrow(InvalidArgumentException::class);
});
it('converts to Mail Message correctly', function () {
$parser = new DomTemplateParser($this->templatesDir);
$cssInliner = new CssInliner($parser);
$renderer = new EmailTemplateRenderer($parser, $cssInliner);
$email = new PasswordResetEmail(
to: new Email('user@example.com'),
userName: 'John Doe',
companyName: 'Test Company',
resetUrl: Url::from('https://example.com/reset?token=abc123'),
resetCode: 'RESET-123-456',
requestIp: '192.168.1.100',
expiryHours: 24
);
$defaultFrom = new Email('security@company.com');
$message = $email->toMessage($renderer, $defaultFrom);
expect($message->from->value)->toBe('security@company.com');
expect($message->to->toArray()[0]->value)->toBe('user@example.com');
expect($message->subject)->toBe('Passwort zurücksetzen');
expect($message->priority)->toBe(Priority::HIGHEST);
expect($message->htmlBody)->toContain('Hallo John Doe');
expect($message->htmlBody)->toContain('192.168.1.100');
expect($message->htmlBody)->toContain('RESET-123-456');
expect($message->htmlBody)->toContain('24 Stunden');
expect($message->htmlBody)->toContain('style='); // CSS should be inlined
expect($message->body)->toContain('John Doe'); // Text version generated
});
it('uses factory method correctly', function () {
$email = PasswordResetEmail::for(
to: 'user@example.com',
userName: 'John Doe',
companyName: 'Test Company',
resetUrl: 'https://example.com/reset?token=abc123',
resetCode: 'RESET-123-456',
requestIp: '192.168.1.100',
expiryHours: 48
);
expect($email->to->value)->toBe('user@example.com');
expect($email->resetUrl->toString())->toBe('https://example.com/reset?token=abc123');
expect($email->expiryHours)->toBe(48);
});
});
describe('EmailService Integration', function () {
it('sends typed emails via EmailService', function () {
// Mock mailer that tracks sent messages
$sentMessages = [];
$mockMailer = new class ($sentMessages) implements MailerInterface {
public function __construct(private array &$sentMessages)
{
}
public function send(\App\Framework\Mail\Message $message): bool
{
$this->sentMessages[] = $message;
return true;
}
public function queue(\App\Framework\Mail\Message $message, int $maxRetries = 3, int $delaySeconds = 0): bool
{
return true;
}
public function sendBatch(array $messages): array
{
return array_fill(0, count($messages), true);
}
};
// Mock logger
$logEntries = [];
$mockLogger = new class ($logEntries) implements Logger {
public function __construct(private array &$logEntries)
{
}
public function debug(string $message, array $context = []): void
{
$this->logEntries[] = ['level' => 'debug', 'message' => $message, 'context' => $context];
}
public function info(string $message, array $context = []): void
{
$this->logEntries[] = ['level' => 'info', 'message' => $message, 'context' => $context];
}
public function notice(string $message, array $context = []): void
{
$this->logEntries[] = ['level' => 'notice', 'message' => $message, 'context' => $context];
}
public function warning(string $message, array $context = []): void
{
$this->logEntries[] = ['level' => 'warning', 'message' => $message, 'context' => $context];
}
public function error(string $message, array $context = []): void
{
$this->logEntries[] = ['level' => 'error', 'message' => $message, 'context' => $context];
}
public function critical(string $message, array $context = []): void
{
$this->logEntries[] = ['level' => 'critical', 'message' => $message, 'context' => $context];
}
public function alert(string $message, array $context = []): void
{
$this->logEntries[] = ['level' => 'alert', 'message' => $message, 'context' => $context];
}
public function emergency(string $message, array $context = []): void
{
$this->logEntries[] = ['level' => 'emergency', 'message' => $message, 'context' => $context];
}
};
$parser = new DomTemplateParser($this->templatesDir);
$cssInliner = new CssInliner($parser);
$emailService = new EmailService(
mailer: $mockMailer,
parser: $parser,
cssInliner: $cssInliner,
logger: $mockLogger,
defaultFrom: ['email' => 'noreply@company.com', 'name' => 'Test Company']
);
// Test WelcomeEmail
$welcomeEmail = WelcomeEmail::for(
to: 'newuser@example.com',
userName: 'Jane Smith',
companyName: 'Test Company',
loginUrl: 'https://company.com/login'
);
$result = $emailService->send($welcomeEmail);
expect($result)->toBeTrue();
expect($sentMessages)->toHaveCount(1);
$sentMessage = $sentMessages[0];
expect($sentMessage->to->toArray()[0]->value)->toBe('newuser@example.com');
expect($sentMessage->subject)->toBe('Willkommen bei Test Company!');
expect($sentMessage->priority)->toBe(Priority::HIGH);
expect($sentMessage->htmlBody)->toContain('Jane Smith');
// Verify logging
expect($logEntries)->toHaveCount(1);
expect($logEntries[0]['level'])->toBe('info');
expect($logEntries[0]['message'])->toBe('Typed email sent successfully');
expect($logEntries[0]['context']['type'])->toBe(WelcomeEmail::class);
// Test PasswordResetEmail
$resetEmail = PasswordResetEmail::for(
to: 'user@example.com',
userName: 'John Doe',
companyName: 'Test Company',
resetUrl: 'https://company.com/reset?token=xyz789',
resetCode: 'RESET-789',
requestIp: '10.0.0.1',
expiryHours: 12
);
$result2 = $emailService->send($resetEmail);
expect($result2)->toBeTrue();
expect($sentMessages)->toHaveCount(2);
$sentMessage2 = $sentMessages[1];
expect($sentMessage2->to->toArray()[0]->value)->toBe('user@example.com');
expect($sentMessage2->subject)->toBe('Passwort zurücksetzen');
expect($sentMessage2->priority)->toBe(Priority::HIGHEST);
expect($sentMessage2->htmlBody)->toContain('RESET-789');
expect($sentMessage2->htmlBody)->toContain('10.0.0.1');
expect($sentMessage2->htmlBody)->toContain('12 Stunden');
// Verify second logging entry
expect($logEntries)->toHaveCount(2);
expect($logEntries[1]['context']['type'])->toBe(PasswordResetEmail::class);
});
it('handles sending failures gracefully', function () {
// Mock mailer that fails
$mockMailer = new class () implements MailerInterface {
public function send(\App\Framework\Mail\Message $message): bool
{
return false;
}
public function queue(\App\Framework\Mail\Message $message, int $maxRetries = 3, int $delaySeconds = 0): bool
{
return false;
}
public function sendBatch(array $messages): array
{
return array_fill(0, count($messages), false);
}
};
$logEntries = [];
$mockLogger = new class ($logEntries) implements Logger {
public function __construct(private array &$logEntries)
{
}
public function debug(string $message, array $context = []): void
{
$this->logEntries[] = ['level' => 'debug', 'message' => $message, 'context' => $context];
}
public function info(string $message, array $context = []): void
{
$this->logEntries[] = ['level' => 'info', 'message' => $message, 'context' => $context];
}
public function notice(string $message, array $context = []): void
{
$this->logEntries[] = ['level' => 'notice', 'message' => $message, 'context' => $context];
}
public function warning(string $message, array $context = []): void
{
$this->logEntries[] = ['level' => 'warning', 'message' => $message, 'context' => $context];
}
public function error(string $message, array $context = []): void
{
$this->logEntries[] = ['level' => 'error', 'message' => $message, 'context' => $context];
}
public function critical(string $message, array $context = []): void
{
$this->logEntries[] = ['level' => 'critical', 'message' => $message, 'context' => $context];
}
public function alert(string $message, array $context = []): void
{
$this->logEntries[] = ['level' => 'alert', 'message' => $message, 'context' => $context];
}
public function emergency(string $message, array $context = []): void
{
$this->logEntries[] = ['level' => 'emergency', 'message' => $message, 'context' => $context];
}
};
$parser = new DomTemplateParser($this->templatesDir);
$cssInliner = new CssInliner($parser);
$emailService = new EmailService(
mailer: $mockMailer,
parser: $parser,
cssInliner: $cssInliner,
logger: $mockLogger
);
$welcomeEmail = WelcomeEmail::for(
to: 'user@example.com',
userName: 'Test User',
companyName: 'Test Company',
loginUrl: 'https://company.com/login'
);
$result = $emailService->send($welcomeEmail);
expect($result)->toBeFalse();
expect($logEntries)->toHaveCount(1);
expect($logEntries[0]['level'])->toBe('error');
expect($logEntries[0]['message'])->toBe('Failed to send typed email');
expect($logEntries[0]['context']['error'])->toBe('Send operation failed');
});
});