161 lines
5.9 KiB
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"
|
|
);
|
|
}
|
|
}
|
|
}
|