by = Operator::factory()->create(); $this->vault = app(SecretVault::class); // HETZNER_DNS_TOKEN is real in this operator's .env and, unlike // STRIPE_WEBHOOK_SECRET, not blanked in phpunit.xml — left alone, the // fallback-to-environment tests below would silently pass or fail // depending on whose machine runs them instead of on the code. config()->set('provisioning.dns.token', null); }); it('reads the slot of the active mode', function () { $this->vault->put('dns.token', 'live-token', $this->by, OperatingMode::Live); $this->vault->put('dns.token', 'test-token', $this->by, OperatingMode::Test); OperatingMode::set(OperatingMode::Test); expect($this->vault->get('dns.token'))->toBe('test-token'); OperatingMode::set(OperatingMode::Live); expect($this->vault->get('dns.token'))->toBe('live-token'); }); it('falls back to the live slot when the test slot is empty', function () { // Die Regel des Inhabers: wo es keinen Testzugang gibt, wird der echte // benutzt, damit der Testbetrieb überhaupt arbeiten kann. $this->vault->put('dns.token', 'live-token', $this->by, OperatingMode::Live); OperatingMode::set(OperatingMode::Test); expect($this->vault->get('dns.token'))->toBe('live-token'); }); it('does not fall back from live to test', function () { // Die andere Richtung wäre absurd: im Livebetrieb einen Testzugang zu // benutzen, weil der echte fehlt. $this->vault->put('dns.token', 'test-token', $this->by, OperatingMode::Test); OperatingMode::set(OperatingMode::Live); expect($this->vault->get('dns.token'))->toBeNull(); }); it('still falls back to the environment when neither slot is filled', function () { // Der Weg für eine Installation, in der noch gar nichts gespeichert ist. // Einwertig und modusfrei — den Anfangszustand gibt es nur einmal. config()->set('provisioning.dns.token', 'from-env'); expect($this->vault->get('dns.token'))->toBe('from-env'); }); it('reports the source per slot', function () { $this->vault->put('dns.token', 'live-token', $this->by, OperatingMode::Live); expect($this->vault->source('dns.token', OperatingMode::Live))->toBe('stored'); // NICHT 'none': im Testbetrieb ist genau diese Live-Zeile in Kraft (der // Rückfall zwei Tests weiter oben). 'none' hieße „nichts hinterlegt" über // ein Zugangsdatum, mit dem gerade gearbeitet wird — siehe den Test // „names the live slot as the source…" weiter unten. expect($this->vault->source('dns.token', OperatingMode::Test))->toBe('stored_live'); }); it('forgets one slot without touching the other', function () { $this->vault->put('dns.token', 'live-token', $this->by, OperatingMode::Live); $this->vault->put('dns.token', 'test-token', $this->by, OperatingMode::Test); $this->vault->forget('dns.token', OperatingMode::Test); // 'stored_live' statt 'stored' ist der Beweis, dass die Testzeile weg ist: // was jetzt im Testplatz gilt, kommt aus dem Live-Platz. expect($this->vault->source('dns.token', OperatingMode::Test))->toBe('stored_live'); expect($this->vault->source('dns.token', OperatingMode::Live))->toBe('stored'); }); /** * Der Widerspruch, den das Schluss-Review gemessen hat — und warum er zählt. * * Modus test, `dns.token` nur im `:live`-Platz, `HETZNER_DNS_TOKEN` in der * `.env`: `get()` lieferte den gespeicherten Live-Wert (der Rückfall oben), * `source()` meldete dazu „environment" und `outline()` null. Die Karte in der * Konsole las sich damit als „Aus der Serverdatei" über ein Zugangsdatum, das * gerade aus der Datenbank in Kraft ist — ein Betreiber, der den Token wechseln * will, wird in die `.env` geschickt, wo eine Änderung wirkungslos bleibt. * Genau die Falle, die im Kopfkommentar von OperatingMode als Anlass dieses * ganzen Vorhabens steht, und R19s Attrappe wörtlich: eine Anzeige, die sich * wie eine Auskunft liest, falsch ist, und den Nächsten vom Nachsehen abhält. * * Ein DRITTER Rückgabewert, nicht 'stored': 'stored' blendet den * „Vergessen"-Knopf ein, und der zeigte dann auf den leeren Testplatz. */ it('names the live slot as the source when it is the one in force', function () { config()->set('provisioning.dns.token', 'from-env'); $this->vault->put('dns.token', 'live-token-value', $this->by, OperatingMode::Live); OperatingMode::set(OperatingMode::Test); expect($this->vault->get('dns.token'))->toBe('live-token-value') ->and($this->vault->source('dns.token'))->toBe('stored_live'); }); it('shows the outline and the date of the value that is actually in force', function () { // Die zweite Folge desselben Befunds: ohne Umriss fehlt der Block // „Hinterlegt / Geändert" ganz, und die Karte behauptet durch Weglassen, // es liege nichts vor. $this->vault->put('dns.token', 'live-token-value', $this->by, OperatingMode::Live); OperatingMode::set(OperatingMode::Test); $outline = $this->vault->outline('dns.token'); expect($outline)->not->toBeNull() ->and($outline)->toContain('alue') ->and($this->vault->updatedAt('dns.token'))->not->toBeNull(); }); it('keeps the strict entry out of the fallback here too', function () { // Stripe hat keinen Rückfall, in keine Richtung — source() darf ihn // deshalb auch nicht als 'stored_live' anzeigen, sonst widerspräche die // Karte wieder der Kasse. $this->vault->put('stripe.secret', 'sk_live_real', $this->by, OperatingMode::Live); OperatingMode::set(OperatingMode::Test); expect($this->vault->get('stripe.secret'))->toBeNull() ->and($this->vault->source('stripe.secret'))->toBe('none') ->and($this->vault->outline('stripe.secret'))->toBeNull(); });