import { describe, it, expect, vi, beforeEach, afterEach, type MockInstance } from 'vitest'; import { ApiError, request } from './client'; import { getManga } from './mangas'; describe('request error envelope parsing', () => { let fetchSpy: MockInstance; beforeEach(() => { fetchSpy = vi.spyOn(globalThis, 'fetch'); }); afterEach(() => { vi.restoreAllMocks(); }); it('parses {error:{code,message}} into ApiError.code and message', async () => { fetchSpy.mockResolvedValueOnce( new Response( JSON.stringify({ error: { code: 'invalid_input', message: 'title is required' } }), { status: 400, headers: { 'content-type': 'application/json' } } ) ); await expect(getManga('x')).rejects.toMatchObject({ status: 400, code: 'invalid_input', message: 'title is required' }); }); it('falls back to http_error code when body is HTML (e.g. upstream proxy)', async () => { fetchSpy.mockResolvedValueOnce( new Response('upstream proxy bad', { status: 502, headers: { 'content-type': 'text/html' } }) ); const err = (await getManga('x').catch((e) => e)) as ApiError; expect(err).toBeInstanceOf(ApiError); expect(err.status).toBe(502); expect(err.code).toBe('http_error'); expect(err.message).toContain('upstream proxy bad'); }); it('falls back to http_error code when body is empty', async () => { fetchSpy.mockResolvedValueOnce(new Response('', { status: 500 })); const err = (await getManga('x').catch((e) => e)) as ApiError; expect(err).toBeInstanceOf(ApiError); expect(err.status).toBe(500); expect(err.code).toBe('http_error'); }); it('treats empty 200/201 bodies as undefined (no JSON.parse crash)', async () => { // Regression: addMangaToCollection is typed `void` and the // backend returns 201 (created) / 200 (already there) with // no body. Without the empty-body short-circuit, `res.json()` // would throw `JSON.parse: unexpected end of data`. fetchSpy.mockResolvedValueOnce(new Response(null, { status: 201 })); const created = await request('/v1/whatever', { method: 'POST' }); expect(created).toBeUndefined(); fetchSpy.mockResolvedValueOnce(new Response(null, { status: 200 })); const ok200 = await request('/v1/whatever', { method: 'POST' }); expect(ok200).toBeUndefined(); }); it('falls back to http_error code when JSON has no error envelope', async () => { fetchSpy.mockResolvedValueOnce( new Response(JSON.stringify({ message: 'oops' }), { status: 500, headers: { 'content-type': 'application/json' } }) ); const err = (await getManga('x').catch((e) => e)) as ApiError; expect(err.code).toBe('http_error'); }); });