get(route('admin.hosts.create'))->assertRedirect('/login'); $this->actingAs(User::factory()->create(['is_admin' => false]))->get(route('admin.hosts.create'))->assertForbidden(); $this->actingAs(admin(), 'operator')->get(route('admin.hosts.create'))->assertOk()->assertSee(__('hosts.create_title')); }); it('gates the host detail page', function () { $host = Host::factory()->create(); $this->get(route('admin.hosts.show', $host))->assertRedirect('/login'); $this->actingAs(User::factory()->create(['is_admin' => false]))->get(route('admin.hosts.show', $host))->assertForbidden(); $this->actingAs(admin(), 'operator')->get(route('admin.hosts.show', $host))->assertOk()->assertSee($host->name); }); it('lists real hosts for admins', function () { Host::factory()->active()->create(['name' => 'pve-zzz-1']); $this->actingAs(admin(), 'operator')->get(route('admin.hosts'))->assertOk()->assertSee('pve-zzz-1'); }); it('creates a host and starts onboarding with an encrypted password', function () { Queue::fake(); \App\Models\Datacenter::query()->firstOrCreate(['code' => 'fsn'], ['name' => 'Falkenstein']); Livewire::actingAs(admin(), 'operator') ->test(HostCreate::class) ->set('name', 'pve-fsn-7') ->set('datacenter', 'fsn') ->set('public_ip', '203.0.113.7') ->set('root_password', 'supersecret') ->call('save') ->assertRedirect(); $host = Host::query()->where('name', 'pve-fsn-7')->first(); expect($host)->not->toBeNull()->and($host->status)->toBe('pending'); $run = ProvisioningRun::query()->where('subject_id', $host->id)->first(); expect($run)->not->toBeNull() ->and($run->pipeline)->toBe('host') ->and(Crypt::decryptString($run->context('root_password')))->toBe('supersecret'); Queue::assertPushed(AdvanceRunJob::class); }); it('rejects a duplicate public ip', function () { \App\Models\Datacenter::query()->firstOrCreate(['code' => 'fsn'], ['name' => 'Falkenstein']); Host::factory()->create(['public_ip' => '203.0.113.99']); Livewire::actingAs(admin(), 'operator') ->test(HostCreate::class) ->set('name', 'pve-dup') ->set('datacenter', 'fsn') ->set('public_ip', '203.0.113.99') ->set('root_password', 'supersecret') ->call('save') ->assertHasErrors(['public_ip']); }); it('validates the add-host form', function () { Livewire::actingAs(admin(), 'operator') ->test(HostCreate::class) ->set('name', '') ->set('public_ip', 'not-an-ip') ->set('root_password', 'short') ->call('save') ->assertHasErrors(['name', 'public_ip', 'root_password']); }); it('retries a failed run from the detail page', function () { Queue::fake(); $host = Host::factory()->create(['status' => 'error']); $run = ProvisioningRun::factory()->forHost($host)->create([ 'status' => 'failed', 'current_step' => 3, 'error' => 'boom', 'started_at' => now()->subHour(), ]); Livewire::actingAs(admin(), 'operator')->test(HostDetail::class, ['host' => $host])->call('retry'); $run->refresh(); expect($run->status)->toBe('running') ->and($run->error)->toBeNull() ->and($run->started_at->greaterThan(now()->subMinute()))->toBeTrue() ->and($host->fresh()->status)->toBe('onboarding'); Queue::assertPushed(AdvanceRunJob::class); }); it('deactivates the host and queues an async purge', function () { Queue::fake(); $host = Host::factory()->active()->create(['wg_pubkey' => 'HOSTPUB=']); ProvisioningRun::factory()->forHost($host)->create(); Livewire::actingAs(admin(), 'operator') ->test(ConfirmRemoveHost::class, ['uuid' => $host->uuid]) ->call('remove') ->assertRedirect(route('admin.hosts')); expect($host->fresh()->status)->toBe('disabled'); Queue::assertPushed(PurgeHost::class, fn ($job) => $job->uuid === $host->uuid); }); it('purges a host: deletes its runs and record and queues peer removal', function () { Queue::fake([RemoveWireguardPeer::class]); $host = Host::factory()->active()->create(['wg_pubkey' => 'HOSTPUB=']); ProvisioningRun::factory()->forHost($host)->create(); (new PurgeHost($host->uuid))->handle(); expect(Host::query()->find($host->id))->toBeNull(); Queue::assertPushed(RemoveWireguardPeer::class, fn ($job) => $job->publicKey === 'HOSTPUB='); }); it('renders the live stepper for a running host', function () { $host = Host::factory()->create(['status' => 'onboarding']); ProvisioningRun::factory()->forHost($host)->create(['status' => 'running', 'current_step' => 2]); $this->actingAs(admin(), 'operator')->get(route('admin.hosts.show', $host)) ->assertOk() ->assertSee(__('hosts.step.prepare_base_system')) ->assertSee(__('hosts.progress')); }); it('filters the host list by search and datacenter', function () { \App\Models\Datacenter::query()->firstOrCreate(['code' => 'fsn'], ['name' => 'Falkenstein']); \App\Models\Datacenter::query()->firstOrCreate(['code' => 'hel'], ['name' => 'Helsinki']); Host::factory()->create(['name' => 'pve-fsn-9', 'datacenter' => 'fsn', 'public_ip' => '203.0.113.9']); Host::factory()->create(['name' => 'pve-hel-9', 'datacenter' => 'hel', 'public_ip' => '203.0.113.19']); Livewire::actingAs(admin(), 'operator')->test(Hosts::class) ->set('search', 'fsn-9') ->assertSee('pve-fsn-9') ->assertDontSee('pve-hel-9') ->set('search', '') ->set('datacenter', 'hel') ->assertSee('pve-hel-9') ->assertDontSee('pve-fsn-9'); }); it('edits host reserve and toggles maintenance', function () { $host = Host::factory()->active()->create(['reserve_pct' => 15]); Livewire::actingAs(admin(), 'operator')->test(HostDetail::class, ['host' => $host]) ->call('saveReserve', 25) ->call('toggleMaintenance'); $host->refresh(); expect($host->reserve_pct)->toBe(25)->and($host->status)->toBe('disabled'); Livewire::actingAs(admin(), 'operator')->test(HostDetail::class, ['host' => $host])->call('toggleMaintenance'); expect($host->fresh()->status)->toBe('active'); }); it('clamps host reserve to a sane range', function () { $host = Host::factory()->active()->create(['reserve_pct' => 15]); Livewire::actingAs(admin(), 'operator')->test(HostDetail::class, ['host' => $host])->call('saveReserve', 500); expect($host->fresh()->reserve_pct)->toBe(90); }); it('reports host heartbeat health from last_seen_at', function () { $online = Host::factory()->create(['last_seen_at' => now()->subMinutes(2)]); $stale = Host::factory()->create(['last_seen_at' => now()->subMinutes(20)]); $offline = Host::factory()->create(['last_seen_at' => now()->subHours(3)]); $never = Host::factory()->create(['last_seen_at' => null]); expect($online->healthState())->toBe('online') ->and($stale->healthState())->toBe('stale') ->and($offline->healthState())->toBe('offline') ->and($never->healthState())->toBe('offline'); });