diff --git a/app/Livewire/Admin/Mail.php b/app/Livewire/Admin/Mail.php index bb28da4..23413c0 100644 --- a/app/Livewire/Admin/Mail.php +++ b/app/Livewire/Admin/Mail.php @@ -4,6 +4,7 @@ namespace App\Livewire\Admin; use App\Livewire\Concerns\ConfirmsPassword; use App\Models\Mailbox; +use App\Services\Mail\MailboxResolver; use App\Services\Mail\MailboxTester; use App\Services\Mail\MailCatalogue; use App\Services\Mail\MailPurpose; @@ -12,22 +13,44 @@ use App\Services\Secrets\SecretCipher; use App\Support\MailDelivery; use App\Support\Settings; use Livewire\Attributes\Layout; +use Livewire\Attributes\Url; use Livewire\Component; /** * The sending addresses, and which kind of mail leaves from which. * - * The server sits at the top because there is one of it; the mailboxes are a - * list because there are several; the mapping is last because it only makes - * sense once both exist. The test-send button lives with the mailboxes: it - * proves one specific mailbox can actually send, which is the only thing - * that makes the rest of this page more than a form. + * The server sits first because there is one of it; the mailboxes come next + * because there are several; the mapping is last because it only makes sense + * once both exist. The test-send button lives with the mailboxes: it proves + * one specific mailbox can actually send, which is the only thing that makes + * the rest of this page more than a form. + * + * Diese Reihenfolge waren vier Karten in einer schmalen Spalte, und die + * letzten beiden allein zweiundzwanzig gleich aussehende Zeilen — fünf Zwecke, + * siebzehn Mailarten, jede „Beschriftung, Auswahlfeld", darunter zweimal + * derselbe Speichern-Knopf. Wer die Zuordnung einer einzelnen Mailart ändern + * wollte, scrollte an allem anderen vorbei und fand am Ende eine Wand. + * + * Jetzt drei Reiter nach dem Muster von Admin\Integrations, das aus demselben + * Grund umgebaut wurde: WOMIT gesendet wird, WER sendet, und WAS von wo + * rausgeht. Der offene Reiter steht in der Adresszeile, damit ein Neuladen + * oder ein Lesezeichen dort landet, wo der Betreiber war. */ #[Layout('layouts.admin')] class Mail extends Component { use ConfirmsPassword; + /** + * Die Reiter, in dieser Reihenfolge. Die Liste IST das Schema: sie prüft + * die Adresszeile, baut die Leiste und entscheidet, was gerendert wird. + */ + public const TABS = ['versand', 'postfaecher', 'zuordnung']; + + /** Welcher Reiter offen ist — mit `history: true` ein Schritt zurück. */ + #[Url(history: true)] + public string $tab = 'versand'; + public string $host = ''; public int|string $port = 587; @@ -155,7 +178,17 @@ class Mail extends Component $this->dispatch('notify', message: __('mail_settings.server_saved')); } - public function savePurposes(): void + /** + * Prüft und schreibt die Zwecke — ohne Meldung. + * + * Getrennt vom öffentlichen savePurposes(), weil die Seite die Zwecke und + * die Wegwahl jetzt in EINEM Block zeigt und mit EINEM Knopf speichert: + * eine Zuordnung, die man an zwei Stellen bestätigen muss, ist der Grund, + * warum unter der alten Seite zwei gleich aussehende Knöpfe standen. Die + * beiden öffentlichen Methoden bleiben, was sie waren — sie sind der + * Einstieg, den die Tests und ein direkter Aufruf kennen. + */ + private function writePurposes(): void { $this->authorize('mail.manage'); @@ -203,6 +236,11 @@ class Mail extends Component foreach (MailPurpose::ALL as $purpose) { Settings::set(MailPurpose::settingKey($purpose), $this->purposes[$purpose] ?? ''); } + } + + public function savePurposes(): void + { + $this->writePurposes(); $this->dispatch('notify', message: __('mail_settings.purposes_saved')); } @@ -215,13 +253,34 @@ class Mail extends Component * auf den Zweck zurückfallen lässt. Eine zweite Prüfung derselben * Sicherung wäre doppelte Arbeit ohne eigenen Wert. */ - public function saveRoutes(): void + private function writeRoutes(): void { $this->authorize('mail.manage'); foreach (MailCatalogue::all() as $key => $entry) { Settings::set(MailRoute::settingKey($key), $this->routes[$key] ?? ''); } + } + + public function saveRoutes(): void + { + $this->writeRoutes(); + + $this->dispatch('notify', message: __('mail_settings.purposes_saved')); + } + + /** + * Der eine Knopf unter dem Reiter „Zuordnung". + * + * Zwecke zuerst: writePurposes() prüft und wirft, bevor irgendetwas + * geschrieben ist — eine abgelehnte Eingabe darf nicht die halbe Zuordnung + * hinterlassen. Eine Meldung für beides, weil es für den Betreiber ein + * Vorgang ist. + */ + public function saveAssignments(): void + { + $this->writePurposes(); + $this->writeRoutes(); $this->dispatch('notify', message: __('mail_settings.purposes_saved')); } @@ -245,11 +304,80 @@ class Mail extends Component { $this->usable = app(SecretCipher::class)->isUsable(); + // Ein Reitername aus der Adresszeile ist eine Zeichenkette, die ein + // Fremder getippt hat. + if (! in_array($this->tab, self::TABS, true)) { + $this->tab = self::TABS[0]; + } + + $gruppen = $this->catalogueByPurpose(); + return view('livewire.admin.mail', [ 'mailboxes' => Mailbox::query()->orderBy('key')->get(), - 'purposeList' => MailPurpose::ALL, - 'mailCatalogue' => MailCatalogue::all(), + 'catalogueByPurpose' => $gruppen, + 'inheritedAddress' => $this->inheritedAddresses(array_keys($gruppen)), 'passwordConfirmed' => $this->passwordRecentlyConfirmed(), ]); } + + /** + * Die Adresse, die eine Mailart ohne eigene Wahl WIRKLICH benutzt. + * + * Nicht das zugeordnete Postfach. MailboxResolver::for() lässt ein + * abgeschaltetes Postfach auf „System" zurückfallen, und writePurposes() + * erlaubt genau das — aktiv sein muss nur „System" selbst. Ein Etikett, + * das dann die abgeschaltete Adresse nennt, sagt dem Betreiber etwas + * Falsches über seinen eigenen Versand, und zwar an der Stelle, an der er + * nachsieht, um es richtig zu machen. + * + * Also dieselbe Funktion, die beim Senden entscheidet, statt einer zweiten + * Meinung darüber. Sie liest den GESPEICHERTEN Stand: was im Formular noch + * nicht gespeichert ist, gilt auf dieser Seite nirgends. + * + * @param array $purposes + * @return array + */ + private function inheritedAddresses(array $purposes): array + { + $adressen = []; + + foreach ($purposes as $purpose) { + // catalogueByPurpose() lässt eine Mailart mit unbekanntem Zweck + // stehen, statt sie lautlos fallen zu lassen; for() würde für die + // werfen. Kein Zweck, keine geerbte Adresse. + $adressen[$purpose] = in_array($purpose, MailPurpose::ALL, true) + ? app(MailboxResolver::class)->for($purpose)?->address + : null; + } + + return $adressen; + } + + /** + * Die Mailarten unter ihrem Zweck, in der Reihenfolge der Zwecke. + * + * Die Wegwahl je Mailart fällt auf den Zweck zurück, wenn sie leer ist — + * das stand bisher nur als Text in der Auswahlbeschriftung („wie der + * Zweck") und war damit siebzehnmal derselbe Satz neben siebzehn Zeilen, + * die alle gleich aussahen. Unter ihrem Zweck einsortiert zeigt die Liste + * dieselbe Regel als Form: das Postfach des Zwecks steht oben, was davon + * abweicht, steht darunter. + * + * Ein Zweck ohne eigene Mailart bleibt trotzdem stehen — sein Postfach ist + * die Rückfalllinie und muss auch dann einstellbar sein. Eine Mailart mit + * einem Zweck außerhalb von MailPurpose::ALL bekommt einen eigenen Block + * am Ende, statt lautlos aus der Seite zu fallen. + * + * @return array> + */ + private function catalogueByPurpose(): array + { + $gruppen = array_fill_keys(MailPurpose::ALL, []); + + foreach (MailCatalogue::all() as $key => $entry) { + $gruppen[$entry['purpose']][$key] = $entry; + } + + return $gruppen; + } } diff --git a/lang/de/mail_settings.php b/lang/de/mail_settings.php index ff73ab8..cc50457 100644 --- a/lang/de/mail_settings.php +++ b/lang/de/mail_settings.php @@ -4,6 +4,12 @@ return [ 'title' => 'E-Mail', 'subtitle' => 'Absenderadressen und der Server, über den sie verschickt werden.', + 'tab' => [ + 'versand' => 'Versand', + 'postfaecher' => 'Postfächer', + 'zuordnung' => 'Zuordnung', + ], + 'no_key' => 'SECRETS_KEY ist auf diesem Server nicht gesetzt. Ohne eigenen Schlüssel werden hier keine Postfach-Passwörter gespeichert — bewusst, denn APP_KEY wird routinemäßig gewechselt.', 'server_title' => 'Mailserver', @@ -42,7 +48,12 @@ return [ 'confirm_button' => 'Bestätigen', 'purposes_title' => 'Wer verschickt was', - 'purposes_hint' => 'Ein Zweck ohne Postfach verschickt über „System".', + 'assignments_hint' => 'Jeder Zweck hat ein Postfach — ein Zweck ohne eines verschickt über „System". Eine einzelne Mailart folgt ihrem Zweck, solange sie kein eigenes bekommt.', + 'purpose_mailbox' => 'Postfach dieses Zwecks', + 'route_exceptions' => 'Abweichend davon', + 'route_inherit' => 'wie der Zweck (:address)', + 'route_inherit_unset' => 'wie der Zweck', + 'purpose_no_types' => 'Keine eigene Mailart — dieser Zweck fängt nur auf, was sonst nirgends hingehört.', 'purpose' => [ 'maintenance' => 'Wartungsankündigungen', 'provisioning' => 'Bereitstellung und Bestellbestätigung', @@ -55,10 +66,6 @@ return [ 'system_inactive' => '„System" braucht ein aktives Postfach — es ist der Rückfall, auf den alles andere angewiesen ist.', 'purpose_unknown_mailbox' => 'Dieses Postfach existiert nicht mehr. Bitte eines aus der Liste wählen.', - 'routes_title' => 'Wegwahl je Mailart', - 'routes_hint' => 'Ohne Eintrag entscheidet der Zweck oben. Nur für die eine Mailart, die anders soll — der Nachbar bleibt, wo er war.', - 'route_default' => 'wie der Zweck (:purpose)', - 'test' => 'Testmail senden', 'testing' => 'Wird gesendet…', 'test_recipient' => 'Testempfänger', diff --git a/lang/en/mail_settings.php b/lang/en/mail_settings.php index b74ac19..0c19579 100644 --- a/lang/en/mail_settings.php +++ b/lang/en/mail_settings.php @@ -4,6 +4,12 @@ return [ 'title' => 'Email', 'subtitle' => 'Sending addresses, and the server they go out through.', + 'tab' => [ + 'versand' => 'Sending', + 'postfaecher' => 'Mailboxes', + 'zuordnung' => 'Assignment', + ], + 'no_key' => 'SECRETS_KEY is not set on this server. Without a key of its own, no mailbox passwords are stored here — deliberately, because APP_KEY is rotated as routine maintenance.', 'server_title' => 'Mail server', @@ -42,7 +48,6 @@ return [ 'confirm_button' => 'Confirm', 'purposes_title' => 'Who sends what', - 'purposes_hint' => 'A purpose with no mailbox sends through "System".', 'purpose' => [ 'maintenance' => 'Maintenance announcements', 'provisioning' => 'Provisioning and order confirmation', @@ -50,15 +55,17 @@ return [ 'billing' => 'Invoices and payment reminders', 'system' => 'System (fallback for everything else)', ], + 'assignments_hint' => 'Every purpose has a mailbox — a purpose without one sends through "System". A single mail type follows its purpose until it is given one of its own.', + 'purpose_mailbox' => 'Mailbox for this purpose', + 'route_exceptions' => 'Differing from that', + 'route_inherit' => 'same as the purpose (:address)', + 'route_inherit_unset' => 'same as the purpose', + 'purpose_no_types' => 'No mail type of its own — this purpose only catches what belongs nowhere else.', 'purposes_saved' => 'Mapping saved.', 'system_required' => '"System" must have a mailbox — it is the fallback for all the others.', 'system_inactive' => '"System" needs a mailbox that is active — it is the fallback everything else depends on.', 'purpose_unknown_mailbox' => 'That mailbox no longer exists. Please choose one from the list.', - 'routes_title' => 'Routing by mail type', - 'routes_hint' => 'With no entry, the purpose above decides. Only for the one mail type that should differ — its neighbours stay where they were.', - 'route_default' => 'same as the purpose (:purpose)', - 'test' => 'Send test mail', 'testing' => 'Sending…', 'test_recipient' => 'Test recipient', diff --git a/resources/views/livewire/admin/mail.blade.php b/resources/views/livewire/admin/mail.blade.php index f930fc5..d7b82f7 100644 --- a/resources/views/livewire/admin/mail.blade.php +++ b/resources/views/livewire/admin/mail.blade.php @@ -1,199 +1,256 @@ -
+

