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:
354
tests/Framework/Email/EmailModuleTest.php
Normal file
354
tests/Framework/Email/EmailModuleTest.php
Normal 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&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=');
|
||||
});
|
||||
});
|
||||
526
tests/Framework/Email/TypedEmailTest.php
Normal file
526
tests/Framework/Email/TypedEmailTest.php
Normal 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>© {{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>© {{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');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user