CluPilotCloud/tests/Feature/Mail/MailLaneRoutingTest.php

108 lines
4.0 KiB
PHP

<?php
use App\Jobs\PacedMail;
use App\Mail\Concerns\RidesALane;
use App\Mail\InvoiceMail;
use App\Mail\ResetPasswordMail;
use App\Models\Customer;
use App\Models\Invoice;
use App\Models\Order;
use App\Models\User;
use App\Services\Billing\IssueInvoice;
use App\Services\Mail\MailLane;
use App\Support\CompanyProfile;
use Illuminate\Mail\Mailable;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Queue;
/**
* Die Spur entsteht beim Einreihen, nicht beim Senden.
*
* Der Arbeiter liest die Schlangen in der Reihenfolge ihrer Dringlichkeit —
* deshalb steht ein Kennwort-Zurücksetzen nie hinter zweihundert Rechnungen.
* Das trägt nur, wenn die Rechnung wirklich in einer anderen Schlange liegt.
*
* Werkzeug: Queue::fake() statt Mail::fake(). Mail::fake() ersetzt den
* ganzen Mailer — der Aufruf, der onQueue() setzt (Mailer::queue(), der
* dann Mailable::queue() aufruft), fände dann nie statt, und der Test würde
* grün, ohne die Spur je berechnet zu haben. Queue::fake() lässt genau
* diesen Aufruf laufen und fängt erst dahinter ab, an der Queue-Fabrik —
* die Stelle, an der pushOn() tatsächlich ankommt.
*
* Der eingereihte Auftrag heißt seit dem Versandtakt PacedMail und nicht mehr
* SendQueuedMailable: eine Bindung im Container tauscht ihn für jede Mail aus.
* Queue::fake() legt die Aufträge unter ihrem EXAKTEN Klassennamen ab, ein
* Oberklassenname trifft dort nichts — die Spur, um die es hier geht, ist von
* dem Tausch unberührt.
*/
function invoiceForTest(): Invoice
{
CompanyProfile::put([
'name' => 'CluPilot Cloud e.U.',
'address' => 'Dreherstraße 66/1/8',
'postcode' => '1110',
'city' => 'Wien',
'vat_id' => 'ATU00000000',
]);
$customer = Customer::factory()->create(['name' => 'Muster GmbH']);
$orders = collect([Order::factory()->create([
'customer_id' => $customer->id, 'amount_cents' => 1900, 'currency' => 'EUR', 'status' => 'paid',
])]);
return app(IssueInvoice::class)->forOrders($customer, $orders);
}
it('reiht jede Mail in die Schlange ihrer Spur ein', function () {
Queue::fake();
Mail::to('kunde@example.test')->queue(new InvoiceMail(invoiceForTest(), 'Muster'));
Queue::assertPushedOn(MailLane::CALM, PacedMail::class);
});
it('reiht eine Direkt-Mail in die Direkt-Spur ein', function () {
Queue::fake();
$user = User::factory()->create();
Mail::to('kunde@example.test')->queue(new ResetPasswordMail($user, 'https://example.test/reset', 60));
Queue::assertPushedOn(MailLane::DIRECT, PacedMail::class);
});
it('folgt einer verschobenen Zuordnung', function () {
Queue::fake();
MailLane::assign(InvoiceMail::class, MailLane::URGENT);
Mail::to('kunde@example.test')->queue(new InvoiceMail(invoiceForTest(), 'Muster'));
Queue::assertPushedOn(MailLane::URGENT, PacedMail::class);
});
/**
* Rekursiv statt `glob('Mail/*.php')`: `app/Mail` hat mit `Concerns/` und
* `Transport/` schon zwei Unterordner und wird eher mehr als weniger. Eine
* fünfzehnte Mailklasse in einem Unterordner sähe der alte `glob()`-Aufruf
* nie — der Wächter schwiege genau in dem Moment, in dem er gebraucht wird.
*
* Eingeschränkt auf echte Mailables (Unterklassen von
* `Illuminate\Mail\Mailable`) statt auf „liegt irgendwo unter app/Mail":
* ein Trait wie `RidesALane` selbst, oder eine künftige Basisklasse, ist
* keine versendbare Mail, braucht keine Spur und soll den Test nicht rot
* machen, nur weil sie im selben Ordner liegt.
*/
it('lässt keine Mailklasse ohne Spur', function () {
$missing = collect(File::allFiles(app_path('Mail')))
->map(fn ($file) => 'App\\Mail\\'.str_replace(
[DIRECTORY_SEPARATOR, '.php'], ['\\', ''], $file->getRelativePathname()
))
->filter(fn (string $class) => is_subclass_of($class, Mailable::class))
->reject(fn (string $class) => in_array(RidesALane::class, class_uses_recursive($class), true))
->values()
->all();
expect($missing)->toBe([]);
});