162 lines
4.6 KiB
JavaScript
162 lines
4.6 KiB
JavaScript
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
|
|
const apiRequestMock = vi.fn();
|
|
|
|
vi.mock('../../src/api/client.js', () => ({
|
|
getPath(key) {
|
|
const paths = {
|
|
items: 'kitchen/items',
|
|
changes: 'kitchen/changes',
|
|
};
|
|
return paths[key];
|
|
},
|
|
apiRequest: (...args) => apiRequestMock(...args),
|
|
}));
|
|
|
|
const {
|
|
applyItemUpsert,
|
|
listKitchenChanges,
|
|
previewItemUpsert,
|
|
useStockItem,
|
|
} = await import('../../src/api/stock.js');
|
|
|
|
describe('api/stock', () => {
|
|
beforeEach(() => {
|
|
apiRequestMock.mockReset();
|
|
});
|
|
|
|
it('listKitchenChanges returns normalized changes payload', async () => {
|
|
apiRequestMock.mockResolvedValueOnce({
|
|
since: 'cursor-1',
|
|
next_cursor: 'cursor-2',
|
|
changes: [{ type: 'item', action: 'upsert', timestamp: '2026-04-10T10:00:00Z' }],
|
|
});
|
|
|
|
const result = await listKitchenChanges({ config: { database: 'db' } }, { limit: 10 });
|
|
|
|
expect(result).toEqual({
|
|
since: 'cursor-1',
|
|
nextCursor: 'cursor-2',
|
|
changes: [{ type: 'item', action: 'upsert', timestamp: '2026-04-10T10:00:00Z' }],
|
|
});
|
|
expect(apiRequestMock).toHaveBeenCalledWith(
|
|
{ config: { database: 'db' } },
|
|
'kitchen/changes',
|
|
{ query: { since: undefined, limit: 10 } },
|
|
);
|
|
});
|
|
|
|
it('listKitchenChanges falls back to empty shape when changes are missing', async () => {
|
|
apiRequestMock.mockResolvedValueOnce({});
|
|
|
|
const result = await listKitchenChanges({ config: { database: 'db' } }, {});
|
|
|
|
expect(result).toEqual({
|
|
since: null,
|
|
nextCursor: null,
|
|
changes: [],
|
|
});
|
|
});
|
|
|
|
it('previewItemUpsert normalizes preview response', async () => {
|
|
apiRequestMock.mockResolvedValueOnce({
|
|
status: 'ok',
|
|
mode: 'preview',
|
|
operation: 'update',
|
|
match_type: 'uuid_b64',
|
|
matched_item: { uuid_b64: 'abc', name: 'Rice' },
|
|
payload: { name: 'Rice' },
|
|
});
|
|
|
|
const response = await previewItemUpsert({ config: { database: 'db' } }, { item: { name: 'Rice' } });
|
|
|
|
expect(response).toEqual({
|
|
status: 'ok',
|
|
mode: 'preview',
|
|
operation: 'update',
|
|
matchType: 'uuid_b64',
|
|
matchedItem: { uuid_b64: 'abc', name: 'Rice' },
|
|
item: null,
|
|
payload: { name: 'Rice' },
|
|
});
|
|
expect(apiRequestMock).toHaveBeenCalledWith(
|
|
{ config: { database: 'db' } },
|
|
'kitchen/items/upsert',
|
|
{
|
|
method: 'POST',
|
|
body: { item: { name: 'Rice' } },
|
|
query: { mode: 'preview' },
|
|
},
|
|
);
|
|
});
|
|
|
|
it('applyItemUpsert normalizes apply response', async () => {
|
|
apiRequestMock.mockResolvedValueOnce({
|
|
status: 'ok',
|
|
mode: 'apply',
|
|
operation: 'create',
|
|
match_type: null,
|
|
item: { uuid_b64: 'new1', name: 'Beans' },
|
|
});
|
|
|
|
const response = await applyItemUpsert({ config: { database: 'db' } }, { item: { name: 'Beans' } });
|
|
|
|
expect(response).toEqual({
|
|
status: 'ok',
|
|
mode: 'apply',
|
|
operation: 'create',
|
|
matchType: null,
|
|
matchedItem: null,
|
|
item: { uuid_b64: 'new1', name: 'Beans' },
|
|
payload: null,
|
|
});
|
|
});
|
|
|
|
it('useStockItem returns used on 204', async () => {
|
|
apiRequestMock.mockResolvedValueOnce(null);
|
|
|
|
const result = await useStockItem({ config: { database: 'db' } }, 'item-1');
|
|
|
|
expect(result).toEqual({ status: 'used' });
|
|
expect(apiRequestMock).toHaveBeenCalledWith(
|
|
{ config: { database: 'db' } },
|
|
'kitchen/items/item-1/use',
|
|
{ method: 'POST' },
|
|
);
|
|
});
|
|
|
|
it('useStockItem returns already_gone on 409', async () => {
|
|
apiRequestMock.mockRejectedValueOnce({ status: 409, message: 'Item is out of stock.' });
|
|
|
|
const result = await useStockItem({ config: { database: 'db' } }, 'item-1');
|
|
|
|
expect(result).toEqual({ status: 'already_gone' });
|
|
});
|
|
|
|
it('useStockItem falls back to delete on 404/405', async () => {
|
|
apiRequestMock
|
|
.mockRejectedValueOnce({ status: 404 })
|
|
.mockResolvedValueOnce(null);
|
|
|
|
const result = await useStockItem({ config: { database: 'db' } }, 'item-1');
|
|
|
|
expect(result).toEqual({ status: 'fallback_delete' });
|
|
expect(apiRequestMock).toHaveBeenNthCalledWith(
|
|
2,
|
|
{ config: { database: 'db' } },
|
|
'kitchen/items/item-1',
|
|
{ method: 'DELETE' },
|
|
);
|
|
});
|
|
|
|
it('useStockItem does not fallback on unrelated client errors', async () => {
|
|
apiRequestMock.mockRejectedValueOnce({ status: 422, message: 'validation_error' });
|
|
|
|
await expect(useStockItem({ config: { database: 'db' } }, 'item-1')).rejects.toMatchObject({
|
|
status: 422,
|
|
message: 'validation_error',
|
|
});
|
|
expect(apiRequestMock).toHaveBeenCalledTimes(1);
|
|
});
|
|
});
|