CluPilotCloud/tests/Feature/Admin/InstanceAdminAccessTest.php

117 lines
4.1 KiB
PHP

<?php
use App\Livewire\Admin\InstanceAdminAccess;
use App\Models\Instance;
use App\Provisioning\Jobs\IssueInstanceAdminAccess;
use App\Services\Proxmox\FakeProxmoxClient;
use App\Services\Proxmox\ProxmoxClient;
use App\Services\Wireguard\ConfigHandoff;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Queue;
function adminAccessInstance(): Instance
{
return Instance::factory()->create([
'host_id' => App\Models\Host::factory()->active()->create()->id,
'vmid' => 1042,
'nc_admin_ref' => 'admin',
'subdomain' => 'kanzlei-berger',
'status' => 'active',
]);
}
it('is offered only to operators who may take over an installation', function () {
$instance = adminAccessInstance();
foreach (['Support', 'Billing', 'Read-only', 'Developer'] as $role) {
Livewire\Livewire::actingAs(operator($role))
->test(InstanceAdminAccess::class, ['uuid' => $instance->uuid])
->assertForbidden();
}
Livewire\Livewire::actingAs(operator('Owner'))
->test(InstanceAdminAccess::class, ['uuid' => $instance->uuid])
->assertOk();
});
it('asks for the operator password before touching the instance', function () {
Queue::fake();
$instance = adminAccessInstance();
$owner = operator('Owner');
$owner->forceFill(['password' => Hash::make('geheim-1234')])->save();
$modal = Livewire\Livewire::actingAs($owner)
->test(InstanceAdminAccess::class, ['uuid' => $instance->uuid])
->set('password', 'falsch')
->call('request')
->assertHasErrors('password');
Queue::assertNothingPushed();
$modal->set('password', 'geheim-1234')->call('request')->assertHasNoErrors();
Queue::assertPushed(IssueInstanceAdminAccess::class);
});
it('resets our managed admin account and hands the credentials over once', function () {
$pve = new FakeProxmoxClient;
app()->instance(ProxmoxClient::class, $pve);
$instance = adminAccessInstance();
$token = 'handoff-token-for-the-test';
(new IssueInstanceAdminAccess($instance->uuid, $token, 1))->handle($pve);
$payload = json_decode(ConfigHandoff::get($token), true);
expect($payload['username'])->toBe('admin')
->and($payload['password'])->toHaveLength(24)
->and($payload['url'])->toContain('kanzlei-berger');
// The password is handed over, never written to the instance record.
expect($instance->fresh()->getAttributes())->not->toContain($payload['password']);
});
it('reports a silent instance instead of pretending it worked', function () {
$pve = new class extends FakeProxmoxClient
{
public function guestExec(string $node, int $vmid, string $command): array
{
return ['exitcode' => 1, 'out-data' => 'connection refused'];
}
};
app()->instance(ProxmoxClient::class, $pve);
$instance = adminAccessInstance();
$token = 'handoff-failure';
(new IssueInstanceAdminAccess($instance->uuid, $token, 1))->handle($pve);
expect(json_decode(ConfigHandoff::get($token), true))->toBe(['error' => 'failed']);
});
it('refuses an instance that is no longer live', function () {
$pve = new FakeProxmoxClient;
app()->instance(ProxmoxClient::class, $pve);
$instance = adminAccessInstance();
$instance->update(['status' => 'terminated']);
(new IssueInstanceAdminAccess($instance->uuid, 'handoff-dead', 1))->handle($pve);
// Its VMID may belong to someone else by now.
expect(json_decode(ConfigHandoff::get('handoff-dead'), true))->toBe(['error' => 'unavailable']);
});
it('tells the console when Proxmox is unreachable', function () {
$pve = new class extends FakeProxmoxClient
{
public function guestExec(string $node, int $vmid, string $command): array
{
throw new RuntimeException('no route to host');
}
};
app()->instance(ProxmoxClient::class, $pve);
$instance = adminAccessInstance();
(new IssueInstanceAdminAccess($instance->uuid, 'handoff-throw', 1))->handle($pve);
// Otherwise the modal polls forever with a spinner.
expect(json_decode(ConfigHandoff::get('handoff-throw'), true))->toBe(['error' => 'failed']);
});