diff --git a/frontend/tests/api-client.integration.test.ts b/frontend/tests/api-client.integration.test.ts new file mode 100644 index 0000000..8da278c --- /dev/null +++ b/frontend/tests/api-client.integration.test.ts @@ -0,0 +1,87 @@ +import { afterAll, beforeAll, describe, expect, it } from 'vitest'; +import { AddressInfo } from 'node:net'; +import { createServer, type IncomingMessage, type Server, type ServerResponse } from 'node:http'; +import { createApiClient } from '../src/api/client'; + +let server: Server; +let baseUrl: string; + +beforeAll(async () => { + server = createServer((req: IncomingMessage, res: ServerResponse) => { + if (req.url === '/healthz') { + res.writeHead(200, { 'content-type': 'application/json' }); + res.end(JSON.stringify({ ok: true, service: 'weirsoe-party-protocol' })); + return; + } + + if (req.url === '/lobby/sessions/ABCD12') { + res.writeHead(200, { 'content-type': 'application/json' }); + res.end( + JSON.stringify({ + session: { code: 'ABCD12', status: 'lobby', host_id: 1, current_round: 1, players_count: 2 }, + players: [], + round_question: null, + phase_view_model: { phase: 'lobby', available_actions: ['start_round'], constraints: {} } + }) + ); + return; + } + + if (req.url?.startsWith('/lobby/sessions/')) { + res.writeHead(404, { 'content-type': 'application/json' }); + res.end(JSON.stringify({ error: 'Session not found' })); + return; + } + + res.writeHead(500, { 'content-type': 'application/json' }); + res.end(JSON.stringify({ error: 'unexpected route' })); + }); + + await new Promise((resolve) => server.listen(0, '127.0.0.1', () => resolve())); + const { port } = server.address() as AddressInfo; + baseUrl = `http://127.0.0.1:${port}`; +}); + +afterAll(async () => { + await new Promise((resolve, reject) => + server.close((err?: Error) => (err ? reject(err) : resolve())) + ); +}); + +describe('createApiClient', () => { + it('reads health + session detail through typed wrappers', async () => { + const client = createApiClient(baseUrl); + + const health = await client.health(); + expect(health.ok).toBe(true); + + const session = await client.getSession('abcd12'); + expect(session.ok).toBe(true); + if (session.ok) { + expect(session.data.session.code).toBe('ABCD12'); + } + }); + + it('returns consistent HTTP error shape for 4xx/5xx', async () => { + const client = createApiClient(baseUrl); + + const missing = await client.getSession('missing'); + expect(missing.ok).toBe(false); + if (!missing.ok) { + expect(missing.status).toBe(404); + expect(missing.error.kind).toBe('http'); + expect(missing.error.payload).toEqual({ error: 'Session not found' }); + } + }); + + it('returns consistent network error shape', async () => { + const client = createApiClient('http://127.0.0.1:9'); + + const health = await client.health(); + expect(health.ok).toBe(false); + if (!health.ok) { + expect(health.error.kind).toBe('network'); + expect(health.status).toBe(0); + } + }); +}); diff --git a/frontend/vitest.config.ts b/frontend/vitest.config.ts new file mode 100644 index 0000000..353adbd --- /dev/null +++ b/frontend/vitest.config.ts @@ -0,0 +1,8 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + include: ['tests/**/*.test.ts'], + exclude: ['**/node_modules/**'] + } +});