153 lines
6.1 KiB
PHP
153 lines
6.1 KiB
PHP
<?php
|
|
|
|
use App\Models\DnsRecord;
|
|
use App\Models\Host;
|
|
use App\Models\Instance;
|
|
use App\Models\Operator;
|
|
use App\Services\Dns\HttpHetznerDnsClient;
|
|
use App\Services\Dns\LegacyRecordIds;
|
|
use App\Services\Secrets\SecretVault;
|
|
use App\Support\OperatingMode;
|
|
use App\Support\Settings;
|
|
use App\Support\StripeWebhookSecret;
|
|
use Illuminate\Support\Facades\Http;
|
|
use Illuminate\Support\Facades\Log;
|
|
|
|
/**
|
|
* Fix-Runde zum Codex-Review vom 31.7.2026, zwei P1.
|
|
*/
|
|
beforeEach(function () {
|
|
config()->set('admin_access.secrets_key', 'base64:'.base64_encode(random_bytes(32)));
|
|
});
|
|
|
|
// ---- P1 (1): Der Signaturschlüssel darf NICHT über die Modusgrenze fallen ----
|
|
|
|
it('never lets a stored live signing secret stand in for an empty test slot', function () {
|
|
// Jeder andere Tresorwert bezeichnet dasselbe Konto in beiden Modi und darf
|
|
// deshalb vom Live-Platz einspringen. Dieser nicht: Stripe stellt je Modus
|
|
// ein EIGENES Signaturgeheimnis aus. Ohne diese Trennung prüft der
|
|
// Testbetrieb seine Ereignisse mit dem Live-Schlüssel — sie scheitern alle,
|
|
// und die Konsole meldet den Platz trotzdem als belegt.
|
|
config()->set('services.stripe.webhook_secret', '');
|
|
config()->set('services.stripe.webhook_secret_test', '');
|
|
|
|
app(SecretVault::class)->put(
|
|
'stripe.webhook_secret', 'whsec_live_only', Operator::factory()->create(), OperatingMode::Live
|
|
);
|
|
|
|
OperatingMode::set(OperatingMode::Test);
|
|
|
|
expect(StripeWebhookSecret::current())->toBe('')
|
|
->and(app(SecretVault::class)->source('stripe.webhook_secret'))->toBe('none');
|
|
});
|
|
|
|
it('still uses the test line from the server file when only the live slot is stored', function () {
|
|
// Die Sperre gilt dem gespeicherten Platz, nicht der Serverdatei: dort
|
|
// steht je Modus eine eigene Zeile, und die ist für diesen Modus richtig.
|
|
config()->set('services.stripe.webhook_secret_test', 'whsec_test_env');
|
|
|
|
app(SecretVault::class)->put(
|
|
'stripe.webhook_secret', 'whsec_live_only', Operator::factory()->create(), OperatingMode::Live
|
|
);
|
|
|
|
OperatingMode::set(OperatingMode::Test);
|
|
|
|
expect(StripeWebhookSecret::current())->toBe('whsec_test_env')
|
|
->and(app(SecretVault::class)->source('stripe.webhook_secret'))->toBe('environment');
|
|
});
|
|
|
|
it('leaves the ordinary entries falling back as they did', function () {
|
|
// Die Trennung gilt NUR dem Signaturschlüssel. Ein DNS-Token bezeichnet
|
|
// dasselbe Hetzner-Konto in beiden Modi — dort ist der Rückfall gewollt und
|
|
// ausdrücklich getestet (SecretVaultModeTest).
|
|
app(SecretVault::class)->put(
|
|
'dns.token', 'token_live', Operator::factory()->create(), OperatingMode::Live
|
|
);
|
|
|
|
OperatingMode::set(OperatingMode::Test);
|
|
|
|
expect(app(SecretVault::class)->get('dns.token'))->toBe('token_live')
|
|
->and(app(SecretVault::class)->source('dns.token'))->toBe('stored_live');
|
|
});
|
|
|
|
// ---- P1 (2): Alte Record-IDs dürfen keinen Host unlöschbar machen ----
|
|
|
|
it('does not make a host unpurgeable because its record id predates the cloud API', function () {
|
|
// Mein Fehler im Review: ich hatte auf DIESER Datenbank null Zeilen gezählt
|
|
// und daraus geschlossen, es gebe nirgends welche. Der Live-Server ist eine
|
|
// andere Installation. Eine Alt-ID muss den Abbau deshalb durchlassen —
|
|
// sonst bleibt der Host für immer im Pool, weil PurgeHost bei jedem Versuch
|
|
// an derselben Zeile stirbt.
|
|
Settings::set('provisioning.dns_zone', 'probe.example');
|
|
Http::preventStrayRequests();
|
|
Http::fake();
|
|
|
|
expect(fn () => (new HttpHetznerDnsClient)->deleteRecord('rec-123'))->not->toThrow(Throwable::class);
|
|
|
|
// Und zwar OHNE einen Aufruf zu bauen, der irgendetwas anderes trifft.
|
|
Http::assertNothingSent();
|
|
});
|
|
|
|
it('says the leftover record out loud instead of swallowing it', function () {
|
|
// Durchlassen heißt nicht verschweigen: der Eintrag bleibt in der Zone
|
|
// stehen, und die einzige Stelle, an der das noch auffallen kann, ist das
|
|
// Log. Ein stiller Rücksprung wäre die Attrappe, vor der R19 warnt.
|
|
Settings::set('provisioning.dns_zone', 'probe.example');
|
|
Http::fake();
|
|
|
|
Log::shouldReceive('warning')
|
|
->once()
|
|
->withArgs(fn (string $message, array $context = []) => str_contains($message, 'legacy')
|
|
&& ($context['record_id'] ?? null) === 'rec-123');
|
|
|
|
(new HttpHetznerDnsClient)->deleteRecord('rec-123');
|
|
});
|
|
|
|
it('migrates the instance records it can address to the new format', function () {
|
|
// `dns_records` trägt fqdn UND typ neben der ID — daraus lässt sich der
|
|
// RRSet-Name exakt bilden. Für Hosts geht das nicht (dort steht nur der
|
|
// Kurzname), die bleiben der toleranten Löschung oben überlassen.
|
|
Settings::set('provisioning.dns_zone', 'probe.example');
|
|
|
|
$instance = Instance::factory()->create();
|
|
$legacy = DnsRecord::query()->create([
|
|
'instance_id' => $instance->id,
|
|
'provider' => 'hetzner',
|
|
'record_id' => 'rec-legacy-42',
|
|
'fqdn' => 'berger.probe.example',
|
|
'type' => 'A',
|
|
'value' => '203.0.113.9',
|
|
]);
|
|
|
|
app(LegacyRecordIds::class)->migrate();
|
|
|
|
expect($legacy->fresh()->record_id)->toBe('berger/A');
|
|
});
|
|
|
|
it('leaves a record it cannot address alone rather than guessing', function () {
|
|
Settings::set('provisioning.dns_zone', 'probe.example');
|
|
|
|
$instance = Instance::factory()->create();
|
|
$foreign = DnsRecord::query()->create([
|
|
'instance_id' => $instance->id,
|
|
'provider' => 'hetzner',
|
|
'record_id' => 'rec-foreign',
|
|
'fqdn' => 'irgendwo.fremde-zone.test',
|
|
'type' => 'A',
|
|
'value' => '203.0.113.9',
|
|
]);
|
|
|
|
app(LegacyRecordIds::class)->migrate();
|
|
|
|
expect($foreign->fresh()->record_id)->toBe('rec-foreign');
|
|
});
|
|
|
|
it('counts as done for a host whose legacy record could not be removed', function () {
|
|
Settings::set('provisioning.dns_zone', 'probe.example');
|
|
$s = fakeServices();
|
|
$host = Host::factory()->active()->create(['dns_record_id' => 'rec-legacy-99']);
|
|
|
|
// Der Fake spricht dieselbe Sprache wie der echte Client — auch hier.
|
|
$s['dns']->deleteRecord($host->dns_record_id);
|
|
})->throwsNoExceptions();
|