import { describe, expect, test, vi, beforeEach } from 'vitest'; import { render, screen, fireEvent, waitFor } from '@testing-library/svelte'; import ActiveSessions from './ActiveSessions.svelte'; const listSessions = vi.fn(); const revokeSession = vi.fn(); const revokeOtherSessions = vi.fn(); vi.mock('#lib/api/me.js', () => ({ listSessions: (...a: unknown[]) => listSessions(...a), revokeSession: (...a: unknown[]) => revokeSession(...a), revokeOtherSessions: (...a: unknown[]) => revokeOtherSessions(...a) })); vi.mock('#lib/stores/toast.svelte.js', () => ({ pushToast: vi.fn() })); type Row = { id: string; user_agent: string; created_ip: string; last_ip: string; created_at: string; last_seen_at: string; current: boolean; }; function row(over: Partial = {}): Row { return { id: 'a1', user_agent: 'Mozilla/5.0 (X11; Linux x86_64) Chrome/120.0', created_ip: '203.0.113.1', last_ip: '203.0.113.1', created_at: new Date().toISOString(), last_seen_at: new Date().toISOString(), current: false, ...over }; } beforeEach(() => { vi.clearAllMocks(); }); describe('ActiveSessions', () => { test('flags the current session and gives it no sign-out button', async () => { listSessions.mockResolvedValue([ row({ id: 'cur', current: true }), row({ id: 'other', current: false }) ]); render(ActiveSessions); await screen.findByText('This device'); // One sign-out button, for the non-current row. Offering one on the // current session would sign the user out of the page they're using. await waitFor(() => { expect(screen.getAllByRole('button', { name: 'Sign out' })).toHaveLength(1); }); }); // The whole reason IP is stored: surfacing the mismatch rather than making // someone compare two addresses by eye. test('warns when a session is used from a different address than it was created', async () => { listSessions.mockResolvedValue([ row({ id: 'moved', created_ip: '203.0.113.1', last_ip: '198.51.100.9' }) ]); render(ActiveSessions); expect(await screen.findByText('Address changed')).toBeTruthy(); }); test('does not warn when the address has not changed', async () => { listSessions.mockResolvedValue([ row({ created_ip: '203.0.113.1', last_ip: '203.0.113.1' }) ]); render(ActiveSessions); await screen.findByText(/Signed in/); expect(screen.queryByText('Address changed')).toBeNull(); }); test('sign-out-all-others confirms before acting', async () => { listSessions.mockResolvedValue([ row({ id: 'cur', current: true }), row({ id: 'o1' }), row({ id: 'o2' }) ]); revokeOtherSessions.mockResolvedValue(2); render(ActiveSessions); const start = await screen.findByRole('button', { name: 'Sign out all other devices' }); await fireEvent.click(start); // First click only arms the action. expect(revokeOtherSessions).not.toHaveBeenCalled(); expect(screen.getByText(/Sign out 2 other devices\?/)).toBeTruthy(); await fireEvent.click(screen.getByRole('button', { name: 'Sign them out' })); await waitFor(() => expect(revokeOtherSessions).toHaveBeenCalledTimes(1)); }); test('offers no bulk action when there are no other devices', async () => { listSessions.mockResolvedValue([row({ id: 'cur', current: true })]); render(ActiveSessions); await screen.findByText('This device'); expect(screen.queryByRole('button', { name: 'Sign out all other devices' })).toBeNull(); }); test('surfaces a retry when loading fails', async () => { listSessions.mockRejectedValue(new Error('boom')); render(ActiveSessions); const retry = await screen.findByRole('button', { name: 'Try again' }); listSessions.mockResolvedValue([row({ id: 'cur', current: true })]); await fireEvent.click(retry); await screen.findByText('This device'); }); test('renders unknown for a missing address rather than an empty cell', async () => { listSessions.mockResolvedValue([row({ created_ip: '', last_ip: '' })]); render(ActiveSessions); await waitFor(() => { expect(screen.getAllByText('unknown').length).toBeGreaterThan(0); }); }); });