343 lines
14 KiB
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'));
|
|
});
|