active()->create(['node' => 'pve']); return Instance::factory()->create([ 'customer_id' => Customer::factory()->create(['name' => $kunde ?? 'Kundin '.$subdomain]), 'host_id' => $host->id, 'vmid' => 900 + strlen($subdomain), 'status' => 'ended', 'subdomain' => $subdomain, 'service_ends_at' => now()->subDays(30), 'teardown_error' => $grund, ]); } // ---- Was gehakt hat: der wichtigere der beiden Kästen ---- it('zeigt dem Betreiber einen hängengebliebenen Abbau samt Grund', function () { // Die Zusicherung, an der der ganze Plan hängt. Ohne diesen Kasten stünde // die Maschine für immer, und der Grund läge in einer Spalte, die in der // ganzen Konsole keinen Leser hat. haengenderAbbau('nc-haengt', 'Die Ablage "local" nimmt keine Sicherungen auf.', kunde: 'Hängenkundin GmbH'); Livewire::actingAs(admin(), 'operator')->test(AdminInstances::class) ->assertSee(__('admin.teardown.stuck_title')) ->assertSee('Hängenkundin GmbH') ->assertSee('nc-haengt') ->assertSee('Die Ablage "local" nimmt keine Sicherungen auf.'); }); it('nennt beim hängenden Abbau das Archiv, wenn es schon liegt', function () { // Der Zwischenstand, den es wirklich gibt: gesichert, aber nicht gelöscht. // Wer ihn von Hand zu Ende bringt, braucht genau diese Kennung — sonst // sichert er ein zweites Mal und zahlt die Stunde noch einmal. $instanz = haengenderAbbau('nc-halbfertig', 'Das Löschen wurde abgewiesen.'); $instanz->update([ 'archive_volid' => 'local:backup/vzdump-qemu-909.vma.zst', 'archived_at' => now()->subHour(), ]); Livewire::actingAs(admin(), 'operator')->test(AdminInstances::class) ->assertSee('local:backup/vzdump-qemu-909.vma.zst'); }); it('stellt den zuletzt gescheiterten Abbau nach oben', function () { // Der Fehler des Nachbarkastens, ausdrücklich nicht wiederholt: „Datenexport // bestellt" sortiert aufsteigend, also besetzt der älteste Eintrag den Kopf // der Liste für immer und die frischen wandern nach unten. Ein Kasten, in // dem der Dauerfall oben klebt, wird nach der zweiten Woche nicht mehr // gelesen. $alt = haengenderAbbau('nc-alterfall', 'scheitert seit Wochen'); // Über den Abfrageerzeuger und nicht über das Modell: `updated_at` steht // nicht in $fillable, ein update() darauf verschluckt den Wert stillschweigend. Instance::query()->whereKey($alt->id)->update(['updated_at' => now()->subMonth()]); haengenderAbbau('nc-neuerfall', 'heute Nacht gescheitert'); $seite = Livewire::actingAs(admin(), 'operator')->test(AdminInstances::class); $reihenfolge = collect($seite->viewData('stuck'))->pluck('address')->all(); expect($reihenfolge)->toBe(['nc-neuerfall', 'nc-alterfall']); }); it('räumt die Zeile ab, sobald der Abbau durchgelaufen ist', function () { // Der Unterschied zum Kasten „Datenexport bestellt", der sich nie abräumen // lässt: hier gibt es einen Mechanismus, der das ohne Buchführung tut. Der // nächtliche Lauf versucht es erneut, und ein erfolgreicher Abbau setzt // `teardown_error` auf null und `torn_down_at`. $instanz = haengenderAbbau('nc-geheilt', 'beim letzten Mal gescheitert'); $instanz->update([ 'teardown_error' => null, 'archive_volid' => 'local:backup/vzdump-qemu-911.vma.zst', 'archived_at' => now(), 'torn_down_at' => now(), ]); Livewire::actingAs(admin(), 'operator')->test(AdminInstances::class) ->assertDontSee(__('admin.teardown.stuck_title')); }); it('zeigt gar keinen Kasten, wenn nichts hängt', function () { // Ein dauerhaft leerer Kasten wird nach der zweiten Woche übersehen — und // mit ihm der erste Eintrag, der wirklich einmal darin steht. Instance::factory()->create(['status' => 'active', 'subdomain' => 'nc-laeuft']); Livewire::actingAs(admin(), 'operator')->test(AdminInstances::class) ->assertDontSee(__('admin.teardown.stuck_title')); }); // ---- Wo das Archiv liegt ---- it('sagt, was abgebaut wurde und wo das Archiv liegt', function () { $customer = Customer::factory()->create(['name' => 'Abgebautkundin GmbH']); $instanz = Instance::factory()->create([ 'customer_id' => $customer->id, 'status' => 'ended', 'subdomain' => 'nc-abgebaut', 'service_ends_at' => now()->subDays(30), 'archive_volid' => 'local:backup/vzdump-qemu-777.vma.zst', 'archived_at' => now()->subDays(2), 'torn_down_at' => now()->subDays(2), ]); Livewire::actingAs(admin(), 'operator')->test(AdminInstances::class) ->assertSee(__('admin.teardown.archive_title')) ->assertSee('Abgebautkundin GmbH') ->assertSee('local:backup/vzdump-qemu-777.vma.zst') // R19: die Wanduhr des Betreibers, nicht die Speicherzone. ->assertSee($instanz->archived_at->local()->isoFormat('LLL')); }); it('lässt ein Archiv aus der Liste fallen, sobald die Aufbewahrung um ist', function () { // Der Kasten hat damit ein Ende, das ihm jemand anderes setzt: was hier // steht, ist genau das, was noch auf einer Ablage liegt. Eine Liste jeder // Löschung seit Anbeginn wäre wieder der Kasten, den nichts abräumen kann. Instance::factory()->create([ 'status' => 'ended', 'subdomain' => 'nc-uralt', 'service_ends_at' => now()->subYears(2), 'archive_volid' => 'local:backup/vzdump-qemu-666.vma.zst', 'archived_at' => now()->subMonths(13), 'torn_down_at' => now()->subMonths(13), ]); Livewire::actingAs(admin(), 'operator')->test(AdminInstances::class) ->assertDontSee('local:backup/vzdump-qemu-666.vma.zst'); }); it('stellt den jüngsten Abbau nach oben', function () { Instance::factory()->create([ 'status' => 'ended', 'subdomain' => 'nc-frueher', 'archive_volid' => 'local:backup/vzdump-qemu-111.vma.zst', 'archived_at' => now()->subMonths(6), 'torn_down_at' => now()->subMonths(6), ]); Instance::factory()->create([ 'status' => 'ended', 'subdomain' => 'nc-gestern', 'archive_volid' => 'local:backup/vzdump-qemu-222.vma.zst', 'archived_at' => now()->subDay(), 'torn_down_at' => now()->subDay(), ]); $seite = Livewire::actingAs(admin(), 'operator')->test(AdminInstances::class); expect(collect($seite->viewData('teardowns'))->pluck('address')->all()) ->toBe(['nc-gestern', 'nc-frueher']); }); it('führt eine Instanz ohne nachgewiesenes Archiv nicht als abgebaut auf', function () { // `torn_down_at` ohne `archive_volid` darf es nach ArchiveAndTearDown gar // nicht geben — und wenn es das doch einmal gibt, ist die Zeile eine // Behauptung über ein Archiv, das niemand finden wird. Lieber gar kein // Eintrag als ein Fundort, den es nicht gibt. Instance::factory()->create([ 'status' => 'ended', 'subdomain' => 'nc-ohnearchiv', 'archived_at' => now()->subDay(), 'torn_down_at' => now()->subDay(), ]); Livewire::actingAs(admin(), 'operator')->test(AdminInstances::class) ->assertDontSee(__('admin.teardown.archive_title')); }); // ---- Beide Sprachdateien tragen dieselben Schlüssel ---- it('hat jeden Schlüssel in beiden Sprachen', function () { $de = __('admin.teardown', locale: 'de'); $en = __('admin.teardown', locale: 'en'); expect(array_keys($de))->toBe(array_keys($en)) ->and($de)->not->toBe($en); });