859 lines
40 KiB
PHP
859 lines
40 KiB
PHP
<?php
|
|
|
|
// tests/Feature/Seats/SyncSeatToNextcloudTest.php
|
|
|
|
use App\Livewire\Users;
|
|
use App\Models\Customer;
|
|
use App\Models\Host;
|
|
use App\Models\Instance;
|
|
use App\Models\Seat;
|
|
use App\Provisioning\Jobs\SyncSeatToNextcloud;
|
|
use App\Services\Nextcloud\NextcloudUsers;
|
|
use App\Services\Proxmox\FakeProxmoxClient;
|
|
use App\Services\Proxmox\ProxmoxClient;
|
|
use Illuminate\Support\Facades\Queue;
|
|
use Livewire\Livewire;
|
|
use Symfony\Component\Finder\Finder;
|
|
|
|
it('schreibt den Erfolg an den Sitz', function () {
|
|
app()->instance(ProxmoxClient::class, new FakeProxmoxClient);
|
|
$customer = Customer::factory()->create();
|
|
// host_id, weil die host-Beziehung sonst null bliebe: der Auftrag bricht
|
|
// dann mit 'no_instance' ab und käme nie bis zum Gast — dieselbe Falle wie
|
|
// in NextcloudUsersTest::gastBereit().
|
|
Instance::factory()->for($customer)->create(['status' => 'active', 'vmid' => 201, 'host_id' => Host::factory()]);
|
|
$sitz = Seat::factory()->for($customer)->create([
|
|
'email' => 'anna@firma.tld', 'nc_username' => 'anna@firma.tld',
|
|
'nc_state' => Seat::STATE_PENDING,
|
|
]);
|
|
|
|
(new SyncSeatToNextcloud($sitz->uuid, 'invite'))->handle(app(NextcloudUsers::class));
|
|
|
|
expect($sitz->fresh()->nc_state)->toBe(Seat::STATE_SYNCED)
|
|
->and($sitz->fresh()->nc_error)->toBeNull()
|
|
->and($sitz->fresh()->nc_synced_at)->not->toBeNull();
|
|
});
|
|
|
|
it('schreibt den Fehlschlag mit Grund an den Sitz', function () {
|
|
$pve = new FakeProxmoxClient;
|
|
$pve->guestThrows[201] = new RuntimeException('guest agent unreachable');
|
|
app()->instance(ProxmoxClient::class, $pve);
|
|
$customer = Customer::factory()->create();
|
|
Instance::factory()->for($customer)->create(['status' => 'active', 'vmid' => 201, 'host_id' => Host::factory()]);
|
|
$sitz = Seat::factory()->for($customer)->create(['nc_username' => 'anna@firma.tld']);
|
|
|
|
(new SyncSeatToNextcloud($sitz->uuid, 'invite'))->handle(app(NextcloudUsers::class));
|
|
|
|
// Nicht nur „irgendein Fehler": der Grund muss der des unerreichbaren
|
|
// Gastes sein, nicht der der fehlenden Instanz — sonst liefe der Test auch
|
|
// dann grün, wenn die Instanz gar nicht gefunden wurde.
|
|
expect($sitz->fresh()->nc_state)->toBe(Seat::STATE_FAILED)
|
|
->and($sitz->fresh()->nc_error)->toBe('guest_failed');
|
|
});
|
|
|
|
it('sagt es am Sitz, wenn es gar keine erreichbare Instanz gibt', function () {
|
|
// Der Kunde hat keine laufende Instanz — es gibt nichts zu spiegeln, und
|
|
// der Inhaber muss den Unterschied zu „der Gast hat abgelehnt" lesen
|
|
// können, weil nur das eine von beiden er selbst beheben kann.
|
|
$customer = Customer::factory()->create();
|
|
$sitz = Seat::factory()->for($customer)->create(['nc_username' => 'anna@firma.tld']);
|
|
|
|
(new SyncSeatToNextcloud($sitz->uuid, 'invite'))->handle(app(NextcloudUsers::class));
|
|
|
|
expect($sitz->fresh()->nc_state)->toBe(Seat::STATE_FAILED)
|
|
->and($sitz->fresh()->nc_error)->toBe('no_instance');
|
|
});
|
|
|
|
it('läuft auf der Bereitstellungs-Warteschlange, nirgends sonst', function () {
|
|
// Nur dieser Arbeiter hat Tunnel und Proxmox-Zugangsdaten. Auf der
|
|
// Standard-Warteschlange erreicht der Auftrag keinen einzigen Gast.
|
|
$auftrag = new SyncSeatToNextcloud('egal', 'invite');
|
|
|
|
expect($auftrag->connection)->toBe('provisioning')
|
|
->and($auftrag->queue)->toBe('provisioning');
|
|
});
|
|
|
|
it('legt beim Anlegen KEINEN Nextcloud-Benutzer an', function () {
|
|
// Anlegen ist nicht Einladen. Der Inhaber soll sein Team vorbereiten
|
|
// können, ohne dass jemand eine Mail bekommt.
|
|
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')
|
|
->set('inviteName', 'Anna')
|
|
->call('addSeat');
|
|
|
|
Queue::assertNothingPushed();
|
|
expect(Seat::where('email', 'anna@firma.tld')->first()->nc_state)->toBe(Seat::STATE_NONE);
|
|
});
|
|
|
|
it('schickt erst beim Einladen einen Auftrag los', function () {
|
|
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);
|
|
|
|
Queue::assertPushed(SyncSeatToNextcloud::class);
|
|
expect($sitz->fresh()->nc_state)->toBe(Seat::STATE_PENDING)
|
|
->and($sitz->fresh()->nc_username)->toBe($sitz->email);
|
|
});
|
|
|
|
it('hält den Benutzernamen fest, auch wenn die Mailadresse sich ändert', function () {
|
|
// Nextcloud kann Benutzer nicht umbenennen. Ein Sitz, dessen Adresse sich
|
|
// später ändert, behält seinen Anmeldenamen.
|
|
Queue::fake();
|
|
$customer = Customer::factory()->create();
|
|
$user = $customer->ensureUser();
|
|
Instance::factory()->for($customer)->create(['status' => 'active']);
|
|
$sitz = Seat::factory()->for($customer)->create(['email' => 'alt@firma.tld', 'nc_username' => 'alt@firma.tld']);
|
|
|
|
$sitz->update(['email' => 'neu@firma.tld']);
|
|
Livewire::actingAs($user)->test(Users::class)->call('sendInvite', $sitz->uuid);
|
|
|
|
expect($sitz->fresh()->nc_username)->toBe('alt@firma.tld');
|
|
});
|
|
|
|
it('weist die elfte Einladung derselben Stunde ab und sagt die Restzeit', function () {
|
|
Queue::fake();
|
|
$customer = Customer::factory()->create();
|
|
$user = $customer->ensureUser();
|
|
Instance::factory()->for($customer)->create(['status' => 'active']);
|
|
$sitze = Seat::factory()->for($customer)->count(11)->create(['role' => 'member']);
|
|
|
|
$seite = Livewire::actingAs($user)->test(Users::class);
|
|
|
|
foreach ($sitze->take(10) as $sitz) {
|
|
$seite->call('sendInvite', $sitz->uuid);
|
|
}
|
|
|
|
$seite->call('sendInvite', $sitze->last()->uuid);
|
|
|
|
// Keine stumme Verweigerung: die Meldung nennt die echte Restzeit. Auf die
|
|
// Zahl geprüft statt auf den Satz — ein Test, der denselben __()-Aufruf
|
|
// nachrechnet wie die Implementierung, würde auch eine fehlende Restzeit
|
|
// durchwinken (siehe R19).
|
|
$seite->assertDispatched(
|
|
'notify',
|
|
fn ($ereignis, $daten) => str_contains($daten['message'], '60'),
|
|
);
|
|
expect($sitze->last()->fresh()->nc_state)->toBe(Seat::STATE_NONE);
|
|
Queue::assertPushed(SyncSeatToNextcloud::class, 10);
|
|
});
|
|
|
|
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();
|
|
$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, 4) as $ignoriert) {
|
|
$seite->call('sendInvite', $sitz->uuid);
|
|
}
|
|
|
|
Queue::assertPushed(SyncSeatToNextcloud::class, 3);
|
|
});
|
|
|
|
it('entzieht, ohne die Zeile zu löschen', function () {
|
|
Queue::fake();
|
|
$customer = Customer::factory()->create();
|
|
$user = $customer->ensureUser();
|
|
Instance::factory()->for($customer)->create(['status' => 'active']);
|
|
// Ein Sitz, der wirklich in der Nextcloud steht: nc_synced_at ist die
|
|
// Angabe, an der die Seite entscheidet, ob es dort etwas zu sperren gibt.
|
|
$sitz = Seat::factory()->for($customer)->create([
|
|
'role' => 'member', 'nc_username' => 'anna@firma.tld',
|
|
'nc_state' => Seat::STATE_SYNCED, 'nc_synced_at' => now(),
|
|
]);
|
|
|
|
Livewire::actingAs($user)->test(Users::class)->call('revoke', $sitz->uuid);
|
|
|
|
// Der Datensatz bleibt. Ein Fehlgriff im Userpanel darf die Arbeit eines
|
|
// Menschen nicht vernichten — und in einem Produkt, das mit
|
|
// Nachvollziehbarkeit verkauft wird, gäbe es danach nichts mehr zu zeigen.
|
|
expect(Seat::find($sitz->id))->not->toBeNull()
|
|
->and($sitz->fresh()->status)->toBe('revoked');
|
|
Queue::assertPushed(SyncSeatToNextcloud::class);
|
|
});
|
|
|
|
it('schickt für einen Sitz, der nie in der Nextcloud war, keinen Auftrag', function () {
|
|
// Angelegt, nie eingeladen: es gäbe dort nichts zu sperren. Ein Auftrag
|
|
// liefe ins Leere und stellte die Zeile danach auf „fehlgeschlagen" —
|
|
// eine Fehlermeldung für etwas, das nie ein Fehler war.
|
|
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('revoke', $sitz->uuid);
|
|
|
|
Queue::assertNothingPushed();
|
|
expect($sitz->fresh()->status)->toBe('revoked')
|
|
->and($sitz->fresh()->nc_state)->toBe(Seat::STATE_NONE);
|
|
});
|
|
|
|
it('sperrt nichts, was in der Nextcloud nie angekommen ist', function () {
|
|
// Einladung losgeschickt, Gast war unerreichbar: der Sitz trägt schon
|
|
// einen Anmeldenamen, in der Nextcloud existiert aber nichts. Ein
|
|
// `user:disable` darauf müsste scheitern und liesse die Zeile dauerhaft
|
|
// rot stehen — eine Fehlermeldung für etwas, das nie ein Fehler war.
|
|
Queue::fake();
|
|
$customer = Customer::factory()->create();
|
|
$user = $customer->ensureUser();
|
|
Instance::factory()->for($customer)->create(['status' => 'active']);
|
|
$sitz = Seat::factory()->for($customer)->create([
|
|
'role' => 'member', 'nc_username' => 'anna@firma.tld',
|
|
'nc_state' => Seat::STATE_FAILED, 'nc_error' => 'no_instance', 'nc_synced_at' => null,
|
|
]);
|
|
|
|
Livewire::actingAs($user)->test(Users::class)->call('revoke', $sitz->uuid);
|
|
|
|
Queue::assertNothingPushed();
|
|
expect($sitz->fresh()->status)->toBe('revoked');
|
|
});
|
|
|
|
it('vermerkt ein angelegtes Konto, auch wenn die Rolle danach scheitert', function () {
|
|
// Der gefährlichste Halbschritt: `user:add` ist durch, `group:adduser`
|
|
// nicht. Das Konto GIBT es ab jetzt — würde der Auftrag das verschweigen,
|
|
// liesse ein späteres Entziehen die Sperre weg, weil die Seite den Sitz
|
|
// für nie angekommen hielte. Der Zustand bleibt trotzdem 'failed'.
|
|
$pve = new FakeProxmoxClient;
|
|
// user:info auf 1, sonst hält invite() den Benutzer für vorhanden und
|
|
// schickt nur `user:welcome` — dann liefe hier gar kein `user:add`, und der
|
|
// Ablauf wäre ein anderer als der beschriebene.
|
|
$pve->guestScript('user:info', 1);
|
|
$pve->guestScript('group:adduser', 1);
|
|
app()->instance(ProxmoxClient::class, $pve);
|
|
$customer = Customer::factory()->create();
|
|
Instance::factory()->for($customer)->create(['status' => 'active', 'vmid' => 201, 'host_id' => Host::factory()]);
|
|
$sitz = Seat::factory()->for($customer)->create(['nc_username' => 'anna@firma.tld']);
|
|
|
|
(new SyncSeatToNextcloud($sitz->uuid, 'invite'))->handle(app(NextcloudUsers::class));
|
|
|
|
expect($pve->guestRan('user:add'))->toBeTrue()
|
|
->and($sitz->fresh()->nc_state)->toBe(Seat::STATE_FAILED)
|
|
->and($sitz->fresh()->nc_synced_at)->not->toBeNull();
|
|
});
|
|
|
|
it('macht die Sperre beim Wiederaufnehmen rückgängig', function () {
|
|
// Ein wiederaufgenommener Sitz war schon einmal in der Nextcloud, und
|
|
// revoke() hat ihm dort ein `user:disable` geschickt. `user:welcome` hebt
|
|
// eine Sperre nicht auf — ohne `user:enable` bekäme er eine
|
|
// Willkommensmail zu einer Tür, die zu bleibt.
|
|
$pve = new FakeProxmoxClient;
|
|
app()->instance(ProxmoxClient::class, $pve);
|
|
$customer = Customer::factory()->create();
|
|
Instance::factory()->for($customer)->create(['status' => 'active', 'vmid' => 201, 'host_id' => Host::factory()]);
|
|
$sitz = Seat::factory()->for($customer)->create([
|
|
'nc_username' => 'anna@firma.tld', 'nc_state' => Seat::STATE_PENDING, 'nc_synced_at' => now()->subYear(),
|
|
]);
|
|
|
|
(new SyncSeatToNextcloud($sitz->uuid, 'invite'))->handle(app(NextcloudUsers::class));
|
|
|
|
expect($pve->guestRan('user:enable'))->toBeTrue()
|
|
->and($sitz->fresh()->nc_state)->toBe(Seat::STATE_SYNCED);
|
|
});
|
|
|
|
it('schickt bei einer ersten Einladung kein Entsperren mit', function () {
|
|
// Ein frisch angelegtes Konto ist nicht gesperrt. Ein `user:enable` wäre
|
|
// hier folgenlos — und ein folgenloser Befehl im Gast ist einer, den
|
|
// niemand mehr erklären kann, wenn er eines Tages doch etwas tut.
|
|
$pve = new FakeProxmoxClient;
|
|
$pve->guestScript('user:info', 1);
|
|
app()->instance(ProxmoxClient::class, $pve);
|
|
$customer = Customer::factory()->create();
|
|
Instance::factory()->for($customer)->create(['status' => 'active', 'vmid' => 201, 'host_id' => Host::factory()]);
|
|
$sitz = Seat::factory()->for($customer)->create(['nc_username' => 'anna@firma.tld']);
|
|
|
|
(new SyncSeatToNextcloud($sitz->uuid, 'invite'))->handle(app(NextcloudUsers::class));
|
|
|
|
expect($pve->guestRan('user:enable'))->toBeFalse()
|
|
->and($sitz->fresh()->nc_state)->toBe(Seat::STATE_SYNCED);
|
|
});
|
|
|
|
it('spiegelt eine Rollenänderung in die Nextcloud', function () {
|
|
Queue::fake();
|
|
$customer = Customer::factory()->create();
|
|
$user = $customer->ensureUser();
|
|
Instance::factory()->for($customer)->create(['status' => 'active']);
|
|
$sitz = Seat::factory()->for($customer)->create([
|
|
'role' => 'member', 'nc_username' => 'anna@firma.tld',
|
|
'nc_state' => Seat::STATE_SYNCED, 'nc_synced_at' => now(),
|
|
]);
|
|
|
|
Livewire::actingAs($user)->test(Users::class)->call('setRole', $sitz->uuid, 'readonly');
|
|
|
|
Queue::assertPushed(SyncSeatToNextcloud::class, fn ($auftrag) => $auftrag->action === 'role');
|
|
expect($sitz->fresh()->nc_state)->toBe(Seat::STATE_PENDING);
|
|
});
|
|
|
|
it('gibt dem Inhaber nach einem Fehlschlag einen zweiten Versuch', function () {
|
|
Queue::fake();
|
|
$customer = Customer::factory()->create();
|
|
$user = $customer->ensureUser();
|
|
Instance::factory()->for($customer)->create(['status' => 'active']);
|
|
$sitz = Seat::factory()->for($customer)->create([
|
|
'role' => 'member', 'nc_username' => 'anna@firma.tld',
|
|
'nc_state' => Seat::STATE_FAILED, 'nc_error' => 'guest_failed',
|
|
]);
|
|
|
|
Livewire::actingAs($user)->test(Users::class)->call('retry', $sitz->uuid);
|
|
|
|
// Der Grund verschwindet mit dem neuen Versuch — ein stehengebliebener
|
|
// alter Fehlertext neben „wird eingerichtet …" wäre die Attrappe zurück.
|
|
Queue::assertPushed(SyncSeatToNextcloud::class, fn ($auftrag) => $auftrag->action === 'invite');
|
|
expect($sitz->fresh()->nc_state)->toBe(Seat::STATE_PENDING)
|
|
->and($sitz->fresh()->nc_error)->toBeNull();
|
|
});
|
|
|
|
it('macht einen entzogenen Sitz nicht über das Sperren wieder aktiv', function () {
|
|
// Der Umschalter in suspend() kannte nur zwei Zustände: an einer
|
|
// entzogenen Zeile machte er beim ersten Klick 'suspended' und beim
|
|
// zweiten 'active', samt `user:enable`. Der Mensch, dessen Zugang der
|
|
// Inhaber beendet hat, könnte sich wieder anmelden — und er zählte
|
|
// wieder gegen die Platzgrenze, die nur in addSeat() geprüft wird.
|
|
Queue::fake();
|
|
$customer = Customer::factory()->create();
|
|
$user = $customer->ensureUser();
|
|
Instance::factory()->for($customer)->create(['status' => 'active']);
|
|
$sitz = Seat::factory()->for($customer)->create([
|
|
'role' => 'member', 'status' => 'revoked', 'nc_username' => 'anna@firma.tld',
|
|
'nc_state' => Seat::STATE_SYNCED, 'nc_synced_at' => now(),
|
|
]);
|
|
|
|
$seite = Livewire::actingAs($user)->test(Users::class);
|
|
$seite->call('suspend', $sitz->uuid);
|
|
$seite->call('suspend', $sitz->uuid);
|
|
|
|
expect($sitz->fresh()->status)->toBe('revoked');
|
|
Queue::assertNothingPushed();
|
|
});
|
|
|
|
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();
|
|
$customer = Customer::factory()->create();
|
|
$user = $customer->ensureUser();
|
|
Instance::factory()->for($customer)->create(['status' => 'active']);
|
|
$sitz = Seat::factory()->for($customer)->create(['role' => 'member', 'status' => 'revoked']);
|
|
|
|
Livewire::actingAs($user)->test(Users::class)->call('sendInvite', $sitz->uuid);
|
|
|
|
Queue::assertNothingPushed();
|
|
expect($sitz->fresh()->nc_state)->toBe(Seat::STATE_NONE)
|
|
->and($sitz->fresh()->nc_username)->toBeNull()
|
|
->and($sitz->fresh()->status)->toBe('revoked');
|
|
});
|
|
|
|
it('zeigt an einer entzogenen Zeile keinen Handlungsknopf mehr', function () {
|
|
// Ein Knopf, den der Server ohnehin abweist, ist eine Einladung zum
|
|
// Draufdrücken. Die Zeile sagt stattdessen, dass sie endgültig ist.
|
|
$customer = Customer::factory()->create();
|
|
$user = $customer->ensureUser();
|
|
Instance::factory()->for($customer)->create(['status' => 'active']);
|
|
$sitz = Seat::factory()->for($customer)->create(['role' => 'member', 'status' => 'revoked']);
|
|
|
|
Livewire::actingAs($user)->test(Users::class)
|
|
->assertSee(__('users.revoked_final'))
|
|
->assertDontSee("sendInvite('{$sitz->uuid}')", escape: false)
|
|
->assertDontSee("suspend('{$sitz->uuid}')", escape: false)
|
|
->assertDontSee("retry('{$sitz->uuid}')", escape: false)
|
|
->assertDontSee('confirm-revoke-seat', escape: false)
|
|
->assertDontSee('edit-seat', escape: false);
|
|
});
|
|
|
|
it('sagt an einer entzogenen Zeile „entzogen", nicht „noch nicht eingeladen"', function () {
|
|
// Die Zustandsspalte muss „entzogen" VOR jeden nc_state stellen. Dieser
|
|
// Sitz steht auf `none` — ohne den Vorrang schriebe die Spalte „angelegt —
|
|
// noch nicht eingeladen" an einen Zugang, den der Inhaber beendet hat, und
|
|
// das läse sich wie ein Weg zurück.
|
|
$customer = Customer::factory()->create();
|
|
$user = $customer->ensureUser();
|
|
Instance::factory()->for($customer)->create(['status' => 'active']);
|
|
Seat::factory()->for($customer)->create(['role' => 'member', 'status' => 'revoked']);
|
|
|
|
Livewire::actingAs($user)->test(Users::class)
|
|
->assertSee(__('users.status_revoked'))
|
|
->assertSee(__('users.revoked_closed'))
|
|
->assertDontSee(__('users.state_none'));
|
|
});
|
|
|
|
it('lädt den Inhaber nicht zu seiner eigenen Cloud ein', function () {
|
|
// Dieses Konto legt die Bereitstellung an (CreateCustomerAdmin). Ein
|
|
// Auftrag von hier aus träfe entweder auf einen bestehenden Benutzer —
|
|
// oder legte, solange die Bereitstellung noch nicht so weit ist, ein
|
|
// ZWEITES Konto in der Gruppe `admin` an, neben dem echten.
|
|
Queue::fake();
|
|
$customer = Customer::factory()->create();
|
|
$user = $customer->ensureUser();
|
|
Instance::factory()->for($customer)->create(['status' => 'active']);
|
|
$sitz = Seat::factory()->for($customer)->owner()->create();
|
|
|
|
Livewire::actingAs($user)->test(Users::class)->call('sendInvite', $sitz->uuid);
|
|
|
|
Queue::assertNothingPushed();
|
|
expect($sitz->fresh()->nc_username)->toBeNull()
|
|
->and($sitz->fresh()->nc_state)->toBe(Seat::STATE_NONE);
|
|
});
|
|
|
|
it('verknüpft den Inhaber-Sitz beim Besuch, auch wenn die Instanz später dazukommt', function () {
|
|
// /users verlangt keine fertige Instanz. Wer die Seite öffnet, bevor die
|
|
// Bereitstellung das Admin-Konto angelegt hat, bekommt einen owner-Sitz
|
|
// ohne Verknüpfung — und ohne diese Zeilen in mount() bliebe er es für
|
|
// immer, während das Panel dem Inhaber anböte, sich SELBST einzuladen.
|
|
// Der Aufruf muss deshalb bei JEDEM Besuch stattfinden, nicht nur beim
|
|
// ersten.
|
|
$customer = Customer::factory()->create();
|
|
$user = $customer->ensureUser();
|
|
|
|
// Erster Besuch, noch ohne Instanz: mount() legt den owner-Sitz an, findet
|
|
// aber kein Konto zum Verknüpfen.
|
|
Livewire::actingAs($user)->test(Users::class)->assertOk();
|
|
$sitz = $customer->seats()->where('role', 'owner')->firstOrFail();
|
|
expect($sitz->nc_username)->toBeNull();
|
|
|
|
Instance::factory()->for($customer)->create(['status' => 'active', 'nc_admin_ref' => 'admin']);
|
|
|
|
Livewire::actingAs($user)->test(Users::class)->assertOk();
|
|
|
|
expect($sitz->fresh()->nc_username)->toBe('admin')
|
|
->and($sitz->fresh()->nc_state)->toBe(Seat::STATE_SYNCED);
|
|
});
|
|
|
|
it('lässt den Wiederholen-Knopf an einer entzogenen Zeile stehen, wenn das Sperren scheiterte', function () {
|
|
// Der gefährlichste Halbschritt auf der anderen Seite: der Inhaber
|
|
// entzieht, der `disable`-Auftrag scheitert (Gast aus, Tunnel weg), und das
|
|
// Konto im Gast ist WEITER OFFEN, während die Zeile „entzogen" sagt. Es
|
|
// gibt keinen Wiederholungslauf (tries = 1), keinen Abgleich im Zeitplan,
|
|
// und diese Seite ist die einzige Stelle im Repo, die nc_state anzeigt —
|
|
// ohne diesen Knopf fällt es nie wieder jemandem auf.
|
|
$customer = Customer::factory()->create();
|
|
$user = $customer->ensureUser();
|
|
Instance::factory()->for($customer)->create(['status' => 'active']);
|
|
$sitz = Seat::factory()->for($customer)->create([
|
|
'role' => 'member', 'status' => 'revoked', 'nc_username' => 'anna@firma.tld',
|
|
'nc_state' => Seat::STATE_FAILED, 'nc_error' => 'guest_failed', 'nc_synced_at' => now(),
|
|
]);
|
|
|
|
Livewire::actingAs($user)->test(Users::class)
|
|
->assertSee(__('users.state_failed'))
|
|
->assertSee(__('users.error_guest_failed'))
|
|
->assertSee("retry('{$sitz->uuid}')", escape: false)
|
|
// Und die Zeile sagt trotzdem, dass der Zugang geschlossen ist.
|
|
->assertSee(__('users.status_revoked'));
|
|
});
|
|
|
|
it('wiederholt an einem entzogenen Sitz das Sperren, nie das Einladen', function () {
|
|
Queue::fake();
|
|
$customer = Customer::factory()->create();
|
|
$user = $customer->ensureUser();
|
|
Instance::factory()->for($customer)->create(['status' => 'active']);
|
|
$sitz = Seat::factory()->for($customer)->create([
|
|
'role' => 'member', 'status' => 'revoked', 'nc_username' => 'anna@firma.tld',
|
|
'nc_state' => Seat::STATE_FAILED, 'nc_error' => 'guest_failed', 'nc_synced_at' => now(),
|
|
]);
|
|
|
|
Livewire::actingAs($user)->test(Users::class)->call('retry', $sitz->uuid);
|
|
|
|
Queue::assertPushed(SyncSeatToNextcloud::class, fn ($auftrag) => $auftrag->action === 'disable');
|
|
expect($sitz->fresh()->status)->toBe('revoked');
|
|
});
|
|
|
|
it('wiederholt nichts an einer entzogenen Zeile, die nie in der Nextcloud war', function () {
|
|
// Anlegen → einladen → Auftrag scheitert an `no_instance` → entziehen.
|
|
// queueSync() bricht korrekt ab, `failed` bleibt stehen, und der
|
|
// Fehlschlag-Block ist zu Recht da. Ohne Vorbehalt liefe ein Druck aber auf
|
|
// `user:disable` gegen einen Benutzer, den es nie gab: scheitert, dieselbe
|
|
// rote Zeile, beliebig oft. Genau der Dauerfehler, nur über die andere
|
|
// Tür — und mit einem veralteten Grund aus einem anderen Vorgang.
|
|
Queue::fake();
|
|
$customer = Customer::factory()->create();
|
|
$user = $customer->ensureUser();
|
|
Instance::factory()->for($customer)->create(['status' => 'active']);
|
|
$sitz = Seat::factory()->for($customer)->create([
|
|
'role' => 'member', 'status' => 'revoked', 'nc_username' => 'anna@firma.tld',
|
|
'nc_state' => Seat::STATE_FAILED, 'nc_error' => 'no_instance', 'nc_synced_at' => null,
|
|
]);
|
|
|
|
Livewire::actingAs($user)->test(Users::class)
|
|
->call('retry', $sitz->uuid)
|
|
// Und der Knopf steht dort gar nicht erst.
|
|
->assertDontSee("retry('{$sitz->uuid}')", escape: false);
|
|
|
|
Queue::assertNothingPushed();
|
|
});
|
|
|
|
it('wiederholt nichts an einem Inhaber-Sitz', function () {
|
|
// retry() war der ungehärtete Zwilling von sendInvite(): an einem
|
|
// beschädigten Inhaber-Sitz (failed, nie gespiegelt) wählte es die Aktion
|
|
// `invite` und legte damit genau das an, was die Wache in sendInvite()
|
|
// verhindern soll — ein ZWEITES Konto in der Gruppe `admin`.
|
|
Queue::fake();
|
|
$customer = Customer::factory()->create();
|
|
$user = $customer->ensureUser();
|
|
Instance::factory()->for($customer)->create(['status' => 'active']);
|
|
$sitz = Seat::factory()->for($customer)->owner()->create([
|
|
'nc_state' => Seat::STATE_FAILED, 'nc_error' => 'guest_failed',
|
|
]);
|
|
|
|
Livewire::actingAs($user)->test(Users::class)->call('retry', $sitz->uuid);
|
|
|
|
Queue::assertNothingPushed();
|
|
expect($sitz->fresh()->nc_state)->toBe(Seat::STATE_FAILED);
|
|
});
|
|
|
|
it('nimmt einen entzogenen Sitz über das Anlegen-Formular wieder auf', function () {
|
|
// Die empfohlene Vordertür muss offen sein. Vor dem Fix prallte dieselbe
|
|
// Adresse an der Eindeutigkeit von (customer_id, email) ab — die Meldung
|
|
// empfahl damit einen Weg, den es nicht gab.
|
|
Queue::fake();
|
|
$customer = Customer::factory()->create();
|
|
$user = $customer->ensureUser();
|
|
Instance::factory()->for($customer)->create(['status' => 'active']);
|
|
$sitz = Seat::factory()->for($customer)->create([
|
|
'email' => 'anna@firma.tld', 'role' => 'member', 'status' => 'revoked',
|
|
'nc_username' => 'anna@firma.tld', 'nc_state' => Seat::STATE_SYNCED, 'nc_synced_at' => now(),
|
|
]);
|
|
|
|
Livewire::actingAs($user)->test(Users::class)
|
|
->set('inviteEmail', 'anna@firma.tld')
|
|
->set('inviteRole', 'readonly')
|
|
->call('addSeat')
|
|
->assertHasNoErrors();
|
|
|
|
// Dieselbe Zeile, kein Duplikat — und der Anmeldename bleibt, wie er ist:
|
|
// Nextcloud kann Benutzer nicht umbenennen, und es ist dieselbe Person.
|
|
expect($customer->seats()->where('email', 'anna@firma.tld')->count())->toBe(1)
|
|
->and($sitz->fresh()->status)->toBe('invited')
|
|
->and($sitz->fresh()->role)->toBe('readonly')
|
|
->and($sitz->fresh()->nc_username)->toBe('anna@firma.tld');
|
|
|
|
// Angelegt ist nicht eingeladen — auch beim Wiederaufnehmen nicht.
|
|
Queue::assertNothingPushed();
|
|
});
|
|
|
|
it('lässt einen wieder Aufgenommenen auch wirklich wieder hinein', function () {
|
|
// Der ganze Weg in einer Prüfung: entziehen, wieder aufnehmen, einladen.
|
|
// Zwei Dinge müssen dabei zugleich stimmen — die Zeile darf nicht
|
|
// „Eingeladen" behaupten, solange niemand eingeladen hat, und die Sperre,
|
|
// die revoke() in den Gast geschickt hat, muss beim Einladen wieder
|
|
// aufgehen. `user:welcome` allein tut das nicht.
|
|
Queue::fake();
|
|
$pve = new FakeProxmoxClient;
|
|
app()->instance(ProxmoxClient::class, $pve);
|
|
$customer = Customer::factory()->create();
|
|
$user = $customer->ensureUser();
|
|
Instance::factory()->for($customer)->create(['status' => 'active', 'vmid' => 201, 'host_id' => Host::factory()]);
|
|
$sitz = Seat::factory()->for($customer)->create([
|
|
'email' => 'anna@firma.tld', 'role' => 'member', 'status' => 'active',
|
|
'nc_username' => 'anna@firma.tld', 'nc_state' => Seat::STATE_SYNCED, 'nc_synced_at' => now()->subYear(),
|
|
]);
|
|
|
|
$seite = Livewire::actingAs($user)->test(Users::class);
|
|
$seite->call('revoke', $sitz->uuid);
|
|
$seite->set('inviteEmail', 'anna@firma.tld')->set('inviteRole', 'member')
|
|
->call('addSeat')->assertHasNoErrors();
|
|
|
|
expect($sitz->fresh()->nc_state)->toBe(Seat::STATE_NONE)
|
|
->and($sitz->fresh()->nc_error)->toBeNull()
|
|
// Anmeldename und Ankunftszeitpunkt bleiben: der Name gehört dieser
|
|
// Person, und dass es das Konto im Gast gibt, bleibt wahr.
|
|
->and($sitz->fresh()->nc_username)->toBe('anna@firma.tld')
|
|
->and($sitz->fresh()->nc_synced_at)->not->toBeNull();
|
|
|
|
// Und die Zeile sagt es auch: „angelegt — noch nicht eingeladen", mit dem
|
|
// Knopf „Einladen" statt „Erneut senden".
|
|
$seite->assertSee(__('users.state_none'))
|
|
->assertSee(__('users.invite'))
|
|
->assertDontSee(__('users.status_invited'));
|
|
|
|
$seite->call('sendInvite', $sitz->uuid);
|
|
(new SyncSeatToNextcloud($sitz->uuid, 'invite'))->handle(app(NextcloudUsers::class));
|
|
|
|
expect($pve->guestRan('user:enable'))->toBeTrue()
|
|
->and($sitz->fresh()->nc_state)->toBe(Seat::STATE_SYNCED);
|
|
});
|
|
|
|
it('weist die Wiederaufnahme ab, wenn kein Platz mehr frei ist', function () {
|
|
// Der Grund, warum es die Vordertür sein muss und kein Knopf an der Zeile:
|
|
// hier steht die Platzprüfung. Ein entzogener Sitz zählt nicht mit, ein
|
|
// wiederaufgenommener schon.
|
|
$customer = Customer::factory()->create();
|
|
$user = $customer->ensureUser();
|
|
Instance::factory()->for($customer)->create(['status' => 'active']);
|
|
Seat::factory()->for($customer)->count(5)->create(['role' => 'member']);
|
|
$sitz = Seat::factory()->for($customer)->create([
|
|
'email' => 'anna@firma.tld', 'role' => 'member', 'status' => 'revoked',
|
|
]);
|
|
|
|
Livewire::actingAs($user)->test(Users::class)
|
|
->set('inviteEmail', 'anna@firma.tld')
|
|
->call('addSeat')
|
|
->assertHasErrors(['inviteEmail']);
|
|
|
|
expect($sitz->fresh()->status)->toBe('revoked');
|
|
});
|
|
|
|
it('weist eine Adresse ab, die noch einen lebenden Platz hat', function () {
|
|
// Die Wiederaufnahme gilt NUR für entzogene Zeilen. Ein aktiver Sitz
|
|
// derselben Adresse bleibt ein Duplikat — sonst wäre das Formular ein
|
|
// stiller Rollenwechsel an einer bestehenden Zeile.
|
|
$customer = Customer::factory()->create();
|
|
$user = $customer->ensureUser();
|
|
Instance::factory()->for($customer)->create(['status' => 'active']);
|
|
$sitz = Seat::factory()->for($customer)->create([
|
|
'email' => 'anna@firma.tld', 'role' => 'member', 'status' => 'active',
|
|
]);
|
|
|
|
Livewire::actingAs($user)->test(Users::class)
|
|
->set('inviteEmail', 'anna@firma.tld')
|
|
->set('inviteRole', 'admin')
|
|
->call('addSeat')
|
|
->assertHasErrors(['inviteEmail']);
|
|
|
|
expect($sitz->fresh()->role)->toBe('member');
|
|
});
|
|
|
|
// --- K1: entziehen, während die Einladung noch in der Warteschlange steht.
|
|
// Der einzige Arbeiter teilt sich die Warteschlange mit der bezahlten
|
|
// Bereitstellung — das Fenster ist MINUTEN lang, nicht Millisekunden.
|
|
|
|
it('schickt die Sperre auch dann, wenn die Einladung noch unterwegs ist', function () {
|
|
// Der dritte Wächter-Fall in queueSync(): `none` und `failed` waren
|
|
// abgedeckt, `pending` nicht — und das ist der einzige Zustand, in dem
|
|
// noch ein Auftrag unterwegs ist. Ohne diesen Zweig kam an der Zeile
|
|
// „Entfernt" ein aktives, freigeschaltetes Konto in der Cloud heraus, und
|
|
// der Inhaber hatte im Portal keinen Weg mehr, die Sperre nachzuschicken:
|
|
// kein Wiederholen-Knopf (nicht `failed`), kein Sperren-Knopf (entzogen).
|
|
Queue::fake();
|
|
$customer = Customer::factory()->create();
|
|
$user = $customer->ensureUser();
|
|
Instance::factory()->for($customer)->create(['status' => 'active']);
|
|
$sitz = Seat::factory()->for($customer)->create([
|
|
'role' => 'member', 'status' => 'invited', 'nc_username' => 'anna@firma.tld',
|
|
'nc_state' => Seat::STATE_PENDING, 'nc_synced_at' => null,
|
|
]);
|
|
|
|
Livewire::actingAs($user)->test(Users::class)->call('revoke', $sitz->uuid);
|
|
|
|
Queue::assertPushed(SyncSeatToNextcloud::class, fn ($auftrag) => $auftrag->action === 'disable');
|
|
});
|
|
|
|
it('sperrt ein Konto, das erst nach dem Entziehen entsteht', function () {
|
|
// Der ganze Weg, rein durch Klicken erreichbar: einladen, entziehen
|
|
// solange der Auftrag wartet, Auftrag ausführen. Zwischen Absenden und
|
|
// Ausführen hat sich die Absicht geändert — der Auftrag darf dann nicht
|
|
// blind ausführen, was beim Absenden galt.
|
|
Queue::fake();
|
|
$pve = new FakeProxmoxClient;
|
|
// Der Benutzer existiert im Gast noch nicht: es läuft ein `user:add`,
|
|
// nicht nur ein `user:welcome` — der beschriebene Ablauf.
|
|
$pve->guestScript('user:info', 1);
|
|
app()->instance(ProxmoxClient::class, $pve);
|
|
$customer = Customer::factory()->create();
|
|
$user = $customer->ensureUser();
|
|
Instance::factory()->for($customer)->create(['status' => 'active', 'vmid' => 201, 'host_id' => Host::factory()]);
|
|
$sitz = Seat::factory()->for($customer)->create(['role' => 'member', 'email' => 'anna@firma.tld']);
|
|
|
|
$seite = Livewire::actingAs($user)->test(Users::class);
|
|
$seite->call('sendInvite', $sitz->uuid);
|
|
$seite->call('revoke', $sitz->uuid);
|
|
|
|
(new SyncSeatToNextcloud($sitz->uuid, 'invite'))->handle(app(NextcloudUsers::class));
|
|
|
|
expect($pve->guestRan('user:add'))->toBeTrue()
|
|
->and($pve->guestRan('user:disable'))->toBeTrue()
|
|
// Und die Zeile darf danach stimmen: entzogen UND gespiegelt.
|
|
->and($sitz->fresh()->status)->toBe('revoked')
|
|
->and($sitz->fresh()->nc_state)->toBe(Seat::STATE_SYNCED);
|
|
});
|
|
|
|
it('schickt bei einem inzwischen entzogenen Sitz kein Entsperren mit', function () {
|
|
// Eine Wiederaufnahme entsperrt das alte Konto. Wurde der Zugang
|
|
// inzwischen wieder entzogen, hiesse das: eine Tür aufmachen, um sie im
|
|
// selben Auftrag wieder zuzuziehen — im besten Fall folgenlos, im
|
|
// schlechteren ein offenes Zeitfenster.
|
|
$pve = new FakeProxmoxClient;
|
|
app()->instance(ProxmoxClient::class, $pve);
|
|
$customer = Customer::factory()->create();
|
|
Instance::factory()->for($customer)->create(['status' => 'active', 'vmid' => 201, 'host_id' => Host::factory()]);
|
|
$sitz = Seat::factory()->for($customer)->create([
|
|
'role' => 'member', 'status' => 'revoked', 'nc_username' => 'anna@firma.tld',
|
|
'nc_state' => Seat::STATE_PENDING, 'nc_synced_at' => now()->subYear(),
|
|
]);
|
|
|
|
(new SyncSeatToNextcloud($sitz->uuid, 'invite'))->handle(app(NextcloudUsers::class));
|
|
|
|
expect($pve->guestRan('user:enable'))->toBeFalse()
|
|
->and($pve->guestRan('user:disable'))->toBeTrue();
|
|
});
|
|
|
|
// --- K2: `retry()` kannte `enable` nicht.
|
|
|
|
it('wiederholt nach einem gescheiterten Entsperren nicht die Rolle', function () {
|
|
// Gesperrter Sitz → Entsperren → suspend() setzt `status` auf 'active' und
|
|
// schickt `enable`; der Gast antwortet nicht. Die Ableitung in retry()
|
|
// kannte `enable` nicht und wählte `role`: der Auftrag fuhr Gruppen und
|
|
// Quota, gelang, und die Zeile zeigte „Aktiv" — während `user:enable` nie
|
|
// geschickt wurde und der Mitarbeiter weiterhin nicht hineinkam. Der
|
|
// Wiederholen-Knopf war danach verschwunden.
|
|
Queue::fake();
|
|
$customer = Customer::factory()->create();
|
|
$user = $customer->ensureUser();
|
|
Instance::factory()->for($customer)->create(['status' => 'active']);
|
|
$sitz = Seat::factory()->for($customer)->create([
|
|
'role' => 'member', 'status' => 'active', 'nc_username' => 'anna@firma.tld',
|
|
'nc_state' => Seat::STATE_FAILED, 'nc_error' => 'guest_failed', 'nc_synced_at' => now(),
|
|
]);
|
|
|
|
Livewire::actingAs($user)->test(Users::class)->call('retry', $sitz->uuid);
|
|
|
|
Queue::assertPushed(SyncSeatToNextcloud::class, fn ($auftrag) => $auftrag->action === 'restore');
|
|
});
|
|
|
|
it('wiederholt auch nach einer Wiederaufnahme, deren Entsperren scheiterte', function () {
|
|
// Die zweite Tür, dieselbe Ursache: scheitert das `enable` INNERHALB des
|
|
// invite-Auftrags, blieb der Status 'invited' — die Ableitung wählte
|
|
// wieder `role`, das Konto blieb gesperrt, und die Zeile sagte
|
|
// „Eingeladen".
|
|
Queue::fake();
|
|
$customer = Customer::factory()->create();
|
|
$user = $customer->ensureUser();
|
|
Instance::factory()->for($customer)->create(['status' => 'active']);
|
|
$sitz = Seat::factory()->for($customer)->create([
|
|
'role' => 'member', 'status' => 'invited', 'nc_username' => 'anna@firma.tld',
|
|
'nc_state' => Seat::STATE_FAILED, 'nc_error' => 'guest_failed', 'nc_synced_at' => now()->subYear(),
|
|
]);
|
|
|
|
Livewire::actingAs($user)->test(Users::class)->call('retry', $sitz->uuid);
|
|
|
|
Queue::assertPushed(SyncSeatToNextcloud::class, fn ($auftrag) => $auftrag->action === 'restore');
|
|
});
|
|
|
|
it('macht beim Wiederholen einer offenen Zeile die Tür auf UND setzt die Rolle', function () {
|
|
// Warum `restore` und nicht `enable`: welcher der beiden Befehle beim
|
|
// letzten Mal scheiterte, weiss dieser Sitz nicht — und ein zweites Feld
|
|
// dafür wäre eine weitere Behauptung über die Cloud des Kunden, die
|
|
// irgendwann nicht mehr stimmt. Beide zu schicken ist billiger und
|
|
// ehrlicher als es zu raten.
|
|
$pve = new FakeProxmoxClient;
|
|
app()->instance(ProxmoxClient::class, $pve);
|
|
$customer = Customer::factory()->create();
|
|
Instance::factory()->for($customer)->create(['status' => 'active', 'vmid' => 201, 'host_id' => Host::factory()]);
|
|
$sitz = Seat::factory()->for($customer)->create([
|
|
'role' => 'member', 'nc_username' => 'anna@firma.tld', 'nc_synced_at' => now(),
|
|
]);
|
|
|
|
(new SyncSeatToNextcloud($sitz->uuid, 'restore'))->handle(app(NextcloudUsers::class));
|
|
|
|
expect($pve->guestRan('user:enable'))->toBeTrue()
|
|
->and($pve->guestRan('group:adduser'))->toBeTrue()
|
|
->and($sitz->fresh()->nc_state)->toBe(Seat::STATE_SYNCED);
|
|
});
|
|
|
|
it('lässt das Entsperren aus dem Sperren-Knopf ein reines Entsperren bleiben', function () {
|
|
// suspend() bleibt unangetastet: dort IST bekannt, was fehlt. Die Rolle
|
|
// wurde auch während der Sperre mitgeführt (setRole schickt `role`), also
|
|
// wäre sie hier fünf folgenlose Befehle im Gast — und ein folgenloser
|
|
// Befehl ist einer, den niemand mehr erklären kann, wenn er eines Tages
|
|
// doch etwas tut.
|
|
Queue::fake();
|
|
$customer = Customer::factory()->create();
|
|
$user = $customer->ensureUser();
|
|
Instance::factory()->for($customer)->create(['status' => 'active']);
|
|
$sitz = Seat::factory()->for($customer)->create([
|
|
'role' => 'member', 'status' => 'suspended', 'nc_username' => 'anna@firma.tld',
|
|
'nc_state' => Seat::STATE_SYNCED, 'nc_synced_at' => now(),
|
|
]);
|
|
|
|
Livewire::actingAs($user)->test(Users::class)->call('suspend', $sitz->uuid);
|
|
|
|
Queue::assertPushed(SyncSeatToNextcloud::class, fn ($auftrag) => $auftrag->action === 'enable');
|
|
});
|
|
|
|
// --- W2: der Inhaber-Sitz.
|
|
|
|
it('entzieht den Inhaber-Sitz auch dann nicht, wenn es einen zweiten gibt', function () {
|
|
// Die Wache in revoke() zählte nur owner-Sitze. Über setRole() liess sich
|
|
// ein zweiter owner erzeugen — danach griff sie nicht mehr, und der
|
|
// ECHTE Inhaber bekam `user:disable admin` samt
|
|
// `user:auth-tokens:delete admin` in seine eigene Cloud. Vor diesem Umbau
|
|
// war revoke() folgenlos; jetzt sperrt es wirklich aus.
|
|
Queue::fake();
|
|
$customer = Customer::factory()->create();
|
|
$user = $customer->ensureUser();
|
|
Instance::factory()->for($customer)->create(['status' => 'active']);
|
|
$inhaber = Seat::factory()->for($customer)->owner()->create([
|
|
'email' => $customer->email, 'nc_username' => 'admin',
|
|
'nc_state' => Seat::STATE_SYNCED, 'nc_synced_at' => now(),
|
|
]);
|
|
Seat::factory()->for($customer)->owner()->create(['email' => 'zweiter@firma.tld']);
|
|
|
|
Livewire::actingAs($user)->test(Users::class)->call('revoke', $inhaber->uuid);
|
|
|
|
expect($inhaber->fresh()->status)->toBe('active');
|
|
Queue::assertNothingPushed();
|
|
});
|
|
|
|
it('macht aus einem Mitarbeiter keinen Inhaber', function () {
|
|
// `setRole()` nahm jede Rolle aus Seat::ROLES an, also auch `owner`. Das
|
|
// Auswahlfeld bietet sie nicht an — die Livewire-Methode ist trotzdem
|
|
// öffentlich erreichbar, und genau das war der erste Schritt, mit dem
|
|
// sich der Inhaber selbst aussperren liess.
|
|
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('setRole', $sitz->uuid, 'owner');
|
|
|
|
expect($sitz->fresh()->role)->toBe('member');
|
|
});
|
|
|
|
it('nimmt dem Inhaber-Sitz seine Rolle auch dann nicht, wenn es einen zweiten gibt', function () {
|
|
// Dieselbe Klasse über die andere Tür: `setRole(<inhaberSitz>, 'member')`
|
|
// — das Admin-Konto verlöre seine Adminrechte im eigenen Gast.
|
|
Queue::fake();
|
|
$customer = Customer::factory()->create();
|
|
$user = $customer->ensureUser();
|
|
Instance::factory()->for($customer)->create(['status' => 'active']);
|
|
$inhaber = Seat::factory()->for($customer)->owner()->create([
|
|
'email' => $customer->email, 'nc_username' => 'admin',
|
|
'nc_state' => Seat::STATE_SYNCED, 'nc_synced_at' => now(),
|
|
]);
|
|
Seat::factory()->for($customer)->owner()->create(['email' => 'zweiter@firma.tld']);
|
|
|
|
Livewire::actingAs($user)->test(Users::class)->call('setRole', $inhaber->uuid, 'member');
|
|
|
|
expect($inhaber->fresh()->role)->toBe('owner');
|
|
Queue::assertNothingPushed();
|
|
});
|
|
|
|
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
|
|
// dabei über die Folgen stolpern.
|
|
$treffer = [];
|
|
|
|
foreach (Finder::create()->files()->in(app_path())->name('*.php') as $datei) {
|
|
if (str_contains($datei->getContents(), 'user:delete')) {
|
|
$treffer[] = $datei->getRelativePathname();
|
|
}
|
|
}
|
|
|
|
expect($treffer)->toBe([]);
|
|
});
|