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