CluPilotCloud/tests/Feature/ReadinessPageTest.php

189 lines
8.5 KiB
PHP

<?php
use App\Livewire\Admin\Readiness as ReadinessPage;
use App\Models\Operator;
use App\Services\Secrets\SecretVault;
use App\Support\OperatingMode;
use App\Support\Readiness;
use Illuminate\Support\Facades\Http;
use Livewire\Livewire;
it('is closed to operators without either capability', function () {
$this->actingAs(Operator::factory()->role('Read-only')->create(), 'operator')
->get(route('admin.readiness'))
->assertForbidden();
});
it('lists every check, satisfied or not', function () {
// Livewire::actingAs() needs the 'operator' guard spelled out — omitting it
// signed the test in on the 'web' guard instead and cost Task 11 a failing
// test for exactly this reason.
Livewire::actingAs(Operator::factory()->role('Owner')->create(), 'operator')
->test(ReadinessPage::class)
->assertSee(__('readiness.billing.company_details'))
->assertSee(__('readiness.provisioning.usable_host'));
});
it('says what breaks, not only that something is missing', function () {
Livewire::actingAs(Operator::factory()->role('Owner')->create(), 'operator')
->test(ReadinessPage::class)
->assertSee(__('readiness.delivery.mailer_not_log_breaks'));
});
it('runs an active check on demand and shows its answer', function () {
// Die drei Prüfungen aus Task 9 sind nur dann etwas wert, wenn jemand sie
// auslösen kann. Sie laufen NICHT bei jedem Seitenaufruf mit: eine davon
// schreibt einen TXT-Eintrag in die echte Zone.
//
// Http::fake() steht hier unbedingt, nicht weil dieser eine Testpfad das
// Netzwerk erreicht (DnsTokenCheck bricht schon vorher an blank($token)
// || blank($zone) ab), sondern weil JEDER Test, der runCheck() anfasst,
// dieselbe Absicherung tragen soll — ein Testpfad, der heute früh
// ausscheidet, ist keine Garantie, dass er es morgen noch tut.
Http::fake();
Livewire::actingAs(Operator::factory()->role('Owner')->create(), 'operator')
->test(ReadinessPage::class)
->call('runCheck', 'provisioning.dns_token')
->assertSet('results.provisioning.dns_token.ok', false);
});
/**
* Der „Prüfen"-Knopf darf der Prüfung nicht widersprechen.
*
* StripeCheck::run() rechnet die Kontoart des Schlüssels aus, die Seite hat
* sie verworfen und nur `ok`/`reason` gerendert — ein Live-Schlüssel im
* Testplatz antwortet bei Stripe einwandfrei, und die Seite las darüber
* „Geprüft: in Ordnung", während die Plakette oben „Testbetrieb" sagte.
*/
it('names the account a checked key belongs to, and says when it is the wrong one', function () {
Http::fake(['*' => Http::response(['id' => 'acct_1'], 200)]);
OperatingMode::set(OperatingMode::Test);
app(SecretVault::class)->put('stripe.secret', 'sk_live_REALMONEY', Operator::factory()->create(), OperatingMode::Test);
Livewire::actingAs(Operator::factory()->role('Owner')->create(), 'operator')
->test(ReadinessPage::class)
->call('runCheck', 'billing.stripe_secret')
->assertSee(__('readiness.check_key_mode', ['mode' => __('readiness.mode.live')]))
->assertSee(__('readiness.check_key_mode_conflict', ['mode' => __('readiness.mode.test')]));
});
it('refuses to run a check that is not on the list', function () {
// Ein Schlüssel aus dem Browser. Ohne diese Prüfung wäre das ein Weg,
// beliebige Klassen aufzurufen.
Http::fake();
Livewire::actingAs(Operator::factory()->role('Owner')->create(), 'operator')
->test(ReadinessPage::class)
->call('runCheck', 'etwas.erfundenes')
->assertSet('results', []);
});
/**
* Der Wächter dieser Spec.
*
* Ein künftig hinzugefügtes Zugangsdatum darf nicht still an der Übersicht
* vorbeigehen. Genau das war der Befund vom 2026-07-30: eine grüne Testsuite
* bei einer Installation, die keinen Host durchinstallieren konnte, weil
* niemand die Liste geführt hat.
*/
it('covers every entry in the secret registry', function () {
$covered = collect(Readiness::all())->pluck('key')->implode(' ');
foreach (array_keys(SecretVault::REGISTRY) as $secret) {
// `stripe.secret` → irgendein Prüfschlüssel, der `stripe_secret` enthält.
$needle = str_replace('.', '_', $secret);
// NICHT expect($covered)->toContain($needle, $message) — Pest's
// toContain() ist variadic, ein zweites Argument ist ein zweiter
// GEFORDERTER Substring, keine eigene Fehlermeldung. Genau diese Falle
// dokumentiert tests/Feature/ModalHeightTest.php bereits selbst
// ("asserted that the footer contains its own filename, which is why
// this failed on a file that was correct") — der Brief-Code für diesen
// Test tappte hinein: er scheiterte auf JEDEM Eintrag, auch auf
// 'stripe_secret', das längst abgedeckt ist. str_contains() + toBeTrue()
// mit Nachricht liefert die Prüfung UND den lesbaren Hinweis.
expect(str_contains($covered, $needle))->toBeTrue(
"Der Tresor-Eintrag [{$secret}] taucht auf der Bereitschaftsseite nicht auf."
);
}
});
/**
* Fix-Runde, Befund 1: der Auflöser (checkUrl()) ist nur so gut wie seine
* Quelle. Neun Prüfschlüssel trugen `tab: 'integrations'` — kein Mitglied von
* Integrations::TABS — und landeten deshalb alle auf dem Standard-Reiter
* ('services'), unabhängig davon, wo ihr Feld tatsächlich liegt. Dieser Test
* hält JEDEN Prüfschlüssel gegen die eine Stelle, an der sein Feld wirklich
* steht — nicht nur die neun, die diesmal falsch waren — damit der nächste
* falsche Wert genauso durchrutscht wie diese.
*/
it('sends every check to the tab or page where its field actually lives', function () {
$expectedTab = [
'billing.stripe_secret' => 'services',
'billing.webhook_secret' => 'env',
'billing.company_details' => 'company',
'billing.invoice_series' => 'company',
'billing.catalogue_synced' => 'services',
'onboarding.ssh_private_key' => 'platform',
'onboarding.secrets_key' => 'env',
'onboarding.vpn_config_key' => 'env',
'onboarding.wg_hub' => 'platform',
'onboarding.datacenter' => 'datacenters',
'provisioning.dns_token' => 'services',
'provisioning.dns_zone' => 'services',
'provisioning.traefik_path' => 'services',
'provisioning.usable_host' => 'hosts',
'provisioning.vm_template' => 'plans',
'provisioning.monitoring_token' => 'services',
'delivery.mailer_not_log' => 'mail',
'delivery.mailbox' => 'mail',
'delivery.mail_templates' => 'templates',
'delivery.inbound_mail_password' => 'services',
'operation.scheduler' => 'env',
'operation.queue_provisioning' => 'env',
];
foreach (Readiness::all() as $check) {
// NICHT expect($expectedTab)->toHaveKey($check->key, $message) — genau
// dieselbe Falle wie bei toContain() weiter oben: toHaveKey()s zweites
// Argument ist der erwartete WERT des Schlüssels, keine Fehlermeldung.
expect(array_key_exists($check->key, $expectedTab))->toBeTrue(
"Neuer Prüfschlüssel [{$check->key}] hat keinen erwarteten tab-Wert in diesem Test — bitte nachtragen."
);
expect($check->tab)->toBe(
$expectedTab[$check->key],
"{$check->key} zeigt auf '{$check->tab}', sein Feld liegt aber unter '{$expectedTab[$check->key]}'."
);
}
});
/**
* Fix-Runde, Befund 2: mount() lässt hosts.manage ODER secrets.manage durch,
* runCheck() verlangt aber secrets.manage allein. Ein Admin (nur
* hosts.manage) erreicht die Seite und sah bislang auch den Knopf — der beim
* Drücken 403 geworfen hätte. Ein sichtbarer Knopf, der beim Drücken
* scheitert, ist schlimmer als gar keiner.
*/
it('hides the check button from an operator who could not press it anyway', function () {
Livewire::actingAs(Operator::factory()->role('Admin')->create(), 'operator')
->test(ReadinessPage::class)
->assertDontSee(__('readiness.run_check'));
});
/**
* Fix-Runde, Befund 3: die eine Eigenschaft, wegen der diese ganze
* Konstruktion existiert — die vier RUNNABLE-Prüfungen laufen NUR auf
* Knopfdruck, weil DnsTokenCheck einen echten TXT-Eintrag in die
* Produktivzone schreibt — war bislang ungeprüft.
*/
it('sends nothing over the network on a plain page load', function () {
Http::fake();
Livewire::actingAs(Operator::factory()->role('Owner')->create(), 'operator')
->test(ReadinessPage::class);
Http::assertNothingSent();
});