Files
lonc/tests/api/client.test.js
T

169 lines
4.2 KiB
JavaScript
Raw Normal View History

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);
});
});