firstWhere('key', $key); } it('needs at least one datacenter', function () { // create_datacenters_table selbst legt 'fsn'/'hel' als Vorgabe an — auch // in dieser Suite, da es sich um eine Migration und keinen Seeder // handelt. Erst geleert, damit dieser Test den echten leeren Zustand // prüft statt der Werkseinstellung dieser Installation. Datacenter::query()->delete(); // ValidateHostInput ist der erste Schritt der Host-Kette und bricht ohne // Rechenzentrum ab — bevor überhaupt eine SSH-Verbindung versucht wird. expect(onboardingCheck('onboarding.datacenter')->satisfied)->toBeFalse(); Datacenter::factory()->create(); expect(onboardingCheck('onboarding.datacenter')->satisfied)->toBeTrue(); }); it('needs an ssh private key', function () { // Mode selbst gesetzt (Testisolation: CACHE_STORE=array wird zwischen // Testdateien nicht geleert), damit dieser Test nicht vom Rest der Zeile // abhängt, in der die Suite läuft. OperatingMode::set(OperatingMode::Live); // Diese Installation hat in .env tatsächlich einen SSH-Schlüssel // konfiguriert (CLUPILOT_SSH_PRIVATE_KEY_PATH), auf den SecretVault::get() // zurückfällt, solange kein Tresor-Eintrag existiert — direkt // zurückgesetzt, damit der Test nicht vom Entwicklerrechner abhängt. config()->set('provisioning.ssh.private_key', ''); expect(onboardingCheck('onboarding.ssh_private_key')->satisfied)->toBeFalse(); app(SecretVault::class)->put( 'ssh.private_key', '-----BEGIN OPENSSH PRIVATE KEY-----', Operator::factory()->create(), OperatingMode::Live, ); expect(onboardingCheck('onboarding.ssh_private_key')->satisfied)->toBeTrue(); }); it('needs a usable vault key', function () { // isUsable() statt eines bloßen filled()-Checks: SecretCipher warnt in // seinem eigenen Kommentar genau davor, eine zweite, schwächere Regel zu // bauen, die einen falsch geformten Schlüssel als gültig durchwinkt. config()->set('admin_access.secrets_key', ''); expect(onboardingCheck('onboarding.secrets_key')->satisfied)->toBeFalse(); config()->set('admin_access.secrets_key', base64_encode(str_repeat('a', 32))); expect(onboardingCheck('onboarding.secrets_key')->satisfied)->toBeTrue(); }); it('needs a usable vpn config key', function () { config()->set('admin_access.vpn_config_key', ''); expect(onboardingCheck('onboarding.vpn_config_key')->satisfied)->toBeFalse(); config()->set('admin_access.vpn_config_key', base64_encode(str_repeat('b', 32))); expect(onboardingCheck('onboarding.vpn_config_key')->satisfied)->toBeTrue(); }); it('needs all three wireguard values, not just some', function () { // Zwei von drei ist kein halber Tunnel, sondern gar keiner. Subnet wird // bewusst direkt über config() geprüft statt über Settings/ // ProvisioningSettings (siehe deren eigener Kopfkommentar: der Compose- // Stack liest denselben Schlüssel beim Containerstart) — und hier im Test // ebenso direkt zurückgesetzt, damit das Ergebnis nicht von den echten // WireGuard-Werten dieser Installation abhängt. config()->set('provisioning.wireguard.hub_public_key', ''); config()->set('provisioning.wireguard.endpoint', ''); config()->set('provisioning.wireguard.subnet', ''); expect(onboardingCheck('onboarding.wg_hub')->satisfied)->toBeFalse(); Settings::set('provisioning.wg_hub_pubkey', 'abc='); Settings::set('provisioning.wg_endpoint', '198.51.45.9:51820'); expect(onboardingCheck('onboarding.wg_hub')->satisfied)->toBeFalse(); config()->set('provisioning.wireguard.subnet', '10.66.0.0/24'); expect(onboardingCheck('onboarding.wg_hub')->satisfied)->toBeTrue(); }); it('names what breaks for every onboarding check, not just what is missing', function () { foreach ([ 'onboarding.ssh_private_key', 'onboarding.secrets_key', 'onboarding.vpn_config_key', 'onboarding.wg_hub', 'onboarding.datacenter', ] as $key) { expect(onboardingCheck($key)->breaks)->not->toBe(''); } });