CluPilotCloud/tests/Feature/Cancellation/DashboardCountdownTest.php

240 lines
10 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\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();
});