CluPilotCloud/tests/Feature/Admin/MailPreviewTest.php

180 lines
7.5 KiB
PHP

<?php
use App\Livewire\Admin\MailPreview;
use App\Models\Invoice;
use App\Models\Mailbox;
use App\Models\MaintenanceWindow;
use App\Models\Order;
use App\Services\Mail\MailPreviews;
use Illuminate\Support\Facades\Mail;
use Livewire\Livewire;
/**
* Looking at a mail before a customer does.
*
* There was no way to: an invoice mail needs an invoice, a maintenance
* announcement needs a window, and "register an account to see whether the
* confirmation reads well on a phone" is not a workflow. Which is how every mail
* went out with a hard 600px table until somebody opened one on a telephone.
*/
it('renders every mail this installation sends', function () {
// The whole point of the list: a mail that cannot be rendered is one nobody
// finds out about until the event that sends it happens for real.
$previews = app(MailPreviews::class);
expect($previews->all())->not->toBeEmpty();
foreach (array_keys($previews->all()) as $key) {
$html = $previews->make($key)?->render();
expect($html)->toBeString()
->and(strlen((string) $html))->toBeGreaterThan(500);
}
});
it('leaves nothing behind in the database', function () {
// make(), never create(). A preview must not leave an invoice, an order or a
// maintenance window lying about — least of all one carrying a number drawn
// from a series.
$before = [
'invoices' => Invoice::query()->count(),
'orders' => Order::query()->count(),
'windows' => MaintenanceWindow::query()->count(),
];
foreach (array_keys(app(MailPreviews::class)->all()) as $key) {
app(MailPreviews::class)->make($key)?->render();
}
expect(Invoice::query()->count())->toBe($before['invoices'])
->and(Order::query()->count())->toBe($before['orders'])
->and(MaintenanceWindow::query()->count())->toBe($before['windows']);
});
it('answers nothing for a key nobody knows, rather than throwing', function () {
// Reached from a URL, and a URL is a string somebody can retype.
expect(app(MailPreviews::class)->make('gibt-es-nicht'))->toBeNull();
});
it('sends a preview to the operator own address and to nobody else', function () {
Mail::fake();
$operator = operator('Owner');
Livewire::actingAs($operator, 'operator')
->test(MailPreview::class)
->call('sendToMe', 'verify-email')
->assertDispatched('notify');
// SENT, not queued. Every mailable here implements ShouldQueue, so send()
// would have deferred to the queue — putting a failure in a worker's log
// while the button reported success to somebody who is waiting for the mail.
Mail::assertSent(App\Mail\VerifyEmailMail::class, fn ($mail) => $mail->hasTo($operator->email));
Mail::assertNotQueued(App\Mail\VerifyEmailMail::class);
});
it('sends each preview through the mailer the mail itself asks for', function () {
// "553 5.7.1 Sender address rejected: not owned by user no-reply@…".
//
// Every purpose mailbox has its OWN SMTP account, and a mail server lets an
// account send only from the address it owns. Mail::to() hands back a pending
// mail bound to the DEFAULT mailer and sendNow() then ignores the mailer the
// mailable asked for — so a support mail addressed support@ went out over the
// no-reply@ login and was rejected. The mailer is not a detail here; it IS
// which account authenticates.
Mail::fake();
Livewire::actingAs(operator('Owner'), 'operator')
->test(MailPreview::class)
->call('sendToMe', 'operator-message');
Mail::assertSent(App\Mail\OperatorMessageMail::class, fn ($mail) => $mail->mailer === 'cp_support');
});
it('gives every preview a sender its own mailbox owns', function () {
// The invariant behind the 553: From has to be the address of the mailbox
// whose account signs in. A preview that takes the framework default
// (MAIL_FROM_ADDRESS, which no mailbox owns) is rejected by any properly
// configured server — which is exactly how the cloud-ready preview failed
// while all nine rendered perfectly in the browser.
// updateOrCreate, because the installation already seeds mailboxes: this
// test cares about the mapping and the addresses, not about who created the
// rows.
foreach (App\Services\Mail\MailPurpose::ALL as $purpose) {
Mailbox::query()->updateOrCreate(['key' => $purpose], [
'address' => $purpose.'@clupilot.test',
'password' => 'test-passwort',
'authenticates' => true,
'active' => true,
]);
App\Support\Settings::set(App\Services\Mail\MailPurpose::settingKey($purpose), $purpose);
}
foreach (array_keys(app(MailPreviews::class)->all()) as $key) {
$mailable = app(MailPreviews::class)->make($key);
// Which mailer it will go out over, and therefore which account signs in.
expect($mailable->mailer)->toBeString()->and($mailable->mailer)->toStartWith('cp_');
$purpose = str_replace('cp_', '', $mailable->mailer);
$mailable->assertFrom($purpose.'@clupilot.test');
}
});
it('has no recipient field, because that would be a form for mailing strangers', function () {
$component = Illuminate\Support\Facades\File::get(app_path('Livewire/Admin/MailPreview.php'));
// The address comes from the signed-in operator, never from the request.
expect($component)->toContain("Auth::guard('operator')->user()")
->and($component)->not->toContain('public string $to');
});
it('serves the rendered mail as the document itself', function () {
// Not escaped into a console page: a mail is a whole document with its own
// background, and judging it inside a frame judges the frame.
$this->actingAs(operator('Owner'), 'operator')
->get(route('admin.mail.preview.show', 'verify-email'))
->assertOk()
->assertSee('max-width:600px', false);
});
it('keeps the preview to operators who may manage mail', function () {
Livewire::actingAs(operator('Read-only'), 'operator')
->test(MailPreview::class)
->assertForbidden();
$this->actingAs(operator('Read-only'), 'operator')
->get(route('admin.mail.preview.show', 'verify-email'))
->assertForbidden();
});
it('answers 404 for a preview that does not exist', function () {
$this->actingAs(operator('Owner'), 'operator')
->get(route('admin.mail.preview.show', 'gibt-es-nicht'))
->assertNotFound();
});
// ---- What made this necessary ----
it('builds the mail layout fluid, so a phone does not scroll sideways', function () {
// width="600" plus width:600px forced every phone to scroll to read a
// sentence. A mail cannot rely on a media query — Outlook renders with Word
// and drops <style> blocks — so the layout has to shrink by itself.
$html = (string) app(MailPreviews::class)->make('verify-email')?->render();
expect($html)->toContain('width:100%;max-width:600px')
->and($html)->not->toContain('width="600"')
// No fixed width anywhere, and max-width does not count as one.
->and(preg_match('/style="[^"]*(?<!max-)width:600px/', $html))->toBe(0);
});
it('does not spend a third of a phone screen on side padding', function () {
// 40px each side leaves 278px of a 390px screen. 24px leaves 310px, and on
// the desktop 552px of a 600px canvas still reads well.
foreach (array_keys(app(MailPreviews::class)->all()) as $key) {
$html = (string) app(MailPreviews::class)->make($key)?->render();
expect(preg_match('/padding:[^";]*\b40px\b[^";]*40px/', $html))->toBe(0, $key);
}
});