import { describe, expect, it, vi } from 'vitest'; import { ApiClient } from '../../app/static/js/systems/ApiClient.js'; const endpoints = { highscores: '/api/highscores', runs: '/api/runs' }; /** Baut eine fetch-Attrappe mit fester Antwort. */ function fakeFetch(status, body) { return vi.fn(async () => ({ status, json: async () => { if (body === undefined) throw new SyntaxError('kein JSON'); return body; }, })); } describe('ApiClient', () => { it('holt ein Run-Token', async () => { const fetch = fakeFetch(201, { run_token: 'abc' }); expect(await new ApiClient(endpoints, fetch).startRun()).toBe('abc'); expect(fetch).toHaveBeenCalledWith('/api/runs', expect.objectContaining({ method: 'POST' })); }); it('liefert null statt Fehler, wenn der Server nicht erreichbar ist', async () => { const fetch = vi.fn(async () => { throw new TypeError('Failed to fetch'); }); const client = new ApiClient(endpoints, fetch); expect(await client.startRun()).toBeNull(); expect(await client.fetchHighscores()).toEqual({ ok: false, highscores: [] }); expect(await client.submitHighscore({ name: 'A', score: 1, wave: 1, durationS: 1, runToken: 't' })).toMatchObject({ ok: false, error: 'network', }); }); it('liest die Highscore-Liste', async () => { const entries = [{ rank: 1, name: 'Ada', score: 10, wave: 1, created_at: 'x' }]; const result = await new ApiClient(endpoints, fakeFetch(200, { highscores: entries })).fetchHighscores(); expect(result).toEqual({ ok: true, highscores: entries }); }); it('behandelt kaputte Antworten als Fehlschlag', async () => { expect(await new ApiClient(endpoints, fakeFetch(200, undefined)).fetchHighscores()).toEqual({ ok: false, highscores: [] }); expect(await new ApiClient(endpoints, fakeFetch(500, { error: 'x' })).startRun()).toBeNull(); }); it('sendet Highscores im API-Format mit ganzzahligen Werten', async () => { const fetch = fakeFetch(201, { rank: 3, highscores: [] }); const result = await new ApiClient(endpoints, fetch).submitHighscore({ name: 'Ada', score: 1234.7, wave: 3, durationS: 95.9, runToken: 'tok', }); expect(result).toEqual({ ok: true, rank: 3, highscores: [] }); const [url, init] = fetch.mock.calls[0]; expect(url).toBe('/api/highscores'); expect(init.method).toBe('POST'); expect(init.headers['Content-Type']).toBe('application/json'); expect(JSON.parse(init.body)).toEqual({ name: 'Ada', score: 1234, wave: 3, duration_s: 95, run_token: 'tok' }); }); it('gibt Validierungs- und Rate-Limit-Fehler lesbar zurück', async () => { const invalid = await new ApiClient( endpoints, fakeFetch(400, { error: 'validation_failed', message: 'Ungültige Eingaben.', fields: { name: 'zu lang' } }), ).submitHighscore({ name: 'A', score: 1, wave: 1, durationS: 1, runToken: 't' }); expect(invalid).toMatchObject({ ok: false, error: 'validation_failed', fields: { name: 'zu lang' } }); const limited = await new ApiClient(endpoints, fakeFetch(429, { error: 'rate_limited', message: 'Zu viele' })).submitHighscore({ name: 'A', score: 1, wave: 1, durationS: 1, runToken: 't', }); expect(limited).toMatchObject({ ok: false, error: 'rate_limited', message: 'Zu viele' }); }); });