158 lines
6.4 KiB
PHP
158 lines
6.4 KiB
PHP
<?php
|
|
|
|
use App\Models\PlanPrice;
|
|
use App\Services\Secrets\SecretCipher;
|
|
use App\Support\OperatingMode;
|
|
use App\Support\Settings;
|
|
use App\Support\StripeCatalogueMode;
|
|
use Illuminate\Support\Facades\DB;
|
|
|
|
/**
|
|
* Die Migration rät nicht.
|
|
*
|
|
* Der Stripe-Schlüssel sagt selbst, wohin er gehört: `sk_test_` ist ein
|
|
* Testschlüssel, unabhängig davon, in welchem Feld er gerade liegt. Ihn stumpf
|
|
* als „Live" einzusortieren, würde eine Installation, die erkennbar im
|
|
* Testbetrieb läuft, beim ersten Kauf echtes Geld abbuchen lassen.
|
|
*/
|
|
function runSlotMigration(): void
|
|
{
|
|
(require base_path('database/migrations/2026_08_01_090000_give_every_secret_a_test_slot.php'))->up();
|
|
}
|
|
|
|
function runSlotMigrationDown(): void
|
|
{
|
|
(require base_path('database/migrations/2026_08_01_090000_give_every_secret_a_test_slot.php'))->down();
|
|
}
|
|
|
|
it('files a stripe test key in the test slot and switches the mode', function () {
|
|
DB::table('app_secrets')->insert([
|
|
'key' => 'stripe.secret',
|
|
// SecretVault::put() always encrypts with SecretCipher (its own
|
|
// SECRETS_KEY), never with the app's default APP_KEY-based encrypt()
|
|
// helper — so a row built with the latter is not a value this
|
|
// migration would ever actually meet, only one that looks unreadable.
|
|
'value' => app(SecretCipher::class)->encrypt('sk_test_abc'),
|
|
'created_at' => now(), 'updated_at' => now(),
|
|
]);
|
|
|
|
runSlotMigration();
|
|
|
|
expect(DB::table('app_secrets')->where('key', 'stripe.secret:test')->exists())->toBeTrue();
|
|
expect(DB::table('app_secrets')->where('key', 'stripe.secret:live')->exists())->toBeFalse();
|
|
expect(Settings::get(OperatingMode::SETTING))->toBe('test');
|
|
});
|
|
|
|
it('files a stripe live key in the live slot and leaves the mode alone', function () {
|
|
DB::table('app_secrets')->insert([
|
|
'key' => 'stripe.secret',
|
|
'value' => app(SecretCipher::class)->encrypt('sk_live_abc'),
|
|
'created_at' => now(), 'updated_at' => now(),
|
|
]);
|
|
|
|
runSlotMigration();
|
|
|
|
expect(DB::table('app_secrets')->where('key', 'stripe.secret:live')->exists())->toBeTrue();
|
|
expect(OperatingMode::current())->toBe(OperatingMode::Live);
|
|
});
|
|
|
|
/**
|
|
* Der Katalog, der schon bei Stripe liegt, bekommt seine Herkunft mit.
|
|
*
|
|
* Ein Stripe-Produkt und ein Stripe-Preis gehören dem Konto, das sie
|
|
* ausgestellt hat. Auf dieser Installation kann das nur das Konto sein, das
|
|
* der eine hinterlegte Schlüssel öffnet — ein zweiter war nie da. Ohne diesen
|
|
* Nachtrag stünde ein längst abgeglichener Katalog nach der Migration als
|
|
* „Herkunft unbekannt" da, und die Bereitschaftsseite verlangte einen neuen
|
|
* Abgleich, den niemand braucht.
|
|
*/
|
|
it('files the already synced catalogue under the account that key opens', function () {
|
|
DB::table('app_secrets')->insert([
|
|
'key' => 'stripe.secret',
|
|
'value' => app(SecretCipher::class)->encrypt('sk_test_abc'),
|
|
'created_at' => now(), 'updated_at' => now(),
|
|
]);
|
|
PlanPrice::query()->limit(1)->update(['stripe_price_id' => 'price_minted_with_that_key']);
|
|
|
|
runSlotMigration();
|
|
|
|
expect(StripeCatalogueMode::recorded())->toBe(OperatingMode::Test);
|
|
});
|
|
|
|
it('claims no catalogue where none was ever synced', function () {
|
|
DB::table('app_secrets')->insert([
|
|
'key' => 'stripe.secret',
|
|
'value' => app(SecretCipher::class)->encrypt('sk_test_abc'),
|
|
'created_at' => now(), 'updated_at' => now(),
|
|
]);
|
|
|
|
runSlotMigration();
|
|
|
|
expect(StripeCatalogueMode::recorded())->toBeNull();
|
|
});
|
|
|
|
it('files every other credential in the live slot', function () {
|
|
DB::table('app_secrets')->insert([
|
|
'key' => 'dns.token',
|
|
'value' => app(SecretCipher::class)->encrypt('whatever'),
|
|
'created_at' => now(), 'updated_at' => now(),
|
|
]);
|
|
|
|
runSlotMigration();
|
|
|
|
expect(DB::table('app_secrets')->where('key', 'dns.token:live')->exists())->toBeTrue();
|
|
expect(DB::table('app_secrets')->where('key', 'dns.token')->exists())->toBeFalse();
|
|
});
|
|
|
|
it('leaves an installation with no stripe key on live', function () {
|
|
runSlotMigration();
|
|
|
|
expect(OperatingMode::current())->toBe(OperatingMode::Live);
|
|
});
|
|
|
|
it('files an unreadable stripe value in the live slot instead of aborting', function () {
|
|
// Fix round (Codex review): the earlier version of this file happened to
|
|
// exercise this branch by accident — its fixture encrypted with the wrong
|
|
// key, so every "test key" assertion secretly ran through this catch
|
|
// instead. Fixing that fixture silently deleted the only coverage of a
|
|
// branch that decides a payment key's mode. This row is deliberately
|
|
// unreadable on purpose, not by mistake.
|
|
DB::table('app_secrets')->insert([
|
|
'key' => 'stripe.secret',
|
|
'value' => 'not-a-valid-payload-at-all',
|
|
'created_at' => now(), 'updated_at' => now(),
|
|
]);
|
|
|
|
runSlotMigration();
|
|
|
|
expect(DB::table('app_secrets')->where('key', 'stripe.secret:live')->exists())->toBeTrue();
|
|
expect(DB::table('app_secrets')->where('key', 'stripe.secret:test')->exists())->toBeFalse();
|
|
expect(OperatingMode::current())->toBe(OperatingMode::Live);
|
|
});
|
|
|
|
it('keeps the live slot on rollback when both slots are occupied, not whichever the query happens to return first', function () {
|
|
// Fix round (Codex review): down() picked whichever of the two rows a
|
|
// plain, unordered get() returned first to rename back to the bare key,
|
|
// and deleted the other — so which credential survived a rollback
|
|
// depended on row order, not on a decision. Inserting the TEST row first
|
|
// (the lower id) reproduces the old bug: an unordered scan tends to
|
|
// return rows in id order, so the test slot would have claimed the bare
|
|
// key and the live one would have been the one discarded.
|
|
DB::table('app_secrets')->insert([
|
|
'key' => 'stripe.secret:test',
|
|
'value' => 'test-slot-value',
|
|
'created_at' => now(), 'updated_at' => now(),
|
|
]);
|
|
DB::table('app_secrets')->insert([
|
|
'key' => 'stripe.secret:live',
|
|
'value' => 'live-slot-value',
|
|
'created_at' => now(), 'updated_at' => now(),
|
|
]);
|
|
|
|
runSlotMigrationDown();
|
|
|
|
expect(DB::table('app_secrets')->where('key', 'stripe.secret')->value('value'))->toBe('live-slot-value');
|
|
expect(DB::table('app_secrets')->where('key', 'stripe.secret:test')->exists())->toBeFalse();
|
|
expect(DB::table('app_secrets')->where('key', 'stripe.secret:live')->exists())->toBeFalse();
|
|
});
|