import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; /** * Unit tests for the sessions API module. * * Tests getSessions(), revokeSession(), and revokeAllSessions() which all * rely on authenticatedFetch from $lib/auth and getApiBaseUrl from $lib/api. */ // Mock $lib/api vi.mock('$lib/api', () => ({ getApiBaseUrl: () => 'http://test.classeo.local:18000/api' })); // Mock $lib/auth const mockAuthenticatedFetch = vi.fn(); vi.mock('$lib/auth', () => ({ authenticatedFetch: (...args: unknown[]) => mockAuthenticatedFetch(...args) })); import { getSessions, revokeSession, revokeAllSessions } from '$lib/features/sessions/api/sessions'; describe('sessions API', () => { beforeEach(() => { vi.clearAllMocks(); }); afterEach(() => { vi.restoreAllMocks(); }); // ========================================================================== // getSessions // ========================================================================== describe('getSessions', () => { it('should return session array on success', async () => { const mockSessions = [ { family_id: 'family-1', device: 'Desktop', browser: 'Chrome', os: 'Linux', location: 'Paris, France', created_at: '2025-01-15T10:00:00Z', last_activity_at: '2025-01-15T12:00:00Z', is_current: true }, { family_id: 'family-2', device: 'Mobile', browser: 'Safari', os: 'iOS', location: 'Lyon, France', created_at: '2025-01-14T08:00:00Z', last_activity_at: '2025-01-14T09:00:00Z', is_current: false } ]; mockAuthenticatedFetch.mockResolvedValueOnce({ ok: true, json: () => Promise.resolve({ sessions: mockSessions }) }); const result = await getSessions(); expect(mockAuthenticatedFetch).toHaveBeenCalledWith( 'http://test.classeo.local:18000/api/me/sessions' ); expect(result).toHaveLength(2); expect(result[0]!.family_id).toBe('family-1'); expect(result[0]!.is_current).toBe(true); expect(result[1]!.family_id).toBe('family-2'); }); it('should throw Error when the API response is not ok', async () => { mockAuthenticatedFetch.mockResolvedValueOnce({ ok: false, status: 500 }); await expect(getSessions()).rejects.toThrow('Failed to fetch sessions'); }); }); // ========================================================================== // revokeSession // ========================================================================== describe('revokeSession', () => { it('should complete without error on success', async () => { mockAuthenticatedFetch.mockResolvedValueOnce({ ok: true, status: 204 }); await expect(revokeSession('family-abc')).resolves.toBeUndefined(); expect(mockAuthenticatedFetch).toHaveBeenCalledWith( 'http://test.classeo.local:18000/api/me/sessions/family-abc', expect.objectContaining({ method: 'DELETE' }) ); }); it('should throw "Cannot revoke current session" on 403', async () => { mockAuthenticatedFetch.mockResolvedValueOnce({ ok: false, status: 403 }); await expect(revokeSession('current-family')).rejects.toThrow( 'Cannot revoke current session' ); }); it('should throw generic error on other failure status', async () => { mockAuthenticatedFetch.mockResolvedValueOnce({ ok: false, status: 500 }); await expect(revokeSession('family-xyz')).rejects.toThrow( 'Failed to revoke session' ); }); }); // ========================================================================== // revokeAllSessions // ========================================================================== describe('revokeAllSessions', () => { it('should return revoked_count on success', async () => { mockAuthenticatedFetch.mockResolvedValueOnce({ ok: true, json: () => Promise.resolve({ message: '3 sessions revoked', revoked_count: 3 }) }); const result = await revokeAllSessions(); expect(mockAuthenticatedFetch).toHaveBeenCalledWith( 'http://test.classeo.local:18000/api/me/sessions', expect.objectContaining({ method: 'DELETE' }) ); expect(result).toBe(3); }); it('should throw Error when the API response is not ok', async () => { mockAuthenticatedFetch.mockResolvedValueOnce({ ok: false, status: 500 }); await expect(revokeAllSessions()).rejects.toThrow('Failed to revoke all sessions'); }); }); });