139 lines
5.0 KiB
PHP
139 lines
5.0 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']);
|
|
});
|
|
|
|
it('shows the password once and not a second time', function () {
|
|
$pve = new FakeProxmoxClient;
|
|
app()->instance(ProxmoxClient::class, $pve);
|
|
$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', 'geheim-1234')
|
|
->call('request'); // sync queue: the job runs immediately
|
|
|
|
$token = $modal->get('token');
|
|
$modal->assertSee(__('instances.admin_ready'));
|
|
|
|
// "Shown once" has to mean it: a replayed request must not fetch it again.
|
|
expect(ConfigHandoff::get($token ?? 'none'))->toBeNull()
|
|
->and($modal->get('token'))->toBeNull();
|
|
|
|
$modal->call('$refresh')->assertDontSee(__('instances.admin_ready'));
|
|
});
|