278 lines
11 KiB
PHP
278 lines
11 KiB
PHP
<?php
|
|
|
|
use App\Livewire\Admin\ConfirmForgetSecret;
|
|
use App\Livewire\Admin\ConfirmSaveSecret;
|
|
use App\Livewire\Admin\Integrations;
|
|
use App\Services\Secrets\SecretVault;
|
|
use App\Support\ProvisioningSettings;
|
|
use Livewire\Livewire;
|
|
|
|
/**
|
|
* Admin\Integrations merges the former admin.secrets and admin.infrastructure
|
|
* pages into one, grouped by what a field configures rather than by which of
|
|
* the two storage mechanisms holds it. This file replaces SecretsPageTest and
|
|
* InfrastructureSettingsTest — the underlying mechanisms (SecretVault,
|
|
* App\Support\Settings) are unchanged, only where they are displayed, so most
|
|
* of their coverage is ported here unchanged in substance.
|
|
*
|
|
* New in this file: the two capabilities are independently checked (an
|
|
* operator with only one of them sees only the matching half), and the
|
|
* redirects off the two retired routes. The raw .env editor gets its own
|
|
* coverage once it lands on this page.
|
|
*/
|
|
beforeEach(function () {
|
|
config()->set('admin_access.secrets_key', 'base64:'.base64_encode(random_bytes(32)));
|
|
});
|
|
|
|
// ---- Reachability: either capability opens the page, neither 403s it. ----
|
|
|
|
it('is not reachable with neither hosts.manage nor secrets.manage', function () {
|
|
Livewire::actingAs(operator('Support'), 'operator')
|
|
->test(Integrations::class)
|
|
->assertForbidden();
|
|
});
|
|
|
|
it('shows the settings sections but not the vault sections to an operator with only hosts.manage', function () {
|
|
Livewire::actingAs(operator('Admin'), 'operator')
|
|
->test(Integrations::class)
|
|
->assertOk()
|
|
->assertViewHas('canInfra', true)
|
|
->assertViewHas('canSecrets', false)
|
|
->assertSee(__('integrations.dns_zone'))
|
|
->assertDontSee(__('secrets.item.stripe_secret'));
|
|
});
|
|
|
|
it('shows the vault sections but not the settings sections to an operator with only secrets.manage', function () {
|
|
$operator = operator('Support');
|
|
$operator->givePermissionTo('secrets.manage');
|
|
|
|
Livewire::actingAs($operator, 'operator')
|
|
->test(Integrations::class)
|
|
->assertOk()
|
|
->assertViewHas('canInfra', false)
|
|
->assertViewHas('canSecrets', true)
|
|
->assertDontSee(__('integrations.dns_zone'))
|
|
->assertSee(__('secrets.item.stripe_secret'));
|
|
});
|
|
|
|
it('shows both halves to the Owner, who has both capabilities', function () {
|
|
Livewire::actingAs(operator('Owner'), 'operator')
|
|
->test(Integrations::class)
|
|
->assertOk()
|
|
->assertSee(__('integrations.dns_zone'))
|
|
->assertSee(__('secrets.item.stripe_secret'));
|
|
});
|
|
|
|
// ---- The retired pages redirect rather than 404 a bookmark. ----
|
|
|
|
it('redirects the retired /admin/secrets route to /admin/integrations', function () {
|
|
$this->actingAs(operator('Owner'), 'operator')
|
|
->get(route('admin.secrets'))
|
|
->assertRedirect(route('admin.integrations'))
|
|
->assertStatus(301);
|
|
});
|
|
|
|
it('redirects the retired /admin/infrastructure route to /admin/integrations', function () {
|
|
$this->actingAs(operator('Owner'), 'operator')
|
|
->get(route('admin.infrastructure'))
|
|
->assertRedirect(route('admin.integrations'))
|
|
->assertStatus(301);
|
|
});
|
|
|
|
// ---- Vault entries: ported from the former SecretsPageTest. ----
|
|
|
|
it('shows the no-key banner for a malformed SECRETS_KEY, not only a missing one', function () {
|
|
config()->set('admin_access.secrets_key', 'zu-kurz');
|
|
|
|
Livewire::actingAs(operator('Owner'), 'operator')
|
|
->test(Integrations::class)
|
|
->assertSee(__('secrets.no_key'));
|
|
});
|
|
|
|
it('shows nothing until the password is confirmed', function () {
|
|
$owner = operator('Owner');
|
|
app(SecretVault::class)->put('stripe.secret', 'sk_live_ABCDEFGH9999', $owner);
|
|
|
|
Livewire::actingAs($owner, 'operator')->test(Integrations::class)
|
|
->assertSee(__('secrets.locked_title'))
|
|
->assertDontSee('9999')
|
|
->assertViewHas('unlocked', false);
|
|
});
|
|
|
|
it('refuses every vault action while locked, not merely hiding the buttons', function () {
|
|
$owner = operator('Owner');
|
|
|
|
foreach ([['save', 'stripe.secret'], ['forget', 'stripe.secret'], ['test', 'stripe.secret']] as [$action, $arg]) {
|
|
Livewire::actingAs($owner, 'operator')
|
|
->test(Integrations::class)
|
|
->call($action, $arg)
|
|
->assertForbidden();
|
|
}
|
|
});
|
|
|
|
it('does not let an operator with only hosts.manage touch a vault action', function () {
|
|
Livewire::actingAs(operator('Admin'), 'operator')
|
|
->test(Integrations::class)
|
|
->call('save', 'stripe.secret')
|
|
->assertForbidden();
|
|
});
|
|
|
|
it('unlocks with the password, then stores and clears the entered value', function () {
|
|
$owner = operator('Owner');
|
|
|
|
$page = Livewire::actingAs($owner, 'operator')
|
|
->test(Integrations::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')
|
|
->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(Integrations::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(Integrations::class)
|
|
->set('confirmablePassword', 'password')
|
|
->call('confirmPassword')
|
|
->assertViewHas('unlocked', true)
|
|
->call('forgetPasswordConfirmation')
|
|
->assertViewHas('unlocked', false);
|
|
});
|
|
|
|
it('does not open the save/forget confirmation without the capability', function () {
|
|
Livewire::actingAs(operator('Admin'), 'operator')
|
|
->test(ConfirmSaveSecret::class, ['key' => 'stripe.secret'])
|
|
->assertForbidden();
|
|
|
|
Livewire::actingAs(operator('Admin'), 'operator')
|
|
->test(ConfirmForgetSecret::class, ['key' => 'stripe.secret'])
|
|
->assertForbidden();
|
|
});
|
|
|
|
it('confirms a save through the modal without storing anything itself', function () {
|
|
$owner = operator('Owner');
|
|
|
|
Livewire::actingAs($owner, 'operator')
|
|
->test(ConfirmSaveSecret::class, ['key' => 'stripe.secret'])
|
|
->assertSee(__('secrets.item.stripe_secret'))
|
|
->call('confirm')
|
|
->assertDispatched('secret-save-confirmed', key: 'stripe.secret');
|
|
|
|
expect(app(SecretVault::class)->has('stripe.secret'))->toBeFalse();
|
|
});
|
|
|
|
it('saves the typed value once the page receives the confirmed event', function () {
|
|
$owner = operator('Owner');
|
|
|
|
Livewire::actingAs($owner, 'operator')
|
|
->test(Integrations::class)
|
|
->set('confirmablePassword', 'password')
|
|
->call('confirmPassword')
|
|
->set('entered.stripe_secret', 'sk_live_VIACONFIRM1')
|
|
->call('onSaveConfirmed', 'stripe.secret')
|
|
->assertHasNoErrors();
|
|
|
|
expect(app(SecretVault::class)->get('stripe.secret'))->toBe('sk_live_VIACONFIRM1');
|
|
});
|
|
|
|
it('forgets the stored value once the page receives the confirmed event', function () {
|
|
$owner = operator('Owner');
|
|
app(SecretVault::class)->put('stripe.secret', 'sk_live_TOFORGET999', $owner);
|
|
|
|
Livewire::actingAs($owner, 'operator')
|
|
->test(Integrations::class)
|
|
->set('confirmablePassword', 'password')
|
|
->call('confirmPassword')
|
|
->call('onForgetConfirmed', 'stripe.secret');
|
|
|
|
expect(app(SecretVault::class)->has('stripe.secret'))->toBeFalse();
|
|
});
|
|
|
|
it('stores the SSH identity with its internal newlines intact', function () {
|
|
$owner = operator('Owner');
|
|
$pem = "-----BEGIN OPENSSH PRIVATE KEY-----\nabcdef\nghijkl\n-----END OPENSSH PRIVATE KEY-----";
|
|
|
|
Livewire::actingAs($owner, 'operator')
|
|
->test(Integrations::class)
|
|
->set('confirmablePassword', 'password')
|
|
->call('confirmPassword')
|
|
->set('entered.ssh_private_key', $pem)
|
|
->call('save', 'ssh.private_key')
|
|
->assertHasNoErrors();
|
|
|
|
expect(app(SecretVault::class)->get('ssh.private_key'))->toBe($pem);
|
|
});
|
|
|
|
it('binds the form to a key Livewire can actually write to', function () {
|
|
expect(Integrations::field('stripe.secret'))->toBe('stripe_secret')
|
|
->and(str_contains(Integrations::field('stripe.secret'), '.'))->toBeFalse();
|
|
});
|
|
|
|
// ---- Plain settings: ported from the former InfrastructureSettingsTest. ----
|
|
|
|
it('loads the .env-derived value when nothing has been saved from the console yet', function () {
|
|
config()->set('provisioning.dns.zone', 'env-zone.example');
|
|
|
|
Livewire::actingAs(operator('Owner'), 'operator')
|
|
->test(Integrations::class)
|
|
->assertSet('dnsZone', 'env-zone.example');
|
|
});
|
|
|
|
it('saves every settings field, and the consumer reads the saved value back', function () {
|
|
Livewire::actingAs(operator('Owner'), 'operator')
|
|
->test(Integrations::class)
|
|
->set('dnsZone', 'console-zone.example')
|
|
->set('wgEndpoint', 'vpn.example:51820')
|
|
->set('wgHubPubkey', 'hubpubkey==')
|
|
->set('traefikDynamicPath', '/etc/traefik/dynamic')
|
|
->set('sshPublicKey', 'ssh-ed25519 AAAAC3 clupilot')
|
|
->set('monitoringUrl', 'http://kuma-bridge:8080')
|
|
->call('saveInfra')
|
|
->assertHasNoErrors();
|
|
|
|
expect(ProvisioningSettings::dnsZone())->toBe('console-zone.example')
|
|
->and(ProvisioningSettings::wgEndpoint())->toBe('vpn.example:51820')
|
|
->and(ProvisioningSettings::wgHubPublicKey())->toBe('hubpubkey==')
|
|
->and(ProvisioningSettings::traefikDynamicPath())->toBe('/etc/traefik/dynamic')
|
|
->and(ProvisioningSettings::sshPublicKey())->toBe('ssh-ed25519 AAAAC3 clupilot')
|
|
->and(ProvisioningSettings::monitoringUrl())->toBe('http://kuma-bridge:8080');
|
|
});
|
|
|
|
it('refuses a monitoring URL that is not a URL', function () {
|
|
Livewire::actingAs(operator('Owner'), 'operator')
|
|
->test(Integrations::class)
|
|
->set('monitoringUrl', 'not a url')
|
|
->call('saveInfra')
|
|
->assertHasErrors('monitoringUrl');
|
|
});
|
|
|
|
it('does not let an operator with only secrets.manage save the settings half', function () {
|
|
$operator = operator('Support');
|
|
$operator->givePermissionTo('secrets.manage');
|
|
|
|
Livewire::actingAs($operator, 'operator')
|
|
->test(Integrations::class)
|
|
->call('saveInfra')
|
|
->assertForbidden();
|
|
});
|