CluPilotCloud/tests/Feature/Readiness/ProvisioningChecksTest.php

156 lines
6.8 KiB
PHP

<?php
use App\Models\Host;
use App\Models\Operator;
use App\Models\PlanFamily;
use App\Services\Secrets\SecretVault;
use App\Support\OperatingMode;
use App\Support\Readiness;
use App\Support\Readiness\Check;
use App\Support\Settings;
function provisioningCheck(string $key): ?Check
{
return collect(Readiness::all())->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('');
}
});