Implement upsert label flow and use-based mark gone handling
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/push/woodpecker Pipeline was successful
This commit is contained in:
@@ -48,17 +48,15 @@ describe('api/client', () => {
|
||||
it('returns configured path constants', () => {
|
||||
expect(getPath('items')).toBe('kitchen/items');
|
||||
expect(getPath('userApplication')).toBe('user/application/');
|
||||
expect(getPath('changes')).toBe('kitchen/changes');
|
||||
});
|
||||
|
||||
it('builds kitchen urls with encoded path segments and query values', () => {
|
||||
it('builds database-scoped kitchen urls with encoded 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', {
|
||||
@@ -68,7 +66,7 @@ describe('api/client', () => {
|
||||
});
|
||||
|
||||
expect(url).toBe(
|
||||
'https://api.example.com/my%20db/kitchen/kitchen%2F01/kitchen/items/grouped?search_name=Milk+%2B+eggs&expanded=1',
|
||||
'https://api.example.com/my%20db/kitchen/items/grouped?search_name=Milk+%2B+eggs&expanded=1',
|
||||
);
|
||||
});
|
||||
|
||||
@@ -97,7 +95,7 @@ describe('api/client', () => {
|
||||
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(url).toBe('/kitchen-db/kitchen/items?label=1');
|
||||
expect(request.method).toBe('POST');
|
||||
expect(request.body).toBe('{"name":"Rice"}');
|
||||
expect(request.headers.get('Accept')).toBe('application/json');
|
||||
|
||||
@@ -0,0 +1,161 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user