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]); } // ── manage-panel: LoginProtection (mount + save + unban + unbanAll + whitelistMyIp) ── public function test_operator_cannot_mount_login_protection(): void { Livewire::actingAs($this->operator()) ->test(LoginProtection::class) ->assertForbidden(); } public function test_viewer_cannot_mount_login_protection(): void { Livewire::actingAs($this->viewer()) ->test(LoginProtection::class) ->assertForbidden(); } public function test_admin_can_mount_login_protection(): void { Livewire::actingAs($this->admin()) ->test(LoginProtection::class) ->assertOk(); } public function test_operator_cannot_save_login_protection(): void { // Boot as admin so the snapshot is valid, then swap to an operator and call the mutation — // the save() method-level guard must still fire 403 (models a demotion / crafted update). $component = Livewire::actingAs($this->admin())->test(LoginProtection::class)->assertOk(); $this->actingAs($this->operator()); $component->set('maxretry', 5)->call('save')->assertForbidden(); } public function test_viewer_cannot_call_whitelist_my_ip(): void { $component = Livewire::actingAs($this->admin())->test(LoginProtection::class)->assertOk(); $this->actingAs($this->viewer()); $component->call('whitelistMyIp')->assertForbidden(); } public function test_operator_cannot_unban(): void { BannedIp::create(['ip' => '203.0.113.5', 'banned_until' => now()->addHour(), 'attempts' => 10]); $component = Livewire::actingAs($this->admin())->test(LoginProtection::class)->assertOk(); $this->actingAs($this->operator()); $component->call('unban', 'any-token')->assertForbidden(); $this->assertDatabaseCount('banned_ips', 1); // guard fired before the ban was touched } public function test_viewer_cannot_unban_all(): void { BannedIp::create(['ip' => '203.0.113.5', 'banned_until' => now()->addHour(), 'attempts' => 10]); $component = Livewire::actingAs($this->admin())->test(LoginProtection::class)->assertOk(); $this->actingAs($this->viewer()); $component->call('unbanAll', 'any-token')->assertForbidden(); $this->assertDatabaseCount('banned_ips', 1); } public function test_admin_can_save_login_protection(): void { Livewire::actingAs($this->admin()) ->test(LoginProtection::class) ->set('maxretry', 7) ->call('save') ->assertHasNoErrors(); // allowed through the guard } // ── manage-panel: Email (mount + save) ────────────────────────────────────── public function test_operator_cannot_mount_email(): void { Livewire::actingAs($this->operator()) ->test(Email::class) ->assertForbidden(); } public function test_viewer_cannot_mount_email(): void { Livewire::actingAs($this->viewer()) ->test(Email::class) ->assertForbidden(); } public function test_operator_cannot_save_email(): void { $component = Livewire::actingAs($this->admin())->test(Email::class)->assertOk(); $this->actingAs($this->operator()); $component ->set('mail_host', 'smtp.example.com') ->set('mail_from_address', 'noreply@example.com') ->set('mail_from_name', 'Clusev') ->call('save') ->assertForbidden(); } public function test_admin_can_save_email(): void { Livewire::actingAs($this->admin()) ->test(Email::class) ->set('mail_host', 'smtp.example.com') ->set('mail_from_address', 'noreply@example.com') ->set('mail_from_name', 'Clusev') ->call('save') ->assertHasNoErrors(); } // ── manage-users: Sessions::logoutAll (destructive GLOBAL logout) ───────────── public function test_operator_cannot_logout_all(): void { $component = Livewire::actingAs($this->admin())->test(Sessions::class)->assertOk(); $this->actingAs($this->operator()); $component->call('logoutAll', 'any-token')->assertForbidden(); } public function test_viewer_cannot_logout_all(): void { $component = Livewire::actingAs($this->admin())->test(Sessions::class)->assertOk(); $this->actingAs($this->viewer()); $component->call('logoutAll', 'any-token')->assertForbidden(); } public function test_admin_can_logout_all(): void { $admin = $this->admin(); // The global logout truncates every session — mock it so the test session survives. $sessions = Mockery::mock(SessionService::class)->makePartial(); $sessions->shouldReceive('logoutEveryone')->once(); $sessions->shouldReceive('forUser')->andReturn([]); $this->app->instance(SessionService::class, $sessions); $this->actingAs($admin); $token = ConfirmToken::issue('sessionsLogoutAll', [], 'session.logout_all', $admin->email); ConfirmToken::confirm($token); Livewire::actingAs($admin) ->test(Sessions::class) ->call('logoutAll', $token) ->assertRedirect(route('login')); // allowed through the guard → redirect to login } public function test_non_admin_self_session_revoke_is_not_forbidden(): void { // A non-admin managing their OWN sessions (per-session self-revoke) is NOT gated. Opening the // "sign out other devices" confirm must not 403 for an operator. Livewire::actingAs($this->operator()) ->test(Sessions::class) ->call('confirmLogoutOthers') ->assertOk(); } // ── manage-panel: Release\Index (flag ON, but role still enforced) ─────────── public function test_operator_cannot_mount_release_even_with_flag_on(): void { config()->set('clusev.release_controls', true); Livewire::actingAs($this->operator()) ->test(ReleaseIndex::class) ->assertForbidden(); } public function test_viewer_cannot_mount_release_even_with_flag_on(): void { config()->set('clusev.release_controls', true); Livewire::actingAs($this->viewer()) ->test(ReleaseIndex::class) ->assertForbidden(); } public function test_admin_can_mount_release_with_flag_on(): void { config()->set('clusev.release_controls', true); Livewire::actingAs($this->admin()) ->test(ReleaseIndex::class) ->assertOk(); } }