import { describe, it, expect, vi, beforeEach, afterEach, type MockInstance } from 'vitest'; import { ApiError } 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('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'); }); });