import { describe, it, expect, vi, afterEach } from 'vitest'; import { createPlatformClient, ApiError } from './index.js'; describe('createPlatformClient', () => { const baseConfig = { baseUrl: 'http://localhost:4003/api', productId: 'testapp', getAccessToken: () => 'test-token', }; afterEach(() => { vi.restoreAllMocks(); }); it('should make GET requests with auth header', async () => { const fetchMock = vi.fn().mockResolvedValue({ ok: true, status: 200, json: () => Promise.resolve({ items: [] }), }); vi.stubGlobal('fetch', fetchMock); const api = createPlatformClient(baseConfig); const result = await api.get('/items'); expect(result).toEqual({ items: [] }); expect(fetchMock).toHaveBeenCalledWith( 'http://localhost:4003/api/items', expect.objectContaining({ method: 'GET', headers: expect.objectContaining({ Authorization: 'Bearer test-token', 'x-product-id': 'testapp', }), }) ); }); it('should make POST requests with body', async () => { const fetchMock = vi.fn().mockResolvedValue({ ok: true, status: 200, json: () => Promise.resolve({ id: '1' }), }); vi.stubGlobal('fetch', fetchMock); const api = createPlatformClient(baseConfig); const result = await api.post('/items', { name: 'test' }); expect(result).toEqual({ id: '1' }); expect(fetchMock).toHaveBeenCalledWith( 'http://localhost:4003/api/items', expect.objectContaining({ method: 'POST', body: JSON.stringify({ name: 'test' }), }) ); }); it('should handle 204 No Content', async () => { vi.stubGlobal( 'fetch', vi.fn().mockResolvedValue({ ok: true, status: 204, json: () => Promise.reject(new Error('no body')), }) ); const api = createPlatformClient(baseConfig); const result = await api.del('/items/1'); expect(result).toBeUndefined(); }); it('should throw ApiError on non-OK response', async () => { vi.stubGlobal( 'fetch', vi.fn().mockResolvedValue({ ok: false, status: 400, json: () => Promise.resolve({ message: 'Bad request' }), }) ); const api = createPlatformClient(baseConfig); await expect(api.get('/items')).rejects.toThrow(ApiError); try { await api.get('/items'); } catch (e) { expect(e).toBeInstanceOf(ApiError); expect((e as ApiError).status).toBe(400); } }); it('should attempt token refresh on 401', async () => { let callCount = 0; vi.stubGlobal( 'fetch', vi.fn().mockImplementation(() => { callCount++; if (callCount === 1) { return Promise.resolve({ ok: false, status: 401, json: () => Promise.resolve({ message: 'Unauthorized' }), }); } return Promise.resolve({ ok: true, status: 200, json: () => Promise.resolve({ refreshed: true }), }); }) ); const refreshFn = vi.fn().mockResolvedValue(true); const api = createPlatformClient({ ...baseConfig, refreshAccessToken: refreshFn, }); const result = await api.get('/items'); expect(refreshFn).toHaveBeenCalledOnce(); expect(result).toEqual({ refreshed: true }); }); it('should not include auth header when token is null', async () => { const fetchMock = vi.fn().mockResolvedValue({ ok: true, status: 200, json: () => Promise.resolve({}), }); vi.stubGlobal('fetch', fetchMock); const api = createPlatformClient({ ...baseConfig, getAccessToken: () => null, }); await api.get('/public/items'); const headers = fetchMock.mock.calls[0][1].headers as Record; expect(headers['Authorization']).toBeUndefined(); }); it('should include x-request-id header', async () => { const fetchMock = vi.fn().mockResolvedValue({ ok: true, status: 200, json: () => Promise.resolve({}), }); vi.stubGlobal('fetch', fetchMock); const api = createPlatformClient(baseConfig); await api.get('/items'); const headers = fetchMock.mock.calls[0][1].headers as Record; expect(headers['x-request-id']).toBeDefined(); expect(headers['x-request-id'].length).toBeGreaterThan(0); }); });