mirror of
https://github.com/NNTmux/newznab-tmux.git
synced 2026-08-29 04:01:24 +00:00
281 lines
11 KiB
PHP
281 lines
11 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace Tests\Feature\Mail;
|
|
|
|
use App\Enums\IncidentImpactEnum;
|
|
use App\Enums\IncidentStatusEnum;
|
|
use App\Mail\AccountChange;
|
|
use App\Mail\AccountDeleted;
|
|
use App\Mail\AccountExpired;
|
|
use App\Mail\AccountWillExpire;
|
|
use App\Mail\ContactUs;
|
|
use App\Mail\ForgottenPassword;
|
|
use App\Mail\IncidentDetected;
|
|
use App\Mail\InvitationMail;
|
|
use App\Mail\NewAccountCreatedEmail;
|
|
use App\Mail\WelcomeEmail;
|
|
use App\Models\Invitation;
|
|
use App\Models\ServiceIncident;
|
|
use App\Models\User;
|
|
use App\Notifications\VerifyEmailBranded;
|
|
use Illuminate\Contracts\Queue\ShouldQueue;
|
|
use Illuminate\Mail\Mailable;
|
|
use Illuminate\Mail\Mailables\Content;
|
|
use Illuminate\Mail\Markdown;
|
|
use Illuminate\Support\Carbon;
|
|
use PHPUnit\Framework\Attributes\DataProvider;
|
|
use Spatie\Permission\Models\Role;
|
|
use Tests\TestCase;
|
|
|
|
/**
|
|
* Renders every mailable in isolation to guarantee that the themed brand
|
|
* palette is applied site-wide and that no template still references the
|
|
* legacy purple gradient (#667eea / #764ba2) from the previous email layout.
|
|
*/
|
|
class MailRenderingTest extends TestCase
|
|
{
|
|
protected function setUp(): void
|
|
{
|
|
parent::setUp();
|
|
config([
|
|
'app.name' => 'NNTmux',
|
|
'app.url' => 'http://localhost',
|
|
'mail.from.address' => 'noreply@example.test',
|
|
'mail.brand.subject_prefix' => '[NNTmux] ',
|
|
'mail.brand.queue' => 'emails',
|
|
'mail.brand.incident_queue' => 'incidents',
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* @return iterable<string, array{0: \Closure}>
|
|
*/
|
|
public static function mailableFactoryProvider(): iterable
|
|
{
|
|
$userFactory = static function (): User {
|
|
$user = new User;
|
|
$user->username = 'tester';
|
|
$user->email = 'tester@example.test';
|
|
$user->setRelation('role', new class
|
|
{
|
|
public string $name = 'Silver';
|
|
});
|
|
|
|
return $user;
|
|
};
|
|
|
|
yield 'WelcomeEmail' => [static fn () => new WelcomeEmail($userFactory())];
|
|
yield 'ForgottenPassword' => [static fn () => new ForgottenPassword('https://example.test/reset/abc123')];
|
|
yield 'NewAccountCreatedEmail' => [static fn () => new NewAccountCreatedEmail($userFactory())];
|
|
yield 'AccountChange' => [static fn () => new AccountChange($userFactory())];
|
|
yield 'AccountWillExpire' => [static fn () => new AccountWillExpire($userFactory(), 5)];
|
|
yield 'AccountExpired' => [static fn () => new AccountExpired($userFactory())];
|
|
yield 'AccountDeleted' => [static fn () => new AccountDeleted($userFactory())];
|
|
yield 'ContactUs' => [static fn () => new ContactUs('admin@example.test', 'visitor@example.test', "Hello!\nQuestion inside.")];
|
|
}
|
|
|
|
#[DataProvider('mailableFactoryProvider')]
|
|
public function test_themed_mailable_renders_with_brand_palette_and_no_legacy_purple(\Closure $factory): void
|
|
{
|
|
/** @var Mailable $mailable */
|
|
$mailable = $factory();
|
|
$html = $mailable->render();
|
|
|
|
$this->assertGreaterThan(2000, strlen($html), 'Rendered email is suspiciously small.');
|
|
$this->assertStringContainsStringIgnoringCase('#2563eb', $html, 'Email should use the brand blue color #2563eb.');
|
|
$this->assertStringNotContainsStringIgnoringCase('#667eea', $html, 'Email still references the legacy purple gradient (#667eea).');
|
|
$this->assertStringNotContainsStringIgnoringCase('#764ba2', $html, 'Email still references the legacy purple gradient (#764ba2).');
|
|
}
|
|
|
|
public function test_markdown_cta_buttons_render_visible_label_and_forced_white_text(): void
|
|
{
|
|
$forgotten = new ForgottenPassword('https://example.test/reset/abc123');
|
|
$html = $forgotten->render();
|
|
$this->assertStringContainsString('>Reset password<', $html, 'Forgotten password CTA label missing from anchor.');
|
|
$this->assertStringContainsStringIgnoringCase('color: #ffffff !important', $html, 'CTA anchor should force white text for contrast after inlining.');
|
|
|
|
$notifiable = new class
|
|
{
|
|
public int $id = 7;
|
|
|
|
public string $email = 'tester@example.test';
|
|
|
|
public string $username = 'tester';
|
|
|
|
public function getKey(): int
|
|
{
|
|
return $this->id;
|
|
}
|
|
|
|
public function getEmailForVerification(): string
|
|
{
|
|
return $this->email;
|
|
}
|
|
};
|
|
|
|
$message = (new VerifyEmailBranded)->toMail($notifiable);
|
|
$html = (string) app(Markdown::class)->render($message->markdown, $message->data());
|
|
$this->assertStringContainsString('>Verify email address<', $html, 'Verify-email CTA label missing from anchor.');
|
|
$this->assertStringContainsStringIgnoringCase('color: #ffffff !important', $html);
|
|
}
|
|
|
|
public function test_account_will_expire_includes_pending_role_details_when_present(): void
|
|
{
|
|
$currentRole = new Role;
|
|
$currentRole->name = 'Silver';
|
|
|
|
$pendingRole = new Role;
|
|
$pendingRole->name = 'Gold';
|
|
|
|
$user = new User;
|
|
$user->username = 'tester';
|
|
$user->email = 'tester@example.test';
|
|
$user->pending_roles_id = 2;
|
|
$user->pending_role_start_date = Carbon::create(2026, 6, 4, 12);
|
|
$user->setRelation('role', $currentRole);
|
|
$user->setRelation('pendingRole', $pendingRole);
|
|
|
|
$mailable = new AccountWillExpire($user, 5);
|
|
|
|
$mailable->assertSeeInHtml('Silver');
|
|
$mailable->assertSeeInHtml('Gold');
|
|
$mailable->assertSeeInHtml('Jun 4, 2026');
|
|
$mailable->assertSeeInHtml('No renewal action is needed');
|
|
$mailable->assertDontSeeInHtml('please take action before your subscription expires');
|
|
}
|
|
|
|
public function test_invitation_mail_renders_with_branded_subject_and_brand_palette(): void
|
|
{
|
|
$inviter = new User;
|
|
$inviter->username = 'mod_alice';
|
|
|
|
$invitation = new Invitation;
|
|
$invitation->token = 'fake-token-abc123';
|
|
$invitation->email = 'newuser@example.test';
|
|
$invitation->expires_at = Carbon::now()->addDays(7);
|
|
$invitation->setRelation('invitedBy', $inviter);
|
|
|
|
$mailable = new InvitationMail($invitation);
|
|
|
|
$mailable->assertHasSubject('[NNTmux] You\'re invited to join NNTmux');
|
|
$mailable->assertSeeInHtml('mod_alice');
|
|
$mailable->assertSeeInHtml('Accept invitation');
|
|
$mailable->assertDontSeeInHtml('#667eea');
|
|
$mailable->assertDontSeeInHtml('#764ba2');
|
|
$this->assertSame('incidents', 'incidents');
|
|
}
|
|
|
|
public function test_incident_detected_uses_alert_and_status_table(): void
|
|
{
|
|
$incident = $this->makeIncident();
|
|
|
|
$mailable = new IncidentDetected($incident, false);
|
|
$mailable->assertHasSubject('[NNTmux] Service incident Detected: NZB Search, API');
|
|
$mailable->assertSeeInHtml('NZB Search, API');
|
|
$mailable->assertSeeInHtml('Critical impact');
|
|
$mailable->assertSeeInHtml('View status dashboard');
|
|
$mailable->assertDontSeeInHtml('#667eea');
|
|
}
|
|
|
|
public function test_incident_resolved_uses_success_alert(): void
|
|
{
|
|
$incident = $this->makeIncident();
|
|
$incident->resolved_at = Carbon::now();
|
|
|
|
$mailable = new IncidentDetected($incident, true);
|
|
$mailable->assertHasSubject('[NNTmux] Service incident Resolved: NZB Search, API');
|
|
$mailable->assertSeeInHtml('Resolved');
|
|
}
|
|
|
|
public function test_branded_subject_prefix_is_applied_to_every_transactional_mailable(): void
|
|
{
|
|
config(['mail.brand.subject_prefix' => '[NNTmux] ']);
|
|
$user = new User;
|
|
$user->username = 'tester';
|
|
$user->email = 'tester@example.test';
|
|
$user->setRelation('role', new class
|
|
{
|
|
public string $name = 'Silver';
|
|
});
|
|
|
|
$cases = [
|
|
[new WelcomeEmail($user), '[NNTmux] Welcome to NNTmux'],
|
|
[new ForgottenPassword('http://example.test/x'), '[NNTmux] Reset your password'],
|
|
[new NewAccountCreatedEmail($user), '[NNTmux] New account registered'],
|
|
[new AccountChange($user), '[NNTmux] Account level changed'],
|
|
[new AccountWillExpire($user, 1), '[NNTmux] Your account is about to expire'],
|
|
[new AccountExpired($user), '[NNTmux] Your account has expired'],
|
|
[new AccountDeleted($user), '[NNTmux] User account deleted'],
|
|
[new ContactUs('admin@example.test', 'visitor@example.test', 'hi'), '[NNTmux] Contact form submitted'],
|
|
];
|
|
|
|
foreach ($cases as [$mailable, $expectedSubject]) {
|
|
$mailable->assertHasSubject($expectedSubject);
|
|
}
|
|
}
|
|
|
|
public function test_blade_side_mailables_implement_should_queue(): void
|
|
{
|
|
$this->assertInstanceOf(ShouldQueue::class, $this->makeIncidentMailable());
|
|
$this->assertInstanceOf(ShouldQueue::class, $this->makeInvitationMailable());
|
|
}
|
|
|
|
public function test_incident_detected_is_routed_to_dedicated_queue(): void
|
|
{
|
|
$mailable = $this->makeIncidentMailable();
|
|
$this->assertSame('incidents', $mailable->queue);
|
|
}
|
|
|
|
public function test_incident_detected_attaches_plain_text_alternative(): void
|
|
{
|
|
$mailable = $this->makeIncidentMailable();
|
|
$mailable->build();
|
|
$this->assertSame('emails.text.incidentDetected', $mailable->textView);
|
|
}
|
|
|
|
public function test_invitation_mail_attaches_plain_text_alternative(): void
|
|
{
|
|
$mailable = $this->makeInvitationMailable();
|
|
/** @var Content $content */
|
|
$content = $mailable->content();
|
|
$this->assertSame('emails.text.invitation', $content->text);
|
|
}
|
|
|
|
private function makeIncident(): ServiceIncident
|
|
{
|
|
$incident = new ServiceIncident;
|
|
$incident->title = 'API latency spike';
|
|
$incident->description = "p99 latency exceeded thresholds.\nFollow status page for updates.";
|
|
$incident->status = IncidentStatusEnum::Identified;
|
|
$incident->impact = IncidentImpactEnum::Critical;
|
|
$incident->started_at = Carbon::now()->subMinutes(15);
|
|
$incident->setRelation('services', collect([
|
|
(object) ['name' => 'NZB Search'],
|
|
(object) ['name' => 'API'],
|
|
]));
|
|
|
|
return $incident;
|
|
}
|
|
|
|
private function makeIncidentMailable(): IncidentDetected
|
|
{
|
|
return new IncidentDetected($this->makeIncident(), false);
|
|
}
|
|
|
|
private function makeInvitationMailable(): InvitationMail
|
|
{
|
|
$inviter = new User;
|
|
$inviter->username = 'mod_alice';
|
|
|
|
$invitation = new Invitation;
|
|
$invitation->token = 'fake-token-abc123';
|
|
$invitation->email = 'newuser@example.test';
|
|
$invitation->expires_at = Carbon::now()->addDays(7);
|
|
$invitation->setRelation('invitedBy', $inviter);
|
|
|
|
return new InvitationMail($invitation);
|
|
}
|
|
}
|