clusev/tests/Feature/UiBatchFixesTest.php

161 lines
5.9 KiB
PHP

<?php
namespace Tests\Feature;
use App\Livewire\Servers\Show;
use App\Livewire\ServerSwitcher;
use App\Models\Server;
use App\Models\User;
use App\Services\Fail2banService;
use App\Services\FirewallService;
use App\Services\HardeningService;
use App\Support\Os\OsDetector;
use App\Support\Os\OsProfile;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Livewire\Livewire;
use Mockery;
use Tests\TestCase;
/**
* UI batch fixes from the fresh-VM test round:
* - server switcher shows an explanatory empty state (with a role-gated add-server CTA)
* instead of an empty dropdown when the fleet has no servers;
* - an INACTIVE firewall hides the misleading rules box + "Regel hinzufügen" and shows
* the tool-agnostic inactive hint instead (previously ufw fell through to the rules box);
* - `hardeningApplied` re-reads only the security state in place — the page must NOT
* flip back to the skeleton (ready stays true);
* - the hardening guard messages fit the modal's failure box unclipped (limit 400).
*/
class UiBatchFixesTest extends TestCase
{
use RefreshDatabase;
protected function tearDown(): void
{
Mockery::close();
parent::tearDown();
}
private function admin(): User
{
return User::factory()->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<string, mixed> */
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"
);
}
}
}