create(['must_change_password' => false]); } private function operator(): User { return User::factory()->operator()->create(['must_change_password' => false]); } private function viewer(): User { return User::factory()->viewer()->create(['must_change_password' => false]); } /** An online server with a credential, selected as the active fleet server. */ private function activeServer(): 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']); session(['active_server_id' => $server->id]); return $server; } /** A confirmed, single-use token for the given apply handler + server. */ private function confirmed(string $event, array $params, int $serverId): string { $token = ConfirmToken::issue($event, $params, '', null, $serverId); ConfirmToken::confirm($token); // the modal confirmation step return $token; } // ── operate: Services\Index::applyService (systemctl restart) ──────────────── public function test_viewer_cannot_apply_a_service_action(): void { $this->actingAs($this->viewer()); $server = $this->activeServer(); // The privileged SSH call must never be reached — the guard fires first. $fleet = Mockery::mock(FleetService::class); $fleet->shouldReceive('serviceAction')->never(); app()->instance(FleetService::class, $fleet); $token = $this->confirmed('serviceConfirmed', ['op' => 'restart', 'name' => 'nginx.service'], $server->id); Livewire::test(Services::class) ->call('applyService', $token) ->assertForbidden(); } public function test_operator_can_apply_a_service_action(): void { $this->actingAs($this->operator()); $server = $this->activeServer(); $fleet = Mockery::mock(FleetService::class); $fleet->shouldReceive('serviceAction')->once() ->with(Mockery::type(Server::class), 'restart', 'nginx.service') ->andReturn(['ok' => true, 'output' => '']); $fleet->shouldReceive('systemd')->andReturn(['services' => []]); // reload after success app()->instance(FleetService::class, $fleet); $token = $this->confirmed('serviceConfirmed', ['op' => 'restart', 'name' => 'nginx.service'], $server->id); Livewire::test(Services::class) ->call('applyService', $token) ->assertOk(); // allowed through the guard, SSH mocked away } public function test_admin_can_apply_a_service_action(): void { $this->actingAs($this->admin()); $server = $this->activeServer(); $fleet = Mockery::mock(FleetService::class); $fleet->shouldReceive('serviceAction')->once() ->with(Mockery::type(Server::class), 'restart', 'nginx.service') ->andReturn(['ok' => true, 'output' => '']); $fleet->shouldReceive('systemd')->andReturn(['services' => []]); app()->instance(FleetService::class, $fleet); $token = $this->confirmed('serviceConfirmed', ['op' => 'restart', 'name' => 'nginx.service'], $server->id); Livewire::test(Services::class) ->call('applyService', $token) ->assertOk(); } // ── operate: Files\Index::deleteEntry (SFTP unlink) ───────────────────────── public function test_viewer_cannot_delete_a_file(): void { $this->actingAs($this->viewer()); $server = $this->activeServer(); $fleet = Mockery::mock(FleetService::class); $fleet->shouldReceive('files')->andReturn([]); // any dir listing during mount/load $fleet->shouldReceive('deleteFile')->never(); // guard fires before the SFTP unlink app()->instance(FleetService::class, $fleet); $token = $this->confirmed('fileConfirmed', ['path' => '/etc/passwd'], $server->id); Livewire::test(Files::class) ->call('deleteEntry', $token) ->assertForbidden(); } public function test_operator_can_delete_a_file(): void { $this->actingAs($this->operator()); $server = $this->activeServer(); $fleet = Mockery::mock(FleetService::class); $fleet->shouldReceive('files')->andReturn([]); $fleet->shouldReceive('deleteFile')->once() ->with(Mockery::type(Server::class), '/etc/passwd'); app()->instance(FleetService::class, $fleet); $token = $this->confirmed('fileConfirmed', ['path' => '/etc/passwd'], $server->id); Livewire::test(Files::class) ->call('deleteEntry', $token) ->assertOk(); // allowed through the guard, SFTP mocked away } public function test_admin_can_delete_a_file(): void { $this->actingAs($this->admin()); $server = $this->activeServer(); $fleet = Mockery::mock(FleetService::class); $fleet->shouldReceive('files')->andReturn([]); $fleet->shouldReceive('deleteFile')->once() ->with(Mockery::type(Server::class), '/etc/passwd'); app()->instance(FleetService::class, $fleet); $token = $this->confirmed('fileConfirmed', ['path' => '/etc/passwd'], $server->id); Livewire::test(Files::class) ->call('deleteEntry', $token) ->assertOk(); } // ── operate: Modals\FileEditor::save (SFTP write) ─────────────────────────── public function test_viewer_cannot_save_a_file_edit(): void { $this->actingAs($this->viewer()); $server = Server::create(['name' => 's', 'ip' => '10.0.0.9', 'ssh_port' => 22, 'status' => 'online']); $fleet = Mockery::mock(FleetService::class); $fleet->shouldReceive('writeFile')->never(); // guard fires before the remote write $this->app->instance(FleetService::class, $fleet); $this->app->instance(Sftp::class, Mockery::mock(Sftp::class)); // resolvable, unused for text Livewire::test(FileEditor::class, ['serverId' => $server->id, 'path' => '/etc/x.conf', 'name' => 'x.conf']) ->set('content', 'edited = wert') ->call('save') ->assertForbidden(); } public function test_operator_can_save_a_file_edit(): void { $this->actingAs($this->operator()); $server = Server::create(['name' => 's', 'ip' => '10.0.0.9', 'ssh_port' => 22, 'status' => 'online']); $fleet = Mockery::mock(FleetService::class); $fleet->shouldReceive('writeFile')->once() ->with(Mockery::type(Server::class), '/etc/x.conf', 'edited = wert'); $this->app->instance(FleetService::class, $fleet); $this->app->instance(Sftp::class, Mockery::mock(Sftp::class)); Livewire::test(FileEditor::class, ['serverId' => $server->id, 'path' => '/etc/x.conf', 'name' => 'x.conf']) ->set('content', 'edited = wert') ->call('save') ->assertOk(); // allowed through the guard, SFTP mocked away } public function test_admin_can_save_a_file_edit(): void { $this->actingAs($this->admin()); $server = Server::create(['name' => 's', 'ip' => '10.0.0.9', 'ssh_port' => 22, 'status' => 'online']); $fleet = Mockery::mock(FleetService::class); $fleet->shouldReceive('writeFile')->once() ->with(Mockery::type(Server::class), '/etc/x.conf', 'edited = wert'); $this->app->instance(FleetService::class, $fleet); $this->app->instance(Sftp::class, Mockery::mock(Sftp::class)); Livewire::test(FileEditor::class, ['serverId' => $server->id, 'path' => '/etc/x.conf', 'name' => 'x.conf']) ->set('content', 'edited = wert') ->call('save') ->assertOk(); } // ── read-only: a viewer keeps READ access (proves read-only, not locked-out) ─ public function test_viewer_can_load_the_services_list(): void { $this->actingAs($this->viewer()); $server = $this->activeServer(); $fleet = Mockery::mock(FleetService::class); $fleet->shouldReceive('systemd')->andReturn([ 'services' => [['name' => 'nginx.service', 'status' => 'online', 'sub' => 'running', 'enabled' => true, 'desc' => 'web']], 'journal' => [], 'cursor' => null, ]); app()->instance(FleetService::class, $fleet); Livewire::test(Services::class) ->assertOk() // page mounts for a viewer ->call('load') // the lazy read runs ->assertOk() ->assertSet('connected', true) ->assertSet('services.0.name', 'nginx.service'); // the list is visible } public function test_viewer_can_browse_the_file_manager(): void { $this->actingAs($this->viewer()); $this->activeServer(); $fleet = Mockery::mock(FleetService::class); $fleet->shouldReceive('files')->andReturn([ ['name' => 'etc', 'type' => 'dir', 'size' => null, 'perms' => 'drwxr-xr-x', 'owner' => 'root', 'modified' => 'now'], ]); app()->instance(FleetService::class, $fleet); Livewire::test(Files::class) ->assertOk() // page mounts for a viewer ->call('load') // the lazy directory read runs ->assertOk() ->assertSet('connected', true) ->assertSet('entries.0.name', 'etc'); // the listing is visible } }