import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { getJson, getText, headOk, postJson, requestJson, shouldRetryError } from '~/helpers/http.js' describe( 'http helper', () => { const fetchMock = vi.fn() beforeEach( () => { vi.stubGlobal( 'fetch', fetchMock ) } ) afterEach( () => { vi.unstubAllGlobals() vi.restoreAllMocks() fetchMock.mockReset() } ) it( 'retries transient 5xx errors and eventually resolves JSON', async () => { fetchMock .mockResolvedValueOnce( new Response( JSON.stringify({ ok: false }), { headers: { 'Content-Type': 'application/json' }, status: 502 } ) ) .mockResolvedValueOnce( new Response( JSON.stringify({ ok: true }), { headers: { 'Content-Type': 'application/json' }, status: 200 } ) ) await expect( getJson( 'https://api.doesitarm.com/sitemap-endpoints.json', { attempts: 2, delayMs: 0 } ) ).resolves.toEqual({ ok: true } ) expect( fetchMock ).toHaveBeenCalledTimes( 2 ) } ) it( 'does not retry non-5xx errors', async () => { fetchMock.mockResolvedValueOnce( new Response( JSON.stringify({ ok: false }), { headers: { 'Content-Type': 'application/json' }, status: 404 } ) ) await expect( getJson( 'https://api.doesitarm.com/sitemap-endpoints.json', { attempts: 3, delayMs: 0 } ) ).rejects.toMatchObject({ response: { status: 404 } }) expect( fetchMock ).toHaveBeenCalledTimes( 1 ) } ) it( 'returns text responses', async () => { fetchMock.mockResolvedValueOnce( new Response( '', { headers: { 'Content-Type': 'application/xml' }, status: 200 } ) ) await expect( getText( 'https://doesitarm.com/sitemap.xml' ) ).resolves.toBe( '' ) } ) it( 'posts JSON payloads and parses JSON responses', async () => { fetchMock.mockResolvedValueOnce( new Response( JSON.stringify({ supportedVersionNumber: '2.1.0' }), { headers: { 'Content-Type': 'application/json' }, status: 200 } ) ) await expect( postJson( 'https://doesitarm.com/api/test-results', { filename: 'App.zip' } ) ).resolves.toEqual({ supportedVersionNumber: '2.1.0' } ) expect( fetchMock ).toHaveBeenCalledWith( 'https://doesitarm.com/api/test-results', expect.objectContaining({ body: JSON.stringify({ filename: 'App.zip' }), method: 'POST' }) ) } ) it( 'supports config-object JSON requests for the API client', async () => { fetchMock.mockResolvedValueOnce( new Response( JSON.stringify({ ok: true }), { headers: { 'Content-Type': 'application/json' }, status: 200 } ) ) await expect( requestJson({ method: 'GET', url: 'https://doesitarm.com/api/app/spotify.json' }) ).resolves.toEqual({ ok: true } ) } ) it( 'maps head requests to booleans', async () => { fetchMock .mockResolvedValueOnce( new Response( null, { status: 200 } ) ) .mockResolvedValueOnce( new Response( null, { status: 404 } ) ) await expect( headOk( 'https://doesitarm.com/sitemap.xml' ) ).resolves.toBe( true ) await expect( headOk( 'https://doesitarm.com/missing.xml' ) ).resolves.toBe( false ) } ) it( 'classifies retryable errors by HTTP status', () => { expect( shouldRetryError( { response: { status: 502 } } ) ).toBe( true ) expect( shouldRetryError( { response: { status: 503 } } ) ).toBe( true ) expect( shouldRetryError( { response: { status: 404 } } ) ).toBe( false ) expect( shouldRetryError( new Error( 'network' ) ) ).toBe( false ) } ) } )