CluPilotCloud/tests/Feature/Admin/SecretsPageTest.php

119 lines
4.6 KiB
PHP

<?php
use App\Livewire\Admin\Secrets;
use App\Livewire\Admin\Secrets as SecretsPage;
use App\Services\Secrets\SecretVault;
use Livewire\Livewire;
/**
* The credentials page has two gates, and the second one is the point.
*
* The capability decides who may open it. The password decides whether THIS
* session may see or change anything — because the realistic threat is not a
* stranger but an unlocked machine, and a session is exactly what that gives
* away.
*/
beforeEach(function () {
config()->set('admin_access.secrets_key', 'base64:'.base64_encode(random_bytes(32)));
});
it('shows the no-key banner for a malformed SECRETS_KEY, not only a missing one', function () {
// Codex R15#3, P2: render() feeds 'usable' => $vault->isUsable() to the
// view, and the banner is the one place an operator actually sees that
// verdict. Before the fix, a malformed (nonempty, wrong-length) key made
// isUsable() answer true, so this banner stayed hidden — the page said
// credentials could be stored here when they could not. Asserted before
// the lock gate specifically because @if (! $usable) sits above @if (!
// $unlocked) in the view; a signed-in-but-locked session must still see
// it.
config()->set('admin_access.secrets_key', 'zu-kurz');
Livewire::actingAs(operator('Owner'), 'operator')
->test(SecretsPage::class)
->assertSee(__('secrets.no_key'));
});
it('is not reachable without the capability', function () {
// Every operator has console.view. That must not mean "can read the
// payment key".
Livewire::actingAs(operator('Admin'), 'operator')
->test(SecretsPage::class)
->assertForbidden();
});
it('shows nothing until the password is confirmed', function () {
$owner = operator('Owner');
app(SecretVault::class)->put('stripe.secret', 'sk_live_ABCDEFGH9999', $owner);
$page = Livewire::actingAs($owner, 'operator')->test(SecretsPage::class);
$page->assertSee(__('secrets.locked_title'))
// Not even the outline before unlocking.
->assertDontSee('9999')
->assertViewHas('unlocked', false);
});
it('refuses every action while locked, not merely hiding the buttons', function () {
// A Livewire action is reachable by anyone who can post to /livewire/update.
$owner = operator('Owner');
foreach ([['save', 'stripe.secret'], ['forget', 'stripe.secret'], ['test', 'stripe.secret']] as [$action, $arg]) {
Livewire::actingAs($owner, 'operator')
->test(SecretsPage::class)
->call($action, $arg)
->assertForbidden();
}
});
it('unlocks with the password, then stores and clears the entered value', function () {
$owner = operator('Owner');
$page = Livewire::actingAs($owner, 'operator')
->test(SecretsPage::class)
->set('confirmablePassword', 'password')
->call('confirmPassword')
->assertHasNoErrors()
->assertViewHas('unlocked', true);
$page->set('entered.stripe_secret', 'sk_live_NEWKEY1234')
->call('save', 'stripe.secret')
->assertHasNoErrors();
expect(app(SecretVault::class)->get('stripe.secret'))->toBe('sk_live_NEWKEY1234')
// Out of the component state as soon as it is stored: a Livewire
// property travels to the browser and back in the snapshot.
->and($page->get('entered')['stripe_secret'])->toBe('');
});
it('never puts a stored secret into the component snapshot', function () {
$owner = operator('Owner');
app(SecretVault::class)->put('stripe.secret', 'sk_live_TOPSECRET42', $owner);
$page = Livewire::actingAs($owner, 'operator')
->test(SecretsPage::class)
->set('confirmablePassword', 'password')
->call('confirmPassword');
expect(json_encode($page->snapshot))->not->toContain('sk_live_TOPSECRET42');
});
it('can be locked again without signing out', function () {
$owner = operator('Owner');
Livewire::actingAs($owner, 'operator')
->test(SecretsPage::class)
->set('confirmablePassword', 'password')
->call('confirmPassword')
->assertViewHas('unlocked', true)
->call('forgetPasswordConfirmation')
->assertViewHas('unlocked', false);
});
it('binds the form to a key Livewire can actually write to', function () {
// A dot in a Livewire property path means nesting, so a registry key with a
// dot in it would be written to entered['stripe']['secret'] and the value
// would never reach the save.
expect(Secrets::field('stripe.secret'))->toBe('stripe_secret')
->and(str_contains(Secrets::field('stripe.secret'), '.'))->toBeFalse();
});