Tests refactor
This commit is contained in:
parent
245520fb50
commit
99134d8556
165 changed files with 911 additions and 531 deletions
167
packages/web/tests/services/api-client.test.ts
Normal file
167
packages/web/tests/services/api-client.test.ts
Normal file
|
|
@ -0,0 +1,167 @@
|
|||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
|
||||
import type { apiClient as ApiClientType } from '../../src/services/api-client';
|
||||
|
||||
// Create a fresh ApiClient for each test by re-importing
|
||||
let apiClient: typeof ApiClientType;
|
||||
|
||||
beforeEach(async () => {
|
||||
vi.restoreAllMocks();
|
||||
// Reset module to get a fresh singleton
|
||||
vi.resetModules();
|
||||
const mod = await import('../../src/services/api-client');
|
||||
apiClient = mod.apiClient;
|
||||
});
|
||||
|
||||
function mockFetch(body: unknown, status = 200) {
|
||||
return vi.spyOn(globalThis, 'fetch').mockResolvedValue({
|
||||
ok: status >= 200 && status < 300,
|
||||
status,
|
||||
statusText: status === 200 ? 'OK' : 'Error',
|
||||
json: () => Promise.resolve(body),
|
||||
} as Response);
|
||||
}
|
||||
|
||||
describe('ApiClient', () => {
|
||||
describe('token management', () => {
|
||||
it('hasToken returns false initially', () => {
|
||||
expect(apiClient.hasToken).toBe(false);
|
||||
});
|
||||
|
||||
it('hasToken returns true after setting token', () => {
|
||||
apiClient.accessToken = 'test-token';
|
||||
expect(apiClient.hasToken).toBe(true);
|
||||
});
|
||||
|
||||
it('includes Authorization header when token is set', async () => {
|
||||
const spy = mockFetch({ data: [] });
|
||||
apiClient.accessToken = 'my-jwt';
|
||||
|
||||
await apiClient.get('/test');
|
||||
|
||||
const headers = spy.mock.calls[0][1]?.headers as Record<string, string>;
|
||||
expect(headers['Authorization']).toBe('Bearer my-jwt');
|
||||
});
|
||||
|
||||
it('omits Authorization header when no token', async () => {
|
||||
const spy = mockFetch({ data: [] });
|
||||
|
||||
await apiClient.get('/test');
|
||||
|
||||
const headers = spy.mock.calls[0][1]?.headers as Record<string, string>;
|
||||
expect(headers['Authorization']).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('get', () => {
|
||||
it('makes GET request to correct URL', async () => {
|
||||
const spy = mockFetch({ id: '1' });
|
||||
|
||||
await apiClient.get('/households/hh1/stores');
|
||||
|
||||
expect(spy).toHaveBeenCalledWith(
|
||||
expect.stringContaining('/households/hh1/stores'),
|
||||
expect.objectContaining({ headers: expect.any(Object) }),
|
||||
);
|
||||
});
|
||||
|
||||
it('returns parsed JSON body', async () => {
|
||||
mockFetch({ id: '1', name: 'CVS' });
|
||||
|
||||
const result = await apiClient.get('/test');
|
||||
|
||||
expect(result).toEqual({ id: '1', name: 'CVS' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('post', () => {
|
||||
it('makes POST request with JSON body', async () => {
|
||||
const spy = mockFetch({ id: '1' });
|
||||
const body = { name: 'Walgreens' };
|
||||
|
||||
await apiClient.post('/stores', body);
|
||||
|
||||
expect(spy).toHaveBeenCalledWith(
|
||||
expect.stringContaining('/stores'),
|
||||
expect.objectContaining({
|
||||
method: 'POST',
|
||||
body: JSON.stringify(body),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('handles post without body', async () => {
|
||||
const spy = mockFetch({ id: '1' });
|
||||
|
||||
await apiClient.post('/test');
|
||||
|
||||
expect(spy).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
expect.objectContaining({ body: undefined }),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('patch', () => {
|
||||
it('makes PATCH request with JSON body', async () => {
|
||||
const spy = mockFetch({ id: '1' });
|
||||
const body = { name: 'Updated' };
|
||||
|
||||
await apiClient.patch('/stores/1', body);
|
||||
|
||||
expect(spy).toHaveBeenCalledWith(
|
||||
expect.stringContaining('/stores/1'),
|
||||
expect.objectContaining({
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify(body),
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('delete', () => {
|
||||
it('makes DELETE request', async () => {
|
||||
const spy = mockFetch(undefined, 204);
|
||||
|
||||
await apiClient.delete('/stores/1');
|
||||
|
||||
expect(spy).toHaveBeenCalledWith(
|
||||
expect.stringContaining('/stores/1'),
|
||||
expect.objectContaining({ method: 'DELETE' }),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('error handling', () => {
|
||||
it('throws with message from error response body', async () => {
|
||||
mockFetch({ message: 'Store not found' }, 404);
|
||||
|
||||
await expect(apiClient.get('/test')).rejects.toThrow('Store not found');
|
||||
});
|
||||
|
||||
it('throws with status code when body has no message', async () => {
|
||||
mockFetch({}, 500);
|
||||
|
||||
await expect(apiClient.get('/test')).rejects.toThrow('Request failed: 500');
|
||||
});
|
||||
|
||||
it('throws with status text when JSON parsing fails', async () => {
|
||||
vi.spyOn(globalThis, 'fetch').mockResolvedValue({
|
||||
ok: false,
|
||||
status: 502,
|
||||
statusText: 'Bad Gateway',
|
||||
json: () => Promise.reject(new Error('parse error')),
|
||||
} as Response);
|
||||
|
||||
await expect(apiClient.get('/test')).rejects.toThrow('Request failed: 502 Bad Gateway');
|
||||
});
|
||||
|
||||
it('returns undefined for 204 No Content', async () => {
|
||||
mockFetch(null, 204);
|
||||
|
||||
const result = await apiClient.get('/test');
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
67
packages/web/tests/services/cabinet-events.test.ts
Normal file
67
packages/web/tests/services/cabinet-events.test.ts
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
|
||||
const { mockGet } = vi.hoisted(() => ({
|
||||
mockGet: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../../src/services/api-client', () => ({
|
||||
apiClient: { get: mockGet },
|
||||
}));
|
||||
|
||||
import { listCabinetEvents, getEventsByItem, getSpendingSummary } from '../../src/services/cabinet-events';
|
||||
|
||||
beforeEach(() => vi.clearAllMocks());
|
||||
|
||||
describe('cabinet-events service', () => {
|
||||
it('listCabinetEvents with no query', async () => {
|
||||
mockGet.mockResolvedValue({ data: [] });
|
||||
await listCabinetEvents('hh1');
|
||||
expect(mockGet).toHaveBeenCalledWith('/households/hh1/cabinet-events');
|
||||
});
|
||||
|
||||
it('listCabinetEvents builds query string', async () => {
|
||||
mockGet.mockResolvedValue({ data: [] });
|
||||
await listCabinetEvents('hh1', {
|
||||
medicineId: 'med-1',
|
||||
eventType: 'dispense',
|
||||
startDate: '2026-01-01',
|
||||
endDate: '2026-02-01',
|
||||
});
|
||||
expect(mockGet).toHaveBeenCalledWith(expect.stringContaining('medicineId=med-1'));
|
||||
expect(mockGet).toHaveBeenCalledWith(expect.stringContaining('eventType=dispense'));
|
||||
expect(mockGet).toHaveBeenCalledWith(expect.stringContaining('startDate=2026-01-01'));
|
||||
});
|
||||
|
||||
it('getEventsByItem builds URL with cabinetItemId', async () => {
|
||||
mockGet.mockResolvedValue({ data: [] });
|
||||
await getEventsByItem('hh1', 'ci-1', { limit: 5 });
|
||||
expect(mockGet).toHaveBeenCalledWith(expect.stringContaining('/cabinet-events/by-item/ci-1'));
|
||||
expect(mockGet).toHaveBeenCalledWith(expect.stringContaining('limit=5'));
|
||||
});
|
||||
|
||||
it('listCabinetEvents with cursor and limit', async () => {
|
||||
mockGet.mockResolvedValue({ data: [] });
|
||||
await listCabinetEvents('hh1', { cursor: 'cur1', limit: 10 });
|
||||
expect(mockGet).toHaveBeenCalledWith(expect.stringContaining('cursor=cur1'));
|
||||
expect(mockGet).toHaveBeenCalledWith(expect.stringContaining('limit=10'));
|
||||
});
|
||||
|
||||
it('getEventsByItem with cursor', async () => {
|
||||
mockGet.mockResolvedValue({ data: [] });
|
||||
await getEventsByItem('hh1', 'ci-1', { cursor: 'cur2' });
|
||||
expect(mockGet).toHaveBeenCalledWith(expect.stringContaining('cursor=cur2'));
|
||||
});
|
||||
|
||||
it('getSpendingSummary with no query', async () => {
|
||||
mockGet.mockResolvedValue({});
|
||||
await getSpendingSummary('hh1');
|
||||
expect(mockGet).toHaveBeenCalledWith('/households/hh1/cabinet-events/spending-summary');
|
||||
});
|
||||
|
||||
it('getSpendingSummary builds query string', async () => {
|
||||
mockGet.mockResolvedValue({});
|
||||
await getSpendingSummary('hh1', { period: 'month', medicineId: 'med-1' });
|
||||
expect(mockGet).toHaveBeenCalledWith(expect.stringContaining('period=month'));
|
||||
expect(mockGet).toHaveBeenCalledWith(expect.stringContaining('medicineId=med-1'));
|
||||
});
|
||||
});
|
||||
100
packages/web/tests/services/cabinet.test.ts
Normal file
100
packages/web/tests/services/cabinet.test.ts
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
|
||||
const { mockGet, mockPost, mockPatch, mockDelete } = vi.hoisted(() => ({
|
||||
mockGet: vi.fn(),
|
||||
mockPost: vi.fn(),
|
||||
mockPatch: vi.fn(),
|
||||
mockDelete: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../../src/services/api-client', () => ({
|
||||
apiClient: { get: mockGet, post: mockPost, patch: mockPatch, delete: mockDelete },
|
||||
}));
|
||||
|
||||
import {
|
||||
listCabinetItems,
|
||||
getCabinetItem,
|
||||
getCabinetSummary,
|
||||
getExpiringSoon,
|
||||
createCabinetItem,
|
||||
updateCabinetItem,
|
||||
adjustCabinetItemQuantity,
|
||||
deleteCabinetItem,
|
||||
} from '../../src/services/cabinet';
|
||||
|
||||
beforeEach(() => vi.clearAllMocks());
|
||||
|
||||
describe('cabinet service', () => {
|
||||
it('listCabinetItems with no query', async () => {
|
||||
mockGet.mockResolvedValue({ data: [] });
|
||||
await listCabinetItems('hh1');
|
||||
expect(mockGet).toHaveBeenCalledWith('/households/hh1/cabinet');
|
||||
});
|
||||
|
||||
it('listCabinetItems builds query string', async () => {
|
||||
mockGet.mockResolvedValue({ data: [] });
|
||||
await listCabinetItems('hh1', { medicineId: 'med-1', status: 'active', expiringWithin: 30 });
|
||||
expect(mockGet).toHaveBeenCalledWith(expect.stringContaining('medicineId=med-1'));
|
||||
expect(mockGet).toHaveBeenCalledWith(expect.stringContaining('status=active'));
|
||||
expect(mockGet).toHaveBeenCalledWith(expect.stringContaining('expiringWithin=30'));
|
||||
});
|
||||
|
||||
it('listCabinetItems with cursor and limit', async () => {
|
||||
mockGet.mockResolvedValue({ data: [] });
|
||||
await listCabinetItems('hh1', { cursor: 'cur1', limit: 20 });
|
||||
expect(mockGet).toHaveBeenCalledWith(expect.stringContaining('cursor=cur1'));
|
||||
expect(mockGet).toHaveBeenCalledWith(expect.stringContaining('limit=20'));
|
||||
});
|
||||
|
||||
it('getCabinetItem calls GET', async () => {
|
||||
mockGet.mockResolvedValue({ _id: 'ci-1' });
|
||||
await getCabinetItem('hh1', 'ci-1');
|
||||
expect(mockGet).toHaveBeenCalledWith('/households/hh1/cabinet/ci-1');
|
||||
});
|
||||
|
||||
it('getCabinetSummary calls GET', async () => {
|
||||
mockGet.mockResolvedValue({});
|
||||
await getCabinetSummary('hh1');
|
||||
expect(mockGet).toHaveBeenCalledWith('/households/hh1/cabinet/summary');
|
||||
});
|
||||
|
||||
it('getExpiringSoon uses default days', async () => {
|
||||
mockGet.mockResolvedValue({ data: [] });
|
||||
await getExpiringSoon('hh1');
|
||||
expect(mockGet).toHaveBeenCalledWith('/households/hh1/cabinet/expiring-soon?days=30');
|
||||
});
|
||||
|
||||
it('getExpiringSoon uses custom days', async () => {
|
||||
mockGet.mockResolvedValue({ data: [] });
|
||||
await getExpiringSoon('hh1', 7);
|
||||
expect(mockGet).toHaveBeenCalledWith('/households/hh1/cabinet/expiring-soon?days=7');
|
||||
});
|
||||
|
||||
it('createCabinetItem calls POST', async () => {
|
||||
mockPost.mockResolvedValue({ _id: 'ci-1' });
|
||||
const data = { medicineId: 'med-1' } as never;
|
||||
await createCabinetItem('hh1', data);
|
||||
expect(mockPost).toHaveBeenCalledWith('/households/hh1/cabinet', data);
|
||||
});
|
||||
|
||||
it('updateCabinetItem calls PATCH', async () => {
|
||||
mockPatch.mockResolvedValue({ _id: 'ci-1' });
|
||||
await updateCabinetItem('hh1', 'ci-1', { notes: 'x' } as never);
|
||||
expect(mockPatch).toHaveBeenCalledWith('/households/hh1/cabinet/ci-1', { notes: 'x' });
|
||||
});
|
||||
|
||||
it('adjustCabinetItemQuantity calls POST', async () => {
|
||||
mockPost.mockResolvedValue({ _id: 'ci-1' });
|
||||
await adjustCabinetItemQuantity('hh1', 'ci-1', { adjustment: -5, reason: 'used' } as never);
|
||||
expect(mockPost).toHaveBeenCalledWith('/households/hh1/cabinet/ci-1/adjust', {
|
||||
adjustment: -5,
|
||||
reason: 'used',
|
||||
});
|
||||
});
|
||||
|
||||
it('deleteCabinetItem calls DELETE', async () => {
|
||||
mockDelete.mockResolvedValue(undefined);
|
||||
await deleteCabinetItem('hh1', 'ci-1');
|
||||
expect(mockDelete).toHaveBeenCalledWith('/households/hh1/cabinet/ci-1');
|
||||
});
|
||||
});
|
||||
53
packages/web/tests/services/households.test.ts
Normal file
53
packages/web/tests/services/households.test.ts
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
|
||||
const { mockGet, mockPost, mockPatch } = vi.hoisted(() => ({
|
||||
mockGet: vi.fn(),
|
||||
mockPost: vi.fn(),
|
||||
mockPatch: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../../src/services/api-client', () => ({
|
||||
apiClient: { get: mockGet, post: mockPost, patch: mockPatch },
|
||||
}));
|
||||
|
||||
import {
|
||||
createHousehold,
|
||||
getHousehold,
|
||||
updateHousehold,
|
||||
generateInviteCode,
|
||||
joinHousehold,
|
||||
} from '../../src/services/households';
|
||||
|
||||
beforeEach(() => vi.clearAllMocks());
|
||||
|
||||
describe('households service', () => {
|
||||
it('createHousehold calls POST', async () => {
|
||||
mockPost.mockResolvedValue({ _id: 'hh1' });
|
||||
await createHousehold('My Home');
|
||||
expect(mockPost).toHaveBeenCalledWith('/households', { name: 'My Home' });
|
||||
});
|
||||
|
||||
it('getHousehold calls GET', async () => {
|
||||
mockGet.mockResolvedValue({ _id: 'hh1' });
|
||||
await getHousehold('hh1');
|
||||
expect(mockGet).toHaveBeenCalledWith('/households/hh1');
|
||||
});
|
||||
|
||||
it('updateHousehold calls PATCH', async () => {
|
||||
mockPatch.mockResolvedValue({ _id: 'hh1' });
|
||||
await updateHousehold('hh1', { name: 'Updated' });
|
||||
expect(mockPatch).toHaveBeenCalledWith('/households/hh1', { name: 'Updated' });
|
||||
});
|
||||
|
||||
it('generateInviteCode calls POST', async () => {
|
||||
mockPost.mockResolvedValue({ _id: 'hh1' });
|
||||
await generateInviteCode('hh1');
|
||||
expect(mockPost).toHaveBeenCalledWith('/households/hh1/invite');
|
||||
});
|
||||
|
||||
it('joinHousehold calls POST with invite code', async () => {
|
||||
mockPost.mockResolvedValue({ _id: 'hh1' });
|
||||
await joinHousehold('ABC123');
|
||||
expect(mockPost).toHaveBeenCalledWith('/households/join', { inviteCode: 'ABC123' });
|
||||
});
|
||||
});
|
||||
68
packages/web/tests/services/meal-plans.test.ts
Normal file
68
packages/web/tests/services/meal-plans.test.ts
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { apiClient } from '../../src/services/api-client';
|
||||
import * as MealPlansService from '../../src/services/meal-plans';
|
||||
|
||||
vi.mock('../../src/services/api-client', () => ({
|
||||
apiClient: {
|
||||
get: vi.fn(),
|
||||
post: vi.fn(),
|
||||
patch: vi.fn(),
|
||||
delete: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
describe('meal-plans service', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('listMealPlans with and without query', async () => {
|
||||
await MealPlansService.listMealPlans('hh1');
|
||||
expect(apiClient.get).toHaveBeenCalledWith('/households/hh1/meal-plans');
|
||||
|
||||
await MealPlansService.listMealPlans('hh1', { cursor: 'cur', limit: 10 });
|
||||
expect(apiClient.get).toHaveBeenCalledWith('/households/hh1/meal-plans?cursor=cur&limit=10');
|
||||
});
|
||||
|
||||
it('getMealPlanByWeek', async () => {
|
||||
await MealPlansService.getMealPlanByWeek('hh1', '2026-05-10');
|
||||
expect(apiClient.get).toHaveBeenCalledWith('/households/hh1/meal-plans/week/2026-05-10');
|
||||
});
|
||||
|
||||
it('getMealPlan', async () => {
|
||||
await MealPlansService.getMealPlan('hh1', 'mp1');
|
||||
expect(apiClient.get).toHaveBeenCalledWith('/households/hh1/meal-plans/mp1');
|
||||
});
|
||||
|
||||
it('createMealPlan', async () => {
|
||||
const data = { weekStartDate: '2026-05-10' } as any;
|
||||
await MealPlansService.createMealPlan('hh1', data);
|
||||
expect(apiClient.post).toHaveBeenCalledWith('/households/hh1/meal-plans', data);
|
||||
});
|
||||
|
||||
it('updateMealPlan', async () => {
|
||||
const data = { days: [] } as any;
|
||||
await MealPlansService.updateMealPlan('hh1', 'mp1', data);
|
||||
expect(apiClient.patch).toHaveBeenCalledWith('/households/hh1/meal-plans/mp1', data);
|
||||
});
|
||||
|
||||
it('updateMealPlanStatus', async () => {
|
||||
await MealPlansService.updateMealPlanStatus('hh1', 'mp1', 'active' as any);
|
||||
expect(apiClient.patch).toHaveBeenCalledWith('/households/hh1/meal-plans/mp1/status', { status: 'active' });
|
||||
});
|
||||
|
||||
it('deleteMealPlan', async () => {
|
||||
await MealPlansService.deleteMealPlan('hh1', 'mp1');
|
||||
expect(apiClient.delete).toHaveBeenCalledWith('/households/hh1/meal-plans/mp1');
|
||||
});
|
||||
|
||||
it('getSuggestions', async () => {
|
||||
await MealPlansService.getSuggestions('hh1', 3);
|
||||
expect(apiClient.get).toHaveBeenCalledWith('/households/hh1/meal-plans/suggestions?limit=3');
|
||||
});
|
||||
|
||||
it('getShoppingGap', async () => {
|
||||
await MealPlansService.getShoppingGap('hh1', 'mp1');
|
||||
expect(apiClient.get).toHaveBeenCalledWith('/households/hh1/meal-plans/mp1/gap');
|
||||
});
|
||||
});
|
||||
62
packages/web/tests/services/medicine-prices.test.ts
Normal file
62
packages/web/tests/services/medicine-prices.test.ts
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
|
||||
const { mockGet, mockPost } = vi.hoisted(() => ({
|
||||
mockGet: vi.fn(),
|
||||
mockPost: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../../src/services/api-client', () => ({
|
||||
apiClient: { get: mockGet, post: mockPost },
|
||||
}));
|
||||
|
||||
import { recordPrice, getPriceHistory, compareStores, getPriceAnalytics } from '../../src/services/medicine-prices';
|
||||
|
||||
beforeEach(() => vi.clearAllMocks());
|
||||
|
||||
describe('medicine-prices service', () => {
|
||||
it('recordPrice calls POST', async () => {
|
||||
mockPost.mockResolvedValue({ _id: 'pr-1' });
|
||||
const data = { medicineProductId: 'mp-1', storeId: 'st-1', price: 10 } as never;
|
||||
await recordPrice('hh1', data);
|
||||
expect(mockPost).toHaveBeenCalledWith('/households/hh1/medicine-prices', data);
|
||||
});
|
||||
|
||||
it('getPriceHistory with no query', async () => {
|
||||
mockGet.mockResolvedValue({ data: [] });
|
||||
await getPriceHistory('hh1', 'med-1');
|
||||
expect(mockGet).toHaveBeenCalledWith('/households/hh1/medicine-prices/history/med-1');
|
||||
});
|
||||
|
||||
it('getPriceHistory builds query string', async () => {
|
||||
mockGet.mockResolvedValue({ data: [] });
|
||||
await getPriceHistory('hh1', 'med-1', { storeId: 'st-1', startDate: '2026-01-01' });
|
||||
expect(mockGet).toHaveBeenCalledWith(expect.stringContaining('storeId=st-1'));
|
||||
expect(mockGet).toHaveBeenCalledWith(expect.stringContaining('startDate=2026-01-01'));
|
||||
});
|
||||
|
||||
it('getPriceHistory with endDate, cursor and limit', async () => {
|
||||
mockGet.mockResolvedValue({ data: [] });
|
||||
await getPriceHistory('hh1', 'med-1', { endDate: '2026-12-31', cursor: 'cur1', limit: 10 });
|
||||
expect(mockGet).toHaveBeenCalledWith(expect.stringContaining('endDate=2026-12-31'));
|
||||
expect(mockGet).toHaveBeenCalledWith(expect.stringContaining('cursor=cur1'));
|
||||
expect(mockGet).toHaveBeenCalledWith(expect.stringContaining('limit=10'));
|
||||
});
|
||||
|
||||
it('compareStores calls GET with medicineId', async () => {
|
||||
mockGet.mockResolvedValue({ data: [] });
|
||||
await compareStores('hh1', 'med-1');
|
||||
expect(mockGet).toHaveBeenCalledWith('/households/hh1/medicine-prices/compare/med-1');
|
||||
});
|
||||
|
||||
it('getPriceAnalytics uses default period', async () => {
|
||||
mockGet.mockResolvedValue({});
|
||||
await getPriceAnalytics('hh1');
|
||||
expect(mockGet).toHaveBeenCalledWith('/households/hh1/medicine-prices/analytics?period=month');
|
||||
});
|
||||
|
||||
it('getPriceAnalytics uses custom period', async () => {
|
||||
mockGet.mockResolvedValue({});
|
||||
await getPriceAnalytics('hh1', 'year');
|
||||
expect(mockGet).toHaveBeenCalledWith('/households/hh1/medicine-prices/analytics?period=year');
|
||||
});
|
||||
});
|
||||
105
packages/web/tests/services/medicines.test.ts
Normal file
105
packages/web/tests/services/medicines.test.ts
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
|
||||
const { mockGet, mockPost, mockPatch, mockDelete } = vi.hoisted(() => ({
|
||||
mockGet: vi.fn(),
|
||||
mockPost: vi.fn(),
|
||||
mockPatch: vi.fn(),
|
||||
mockDelete: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../../src/services/api-client', () => ({
|
||||
apiClient: { get: mockGet, post: mockPost, patch: mockPatch, delete: mockDelete },
|
||||
}));
|
||||
|
||||
import {
|
||||
listMedicines,
|
||||
getMedicine,
|
||||
createMedicine,
|
||||
updateMedicine,
|
||||
deleteMedicine,
|
||||
listMedicineProducts,
|
||||
createMedicineProduct,
|
||||
updateMedicineProduct,
|
||||
deleteMedicineProduct,
|
||||
} from '../../src/services/medicines';
|
||||
|
||||
beforeEach(() => vi.clearAllMocks());
|
||||
|
||||
describe('medicines service', () => {
|
||||
it('listMedicines with no query', async () => {
|
||||
mockGet.mockResolvedValue({ data: [] });
|
||||
await listMedicines('hh1');
|
||||
expect(mockGet).toHaveBeenCalledWith('/households/hh1/medicines');
|
||||
});
|
||||
|
||||
it('listMedicines builds query string', async () => {
|
||||
mockGet.mockResolvedValue({ data: [] });
|
||||
await listMedicines('hh1', { q: 'aspirin', category: 'pain', form: 'tablet', limit: 10 });
|
||||
expect(mockGet).toHaveBeenCalledWith(expect.stringContaining('q=aspirin'));
|
||||
expect(mockGet).toHaveBeenCalledWith(expect.stringContaining('category=pain'));
|
||||
});
|
||||
|
||||
it('listMedicines with cursor', async () => {
|
||||
mockGet.mockResolvedValue({ data: [] });
|
||||
await listMedicines('hh1', { cursor: 'cur1' });
|
||||
expect(mockGet).toHaveBeenCalledWith(expect.stringContaining('cursor=cur1'));
|
||||
});
|
||||
|
||||
it('getMedicine calls GET', async () => {
|
||||
mockGet.mockResolvedValue({ _id: 'med-1' });
|
||||
await getMedicine('hh1', 'med-1');
|
||||
expect(mockGet).toHaveBeenCalledWith('/households/hh1/medicines/med-1');
|
||||
});
|
||||
|
||||
it('createMedicine calls POST', async () => {
|
||||
mockPost.mockResolvedValue({ _id: 'med-1' });
|
||||
await createMedicine('hh1', { name: 'Aspirin' } as never);
|
||||
expect(mockPost).toHaveBeenCalledWith('/households/hh1/medicines', { name: 'Aspirin' });
|
||||
});
|
||||
|
||||
it('updateMedicine calls PATCH', async () => {
|
||||
mockPatch.mockResolvedValue({ _id: 'med-1' });
|
||||
await updateMedicine('hh1', 'med-1', { name: 'Updated' } as never);
|
||||
expect(mockPatch).toHaveBeenCalledWith('/households/hh1/medicines/med-1', { name: 'Updated' });
|
||||
});
|
||||
|
||||
it('deleteMedicine calls DELETE', async () => {
|
||||
mockDelete.mockResolvedValue(undefined);
|
||||
await deleteMedicine('hh1', 'med-1');
|
||||
expect(mockDelete).toHaveBeenCalledWith('/households/hh1/medicines/med-1');
|
||||
});
|
||||
|
||||
it('listMedicineProducts builds URL with medicineId', async () => {
|
||||
mockGet.mockResolvedValue({ data: [] });
|
||||
await listMedicineProducts('hh1', 'med-1', { limit: 5 });
|
||||
expect(mockGet).toHaveBeenCalledWith(expect.stringContaining('/medicines/med-1/products'));
|
||||
});
|
||||
|
||||
it('listMedicineProducts with cursor', async () => {
|
||||
mockGet.mockResolvedValue({ data: [] });
|
||||
await listMedicineProducts('hh1', 'med-1', { cursor: 'cur1' });
|
||||
expect(mockGet).toHaveBeenCalledWith(expect.stringContaining('cursor=cur1'));
|
||||
});
|
||||
|
||||
it('createMedicineProduct calls POST', async () => {
|
||||
mockPost.mockResolvedValue({ _id: 'mp-1' });
|
||||
await createMedicineProduct('hh1', 'med-1', { brand: 'Bayer' } as never);
|
||||
expect(mockPost).toHaveBeenCalledWith('/households/hh1/medicines/med-1/products', {
|
||||
brand: 'Bayer',
|
||||
});
|
||||
});
|
||||
|
||||
it('updateMedicineProduct uses medicine-products path', async () => {
|
||||
mockPatch.mockResolvedValue({ _id: 'mp-1' });
|
||||
await updateMedicineProduct('hh1', 'mp-1', { brand: 'Updated' } as never);
|
||||
expect(mockPatch).toHaveBeenCalledWith('/households/hh1/medicine-products/mp-1', {
|
||||
brand: 'Updated',
|
||||
});
|
||||
});
|
||||
|
||||
it('deleteMedicineProduct calls DELETE', async () => {
|
||||
mockDelete.mockResolvedValue(undefined);
|
||||
await deleteMedicineProduct('hh1', 'mp-1');
|
||||
expect(mockDelete).toHaveBeenCalledWith('/households/hh1/medicine-products/mp-1');
|
||||
});
|
||||
});
|
||||
37
packages/web/tests/services/nutrition-targets.test.ts
Normal file
37
packages/web/tests/services/nutrition-targets.test.ts
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { apiClient } from '../../src/services/api-client';
|
||||
import * as NutritionTargetsService from '../../src/services/nutrition-targets';
|
||||
|
||||
vi.mock('../../src/services/api-client', () => ({
|
||||
apiClient: {
|
||||
get: vi.fn(),
|
||||
post: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
describe('nutrition-targets service', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('getActiveNutritionTarget', async () => {
|
||||
await NutritionTargetsService.getActiveNutritionTarget('hh1');
|
||||
expect(apiClient.get).toHaveBeenCalledWith('/households/hh1/nutrition-targets');
|
||||
});
|
||||
|
||||
it('getNutritionTargetHistory', async () => {
|
||||
await NutritionTargetsService.getNutritionTargetHistory('hh1');
|
||||
expect(apiClient.get).toHaveBeenCalledWith('/households/hh1/nutrition-targets/history');
|
||||
});
|
||||
|
||||
it('setNutritionTarget', async () => {
|
||||
const data = { calories: 2000 } as any;
|
||||
await NutritionTargetsService.setNutritionTarget('hh1', data);
|
||||
expect(apiClient.post).toHaveBeenCalledWith('/households/hh1/nutrition-targets', data);
|
||||
});
|
||||
|
||||
it('calculateTargetPreset', async () => {
|
||||
await NutritionTargetsService.calculateTargetPreset('hh1', 2500, 'gain');
|
||||
expect(apiClient.post).toHaveBeenCalledWith('/households/hh1/nutrition-targets/presets', { calories: 2500, strategy: 'gain' });
|
||||
});
|
||||
});
|
||||
62
packages/web/tests/services/organizer.test.ts
Normal file
62
packages/web/tests/services/organizer.test.ts
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
|
||||
const { mockGet, mockPost } = vi.hoisted(() => ({
|
||||
mockGet: vi.fn(),
|
||||
mockPost: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../../src/services/api-client', () => ({
|
||||
apiClient: { get: mockGet, post: mockPost },
|
||||
}));
|
||||
|
||||
import { listFills, getFill, previewFill, executeFill, undoFill } from '../../src/services/organizer';
|
||||
|
||||
beforeEach(() => vi.clearAllMocks());
|
||||
|
||||
describe('organizer service', () => {
|
||||
it('listFills with no query', async () => {
|
||||
mockGet.mockResolvedValue({ data: [] });
|
||||
await listFills('hh1');
|
||||
expect(mockGet).toHaveBeenCalledWith('/households/hh1/organizer/fills');
|
||||
});
|
||||
|
||||
it('listFills builds query string', async () => {
|
||||
mockGet.mockResolvedValue({ data: [] });
|
||||
await listFills('hh1', { regimenId: 'reg-1', status: 'active' });
|
||||
expect(mockGet).toHaveBeenCalledWith(expect.stringContaining('regimenId=reg-1'));
|
||||
expect(mockGet).toHaveBeenCalledWith(expect.stringContaining('status=active'));
|
||||
});
|
||||
|
||||
it('listFills with cursor and limit', async () => {
|
||||
mockGet.mockResolvedValue({ data: [] });
|
||||
await listFills('hh1', { cursor: 'cur1', limit: 5 });
|
||||
expect(mockGet).toHaveBeenCalledWith(expect.stringContaining('cursor=cur1'));
|
||||
expect(mockGet).toHaveBeenCalledWith(expect.stringContaining('limit=5'));
|
||||
});
|
||||
|
||||
it('getFill calls GET', async () => {
|
||||
mockGet.mockResolvedValue({ _id: 'f-1' });
|
||||
await getFill('hh1', 'f-1');
|
||||
expect(mockGet).toHaveBeenCalledWith('/households/hh1/organizer/fills/f-1');
|
||||
});
|
||||
|
||||
it('previewFill calls POST', async () => {
|
||||
mockPost.mockResolvedValue({});
|
||||
const data = { regimenId: 'reg-1' } as never;
|
||||
await previewFill('hh1', data);
|
||||
expect(mockPost).toHaveBeenCalledWith('/households/hh1/organizer/preview', data);
|
||||
});
|
||||
|
||||
it('executeFill calls POST', async () => {
|
||||
mockPost.mockResolvedValue({ _id: 'f-1' });
|
||||
const data = { regimenId: 'reg-1' } as never;
|
||||
await executeFill('hh1', data);
|
||||
expect(mockPost).toHaveBeenCalledWith('/households/hh1/organizer/fill', data);
|
||||
});
|
||||
|
||||
it('undoFill calls POST with empty body', async () => {
|
||||
mockPost.mockResolvedValue({ _id: 'f-1' });
|
||||
await undoFill('hh1', 'f-1');
|
||||
expect(mockPost).toHaveBeenCalledWith('/households/hh1/organizer/fills/f-1/undo', {});
|
||||
});
|
||||
});
|
||||
124
packages/web/tests/services/pantry.test.ts
Normal file
124
packages/web/tests/services/pantry.test.ts
Normal file
|
|
@ -0,0 +1,124 @@
|
|||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
|
||||
const { mockGet, mockPost, mockPatch, mockDelete } = vi.hoisted(() => ({
|
||||
mockGet: vi.fn(),
|
||||
mockPost: vi.fn(),
|
||||
mockPatch: vi.fn(),
|
||||
mockDelete: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../../src/services/api-client', () => ({
|
||||
apiClient: { get: mockGet, post: mockPost, patch: mockPatch, delete: mockDelete },
|
||||
}));
|
||||
|
||||
import {
|
||||
listPantryItems,
|
||||
getPantryItem,
|
||||
createPantryItem,
|
||||
updatePantryItem,
|
||||
transitionPantryItem,
|
||||
batchTransitionPantryItems,
|
||||
getExpiringSoon,
|
||||
getWasteStats,
|
||||
deletePantryItem,
|
||||
} from '../../src/services/pantry';
|
||||
|
||||
beforeEach(() => vi.clearAllMocks());
|
||||
|
||||
describe('pantry service', () => {
|
||||
it('listPantryItems with no query', async () => {
|
||||
mockGet.mockResolvedValue({ data: [] });
|
||||
await listPantryItems('hh1');
|
||||
expect(mockGet).toHaveBeenCalledWith('/households/hh1/pantry');
|
||||
});
|
||||
|
||||
it('listPantryItems builds query string', async () => {
|
||||
mockGet.mockResolvedValue({ data: [] });
|
||||
await listPantryItems('hh1', {
|
||||
storageLocation: 'fridge',
|
||||
status: 'sealed',
|
||||
urgency: 'urgent',
|
||||
productId: 'p1',
|
||||
limit: 10,
|
||||
});
|
||||
const url = mockGet.mock.calls[0][0] as string;
|
||||
expect(url).toContain('storageLocation=fridge');
|
||||
expect(url).toContain('status=sealed');
|
||||
expect(url).toContain('urgency=urgent');
|
||||
expect(url).toContain('productId=p1');
|
||||
expect(url).toContain('limit=10');
|
||||
});
|
||||
|
||||
it('listPantryItems with cursor', async () => {
|
||||
mockGet.mockResolvedValue({ data: [] });
|
||||
await listPantryItems('hh1', { cursor: 'cur1' });
|
||||
expect(mockGet).toHaveBeenCalledWith(expect.stringContaining('cursor=cur1'));
|
||||
});
|
||||
|
||||
it('getPantryItem calls GET', async () => {
|
||||
mockGet.mockResolvedValue({ _id: 'item1' });
|
||||
await getPantryItem('hh1', 'item1');
|
||||
expect(mockGet).toHaveBeenCalledWith('/households/hh1/pantry/item1');
|
||||
});
|
||||
|
||||
it('createPantryItem calls POST', async () => {
|
||||
mockPost.mockResolvedValue({ _id: 'item1' });
|
||||
const data = { productId: 'p1', storageLocation: 'fridge', quantity: 1, unit: 'piece' };
|
||||
await createPantryItem('hh1', data as never);
|
||||
expect(mockPost).toHaveBeenCalledWith('/households/hh1/pantry', data);
|
||||
});
|
||||
|
||||
it('updatePantryItem calls PATCH', async () => {
|
||||
mockPatch.mockResolvedValue({ _id: 'item1' });
|
||||
await updatePantryItem('hh1', 'item1', { quantity: 2 });
|
||||
expect(mockPatch).toHaveBeenCalledWith('/households/hh1/pantry/item1', { quantity: 2 });
|
||||
});
|
||||
|
||||
it('transitionPantryItem calls POST', async () => {
|
||||
mockPost.mockResolvedValue({ _id: 'item1' });
|
||||
await transitionPantryItem('hh1', 'item1', { status: 'opened' as never });
|
||||
expect(mockPost).toHaveBeenCalledWith('/households/hh1/pantry/item1/transition', {
|
||||
status: 'opened',
|
||||
});
|
||||
});
|
||||
|
||||
it('batchTransitionPantryItems calls POST', async () => {
|
||||
mockPost.mockResolvedValue({ transitioned: 2, failed: 0 });
|
||||
const data = { itemIds: ['a', 'b'], status: 'consumed' as const };
|
||||
await batchTransitionPantryItems('hh1', data as never);
|
||||
expect(mockPost).toHaveBeenCalledWith('/households/hh1/pantry/batch-transition', data);
|
||||
});
|
||||
|
||||
it('getExpiringSoon with no query', async () => {
|
||||
mockGet.mockResolvedValue({ data: [] });
|
||||
await getExpiringSoon('hh1');
|
||||
expect(mockGet).toHaveBeenCalledWith('/households/hh1/pantry/expiring-soon');
|
||||
});
|
||||
|
||||
it('getExpiringSoon builds query string', async () => {
|
||||
mockGet.mockResolvedValue({ data: [] });
|
||||
await getExpiringSoon('hh1', { days: 3, cursor: 'c1', limit: 5 });
|
||||
const url = mockGet.mock.calls[0][0] as string;
|
||||
expect(url).toContain('days=3');
|
||||
expect(url).toContain('cursor=c1');
|
||||
expect(url).toContain('limit=5');
|
||||
});
|
||||
|
||||
it('getWasteStats with no period', async () => {
|
||||
mockGet.mockResolvedValue({ wastePercentage: 10 });
|
||||
await getWasteStats('hh1');
|
||||
expect(mockGet).toHaveBeenCalledWith('/households/hh1/pantry/stats');
|
||||
});
|
||||
|
||||
it('getWasteStats with period', async () => {
|
||||
mockGet.mockResolvedValue({ wastePercentage: 10 });
|
||||
await getWasteStats('hh1', 'week');
|
||||
expect(mockGet).toHaveBeenCalledWith('/households/hh1/pantry/stats?period=week');
|
||||
});
|
||||
|
||||
it('deletePantryItem calls DELETE', async () => {
|
||||
mockDelete.mockResolvedValue(undefined);
|
||||
await deletePantryItem('hh1', 'item1');
|
||||
expect(mockDelete).toHaveBeenCalledWith('/households/hh1/pantry/item1');
|
||||
});
|
||||
});
|
||||
54
packages/web/tests/services/prices.test.ts
Normal file
54
packages/web/tests/services/prices.test.ts
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { apiClient } from '../../src/services/api-client';
|
||||
import * as PricesService from '../../src/services/prices';
|
||||
|
||||
vi.mock('../../src/services/api-client', () => ({
|
||||
apiClient: {
|
||||
get: vi.fn(),
|
||||
post: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
describe('prices service', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('recordPrice', async () => {
|
||||
const data = { productId: 'p1', price: 10 } as any;
|
||||
await PricesService.recordPrice('hh1', data);
|
||||
expect(apiClient.post).toHaveBeenCalledWith('/households/hh1/prices', data);
|
||||
});
|
||||
|
||||
it('recordBulkPrices', async () => {
|
||||
const data = { storeId: 's1', items: [] } as any;
|
||||
await PricesService.recordBulkPrices('hh1', data);
|
||||
expect(apiClient.post).toHaveBeenCalledWith('/households/hh1/prices/bulk', data);
|
||||
});
|
||||
|
||||
it('getPriceHistory with and without query', async () => {
|
||||
await PricesService.getPriceHistory('hh1', 'p1');
|
||||
expect(apiClient.get).toHaveBeenCalledWith('/households/hh1/prices/history/p1');
|
||||
|
||||
await PricesService.getPriceHistory('hh1', 'p1', {
|
||||
storeId: 's1',
|
||||
startDate: '2026-05-01',
|
||||
endDate: '2026-05-10',
|
||||
cursor: 'cur',
|
||||
limit: 10,
|
||||
});
|
||||
expect(apiClient.get).toHaveBeenCalledWith(
|
||||
'/households/hh1/prices/history/p1?storeId=s1&startDate=2026-05-01&endDate=2026-05-10&cursor=cur&limit=10'
|
||||
);
|
||||
});
|
||||
|
||||
it('compareStores', async () => {
|
||||
await PricesService.compareStores('hh1', 'p1');
|
||||
expect(apiClient.get).toHaveBeenCalledWith('/households/hh1/prices/compare/p1');
|
||||
});
|
||||
|
||||
it('getPriceAnalytics', async () => {
|
||||
await PricesService.getPriceAnalytics('hh1');
|
||||
expect(apiClient.get).toHaveBeenCalledWith('/households/hh1/prices/analytics');
|
||||
});
|
||||
});
|
||||
163
packages/web/tests/services/products.test.ts
Normal file
163
packages/web/tests/services/products.test.ts
Normal file
|
|
@ -0,0 +1,163 @@
|
|||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
|
||||
const { mockGet, mockPost, mockPatch, mockDelete } = vi.hoisted(() => ({
|
||||
mockGet: vi.fn(),
|
||||
mockPost: vi.fn(),
|
||||
mockPatch: vi.fn(),
|
||||
mockDelete: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../../src/services/api-client', () => ({
|
||||
apiClient: { get: mockGet, post: mockPost, patch: mockPatch, delete: mockDelete },
|
||||
}));
|
||||
|
||||
import {
|
||||
listProducts,
|
||||
getProduct,
|
||||
lookupBarcode,
|
||||
createProduct,
|
||||
updateProduct,
|
||||
deleteProduct,
|
||||
smartAddProduct,
|
||||
importProducts,
|
||||
} from '../../src/services/products';
|
||||
|
||||
beforeEach(() => vi.clearAllMocks());
|
||||
|
||||
describe('products service', () => {
|
||||
it('listProducts with no query', async () => {
|
||||
mockGet.mockResolvedValue({ data: [] });
|
||||
await listProducts('hh1');
|
||||
expect(mockGet).toHaveBeenCalledWith('/households/hh1/products');
|
||||
});
|
||||
|
||||
it('listProducts builds query string', async () => {
|
||||
mockGet.mockResolvedValue({ data: [] });
|
||||
await listProducts('hh1', {
|
||||
q: 'chicken',
|
||||
category: 'meat',
|
||||
tags: 'organic',
|
||||
barcode: '1234',
|
||||
cursor: 'cur1',
|
||||
limit: 10,
|
||||
});
|
||||
const url = mockGet.mock.calls[0][0] as string;
|
||||
expect(url).toContain('q=chicken');
|
||||
expect(url).toContain('category=meat');
|
||||
expect(url).toContain('tags=organic');
|
||||
expect(url).toContain('barcode=1234');
|
||||
expect(url).toContain('cursor=cur1');
|
||||
expect(url).toContain('limit=10');
|
||||
});
|
||||
|
||||
it('getProduct calls GET with correct path', async () => {
|
||||
mockGet.mockResolvedValue({ _id: 'p1' });
|
||||
await getProduct('hh1', 'p1');
|
||||
expect(mockGet).toHaveBeenCalledWith('/households/hh1/products/p1');
|
||||
});
|
||||
|
||||
it('lookupBarcode calls GET barcode endpoint', async () => {
|
||||
mockGet.mockResolvedValue({ _id: 'p1' });
|
||||
await lookupBarcode('hh1', '1234567890');
|
||||
expect(mockGet).toHaveBeenCalledWith('/households/hh1/products/barcode/1234567890');
|
||||
});
|
||||
|
||||
it('createProduct calls POST', async () => {
|
||||
mockPost.mockResolvedValue({ _id: 'p1' });
|
||||
const data = {
|
||||
name: 'Chicken',
|
||||
category: 'meat' as never,
|
||||
servingSize: 100,
|
||||
servingUnit: 'g' as never,
|
||||
nutrition: { calories: 165, protein: 31, carbs: 0, fat: 3.6 },
|
||||
tags: [],
|
||||
source: 'manual' as never,
|
||||
};
|
||||
await createProduct('hh1', data);
|
||||
expect(mockPost).toHaveBeenCalledWith('/households/hh1/products', data);
|
||||
});
|
||||
|
||||
it('updateProduct calls PATCH', async () => {
|
||||
mockPatch.mockResolvedValue({ _id: 'p1' });
|
||||
await updateProduct('hh1', 'p1', { name: 'Updated' });
|
||||
expect(mockPatch).toHaveBeenCalledWith('/households/hh1/products/p1', { name: 'Updated' });
|
||||
});
|
||||
|
||||
it('deleteProduct calls DELETE', async () => {
|
||||
mockDelete.mockResolvedValue(undefined);
|
||||
await deleteProduct('hh1', 'p1');
|
||||
expect(mockDelete).toHaveBeenCalledWith('/households/hh1/products/p1');
|
||||
});
|
||||
|
||||
it('smartAddProduct calls POST smart-add', async () => {
|
||||
mockPost.mockResolvedValue({ available: false, message: 'LLM not configured' });
|
||||
const result = await smartAddProduct('hh1', 'chicken breast');
|
||||
expect(mockPost).toHaveBeenCalledWith('/households/hh1/products/smart-add', {
|
||||
text: 'chicken breast',
|
||||
});
|
||||
expect(result.available).toBe(false);
|
||||
});
|
||||
|
||||
describe('importProducts', () => {
|
||||
const mockFetch = vi.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
vi.stubGlobal('fetch', mockFetch);
|
||||
});
|
||||
|
||||
it('uploads file via fetch and returns result', async () => {
|
||||
mockFetch.mockResolvedValue({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ imported: 3, skippedDuplicates: 1, errors: [] }),
|
||||
});
|
||||
|
||||
const file = new File(['data'], 'products.csv', { type: 'text/csv' });
|
||||
const result = await importProducts('hh1', file);
|
||||
|
||||
expect(result.imported).toBe(3);
|
||||
expect(result.skippedDuplicates).toBe(1);
|
||||
expect(mockFetch).toHaveBeenCalledWith(
|
||||
expect.stringContaining('/households/hh1/products/import'),
|
||||
expect.objectContaining({ method: 'POST' }),
|
||||
);
|
||||
});
|
||||
|
||||
it('throws on non-ok response with JSON body', async () => {
|
||||
mockFetch.mockResolvedValue({
|
||||
ok: false,
|
||||
status: 400,
|
||||
statusText: 'Bad Request',
|
||||
json: () => Promise.resolve({ message: 'File too large' }),
|
||||
});
|
||||
|
||||
const file = new File(['data'], 'products.csv', { type: 'text/csv' });
|
||||
await expect(importProducts('hh1', file)).rejects.toThrow('File too large');
|
||||
});
|
||||
|
||||
it('throws with status on non-ok response without JSON', async () => {
|
||||
mockFetch.mockResolvedValue({
|
||||
ok: false,
|
||||
status: 500,
|
||||
statusText: 'Internal Server Error',
|
||||
json: () => Promise.reject(new Error('not json')),
|
||||
});
|
||||
|
||||
const file = new File(['data'], 'products.csv', { type: 'text/csv' });
|
||||
await expect(importProducts('hh1', file)).rejects.toThrow(
|
||||
'Import failed: 500 Internal Server Error',
|
||||
);
|
||||
});
|
||||
|
||||
it('throws with status when JSON body has no message', async () => {
|
||||
mockFetch.mockResolvedValue({
|
||||
ok: false,
|
||||
status: 422,
|
||||
statusText: 'Unprocessable Entity',
|
||||
json: () => Promise.resolve({}),
|
||||
});
|
||||
|
||||
const file = new File(['data'], 'products.csv', { type: 'text/csv' });
|
||||
await expect(importProducts('hh1', file)).rejects.toThrow('Import failed: 422');
|
||||
});
|
||||
});
|
||||
});
|
||||
69
packages/web/tests/services/purchases.test.ts
Normal file
69
packages/web/tests/services/purchases.test.ts
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
|
||||
const { mockGet, mockPost, mockPatch, mockDelete } = vi.hoisted(() => ({
|
||||
mockGet: vi.fn(),
|
||||
mockPost: vi.fn(),
|
||||
mockPatch: vi.fn(),
|
||||
mockDelete: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../../src/services/api-client', () => ({
|
||||
apiClient: { get: mockGet, post: mockPost, patch: mockPatch, delete: mockDelete },
|
||||
}));
|
||||
|
||||
import {
|
||||
listPurchases,
|
||||
getPurchase,
|
||||
createPurchase,
|
||||
updatePurchase,
|
||||
receivePurchase,
|
||||
deletePurchase,
|
||||
} from '../../src/services/purchases';
|
||||
|
||||
beforeEach(() => vi.clearAllMocks());
|
||||
|
||||
describe('purchases service', () => {
|
||||
it('listPurchases with no query', async () => {
|
||||
mockGet.mockResolvedValue({ data: [] });
|
||||
await listPurchases('hh1');
|
||||
expect(mockGet).toHaveBeenCalledWith('/households/hh1/purchases');
|
||||
});
|
||||
|
||||
it('listPurchases builds query string', async () => {
|
||||
mockGet.mockResolvedValue({ data: [] });
|
||||
await listPurchases('hh1', { status: 'ordered', storeId: 'st-1', limit: 10 });
|
||||
expect(mockGet).toHaveBeenCalledWith(expect.stringContaining('status=ordered'));
|
||||
expect(mockGet).toHaveBeenCalledWith(expect.stringContaining('storeId=st-1'));
|
||||
});
|
||||
|
||||
it('getPurchase calls GET', async () => {
|
||||
mockGet.mockResolvedValue({ _id: 'p-1' });
|
||||
await getPurchase('hh1', 'p-1');
|
||||
expect(mockGet).toHaveBeenCalledWith('/households/hh1/purchases/p-1');
|
||||
});
|
||||
|
||||
it('createPurchase calls POST', async () => {
|
||||
mockPost.mockResolvedValue({ _id: 'p-1' });
|
||||
const data = { storeId: 'st-1', items: [] } as never;
|
||||
await createPurchase('hh1', data);
|
||||
expect(mockPost).toHaveBeenCalledWith('/households/hh1/purchases', data);
|
||||
});
|
||||
|
||||
it('updatePurchase calls PATCH', async () => {
|
||||
mockPatch.mockResolvedValue({ _id: 'p-1' });
|
||||
await updatePurchase('hh1', 'p-1', { notes: 'updated' } as never);
|
||||
expect(mockPatch).toHaveBeenCalledWith('/households/hh1/purchases/p-1', { notes: 'updated' });
|
||||
});
|
||||
|
||||
it('receivePurchase calls POST with empty body', async () => {
|
||||
mockPost.mockResolvedValue({ addedCount: 1, priceRecordsCreated: 0 });
|
||||
await receivePurchase('hh1', 'p-1');
|
||||
expect(mockPost).toHaveBeenCalledWith('/households/hh1/purchases/p-1/receive', {});
|
||||
});
|
||||
|
||||
it('deletePurchase calls DELETE', async () => {
|
||||
mockDelete.mockResolvedValue({ _id: 'p-1' });
|
||||
await deletePurchase('hh1', 'p-1');
|
||||
expect(mockDelete).toHaveBeenCalledWith('/households/hh1/purchases/p-1');
|
||||
});
|
||||
});
|
||||
118
packages/web/tests/services/recipes.test.ts
Normal file
118
packages/web/tests/services/recipes.test.ts
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
|
||||
const { mockGet, mockPost, mockPatch, mockDelete } = vi.hoisted(() => ({
|
||||
mockGet: vi.fn(),
|
||||
mockPost: vi.fn(),
|
||||
mockPatch: vi.fn(),
|
||||
mockDelete: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../../src/services/api-client', () => ({
|
||||
apiClient: { get: mockGet, post: mockPost, patch: mockPatch, delete: mockDelete },
|
||||
}));
|
||||
|
||||
import {
|
||||
listRecipes,
|
||||
getRecipe,
|
||||
createRecipe,
|
||||
updateRecipe,
|
||||
deleteRecipe,
|
||||
scaleRecipe,
|
||||
importRecipeFromText,
|
||||
importRecipeFromUrl,
|
||||
listRecipesByProduct,
|
||||
} from '../../src/services/recipes';
|
||||
|
||||
beforeEach(() => vi.clearAllMocks());
|
||||
|
||||
describe('recipes service', () => {
|
||||
it('listRecipes with no query', async () => {
|
||||
mockGet.mockResolvedValue({ data: [] });
|
||||
await listRecipes('hh1');
|
||||
expect(mockGet).toHaveBeenCalledWith('/households/hh1/recipes');
|
||||
});
|
||||
|
||||
it('listRecipes builds query string', async () => {
|
||||
mockGet.mockResolvedValue({ data: [] });
|
||||
await listRecipes('hh1', {
|
||||
q: 'pasta',
|
||||
tags: 'italian',
|
||||
cuisine: 'Italian',
|
||||
maxCalories: 500,
|
||||
isFavorite: true,
|
||||
cursor: 'cur1',
|
||||
limit: 10,
|
||||
});
|
||||
const url = mockGet.mock.calls[0][0] as string;
|
||||
expect(url).toContain('q=pasta');
|
||||
expect(url).toContain('tags=italian');
|
||||
expect(url).toContain('cuisine=Italian');
|
||||
expect(url).toContain('maxCalories=500');
|
||||
expect(url).toContain('isFavorite=true');
|
||||
expect(url).toContain('cursor=cur1');
|
||||
expect(url).toContain('limit=10');
|
||||
});
|
||||
|
||||
it('getRecipe calls GET', async () => {
|
||||
mockGet.mockResolvedValue({ _id: 'r1' });
|
||||
await getRecipe('hh1', 'r1');
|
||||
expect(mockGet).toHaveBeenCalledWith('/households/hh1/recipes/r1');
|
||||
});
|
||||
|
||||
it('createRecipe calls POST', async () => {
|
||||
mockPost.mockResolvedValue({ _id: 'r1' });
|
||||
const data = { name: 'Pasta' };
|
||||
await createRecipe('hh1', data as never);
|
||||
expect(mockPost).toHaveBeenCalledWith('/households/hh1/recipes', data);
|
||||
});
|
||||
|
||||
it('updateRecipe calls PATCH', async () => {
|
||||
mockPatch.mockResolvedValue({ _id: 'r1' });
|
||||
await updateRecipe('hh1', 'r1', { name: 'Updated' } as never);
|
||||
expect(mockPatch).toHaveBeenCalledWith('/households/hh1/recipes/r1', { name: 'Updated' });
|
||||
});
|
||||
|
||||
it('deleteRecipe calls DELETE', async () => {
|
||||
mockDelete.mockResolvedValue(undefined);
|
||||
await deleteRecipe('hh1', 'r1');
|
||||
expect(mockDelete).toHaveBeenCalledWith('/households/hh1/recipes/r1');
|
||||
});
|
||||
|
||||
it('scaleRecipe calls POST', async () => {
|
||||
mockPost.mockResolvedValue({ _id: 'r1' });
|
||||
await scaleRecipe('hh1', 'r1', { targetServings: 8 });
|
||||
expect(mockPost).toHaveBeenCalledWith('/households/hh1/recipes/r1/scale', {
|
||||
targetServings: 8,
|
||||
});
|
||||
});
|
||||
|
||||
it('importRecipeFromText calls POST', async () => {
|
||||
mockPost.mockResolvedValue({ available: false });
|
||||
await importRecipeFromText('hh1', { text: 'recipe text' } as never);
|
||||
expect(mockPost).toHaveBeenCalledWith('/households/hh1/recipes/import-text', {
|
||||
text: 'recipe text',
|
||||
});
|
||||
});
|
||||
|
||||
it('importRecipeFromUrl calls POST', async () => {
|
||||
mockPost.mockResolvedValue({ available: false });
|
||||
await importRecipeFromUrl('hh1', { url: 'http://example.com' } as never);
|
||||
expect(mockPost).toHaveBeenCalledWith('/households/hh1/recipes/import-url', {
|
||||
url: 'http://example.com',
|
||||
});
|
||||
});
|
||||
|
||||
it('listRecipesByProduct with no query', async () => {
|
||||
mockGet.mockResolvedValue({ data: [] });
|
||||
await listRecipesByProduct('hh1', 'p1');
|
||||
expect(mockGet).toHaveBeenCalledWith('/households/hh1/recipes/by-product/p1');
|
||||
});
|
||||
|
||||
it('listRecipesByProduct with query', async () => {
|
||||
mockGet.mockResolvedValue({ data: [] });
|
||||
await listRecipesByProduct('hh1', 'p1', { cursor: 'c1', limit: 5 });
|
||||
const url = mockGet.mock.calls[0][0] as string;
|
||||
expect(url).toContain('cursor=c1');
|
||||
expect(url).toContain('limit=5');
|
||||
});
|
||||
});
|
||||
86
packages/web/tests/services/refills.test.ts
Normal file
86
packages/web/tests/services/refills.test.ts
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
|
||||
const { mockGet, mockPost, mockPatch } = vi.hoisted(() => ({
|
||||
mockGet: vi.fn(),
|
||||
mockPost: vi.fn(),
|
||||
mockPatch: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../../src/services/api-client', () => ({
|
||||
apiClient: { get: mockGet, post: mockPost, patch: mockPatch },
|
||||
}));
|
||||
|
||||
import {
|
||||
getRefillAlerts,
|
||||
listRefillLists,
|
||||
createRefillList,
|
||||
getRefillList,
|
||||
updateRefillList,
|
||||
updateRefillListItem,
|
||||
addToCabinet,
|
||||
} from '../../src/services/refills';
|
||||
|
||||
beforeEach(() => vi.clearAllMocks());
|
||||
|
||||
describe('refills service', () => {
|
||||
it('getRefillAlerts with no query', async () => {
|
||||
mockGet.mockResolvedValue({ data: [] });
|
||||
await getRefillAlerts('hh1');
|
||||
expect(mockGet).toHaveBeenCalledWith('/households/hh1/refills/alerts');
|
||||
});
|
||||
|
||||
it('getRefillAlerts builds query string', async () => {
|
||||
mockGet.mockResolvedValue({ data: [] });
|
||||
await getRefillAlerts('hh1', { thresholdDays: 14, userId: 'u-1' });
|
||||
expect(mockGet).toHaveBeenCalledWith(expect.stringContaining('thresholdDays=14'));
|
||||
expect(mockGet).toHaveBeenCalledWith(expect.stringContaining('userId=u-1'));
|
||||
});
|
||||
|
||||
it('listRefillLists with query', async () => {
|
||||
mockGet.mockResolvedValue({ data: [] });
|
||||
await listRefillLists('hh1', { status: 'active' });
|
||||
expect(mockGet).toHaveBeenCalledWith(expect.stringContaining('status=active'));
|
||||
});
|
||||
|
||||
it('listRefillLists with cursor and limit', async () => {
|
||||
mockGet.mockResolvedValue({ data: [] });
|
||||
await listRefillLists('hh1', { cursor: 'cur1', limit: 25 });
|
||||
expect(mockGet).toHaveBeenCalledWith(expect.stringContaining('cursor=cur1'));
|
||||
expect(mockGet).toHaveBeenCalledWith(expect.stringContaining('limit=25'));
|
||||
});
|
||||
|
||||
it('createRefillList calls POST', async () => {
|
||||
mockPost.mockResolvedValue({ _id: 'rl-1' });
|
||||
const data = { name: 'Weekly' } as never;
|
||||
await createRefillList('hh1', data);
|
||||
expect(mockPost).toHaveBeenCalledWith('/households/hh1/refills/lists', data);
|
||||
});
|
||||
|
||||
it('getRefillList calls GET', async () => {
|
||||
mockGet.mockResolvedValue({ _id: 'rl-1' });
|
||||
await getRefillList('hh1', 'rl-1');
|
||||
expect(mockGet).toHaveBeenCalledWith('/households/hh1/refills/lists/rl-1');
|
||||
});
|
||||
|
||||
it('updateRefillList calls PATCH', async () => {
|
||||
mockPatch.mockResolvedValue({ _id: 'rl-1' });
|
||||
await updateRefillList('hh1', 'rl-1', { name: 'Updated' } as never);
|
||||
expect(mockPatch).toHaveBeenCalledWith('/households/hh1/refills/lists/rl-1', {
|
||||
name: 'Updated',
|
||||
});
|
||||
});
|
||||
|
||||
it('updateRefillListItem calls PATCH with nested path', async () => {
|
||||
mockPatch.mockResolvedValue({ _id: 'rl-1' });
|
||||
await updateRefillListItem('hh1', 'rl-1', 'item-1', { purchased: true } as never);
|
||||
expect(mockPatch).toHaveBeenCalledWith('/households/hh1/refills/lists/rl-1/items/item-1', {
|
||||
purchased: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('addToCabinet calls POST with empty body', async () => {
|
||||
mockPost.mockResolvedValue({});
|
||||
await addToCabinet('hh1', 'rl-1');
|
||||
expect(mockPost).toHaveBeenCalledWith('/households/hh1/refills/lists/rl-1/add-to-cabinet', {});
|
||||
});
|
||||
});
|
||||
75
packages/web/tests/services/regimens.test.ts
Normal file
75
packages/web/tests/services/regimens.test.ts
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
|
||||
const { mockGet, mockPost, mockPatch, mockDelete } = vi.hoisted(() => ({
|
||||
mockGet: vi.fn(),
|
||||
mockPost: vi.fn(),
|
||||
mockPatch: vi.fn(),
|
||||
mockDelete: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../../src/services/api-client', () => ({
|
||||
apiClient: { get: mockGet, post: mockPost, patch: mockPatch, delete: mockDelete },
|
||||
}));
|
||||
|
||||
import {
|
||||
listRegimens,
|
||||
getRegimen,
|
||||
getBurnRates,
|
||||
createRegimen,
|
||||
updateRegimen,
|
||||
deleteRegimen,
|
||||
} from '../../src/services/regimens';
|
||||
|
||||
beforeEach(() => vi.clearAllMocks());
|
||||
|
||||
describe('regimens service', () => {
|
||||
it('listRegimens with no query', async () => {
|
||||
mockGet.mockResolvedValue({ data: [] });
|
||||
await listRegimens('hh1');
|
||||
expect(mockGet).toHaveBeenCalledWith('/households/hh1/regimens');
|
||||
});
|
||||
|
||||
it('listRegimens builds query string with isActive', async () => {
|
||||
mockGet.mockResolvedValue({ data: [] });
|
||||
await listRegimens('hh1', { isActive: true, limit: 10 });
|
||||
expect(mockGet).toHaveBeenCalledWith(expect.stringContaining('isActive=true'));
|
||||
expect(mockGet).toHaveBeenCalledWith(expect.stringContaining('limit=10'));
|
||||
});
|
||||
|
||||
it('listRegimens with cursor', async () => {
|
||||
mockGet.mockResolvedValue({ data: [] });
|
||||
await listRegimens('hh1', { cursor: 'cur1' });
|
||||
expect(mockGet).toHaveBeenCalledWith(expect.stringContaining('cursor=cur1'));
|
||||
});
|
||||
|
||||
it('getRegimen calls GET', async () => {
|
||||
mockGet.mockResolvedValue({ _id: 'reg-1' });
|
||||
await getRegimen('hh1', 'reg-1');
|
||||
expect(mockGet).toHaveBeenCalledWith('/households/hh1/regimens/reg-1');
|
||||
});
|
||||
|
||||
it('getBurnRates calls GET', async () => {
|
||||
mockGet.mockResolvedValue({});
|
||||
await getBurnRates('hh1');
|
||||
expect(mockGet).toHaveBeenCalledWith('/households/hh1/regimens/burn-rate');
|
||||
});
|
||||
|
||||
it('createRegimen calls POST', async () => {
|
||||
mockPost.mockResolvedValue({ _id: 'reg-1' });
|
||||
const data = { name: 'Morning' } as never;
|
||||
await createRegimen('hh1', data);
|
||||
expect(mockPost).toHaveBeenCalledWith('/households/hh1/regimens', data);
|
||||
});
|
||||
|
||||
it('updateRegimen calls PATCH', async () => {
|
||||
mockPatch.mockResolvedValue({ _id: 'reg-1' });
|
||||
await updateRegimen('hh1', 'reg-1', { name: 'Updated' } as never);
|
||||
expect(mockPatch).toHaveBeenCalledWith('/households/hh1/regimens/reg-1', { name: 'Updated' });
|
||||
});
|
||||
|
||||
it('deleteRegimen calls DELETE', async () => {
|
||||
mockDelete.mockResolvedValue(undefined);
|
||||
await deleteRegimen('hh1', 'reg-1');
|
||||
expect(mockDelete).toHaveBeenCalledWith('/households/hh1/regimens/reg-1');
|
||||
});
|
||||
});
|
||||
87
packages/web/tests/services/shopping-lists.test.ts
Normal file
87
packages/web/tests/services/shopping-lists.test.ts
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { apiClient } from '../../src/services/api-client';
|
||||
import * as ShoppingListsService from '../../src/services/shopping-lists';
|
||||
|
||||
vi.mock('../../src/services/api-client', () => ({
|
||||
apiClient: {
|
||||
get: vi.fn(),
|
||||
post: vi.fn(),
|
||||
patch: vi.fn(),
|
||||
delete: vi.fn(),
|
||||
baseUrl: 'http://localhost:3001',
|
||||
},
|
||||
}));
|
||||
|
||||
describe('shopping-lists service', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('getShoppingLists', async () => {
|
||||
await ShoppingListsService.getShoppingLists('hh1');
|
||||
expect(apiClient.get).toHaveBeenCalledWith('/households/hh1/shopping-lists');
|
||||
});
|
||||
|
||||
it('getShoppingList', async () => {
|
||||
await ShoppingListsService.getShoppingList('hh1', 'list1');
|
||||
expect(apiClient.get).toHaveBeenCalledWith('/households/hh1/shopping-lists/list1');
|
||||
});
|
||||
|
||||
it('createShoppingList', async () => {
|
||||
const data = { name: 'Test' } as any;
|
||||
await ShoppingListsService.createShoppingList('hh1', data);
|
||||
expect(apiClient.post).toHaveBeenCalledWith('/households/hh1/shopping-lists', data);
|
||||
});
|
||||
|
||||
it('updateShoppingList', async () => {
|
||||
const data = { name: 'Updated' } as any;
|
||||
await ShoppingListsService.updateShoppingList('hh1', 'list1', data);
|
||||
expect(apiClient.patch).toHaveBeenCalledWith('/households/hh1/shopping-lists/list1', data);
|
||||
});
|
||||
|
||||
it('deleteShoppingList', async () => {
|
||||
await ShoppingListsService.deleteShoppingList('hh1', 'list1');
|
||||
expect(apiClient.delete).toHaveBeenCalledWith('/households/hh1/shopping-lists/list1');
|
||||
});
|
||||
|
||||
it('addShoppingItem', async () => {
|
||||
const data = { productId: 'p1' } as any;
|
||||
await ShoppingListsService.addShoppingItem('hh1', 'list1', data);
|
||||
expect(apiClient.post).toHaveBeenCalledWith('/households/hh1/shopping-lists/list1/items', data);
|
||||
});
|
||||
|
||||
it('updateShoppingItem', async () => {
|
||||
const data = { checked: true } as any;
|
||||
await ShoppingListsService.updateShoppingItem('hh1', 'list1', 'item1', data);
|
||||
expect(apiClient.patch).toHaveBeenCalledWith('/households/hh1/shopping-lists/list1/items/item1', data);
|
||||
});
|
||||
|
||||
it('removeShoppingItem', async () => {
|
||||
await ShoppingListsService.removeShoppingItem('hh1', 'list1', 'item1');
|
||||
expect(apiClient.delete).toHaveBeenCalledWith('/households/hh1/shopping-lists/list1/items/item1');
|
||||
});
|
||||
|
||||
it('generateFromMealPlan', async () => {
|
||||
await ShoppingListsService.generateFromMealPlan('hh1', 'mp1');
|
||||
expect(apiClient.post).toHaveBeenCalledWith('/households/hh1/shopping-lists/from-meal-plan/mp1');
|
||||
});
|
||||
|
||||
it('syncToPantry', async () => {
|
||||
await ShoppingListsService.syncToPantry('hh1', 'list1');
|
||||
expect(apiClient.post).toHaveBeenCalledWith('/households/hh1/shopping-lists/list1/sync-to-pantry');
|
||||
});
|
||||
|
||||
it('getBasketStoreComparison', async () => {
|
||||
await ShoppingListsService.getBasketStoreComparison('hh1', 'list1');
|
||||
expect(apiClient.get).toHaveBeenCalledWith('/households/hh1/shopping-lists/list1/stores');
|
||||
});
|
||||
|
||||
it('getShoppingListSyncSocketUrl handles insecure and secure contexts', () => {
|
||||
const urlInsecure = ShoppingListsService.getShoppingListSyncSocketUrl('hh1', 'list1');
|
||||
expect(urlInsecure).toBe('ws://localhost:3001/households/hh1/shopping-lists/list1/sync');
|
||||
|
||||
apiClient.baseUrl = 'https://api.meshitrack.com';
|
||||
const urlSecure = ShoppingListsService.getShoppingListSyncSocketUrl('hh1', 'list1');
|
||||
expect(urlSecure).toBe('wss://api.meshitrack.com/households/hh1/shopping-lists/list1/sync');
|
||||
});
|
||||
});
|
||||
56
packages/web/tests/services/stores.test.ts
Normal file
56
packages/web/tests/services/stores.test.ts
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
|
||||
const { mockGet, mockPost, mockPatch, mockDelete } = vi.hoisted(() => ({
|
||||
mockGet: vi.fn(),
|
||||
mockPost: vi.fn(),
|
||||
mockPatch: vi.fn(),
|
||||
mockDelete: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../../src/services/api-client', () => ({
|
||||
apiClient: { get: mockGet, post: mockPost, patch: mockPatch, delete: mockDelete },
|
||||
}));
|
||||
|
||||
import { listStores, getStore, createStore, updateStore, deactivateStore } from '../../src/services/stores';
|
||||
|
||||
beforeEach(() => vi.clearAllMocks());
|
||||
|
||||
describe('stores service', () => {
|
||||
it('listStores calls GET with no query', async () => {
|
||||
mockGet.mockResolvedValue({ data: [] });
|
||||
await listStores('hh1');
|
||||
expect(mockGet).toHaveBeenCalledWith('/households/hh1/stores');
|
||||
});
|
||||
|
||||
it('listStores builds query string', async () => {
|
||||
mockGet.mockResolvedValue({ data: [] });
|
||||
await listStores('hh1', { tags: 'pharmacy', search: 'cvs', cursor: 'c1', limit: 5 });
|
||||
expect(mockGet).toHaveBeenCalledWith(expect.stringContaining('tags=pharmacy'));
|
||||
expect(mockGet).toHaveBeenCalledWith(expect.stringContaining('search=cvs'));
|
||||
expect(mockGet).toHaveBeenCalledWith(expect.stringContaining('limit=5'));
|
||||
});
|
||||
|
||||
it('getStore calls GET with id', async () => {
|
||||
mockGet.mockResolvedValue({ _id: 'st-1' });
|
||||
await getStore('hh1', 'st-1');
|
||||
expect(mockGet).toHaveBeenCalledWith('/households/hh1/stores/st-1');
|
||||
});
|
||||
|
||||
it('createStore calls POST', async () => {
|
||||
mockPost.mockResolvedValue({ _id: 'st-1' });
|
||||
await createStore('hh1', { name: 'CVS' } as never);
|
||||
expect(mockPost).toHaveBeenCalledWith('/households/hh1/stores', { name: 'CVS' });
|
||||
});
|
||||
|
||||
it('updateStore calls PATCH', async () => {
|
||||
mockPatch.mockResolvedValue({ _id: 'st-1' });
|
||||
await updateStore('hh1', 'st-1', { name: 'Updated' } as never);
|
||||
expect(mockPatch).toHaveBeenCalledWith('/households/hh1/stores/st-1', { name: 'Updated' });
|
||||
});
|
||||
|
||||
it('deactivateStore calls DELETE', async () => {
|
||||
mockDelete.mockResolvedValue({ _id: 'st-1' });
|
||||
await deactivateStore('hh1', 'st-1');
|
||||
expect(mockDelete).toHaveBeenCalledWith('/households/hh1/stores/st-1');
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue