actingAs(User::factory()->create()); $server = Server::create(['name' => 'box', 'ip' => '10.0.0.9', 'ssh_port' => 22, 'status' => 'online']); // status() must NOT be called during mount — it is deferred to load(). $svc = Mockery::mock(Fail2banService::class); $svc->shouldReceive('status')->never(); app()->instance(Fail2banService::class, $svc); Livewire::test(Fail2banBans::class, ['serverId' => $server->id]) ->assertSet('loaded', false) ->assertSet('jails', []); } /** load() (fired by wire:init) reads the live status and renders the jail/IP list. */ public function test_load_reads_status_and_renders_the_jail_list(): void { $this->actingAs(User::factory()->create()); $server = Server::create(['name' => 'box', 'ip' => '10.0.0.9', 'ssh_port' => 22, 'status' => 'online']); $svc = Mockery::mock(Fail2banService::class); $svc->shouldReceive('status')->once()->with(Mockery::on(fn ($s) => $s->id === $server->id)) ->andReturn([ 'jails' => [ ['name' => 'sshd', 'currentlyBanned' => 1, 'currentlyFailed' => 3, 'bannedIps' => ['203.0.113.7']], ], ]); app()->instance(Fail2banService::class, $svc); Livewire::test(Fail2banBans::class, ['serverId' => $server->id]) ->call('load') ->assertSet('loaded', true) ->assertSee('sshd') ->assertSee('203.0.113.7'); } /** A read error from status() surfaces the translated error message. */ public function test_load_surfaces_a_read_error(): void { $this->actingAs(User::factory()->create()); $server = Server::create(['name' => 'box', 'ip' => '10.0.0.9', 'ssh_port' => 22, 'status' => 'online']); $svc = Mockery::mock(Fail2banService::class); $svc->shouldReceive('status')->once()->andReturn(['jails' => [], 'readError' => true]); app()->instance(Fail2banService::class, $svc); Livewire::test(Fail2banBans::class, ['serverId' => $server->id]) ->call('load') ->assertSet('loaded', true) ->assertSet('error', __('servers.fail2ban_read_error')); } /** unban() calls the service, audits the success, refreshes the list, and signals the page. */ public function test_unban_calls_service_audits_refreshes_and_dispatches_change(): void { $this->actingAs(User::factory()->create()); $server = Server::create(['name' => 'box', 'ip' => '10.0.0.9', 'ssh_port' => 22, 'status' => 'online']); $svc = Mockery::mock(Fail2banService::class); // Initial load: one banned IP. $svc->shouldReceive('status')->once()->andReturn([ 'jails' => [ ['name' => 'sshd', 'currentlyBanned' => 1, 'currentlyFailed' => 3, 'bannedIps' => ['203.0.113.7']], ], ]); $svc->shouldReceive('unban')->once() ->with(Mockery::on(fn ($s) => $s->id === $server->id), 'sshd', '203.0.113.7') ->andReturn(['ok' => true, 'output' => 'unbanned']); // Refresh after unban: the IP is gone. $svc->shouldReceive('status')->once()->andReturn([ 'jails' => [ ['name' => 'sshd', 'currentlyBanned' => 0, 'currentlyFailed' => 3, 'bannedIps' => []], ], ]); app()->instance(Fail2banService::class, $svc); Livewire::test(Fail2banBans::class, ['serverId' => $server->id]) ->call('load') ->assertSee('203.0.113.7') ->call('unbanIp', 'sshd', '203.0.113.7') ->assertDispatched('fail2banChanged') ->assertDispatched('notify') ->assertDontSee('203.0.113.7'); $this->assertDatabaseHas('audit_events', [ 'server_id' => $server->id, 'action' => 'fail2ban.unban', ]); } /** A failed unban surfaces a notice and does NOT audit or signal the page. */ public function test_unban_failure_notifies_without_auditing(): void { $this->actingAs(User::factory()->create()); $server = Server::create(['name' => 'box', 'ip' => '10.0.0.9', 'ssh_port' => 22, 'status' => 'online']); $svc = Mockery::mock(Fail2banService::class); $svc->shouldReceive('status')->once()->andReturn([ 'jails' => [ ['name' => 'sshd', 'currentlyBanned' => 1, 'currentlyFailed' => 3, 'bannedIps' => ['203.0.113.7']], ], ]); $svc->shouldReceive('unban')->once()->andReturn(['ok' => false, 'output' => 'no permission']); app()->instance(Fail2banService::class, $svc); Livewire::test(Fail2banBans::class, ['serverId' => $server->id]) ->call('load') ->call('unbanIp', 'sshd', '203.0.113.7') ->assertDispatched('notify') ->assertNotDispatched('fail2banChanged'); $this->assertSame(0, AuditEvent::where('action', 'fail2ban.unban')->count()); } }