Ein Knopf, der eine Mail verspricht, die niemand verschicken kann
Die zweite Haelfte von K4. Die Wanderung legt das Versandkonto an; solange es niemand ausgefuellt hat, bestand die Attrappe unveraendert fort — und sie ist nicht der Randfall, sondern der Normalfall: der Mailserver dieses Produkts entsteht gerade erst, das Postfach ist am ersten Tag garantiert leer. Die Einladung verschickt NEXTCLOUD, nicht CluPilot — nur so entsteht das Passwort dort, wo niemand sonst es zu sehen bekommt. Ohne eingerichteten Versand kommt dort aber nichts heraus, und `occ user:add --generate-password --email` gelingt trotzdem: Nextcloud legt das Konto an, versucht die Willkommensmail, protokolliert intern einen Fehler und beendet mit 0. Der Auftrag meldete Erfolg, die Zeile sprang auf "Eingeladen", die Meldung versprach einen Link, an dem der Mitarbeiter sein Passwort selbst setzt — und niemand bekam etwas. sendInvite() fragt jetzt GuestMailConfig::for($instance)->available(): ein reiner Blick in die Datenbank, kein Tunnel, keine Warteschlange, deshalb darf er auf der Seite stehen. Steht der Versand nicht, geht kein Auftrag hinaus, der Sitz bleibt unveraendert — insbesondere ohne Anmeldenamen — und die Meldung sagt, dass nichts verschickt wurde und woran es liegt. Drei Entscheidungen dabei: Der Knopf bleibt stehen, der Hinweis steht ueber der Tabelle. Ihn an jeder Zeile verschwinden zu lassen liest sich nicht als "geht hier gerade nicht", sondern als "das kann dieses Produkt nicht" — genau die Beschwerde, die schon einmal dazu gefuehrt hat, dass die Aktionsspalte immer gezeichnet wird. Und der Zustand ist voruebergehend: er endet, sobald der Betrieb das Konto ausfuellt. Die Wache steht VOR dem Ratelimit. Sonst haette ein Inhaber seine Versuche aufgebraucht, bevor ueberhaupt einer hinausgehen konnte. Ohne laufende Instanz greift sie gar nicht: dann scheitert der Auftrag ohnehin an `no_instance` und die Zeile sagt das im Klartext. Diese Wache gilt dem anderen Fall — die Cloud laeuft, nur der Versand fehlt. Das Anlegen bleibt offen: es verspricht ausdruecklich keine Mail, und der Hinweistext sagt das auch. Der Bereitstellungsschritt ist unangetastet und bleibt bei "nicht eingerichtet ist etwas anderes als kaputt". Fuenf Zusicherungen, drei davon gegen den zurueckgedrehten Fix rot gesehen; die uebrigen zwei sind Grenzpruefungen und muessen in beide Richtungen gruen sein. Acht Bestandspruefungen richten den Versand jetzt ueber eine eigene Hilfsfunktion ein — zwei davon haetten sonst aus dem falschen Grund bestanden. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>feat/versandtakt
parent
01eff483fa
commit
40f255565b
|
|
@ -6,6 +6,7 @@ use App\Livewire\Concerns\ResolvesCustomer;
|
|||
use App\Models\Customer;
|
||||
use App\Models\Seat;
|
||||
use App\Provisioning\Jobs\SyncSeatToNextcloud;
|
||||
use App\Services\Mail\GuestMailConfig;
|
||||
use Illuminate\Database\UniqueConstraintViolationException;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\RateLimiter;
|
||||
|
|
@ -226,6 +227,28 @@ class Users extends Component
|
|||
return;
|
||||
}
|
||||
|
||||
// Die Einladung verschickt NEXTCLOUD, nicht CluPilot — nur so entsteht
|
||||
// das Passwort dort, wo niemand sonst es zu sehen bekommt. Ohne
|
||||
// eingerichteten Mailversand kommt dort aber nichts heraus, und
|
||||
// `occ user:add --generate-password --email` GELINGT trotzdem:
|
||||
// Nextcloud legt das Konto an, versucht die Willkommensmail,
|
||||
// protokolliert intern einen Fehler und beendet mit 0. Der Auftrag
|
||||
// meldete Erfolg, die Zeile sprang auf „Eingeladen", die Meldung
|
||||
// versprach einen Link — und niemand bekam etwas.
|
||||
//
|
||||
// Am Tag der Auslieferung ist das kein Randfall, sondern der
|
||||
// Normalzustand: der Mailserver dieses Produkts entsteht gerade erst,
|
||||
// und das Postfach steht bis dahin leer.
|
||||
//
|
||||
// VOR dem Ratelimit, damit ein abgewiesener Versuch kein Kontingent
|
||||
// verbraucht: sonst hätte der Inhaber seine Versuche aufgebraucht,
|
||||
// bevor überhaupt einer hinausgehen konnte.
|
||||
if (! $this->mailversandBereit($customer)) {
|
||||
$this->dispatch('notify', message: __('users.invite_no_mail'));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (($warten = $this->rateLimited($customer, $seat)) !== null) {
|
||||
$this->dispatch('notify', message: __('users.too_many_invites', ['minutes' => $warten]));
|
||||
|
||||
|
|
@ -550,6 +573,36 @@ class Users extends Component
|
|||
$this->revoke($uuid);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ob eine Einladung dieses Kunden überhaupt ankommen könnte.
|
||||
*
|
||||
* Ein reiner Blick in die Datenbank — Servereinstellung und
|
||||
* Versandkonto — kein Tunnel, keine Warteschlange. Deshalb darf er auf der
|
||||
* Seite stehen und nicht erst im Auftrag.
|
||||
*
|
||||
* OHNE Instanz gibt es hier nichts zu entscheiden: dann scheitert der
|
||||
* Auftrag ohnehin an `no_instance`, und die Zeile sagt das im Klartext.
|
||||
* Diese Wache gilt dem anderen Fall — die Cloud LÄUFT, nur der Versand
|
||||
* fehlt. Genau dort meldet occ Erfolg, und niemand bekommt eine Mail.
|
||||
*
|
||||
* Dieselbe Auswahl wie im Auftrag (SyncSeatToNextcloud): die Instanz, in
|
||||
* der die Einladung tatsächlich landen würde.
|
||||
*
|
||||
* Der Bereitstellungsschritt bleibt davon unberührt. Dort gilt weiter
|
||||
* „nicht eingerichtet ist etwas anderes als kaputt", und eine bezahlte
|
||||
* Bestellung läuft durch — eine Cloud ohne Mailversand funktioniert
|
||||
* vollständig. Was sie nicht kann, ist eine Einladung verschicken, und nur
|
||||
* darum geht es hier.
|
||||
*/
|
||||
private function mailversandBereit(Customer $customer): bool
|
||||
{
|
||||
$instance = $customer->instances()
|
||||
->whereIn('status', ['active', 'cancellation_scheduled'])
|
||||
->latest('id')->first();
|
||||
|
||||
return $instance === null || GuestMailConfig::for($instance)->available();
|
||||
}
|
||||
|
||||
private function usedSeats(Customer $customer): int
|
||||
{
|
||||
return $customer->seats()->where('status', '!=', 'revoked')->count();
|
||||
|
|
@ -583,6 +636,9 @@ class Users extends Component
|
|||
'used' => $customer ? $this->usedSeats($customer) : 0,
|
||||
'limit' => $customer ? $this->seatLimit($customer) : 0,
|
||||
'roles' => Seat::ROLES,
|
||||
// Der Hinweis steht dort, wo der Inhaber ihn liest, bevor er
|
||||
// drückt. Der Knopf bleibt trotzdem stehen — siehe das Blade.
|
||||
'versandBereit' => $customer === null || $this->mailversandBereit($customer),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -90,6 +90,12 @@ return [
|
|||
'added' => 'Benutzer angelegt. Die Einladung verschicken Sie mit „Einladen".',
|
||||
'reinstated' => 'Zugang wieder aufgenommen. Die Einladung verschicken Sie mit „Einladen".',
|
||||
'invite_sent' => 'Einladung verschickt. Der Mitarbeiter bekommt einen Link, an dem er sein Passwort selbst setzt.',
|
||||
// Die Einladung verschickt die Nextcloud des Kunden selbst — ohne
|
||||
// eingerichteten Mailversand kommt dort nichts heraus, und `occ user:add`
|
||||
// meldet trotzdem Erfolg. Der Satz sagt deshalb beides: dass nichts
|
||||
// verschickt wurde, und woran es liegt.
|
||||
'mail_not_ready' => 'Einladungen sind noch nicht möglich: Der Mailversand für Ihre Cloud ist noch nicht eingerichtet. Die Einladung verschickt Ihre Cloud selbst — ohne Versand bekäme niemand eine Mail. Bitte wenden Sie sich an den Support. Anlegen können Sie Ihre Leute schon jetzt.',
|
||||
'invite_no_mail' => 'Es wurde nichts verschickt: Der Mailversand für Ihre Cloud ist noch nicht eingerichtet. Bitte wenden Sie sich an den Support.',
|
||||
'too_many_invites' => 'Zu viele Einladungen — in :minutes Minuten wieder möglich.',
|
||||
'revoked' => 'Benutzer entfernt.',
|
||||
'resent' => 'Einladung erneut gesendet.',
|
||||
|
|
|
|||
|
|
@ -89,6 +89,12 @@ return [
|
|||
'added' => 'User added. Send the invitation with "Invite".',
|
||||
'reinstated' => 'Access taken back on. Send the invitation with "Invite".',
|
||||
'invite_sent' => 'Invitation sent. They get a link to set their own password.',
|
||||
// The invitation is sent by the customer's own Nextcloud — with no mail
|
||||
// delivery configured, nothing leaves it, and `occ user:add` reports
|
||||
// success all the same. The sentence therefore says both: that nothing
|
||||
// was sent, and why.
|
||||
'mail_not_ready' => 'Invitations are not possible yet: mail delivery for your cloud is not set up. The invitation is sent by your own cloud — without delivery, nobody would receive anything. Please contact support. You can already add your people.',
|
||||
'invite_no_mail' => 'Nothing was sent: mail delivery for your cloud is not set up yet. Please contact support.',
|
||||
'too_many_invites' => 'Too many invitations — possible again in :minutes minutes.',
|
||||
'revoked' => 'User removed.',
|
||||
'resent' => 'Invitation resent.',
|
||||
|
|
|
|||
|
|
@ -11,9 +11,9 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Anlegen. Ausdruecklich NICHT einladen: hier entsteht nur die Zeile,
|
||||
{{-- Anlegen. Ausdrücklich NICHT einladen: hier entsteht nur die Zeile,
|
||||
die Einladung verschickt der Knopf in der Tabelle. Ein Inhaber soll
|
||||
sein Team vorbereiten koennen, ohne dass jemand eine Mail bekommt. --}}
|
||||
sein Team vorbereiten können, ohne dass jemand eine Mail bekommt. --}}
|
||||
<form wire:submit="addSeat" class="grid grid-cols-1 gap-3 rounded-lg border border-line bg-surface p-5 shadow-xs animate-rise [animation-delay:60ms] sm:grid-cols-[1fr_1fr_auto_auto] sm:items-end">
|
||||
<div>
|
||||
<label class="text-sm font-medium text-body" for="inviteEmail">{{ __('users.invite_email') }}</label>
|
||||
|
|
@ -40,6 +40,25 @@
|
|||
</x-ui.button>
|
||||
</form>
|
||||
|
||||
{{-- Der Mailversand fehlt: gesagt, bevor jemand drückt.
|
||||
|
||||
Der Einladen-Knopf bleibt trotzdem stehen. Ihn an jeder Zeile
|
||||
verschwinden zu lassen liest sich nicht als „geht hier gerade nicht",
|
||||
sondern als „das kann dieses Produkt nicht" — genau die Beschwerde,
|
||||
die schon einmal dazu geführt hat, dass die Aktionsspalte IMMER
|
||||
gezeichnet wird (siehe Users::render()). Und der Zustand ist
|
||||
vorübergehend: er endet, sobald der Betrieb das Versandkonto
|
||||
ausfüllt, ohne dass sich an dieser Seite etwas ändert.
|
||||
|
||||
Einmal über der Tabelle statt einmal je Zeile: der Grund ist für
|
||||
alle derselbe, und an jeder Zeile stünde derselbe Satz noch einmal.
|
||||
Der Server weist die Einladung mit derselben Begründung ab — der
|
||||
Knopf verspricht also nichts, was der Hinweis nicht schon
|
||||
zurücknimmt. --}}
|
||||
@unless ($versandBereit)
|
||||
<x-ui.alert variant="warning" class="animate-rise [animation-delay:90ms]">{{ __('users.mail_not_ready') }}</x-ui.alert>
|
||||
@endunless
|
||||
|
||||
{{-- Seats --}}
|
||||
<div class="overflow-hidden rounded-lg border border-line bg-surface shadow-xs animate-rise [animation-delay:120ms]">
|
||||
<div class="overflow-x-auto">
|
||||
|
|
@ -73,10 +92,10 @@
|
|||
{{-- Was der Inhaber WILL (`status`) und was in der
|
||||
Nextcloud WIRKLICH ist (`nc_state`) stehen in
|
||||
einer Zelle, und der wahre Zustand geht vor:
|
||||
solange der Auftrag laeuft oder gescheitert
|
||||
ist, waere „Eingeladen" eine Behauptung ueber
|
||||
solange der Auftrag läuft oder gescheitert
|
||||
ist, wäre „Eingeladen" eine Behauptung über
|
||||
etwas, das die Warteschlange noch gar nicht
|
||||
erledigt hat. Ohne diese Spalte drueckt der
|
||||
erledigt hat. Ohne diese Spalte drückt der
|
||||
Inhaber wieder und wieder, weil nichts
|
||||
sichtbar geschieht. --}}
|
||||
<td class="px-4 py-3">
|
||||
|
|
@ -84,7 +103,7 @@
|
|||
{{-- Entzogen geht dem Zustand vor: ein
|
||||
„wird eingerichtet …" an einer
|
||||
geschlossenen Zeile las sich wie ein
|
||||
Weg zurueck, den es an DIESER Zeile
|
||||
Weg zurück, den es an DIESER Zeile
|
||||
nicht gibt. --}}
|
||||
<x-ui.badge status="suspended">{{ __('users.status_revoked') }}</x-ui.badge>
|
||||
<p class="mt-1 text-xs text-muted">{{ __('users.revoked_closed') }}</p>
|
||||
|
|
@ -98,15 +117,15 @@
|
|||
@endif
|
||||
|
||||
{{-- Der Fehlschlag steht NEBEN dem Entzogen,
|
||||
nicht dahinter. Sonst verschwaende genau
|
||||
dort die Rueckfahrkarte, wo sie am
|
||||
noetigsten ist: scheitert das `disable`,
|
||||
nicht dahinter. Sonst verschwände genau
|
||||
dort die Rückfahrkarte, wo sie am
|
||||
nötigsten ist: scheitert das `disable`,
|
||||
ist das Konto im Gast weiter offen,
|
||||
waehrend die Zeile „entzogen" sagt — kein
|
||||
während die Zeile „entzogen" sagt — kein
|
||||
Wiederholungslauf (tries = 1), kein
|
||||
Abgleich im Zeitplan, und diese Seite ist
|
||||
die einzige Stelle im Repo, die nc_state
|
||||
ueberhaupt anzeigt. --}}
|
||||
überhaupt anzeigt. --}}
|
||||
@if ($seat->nc_state === \App\Models\Seat::STATE_FAILED)
|
||||
<div @class(['mt-1' => $seat->status === 'revoked'])>
|
||||
<x-ui.badge status="failed">{{ __('users.state_failed') }}</x-ui.badge>
|
||||
|
|
@ -117,10 +136,10 @@
|
|||
<p class="mt-1 text-xs text-muted">{{ __('users.error_'.($seat->nc_error ?: 'unexpected')) }}</p>
|
||||
@php
|
||||
// Dieselbe Bedingung wie in retry(): ein Knopf, der
|
||||
// nur wieder rot zurueckkommt, ist schlimmer als
|
||||
// nur wieder rot zurückkommt, ist schlimmer als
|
||||
// keiner. Eine entzogene oder gesperrte Zeile, die
|
||||
// nie in der Nextcloud ankam, hat dort nichts zu
|
||||
// sperren — da gaebe es nichts zu wiederholen.
|
||||
// sperren — da gäbe es nichts zu wiederholen.
|
||||
$wiederholbar = $seat->role !== 'owner'
|
||||
&& ($seat->nc_synced_at !== null
|
||||
|| ! in_array($seat->status, ['revoked', 'suspended'], true));
|
||||
|
|
@ -137,11 +156,11 @@
|
|||
<div class="flex items-center justify-end gap-1.5">
|
||||
@if ($seat->status === 'revoked')
|
||||
{{-- Kein Knopf an der Zeile. Der Weg
|
||||
zurueck geht durch die Vordertuer: das
|
||||
zurück geht durch die Vordertür: das
|
||||
Anlegen-Formular oben, mit derselben
|
||||
Adresse — und damit durch die
|
||||
Platzpruefung, die es an der Zeile
|
||||
nicht gaebe. Gesagt statt leer
|
||||
Platzprüfung, die es an der Zeile
|
||||
nicht gäbe. Gesagt statt leer
|
||||
gelassen, wie bei der Inhaber-Zeile —
|
||||
eine leere Zelle liesse jemanden nach
|
||||
einem Knopf suchen, der absichtlich
|
||||
|
|
@ -153,7 +172,7 @@
|
|||
steht nur an einer Zeile, die noch
|
||||
nichts in der Nextcloud hat; ist der
|
||||
Auftrag unterwegs, gibt es nichts zu
|
||||
druecken, und bei einem Fehlschlag
|
||||
drücken, und bei einem Fehlschlag
|
||||
steht „Nochmal versuchen" schon in der
|
||||
Zustandsspalte. Nie an der
|
||||
Inhaber-Zeile: dieses Konto legt die
|
||||
|
|
|
|||
|
|
@ -6,15 +6,37 @@ use App\Livewire\Users;
|
|||
use App\Models\Customer;
|
||||
use App\Models\Host;
|
||||
use App\Models\Instance;
|
||||
use App\Models\Mailbox;
|
||||
use App\Models\Seat;
|
||||
use App\Provisioning\Jobs\SyncSeatToNextcloud;
|
||||
use App\Services\Mail\GuestMailConfig;
|
||||
use App\Services\Nextcloud\NextcloudUsers;
|
||||
use App\Services\Proxmox\FakeProxmoxClient;
|
||||
use App\Services\Proxmox\ProxmoxClient;
|
||||
use App\Support\Settings;
|
||||
use Illuminate\Support\Facades\Queue;
|
||||
use Livewire\Livewire;
|
||||
use Symfony\Component\Finder\Finder;
|
||||
|
||||
/**
|
||||
* Der Mailversand, eingerichtet — Server und ausgefülltes Versandkonto.
|
||||
*
|
||||
* Steht jetzt in jedem Test, der wirklich einladen will: die Einladung
|
||||
* verschickt die Nextcloud des Kunden selbst, und ohne Versand käme dort nie
|
||||
* eine Mail heraus. Angelegt ist das Postfach ab der eigenen Wanderung — aber
|
||||
* inaktiv und ohne Zugangsdaten, also genau der Zustand, den „Einladen" jetzt
|
||||
* abweist.
|
||||
*/
|
||||
function mailversandEingerichtet(): void
|
||||
{
|
||||
Settings::set('mail.host', 'mail.clupilot.cloud');
|
||||
Settings::set('mail.port', 587);
|
||||
|
||||
Mailbox::findByKey(GuestMailConfig::RELAY_KEY)->update([
|
||||
'address' => 'noreply@clupilot.cloud', 'password' => 'geheim', 'active' => true,
|
||||
]);
|
||||
}
|
||||
|
||||
it('schreibt den Erfolg an den Sitz', function () {
|
||||
app()->instance(ProxmoxClient::class, new FakeProxmoxClient);
|
||||
$customer = Customer::factory()->create();
|
||||
|
|
@ -92,6 +114,7 @@ it('legt beim Anlegen KEINEN Nextcloud-Benutzer an', function () {
|
|||
|
||||
it('schickt erst beim Einladen einen Auftrag los', function () {
|
||||
Queue::fake();
|
||||
mailversandEingerichtet();
|
||||
$customer = Customer::factory()->create();
|
||||
$user = $customer->ensureUser();
|
||||
Instance::factory()->for($customer)->create(['status' => 'active']);
|
||||
|
|
@ -108,6 +131,7 @@ it('hält den Benutzernamen fest, auch wenn die Mailadresse sich ändert', funct
|
|||
// Nextcloud kann Benutzer nicht umbenennen. Ein Sitz, dessen Adresse sich
|
||||
// später ändert, behält seinen Anmeldenamen.
|
||||
Queue::fake();
|
||||
mailversandEingerichtet();
|
||||
$customer = Customer::factory()->create();
|
||||
$user = $customer->ensureUser();
|
||||
Instance::factory()->for($customer)->create(['status' => 'active']);
|
||||
|
|
@ -121,6 +145,7 @@ it('hält den Benutzernamen fest, auch wenn die Mailadresse sich ändert', funct
|
|||
|
||||
it('weist die elfte Einladung derselben Stunde ab und sagt die Restzeit', function () {
|
||||
Queue::fake();
|
||||
mailversandEingerichtet();
|
||||
$customer = Customer::factory()->create();
|
||||
$user = $customer->ensureUser();
|
||||
Instance::factory()->for($customer)->create(['status' => 'active']);
|
||||
|
|
@ -150,6 +175,7 @@ it('weist das vierte Draufdrücken auf denselben Sitz ab', function () {
|
|||
// Die zweite Grenze: gegen das wiederholte Klicken an EINER Zeile, das
|
||||
// demselben Menschen sonst vier Willkommensmails schickt.
|
||||
Queue::fake();
|
||||
mailversandEingerichtet();
|
||||
$customer = Customer::factory()->create();
|
||||
$user = $customer->ensureUser();
|
||||
Instance::factory()->for($customer)->create(['status' => 'active']);
|
||||
|
|
@ -344,6 +370,7 @@ it('lädt einen entzogenen Sitz nicht erneut ein', function () {
|
|||
// Derselbe Weg zurück, nur über die andere Tür — und ebenfalls an der
|
||||
// Platzgrenze vorbei. Der richtige Weg ist ein NEUER Sitz.
|
||||
Queue::fake();
|
||||
mailversandEingerichtet();
|
||||
$customer = Customer::factory()->create();
|
||||
$user = $customer->ensureUser();
|
||||
Instance::factory()->for($customer)->create(['status' => 'active']);
|
||||
|
|
@ -396,6 +423,7 @@ it('lädt den Inhaber nicht zu seiner eigenen Cloud ein', function () {
|
|||
// oder legte, solange die Bereitstellung noch nicht so weit ist, ein
|
||||
// ZWEITES Konto in der Gruppe `admin` an, neben dem echten.
|
||||
Queue::fake();
|
||||
mailversandEingerichtet();
|
||||
$customer = Customer::factory()->create();
|
||||
$user = $customer->ensureUser();
|
||||
Instance::factory()->for($customer)->create(['status' => 'active']);
|
||||
|
|
@ -551,6 +579,7 @@ it('lässt einen wieder Aufgenommenen auch wirklich wieder hinein', function ()
|
|||
// die revoke() in den Gast geschickt hat, muss beim Einladen wieder
|
||||
// aufgehen. `user:welcome` allein tut das nicht.
|
||||
Queue::fake();
|
||||
mailversandEingerichtet();
|
||||
$pve = new FakeProxmoxClient;
|
||||
app()->instance(ProxmoxClient::class, $pve);
|
||||
$customer = Customer::factory()->create();
|
||||
|
|
@ -657,6 +686,7 @@ it('sperrt ein Konto, das erst nach dem Entziehen entsteht', function () {
|
|||
// Ausführen hat sich die Absicht geändert — der Auftrag darf dann nicht
|
||||
// blind ausführen, was beim Absenden galt.
|
||||
Queue::fake();
|
||||
mailversandEingerichtet();
|
||||
$pve = new FakeProxmoxClient;
|
||||
// Der Benutzer existiert im Gast noch nicht: es läuft ein `user:add`,
|
||||
// nicht nur ein `user:welcome` — der beschriebene Ablauf.
|
||||
|
|
@ -842,6 +872,116 @@ it('nimmt dem Inhaber-Sitz seine Rolle auch dann nicht, wenn es einen zweiten gi
|
|||
Queue::assertNothingPushed();
|
||||
});
|
||||
|
||||
// --- Der Knopf, der eine Mail verspricht, die niemand verschicken kann.
|
||||
|
||||
it('verschickt keine Einladung, solange der Mailversand nicht eingerichtet ist', function () {
|
||||
// Die Einladung verschickt NEXTCLOUD, nicht CluPilot — nur so entsteht das
|
||||
// Passwort dort, wo niemand sonst es zu sehen bekommt. Ohne eingerichteten
|
||||
// Versand kommt aber keine Mail heraus, und `occ user:add
|
||||
// --generate-password --email` GELINGT trotzdem: Nextcloud legt das Konto
|
||||
// an, versucht die Willkommensmail, protokolliert intern einen Fehler und
|
||||
// beendet mit 0.
|
||||
//
|
||||
// Die Zeile stünde also auf „Eingeladen", die Meldung verspräche einen
|
||||
// Link, an dem der Mitarbeiter sein Passwort selbst setzt — und niemand
|
||||
// bekäme etwas. Dieselbe Attrappe, gegen die dieses ganze Feature gebaut
|
||||
// wurde, eine Schicht tiefer.
|
||||
//
|
||||
// Kein ausgefülltes Postfach in dieser Fixture: das ist der Zustand, in
|
||||
// dem JEDE Instanz am ersten Tag steht, solange der Mailserver dieses
|
||||
// Produkts noch nicht existiert.
|
||||
Queue::fake();
|
||||
$customer = Customer::factory()->create();
|
||||
$user = $customer->ensureUser();
|
||||
Instance::factory()->for($customer)->create(['status' => 'active']);
|
||||
$sitz = Seat::factory()->for($customer)->create(['role' => 'member']);
|
||||
|
||||
Livewire::actingAs($user)->test(Users::class)
|
||||
->call('sendInvite', $sitz->uuid)
|
||||
// Kein Erfolg gemeldet. Auf den Satz selbst geprüft, nicht auf
|
||||
// irgendeine Meldung: „Einladung verschickt" ist genau das
|
||||
// Versprechen, das hier nicht zu halten ist.
|
||||
->assertDispatched('notify', fn ($ereignis, $daten) => $daten['message'] !== __('users.invite_sent'));
|
||||
|
||||
// Kein Auftrag, und der Sitz bleibt, wie er war — insbesondere ohne
|
||||
// Anmeldenamen, denn den vergibt erst das Einladen.
|
||||
Queue::assertNothingPushed();
|
||||
expect($sitz->fresh()->nc_state)->toBe(Seat::STATE_NONE)
|
||||
->and($sitz->fresh()->nc_username)->toBeNull();
|
||||
});
|
||||
|
||||
it('sagt es auf der Seite, statt den Knopf verschwinden zu lassen', function () {
|
||||
// Der Hinweis steht dort, wo der Inhaber ihn VOR dem Klicken liest. Der
|
||||
// Knopf bleibt: die Einladen-Spalte an jeder Zeile verschwinden zu lassen
|
||||
// liest sich nicht als „geht hier gerade nicht", sondern als „das kann
|
||||
// dieses Produkt nicht" — genau die Beschwerde, die schon einmal zur
|
||||
// immer sichtbaren Aktionsspalte geführt hat.
|
||||
$customer = Customer::factory()->create();
|
||||
$user = $customer->ensureUser();
|
||||
Instance::factory()->for($customer)->create(['status' => 'active']);
|
||||
$sitz = Seat::factory()->for($customer)->create(['role' => 'member']);
|
||||
|
||||
Livewire::actingAs($user)->test(Users::class)
|
||||
->assertSee(__('users.mail_not_ready'))
|
||||
->assertSee("sendInvite('{$sitz->uuid}')", escape: false);
|
||||
});
|
||||
|
||||
it('laedt wieder ein, sobald der Versand steht', function () {
|
||||
// Die Gegenrichtung: die Wache darf nicht mehr abweisen, als sie soll.
|
||||
Queue::fake();
|
||||
mailversandEingerichtet();
|
||||
$customer = Customer::factory()->create();
|
||||
$user = $customer->ensureUser();
|
||||
Instance::factory()->for($customer)->create(['status' => 'active']);
|
||||
$sitz = Seat::factory()->for($customer)->create(['role' => 'member']);
|
||||
|
||||
Livewire::actingAs($user)->test(Users::class)
|
||||
->call('sendInvite', $sitz->uuid)
|
||||
->assertDontSee(__('users.mail_not_ready'));
|
||||
|
||||
Queue::assertPushed(SyncSeatToNextcloud::class, fn ($auftrag) => $auftrag->action === 'invite');
|
||||
});
|
||||
|
||||
it('verbraucht fuer eine abgewiesene Einladung kein Kontingent', function () {
|
||||
// Die Wache steht VOR dem Ratelimit. Sonst hätte ein Inhaber, dessen
|
||||
// Versand noch nicht steht, seine drei Versuche an dieser Zeile
|
||||
// verbraucht, bevor überhaupt einer hinausgehen konnte — und müsste
|
||||
// warten, sobald der Betrieb den Versand einrichtet.
|
||||
Queue::fake();
|
||||
$customer = Customer::factory()->create();
|
||||
$user = $customer->ensureUser();
|
||||
Instance::factory()->for($customer)->create(['status' => 'active']);
|
||||
$sitz = Seat::factory()->for($customer)->create(['role' => 'member']);
|
||||
|
||||
$seite = Livewire::actingAs($user)->test(Users::class);
|
||||
|
||||
foreach (range(1, 3) as $ignoriert) {
|
||||
$seite->call('sendInvite', $sitz->uuid);
|
||||
}
|
||||
|
||||
mailversandEingerichtet();
|
||||
$seite->call('sendInvite', $sitz->uuid);
|
||||
|
||||
Queue::assertPushed(SyncSeatToNextcloud::class, 1);
|
||||
});
|
||||
|
||||
it('legt weiterhin Sitze an, auch ohne Versand', function () {
|
||||
// Anlegen verspricht ausdrücklich KEINE Mail — ein Inhaber soll sein Team
|
||||
// vorbereiten können, ohne dass jemand eine bekommt. Die Wache gehört
|
||||
// deshalb an „Einladen" und nirgendwo sonst.
|
||||
Queue::fake();
|
||||
$customer = Customer::factory()->create();
|
||||
$user = $customer->ensureUser();
|
||||
Instance::factory()->for($customer)->create(['status' => 'active']);
|
||||
|
||||
Livewire::actingAs($user)->test(Users::class)
|
||||
->set('inviteEmail', 'anna@firma.tld')
|
||||
->call('addSeat')
|
||||
->assertHasNoErrors();
|
||||
|
||||
expect($customer->seats()->where('email', 'anna@firma.tld')->exists())->toBeTrue();
|
||||
});
|
||||
|
||||
it('nennt in keiner Datei unter app/ das Löschen eines Benutzers', function () {
|
||||
// Testerzwungene Regel: kein Nextcloud-Benutzer wird je gelöscht, und
|
||||
// keine Datei. Wer das ändern will, muss diese Prüfung anfassen und
|
||||
|
|
|
|||
Loading…
Reference in New Issue