CluPilotCloud/tests/Feature/Cancellation/TeardownHasReadersTest.php

204 lines
8.7 KiB
PHP

<?php
// tests/Feature/Cancellation/TeardownHasReadersTest.php
use App\Livewire\Admin\Instances as AdminInstances;
use App\Models\Customer;
use App\Models\Host;
use App\Models\Instance;
use Livewire\Livewire;
/**
* Wer sieht, was der Abbau hinterlassen hat.
*
* Zwei Zustände standen bis hierher nur in der Datenbank, und der Fehler wäre
* derselbe gewesen wie bei `export_wish` vor Teil A — nur teurer:
*
* - `teardown_error`: eine Instanz mit gefülltem Grund steht **unbegrenzt** und
* belegt weiter einen Platz auf ihrem Host. Sieht sie niemand, war der ganze
* Abbau umsonst.
* - `archive_volid` / `archived_at`: ohne diese Angabe ist eine
* Wiederherstellung eine Suche über fremde Ablagen.
*
* Und einen Fehler ausdrücklich nicht noch einmal: der Kasten „Datenexport
* bestellt" lässt sich bis heute nicht abräumen und sortiert die ältesten nach
* oben. Beide Kästen hier tun das Gegenteil, und beides ist unten festgenagelt.
*/
/**
* Eine Instanz, deren Abbau hängengeblieben ist: Grund vermerkt, Maschine
* steht noch. Eigener Name statt der Vorlagen aus den Nachbardateien — Pest
* führt diese Funktionen in einem gemeinsamen Namensraum.
*/
function haengenderAbbau(string $subdomain, string $grund, ?string $kunde = null): Instance
{
$host = Host::factory()->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);
});