Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ff912b9055 | ||
| ec32027bd4 | |||
|
|
59f773639b | ||
| dd2be5eecf | |||
|
|
a94bfbd3e9 | ||
| 338bbc9440 | |||
|
|
60aad04642 | ||
| 7f2aff9a24 |
@@ -54,7 +54,9 @@
|
||||
"mcp__memory__create_entities",
|
||||
"mcp__memory__search_nodes",
|
||||
"mcp__memory__delete_entities",
|
||||
"mcp__sequential-thinking__sequentialthinking"
|
||||
"mcp__sequential-thinking__sequentialthinking",
|
||||
"mcp__filesystem__list_directory",
|
||||
"mcp__filesystem__read_multiple_files"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "flyer-crawler",
|
||||
"version": "0.9.63",
|
||||
"version": "0.9.67",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "flyer-crawler",
|
||||
"version": "0.9.63",
|
||||
"version": "0.9.67",
|
||||
"dependencies": {
|
||||
"@bull-board/api": "^6.14.2",
|
||||
"@bull-board/express": "^6.14.2",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "flyer-crawler",
|
||||
"private": true,
|
||||
"version": "0.9.63",
|
||||
"version": "0.9.67",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "concurrently \"npm:start:dev\" \"vite\"",
|
||||
|
||||
128
src/hooks/mutations/useAddShoppingListItemMutation.test.tsx
Normal file
128
src/hooks/mutations/useAddShoppingListItemMutation.test.tsx
Normal file
@@ -0,0 +1,128 @@
|
||||
// src/hooks/mutations/useAddShoppingListItemMutation.test.tsx
|
||||
import { renderHook, waitFor } from '@testing-library/react';
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import type { ReactNode } from 'react';
|
||||
import { useAddShoppingListItemMutation } from './useAddShoppingListItemMutation';
|
||||
import * as apiClient from '../../services/apiClient';
|
||||
import * as notificationService from '../../services/notificationService';
|
||||
|
||||
vi.mock('../../services/apiClient');
|
||||
vi.mock('../../services/notificationService');
|
||||
|
||||
const mockedApiClient = vi.mocked(apiClient);
|
||||
const mockedNotifications = vi.mocked(notificationService);
|
||||
|
||||
describe('useAddShoppingListItemMutation', () => {
|
||||
let queryClient: QueryClient;
|
||||
|
||||
const wrapper = ({ children }: { children: ReactNode }) => (
|
||||
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
||||
);
|
||||
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks();
|
||||
queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: { retry: false },
|
||||
mutations: { retry: false },
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should add a master item to shopping list successfully', async () => {
|
||||
const mockResponse = { shopping_list_item_id: 1, master_item_id: 42 };
|
||||
mockedApiClient.addShoppingListItem.mockResolvedValue({
|
||||
ok: true,
|
||||
json: () => Promise.resolve(mockResponse),
|
||||
} as Response);
|
||||
|
||||
const { result } = renderHook(() => useAddShoppingListItemMutation(), { wrapper });
|
||||
|
||||
result.current.mutate({ listId: 1, item: { masterItemId: 42 } });
|
||||
|
||||
await waitFor(() => expect(result.current.isSuccess).toBe(true));
|
||||
|
||||
expect(mockedApiClient.addShoppingListItem).toHaveBeenCalledWith(1, { masterItemId: 42 });
|
||||
expect(mockedNotifications.notifySuccess).toHaveBeenCalledWith('Item added to shopping list');
|
||||
});
|
||||
|
||||
it('should add a custom item to shopping list successfully', async () => {
|
||||
const mockResponse = { shopping_list_item_id: 2, custom_item_name: 'Special Milk' };
|
||||
mockedApiClient.addShoppingListItem.mockResolvedValue({
|
||||
ok: true,
|
||||
json: () => Promise.resolve(mockResponse),
|
||||
} as Response);
|
||||
|
||||
const { result } = renderHook(() => useAddShoppingListItemMutation(), { wrapper });
|
||||
|
||||
result.current.mutate({ listId: 1, item: { customItemName: 'Special Milk' } });
|
||||
|
||||
await waitFor(() => expect(result.current.isSuccess).toBe(true));
|
||||
|
||||
expect(mockedApiClient.addShoppingListItem).toHaveBeenCalledWith(1, { customItemName: 'Special Milk' });
|
||||
});
|
||||
|
||||
it('should invalidate shopping-lists query on success', async () => {
|
||||
mockedApiClient.addShoppingListItem.mockResolvedValue({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({}),
|
||||
} as Response);
|
||||
|
||||
const invalidateQueriesSpy = vi.spyOn(queryClient, 'invalidateQueries');
|
||||
|
||||
const { result } = renderHook(() => useAddShoppingListItemMutation(), { wrapper });
|
||||
|
||||
result.current.mutate({ listId: 1, item: { masterItemId: 42 } });
|
||||
|
||||
await waitFor(() => expect(result.current.isSuccess).toBe(true));
|
||||
|
||||
expect(invalidateQueriesSpy).toHaveBeenCalledWith({ queryKey: ['shopping-lists'] });
|
||||
});
|
||||
|
||||
it('should handle API error with error message', async () => {
|
||||
mockedApiClient.addShoppingListItem.mockResolvedValue({
|
||||
ok: false,
|
||||
status: 400,
|
||||
json: () => Promise.resolve({ message: 'Item already exists' }),
|
||||
} as Response);
|
||||
|
||||
const { result } = renderHook(() => useAddShoppingListItemMutation(), { wrapper });
|
||||
|
||||
result.current.mutate({ listId: 1, item: { masterItemId: 42 } });
|
||||
|
||||
await waitFor(() => expect(result.current.isError).toBe(true));
|
||||
|
||||
expect(result.current.error?.message).toBe('Item already exists');
|
||||
expect(mockedNotifications.notifyError).toHaveBeenCalledWith('Item already exists');
|
||||
});
|
||||
|
||||
it('should handle API error without message', async () => {
|
||||
mockedApiClient.addShoppingListItem.mockResolvedValue({
|
||||
ok: false,
|
||||
status: 500,
|
||||
json: () => Promise.reject(new Error('Parse error')),
|
||||
} as Response);
|
||||
|
||||
const { result } = renderHook(() => useAddShoppingListItemMutation(), { wrapper });
|
||||
|
||||
result.current.mutate({ listId: 1, item: { masterItemId: 42 } });
|
||||
|
||||
await waitFor(() => expect(result.current.isError).toBe(true));
|
||||
|
||||
expect(result.current.error?.message).toBe('Request failed with status 500');
|
||||
expect(mockedNotifications.notifyError).toHaveBeenCalledWith('Request failed with status 500');
|
||||
});
|
||||
|
||||
it('should handle network error', async () => {
|
||||
mockedApiClient.addShoppingListItem.mockRejectedValue(new Error('Network error'));
|
||||
|
||||
const { result } = renderHook(() => useAddShoppingListItemMutation(), { wrapper });
|
||||
|
||||
result.current.mutate({ listId: 1, item: { masterItemId: 42 } });
|
||||
|
||||
await waitFor(() => expect(result.current.isError).toBe(true));
|
||||
|
||||
expect(result.current.error?.message).toBe('Network error');
|
||||
});
|
||||
});
|
||||
115
src/hooks/mutations/useAddWatchedItemMutation.test.tsx
Normal file
115
src/hooks/mutations/useAddWatchedItemMutation.test.tsx
Normal file
@@ -0,0 +1,115 @@
|
||||
// src/hooks/mutations/useAddWatchedItemMutation.test.tsx
|
||||
import { renderHook, waitFor } from '@testing-library/react';
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import type { ReactNode } from 'react';
|
||||
import { useAddWatchedItemMutation } from './useAddWatchedItemMutation';
|
||||
import * as apiClient from '../../services/apiClient';
|
||||
import * as notificationService from '../../services/notificationService';
|
||||
|
||||
vi.mock('../../services/apiClient');
|
||||
vi.mock('../../services/notificationService');
|
||||
|
||||
const mockedApiClient = vi.mocked(apiClient);
|
||||
const mockedNotifications = vi.mocked(notificationService);
|
||||
|
||||
describe('useAddWatchedItemMutation', () => {
|
||||
let queryClient: QueryClient;
|
||||
|
||||
const wrapper = ({ children }: { children: ReactNode }) => (
|
||||
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
||||
);
|
||||
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks();
|
||||
queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: { retry: false },
|
||||
mutations: { retry: false },
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should add a watched item successfully with category', async () => {
|
||||
const mockResponse = { id: 1, item_name: 'Milk', category: 'Dairy' };
|
||||
mockedApiClient.addWatchedItem.mockResolvedValue({
|
||||
ok: true,
|
||||
json: () => Promise.resolve(mockResponse),
|
||||
} as Response);
|
||||
|
||||
const { result } = renderHook(() => useAddWatchedItemMutation(), { wrapper });
|
||||
|
||||
result.current.mutate({ itemName: 'Milk', category: 'Dairy' });
|
||||
|
||||
await waitFor(() => expect(result.current.isSuccess).toBe(true));
|
||||
|
||||
expect(mockedApiClient.addWatchedItem).toHaveBeenCalledWith('Milk', 'Dairy');
|
||||
expect(mockedNotifications.notifySuccess).toHaveBeenCalledWith('Item added to watched list');
|
||||
});
|
||||
|
||||
it('should add a watched item without category', async () => {
|
||||
const mockResponse = { id: 1, item_name: 'Bread' };
|
||||
mockedApiClient.addWatchedItem.mockResolvedValue({
|
||||
ok: true,
|
||||
json: () => Promise.resolve(mockResponse),
|
||||
} as Response);
|
||||
|
||||
const { result } = renderHook(() => useAddWatchedItemMutation(), { wrapper });
|
||||
|
||||
result.current.mutate({ itemName: 'Bread' });
|
||||
|
||||
await waitFor(() => expect(result.current.isSuccess).toBe(true));
|
||||
|
||||
expect(mockedApiClient.addWatchedItem).toHaveBeenCalledWith('Bread', '');
|
||||
});
|
||||
|
||||
it('should invalidate watched-items query on success', async () => {
|
||||
mockedApiClient.addWatchedItem.mockResolvedValue({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ id: 1 }),
|
||||
} as Response);
|
||||
|
||||
const invalidateQueriesSpy = vi.spyOn(queryClient, 'invalidateQueries');
|
||||
|
||||
const { result } = renderHook(() => useAddWatchedItemMutation(), { wrapper });
|
||||
|
||||
result.current.mutate({ itemName: 'Eggs' });
|
||||
|
||||
await waitFor(() => expect(result.current.isSuccess).toBe(true));
|
||||
|
||||
expect(invalidateQueriesSpy).toHaveBeenCalledWith({ queryKey: ['watched-items'] });
|
||||
});
|
||||
|
||||
it('should handle API error with error message', async () => {
|
||||
mockedApiClient.addWatchedItem.mockResolvedValue({
|
||||
ok: false,
|
||||
status: 409,
|
||||
json: () => Promise.resolve({ message: 'Item already watched' }),
|
||||
} as Response);
|
||||
|
||||
const { result } = renderHook(() => useAddWatchedItemMutation(), { wrapper });
|
||||
|
||||
result.current.mutate({ itemName: 'Milk' });
|
||||
|
||||
await waitFor(() => expect(result.current.isError).toBe(true));
|
||||
|
||||
expect(result.current.error?.message).toBe('Item already watched');
|
||||
expect(mockedNotifications.notifyError).toHaveBeenCalledWith('Item already watched');
|
||||
});
|
||||
|
||||
it('should handle API error without message', async () => {
|
||||
mockedApiClient.addWatchedItem.mockResolvedValue({
|
||||
ok: false,
|
||||
status: 500,
|
||||
json: () => Promise.reject(new Error('Parse error')),
|
||||
} as Response);
|
||||
|
||||
const { result } = renderHook(() => useAddWatchedItemMutation(), { wrapper });
|
||||
|
||||
result.current.mutate({ itemName: 'Cheese' });
|
||||
|
||||
await waitFor(() => expect(result.current.isError).toBe(true));
|
||||
|
||||
expect(result.current.error?.message).toBe('Request failed with status 500');
|
||||
});
|
||||
});
|
||||
99
src/hooks/mutations/useCreateShoppingListMutation.test.tsx
Normal file
99
src/hooks/mutations/useCreateShoppingListMutation.test.tsx
Normal file
@@ -0,0 +1,99 @@
|
||||
// src/hooks/mutations/useCreateShoppingListMutation.test.tsx
|
||||
import { renderHook, waitFor } from '@testing-library/react';
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import type { ReactNode } from 'react';
|
||||
import { useCreateShoppingListMutation } from './useCreateShoppingListMutation';
|
||||
import * as apiClient from '../../services/apiClient';
|
||||
import * as notificationService from '../../services/notificationService';
|
||||
|
||||
vi.mock('../../services/apiClient');
|
||||
vi.mock('../../services/notificationService');
|
||||
|
||||
const mockedApiClient = vi.mocked(apiClient);
|
||||
const mockedNotifications = vi.mocked(notificationService);
|
||||
|
||||
describe('useCreateShoppingListMutation', () => {
|
||||
let queryClient: QueryClient;
|
||||
|
||||
const wrapper = ({ children }: { children: ReactNode }) => (
|
||||
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
||||
);
|
||||
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks();
|
||||
queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: { retry: false },
|
||||
mutations: { retry: false },
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should create a shopping list successfully', async () => {
|
||||
const mockResponse = { shopping_list_id: 1, name: 'Weekly Groceries' };
|
||||
mockedApiClient.createShoppingList.mockResolvedValue({
|
||||
ok: true,
|
||||
json: () => Promise.resolve(mockResponse),
|
||||
} as Response);
|
||||
|
||||
const { result } = renderHook(() => useCreateShoppingListMutation(), { wrapper });
|
||||
|
||||
result.current.mutate({ name: 'Weekly Groceries' });
|
||||
|
||||
await waitFor(() => expect(result.current.isSuccess).toBe(true));
|
||||
|
||||
expect(mockedApiClient.createShoppingList).toHaveBeenCalledWith('Weekly Groceries');
|
||||
expect(mockedNotifications.notifySuccess).toHaveBeenCalledWith('Shopping list created');
|
||||
});
|
||||
|
||||
it('should invalidate shopping-lists query on success', async () => {
|
||||
mockedApiClient.createShoppingList.mockResolvedValue({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ shopping_list_id: 1 }),
|
||||
} as Response);
|
||||
|
||||
const invalidateQueriesSpy = vi.spyOn(queryClient, 'invalidateQueries');
|
||||
|
||||
const { result } = renderHook(() => useCreateShoppingListMutation(), { wrapper });
|
||||
|
||||
result.current.mutate({ name: 'Test List' });
|
||||
|
||||
await waitFor(() => expect(result.current.isSuccess).toBe(true));
|
||||
|
||||
expect(invalidateQueriesSpy).toHaveBeenCalledWith({ queryKey: ['shopping-lists'] });
|
||||
});
|
||||
|
||||
it('should handle API error with error message', async () => {
|
||||
mockedApiClient.createShoppingList.mockResolvedValue({
|
||||
ok: false,
|
||||
status: 400,
|
||||
json: () => Promise.resolve({ message: 'List name already exists' }),
|
||||
} as Response);
|
||||
|
||||
const { result } = renderHook(() => useCreateShoppingListMutation(), { wrapper });
|
||||
|
||||
result.current.mutate({ name: 'Duplicate List' });
|
||||
|
||||
await waitFor(() => expect(result.current.isError).toBe(true));
|
||||
|
||||
expect(result.current.error?.message).toBe('List name already exists');
|
||||
expect(mockedNotifications.notifyError).toHaveBeenCalledWith('List name already exists');
|
||||
});
|
||||
|
||||
it('should handle API error without message', async () => {
|
||||
mockedApiClient.createShoppingList.mockResolvedValue({
|
||||
ok: false,
|
||||
status: 500,
|
||||
json: () => Promise.reject(new Error('Parse error')),
|
||||
} as Response);
|
||||
|
||||
const { result } = renderHook(() => useCreateShoppingListMutation(), { wrapper });
|
||||
|
||||
result.current.mutate({ name: 'Test' });
|
||||
|
||||
await waitFor(() => expect(result.current.isError).toBe(true));
|
||||
|
||||
expect(result.current.error?.message).toBe('Request failed with status 500');
|
||||
});
|
||||
});
|
||||
99
src/hooks/mutations/useDeleteShoppingListMutation.test.tsx
Normal file
99
src/hooks/mutations/useDeleteShoppingListMutation.test.tsx
Normal file
@@ -0,0 +1,99 @@
|
||||
// src/hooks/mutations/useDeleteShoppingListMutation.test.tsx
|
||||
import { renderHook, waitFor } from '@testing-library/react';
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import type { ReactNode } from 'react';
|
||||
import { useDeleteShoppingListMutation } from './useDeleteShoppingListMutation';
|
||||
import * as apiClient from '../../services/apiClient';
|
||||
import * as notificationService from '../../services/notificationService';
|
||||
|
||||
vi.mock('../../services/apiClient');
|
||||
vi.mock('../../services/notificationService');
|
||||
|
||||
const mockedApiClient = vi.mocked(apiClient);
|
||||
const mockedNotifications = vi.mocked(notificationService);
|
||||
|
||||
describe('useDeleteShoppingListMutation', () => {
|
||||
let queryClient: QueryClient;
|
||||
|
||||
const wrapper = ({ children }: { children: ReactNode }) => (
|
||||
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
||||
);
|
||||
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks();
|
||||
queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: { retry: false },
|
||||
mutations: { retry: false },
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should delete a shopping list successfully', async () => {
|
||||
const mockResponse = { success: true };
|
||||
mockedApiClient.deleteShoppingList.mockResolvedValue({
|
||||
ok: true,
|
||||
json: () => Promise.resolve(mockResponse),
|
||||
} as Response);
|
||||
|
||||
const { result } = renderHook(() => useDeleteShoppingListMutation(), { wrapper });
|
||||
|
||||
result.current.mutate({ listId: 123 });
|
||||
|
||||
await waitFor(() => expect(result.current.isSuccess).toBe(true));
|
||||
|
||||
expect(mockedApiClient.deleteShoppingList).toHaveBeenCalledWith(123);
|
||||
expect(mockedNotifications.notifySuccess).toHaveBeenCalledWith('Shopping list deleted');
|
||||
});
|
||||
|
||||
it('should invalidate shopping-lists query on success', async () => {
|
||||
mockedApiClient.deleteShoppingList.mockResolvedValue({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ success: true }),
|
||||
} as Response);
|
||||
|
||||
const invalidateQueriesSpy = vi.spyOn(queryClient, 'invalidateQueries');
|
||||
|
||||
const { result } = renderHook(() => useDeleteShoppingListMutation(), { wrapper });
|
||||
|
||||
result.current.mutate({ listId: 456 });
|
||||
|
||||
await waitFor(() => expect(result.current.isSuccess).toBe(true));
|
||||
|
||||
expect(invalidateQueriesSpy).toHaveBeenCalledWith({ queryKey: ['shopping-lists'] });
|
||||
});
|
||||
|
||||
it('should handle API error with error message', async () => {
|
||||
mockedApiClient.deleteShoppingList.mockResolvedValue({
|
||||
ok: false,
|
||||
status: 404,
|
||||
json: () => Promise.resolve({ message: 'Shopping list not found' }),
|
||||
} as Response);
|
||||
|
||||
const { result } = renderHook(() => useDeleteShoppingListMutation(), { wrapper });
|
||||
|
||||
result.current.mutate({ listId: 999 });
|
||||
|
||||
await waitFor(() => expect(result.current.isError).toBe(true));
|
||||
|
||||
expect(result.current.error?.message).toBe('Shopping list not found');
|
||||
expect(mockedNotifications.notifyError).toHaveBeenCalledWith('Shopping list not found');
|
||||
});
|
||||
|
||||
it('should handle API error without message', async () => {
|
||||
mockedApiClient.deleteShoppingList.mockResolvedValue({
|
||||
ok: false,
|
||||
status: 500,
|
||||
json: () => Promise.reject(new Error('Parse error')),
|
||||
} as Response);
|
||||
|
||||
const { result } = renderHook(() => useDeleteShoppingListMutation(), { wrapper });
|
||||
|
||||
result.current.mutate({ listId: 123 });
|
||||
|
||||
await waitFor(() => expect(result.current.isError).toBe(true));
|
||||
|
||||
expect(result.current.error?.message).toBe('Request failed with status 500');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,99 @@
|
||||
// src/hooks/mutations/useRemoveShoppingListItemMutation.test.tsx
|
||||
import { renderHook, waitFor } from '@testing-library/react';
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import type { ReactNode } from 'react';
|
||||
import { useRemoveShoppingListItemMutation } from './useRemoveShoppingListItemMutation';
|
||||
import * as apiClient from '../../services/apiClient';
|
||||
import * as notificationService from '../../services/notificationService';
|
||||
|
||||
vi.mock('../../services/apiClient');
|
||||
vi.mock('../../services/notificationService');
|
||||
|
||||
const mockedApiClient = vi.mocked(apiClient);
|
||||
const mockedNotifications = vi.mocked(notificationService);
|
||||
|
||||
describe('useRemoveShoppingListItemMutation', () => {
|
||||
let queryClient: QueryClient;
|
||||
|
||||
const wrapper = ({ children }: { children: ReactNode }) => (
|
||||
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
||||
);
|
||||
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks();
|
||||
queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: { retry: false },
|
||||
mutations: { retry: false },
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should remove an item from shopping list successfully', async () => {
|
||||
const mockResponse = { success: true };
|
||||
mockedApiClient.removeShoppingListItem.mockResolvedValue({
|
||||
ok: true,
|
||||
json: () => Promise.resolve(mockResponse),
|
||||
} as Response);
|
||||
|
||||
const { result } = renderHook(() => useRemoveShoppingListItemMutation(), { wrapper });
|
||||
|
||||
result.current.mutate({ itemId: 42 });
|
||||
|
||||
await waitFor(() => expect(result.current.isSuccess).toBe(true));
|
||||
|
||||
expect(mockedApiClient.removeShoppingListItem).toHaveBeenCalledWith(42);
|
||||
expect(mockedNotifications.notifySuccess).toHaveBeenCalledWith('Item removed from shopping list');
|
||||
});
|
||||
|
||||
it('should invalidate shopping-lists query on success', async () => {
|
||||
mockedApiClient.removeShoppingListItem.mockResolvedValue({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ success: true }),
|
||||
} as Response);
|
||||
|
||||
const invalidateQueriesSpy = vi.spyOn(queryClient, 'invalidateQueries');
|
||||
|
||||
const { result } = renderHook(() => useRemoveShoppingListItemMutation(), { wrapper });
|
||||
|
||||
result.current.mutate({ itemId: 100 });
|
||||
|
||||
await waitFor(() => expect(result.current.isSuccess).toBe(true));
|
||||
|
||||
expect(invalidateQueriesSpy).toHaveBeenCalledWith({ queryKey: ['shopping-lists'] });
|
||||
});
|
||||
|
||||
it('should handle API error with error message', async () => {
|
||||
mockedApiClient.removeShoppingListItem.mockResolvedValue({
|
||||
ok: false,
|
||||
status: 404,
|
||||
json: () => Promise.resolve({ message: 'Item not found' }),
|
||||
} as Response);
|
||||
|
||||
const { result } = renderHook(() => useRemoveShoppingListItemMutation(), { wrapper });
|
||||
|
||||
result.current.mutate({ itemId: 999 });
|
||||
|
||||
await waitFor(() => expect(result.current.isError).toBe(true));
|
||||
|
||||
expect(result.current.error?.message).toBe('Item not found');
|
||||
expect(mockedNotifications.notifyError).toHaveBeenCalledWith('Item not found');
|
||||
});
|
||||
|
||||
it('should handle API error without message', async () => {
|
||||
mockedApiClient.removeShoppingListItem.mockResolvedValue({
|
||||
ok: false,
|
||||
status: 500,
|
||||
json: () => Promise.reject(new Error('Parse error')),
|
||||
} as Response);
|
||||
|
||||
const { result } = renderHook(() => useRemoveShoppingListItemMutation(), { wrapper });
|
||||
|
||||
result.current.mutate({ itemId: 42 });
|
||||
|
||||
await waitFor(() => expect(result.current.isError).toBe(true));
|
||||
|
||||
expect(result.current.error?.message).toBe('Request failed with status 500');
|
||||
});
|
||||
});
|
||||
99
src/hooks/mutations/useRemoveWatchedItemMutation.test.tsx
Normal file
99
src/hooks/mutations/useRemoveWatchedItemMutation.test.tsx
Normal file
@@ -0,0 +1,99 @@
|
||||
// src/hooks/mutations/useRemoveWatchedItemMutation.test.tsx
|
||||
import { renderHook, waitFor } from '@testing-library/react';
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import type { ReactNode } from 'react';
|
||||
import { useRemoveWatchedItemMutation } from './useRemoveWatchedItemMutation';
|
||||
import * as apiClient from '../../services/apiClient';
|
||||
import * as notificationService from '../../services/notificationService';
|
||||
|
||||
vi.mock('../../services/apiClient');
|
||||
vi.mock('../../services/notificationService');
|
||||
|
||||
const mockedApiClient = vi.mocked(apiClient);
|
||||
const mockedNotifications = vi.mocked(notificationService);
|
||||
|
||||
describe('useRemoveWatchedItemMutation', () => {
|
||||
let queryClient: QueryClient;
|
||||
|
||||
const wrapper = ({ children }: { children: ReactNode }) => (
|
||||
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
||||
);
|
||||
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks();
|
||||
queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: { retry: false },
|
||||
mutations: { retry: false },
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should remove a watched item successfully', async () => {
|
||||
const mockResponse = { success: true };
|
||||
mockedApiClient.removeWatchedItem.mockResolvedValue({
|
||||
ok: true,
|
||||
json: () => Promise.resolve(mockResponse),
|
||||
} as Response);
|
||||
|
||||
const { result } = renderHook(() => useRemoveWatchedItemMutation(), { wrapper });
|
||||
|
||||
result.current.mutate({ masterItemId: 123 });
|
||||
|
||||
await waitFor(() => expect(result.current.isSuccess).toBe(true));
|
||||
|
||||
expect(mockedApiClient.removeWatchedItem).toHaveBeenCalledWith(123);
|
||||
expect(mockedNotifications.notifySuccess).toHaveBeenCalledWith('Item removed from watched list');
|
||||
});
|
||||
|
||||
it('should invalidate watched-items query on success', async () => {
|
||||
mockedApiClient.removeWatchedItem.mockResolvedValue({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ success: true }),
|
||||
} as Response);
|
||||
|
||||
const invalidateQueriesSpy = vi.spyOn(queryClient, 'invalidateQueries');
|
||||
|
||||
const { result } = renderHook(() => useRemoveWatchedItemMutation(), { wrapper });
|
||||
|
||||
result.current.mutate({ masterItemId: 456 });
|
||||
|
||||
await waitFor(() => expect(result.current.isSuccess).toBe(true));
|
||||
|
||||
expect(invalidateQueriesSpy).toHaveBeenCalledWith({ queryKey: ['watched-items'] });
|
||||
});
|
||||
|
||||
it('should handle API error with error message', async () => {
|
||||
mockedApiClient.removeWatchedItem.mockResolvedValue({
|
||||
ok: false,
|
||||
status: 404,
|
||||
json: () => Promise.resolve({ message: 'Watched item not found' }),
|
||||
} as Response);
|
||||
|
||||
const { result } = renderHook(() => useRemoveWatchedItemMutation(), { wrapper });
|
||||
|
||||
result.current.mutate({ masterItemId: 999 });
|
||||
|
||||
await waitFor(() => expect(result.current.isError).toBe(true));
|
||||
|
||||
expect(result.current.error?.message).toBe('Watched item not found');
|
||||
expect(mockedNotifications.notifyError).toHaveBeenCalledWith('Watched item not found');
|
||||
});
|
||||
|
||||
it('should handle API error without message', async () => {
|
||||
mockedApiClient.removeWatchedItem.mockResolvedValue({
|
||||
ok: false,
|
||||
status: 500,
|
||||
json: () => Promise.reject(new Error('Parse error')),
|
||||
} as Response);
|
||||
|
||||
const { result } = renderHook(() => useRemoveWatchedItemMutation(), { wrapper });
|
||||
|
||||
result.current.mutate({ masterItemId: 123 });
|
||||
|
||||
await waitFor(() => expect(result.current.isError).toBe(true));
|
||||
|
||||
expect(result.current.error?.message).toBe('Request failed with status 500');
|
||||
});
|
||||
});
|
||||
159
src/hooks/mutations/useUpdateShoppingListItemMutation.test.tsx
Normal file
159
src/hooks/mutations/useUpdateShoppingListItemMutation.test.tsx
Normal file
@@ -0,0 +1,159 @@
|
||||
// src/hooks/mutations/useUpdateShoppingListItemMutation.test.tsx
|
||||
import { renderHook, waitFor } from '@testing-library/react';
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import type { ReactNode } from 'react';
|
||||
import { useUpdateShoppingListItemMutation } from './useUpdateShoppingListItemMutation';
|
||||
import * as apiClient from '../../services/apiClient';
|
||||
import * as notificationService from '../../services/notificationService';
|
||||
|
||||
vi.mock('../../services/apiClient');
|
||||
vi.mock('../../services/notificationService');
|
||||
|
||||
const mockedApiClient = vi.mocked(apiClient);
|
||||
const mockedNotifications = vi.mocked(notificationService);
|
||||
|
||||
describe('useUpdateShoppingListItemMutation', () => {
|
||||
let queryClient: QueryClient;
|
||||
|
||||
const wrapper = ({ children }: { children: ReactNode }) => (
|
||||
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
||||
);
|
||||
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks();
|
||||
queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: { retry: false },
|
||||
mutations: { retry: false },
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should update a shopping list item successfully', async () => {
|
||||
const mockResponse = { id: 42, quantity: 3 };
|
||||
mockedApiClient.updateShoppingListItem.mockResolvedValue({
|
||||
ok: true,
|
||||
json: () => Promise.resolve(mockResponse),
|
||||
} as Response);
|
||||
|
||||
const { result } = renderHook(() => useUpdateShoppingListItemMutation(), { wrapper });
|
||||
|
||||
result.current.mutate({ itemId: 42, updates: { quantity: 3 } });
|
||||
|
||||
await waitFor(() => expect(result.current.isSuccess).toBe(true));
|
||||
|
||||
expect(mockedApiClient.updateShoppingListItem).toHaveBeenCalledWith(42, { quantity: 3 });
|
||||
expect(mockedNotifications.notifySuccess).toHaveBeenCalledWith('Shopping list item updated');
|
||||
});
|
||||
|
||||
it('should update is_purchased status', async () => {
|
||||
mockedApiClient.updateShoppingListItem.mockResolvedValue({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ id: 42, is_purchased: true }),
|
||||
} as Response);
|
||||
|
||||
const { result } = renderHook(() => useUpdateShoppingListItemMutation(), { wrapper });
|
||||
|
||||
result.current.mutate({ itemId: 42, updates: { is_purchased: true } });
|
||||
|
||||
await waitFor(() => expect(result.current.isSuccess).toBe(true));
|
||||
|
||||
expect(mockedApiClient.updateShoppingListItem).toHaveBeenCalledWith(42, { is_purchased: true });
|
||||
});
|
||||
|
||||
it('should update custom_item_name', async () => {
|
||||
mockedApiClient.updateShoppingListItem.mockResolvedValue({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ id: 42, custom_item_name: 'Organic Milk' }),
|
||||
} as Response);
|
||||
|
||||
const { result } = renderHook(() => useUpdateShoppingListItemMutation(), { wrapper });
|
||||
|
||||
result.current.mutate({ itemId: 42, updates: { custom_item_name: 'Organic Milk' } });
|
||||
|
||||
await waitFor(() => expect(result.current.isSuccess).toBe(true));
|
||||
|
||||
expect(mockedApiClient.updateShoppingListItem).toHaveBeenCalledWith(42, { custom_item_name: 'Organic Milk' });
|
||||
});
|
||||
|
||||
it('should update notes', async () => {
|
||||
mockedApiClient.updateShoppingListItem.mockResolvedValue({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ id: 42, notes: 'Get the 2% variety' }),
|
||||
} as Response);
|
||||
|
||||
const { result } = renderHook(() => useUpdateShoppingListItemMutation(), { wrapper });
|
||||
|
||||
result.current.mutate({ itemId: 42, updates: { notes: 'Get the 2% variety' } });
|
||||
|
||||
await waitFor(() => expect(result.current.isSuccess).toBe(true));
|
||||
|
||||
expect(mockedApiClient.updateShoppingListItem).toHaveBeenCalledWith(42, { notes: 'Get the 2% variety' });
|
||||
});
|
||||
|
||||
it('should update multiple fields at once', async () => {
|
||||
mockedApiClient.updateShoppingListItem.mockResolvedValue({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ id: 42, quantity: 2, notes: 'Important' }),
|
||||
} as Response);
|
||||
|
||||
const { result } = renderHook(() => useUpdateShoppingListItemMutation(), { wrapper });
|
||||
|
||||
result.current.mutate({ itemId: 42, updates: { quantity: 2, notes: 'Important' } });
|
||||
|
||||
await waitFor(() => expect(result.current.isSuccess).toBe(true));
|
||||
|
||||
expect(mockedApiClient.updateShoppingListItem).toHaveBeenCalledWith(42, { quantity: 2, notes: 'Important' });
|
||||
});
|
||||
|
||||
it('should invalidate shopping-lists query on success', async () => {
|
||||
mockedApiClient.updateShoppingListItem.mockResolvedValue({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ id: 42 }),
|
||||
} as Response);
|
||||
|
||||
const invalidateQueriesSpy = vi.spyOn(queryClient, 'invalidateQueries');
|
||||
|
||||
const { result } = renderHook(() => useUpdateShoppingListItemMutation(), { wrapper });
|
||||
|
||||
result.current.mutate({ itemId: 42, updates: { quantity: 5 } });
|
||||
|
||||
await waitFor(() => expect(result.current.isSuccess).toBe(true));
|
||||
|
||||
expect(invalidateQueriesSpy).toHaveBeenCalledWith({ queryKey: ['shopping-lists'] });
|
||||
});
|
||||
|
||||
it('should handle API error with error message', async () => {
|
||||
mockedApiClient.updateShoppingListItem.mockResolvedValue({
|
||||
ok: false,
|
||||
status: 404,
|
||||
json: () => Promise.resolve({ message: 'Item not found' }),
|
||||
} as Response);
|
||||
|
||||
const { result } = renderHook(() => useUpdateShoppingListItemMutation(), { wrapper });
|
||||
|
||||
result.current.mutate({ itemId: 999, updates: { quantity: 1 } });
|
||||
|
||||
await waitFor(() => expect(result.current.isError).toBe(true));
|
||||
|
||||
expect(result.current.error?.message).toBe('Item not found');
|
||||
expect(mockedNotifications.notifyError).toHaveBeenCalledWith('Item not found');
|
||||
});
|
||||
|
||||
it('should handle API error without message', async () => {
|
||||
mockedApiClient.updateShoppingListItem.mockResolvedValue({
|
||||
ok: false,
|
||||
status: 500,
|
||||
json: () => Promise.reject(new Error('Parse error')),
|
||||
} as Response);
|
||||
|
||||
const { result } = renderHook(() => useUpdateShoppingListItemMutation(), { wrapper });
|
||||
|
||||
result.current.mutate({ itemId: 42, updates: { quantity: 1 } });
|
||||
|
||||
await waitFor(() => expect(result.current.isError).toBe(true));
|
||||
|
||||
expect(result.current.error?.message).toBe('Request failed with status 500');
|
||||
});
|
||||
});
|
||||
102
src/hooks/queries/useActivityLogQuery.test.tsx
Normal file
102
src/hooks/queries/useActivityLogQuery.test.tsx
Normal file
@@ -0,0 +1,102 @@
|
||||
// src/hooks/queries/useActivityLogQuery.test.tsx
|
||||
import { renderHook, waitFor } from '@testing-library/react';
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import type { ReactNode } from 'react';
|
||||
import { useActivityLogQuery } from './useActivityLogQuery';
|
||||
import * as apiClient from '../../services/apiClient';
|
||||
|
||||
vi.mock('../../services/apiClient');
|
||||
|
||||
const mockedApiClient = vi.mocked(apiClient);
|
||||
|
||||
describe('useActivityLogQuery', () => {
|
||||
let queryClient: QueryClient;
|
||||
|
||||
const wrapper = ({ children }: { children: ReactNode }) => (
|
||||
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
||||
);
|
||||
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks();
|
||||
queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: { retry: false },
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should fetch activity log with default params', async () => {
|
||||
const mockActivityLog = [
|
||||
{ id: 1, action: 'user_login', timestamp: '2024-01-01T10:00:00Z' },
|
||||
{ id: 2, action: 'flyer_uploaded', timestamp: '2024-01-01T11:00:00Z' },
|
||||
];
|
||||
mockedApiClient.fetchActivityLog.mockResolvedValue({
|
||||
ok: true,
|
||||
json: () => Promise.resolve(mockActivityLog),
|
||||
} as Response);
|
||||
|
||||
const { result } = renderHook(() => useActivityLogQuery(), { wrapper });
|
||||
|
||||
await waitFor(() => expect(result.current.isSuccess).toBe(true));
|
||||
|
||||
expect(mockedApiClient.fetchActivityLog).toHaveBeenCalledWith(20, 0);
|
||||
expect(result.current.data).toEqual(mockActivityLog);
|
||||
});
|
||||
|
||||
it('should fetch activity log with custom limit and offset', async () => {
|
||||
const mockActivityLog = [{ id: 3, action: 'item_added', timestamp: '2024-01-01T12:00:00Z' }];
|
||||
mockedApiClient.fetchActivityLog.mockResolvedValue({
|
||||
ok: true,
|
||||
json: () => Promise.resolve(mockActivityLog),
|
||||
} as Response);
|
||||
|
||||
const { result } = renderHook(() => useActivityLogQuery(10, 5), { wrapper });
|
||||
|
||||
await waitFor(() => expect(result.current.isSuccess).toBe(true));
|
||||
|
||||
expect(mockedApiClient.fetchActivityLog).toHaveBeenCalledWith(10, 5);
|
||||
expect(result.current.data).toEqual(mockActivityLog);
|
||||
});
|
||||
|
||||
it('should handle API error with error message', async () => {
|
||||
mockedApiClient.fetchActivityLog.mockResolvedValue({
|
||||
ok: false,
|
||||
status: 403,
|
||||
json: () => Promise.resolve({ message: 'Admin access required' }),
|
||||
} as Response);
|
||||
|
||||
const { result } = renderHook(() => useActivityLogQuery(), { wrapper });
|
||||
|
||||
await waitFor(() => expect(result.current.isError).toBe(true));
|
||||
|
||||
expect(result.current.error?.message).toBe('Admin access required');
|
||||
});
|
||||
|
||||
it('should handle API error without message', async () => {
|
||||
mockedApiClient.fetchActivityLog.mockResolvedValue({
|
||||
ok: false,
|
||||
status: 500,
|
||||
json: () => Promise.reject(new Error('Parse error')),
|
||||
} as Response);
|
||||
|
||||
const { result } = renderHook(() => useActivityLogQuery(), { wrapper });
|
||||
|
||||
await waitFor(() => expect(result.current.isError).toBe(true));
|
||||
|
||||
expect(result.current.error?.message).toBe('Request failed with status 500');
|
||||
});
|
||||
|
||||
it('should return empty array for no activity log entries', async () => {
|
||||
mockedApiClient.fetchActivityLog.mockResolvedValue({
|
||||
ok: true,
|
||||
json: () => Promise.resolve([]),
|
||||
} as Response);
|
||||
|
||||
const { result } = renderHook(() => useActivityLogQuery(), { wrapper });
|
||||
|
||||
await waitFor(() => expect(result.current.isSuccess).toBe(true));
|
||||
|
||||
expect(result.current.data).toEqual([]);
|
||||
});
|
||||
});
|
||||
78
src/hooks/queries/useApplicationStatsQuery.test.tsx
Normal file
78
src/hooks/queries/useApplicationStatsQuery.test.tsx
Normal file
@@ -0,0 +1,78 @@
|
||||
// src/hooks/queries/useApplicationStatsQuery.test.tsx
|
||||
import { renderHook, waitFor } from '@testing-library/react';
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import type { ReactNode } from 'react';
|
||||
import { useApplicationStatsQuery } from './useApplicationStatsQuery';
|
||||
import * as apiClient from '../../services/apiClient';
|
||||
|
||||
vi.mock('../../services/apiClient');
|
||||
|
||||
const mockedApiClient = vi.mocked(apiClient);
|
||||
|
||||
describe('useApplicationStatsQuery', () => {
|
||||
let queryClient: QueryClient;
|
||||
|
||||
const wrapper = ({ children }: { children: ReactNode }) => (
|
||||
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
||||
);
|
||||
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks();
|
||||
queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: { retry: false },
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should fetch application stats successfully', async () => {
|
||||
const mockStats = {
|
||||
flyerCount: 150,
|
||||
userCount: 500,
|
||||
flyerItemCount: 5000,
|
||||
storeCount: 25,
|
||||
pendingCorrectionsCount: 10,
|
||||
recipeCount: 75,
|
||||
};
|
||||
mockedApiClient.getApplicationStats.mockResolvedValue({
|
||||
ok: true,
|
||||
json: () => Promise.resolve(mockStats),
|
||||
} as Response);
|
||||
|
||||
const { result } = renderHook(() => useApplicationStatsQuery(), { wrapper });
|
||||
|
||||
await waitFor(() => expect(result.current.isSuccess).toBe(true));
|
||||
|
||||
expect(mockedApiClient.getApplicationStats).toHaveBeenCalled();
|
||||
expect(result.current.data).toEqual(mockStats);
|
||||
});
|
||||
|
||||
it('should handle API error with error message', async () => {
|
||||
mockedApiClient.getApplicationStats.mockResolvedValue({
|
||||
ok: false,
|
||||
status: 403,
|
||||
json: () => Promise.resolve({ message: 'Admin access required' }),
|
||||
} as Response);
|
||||
|
||||
const { result } = renderHook(() => useApplicationStatsQuery(), { wrapper });
|
||||
|
||||
await waitFor(() => expect(result.current.isError).toBe(true));
|
||||
|
||||
expect(result.current.error?.message).toBe('Admin access required');
|
||||
});
|
||||
|
||||
it('should handle API error without message', async () => {
|
||||
mockedApiClient.getApplicationStats.mockResolvedValue({
|
||||
ok: false,
|
||||
status: 500,
|
||||
json: () => Promise.reject(new Error('Parse error')),
|
||||
} as Response);
|
||||
|
||||
const { result } = renderHook(() => useApplicationStatsQuery(), { wrapper });
|
||||
|
||||
await waitFor(() => expect(result.current.isError).toBe(true));
|
||||
|
||||
expect(result.current.error?.message).toBe('Request failed with status 500');
|
||||
});
|
||||
});
|
||||
88
src/hooks/queries/useCategoriesQuery.test.tsx
Normal file
88
src/hooks/queries/useCategoriesQuery.test.tsx
Normal file
@@ -0,0 +1,88 @@
|
||||
// src/hooks/queries/useCategoriesQuery.test.tsx
|
||||
import { renderHook, waitFor } from '@testing-library/react';
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import type { ReactNode } from 'react';
|
||||
import { useCategoriesQuery } from './useCategoriesQuery';
|
||||
import * as apiClient from '../../services/apiClient';
|
||||
|
||||
vi.mock('../../services/apiClient');
|
||||
|
||||
const mockedApiClient = vi.mocked(apiClient);
|
||||
|
||||
describe('useCategoriesQuery', () => {
|
||||
let queryClient: QueryClient;
|
||||
|
||||
const wrapper = ({ children }: { children: ReactNode }) => (
|
||||
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
||||
);
|
||||
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks();
|
||||
queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: { retry: false },
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should fetch categories successfully', async () => {
|
||||
const mockCategories = [
|
||||
{ category_id: 1, name: 'Dairy' },
|
||||
{ category_id: 2, name: 'Bakery' },
|
||||
{ category_id: 3, name: 'Produce' },
|
||||
];
|
||||
mockedApiClient.fetchCategories.mockResolvedValue({
|
||||
ok: true,
|
||||
json: () => Promise.resolve(mockCategories),
|
||||
} as Response);
|
||||
|
||||
const { result } = renderHook(() => useCategoriesQuery(), { wrapper });
|
||||
|
||||
await waitFor(() => expect(result.current.isSuccess).toBe(true));
|
||||
|
||||
expect(mockedApiClient.fetchCategories).toHaveBeenCalled();
|
||||
expect(result.current.data).toEqual(mockCategories);
|
||||
});
|
||||
|
||||
it('should handle API error with error message', async () => {
|
||||
mockedApiClient.fetchCategories.mockResolvedValue({
|
||||
ok: false,
|
||||
status: 500,
|
||||
json: () => Promise.resolve({ message: 'Database error' }),
|
||||
} as Response);
|
||||
|
||||
const { result } = renderHook(() => useCategoriesQuery(), { wrapper });
|
||||
|
||||
await waitFor(() => expect(result.current.isError).toBe(true));
|
||||
|
||||
expect(result.current.error?.message).toBe('Database error');
|
||||
});
|
||||
|
||||
it('should handle API error without message', async () => {
|
||||
mockedApiClient.fetchCategories.mockResolvedValue({
|
||||
ok: false,
|
||||
status: 500,
|
||||
json: () => Promise.reject(new Error('Parse error')),
|
||||
} as Response);
|
||||
|
||||
const { result } = renderHook(() => useCategoriesQuery(), { wrapper });
|
||||
|
||||
await waitFor(() => expect(result.current.isError).toBe(true));
|
||||
|
||||
expect(result.current.error?.message).toBe('Request failed with status 500');
|
||||
});
|
||||
|
||||
it('should return empty array for no categories', async () => {
|
||||
mockedApiClient.fetchCategories.mockResolvedValue({
|
||||
ok: true,
|
||||
json: () => Promise.resolve([]),
|
||||
} as Response);
|
||||
|
||||
const { result } = renderHook(() => useCategoriesQuery(), { wrapper });
|
||||
|
||||
await waitFor(() => expect(result.current.isSuccess).toBe(true));
|
||||
|
||||
expect(result.current.data).toEqual([]);
|
||||
});
|
||||
});
|
||||
111
src/hooks/queries/useFlyerItemsQuery.test.tsx
Normal file
111
src/hooks/queries/useFlyerItemsQuery.test.tsx
Normal file
@@ -0,0 +1,111 @@
|
||||
// src/hooks/queries/useFlyerItemsQuery.test.tsx
|
||||
import { renderHook, waitFor } from '@testing-library/react';
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import type { ReactNode } from 'react';
|
||||
import { useFlyerItemsQuery } from './useFlyerItemsQuery';
|
||||
import * as apiClient from '../../services/apiClient';
|
||||
|
||||
vi.mock('../../services/apiClient');
|
||||
|
||||
const mockedApiClient = vi.mocked(apiClient);
|
||||
|
||||
describe('useFlyerItemsQuery', () => {
|
||||
let queryClient: QueryClient;
|
||||
|
||||
const wrapper = ({ children }: { children: ReactNode }) => (
|
||||
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
||||
);
|
||||
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks();
|
||||
queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: { retry: false },
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should fetch flyer items when flyerId is provided', async () => {
|
||||
const mockFlyerItems = [
|
||||
{ item_id: 1, name: 'Milk', price: 3.99, flyer_id: 42 },
|
||||
{ item_id: 2, name: 'Bread', price: 2.49, flyer_id: 42 },
|
||||
];
|
||||
mockedApiClient.fetchFlyerItems.mockResolvedValue({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ items: mockFlyerItems }),
|
||||
} as Response);
|
||||
|
||||
const { result } = renderHook(() => useFlyerItemsQuery(42), { wrapper });
|
||||
|
||||
await waitFor(() => expect(result.current.isSuccess).toBe(true));
|
||||
|
||||
expect(mockedApiClient.fetchFlyerItems).toHaveBeenCalledWith(42);
|
||||
expect(result.current.data).toEqual(mockFlyerItems);
|
||||
});
|
||||
|
||||
it('should not fetch when flyerId is undefined', async () => {
|
||||
const { result } = renderHook(() => useFlyerItemsQuery(undefined), { wrapper });
|
||||
|
||||
// Wait a bit to ensure the query doesn't run
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
|
||||
expect(mockedApiClient.fetchFlyerItems).not.toHaveBeenCalled();
|
||||
expect(result.current.isLoading).toBe(false);
|
||||
expect(result.current.isFetching).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle API error with error message', async () => {
|
||||
mockedApiClient.fetchFlyerItems.mockResolvedValue({
|
||||
ok: false,
|
||||
status: 404,
|
||||
json: () => Promise.resolve({ message: 'Flyer not found' }),
|
||||
} as Response);
|
||||
|
||||
const { result } = renderHook(() => useFlyerItemsQuery(999), { wrapper });
|
||||
|
||||
await waitFor(() => expect(result.current.isError).toBe(true));
|
||||
|
||||
expect(result.current.error?.message).toBe('Flyer not found');
|
||||
});
|
||||
|
||||
it('should handle API error without message', async () => {
|
||||
mockedApiClient.fetchFlyerItems.mockResolvedValue({
|
||||
ok: false,
|
||||
status: 500,
|
||||
json: () => Promise.reject(new Error('Parse error')),
|
||||
} as Response);
|
||||
|
||||
const { result } = renderHook(() => useFlyerItemsQuery(42), { wrapper });
|
||||
|
||||
await waitFor(() => expect(result.current.isError).toBe(true));
|
||||
|
||||
expect(result.current.error?.message).toBe('Request failed with status 500');
|
||||
});
|
||||
|
||||
it('should return empty array when API returns no items', async () => {
|
||||
mockedApiClient.fetchFlyerItems.mockResolvedValue({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ items: [] }),
|
||||
} as Response);
|
||||
|
||||
const { result } = renderHook(() => useFlyerItemsQuery(42), { wrapper });
|
||||
|
||||
await waitFor(() => expect(result.current.isSuccess).toBe(true));
|
||||
|
||||
expect(result.current.data).toEqual([]);
|
||||
});
|
||||
|
||||
it('should handle response without items property', async () => {
|
||||
mockedApiClient.fetchFlyerItems.mockResolvedValue({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({}),
|
||||
} as Response);
|
||||
|
||||
const { result } = renderHook(() => useFlyerItemsQuery(42), { wrapper });
|
||||
|
||||
await waitFor(() => expect(result.current.isSuccess).toBe(true));
|
||||
|
||||
expect(result.current.data).toEqual([]);
|
||||
});
|
||||
});
|
||||
102
src/hooks/queries/useFlyersQuery.test.tsx
Normal file
102
src/hooks/queries/useFlyersQuery.test.tsx
Normal file
@@ -0,0 +1,102 @@
|
||||
// src/hooks/queries/useFlyersQuery.test.tsx
|
||||
import { renderHook, waitFor } from '@testing-library/react';
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import type { ReactNode } from 'react';
|
||||
import { useFlyersQuery } from './useFlyersQuery';
|
||||
import * as apiClient from '../../services/apiClient';
|
||||
|
||||
vi.mock('../../services/apiClient');
|
||||
|
||||
const mockedApiClient = vi.mocked(apiClient);
|
||||
|
||||
describe('useFlyersQuery', () => {
|
||||
let queryClient: QueryClient;
|
||||
|
||||
const wrapper = ({ children }: { children: ReactNode }) => (
|
||||
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
||||
);
|
||||
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks();
|
||||
queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: { retry: false },
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should fetch flyers successfully with default params', async () => {
|
||||
const mockFlyers = [
|
||||
{ flyer_id: 1, store_name: 'Store A', valid_from: '2024-01-01', valid_to: '2024-01-07' },
|
||||
{ flyer_id: 2, store_name: 'Store B', valid_from: '2024-01-01', valid_to: '2024-01-07' },
|
||||
];
|
||||
mockedApiClient.fetchFlyers.mockResolvedValue({
|
||||
ok: true,
|
||||
json: () => Promise.resolve(mockFlyers),
|
||||
} as Response);
|
||||
|
||||
const { result } = renderHook(() => useFlyersQuery(), { wrapper });
|
||||
|
||||
await waitFor(() => expect(result.current.isSuccess).toBe(true));
|
||||
|
||||
expect(mockedApiClient.fetchFlyers).toHaveBeenCalledWith(20, 0);
|
||||
expect(result.current.data).toEqual(mockFlyers);
|
||||
});
|
||||
|
||||
it('should fetch flyers with custom limit and offset', async () => {
|
||||
const mockFlyers = [{ flyer_id: 3, store_name: 'Store C' }];
|
||||
mockedApiClient.fetchFlyers.mockResolvedValue({
|
||||
ok: true,
|
||||
json: () => Promise.resolve(mockFlyers),
|
||||
} as Response);
|
||||
|
||||
const { result } = renderHook(() => useFlyersQuery(10, 5), { wrapper });
|
||||
|
||||
await waitFor(() => expect(result.current.isSuccess).toBe(true));
|
||||
|
||||
expect(mockedApiClient.fetchFlyers).toHaveBeenCalledWith(10, 5);
|
||||
expect(result.current.data).toEqual(mockFlyers);
|
||||
});
|
||||
|
||||
it('should handle API error with error message', async () => {
|
||||
mockedApiClient.fetchFlyers.mockResolvedValue({
|
||||
ok: false,
|
||||
status: 500,
|
||||
json: () => Promise.resolve({ message: 'Server error' }),
|
||||
} as Response);
|
||||
|
||||
const { result } = renderHook(() => useFlyersQuery(), { wrapper });
|
||||
|
||||
await waitFor(() => expect(result.current.isError).toBe(true));
|
||||
|
||||
expect(result.current.error?.message).toBe('Server error');
|
||||
});
|
||||
|
||||
it('should handle API error without message', async () => {
|
||||
mockedApiClient.fetchFlyers.mockResolvedValue({
|
||||
ok: false,
|
||||
status: 500,
|
||||
json: () => Promise.reject(new Error('Parse error')),
|
||||
} as Response);
|
||||
|
||||
const { result } = renderHook(() => useFlyersQuery(), { wrapper });
|
||||
|
||||
await waitFor(() => expect(result.current.isError).toBe(true));
|
||||
|
||||
expect(result.current.error?.message).toBe('Request failed with status 500');
|
||||
});
|
||||
|
||||
it('should return empty array for no flyers', async () => {
|
||||
mockedApiClient.fetchFlyers.mockResolvedValue({
|
||||
ok: true,
|
||||
json: () => Promise.resolve([]),
|
||||
} as Response);
|
||||
|
||||
const { result } = renderHook(() => useFlyersQuery(), { wrapper });
|
||||
|
||||
await waitFor(() => expect(result.current.isSuccess).toBe(true));
|
||||
|
||||
expect(result.current.data).toEqual([]);
|
||||
});
|
||||
});
|
||||
88
src/hooks/queries/useMasterItemsQuery.test.tsx
Normal file
88
src/hooks/queries/useMasterItemsQuery.test.tsx
Normal file
@@ -0,0 +1,88 @@
|
||||
// src/hooks/queries/useMasterItemsQuery.test.tsx
|
||||
import { renderHook, waitFor } from '@testing-library/react';
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import type { ReactNode } from 'react';
|
||||
import { useMasterItemsQuery } from './useMasterItemsQuery';
|
||||
import * as apiClient from '../../services/apiClient';
|
||||
|
||||
vi.mock('../../services/apiClient');
|
||||
|
||||
const mockedApiClient = vi.mocked(apiClient);
|
||||
|
||||
describe('useMasterItemsQuery', () => {
|
||||
let queryClient: QueryClient;
|
||||
|
||||
const wrapper = ({ children }: { children: ReactNode }) => (
|
||||
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
||||
);
|
||||
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks();
|
||||
queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: { retry: false },
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should fetch master items successfully', async () => {
|
||||
const mockMasterItems = [
|
||||
{ master_item_id: 1, name: 'Milk', category: 'Dairy' },
|
||||
{ master_item_id: 2, name: 'Bread', category: 'Bakery' },
|
||||
{ master_item_id: 3, name: 'Eggs', category: 'Dairy' },
|
||||
];
|
||||
mockedApiClient.fetchMasterItems.mockResolvedValue({
|
||||
ok: true,
|
||||
json: () => Promise.resolve(mockMasterItems),
|
||||
} as Response);
|
||||
|
||||
const { result } = renderHook(() => useMasterItemsQuery(), { wrapper });
|
||||
|
||||
await waitFor(() => expect(result.current.isSuccess).toBe(true));
|
||||
|
||||
expect(mockedApiClient.fetchMasterItems).toHaveBeenCalled();
|
||||
expect(result.current.data).toEqual(mockMasterItems);
|
||||
});
|
||||
|
||||
it('should handle API error with error message', async () => {
|
||||
mockedApiClient.fetchMasterItems.mockResolvedValue({
|
||||
ok: false,
|
||||
status: 500,
|
||||
json: () => Promise.resolve({ message: 'Database error' }),
|
||||
} as Response);
|
||||
|
||||
const { result } = renderHook(() => useMasterItemsQuery(), { wrapper });
|
||||
|
||||
await waitFor(() => expect(result.current.isError).toBe(true));
|
||||
|
||||
expect(result.current.error?.message).toBe('Database error');
|
||||
});
|
||||
|
||||
it('should handle API error without message', async () => {
|
||||
mockedApiClient.fetchMasterItems.mockResolvedValue({
|
||||
ok: false,
|
||||
status: 500,
|
||||
json: () => Promise.reject(new Error('Parse error')),
|
||||
} as Response);
|
||||
|
||||
const { result } = renderHook(() => useMasterItemsQuery(), { wrapper });
|
||||
|
||||
await waitFor(() => expect(result.current.isError).toBe(true));
|
||||
|
||||
expect(result.current.error?.message).toBe('Request failed with status 500');
|
||||
});
|
||||
|
||||
it('should return empty array for no master items', async () => {
|
||||
mockedApiClient.fetchMasterItems.mockResolvedValue({
|
||||
ok: true,
|
||||
json: () => Promise.resolve([]),
|
||||
} as Response);
|
||||
|
||||
const { result } = renderHook(() => useMasterItemsQuery(), { wrapper });
|
||||
|
||||
await waitFor(() => expect(result.current.isSuccess).toBe(true));
|
||||
|
||||
expect(result.current.data).toEqual([]);
|
||||
});
|
||||
});
|
||||
98
src/hooks/queries/useShoppingListsQuery.test.tsx
Normal file
98
src/hooks/queries/useShoppingListsQuery.test.tsx
Normal file
@@ -0,0 +1,98 @@
|
||||
// src/hooks/queries/useShoppingListsQuery.test.tsx
|
||||
import { renderHook, waitFor } from '@testing-library/react';
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import type { ReactNode } from 'react';
|
||||
import { useShoppingListsQuery } from './useShoppingListsQuery';
|
||||
import * as apiClient from '../../services/apiClient';
|
||||
|
||||
vi.mock('../../services/apiClient');
|
||||
|
||||
const mockedApiClient = vi.mocked(apiClient);
|
||||
|
||||
describe('useShoppingListsQuery', () => {
|
||||
let queryClient: QueryClient;
|
||||
|
||||
const wrapper = ({ children }: { children: ReactNode }) => (
|
||||
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
||||
);
|
||||
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks();
|
||||
queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: { retry: false },
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should fetch shopping lists when enabled', async () => {
|
||||
const mockShoppingLists = [
|
||||
{ shopping_list_id: 1, name: 'Weekly Groceries', items: [] },
|
||||
{ shopping_list_id: 2, name: 'Party Supplies', items: [] },
|
||||
];
|
||||
mockedApiClient.fetchShoppingLists.mockResolvedValue({
|
||||
ok: true,
|
||||
json: () => Promise.resolve(mockShoppingLists),
|
||||
} as Response);
|
||||
|
||||
const { result } = renderHook(() => useShoppingListsQuery(true), { wrapper });
|
||||
|
||||
await waitFor(() => expect(result.current.isSuccess).toBe(true));
|
||||
|
||||
expect(mockedApiClient.fetchShoppingLists).toHaveBeenCalled();
|
||||
expect(result.current.data).toEqual(mockShoppingLists);
|
||||
});
|
||||
|
||||
it('should not fetch shopping lists when disabled', async () => {
|
||||
const { result } = renderHook(() => useShoppingListsQuery(false), { wrapper });
|
||||
|
||||
// Wait a bit to ensure the query doesn't run
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
|
||||
expect(mockedApiClient.fetchShoppingLists).not.toHaveBeenCalled();
|
||||
expect(result.current.isLoading).toBe(false);
|
||||
expect(result.current.isFetching).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle API error with error message', async () => {
|
||||
mockedApiClient.fetchShoppingLists.mockResolvedValue({
|
||||
ok: false,
|
||||
status: 401,
|
||||
json: () => Promise.resolve({ message: 'Unauthorized' }),
|
||||
} as Response);
|
||||
|
||||
const { result } = renderHook(() => useShoppingListsQuery(true), { wrapper });
|
||||
|
||||
await waitFor(() => expect(result.current.isError).toBe(true));
|
||||
|
||||
expect(result.current.error?.message).toBe('Unauthorized');
|
||||
});
|
||||
|
||||
it('should handle API error without message', async () => {
|
||||
mockedApiClient.fetchShoppingLists.mockResolvedValue({
|
||||
ok: false,
|
||||
status: 500,
|
||||
json: () => Promise.reject(new Error('Parse error')),
|
||||
} as Response);
|
||||
|
||||
const { result } = renderHook(() => useShoppingListsQuery(true), { wrapper });
|
||||
|
||||
await waitFor(() => expect(result.current.isError).toBe(true));
|
||||
|
||||
expect(result.current.error?.message).toBe('Request failed with status 500');
|
||||
});
|
||||
|
||||
it('should return empty array for no shopping lists', async () => {
|
||||
mockedApiClient.fetchShoppingLists.mockResolvedValue({
|
||||
ok: true,
|
||||
json: () => Promise.resolve([]),
|
||||
} as Response);
|
||||
|
||||
const { result } = renderHook(() => useShoppingListsQuery(true), { wrapper });
|
||||
|
||||
await waitFor(() => expect(result.current.isSuccess).toBe(true));
|
||||
|
||||
expect(result.current.data).toEqual([]);
|
||||
});
|
||||
});
|
||||
87
src/hooks/queries/useSuggestedCorrectionsQuery.test.tsx
Normal file
87
src/hooks/queries/useSuggestedCorrectionsQuery.test.tsx
Normal file
@@ -0,0 +1,87 @@
|
||||
// src/hooks/queries/useSuggestedCorrectionsQuery.test.tsx
|
||||
import { renderHook, waitFor } from '@testing-library/react';
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import type { ReactNode } from 'react';
|
||||
import { useSuggestedCorrectionsQuery } from './useSuggestedCorrectionsQuery';
|
||||
import * as apiClient from '../../services/apiClient';
|
||||
|
||||
vi.mock('../../services/apiClient');
|
||||
|
||||
const mockedApiClient = vi.mocked(apiClient);
|
||||
|
||||
describe('useSuggestedCorrectionsQuery', () => {
|
||||
let queryClient: QueryClient;
|
||||
|
||||
const wrapper = ({ children }: { children: ReactNode }) => (
|
||||
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
||||
);
|
||||
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks();
|
||||
queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: { retry: false },
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should fetch suggested corrections successfully', async () => {
|
||||
const mockCorrections = [
|
||||
{ correction_id: 1, item_name: 'Milk', suggested_name: 'Whole Milk', status: 'pending' },
|
||||
{ correction_id: 2, item_name: 'Bread', suggested_name: 'White Bread', status: 'pending' },
|
||||
];
|
||||
mockedApiClient.getSuggestedCorrections.mockResolvedValue({
|
||||
ok: true,
|
||||
json: () => Promise.resolve(mockCorrections),
|
||||
} as Response);
|
||||
|
||||
const { result } = renderHook(() => useSuggestedCorrectionsQuery(), { wrapper });
|
||||
|
||||
await waitFor(() => expect(result.current.isSuccess).toBe(true));
|
||||
|
||||
expect(mockedApiClient.getSuggestedCorrections).toHaveBeenCalled();
|
||||
expect(result.current.data).toEqual(mockCorrections);
|
||||
});
|
||||
|
||||
it('should handle API error with error message', async () => {
|
||||
mockedApiClient.getSuggestedCorrections.mockResolvedValue({
|
||||
ok: false,
|
||||
status: 403,
|
||||
json: () => Promise.resolve({ message: 'Admin access required' }),
|
||||
} as Response);
|
||||
|
||||
const { result } = renderHook(() => useSuggestedCorrectionsQuery(), { wrapper });
|
||||
|
||||
await waitFor(() => expect(result.current.isError).toBe(true));
|
||||
|
||||
expect(result.current.error?.message).toBe('Admin access required');
|
||||
});
|
||||
|
||||
it('should handle API error without message', async () => {
|
||||
mockedApiClient.getSuggestedCorrections.mockResolvedValue({
|
||||
ok: false,
|
||||
status: 500,
|
||||
json: () => Promise.reject(new Error('Parse error')),
|
||||
} as Response);
|
||||
|
||||
const { result } = renderHook(() => useSuggestedCorrectionsQuery(), { wrapper });
|
||||
|
||||
await waitFor(() => expect(result.current.isError).toBe(true));
|
||||
|
||||
expect(result.current.error?.message).toBe('Request failed with status 500');
|
||||
});
|
||||
|
||||
it('should return empty array for no corrections', async () => {
|
||||
mockedApiClient.getSuggestedCorrections.mockResolvedValue({
|
||||
ok: true,
|
||||
json: () => Promise.resolve([]),
|
||||
} as Response);
|
||||
|
||||
const { result } = renderHook(() => useSuggestedCorrectionsQuery(), { wrapper });
|
||||
|
||||
await waitFor(() => expect(result.current.isSuccess).toBe(true));
|
||||
|
||||
expect(result.current.data).toEqual([]);
|
||||
});
|
||||
});
|
||||
98
src/hooks/queries/useWatchedItemsQuery.test.tsx
Normal file
98
src/hooks/queries/useWatchedItemsQuery.test.tsx
Normal file
@@ -0,0 +1,98 @@
|
||||
// src/hooks/queries/useWatchedItemsQuery.test.tsx
|
||||
import { renderHook, waitFor } from '@testing-library/react';
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import type { ReactNode } from 'react';
|
||||
import { useWatchedItemsQuery } from './useWatchedItemsQuery';
|
||||
import * as apiClient from '../../services/apiClient';
|
||||
|
||||
vi.mock('../../services/apiClient');
|
||||
|
||||
const mockedApiClient = vi.mocked(apiClient);
|
||||
|
||||
describe('useWatchedItemsQuery', () => {
|
||||
let queryClient: QueryClient;
|
||||
|
||||
const wrapper = ({ children }: { children: ReactNode }) => (
|
||||
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
||||
);
|
||||
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks();
|
||||
queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: { retry: false },
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should fetch watched items when enabled', async () => {
|
||||
const mockWatchedItems = [
|
||||
{ master_item_id: 1, name: 'Milk', category: 'Dairy' },
|
||||
{ master_item_id: 2, name: 'Bread', category: 'Bakery' },
|
||||
];
|
||||
mockedApiClient.fetchWatchedItems.mockResolvedValue({
|
||||
ok: true,
|
||||
json: () => Promise.resolve(mockWatchedItems),
|
||||
} as Response);
|
||||
|
||||
const { result } = renderHook(() => useWatchedItemsQuery(true), { wrapper });
|
||||
|
||||
await waitFor(() => expect(result.current.isSuccess).toBe(true));
|
||||
|
||||
expect(mockedApiClient.fetchWatchedItems).toHaveBeenCalled();
|
||||
expect(result.current.data).toEqual(mockWatchedItems);
|
||||
});
|
||||
|
||||
it('should not fetch watched items when disabled', async () => {
|
||||
const { result } = renderHook(() => useWatchedItemsQuery(false), { wrapper });
|
||||
|
||||
// Wait a bit to ensure the query doesn't run
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
|
||||
expect(mockedApiClient.fetchWatchedItems).not.toHaveBeenCalled();
|
||||
expect(result.current.isLoading).toBe(false);
|
||||
expect(result.current.isFetching).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle API error with error message', async () => {
|
||||
mockedApiClient.fetchWatchedItems.mockResolvedValue({
|
||||
ok: false,
|
||||
status: 401,
|
||||
json: () => Promise.resolve({ message: 'Unauthorized' }),
|
||||
} as Response);
|
||||
|
||||
const { result } = renderHook(() => useWatchedItemsQuery(true), { wrapper });
|
||||
|
||||
await waitFor(() => expect(result.current.isError).toBe(true));
|
||||
|
||||
expect(result.current.error?.message).toBe('Unauthorized');
|
||||
});
|
||||
|
||||
it('should handle API error without message', async () => {
|
||||
mockedApiClient.fetchWatchedItems.mockResolvedValue({
|
||||
ok: false,
|
||||
status: 500,
|
||||
json: () => Promise.reject(new Error('Parse error')),
|
||||
} as Response);
|
||||
|
||||
const { result } = renderHook(() => useWatchedItemsQuery(true), { wrapper });
|
||||
|
||||
await waitFor(() => expect(result.current.isError).toBe(true));
|
||||
|
||||
expect(result.current.error?.message).toBe('Request failed with status 500');
|
||||
});
|
||||
|
||||
it('should return empty array for no watched items', async () => {
|
||||
mockedApiClient.fetchWatchedItems.mockResolvedValue({
|
||||
ok: true,
|
||||
json: () => Promise.resolve([]),
|
||||
} as Response);
|
||||
|
||||
const { result } = renderHook(() => useWatchedItemsQuery(true), { wrapper });
|
||||
|
||||
await waitFor(() => expect(result.current.isSuccess).toBe(true));
|
||||
|
||||
expect(result.current.data).toEqual([]);
|
||||
});
|
||||
});
|
||||
168
src/schemas/flyer.schemas.test.ts
Normal file
168
src/schemas/flyer.schemas.test.ts
Normal file
@@ -0,0 +1,168 @@
|
||||
// src/schemas/flyer.schemas.test.ts
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { flyerInsertSchema, flyerDbInsertSchema } from './flyer.schemas';
|
||||
|
||||
describe('flyerInsertSchema', () => {
|
||||
const validFlyer = {
|
||||
file_name: 'flyer.jpg',
|
||||
image_url: 'https://example.com/flyer.jpg',
|
||||
icon_url: 'https://example.com/icon.jpg',
|
||||
checksum: 'a'.repeat(64),
|
||||
store_name: 'Test Store',
|
||||
valid_from: '2023-01-01T00:00:00Z',
|
||||
valid_to: '2023-01-07T00:00:00Z',
|
||||
store_address: '123 Main St',
|
||||
status: 'processed',
|
||||
item_count: 10,
|
||||
uploaded_by: '123e4567-e89b-12d3-a456-426614174000',
|
||||
};
|
||||
|
||||
it('should validate a correct flyer object', () => {
|
||||
const result = flyerInsertSchema.safeParse(validFlyer);
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it('should fail if file_name is missing or empty', () => {
|
||||
const invalid = { ...validFlyer, file_name: '' };
|
||||
const result = flyerInsertSchema.safeParse(invalid);
|
||||
expect(result.success).toBe(false);
|
||||
if (!result.success) {
|
||||
expect(result.error.issues[0].message).toBe('File name is required');
|
||||
}
|
||||
});
|
||||
|
||||
it('should fail if image_url is invalid', () => {
|
||||
const invalid = { ...validFlyer, image_url: 'ftp://invalid.com' };
|
||||
const result = flyerInsertSchema.safeParse(invalid);
|
||||
expect(result.success).toBe(false);
|
||||
if (!result.success) {
|
||||
expect(result.error.issues[0].message).toBe(
|
||||
'Flyer image URL must be a valid HTTP or HTTPS URL',
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it('should fail if icon_url is invalid', () => {
|
||||
const invalid = { ...validFlyer, icon_url: 'not-a-url' };
|
||||
const result = flyerInsertSchema.safeParse(invalid);
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
|
||||
it('should fail if checksum length is incorrect', () => {
|
||||
const invalid = { ...validFlyer, checksum: 'abc' };
|
||||
const result = flyerInsertSchema.safeParse(invalid);
|
||||
expect(result.success).toBe(false);
|
||||
if (!result.success) {
|
||||
expect(result.error.issues[0].message).toBe('Checksum must be 64 characters');
|
||||
}
|
||||
});
|
||||
|
||||
it('should fail if checksum is not hex', () => {
|
||||
const invalid = { ...validFlyer, checksum: 'z'.repeat(64) };
|
||||
const result = flyerInsertSchema.safeParse(invalid);
|
||||
expect(result.success).toBe(false);
|
||||
if (!result.success) {
|
||||
expect(result.error.issues[0].message).toBe('Checksum must be a valid hexadecimal string');
|
||||
}
|
||||
});
|
||||
|
||||
it('should allow null checksum', () => {
|
||||
const valid = { ...validFlyer, checksum: null };
|
||||
const result = flyerInsertSchema.safeParse(valid);
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it('should fail if store_name is missing', () => {
|
||||
const invalid = { ...validFlyer, store_name: '' };
|
||||
const result = flyerInsertSchema.safeParse(invalid);
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
|
||||
it('should validate valid_from and valid_to as datetimes', () => {
|
||||
const invalid = { ...validFlyer, valid_from: 'not-a-date' };
|
||||
const result = flyerInsertSchema.safeParse(invalid);
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
|
||||
it('should allow null valid_from, valid_to, store_address', () => {
|
||||
const valid = {
|
||||
...validFlyer,
|
||||
valid_from: null,
|
||||
valid_to: null,
|
||||
store_address: null,
|
||||
};
|
||||
const result = flyerInsertSchema.safeParse(valid);
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it('should validate status enum', () => {
|
||||
const invalid = { ...validFlyer, status: 'invalid_status' };
|
||||
const result = flyerInsertSchema.safeParse(invalid);
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
|
||||
it('should fail if item_count is negative', () => {
|
||||
const invalid = { ...validFlyer, item_count: -1 };
|
||||
const result = flyerInsertSchema.safeParse(invalid);
|
||||
expect(result.success).toBe(false);
|
||||
if (!result.success) {
|
||||
expect(result.error.issues[0].message).toBe('Item count must be non-negative');
|
||||
}
|
||||
});
|
||||
|
||||
it('should validate uploaded_by as UUID if present', () => {
|
||||
const invalid = { ...validFlyer, uploaded_by: 'not-a-uuid' };
|
||||
const result = flyerInsertSchema.safeParse(invalid);
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
|
||||
it('should allow null or undefined uploaded_by', () => {
|
||||
const validNull = { ...validFlyer, uploaded_by: null };
|
||||
expect(flyerInsertSchema.safeParse(validNull).success).toBe(true);
|
||||
|
||||
const validUndefined = { ...validFlyer, uploaded_by: undefined };
|
||||
expect(flyerInsertSchema.safeParse(validUndefined).success).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('flyerDbInsertSchema', () => {
|
||||
const validDbFlyer = {
|
||||
file_name: 'flyer.jpg',
|
||||
image_url: 'https://example.com/flyer.jpg',
|
||||
icon_url: 'https://example.com/icon.jpg',
|
||||
checksum: 'a'.repeat(64),
|
||||
store_id: 1,
|
||||
valid_from: '2023-01-01T00:00:00Z',
|
||||
valid_to: '2023-01-07T00:00:00Z',
|
||||
store_address: '123 Main St',
|
||||
status: 'processed',
|
||||
item_count: 10,
|
||||
uploaded_by: '123e4567-e89b-12d3-a456-426614174000',
|
||||
};
|
||||
|
||||
it('should validate a correct DB flyer object', () => {
|
||||
const result = flyerDbInsertSchema.safeParse(validDbFlyer);
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it('should fail if store_id is missing', () => {
|
||||
const { store_id, ...invalid } = validDbFlyer;
|
||||
const result = flyerDbInsertSchema.safeParse(invalid);
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
|
||||
it('should fail if store_id is not positive', () => {
|
||||
const invalid = { ...validDbFlyer, store_id: 0 };
|
||||
const result = flyerDbInsertSchema.safeParse(invalid);
|
||||
expect(result.success).toBe(false);
|
||||
if (!result.success) {
|
||||
expect(result.error.issues[0].message).toBe('Store ID must be a positive integer');
|
||||
}
|
||||
});
|
||||
|
||||
it('should fail if store_id is not an integer', () => {
|
||||
const invalid = { ...validDbFlyer, store_id: 1.5 };
|
||||
const result = flyerDbInsertSchema.safeParse(invalid);
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -244,8 +244,9 @@ describe('Flyer DB Service', () => {
|
||||
await expect(flyerRepo.insertFlyer(flyerData, mockLogger)).rejects.toThrow(
|
||||
CheckConstraintError,
|
||||
);
|
||||
// The implementation now generates a more detailed error message.
|
||||
await expect(flyerRepo.insertFlyer(flyerData, mockLogger)).rejects.toThrow(
|
||||
'Invalid URL format provided for image or icon.',
|
||||
"[URL_CHECK_FAIL] Invalid URL format. Image: 'https://example.com/not-a-url', Icon: 'null'",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
// src/services/logger.server.test.ts
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
|
||||
// Unmock the module we are testing to override the global mock from setupFiles.
|
||||
vi.unmock('./logger.server');
|
||||
|
||||
// Mock pino before importing the logger
|
||||
const pinoMock = vi.fn(() => ({
|
||||
info: vi.fn(),
|
||||
@@ -25,14 +28,25 @@ describe('Server Logger', () => {
|
||||
it('should initialize pino with the correct level for production', async () => {
|
||||
vi.stubEnv('NODE_ENV', 'production');
|
||||
await import('./logger.server');
|
||||
expect(pinoMock).toHaveBeenCalledWith(expect.objectContaining({ level: 'info' }));
|
||||
expect(pinoMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ level: 'info', transport: undefined }),
|
||||
);
|
||||
});
|
||||
|
||||
it('should initialize pino with pretty-print transport for development', async () => {
|
||||
vi.stubEnv('NODE_ENV', 'development');
|
||||
await import('./logger.server');
|
||||
expect(pinoMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ transport: expect.any(Object) }),
|
||||
expect.objectContaining({ level: 'debug', transport: expect.any(Object) }),
|
||||
);
|
||||
});
|
||||
|
||||
it('should initialize pino with debug level and no transport for test', async () => {
|
||||
// This is the default for vitest, but we stub it for clarity.
|
||||
vi.stubEnv('NODE_ENV', 'test');
|
||||
await import('./logger.server');
|
||||
expect(pinoMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ level: 'debug', transport: undefined }),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -59,17 +59,40 @@ vi.mock('../../services/storage/storageService', () => {
|
||||
};
|
||||
});
|
||||
|
||||
// FIX: Import the singleton instance directly to spy on it
|
||||
import { aiService } from '../../services/aiService.server';
|
||||
|
||||
|
||||
/**
|
||||
* @vitest-environment node
|
||||
*/
|
||||
|
||||
const { mockExtractCoreData } = vi.hoisted(() => ({
|
||||
mockExtractCoreData: vi.fn(),
|
||||
}));
|
||||
// CRITICAL: This mock function must be declared with vi.hoisted() to ensure it's available
|
||||
// at the module level BEFORE any imports are resolved.
|
||||
const { mockExtractCoreData } = vi.hoisted(() => {
|
||||
return {
|
||||
mockExtractCoreData: vi.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
// CRITICAL: Mock the aiService module BEFORE any other imports that depend on it.
|
||||
// This ensures workers get the mocked version, not the real one.
|
||||
// We use a partial mock that only overrides extractCoreDataFromFlyerImage.
|
||||
vi.mock('../../services/aiService.server', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('../../services/aiService.server')>();
|
||||
|
||||
// Create a proxy around the actual aiService that intercepts extractCoreDataFromFlyerImage
|
||||
const proxiedAiService = new Proxy(actual.aiService, {
|
||||
get(target, prop) {
|
||||
if (prop === 'extractCoreDataFromFlyerImage') {
|
||||
return mockExtractCoreData;
|
||||
}
|
||||
// For all other properties/methods, return the original
|
||||
return target[prop as keyof typeof target];
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
...actual,
|
||||
aiService: proxiedAiService,
|
||||
};
|
||||
});
|
||||
|
||||
// Mock the connection DB service to intercept withTransaction.
|
||||
// This is crucial because FlyerPersistenceService imports directly from connection.db,
|
||||
@@ -99,9 +122,8 @@ describe('Flyer Processing Background Job Integration Test', () => {
|
||||
process.env.FRONTEND_URL = 'https://example.com';
|
||||
console.error('[TEST SETUP] FRONTEND_URL stubbed to:', process.env.FRONTEND_URL);
|
||||
|
||||
// FIX: Spy on the actual singleton instance. This ensures that when the worker
|
||||
// imports 'aiService', it gets the instance we are controlling here.
|
||||
vi.spyOn(aiService, 'extractCoreDataFromFlyerImage').mockImplementation(mockExtractCoreData);
|
||||
// NOTE: The aiService mock is now set up via vi.mock() at the module level (above).
|
||||
// This ensures workers get the mocked version when they import aiService.
|
||||
|
||||
// NEW: Import workers to start them IN-PROCESS.
|
||||
// This ensures they run in the same memory space as our mocks.
|
||||
|
||||
@@ -9,6 +9,29 @@ let server: Server;
|
||||
// This will hold the single database pool instance for the entire test run.
|
||||
let globalPool: ReturnType<typeof getPool> | null = null;
|
||||
|
||||
/**
|
||||
* Cleans all BullMQ queues to ensure no stale jobs from previous test runs.
|
||||
* This is critical because old jobs with outdated error messages can pollute test results.
|
||||
*/
|
||||
async function cleanAllQueues() {
|
||||
console.log(`[PID:${process.pid}] Cleaning all BullMQ queues...`);
|
||||
const { flyerQueue, cleanupQueue, emailQueue, analyticsQueue, weeklyAnalyticsQueue, tokenCleanupQueue } = await import('../../services/queues.server');
|
||||
|
||||
const queues = [flyerQueue, cleanupQueue, emailQueue, analyticsQueue, weeklyAnalyticsQueue, tokenCleanupQueue];
|
||||
|
||||
for (const queue of queues) {
|
||||
try {
|
||||
// obliterate() removes ALL data associated with the queue from Redis
|
||||
await queue.obliterate({ force: true });
|
||||
console.log(` ✅ Cleaned queue: ${queue.name}`);
|
||||
} catch (error) {
|
||||
// Log but don't fail - the queue might not exist yet
|
||||
console.log(` ⚠️ Could not clean queue ${queue.name}: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||
}
|
||||
}
|
||||
console.log(`✅ [PID:${process.pid}] All queues cleaned.`);
|
||||
}
|
||||
|
||||
export async function setup() {
|
||||
// Ensure we are in the correct environment for these tests.
|
||||
process.env.NODE_ENV = 'test';
|
||||
@@ -17,6 +40,10 @@ export async function setup() {
|
||||
|
||||
console.log(`\n--- [PID:${process.pid}] Running Integration Test GLOBAL Setup ---`);
|
||||
|
||||
// CRITICAL: Clean all queues BEFORE running any tests to remove stale jobs
|
||||
// from previous test runs that may have outdated error messages.
|
||||
await cleanAllQueues();
|
||||
|
||||
// The integration setup is now the single source of truth for preparing the test DB.
|
||||
// It runs the same seed script that `npm run db:reset:test` used.
|
||||
try {
|
||||
|
||||
@@ -1,14 +1,23 @@
|
||||
// src/utils/imageProcessor.test.ts
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import type { Logger } from 'pino';
|
||||
import path from 'path';
|
||||
|
||||
// --- Hoisted Mocks ---
|
||||
const mocks = vi.hoisted(() => {
|
||||
// Create a chainable mock for the sharp library
|
||||
const toFile = vi.fn().mockResolvedValue({ info: 'mocked' });
|
||||
|
||||
// Chain for generateFlyerIcon
|
||||
const webp = vi.fn(() => ({ toFile }));
|
||||
const resize = vi.fn(() => ({ webp }));
|
||||
const sharpInstance = { resize };
|
||||
|
||||
// Chain for processAndSaveImage
|
||||
const png = vi.fn(() => ({ toFile }));
|
||||
const jpeg = vi.fn(() => ({ png }));
|
||||
const withMetadata = vi.fn(() => ({ jpeg }));
|
||||
|
||||
const sharpInstance = { resize, withMetadata };
|
||||
|
||||
// Mock the sharp function and attach static properties required by the implementation
|
||||
const sharp = vi.fn(() => sharpInstance);
|
||||
@@ -18,6 +27,9 @@ const mocks = vi.hoisted(() => {
|
||||
sharp: sharp,
|
||||
resize,
|
||||
webp,
|
||||
withMetadata,
|
||||
jpeg,
|
||||
png,
|
||||
toFile,
|
||||
mkdir: vi.fn().mockResolvedValue(undefined),
|
||||
};
|
||||
@@ -54,7 +66,7 @@ const logger = createMockLogger();
|
||||
vi.mock('../services/logger.server', () => ({ logger }));
|
||||
|
||||
// --- Import the function to be tested ---
|
||||
import { generateFlyerIcon } from './imageProcessor';
|
||||
import { generateFlyerIcon, processAndSaveImage } from './imageProcessor';
|
||||
|
||||
describe('generateFlyerIcon', () => {
|
||||
beforeEach(() => {
|
||||
@@ -95,3 +107,48 @@ describe('generateFlyerIcon', () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('processAndSaveImage', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
// Ensure toFile is in a resolved state
|
||||
mocks.toFile.mockResolvedValue({ info: 'mocked' });
|
||||
});
|
||||
|
||||
it('should process the image, strip metadata, and return the new filename', async () => {
|
||||
const sourcePath = '/tmp/upload/original.jpg';
|
||||
const destinationDir = '/var/www/images';
|
||||
const originalFileName = 'original.jpg';
|
||||
|
||||
const result = await processAndSaveImage(sourcePath, destinationDir, originalFileName, logger);
|
||||
|
||||
// Check that the destination directory was created
|
||||
expect(mocks.mkdir).toHaveBeenCalledWith(destinationDir, { recursive: true });
|
||||
|
||||
// Check that sharp was called with the correct source
|
||||
expect(mocks.sharp).toHaveBeenCalledWith(sourcePath, { failOn: 'none' });
|
||||
|
||||
// Check the processing chain
|
||||
expect(mocks.withMetadata).toHaveBeenCalledWith({});
|
||||
expect(mocks.jpeg).toHaveBeenCalledWith({ quality: 85, mozjpeg: true });
|
||||
expect(mocks.png).toHaveBeenCalledWith({ compressionLevel: 8, quality: 85 });
|
||||
expect(mocks.toFile).toHaveBeenCalledWith(expect.stringContaining(path.join(destinationDir, 'original-')));
|
||||
|
||||
// Check the returned filename format (original-timestamp.jpg)
|
||||
expect(result).toMatch(/^original-\d+\.jpg$/);
|
||||
});
|
||||
|
||||
it('should throw an error if sharp fails to process the image', async () => {
|
||||
const sharpError = new Error('Processing failed');
|
||||
mocks.toFile.mockRejectedValueOnce(sharpError);
|
||||
|
||||
await expect(
|
||||
processAndSaveImage('/path/img.jpg', '/dest', 'img.jpg', logger),
|
||||
).rejects.toThrow('Failed to process image img.jpg.');
|
||||
|
||||
expect(logger.error).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ err: sharpError, sourcePath: '/path/img.jpg' }),
|
||||
'An error occurred during image processing and saving.',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -20,57 +20,100 @@ const createMockLogger = (): Logger =>
|
||||
|
||||
describe('serverUtils', () => {
|
||||
describe('getBaseUrl', () => {
|
||||
const originalEnv = process.env;
|
||||
let mockLogger: Logger;
|
||||
|
||||
// Store original env values to restore after tests
|
||||
const originalFrontendUrl = process.env.FRONTEND_URL;
|
||||
const originalBaseUrl = process.env.BASE_URL;
|
||||
const originalNodeEnv = process.env.NODE_ENV;
|
||||
const originalPort = process.env.PORT;
|
||||
|
||||
beforeEach(() => {
|
||||
// Reset mocks and environment variables before each test for isolation
|
||||
vi.resetModules();
|
||||
process.env = { ...originalEnv };
|
||||
vi.unstubAllEnvs();
|
||||
// CRITICAL: Clear env vars that might be set globally (e.g., from vitest config)
|
||||
// vi.unstubAllEnvs() only removes vars set via vi.stubEnv(), not direct assignments
|
||||
delete process.env.FRONTEND_URL;
|
||||
delete process.env.BASE_URL;
|
||||
delete process.env.NODE_ENV;
|
||||
delete process.env.PORT;
|
||||
mockLogger = createMockLogger();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// Restore original environment variables after each test
|
||||
process.env = originalEnv;
|
||||
vi.unstubAllEnvs();
|
||||
// Restore original values
|
||||
if (originalFrontendUrl !== undefined) process.env.FRONTEND_URL = originalFrontendUrl;
|
||||
else delete process.env.FRONTEND_URL;
|
||||
if (originalBaseUrl !== undefined) process.env.BASE_URL = originalBaseUrl;
|
||||
else delete process.env.BASE_URL;
|
||||
if (originalNodeEnv !== undefined) process.env.NODE_ENV = originalNodeEnv;
|
||||
else delete process.env.NODE_ENV;
|
||||
if (originalPort !== undefined) process.env.PORT = originalPort;
|
||||
else delete process.env.PORT;
|
||||
});
|
||||
|
||||
it('should use FRONTEND_URL if it is a valid URL', () => {
|
||||
process.env.FRONTEND_URL = 'https://valid.example.com';
|
||||
vi.stubEnv('FRONTEND_URL', 'https://valid.example.com');
|
||||
const baseUrl = getBaseUrl(mockLogger);
|
||||
expect(baseUrl).toBe('https://valid.example.com');
|
||||
expect(mockLogger.warn).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should trim a trailing slash from FRONTEND_URL', () => {
|
||||
process.env.FRONTEND_URL = 'https://valid.example.com/';
|
||||
vi.stubEnv('FRONTEND_URL', 'https://valid.example.com/');
|
||||
const baseUrl = getBaseUrl(mockLogger);
|
||||
expect(baseUrl).toBe('https://valid.example.com');
|
||||
});
|
||||
|
||||
it('should use BASE_URL if FRONTEND_URL is not set', () => {
|
||||
delete process.env.FRONTEND_URL;
|
||||
process.env.BASE_URL = 'https://base.example.com';
|
||||
vi.stubEnv('BASE_URL', 'https://base.example.com');
|
||||
const baseUrl = getBaseUrl(mockLogger);
|
||||
expect(baseUrl).toBe('https://base.example.com');
|
||||
expect(mockLogger.warn).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should fall back to example.com with default port 3000 if no URL is provided', () => {
|
||||
delete process.env.FRONTEND_URL;
|
||||
delete process.env.BASE_URL;
|
||||
delete process.env.PORT;
|
||||
it('should fall back to localhost with default port 3000 in test environment', () => {
|
||||
vi.stubEnv('NODE_ENV', 'test');
|
||||
const baseUrl = getBaseUrl(mockLogger);
|
||||
expect(baseUrl).toBe('https://example.com:3000');
|
||||
expect(baseUrl).toBe('http://localhost:3000');
|
||||
expect(mockLogger.warn).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should log a warning and fall back if FRONTEND_URL is invalid (does not start with http)', () => {
|
||||
process.env.FRONTEND_URL = 'invalid.url.com';
|
||||
it('should fall back to example.com in non-test environment', () => {
|
||||
vi.stubEnv('NODE_ENV', 'development');
|
||||
vi.stubEnv('PORT', '4000');
|
||||
const baseUrl = getBaseUrl(mockLogger);
|
||||
expect(baseUrl).toBe('https://example.com:3000');
|
||||
expect(baseUrl).toBe('http://example.com:4000');
|
||||
expect(mockLogger.warn).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should log a warning and fall back to localhost if FRONTEND_URL is invalid in test env', () => {
|
||||
vi.stubEnv('NODE_ENV', 'test');
|
||||
vi.stubEnv('FRONTEND_URL', 'invalid.url.com');
|
||||
const baseUrl = getBaseUrl(mockLogger);
|
||||
expect(baseUrl).toBe('http://localhost:3000');
|
||||
expect(mockLogger.warn).toHaveBeenCalledWith(
|
||||
"[getBaseUrl] FRONTEND_URL/BASE_URL is invalid or incomplete ('invalid.url.com'). Falling back to default local URL: https://example.com:3000",
|
||||
"[getBaseUrl] FRONTEND_URL/BASE_URL is invalid or incomplete ('invalid.url.com'). Falling back to: http://localhost:3000",
|
||||
);
|
||||
});
|
||||
|
||||
it('should log a warning and fall back to example.com if FRONTEND_URL is invalid in non-test env', () => {
|
||||
vi.stubEnv('NODE_ENV', 'production');
|
||||
vi.stubEnv('FRONTEND_URL', 'invalid.url.com');
|
||||
const baseUrl = getBaseUrl(mockLogger);
|
||||
expect(baseUrl).toBe('http://example.com:3000');
|
||||
expect(mockLogger.warn).toHaveBeenCalledWith(
|
||||
"[getBaseUrl] FRONTEND_URL/BASE_URL is invalid or incomplete ('invalid.url.com'). Falling back to: http://example.com:3000",
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw an error if the final URL is invalid', () => {
|
||||
vi.stubEnv('FRONTEND_URL', 'http:invalid');
|
||||
expect(() => getBaseUrl(mockLogger)).toThrow(
|
||||
`[getBaseUrl] Generated URL 'http:invalid' does not match required pattern (must start with http:// or https://)`,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user