import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { apiRequest, buildKitchenApiUrl, getPath } from '../../src/api/client.js'; function createStore(overrides = {}) { return { config: { baseUrl: '', database: 'kitchen-db', ...(overrides.config || {}), }, session: { applicationKey: 'app-key', hasValidated: false, state: 'pending_validation', ...(overrides.session || {}), }, activeKitchen: { id: 'kitchen-1', ...(overrides.activeKitchen || {}), }, ...overrides, }; } describe('api/client', () => { let authFailureSpy; beforeEach(() => { authFailureSpy = vi.fn(); globalThis.window = { location: { origin: 'https://app.local', }, __loncApp: { handleAuthFailure: authFailureSpy, }, }; vi.spyOn(console, 'error').mockImplementation(() => {}); }); afterEach(() => { vi.restoreAllMocks(); vi.unstubAllGlobals(); delete globalThis.window; }); it('returns configured path constants', () => { expect(getPath('items')).toBe('kitchen/items'); expect(getPath('userApplication')).toBe('user/application/'); }); it('builds kitchen urls with encoded path segments and query values', () => { const store = createStore({ config: { baseUrl: 'https://api.example.com', database: 'my db', }, activeKitchen: { id: 'kitchen/01', }, }); const url = buildKitchenApiUrl(store, 'kitchen/items/grouped', { search_name: 'Milk + eggs', expanded: 1, ignored: '', }); expect(url).toBe( 'https://api.example.com/my%20db/kitchen/kitchen%2F01/kitchen/items/grouped?search_name=Milk+%2B+eggs&expanded=1', ); }); it('sends json request data and auth header through fetch', async () => { const store = createStore(); const fetchSpy = vi.fn(async () => new Response(JSON.stringify({ ok: true }), { status: 200, headers: { 'content-type': 'application/json', }, }), ); vi.stubGlobal('fetch', fetchSpy); const payload = await apiRequest(store, getPath('items'), { method: 'POST', body: { name: 'Rice', }, query: { label: 1, }, }); expect(payload).toEqual({ ok: true }); const [url, request] = fetchSpy.mock.calls[0]; expect(url).toBe('/kitchen-db/kitchen/kitchen-1/kitchen/items?label=1'); expect(request.method).toBe('POST'); expect(request.body).toBe('{"name":"Rice"}'); expect(request.headers.get('Accept')).toBe('application/json'); expect(request.headers.get('Content-Type')).toBe('application/json'); expect(request.headers.get('Authorization')).toBe('Bearer app-key'); }); it('normalizes api error payload into ApiRequestError details', async () => { const store = createStore(); vi.stubGlobal( 'fetch', vi.fn(async () => new Response( JSON.stringify({ errors: { name: ['Required'], quantity: 'Must be positive', }, }), { status: 400, headers: { 'content-type': 'application/json', }, }, ), ), ); await expect(apiRequest(store, getPath('items'))).rejects.toMatchObject({ name: 'ApiRequestError', status: 400, details: { name: 'Required', quantity: 'Must be positive', }, }); }); it('triggers auth failure handler after validated session receives auth errors', async () => { const store = createStore({ session: { applicationKey: 'app-key', hasValidated: true, state: 'connected', }, }); vi.stubGlobal( 'fetch', vi.fn(async () => new Response(JSON.stringify({ detail: 'Unauthorized' }), { status: 401, headers: { 'content-type': 'application/json', }, }), ), ); await expect(apiRequest(store, getPath('items'))).rejects.toMatchObject({ name: 'ApiRequestError', status: 401, }); expect(authFailureSpy).toHaveBeenCalledTimes(1); }); });