180 lines
7.5 KiB
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);
|
|
}
|
|
});
|