'database']); } /** Insert a raw session row for $user; returns the generated id. */ private function seedSession(User $user, string $id, ?string $ip = '203.0.113.1', ?string $ua = 'Mozilla/5.0 Chrome', int $ago = 0): string { DB::table('sessions')->insert([ 'id' => $id, 'user_id' => $user->id, 'ip_address' => $ip, 'user_agent' => $ua, 'payload' => base64_encode(serialize([])), 'last_activity' => now()->subSeconds($ago)->getTimestamp(), ]); return $id; } public function test_for_user_returns_rows_with_current_flagged(): void { $user = User::factory()->create(); $other = User::factory()->create(); $currentId = Session::getId(); $this->seedSession($user, $currentId, ago: 0); $this->seedSession($user, 'sess-old', ago: 600); $this->seedSession($other, 'sess-other'); // must NOT appear $rows = app(SessionService::class)->forUser($user); $this->assertCount(2, $rows); // Newest activity first. $this->assertSame($currentId, $rows[0]['id']); $this->assertTrue($rows[0]['isCurrent']); $this->assertSame('sess-old', $rows[1]['id']); $this->assertFalse($rows[1]['isCurrent']); $this->assertSame('203.0.113.1', $rows[0]['ip']); $this->assertStringContainsString('Chrome', $rows[0]['userAgentShort']); $this->assertTrue($rows[0]['lastActivity']->isToday()); } public function test_logout_other_devices_deletes_others_keeps_current(): void { $user = User::factory()->create(); $currentId = Session::getId(); $this->seedSession($user, $currentId); $this->seedSession($user, 'sess-b'); $this->seedSession($user, 'sess-c'); app(SessionService::class)->logoutOtherDevices($user); $this->assertDatabaseHas('sessions', ['id' => $currentId]); $this->assertDatabaseMissing('sessions', ['id' => 'sess-b']); $this->assertDatabaseMissing('sessions', ['id' => 'sess-c']); } public function test_logout_user_everywhere_deletes_all_and_rotates_token(): void { $user = User::factory()->create(['remember_token' => 'original-token']); $this->seedSession($user, Session::getId()); $this->seedSession($user, 'sess-b'); app(SessionService::class)->logoutUserEverywhere($user); $this->assertSame(0, DB::table('sessions')->where('user_id', $user->id)->count()); $this->assertNotSame('original-token', $user->fresh()->remember_token); $this->assertNotNull($user->fresh()->remember_token); } public function test_logout_everyone_truncates_and_rotates_all_tokens(): void { $a = User::factory()->create(['remember_token' => 'token-a']); $b = User::factory()->create(['remember_token' => 'token-b']); $this->seedSession($a, 'sess-a'); $this->seedSession($b, 'sess-b'); app(SessionService::class)->logoutEveryone(); $this->assertSame(0, DB::table('sessions')->count()); $this->assertNotSame('token-a', $a->fresh()->remember_token); $this->assertNotSame('token-b', $b->fresh()->remember_token); } public function test_component_lists_the_users_sessions(): void { $user = User::factory()->create(); $this->seedSession($user, Session::getId()); Livewire::actingAs($user)->test(Sessions::class) ->assertOk() ->assertViewHas('sessions', fn ($sessions) => count($sessions) === 1); } public function test_logout_others_action_confirms_then_deletes(): void { $user = User::factory()->create(); $current = Session::getId(); $this->seedSession($user, $current); $this->seedSession($user, 'sess-other'); // The button opens the R5 confirm modal carrying the apply event + audit descriptor. Livewire::actingAs($user)->test(Sessions::class) ->call('confirmLogoutOthers') ->assertDispatched('openModal'); // The confirm modal re-dispatches the apply event; simulate that. Livewire::actingAs($user)->test(Sessions::class) ->call('logoutOthers'); $this->assertDatabaseHas('sessions', ['id' => $current]); $this->assertDatabaseMissing('sessions', ['id' => 'sess-other']); } public function test_logout_all_action_truncates_and_redirects_to_login(): void { $user = User::factory()->create(['remember_token' => 'tok']); $this->seedSession($user, Session::getId()); Livewire::actingAs($user)->test(Sessions::class) ->call('confirmLogoutAll') ->assertDispatched('openModal'); Livewire::actingAs($user)->test(Sessions::class) ->call('logoutAll') ->assertRedirect(route('login')); $this->assertSame(0, DB::table('sessions')->count()); $this->assertNotSame('tok', $user->fresh()->remember_token); } public function test_confirm_modal_writes_audit_row_on_confirm(): void { // The generic confirm modal persists the audit row from the descriptor the // Sessions component passes (mirrors Settings\Security). Exercise it directly. $user = User::factory()->create(); Livewire::actingAs($user)->test(ConfirmAction::class, [ 'auditAction' => 'session.logout_others', 'auditTarget' => $user->email, 'event' => 'sessionsLogoutOthers', ])->call('confirm'); $this->assertDatabaseHas('audit_events', [ 'action' => 'session.logout_others', 'target' => $user->email, ]); $this->assertSame(1, AuditEvent::where('action', 'session.logout_others')->count()); } }