import { describe, expect, it, vi } from 'vitest'; import { createAngularApiClient, type AngularHttpClientLike } from '../src/api/angular-client'; describe('createAngularApiClient', () => { it('reads health and session detail using Django-compatible endpoints', async () => { const get = vi.fn(async (url: string) => { if (url === '/healthz') { return { ok: true, service: 'partyhub' } as T; } if (url === '/lobby/sessions/ABCD12') { return { session: { code: 'ABCD12', status: 'lobby', host_id: 1, current_round: 1, players_count: 2 }, players: [ { id: 2, nickname: 'Maja', score: 0, is_connected: true }, { id: 3, nickname: 'Bo', score: 0, is_connected: false } ], round_question: null, phase_view_model: { status: 'lobby', round_number: 1, players_count: 2, constraints: { min_players_to_start: 2, max_players_mvp: 8, min_players_reached: true, max_players_allowed: true }, host: { can_start_round: true, can_show_question: false, can_mix_answers: false, can_calculate_scores: false, can_reveal_scoreboard: false, can_start_next_round: false, can_finish_game: false }, player: { can_join: true, can_submit_lie: false, can_submit_guess: false, can_view_final_result: false } } } as T; } throw { status: 404, error: { error: 'Not found' } }; }); const post = vi.fn(async (url: string, body: unknown) => { if (url === '/lobby/sessions/join') { expect(body).toEqual({ code: 'ABCD12', nickname: 'Maja' }); return { player: { id: 9, nickname: 'Maja', session_token: 'token-1', score: 0 }, session: { code: 'ABCD12', status: 'lobby' } } as T; } if (url === '/lobby/sessions/ABCD12/rounds/start') { expect(body).toEqual({ category_slug: 'history' }); return { session: { code: 'ABCD12', status: 'lie', current_round: 1 }, round: { number: 1, category: { slug: 'history', name: 'History' } } } as T; } throw { status: 404, error: { error: 'Not found' } }; }); const http = { get, post }; const client = createAngularApiClient(http as AngularHttpClientLike); const health = await client.health(); expect(health.ok).toBe(true); if (health.ok) { expect(health.data.ok).toBe(true); expect(health.data.service).toBe('partyhub'); } const session = await client.getSession(' abcd12 '); expect(session.ok).toBe(true); if (session.ok) { expect(session.data.session.code).toBe('ABCD12'); expect(session.data.session.host_id).toBe(1); expect(session.data.phase_view_model.host.can_start_round).toBe(true); } const join = await client.joinSession({ code: ' abcd12 ', nickname: ' Maja ' }); expect(join.ok).toBe(true); const start = await client.startRound(' abcd12 ', { category_slug: 'history' }); expect(start.ok).toBe(true); expect(get).toHaveBeenNthCalledWith(1, '/healthz', { withCredentials: true }); expect(get).toHaveBeenNthCalledWith(2, '/lobby/sessions/ABCD12', { withCredentials: true }); expect(post).toHaveBeenNthCalledWith( 1, '/lobby/sessions/join', { code: 'ABCD12', nickname: 'Maja' }, { withCredentials: true } ); expect(post).toHaveBeenNthCalledWith( 2, '/lobby/sessions/ABCD12/rounds/start', { category_slug: 'history' }, { withCredentials: true } ); }); it('normalizes baseUrl with trailing slash to keep Django endpoint paths canonical', async () => { const get = vi.fn(async (url: string) => { if (url === '/api/healthz') { return { ok: true, service: 'partyhub' } as T; } if (url === '/api/lobby/sessions/ABCD12') { return { session: { code: 'ABCD12', status: 'lobby', host_id: 1, current_round: 1, players_count: 2 }, players: [], round_question: null, phase_view_model: { status: 'lobby', round_number: 1, players_count: 2, constraints: { min_players_to_start: 2, max_players_mvp: 8, min_players_reached: true, max_players_allowed: true }, host: { can_start_round: true, can_show_question: false, can_mix_answers: false, can_calculate_scores: false, can_reveal_scoreboard: false, can_start_next_round: false, can_finish_game: false }, player: { can_join: true, can_submit_lie: false, can_submit_guess: false, can_view_final_result: false } } } as T; } throw { status: 404, error: { error: 'Not found' } }; }); const post = vi.fn(async (url: string) => { if (url === '/api/lobby/sessions/join') { return { player: { id: 9, nickname: 'Maja', session_token: 'token-1', score: 0 }, session: { code: 'ABCD12', status: 'lobby' } } as T; } if (url === '/api/lobby/sessions/ABCD12/rounds/start') { return { session: { code: 'ABCD12', status: 'lie', current_round: 1 }, round: { number: 1, category: { slug: 'history', name: 'History' } } } as T; } throw { status: 404, error: { error: 'Not found' } }; }); const client = createAngularApiClient({ get, post } as AngularHttpClientLike, '/api/'); await client.health(); await client.getSession('abcd12'); await client.joinSession({ code: 'abcd12', nickname: 'Maja' }); await client.startRound('abcd12', { category_slug: 'history' }); expect(get).toHaveBeenNthCalledWith(1, '/api/healthz', { withCredentials: true }); expect(get).toHaveBeenNthCalledWith(2, '/api/lobby/sessions/ABCD12', { withCredentials: true }); expect(post).toHaveBeenNthCalledWith( 1, '/api/lobby/sessions/join', { code: 'ABCD12', nickname: 'Maja' }, { withCredentials: true } ); expect(post).toHaveBeenNthCalledWith( 2, '/api/lobby/sessions/ABCD12/rounds/start', { category_slug: 'history' }, { withCredentials: true } ); }); it('maps HttpErrorResponse-style failures to ApiResult errors', async () => { const http = { get: vi.fn(async () => { throw { status: 503, message: 'Service unavailable', error: { error: 'maintenance' } }; }), post: vi.fn(async () => { throw { status: 403, message: 'Forbidden', error: { error: 'Only host can start round' } }; }) }; const client = createAngularApiClient(http as AngularHttpClientLike); const health = await client.health(); expect(health.ok).toBe(false); if (!health.ok) { expect(health.status).toBe(503); expect(health.error.kind).toBe('http'); expect(health.error.payload).toEqual({ error: 'maintenance' }); expect(health.error.message).toContain('Service unavailable'); } const start = await client.startRound('ABCD12', { category_slug: 'history' }); expect(start.ok).toBe(false); if (!start.ok) { expect(start.status).toBe(403); expect(start.error.kind).toBe('http'); expect(start.error.payload).toEqual({ error: 'Only host can start round' }); } }); });