{{ __('mail_settings.title') }}

-

{{ __('mail_settings.subtitle') }}

+

{{ __('mail_settings.subtitle') }}

- {{-- The one thing that makes every mailbox on this page decorative. Said - here as well as on the front page, because this is where somebody comes - to find out why no mail arrived — and the test button below reports - success regardless, since MailboxTester builds its own transport on - purpose (a check that honoured MAIL_MAILER=log would report success - while writing to a file). --}} + {{-- ── Die Reiter ──────────────────────────────────────────────────── + Das waren vier Karten untereinander in einer schmalen Spalte, und die + letzten beiden allein zweiundzwanzig gleich aussehende Zeilen mit zwei + gleich aussehenden Speichern-Knöpfen darunter. Geteilt nach der Frage, + die man an die Seite mitbringt: WOMIT wird gesendet, WER sendet, und + WAS geht von wo raus — dieselbe Achse, nach der Admin\Integrations aus + demselben Grund umgebaut wurde. + + Der offene Reiter steht in der Adresszeile, damit ein Neuladen oder + ein Lesezeichen dort landet, wo der Betreiber war. Die Bestätigung des + Passworts überlebt einen Wechsel: sie gehört zur Sitzung, nicht zum + Reiter. --}} +
+ @foreach (\App\Livewire\Admin\Mail::TABS as $name) + + @endforeach +
+ + {{-- Beide Hinweise stehen ÜBER den Reitern, nicht in einem davon: sie + gelten für die ganze Seite. Der erste ist das eine, was jedes Postfach + hier zur Zierde macht — und er gehört genau dorthin, wo jemand + nachsieht, warum keine Mail ankam; der Postfach-Test darunter meldet + nämlich trotzdem Erfolg, weil MailboxTester absichtlich seinen eigenen + Weg baut. --}} @if (! \App\Support\MailDelivery::delivers()) {{ __('admin.notice.mail_not_delivering', ['mailer' => (string) (\App\Support\MailDelivery::transport() ?? 'null')]) }} @endif - @if (! $usable) {{ __('mail_settings.no_key') }} @endif - {{-- 1. The server. One of it, so one card. --}} - -

