CluPilotCloud/tests/Feature/Cancellation/DashboardCountdownTest.php

343 lines
14 KiB
PHP

<?php // tests/Feature/Cancellation/DashboardCountdownTest.php
use App\Livewire\Dashboard;
use App\Models\Customer;
use App\Models\Instance;
use App\Models\Order;
use App\Models\ProvisioningRun;
use App\Models\User;
use Livewire\Livewire;
/**
* Ein Kunde mit einer gekündigten Instanz — nach dem Muster von
* kuendbareInstanz() (tests/Feature/Cancellation/CancelAsksAboutExportTest.php):
* Nutzer, Kunde, Bestellung, Instanz. Anders als dort schon im Zustand NACH
* der Kündigung, so wie ihn ConfirmCancelPackage::cancelPackage() hinterlässt
* — status und service_ends_at sind bereits gesetzt.
*
* @return array{0: User, 1: Instance}
*/
function gekuendigteInstanz(\Illuminate\Support\Carbon $endetIn, string $status = 'cancellation_scheduled'): array
{
$user = User::factory()->create(['email' => 'gekuendigt@example.test', 'email_verified_at' => now()]);
$customer = Customer::factory()->create([
'email' => 'gekuendigt@example.test',
'user_id' => $user->id,
'status' => 'active',
]);
$order = Order::factory()->create(['customer_id' => $customer->id, 'plan' => 'team']);
$instance = Instance::factory()->create([
'customer_id' => $customer->id,
'order_id' => $order->id,
'plan' => 'team',
'status' => $status,
'subdomain' => 'gekuendigt',
'cancel_requested_at' => now()->subDays(2),
'service_ends_at' => $endetIn,
]);
return [$user, $instance->refresh()];
}
/**
* Ein Kunde mit einer laufenden Instanz, die niemand gekündigt hat —
* dieselbe Form wie gekuendigteInstanz(), nur ohne Kündigung.
*
* @return array{0: User, 1: Instance}
*/
function laufendeInstanz(): array
{
$user = User::factory()->create(['email' => 'laeuft@example.test', 'email_verified_at' => now()]);
$customer = Customer::factory()->create([
'email' => 'laeuft@example.test',
'user_id' => $user->id,
'status' => 'active',
]);
$order = Order::factory()->create(['customer_id' => $customer->id, 'plan' => 'team']);
$instance = Instance::factory()->create([
'customer_id' => $customer->id,
'order_id' => $order->id,
'plan' => 'team',
'status' => 'active',
'subdomain' => 'laeuft',
]);
return [$user, $instance->refresh()];
}
it('zeigt nach einer Kuendigung, wie lange der Zugang noch steht', function () {
[$user, $instance] = gekuendigteInstanz(endetIn: now()->addDays(12));
Livewire::actingAs($user)->test(Dashboard::class)
->assertSee(__('dashboard.ending.title'))
->assertSee($instance->service_ends_at->local()->isoFormat('LL'));
});
it('zeigt den Streifen nicht, solange nicht gekuendigt wurde', function () {
[$user] = laufendeInstanz();
Livewire::actingAs($user)->test(Dashboard::class)
->assertDontSee(__('dashboard.ending.title'));
});
it('zeigt ihn auch nach dem Laufzeitende nicht mehr', function () {
// Danach ist der Zugang zu, und ein Streifen, der eine Restzeit nennt,
// wäre schlicht falsch. Was dann gilt, sagt der Kasten aus der Prüfung
// darunter — bis zur Fix-Welle behauptete dieser Kommentar das, und die
// Seite sagte das Gegenteil.
[$user] = gekuendigteInstanz(endetIn: now()->subDay(), status: 'ended');
Livewire::actingAs($user)->test(Dashboard::class)
->assertDontSee(__('dashboard.ending.title'));
});
it('sagt nach dem Laufzeitende, dass die Laufzeit beendet ist', function () {
// K2, zweite Hälfte: Dashboard::render() holt eine `ended`-Instanz nicht
// mehr, und der Kunde fiel damit in denselben Zweig wie jemand, der noch
// nie etwas bestellt hat — „Ihre Cloud wird eingerichtet." samt „Paket
// buchen", am Tag, an dem ihm EndInstanceService die Adresse eingezogen
// hat. Das ist kein Kunde ohne Paket, sondern einer, dessen Paket vorbei
// ist, und die Seite muss das sagen, samt dem Datum.
[$user, $instance] = gekuendigteInstanz(endetIn: now()->subDay(), status: 'ended');
Livewire::actingAs($user)->test(Dashboard::class)
->assertSee(__('dashboard.ended.label'))
->assertSee($instance->service_ends_at->local()->isoFormat('LL'))
->assertDontSee(__('dashboard.title_pending'))
->assertDontSee(__('dashboard.no_instance_label'));
});
it('zeigt einem Kunden ohne jede Instanz weiterhin den leeren Zustand', function () {
// Die Gegenprobe zur Prüfung darüber: der beendete Fall darf den ehrlichen
// Leerzustand nicht verschlucken. Wer nie etwas bestellt hat, bekommt
// weiterhin „Noch keine Instanz" — und keine Rede von einer Laufzeit, die
// es nie gab.
$user = User::factory()->create(['email' => 'ohne@example.test', 'email_verified_at' => now()]);
Customer::factory()->create([
'email' => 'ohne@example.test',
'user_id' => $user->id,
'status' => 'active',
]);
Livewire::actingAs($user)->test(Dashboard::class)
->assertSee(__('dashboard.no_instance_label'))
->assertDontSee(__('dashboard.ended.label'));
});
it('nennt keine Restzeit, die schon abgelaufen ist', function () {
// Der Fall zwischen den Stühlen: das Laufzeitende ist vorbei, aber der
// stündliche Lauf hat die Instanz noch nicht auf `ended` gestellt. Eine
// Zeile "noch -3 Stunden" ist die Sorte Fehler, die niemand meldet und
// jeder sieht.
//
// Korrektur gegenüber dem Zettel: `not->toContain('-')` über das ganze
// Dokument trägt nicht — ein Datum und eine CSS-Klasse enthalten ebenso
// Bindestriche, und die Prüfung wäre aus dem falschen Grund rot oder
// grün. Sie zielt stattdessen auf die Restzeit selbst: der Streifen darf
// gar nicht erst erscheinen, wenn das Laufzeitende vorbei ist — unabhängig
// davon, ob der Status das schon nachvollzogen hat. Genau das ist die
// Prüfung, die eine Implementierung ertappt, die sich allein auf den
// Status verlässt und das Datum ignoriert.
[$user] = gekuendigteInstanz(endetIn: now()->subHours(3));
Livewire::actingAs($user)->test(Dashboard::class)
->assertDontSee(__('dashboard.ending.title'));
});
it('nennt die Restzeit in ganzen Tagen, solange mehr als ein Tag bleibt', function () {
// Nicht im Zettel geprüft, aber ausdrücklich gefordert: "noch 12 Tage"
// liest sich, "noch 11 Tage 7 Stunden" nicht — abgerundet auf ganze Tage.
[$user] = gekuendigteInstanz(endetIn: now()->addDays(3)->addHours(5));
Livewire::actingAs($user)->test(Dashboard::class)
->assertSee(trans_choice('dashboard.ending.remaining_days', 3));
});
it('zaehlt am letzten Tag in Stunden, nicht mehr in Tagen', function () {
[$user] = gekuendigteInstanz(endetIn: now()->addHours(5));
Livewire::actingAs($user)->test(Dashboard::class)
->assertSee(trans_choice('dashboard.ending.remaining_hours', 5));
});
/*
* Die drei Zustände am Streifen, je einer pro Prüfung.
*
* Vorher stand hier EINE Prüfung, und sie konnte nicht fehlschlagen:
* `x-ui.switch` rendert beide Wörter und überlässt dem CSS, welches sichtbar
* ist — `assertSee('Kein Export gewünscht')` war deshalb bei `true`, bei
* `false` UND bei `null` grün. Genau deshalb kam K1 durch: `(bool) null` ist
* `false`, der Schalter behauptete einem nie gefragten Menschen seine eigene
* Antwort, und nichts hielt die drei Zustände auseinander.
*
* Die Auswahlfelder tragen ihren Zustand dagegen im Markup (`checked`), also
* prüft das hier wirklich das, was der Kunde sieht.
*/
it('zeigt am Streifen ein Ja als Ja', function () {
[$user, $instance] = gekuendigteInstanz(endetIn: now()->addDays(12));
$instance->update(['export_wish' => true]);
Livewire::actingAs($user)->test(Dashboard::class)
->assertSeeHtml('id="export-wish-yes" checked')
->assertDontSeeHtml('id="export-wish-no" checked');
});
it('zeigt am Streifen ein Nein als Nein', function () {
[$user, $instance] = gekuendigteInstanz(endetIn: now()->addDays(12));
$instance->update(['export_wish' => false]);
Livewire::actingAs($user)->test(Dashboard::class)
->assertSeeHtml('id="export-wish-no" checked')
->assertDontSeeHtml('id="export-wish-yes" checked');
});
it('behauptet keine Antwort, wenn niemand gefragt wurde', function () {
// Der Schaden, gegen den der Kopfkommentar der Wanderung geschrieben
// wurde: eine vor dieser Ausrollung gekündigte Instanz trägt `null`, und
// der Streifen sagte darunter „Kein Export gewünscht" — eine Aussage über
// einen Wunsch, den dieser Mensch nie geäußert hat, dargestellt als seine
// eigene Antwort. Keine Auswahl ist markiert, und der Satz daneben fragt.
[$user, $instance] = gekuendigteInstanz(endetIn: now()->addDays(12));
expect($instance->export_wish)->toBeNull();
Livewire::actingAs($user)->test(Dashboard::class)
->assertDontSeeHtml('id="export-wish-yes" checked')
->assertDontSeeHtml('id="export-wish-no" checked')
->assertSee(__('dashboard.ending.export_open'));
});
it('laesst die Antwort am Streifen aendern', function () {
[$user, $instance] = gekuendigteInstanz(endetIn: now()->addDays(12));
$instance->update(['export_wish' => false]);
Livewire::actingAs($user)->test(Dashboard::class)
->call('setExportWish', true);
expect($instance->fresh()->export_wish)->toBeTrue();
});
it('laesst die Antwort nach dem Laufzeitende nicht mehr aendern', function () {
// Aufgabe 4: danach ist die Instanz `ended`, die Adresse eingezogen, und
// ob ein Export vorbereitet wurde, ist entschieden. Ein Aufruf, der dort
// noch etwas verspricht, wäre die nächste Attrappe — geprüft direkt gegen
// die Methode, nicht nur gegen den (dann ohnehin nicht mehr sichtbaren)
// Schalter im Streifen: setExportWish() ist über /livewire/update auch am
// Formular vorbei erreichbar.
[$user, $instance] = gekuendigteInstanz(endetIn: now()->subDay(), status: 'ended');
$instance->update(['export_wish' => false]);
Livewire::actingAs($user)->test(Dashboard::class)
->call('setExportWish', true);
expect($instance->fresh()->export_wish)->toBeFalse();
});
/**
* Ein wiederkehrender Kunde: die alte Instanz ist beendet, die neue eben
* bestellt und bezahlt. Der Aufbau legt sie als `reserving` an
* (App\Provisioning\Steps\Customer\ReserveResources) und lässt sie den ganzen
* Lauf lang so stehen; scheitert er, setzt Order::markFailed() sie auf
* `failed`, bis ein Betreiber wiederholt. Beide Zustände fehlen in der
* Betriebs-Abfrage von Dashboard::render() — genau deshalb braucht es den
* Wächter in endedAt().
*
* @return array{0: User, 1: Instance} Nutzer und die NEUE Instanz
*/
function wiederkehrenderKunde(string $neuerStatus): array
{
$user = User::factory()->create(['email' => 'wiederkehrer@example.test', 'email_verified_at' => now()]);
$customer = Customer::factory()->create([
'email' => 'wiederkehrer@example.test',
'user_id' => $user->id,
'status' => 'active',
]);
$alteBestellung = Order::factory()->create(['customer_id' => $customer->id, 'plan' => 'team']);
Instance::factory()->create([
'customer_id' => $customer->id,
'order_id' => $alteBestellung->id,
'plan' => 'team',
'status' => 'ended',
'subdomain' => 'altbestand',
'cancel_requested_at' => now()->subMonths(2),
'service_ends_at' => now()->subMonth(),
]);
$neueBestellung = Order::factory()->create(['customer_id' => $customer->id, 'plan' => 'team']);
$neu = Instance::factory()->create([
'customer_id' => $customer->id,
'order_id' => $neueBestellung->id,
'plan' => 'team',
'status' => $neuerStatus,
'subdomain' => 'neubau',
]);
return [$user, $neu->refresh()];
}
it('sagt einem wiederkehrenden Kunden nicht, sein Paket sei beendet, während die neue Cloud gebaut wird', function (string $neuerStatus) {
// Der Rückschlag der Fix-Welle: endedAt() fragte nur „läuft gerade nichts?"
// und nicht „ist die beendete auch die jüngste?". Wer nach einer beendeten
// Laufzeit neu bestellt und bezahlt hat, las deshalb „Ihre Cloud ist
// beendet." samt „Neues Paket buchen" — direkt über dem Streifen, der
// seinen laufenden Aufbau zeigt. Beide Zustände geprüft, weil beide aus der
// Betriebs-Abfrage herausfallen und beide tagelang stehen können.
[$user] = wiederkehrenderKunde($neuerStatus);
Livewire::actingAs($user)->test(Dashboard::class)
->assertDontSee(__('dashboard.title_ended'))
->assertDontSee(__('dashboard.ended.label'))
->assertDontSee(__('dashboard.ended.order_again'));
})->with(['reserving', 'failed']);
it('sagt es auch dann nicht, wenn für die neue Bestellung noch gar keine Instanz angelegt ist', function () {
// Das zweite Fenster derselben Falschaussage, und das längere:
// ReserveResources parkt eine bezahlte Bestellung, für die kein Host Platz
// hat, und legt dabei KEINE Instanz an — bis zu vierzehn Tage. Die jüngste
// Instanz des Kunden ist in diesem Fenster weiterhin die alte, beendete,
// der Wächter über den Instanzzustand allein greift also nicht. Was ihn
// trägt, ist der laufende Aufbau selbst — dieselbe Bedingung, unter der
// CustomerProvisioning den Fortschrittsstreifen zeigt.
$user = User::factory()->create(['email' => 'geparkt@example.test', 'email_verified_at' => now()]);
$customer = Customer::factory()->create([
'email' => 'geparkt@example.test',
'user_id' => $user->id,
'status' => 'active',
]);
$alteBestellung = Order::factory()->create(['customer_id' => $customer->id, 'plan' => 'team']);
Instance::factory()->create([
'customer_id' => $customer->id,
'order_id' => $alteBestellung->id,
'plan' => 'team',
'status' => 'ended',
'subdomain' => 'altbestand',
'service_ends_at' => now()->subMonth(),
]);
$neueBestellung = Order::factory()->create(['customer_id' => $customer->id, 'plan' => 'team']);
ProvisioningRun::factory()->create([
'subject_type' => Order::class,
'subject_id' => $neueBestellung->id,
'pipeline' => 'customer',
'status' => ProvisioningRun::STATUS_WAITING,
]);
Livewire::actingAs($user)->test(Dashboard::class)
->assertDontSee(__('dashboard.title_ended'))
->assertDontSee(__('dashboard.ended.label'));
});