create(['must_change_password' => false]); } private function operator(): User { return User::factory()->operator()->create(['must_change_password' => false]); } private function server(): Server { $server = Server::create(['name' => 'box', 'ip' => '10.0.0.2', 'ssh_port' => 22, 'status' => 'online']); $server->credential()->create(['username' => 'root', 'auth_type' => 'password', 'secret' => 'x']); return $server; } private function hostCred(): HostCredential { return HostCredential::create([ 'host' => 'host.docker.internal', 'port' => 22, 'username' => 'root', 'auth_type' => 'password', 'secret' => 'x', ]); } private function stubDocker(array $containers = []): DockerService { $docker = Mockery::mock(DockerService::class); $docker->shouldReceive('available')->andReturn(true); $docker->shouldReceive('containers')->andReturn($containers); app()->instance(DockerService::class, $docker); return $docker; } public function test_admin_sees_the_host_containers(): void { $this->actingAs($this->admin()); $this->hostCred(); $this->stubDocker([['id' => 'h1', 'name' => 'clusev-app-1', 'image' => 'clusev-app:prod', 'state' => 'running', 'status' => 'Up', 'ports' => '']]); Livewire::test(Index::class) ->call('load') ->assertOk() ->assertSet('connected', true) ->assertSee('clusev-app-1'); } public function test_non_admin_is_refused_the_host_docker_route(): void { $this->actingAs($this->operator()); $this->get(route('docker'))->assertForbidden(); } public function test_host_not_configured_state_when_no_login_exists(): void { $this->actingAs($this->admin()); $this->stubDocker(); Livewire::test(Index::class) ->call('load') ->assertOk() ->assertSet('hostNotConfigured', true) ->assertSet('connected', false) ->assertSee(__('docker.host_unconfigured_title')); } public function test_shows_a_clean_not_installed_state_when_the_host_has_no_docker(): void { $this->actingAs($this->admin()); $this->hostCred(); $docker = Mockery::mock(DockerService::class); $docker->shouldReceive('available')->once()->andReturn(false); $docker->shouldReceive('containers')->never(); app()->instance(DockerService::class, $docker); Livewire::test(Index::class) ->call('load') ->assertOk() ->assertSet('notInstalled', true) ->assertSet('error', null) ->assertSee(__('docker.not_installed_title')) ->assertDontSee('not found'); } public function test_admin_can_action_a_host_container_and_it_is_audited(): void { $this->actingAs($this->admin()); $this->hostCred(); $docker = $this->stubDocker(); $docker->shouldReceive('containerAction')->once() ->with(Mockery::type(Server::class), 'clusev-redis-1', 'restart') ->andReturn(['ok' => true, 'output' => '']); Livewire::test(Index::class) ->call('action', 'clusev-redis-1', 'restart') ->assertOk(); // Host actions carry a null server_id (transient host server); the target label names it. $this->assertDatabaseHas('audit_events', ['action' => 'docker.action', 'server_id' => null]); } // ── ContainerLogs modal (shared by the host page + the server Docker tab) ── public function test_logs_modal_loads_a_server_container_tail(): void { $this->actingAs($this->operator()); $server = $this->server(); $docker = Mockery::mock(DockerService::class); $docker->shouldReceive('logs')->once()->with(Mockery::type(Server::class), 'web', 200)->andReturn('log line 1'); app()->instance(DockerService::class, $docker); Livewire::test(ContainerLogs::class, ['serverId' => $server->id, 'ref' => 'web']) ->call('load') ->assertOk() ->assertSet('logs', 'log line 1'); } public function test_logs_modal_loads_a_host_container_tail_for_admins(): void { $this->actingAs($this->admin()); $this->hostCred(); $docker = Mockery::mock(DockerService::class); $docker->shouldReceive('logs')->once()->with(Mockery::type(Server::class), 'clusev-app-1', 200)->andReturn('host log line'); app()->instance(DockerService::class, $docker); Livewire::test(ContainerLogs::class, ['serverId' => 0, 'ref' => 'clusev-app-1']) ->call('load') ->assertOk() ->assertSet('logs', 'host log line'); } public function test_operator_cannot_open_host_logs(): void { // The host-logs sentinel (serverId 0) is admin-only; an operator is refused at the read. $this->actingAs($this->operator()); $this->hostCred(); $docker = Mockery::mock(DockerService::class); $docker->shouldReceive('logs')->never(); app()->instance(DockerService::class, $docker); Livewire::test(ContainerLogs::class, ['serverId' => 0, 'ref' => 'clusev-app-1']) ->call('load') ->assertForbidden(); } public function test_viewer_cannot_read_server_logs_in_the_modal(): void { $this->actingAs(User::factory()->viewer()->create(['must_change_password' => false])); $server = $this->server(); $docker = Mockery::mock(DockerService::class); $docker->shouldReceive('logs')->never(); // guard fires before the read app()->instance(DockerService::class, $docker); Livewire::test(ContainerLogs::class, ['serverId' => $server->id, 'ref' => 'web']) ->call('load') ->assertForbidden(); } }