{{ __('mail_settings.server_title') }}

-

{{ __('mail_settings.server_hint') }}

+ {{-- ── 1. Womit gesendet wird ─────────────────────────────────────── + Ein Server, eine Karte. Der Hauptschalter steht ÜBER den Serverfeldern + und in derselben Karte: er entscheidet, ob die überhaupt benutzt + werden, und derselbe Knopf speichert beides. --}} + @if ($tab === 'versand') + +

{{ __('mail_settings.server_title') }}

+

{{ __('mail_settings.server_hint') }}

- {{-- Der Hauptschalter, und er steht ÜBER den Serverfeldern: er - entscheidet, ob die überhaupt benutzt werden. Bis hierher stand er - nur als MAIL_MAILER in der .env — die Bereitschaftsseite meldete - ihn als blockierend, ohne sagen zu können, worauf man ihn stellt. - Zwei Zustände, keine Treiberliste: was tatsächlich sendet, ist - MailboxTransport mit den Feldern darunter. --}} -
- -
- -
- - -
- - - @error('encryption')

{{ $message }}

@enderror +
+
-
- @if (! $passwordConfirmed) - {{-- The second gate. $host is the platform's outbound relay for - every purpose at once — being signed in is not enough to - repoint it, the same reasoning as the credentials page. --}} -
-

{{ __('mail_settings.confirm_first') }}

-
-
- -
- - {{ __('mail_settings.confirm_button') }} - +
+ + +
+ + + @error('encryption')

{{ $message }}

@enderror
- - @else - +
+ + @if (! $passwordConfirmed) + {{-- The second gate. $host is the platform's outbound relay for + every purpose at once — being signed in is not enough to + repoint it, the same reasoning as the credentials page. --}} +
+

{{ __('mail_settings.confirm_first') }}

+
+
+ +
+ + {{ __('mail_settings.confirm_button') }} + +
+
+ @else + + {{ __('mail_settings.save') }} + + @endif + + @endif + + {{-- ── 2. Wer sendet ──────────────────────────────────────────────── + Kein Eingabefeld in einem — R20. Das Empfängerfeld für den + Testversand steht ÜBER der Tabelle, eines für alle Zeilen. --}} + @if ($tab === 'postfaecher') + +

{{ __('mail_settings.boxes_title') }}

+ +
+ +

{{ __('mail_settings.test_hint') }}

+
+ + @if ($testResult) + + {{ $testedKey }}: + @if ($testResult['ok']) + {{ __('mail_settings.test_ok') }} + @else + {{ __('mail_settings.test_failed') }} {{ $testResult['error'] }} + @endif + + @endif + + + + + + + + + + + @foreach ($mailboxes as $box) + + + + + + @endforeach + +
{{ __('mail_settings.address') }}{{ __('mail_settings.last_verified') }}
+ {{ $box->address }} + {{ $box->key }} + + {{ $box->last_verified_at?->local()->isoFormat('DD.MM.YYYY HH:mm') ?? __('mail_settings.never_verified') }} + + {{-- R18: icon beside its text, single line, size-4. + Two spans swapped by wire:loading, same + pattern as admin/host-create.blade.php's save + button — the disabled attribute alone left an + operator staring at a greyed-out button with + no sign anything was happening for up to the + full connect timeout. --}} + + + {{ __('mail_settings.test') }} + {{ __('mail_settings.testing') }} + + + {{ __('mail_settings.edit') }} + +
+
+ @endif + + {{-- ── 3. Was von wo rausgeht ─────────────────────────────────────── + Zwei Listen waren es: fünf Zwecke, darunter siebzehn Mailarten, beide + als „Beschriftung, Auswahlfeld" und optisch nicht auseinanderzuhalten. + Dass die zweite Liste auf die erste zurückfällt, stand nur als Satz in + der leeren Auswahl — siebzehnmal derselbe Satz. + + Jetzt steht jede Mailart unter ihrem Zweck: oben das Postfach, aus dem + der Zweck verschickt, darunter eingerückt die Mailarten, die davon + abweichen dürfen. Die Regel ist damit die Form, und die leere Auswahl + nennt die Adresse, die stattdessen greift, statt nur den Zwecknamen. + + Ein + + @foreach ($mailboxes as $box) + + @endforeach + +
+ + @error('purposes.'.$purpose) +

{{ $message }}

+ @enderror + + @if ($entries === []) +

{{ __('mail_settings.purpose_no_types') }}

+ @else +

{{ __('mail_settings.route_exceptions') }}

+
+ @foreach ($entries as $key => $entry) +
+ {{ $entry['label'] }} + +
+ @endforeach +
+ @endif + + @endforeach +
+ + {{ __('mail_settings.save') }} - @endif -
- - {{-- 2. The mailboxes. NO input field inside a — R20. --}} - -

{{ __('mail_settings.boxes_title') }}

- - {{-- One shared recipient field for every row's test button below — a - field ABOVE the table, not one growing inside a (R20). --}} -
- -

{{ __('mail_settings.test_hint') }}

-
- - @if ($testResult) - - {{ $testedKey }}: - @if ($testResult['ok']) - {{ __('mail_settings.test_ok') }} - @else - {{ __('mail_settings.test_failed') }} {{ $testResult['error'] }} - @endif - - @endif - - - - - - - - - - - @foreach ($mailboxes as $box) - - - - - - @endforeach - -
{{ __('mail_settings.address') }}{{ __('mail_settings.last_verified') }}
- {{ $box->address }} - {{ $box->key }} - - {{ $box->last_verified_at?->local()->isoFormat('DD.MM.YYYY HH:mm') ?? __('mail_settings.never_verified') }} - - {{-- R18: icon beside its text, single line, size-4. - Two spans swapped by wire:loading, same - pattern as admin/host-create.blade.php's save - button — the disabled attribute alone left an - operator staring at a greyed-out button with - no sign anything was happening for up to the - full connect timeout. --}} - - - {{ __('mail_settings.test') }} - {{ __('mail_settings.testing') }} - - - {{ __('mail_settings.edit') }} - -
-
- - {{-- 3. The mapping. One - - @foreach ($mailboxes as $box) - - @endforeach - -
- @endforeach -
- - @error('purposes.system')

{{ $message }}

@enderror - - - {{ __('mail_settings.save') }} - - - - {{-- 4. Die Wegwahl je Mailart. Eine Ebene ÜBER den Zwecken oben, kein - Ersatz — leer heißt „wie der Zweck". Ein - - @foreach ($mailboxes->where('active', true) as $box) - - @endforeach - - - @endforeach - - - - {{ __('mail_settings.save') }} - - + + @endif diff --git a/tests/Feature/Mail/MailSettingsPageTest.php b/tests/Feature/Mail/MailSettingsPageTest.php index 401d485..55eb80a 100644 --- a/tests/Feature/Mail/MailSettingsPageTest.php +++ b/tests/Feature/Mail/MailSettingsPageTest.php @@ -4,7 +4,9 @@ use App\Livewire\Admin\Mail as MailPage; use App\Livewire\EditMailbox; use App\Models\Mailbox; use App\Models\Operator; +use App\Services\Mail\MailCatalogue; use App\Services\Mail\MailPurpose; +use App\Services\Mail\MailRoute; use App\Support\Settings; use Livewire\Livewire; @@ -224,6 +226,9 @@ it('lists every mailbox on the page', function () { Livewire::actingAs(Operator::factory()->role('Owner')->create(['password' => 'password']), 'operator') ->test(MailPage::class) + // Die Postfächer haben seit dem Umbau einen eigenen Reiter — vier + // Karten in einer Spalte waren der Grund dafür. + ->set('tab', 'postfaecher') ->assertSee('support@clupilot.com') ->assertSee('support'); }); @@ -369,6 +374,7 @@ it('shows "not yet verified" instead of a blank cell once the server change clea ->call('confirmPassword') ->set('host', 'new-relay.example.com') ->call('saveServer') + ->set('tab', 'postfaecher') ->assertSee(__('mail_settings.never_verified')); }); @@ -877,6 +883,7 @@ it('shows the result against the mailbox it actually tested', function () { Livewire::actingAs(Operator::factory()->role('Owner')->create(['password' => 'password']), 'operator') ->test(MailPage::class) + ->set('tab', 'postfaecher') ->set('testRecipient', 'ziel@example.com') ->call('test', $box->uuid) ->assertSee('support:') @@ -946,5 +953,88 @@ it('offers a per-row test-send button, not only edit', function () { it('tells the operator the test send really goes out, even though ordinary mail only logs here', function () { Livewire::actingAs(Operator::factory()->role('Owner')->create(['password' => 'password']), 'operator') ->test(MailPage::class) + ->set('tab', 'postfaecher') ->assertSee(__('mail_settings.test_hint')); }); + +/** + * ── Der Umbau auf Reiter ────────────────────────────────────────────────── + * + * Vier Karten in einer schmalen Spalte, davon zwei mit zusammen + * zweiundzwanzig gleich aussehenden Zeilen und zwei gleich aussehenden + * Speichern-Knöpfen: der Betreiber hat die Seite als unlesbar gemeldet. Die + * Tests hier halten fest, was der Umbau zusichert — nicht, wie er aussieht. + */ +it('falls back to the first tab when the address bar names one that does not exist', function () { + // Ein Reitername aus der Adresszeile ist eine Zeichenkette, die ein + // Fremder getippt hat. + Livewire::actingAs(Operator::factory()->role('Owner')->create(['password' => 'password']), 'operator') + ->test(MailPage::class) + ->set('tab', 'gibt-es-nicht') + ->assertSet('tab', 'versand'); +}); + +it('shows every mail type under its purpose, naming the address it would inherit', function () { + Mailbox::factory()->create(['key' => 'rechnung', 'address' => 'rechnung@clupilot.com', 'active' => true]); + Settings::set(MailPurpose::settingKey(MailPurpose::BILLING), 'rechnung'); + + Livewire::actingAs(Operator::factory()->role('Owner')->create(['password' => 'password']), 'operator') + ->test(MailPage::class) + ->set('tab', 'zuordnung') + // Die Mailart steht unter ihrem Zweck … + ->assertSee(MailCatalogue::all()['invoice']['label']) + // … und die leere Auswahl nennt die Adresse, die sonst greift, statt + // wie bisher nur den Namen des Zwecks. + ->assertSee(__('mail_settings.route_inherit', ['address' => 'rechnung@clupilot.com'])); +}); + +it('saves purposes and routing with the one button under the assignment tab', function () { + Mailbox::factory()->create(['key' => 'system', 'active' => true]); + Mailbox::factory()->create(['key' => 'rechnung', 'active' => true]); + + Livewire::actingAs(Operator::factory()->role('Owner')->create(['password' => 'password']), 'operator') + ->test(MailPage::class) + ->set('purposes.system', 'system') + ->set('routes.invoice', 'rechnung') + ->call('saveAssignments') + ->assertHasNoErrors(); + + expect(Settings::get(MailPurpose::settingKey(MailPurpose::SYSTEM)))->toBe('system') + ->and(Settings::get(MailRoute::settingKey('invoice')))->toBe('rechnung'); +}); + +it('writes no routing at all when the purposes it depends on are refused', function () { + // Die Reihenfolge im einen Knopf ist die Zusicherung: die Zwecke werden + // zuerst geprüft und werfen, BEVOR eine einzige Wegwahl geschrieben ist. + // Andernfalls hinterliesse eine abgelehnte Eingabe die halbe Zuordnung — + // Wegwahlen, die auf einen Zweck zeigen, den es so nicht gibt. + Mailbox::factory()->create(['key' => 'rechnung', 'active' => true]); + + Livewire::actingAs(Operator::factory()->role('Owner')->create(['password' => 'password']), 'operator') + ->test(MailPage::class) + ->set('purposes.system', '') // „System" ist der Rückfall und darf nicht leer sein + ->set('routes.invoice', 'rechnung') + ->call('saveAssignments') + ->assertHasErrors('purposes.system'); + + expect(Settings::get(MailRoute::settingKey('invoice')))->toBeNull(); +}); + +it('names the mailbox that would really carry the mail, not one that is switched off', function () { + // Codex R15, P2 am Umbau: das Etikett „wie der Zweck (…)" nannte das + // ZUGEORDNETE Postfach. MailboxResolver::for() lässt ein abgeschaltetes + // aber auf „System" zurückfallen, und writePurposes() erlaubt genau das — + // aktiv sein muss nur „System" selbst. Die Seite hätte dem Betreiber damit + // eine Adresse genannt, die nichts verschickt, und zwar an der Stelle, an + // der er nachsieht, um es richtig zu machen. + Mailbox::factory()->create(['key' => 'system', 'address' => 'noreply@clupilot.com', 'active' => true]); + Mailbox::factory()->create(['key' => 'rechnung', 'address' => 'rechnung@clupilot.com', 'active' => false]); + Settings::set(MailPurpose::settingKey(MailPurpose::SYSTEM), 'system'); + Settings::set(MailPurpose::settingKey(MailPurpose::BILLING), 'rechnung'); + + Livewire::actingAs(Operator::factory()->role('Owner')->create(['password' => 'password']), 'operator') + ->test(MailPage::class) + ->set('tab', 'zuordnung') + ->assertSee(__('mail_settings.route_inherit', ['address' => 'noreply@clupilot.com'])) + ->assertDontSee(__('mail_settings.route_inherit', ['address' => 'rechnung@clupilot.com'])); +});