Sicht auf die Spuren, und ein Schalter, der die Drossel abstellt
Wenn die Drossel je klemmt, muss der Ausweg ein Klick sein und kein Deployment. Eigene Seite (admin/mail-pace) statt eines fünften Abschnitts auf admin/mail: die dortige Seite ist bereits "alles in einer Wurst", und diese hier beobachtet laufenden Betrieb statt etwas einzurichten — deshalb auch in der Navigation unter "Betrieb", nicht unter "System". Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>feat/versandtakt
parent
c78866e360
commit
1886bf2076
|
|
@ -0,0 +1,168 @@
|
|||
<?php
|
||||
|
||||
namespace App\Livewire\Admin;
|
||||
|
||||
use App\Services\Mail\MailLane;
|
||||
use App\Support\Settings;
|
||||
use Illuminate\Support\Facades\Queue;
|
||||
use Livewire\Attributes\Layout;
|
||||
use Livewire\Component;
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* Der Notschalter und die Sicht auf die drei Versandspuren.
|
||||
*
|
||||
* Eigene Seite statt eines fünften Abschnitts auf admin/mail (App\Livewire\
|
||||
* Admin\Mail): dort liegen Server, Postfächer, Zwecke und Wegwahl bereits
|
||||
* „alles in einer Wurst" — eine weitere Scheibe wäre keine Verbesserung. Es
|
||||
* ist auch inhaltlich eine andere Art Seite: admin/mail RICHTET EIN (einmal,
|
||||
* und es gilt), diese hier BEOBACHTET UND GREIFT EIN (laufend, im Ernstfall
|
||||
* mit einem Klick) — deshalb steht sie in der Navigation unter „Betrieb", wo
|
||||
* schon Provisioning, Wartungen und Störungen stehen, nicht unter „System".
|
||||
*
|
||||
* Dieselbe Fähigkeit wie admin/mail (`mail.manage`): wer die Postfächer und
|
||||
* den Server sehen und ändern darf, darf auch die Spuren sehen und die
|
||||
* Drossel abstellen.
|
||||
*/
|
||||
#[Layout('layouts.admin')]
|
||||
class MailPace extends Component
|
||||
{
|
||||
public bool $enabled = true;
|
||||
|
||||
public int $urgentCount = 30;
|
||||
|
||||
public int $urgentMinutes = 5;
|
||||
|
||||
public int $calmCount = 20;
|
||||
|
||||
public int $calmMinutes = 10;
|
||||
|
||||
public function mount(): void
|
||||
{
|
||||
$this->authorize('mail.manage');
|
||||
|
||||
$this->enabled = Settings::bool('mail.pace.enabled', true);
|
||||
$this->urgentCount = (int) Settings::get('mail.pace.urgent.count', 30);
|
||||
$this->urgentMinutes = (int) Settings::get('mail.pace.urgent.minutes', 5);
|
||||
$this->calmCount = (int) Settings::get('mail.pace.calm.count', 20);
|
||||
$this->calmMinutes = (int) Settings::get('mail.pace.calm.minutes', 10);
|
||||
}
|
||||
|
||||
/**
|
||||
* Der Notschalter: nach dem Muster von Admin\Plans::toggleSales() — eine
|
||||
* Methode, eine Rückmeldung, kein Bestätigungsmodal. Er ist umkehrbar,
|
||||
* derselbe Klick legt ihn zurück.
|
||||
*/
|
||||
public function togglePace(): void
|
||||
{
|
||||
$this->authorize('mail.manage');
|
||||
|
||||
$this->enabled = ! $this->enabled;
|
||||
Settings::set('mail.pace.enabled', $this->enabled);
|
||||
|
||||
$this->dispatch('notify', message: __($this->enabled ? 'mail_pace.enabled_on_notice' : 'mail_pace.enabled_off_notice'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Die beiden Kontingente speichern.
|
||||
*
|
||||
* `min:1` auf allen vier Feldern ist die zweite Hälfte der Untergrenze aus
|
||||
* MailPaceServiceProvider::mindestensEins() — die liest nur ab, was hier
|
||||
* gar nicht erst gespeichert werden darf. Ohne diese Prüfung käme eine
|
||||
* eingetippte 0 ungeprüft bis in die Einstellung durch und flöge erst beim
|
||||
* nächsten Auftrag an der Lesestelle auf, nachdem der Betreiber schon
|
||||
* „gespeichert" gesehen hat — ein Kontingent von 0 heißt dort: jede Mail
|
||||
* wird endlos zurückgelegt, ein Fenster von 0 Minuten schaltet die Drossel
|
||||
* still ab.
|
||||
*/
|
||||
public function savePace(): void
|
||||
{
|
||||
$this->authorize('mail.manage');
|
||||
|
||||
$data = $this->validate([
|
||||
'urgentCount' => ['required', 'integer', 'min:1'],
|
||||
'urgentMinutes' => ['required', 'integer', 'min:1'],
|
||||
'calmCount' => ['required', 'integer', 'min:1'],
|
||||
'calmMinutes' => ['required', 'integer', 'min:1'],
|
||||
]);
|
||||
|
||||
Settings::set('mail.pace.urgent.count', $data['urgentCount']);
|
||||
Settings::set('mail.pace.urgent.minutes', $data['urgentMinutes']);
|
||||
Settings::set('mail.pace.calm.count', $data['calmCount']);
|
||||
Settings::set('mail.pace.calm.minutes', $data['calmMinutes']);
|
||||
|
||||
$this->dispatch('notify', message: __('mail_pace.saved'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Eine Mailklasse in eine andere Spur verschieben.
|
||||
*
|
||||
* Ein Ziel außerhalb der drei bekannten Spuren kommt nur über einen
|
||||
* manuellen Aufruf zustande — kein <select> im DOM bietet eines an — und
|
||||
* wird verworfen statt in die Einstellungen zu wandern, wo `MailLane::
|
||||
* for()` es beim nächsten Lesen ohnehin still auf „ruhig" zurückfallen
|
||||
* ließe, ohne dass der Betreiber sähe, dass sein Klick nichts bewirkt hat.
|
||||
*/
|
||||
public function move(string $mailableClass, string $lane): void
|
||||
{
|
||||
$this->authorize('mail.manage');
|
||||
|
||||
if (! in_array($lane, [MailLane::DIRECT, MailLane::URGENT, MailLane::CALM], true)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
MailLane::assign($mailableClass, $lane);
|
||||
} catch (RuntimeException) {
|
||||
// Gesperrt: der Server lehnt so oder so ab (Task 1), das Schloss in
|
||||
// der Ansicht ist nur die Höflichkeit davor. Wer trotzdem hier
|
||||
// ankommt — kein <select> bietet einer gesperrten Klasse eines an,
|
||||
// also nur ein manueller Aufruf — bekommt eine Meldung statt eines
|
||||
// Serverfehlers.
|
||||
$this->dispatch('notify', message: __('mail_pace.locked_notice'));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->dispatch('notify', message: __('mail_pace.moved'));
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
// Je Spur: die Warteschlange, die App\Mail\Concerns\RidesALane
|
||||
// tatsächlich befüllt (Task 2) — derselbe Name, den auch der Arbeiter
|
||||
// in docker-compose.yml abhört (Task 4). Im echten Betrieb (Redis)
|
||||
// eine ehrliche Zahl aus der tatsächlichen Liste; unter der
|
||||
// Sync-Warteschlange, die die Testsuite erzwingt, immer 0, weil dort
|
||||
// nichts je wartet statt verschickt zu werden — dort ist die Zahl
|
||||
// ebenso ehrlich, nur bedeutungslos für einen Testfall, der einen
|
||||
// Rückstand nachstellen wollte.
|
||||
$lanes = [
|
||||
[
|
||||
'key' => MailLane::DIRECT,
|
||||
'throttled' => false,
|
||||
'waiting' => Queue::size(MailLane::DIRECT),
|
||||
],
|
||||
[
|
||||
'key' => MailLane::URGENT,
|
||||
'throttled' => true,
|
||||
'waiting' => Queue::size(MailLane::URGENT),
|
||||
'countField' => 'urgentCount',
|
||||
'minutesField' => 'urgentMinutes',
|
||||
],
|
||||
[
|
||||
'key' => MailLane::CALM,
|
||||
'throttled' => true,
|
||||
'waiting' => Queue::size(MailLane::CALM),
|
||||
'countField' => 'calmCount',
|
||||
'minutesField' => 'calmMinutes',
|
||||
],
|
||||
];
|
||||
|
||||
return view('livewire.admin.mail-pace', [
|
||||
'lanes' => $lanes,
|
||||
'laneOptions' => [MailLane::DIRECT, MailLane::URGENT, MailLane::CALM],
|
||||
'assignments' => MailLane::all(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
@ -81,6 +81,9 @@ final class Navigation
|
|||
['admin.provisioning', 'activity', 'provisioning', null],
|
||||
['admin.maintenance', 'alert-triangle', 'maintenance', null],
|
||||
['admin.incidents', 'bell', 'incidents', null],
|
||||
// Läuft, statt eingerichtet zu werden — deshalb hier und nicht
|
||||
// neben admin.mail unter System.
|
||||
['admin.mail-pace', 'gauge', 'mail_pace', 'mail.manage'],
|
||||
]],
|
||||
// Alles, wo Geld drinsteht.
|
||||
['label' => __('admin.nav_group.billing'), 'items' => [
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ return [
|
|||
'payment_problems' => 'Zahlungsprobleme',
|
||||
'revenue' => 'Umsatz',
|
||||
'mail' => 'E-Mail',
|
||||
'mail_pace' => 'Versandtakt',
|
||||
'integrations' => 'Integrationen',
|
||||
'readiness' => 'Bereitschaft',
|
||||
'settings' => 'Einstellungen',
|
||||
|
|
|
|||
|
|
@ -0,0 +1,38 @@
|
|||
<?php
|
||||
|
||||
// Der Notschalter und die Sicht auf die drei Versandspuren. Eigene Datei statt
|
||||
// eines Abschnitts in mail_settings.php: eigene Seite, eigene Berechtigung an
|
||||
// derselben Stelle geprüft, aber ein eigenständiges Thema (laufender Betrieb,
|
||||
// nicht Einrichtung).
|
||||
return [
|
||||
'eyebrow' => 'Betrieb',
|
||||
'title' => 'Versandtakt',
|
||||
'subtitle' => 'Wie schnell die drei Spuren fahren, was gerade wartet, und der Schalter, der die Drossel im Ernstfall sofort abstellt.',
|
||||
|
||||
'enabled' => 'Drossel',
|
||||
'enabled_hint' => 'Ein Umlegen wirkt erst für neu eingereihte Mails voll: bereits wartende Aufträge behalten das Zeitfenster oder die Drossel, mit der sie eingereiht wurden.',
|
||||
'enabled_on' => 'Aktiv',
|
||||
'enabled_off' => 'Aus',
|
||||
'enabled_on_notice' => 'Drossel wieder eingeschaltet.',
|
||||
'enabled_off_notice' => 'Drossel abgeschaltet — jede Mail fährt jetzt ungebremst.',
|
||||
|
||||
'lanes_title' => 'Die drei Spuren',
|
||||
'lanes_sub' => 'Direkt fährt immer ungedrosselt — dort wartet gerade ein Mensch. Wichtig und ruhig haben ein eigenes Kontingent; beide Zahlen müssen mindestens eins sein, sonst wird entweder jede Mail endlos zurückgelegt oder gar nicht mehr gedrosselt.',
|
||||
'lane' => [
|
||||
'mail-direkt' => 'Direkt',
|
||||
'mail-wichtig' => 'Wichtig',
|
||||
'mail-ruhig' => 'Ruhig',
|
||||
],
|
||||
'unthrottled' => 'ungedrosselt',
|
||||
'count' => 'Kontingent',
|
||||
'minutes' => 'Minuten',
|
||||
'waiting' => ':count wartend',
|
||||
'save' => 'Speichern',
|
||||
'saved' => 'Takt gespeichert.',
|
||||
|
||||
'assignments_title' => 'Zuordnung',
|
||||
'assignments_sub' => 'Welche Mailklasse in welcher Spur fährt. Gesperrte Klassen fahren immer direkt — darauf wartet gerade jemand, drosseln würde dort nur schaden.',
|
||||
'locked_hint' => 'Immer direkt (gesperrt)',
|
||||
'locked_notice' => 'Diese Mail fährt immer direkt: darauf wartet gerade jemand.',
|
||||
'moved' => 'Verschoben.',
|
||||
];
|
||||
|
|
@ -37,6 +37,7 @@ return [
|
|||
'payment_problems' => 'Payment problems',
|
||||
'revenue' => 'Revenue',
|
||||
'mail' => 'Email',
|
||||
'mail_pace' => 'Send pace',
|
||||
'integrations' => 'Integrations',
|
||||
'readiness' => 'Readiness',
|
||||
'settings' => 'Settings',
|
||||
|
|
|
|||
|
|
@ -0,0 +1,37 @@
|
|||
<?php
|
||||
|
||||
// The kill switch and the view onto the three send lanes. Its own file rather
|
||||
// than a section of mail_settings.php: its own page, the same capability
|
||||
// check, but a different subject (running operation, not one-time setup).
|
||||
return [
|
||||
'eyebrow' => 'Operations',
|
||||
'title' => 'Send pace',
|
||||
'subtitle' => 'How fast the three lanes move, what is waiting right now, and the switch that stops the throttle immediately if it jams.',
|
||||
|
||||
'enabled' => 'Throttle',
|
||||
'enabled_hint' => 'Flipping this only fully applies to mail queued from now on: jobs already waiting keep whichever time window or throttle they were queued with.',
|
||||
'enabled_on' => 'On',
|
||||
'enabled_off' => 'Off',
|
||||
'enabled_on_notice' => 'Throttle switched back on.',
|
||||
'enabled_off_notice' => 'Throttle switched off — every mail now sends unthrottled.',
|
||||
|
||||
'lanes_title' => 'The three lanes',
|
||||
'lanes_sub' => 'Direct always runs unthrottled — a human is waiting there right now. Urgent and calm each have their own allowance; both numbers must be at least one, or mail is either delayed forever or not throttled at all.',
|
||||
'lane' => [
|
||||
'mail-direkt' => 'Direct',
|
||||
'mail-wichtig' => 'Urgent',
|
||||
'mail-ruhig' => 'Calm',
|
||||
],
|
||||
'unthrottled' => 'unthrottled',
|
||||
'count' => 'Allowance',
|
||||
'minutes' => 'Minutes',
|
||||
'waiting' => ':count waiting',
|
||||
'save' => 'Save',
|
||||
'saved' => 'Pace saved.',
|
||||
|
||||
'assignments_title' => 'Assignment',
|
||||
'assignments_sub' => 'Which mail class rides in which lane. Locked classes always ride direct — someone is waiting on them right now, and throttling would only hurt.',
|
||||
'locked_hint' => 'Always direct (locked)',
|
||||
'locked_notice' => 'This mail always rides direct: someone is waiting on it right now.',
|
||||
'moved' => 'Moved.',
|
||||
];
|
||||
|
|
@ -0,0 +1,93 @@
|
|||
<div class="space-y-6">
|
||||
<header class="animate-rise">
|
||||
<p class="lbl">{{ __('mail_pace.eyebrow') }}</p>
|
||||
<h1 class="mt-[7px] text-[23px] font-bold leading-[1.12] tracking-[-0.03em] text-ink min-[901px]:text-[30px]">
|
||||
{{ __('mail_pace.title') }}
|
||||
</h1>
|
||||
<p class="mt-2 max-w-[76ch] text-sm leading-relaxed text-muted">{{ __('mail_pace.subtitle') }}</p>
|
||||
</header>
|
||||
|
||||
{{-- Der Notschalter, ganz oben und in einem eigenen Panel: das ist der
|
||||
Griff, den jemand im Ernstfall zuerst sucht, nicht der letzte von
|
||||
mehreren. Kein Bestätigungsmodal (R23 verlangt eines nur dort, wo ein
|
||||
Vorgang Folgen hat, die man nicht zurückdrehen kann) — dieser Schalter
|
||||
ist umkehrbar, derselbe Klick legt ihn zurück, genau wie
|
||||
Admin\Plans::toggleSales(). --}}
|
||||
<x-ui.panel class="animate-rise [animation-delay:20ms]">
|
||||
<div class="px-6 py-4">
|
||||
<x-ui.switch name="pace_enabled" wire:click="togglePace" :checked="$enabled"
|
||||
:label="__('mail_pace.enabled')"
|
||||
:hint="__('mail_pace.enabled_hint')"
|
||||
:on="__('mail_pace.enabled_on')"
|
||||
:off="__('mail_pace.enabled_off')" />
|
||||
</div>
|
||||
</x-ui.panel>
|
||||
|
||||
{{-- Die drei Spuren: Name, Takt, was gerade wartet. --}}
|
||||
<form wire:submit="savePace" class="animate-rise [animation-delay:40ms]">
|
||||
<h2 class="text-md font-bold tracking-[-0.01em] text-ink">{{ __('mail_pace.lanes_title') }}</h2>
|
||||
<p class="mt-1 max-w-[70ch] text-sm leading-relaxed text-muted">{{ __('mail_pace.lanes_sub') }}</p>
|
||||
|
||||
<x-ui.panel class="mt-4">
|
||||
@foreach ($lanes as $lane)
|
||||
<x-ui.row :label="__('mail_pace.lane.'.$lane['key'])" :for="$lane['throttled'] ? $lane['countField'] : null">
|
||||
<div class="flex flex-wrap items-end gap-3">
|
||||
<span class="font-mono text-xs text-muted">{{ $lane['key'] }}</span>
|
||||
|
||||
@if ($lane['throttled'])
|
||||
<div class="w-24">
|
||||
<x-ui.input :name="$lane['countField']" type="number" min="1"
|
||||
wire:model="{{ $lane['countField'] }}"
|
||||
:label="__('mail_pace.count')" />
|
||||
</div>
|
||||
<div class="w-28">
|
||||
<x-ui.input :name="$lane['minutesField']" type="number" min="1"
|
||||
wire:model="{{ $lane['minutesField'] }}"
|
||||
:label="__('mail_pace.minutes')" />
|
||||
</div>
|
||||
@else
|
||||
<x-ui.badge status="info">{{ __('mail_pace.unthrottled') }}</x-ui.badge>
|
||||
@endif
|
||||
|
||||
<x-ui.badge status="info">{{ __('mail_pace.waiting', ['count' => $lane['waiting']]) }}</x-ui.badge>
|
||||
</div>
|
||||
{{-- Kein eigenes @error hier: x-ui.input zeigt die Meldung
|
||||
zum jeweiligen Feld schon selbst unter sich an. --}}
|
||||
</x-ui.row>
|
||||
@endforeach
|
||||
</x-ui.panel>
|
||||
|
||||
<div class="mt-4 flex justify-end">
|
||||
<x-ui.button variant="primary" type="submit">{{ __('mail_pace.save') }}</x-ui.button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{{-- Die Zuordnung: welche Mailklasse in welcher Spur fährt. Ein <select>
|
||||
pro Zeile, R20's ausdrückliche Ausnahme — dieselbe Zeile, kein
|
||||
Höhensprung. Gesperrte Klassen zeigen ein Schloss statt der Auswahl:
|
||||
der Server lehnt sie ohnehin ab (Task 1, MailLane::assign()), das
|
||||
Schloss ist nur die Höflichkeit davor. --}}
|
||||
<section class="animate-rise [animation-delay:60ms]">
|
||||
<h2 class="text-md font-bold tracking-[-0.01em] text-ink">{{ __('mail_pace.assignments_title') }}</h2>
|
||||
<p class="mt-1 max-w-[70ch] text-sm leading-relaxed text-muted">{{ __('mail_pace.assignments_sub') }}</p>
|
||||
|
||||
<x-ui.panel class="mt-4">
|
||||
@foreach ($assignments as $class => $lane)
|
||||
<x-ui.row :label="class_basename($class)">
|
||||
@if (\App\Services\Mail\MailLane::isLocked($class))
|
||||
<span class="inline-flex items-center gap-1.5 text-sm text-muted">
|
||||
<x-ui.icon name="lock" class="size-4" />{{ __('mail_pace.locked_hint') }}
|
||||
</span>
|
||||
@else
|
||||
<select wire:change="move('{{ $class }}', $event.target.value)"
|
||||
class="w-48 rounded-md border border-line-strong bg-surface px-3 py-2 text-sm text-body">
|
||||
@foreach ($laneOptions as $option)
|
||||
<option value="{{ $option }}" @selected($lane === $option)>{{ __('mail_pace.lane.'.$option) }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
@endif
|
||||
</x-ui.row>
|
||||
@endforeach
|
||||
</x-ui.panel>
|
||||
</section>
|
||||
</div>
|
||||
|
|
@ -123,6 +123,10 @@ Route::get('/invoices/{uuid}/pdf', function (string $uuid) {
|
|||
);
|
||||
})->name('invoices.pdf');
|
||||
Route::get('/mail', Admin\Mail::class)->name('mail');
|
||||
// Der Notschalter und die Sicht auf die drei Versandspuren — eigene Route statt
|
||||
// eines weiteren Abschnitts auf admin/mail (siehe deren Kommentar oben), weil
|
||||
// diese Seite den laufenden Betrieb beobachtet statt etwas einzurichten.
|
||||
Route::get('/mail-pace', Admin\MailPace::class)->name('mail-pace');
|
||||
Route::get('/integrations', Admin\Integrations::class)->name('integrations');
|
||||
// The former admin.secrets and admin.infrastructure pages, merged into the
|
||||
// one above — grouped by what each value configures, not by which of the two
|
||||
|
|
|
|||
|
|
@ -43,7 +43,8 @@ it('verliert beim Umsortieren keinen Eintrag und legt keinen doppelt an', functi
|
|||
|
||||
// Die Zahl steht hier bewusst als Zahl: sinkt sie, ist beim Umsortieren
|
||||
// ein Eintrag unter den Tisch gefallen, und genau das sähe niemand.
|
||||
expect($routes)->toHaveCount(27);
|
||||
// 28 statt 27 seit admin.mail-pace (Versandtakt) dazukam.
|
||||
expect($routes)->toHaveCount(28);
|
||||
});
|
||||
|
||||
it('führt keinen Eintrag, dessen Route es nicht gibt', function () {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,107 @@
|
|||
<?php
|
||||
|
||||
use App\Livewire\Admin\MailPace;
|
||||
use App\Mail\InvoiceMail;
|
||||
use App\Mail\ResetPasswordMail;
|
||||
use App\Services\Mail\MailLane;
|
||||
use App\Support\Settings;
|
||||
use Livewire\Livewire;
|
||||
|
||||
/**
|
||||
* Die Sicht auf die drei Spuren, und der Notschalter davor.
|
||||
*
|
||||
* `mail.manage` ist dieselbe Fähigkeit wie auf admin/mail (App\Livewire\
|
||||
* Admin\Mail) — dort liegen die anderen Maileinstellungen, und wer die lesen
|
||||
* und ändern darf, darf auch sehen, was in den Spuren wartet, und die Drossel
|
||||
* abstellen.
|
||||
*/
|
||||
it('zeigt jede Spur mit ihrem Takt und dem, was wartet', function () {
|
||||
Livewire::actingAs(operator('Owner'), 'operator')
|
||||
->test(MailPace::class)
|
||||
->assertSee(MailLane::DIRECT)
|
||||
->assertSee(MailLane::URGENT)
|
||||
->assertSee(MailLane::CALM);
|
||||
});
|
||||
|
||||
it('laesst den Betreiber eine Mail verschieben', function () {
|
||||
Livewire::actingAs(operator('Owner'), 'operator')
|
||||
->test(MailPace::class)
|
||||
->call('move', InvoiceMail::class, MailLane::URGENT);
|
||||
|
||||
expect(MailLane::for(InvoiceMail::class))->toBe(MailLane::URGENT);
|
||||
});
|
||||
|
||||
it('verschiebt eine gesperrte Mail nicht, auch nicht ueber die Komponente', function () {
|
||||
Livewire::actingAs(operator('Owner'), 'operator')
|
||||
->test(MailPace::class)
|
||||
->call('move', ResetPasswordMail::class, MailLane::CALM);
|
||||
|
||||
expect(MailLane::for(ResetPasswordMail::class))->toBe(MailLane::DIRECT);
|
||||
});
|
||||
|
||||
it('zeigt eine gesperrte Klasse als gesperrt statt mit einem Auswahlfeld', function () {
|
||||
// Das Schloss ist nur die Höflichkeit vor der serverseitigen Ablehnung im
|
||||
// Test darüber — aber eine Auswahl, die eine gesperrte Klasse anbietet,
|
||||
// wäre die Einladung zu einem Klick, der ohnehin nichts bewirkt.
|
||||
$html = Livewire::actingAs(operator('Owner'), 'operator')
|
||||
->test(MailPace::class)
|
||||
->assertSee(class_basename(ResetPasswordMail::class))
|
||||
->html();
|
||||
|
||||
expect($html)->not->toContain("move('".ResetPasswordMail::class."'");
|
||||
});
|
||||
|
||||
it('schaltet die Drossel ab und wieder an', function () {
|
||||
$component = Livewire::actingAs(operator('Owner'), 'operator')
|
||||
->test(MailPace::class)
|
||||
->call('togglePace');
|
||||
|
||||
expect(Settings::bool('mail.pace.enabled', true))->toBeFalse();
|
||||
|
||||
$component->call('togglePace');
|
||||
|
||||
expect(Settings::bool('mail.pace.enabled', true))->toBeTrue();
|
||||
});
|
||||
|
||||
/**
|
||||
* Die zweite Hälfte der Untergrenze aus MailPaceServiceProvider::
|
||||
* mindestensEins() — die dortige liest nur ab, was hier gar nicht erst
|
||||
* gespeichert werden darf. Ohne diese Prüfung käme eine eingetippte 0 durch
|
||||
* bis zur Einstellung und würde erst beim nächsten Auftrag an der Lesestelle
|
||||
* abgefangen, nachdem der Betreiber schon „gespeichert" gesehen hat.
|
||||
*/
|
||||
it('verweigert ein Kontingent von null oder weniger', function () {
|
||||
Livewire::actingAs(operator('Owner'), 'operator')
|
||||
->test(MailPace::class)
|
||||
->set('urgentCount', 0)
|
||||
->call('savePace')
|
||||
->assertHasErrors(['urgentCount' => 'min']);
|
||||
|
||||
expect(Settings::get('mail.pace.urgent.count'))->toBeNull();
|
||||
});
|
||||
|
||||
it('verweigert ein Fenster von null Minuten', function () {
|
||||
Livewire::actingAs(operator('Owner'), 'operator')
|
||||
->test(MailPace::class)
|
||||
->set('calmMinutes', 0)
|
||||
->call('savePace')
|
||||
->assertHasErrors(['calmMinutes' => 'min']);
|
||||
|
||||
expect(Settings::get('mail.pace.calm.minutes'))->toBeNull();
|
||||
});
|
||||
|
||||
it('speichert einen gueltigen Takt fuer beide gedrosselten Spuren', function () {
|
||||
Livewire::actingAs(operator('Owner'), 'operator')
|
||||
->test(MailPace::class)
|
||||
->set('urgentCount', 12)
|
||||
->set('urgentMinutes', 3)
|
||||
->set('calmCount', 7)
|
||||
->set('calmMinutes', 9)
|
||||
->call('savePace')
|
||||
->assertHasNoErrors();
|
||||
|
||||
expect(Settings::get('mail.pace.urgent.count'))->toBe(12)
|
||||
->and(Settings::get('mail.pace.urgent.minutes'))->toBe(3)
|
||||
->and(Settings::get('mail.pace.calm.count'))->toBe(7)
|
||||
->and(Settings::get('mail.pace.calm.minutes'))->toBe(9);
|
||||
});
|
||||
Loading…
Reference in New Issue