189 lines
8.5 KiB
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();
|
|
});
|