137 lines
5.9 KiB
PHP
137 lines
5.9 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, never as a blocker', function () {
|
|
// Überwachung darf eine Bereitstellung nie aufhalten.
|
|
expect(provisioningCheck('provisioning.monitoring_token')->severity)
|
|
->toBe(Check::SEVERITY_WARNING);
|
|
});
|
|
|
|
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('');
|
|
}
|
|
});
|