diff --git a/app/Services/Secrets/SecretVault.php b/app/Services/Secrets/SecretVault.php index 9969aff..1788b52 100644 --- a/app/Services/Secrets/SecretVault.php +++ b/app/Services/Secrets/SecretVault.php @@ -169,6 +169,7 @@ final class SecretVault return match (true) { $this->row($key, $mode) !== null => 'stored', + $this->isStrict($key) => 'none', filled(config(self::REGISTRY[$key]['config'])) => 'environment', default => 'none', }; diff --git a/tests/Feature/Admin/SecretVaultTest.php b/tests/Feature/Admin/SecretVaultTest.php index c93ef25..90e0bbf 100644 --- a/tests/Feature/Admin/SecretVaultTest.php +++ b/tests/Feature/Admin/SecretVaultTest.php @@ -29,17 +29,37 @@ beforeEach(function () { it('prefers a stored key over the environment, and says which is in force', function () { $vault = app(SecretVault::class); - expect($vault->get('stripe.secret'))->toBe('sk_env_fallback') - ->and($vault->source('stripe.secret'))->toBe('environment'); + // This test uses dns.token (not stripe.secret) to preserve the original + // assertion: a non-strict entry falls back to the environment and reports + // it as its source. Stripe is exempt from fallback (it is strict), + // tested separately in StripeStrictModeTest. + config()->set('provisioning.dns.token', 'env_token_fallback'); - $vault->put('stripe.secret', 'sk_live_stored_value', Operator::factory()->create()); + expect($vault->get('dns.token'))->toBe('env_token_fallback') + ->and($vault->source('dns.token'))->toBe('environment'); - expect($vault->get('stripe.secret'))->toBe('sk_live_stored_value') - ->and($vault->source('stripe.secret'))->toBe('stored'); + $vault->put('dns.token', 'stored_value', Operator::factory()->create()); + + expect($vault->get('dns.token'))->toBe('stored_value') + ->and($vault->source('dns.token'))->toBe('stored'); // Removing it falls back rather than leaving nothing. - $vault->forget('stripe.secret'); - expect($vault->get('stripe.secret'))->toBe('sk_env_fallback'); + $vault->forget('dns.token'); + expect($vault->get('dns.token'))->toBe('env_token_fallback'); +}); + +it('strips the environment from stripe when nothing is stored', function () { + // Stripe is a strict entry: it never falls back to the environment, and + // source() never reports it as in force when it is not stored. This + // prevents the silent contradiction: get() returns null while source() + // claims 'environment', and the console would tell the operator a key is + // in force while the billing system refuses to use it. + $vault = app(SecretVault::class); + + config()->set('services.stripe.secret', 'sk_env_from_config'); + + expect($vault->get('stripe.secret'))->toBeNull() + ->and($vault->source('stripe.secret'))->toBe('none'); }); it('stores the value encrypted, and never in the clear', function () { diff --git a/tests/Feature/StripeStrictModeTest.php b/tests/Feature/StripeStrictModeTest.php index 157b8ad..3f1d0c3 100644 --- a/tests/Feature/StripeStrictModeTest.php +++ b/tests/Feature/StripeStrictModeTest.php @@ -54,3 +54,16 @@ it('lets every other entry keep falling back', function () { expect($this->vault->get('dns.token'))->toBe('live-token'); }); + +it('does not report the environment as the source of a strict key', function () { + // source() muss dieselbe Regel anwenden wie get(): ist der Eintrag strikt + // und der Platz des aktiven Modus leer, ist die Quelle 'none', nicht + // 'environment'. Sonst meldet die Konsole dem Betreiber einen Stripe- + // Schlüssel als „in Kraft" (source sagt 'environment'), während die + // Kasse ihn nicht benutzt (get gibt null zurück). Dieser stille Widerspruch + // ist genau die Art, gegen die dieses Vorhaben gebaut ist. + config()->set('services.stripe.secret', 'sk_live_from_env'); + OperatingMode::set(OperatingMode::Test); + + expect($this->vault->source('stripe.secret'))->toBe('none'); +});