firstWhere('key', $key); } /** * Am 2026-07-30 standen zwei Hosts auf `active`, ohne api_token_ref und mit * Adressen aus dem Dokumentationsbereich (203.0.113.0/24, RFC 5737). In der * Konsole waren sie von einem echten Host nicht zu unterscheiden — und * ReserveResources wählt aus `status = active`. Ein Kauf hätte sich einen * dieser Phantom-Hosts reserviert und wäre NACH der Zahlung gestorben. */ it('does not accept an active host without a readable token', function () { Host::factory()->create(['status' => 'active', 'api_token_ref' => null]); expect(provisioningCheck('provisioning.usable_host')->satisfied)->toBeFalse(); }); it('accepts an active host that carries a token', function () { Host::factory()->create(['status' => 'active', 'api_token_ref' => 'ref-1']); expect(provisioningCheck('provisioning.usable_host')->satisfied)->toBeTrue(); }); it('does not accept a host that carries a token but is not active', function () { Host::factory()->create(['status' => 'onboarding', 'api_token_ref' => 'ref-1']); expect(provisioningCheck('provisioning.usable_host')->satisfied)->toBeFalse(); }); it('treats the monitoring token as a warning while monitoring is not required', function () { // Überwachung darf eine Bereitstellung nie aufhalten — solange der // Betreiber sie nicht selbst zur Pflicht gemacht hat. Explizit gesetzt, // statt sich auf die Vorgabe dieser Installation zu verlassen. config()->set('provisioning.monitoring.required', false); expect(provisioningCheck('provisioning.monitoring_token')->severity) ->toBe(Check::SEVERITY_WARNING); }); /** * Fix-Runde (Befund 2): RegisterMonitoring wirft die Ausnahme weiter, statt * zu degradieren, sobald `provisioning.monitoring.required` auf true steht * (RegisterMonitoring.php: `if (config('provisioning.monitoring.required', * false)) { throw $e; }`). Ein starrer Schweregrad hätte in genau diesem * Fall etwas Falsches behauptet — die Seite müsste eine Sperre melden, die * es unter dieser Einstellung tatsächlich gibt. */ it('treats the monitoring token as a blocker once the operator makes monitoring required', function () { config()->set('provisioning.monitoring.required', true); expect(provisioningCheck('provisioning.monitoring_token')->severity) ->toBe(Check::SEVERITY_BLOCKING); }); it('needs a dns token', function () { OperatingMode::set(OperatingMode::Live); // Wie beim SSH-Schlüssel: HETZNER_DNS_TOKEN ist auf dieser Installation in // .env tatsächlich gesetzt, und SecretVault::get() fällt darauf zurück, // solange kein Tresor-Eintrag existiert. Zurückgesetzt, damit der Test // nicht vom Entwicklerrechner abhängt. config()->set('provisioning.dns.token', ''); expect(provisioningCheck('provisioning.dns_token')->satisfied)->toBeFalse(); app(SecretVault::class)->put('dns.token', 'token-value', Operator::factory()->create(), OperatingMode::Live); expect(provisioningCheck('provisioning.dns_token')->satisfied)->toBeTrue(); }); it('needs a dns zone', function () { // Direkt über config() zurückgesetzt statt sich auf den echten Wert // dieser Installation zu verlassen — genau die Falle, die // phpunit.xml zu CLUPILOT_DNS_ZONE dokumentiert. config()->set('provisioning.dns.zone', ''); expect(provisioningCheck('provisioning.dns_zone')->satisfied)->toBeFalse(); Settings::set('provisioning.dns_zone', 'clupilot.cloud'); expect(provisioningCheck('provisioning.dns_zone')->satisfied)->toBeTrue(); }); it('treats a missing traefik path as a warning, never as a blocker', function () { config()->set('provisioning.traefik.dynamic_path', ''); expect(provisioningCheck('provisioning.traefik_path')->satisfied)->toBeFalse(); expect(provisioningCheck('provisioning.traefik_path')->severity)->toBe(Check::SEVERITY_WARNING); Settings::set('provisioning.traefik_dynamic_path', '/etc/traefik/dynamic'); expect(provisioningCheck('provisioning.traefik_path')->satisfied)->toBeTrue(); }); it('does not report a template gap for a draft version nobody can buy yet', function () { $family = PlanFamily::query()->create(['key' => 'tmpl-draft', 'name' => 'Draft', 'tier' => 9]); $family->versions()->create([ 'version' => 1, 'quota_gb' => 10, 'traffic_gb' => 100, 'seats' => 1, 'ram_mb' => 1024, 'cores' => 1, 'disk_gb' => 20, 'performance' => 'standard', 'features' => [], 'available_from' => now(), // published_at bleibt null: ein Entwurf, den niemand kaufen kann. ]); expect(provisioningCheck('provisioning.vm_template')->satisfied)->toBeTrue(); }); it('reports a published version with no template as a gap', function () { // CloneVirtualMachine scheitert sofort mit `template_missing`, sobald // eine Bestellung dieses Plans läuft — nachdem der Kunde schon bezahlt hat. $family = PlanFamily::query()->create(['key' => 'tmpl-missing', 'name' => 'No Template', 'tier' => 9]); $version = $family->versions()->create([ 'version' => 1, 'quota_gb' => 10, 'traffic_gb' => 100, 'seats' => 1, 'ram_mb' => 1024, 'cores' => 1, 'disk_gb' => 20, 'performance' => 'standard', 'features' => [], 'available_from' => now(), ]); // published_at erst per update setzen: template_vmid ist FROZEN, sobald // eine Version schon veröffentlicht in der Datenbank steht, aber der // ERSTE Übergang von Entwurf zu veröffentlicht ist selbst kein Verstoß. $version->update(['published_at' => now()]); expect(provisioningCheck('provisioning.vm_template')->satisfied)->toBeFalse(); }); it('is satisfied once every published version carries a template', function () { $family = PlanFamily::query()->create(['key' => 'tmpl-ok', 'name' => 'Has Template', 'tier' => 9]); $family->versions()->create([ 'version' => 1, 'quota_gb' => 10, 'traffic_gb' => 100, 'seats' => 1, 'ram_mb' => 1024, 'cores' => 1, 'disk_gb' => 20, 'performance' => 'standard', 'features' => [], 'available_from' => now(), 'published_at' => now(), 'template_vmid' => 100, ]); expect(provisioningCheck('provisioning.vm_template')->satisfied)->toBeTrue(); }); it('names what breaks for every provisioning check, not just what is missing', function () { foreach ([ 'provisioning.dns_token', 'provisioning.dns_zone', 'provisioning.traefik_path', 'provisioning.usable_host', 'provisioning.vm_template', 'provisioning.monitoring_token', ] as $key) { expect(provisioningCheck($key)->breaks)->not->toBe(''); } });