create(['must_change_password' => false]); } private function viewer(): User { return User::factory()->create(['must_change_password' => false, 'role' => 'viewer']); } private function server(): Server { return Server::create(['name' => 's1', 'ip' => '10.0.0.9', 'ssh_port' => 22, 'status' => 'online']); } /** @return array */ private function ufwState(bool $active): array { return [ 'tool' => 'ufw', 'supported' => true, 'installed' => true, 'active' => $active, 'manageable' => true, 'readOnly' => false, 'readError' => false, 'defaults' => ['incoming' => 'deny', 'outgoing' => 'allow'], 'rules' => [['num' => 1, 'to' => '22/tcp', 'action' => 'ALLOW', 'from' => 'Anywhere', 'v6' => false]], ]; } public function test_switcher_shows_empty_state_with_cta_for_admin(): void { Livewire::actingAs($this->admin()) ->test(ServerSwitcher::class) ->assertSee(__('servers.switcher_no_server')) ->assertSee(__('servers.switcher_empty_title')) ->assertSee(__('servers.switcher_empty_hint')) ->assertSee(__('servers.add_server')); } public function test_switcher_empty_state_hides_cta_for_viewer(): void { Livewire::actingAs($this->viewer()) ->test(ServerSwitcher::class) ->assertSee(__('servers.switcher_empty_title')) ->assertDontSee(__('servers.add_server')); } public function test_switcher_with_servers_has_no_empty_state(): void { $this->server(); Livewire::actingAs($this->admin()) ->test(ServerSwitcher::class) ->assertSee('s1') ->assertDontSee(__('servers.switcher_empty_title')); } public function test_inactive_ufw_shows_hint_and_hides_rules_and_add_rule(): void { Livewire::actingAs($this->admin()) ->test(Show::class, ['server' => $this->server()]) ->set('ready', true) ->set('firewall', $this->ufwState(active: false)) ->assertSee(__('servers.firewall_inactive_hint', ['tool' => 'UFW'])) // The add-rule button label is just "Regel" (substring of the panel title), so assert // on the trigger's unique modal component name instead of the visible label. ->assertDontSee('modals.firewall-rule', escape: false) ->assertDontSee('22/tcp'); } public function test_active_ufw_shows_rules_and_add_rule(): void { Livewire::actingAs($this->admin()) ->test(Show::class, ['server' => $this->server()]) ->set('ready', true) ->set('firewall', $this->ufwState(active: true)) ->assertSee('modals.firewall-rule', escape: false) ->assertSee('22/tcp') ->assertDontSee(__('servers.firewall_inactive_hint', ['tool' => 'UFW'])); } public function test_hardening_applied_reloads_security_in_place_without_skeleton(): void { $server = $this->server(); $profile = new OsProfile( family: 'debian', id: 'debian', prettyName: 'Debian 12', packageManager: 'apt', firewallTool: 'ufw', serviceManager: 'systemd', ); $detector = Mockery::mock(OsDetector::class); $detector->shouldReceive('detect')->andReturn($profile); $this->instance(OsDetector::class, $detector); $hardening = Mockery::mock(HardeningService::class); $hardening->shouldReceive('state')->andReturn([]); $this->instance(HardeningService::class, $hardening); $firewall = Mockery::mock(FirewallService::class); $firewall->shouldReceive('status')->andReturn($this->ufwState(active: true)); $this->instance(FirewallService::class, $firewall); $fail2ban = Mockery::mock(Fail2banService::class); $fail2ban->shouldReceive('status')->andReturn(['supported' => false]); $this->instance(Fail2banService::class, $fail2ban); Livewire::actingAs($this->admin()) ->test(Show::class, ['server' => $server]) ->set('ready', true) ->dispatch('hardeningApplied') ->assertSet('ready', true) ->assertSet('firewall.active', true); } public function test_root_lockout_guard_message_fits_the_modal_output_limit(): void { foreach (['de', 'en'] as $locale) { $msg = trans('backend.ssh_root_self_lockout', [], $locale); $this->assertLessThanOrEqual( 400, mb_strlen($msg), "backend.ssh_root_self_lockout ({$locale}) must fit the modal's 400-char failure box unclipped" ); } } }