more req work
All checks were successful
Deploy to Test Environment / deploy-to-test (push) Successful in 26m51s
All checks were successful
Deploy to Test Environment / deploy-to-test (push) Successful in 26m51s
This commit is contained in:
@@ -4,15 +4,15 @@ import { renderHook } from '@testing-library/react';
|
|||||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||||
import { useMasterItems } from './useMasterItems';
|
import { useMasterItems } from './useMasterItems';
|
||||||
import { MasterItemsProvider } from '../providers/MasterItemsProvider';
|
import { MasterItemsProvider } from '../providers/MasterItemsProvider';
|
||||||
import { useApiOnMount } from './useApiOnMount';
|
import { useMasterItemsQuery } from './queries/useMasterItemsQuery';
|
||||||
import type { MasterGroceryItem } from '../types';
|
import type { MasterGroceryItem } from '../types';
|
||||||
import { createMockMasterGroceryItem } from '../tests/utils/mockFactories';
|
import { createMockMasterGroceryItem } from '../tests/utils/mockFactories';
|
||||||
|
|
||||||
// 1. Mock the useApiOnMount hook, which is the dependency of our provider.
|
// 1. Mock the useMasterItemsQuery hook, which is the dependency of our provider.
|
||||||
vi.mock('./useApiOnMount');
|
vi.mock('./queries/useMasterItemsQuery');
|
||||||
|
|
||||||
// 2. Create a typed mock for type safety and autocompletion.
|
// 2. Create a typed mock for type safety and autocompletion.
|
||||||
const mockedUseApiOnMount = vi.mocked(useApiOnMount);
|
const mockedUseMasterItemsQuery = vi.mocked(useMasterItemsQuery);
|
||||||
|
|
||||||
// 3. A simple wrapper component that renders our provider.
|
// 3. A simple wrapper component that renders our provider.
|
||||||
// This is necessary because the useMasterItems hook needs to be a child of MasterItemsProvider.
|
// This is necessary because the useMasterItems hook needs to be a child of MasterItemsProvider.
|
||||||
@@ -42,13 +42,11 @@ describe('useMasterItems Hook and MasterItemsProvider', () => {
|
|||||||
|
|
||||||
it('should return the initial loading state correctly', () => {
|
it('should return the initial loading state correctly', () => {
|
||||||
// Arrange: Configure the mocked hook to return a loading state.
|
// Arrange: Configure the mocked hook to return a loading state.
|
||||||
mockedUseApiOnMount.mockReturnValue({
|
mockedUseMasterItemsQuery.mockReturnValue({
|
||||||
data: null,
|
data: undefined,
|
||||||
loading: true,
|
isLoading: true,
|
||||||
error: null,
|
error: null,
|
||||||
isRefetching: false,
|
} as any);
|
||||||
reset: vi.fn(),
|
|
||||||
});
|
|
||||||
|
|
||||||
// Act: Render the hook within the provider wrapper.
|
// Act: Render the hook within the provider wrapper.
|
||||||
const { result } = renderHook(() => useMasterItems(), { wrapper });
|
const { result } = renderHook(() => useMasterItems(), { wrapper });
|
||||||
@@ -75,13 +73,11 @@ describe('useMasterItems Hook and MasterItemsProvider', () => {
|
|||||||
category_name: 'Bakery',
|
category_name: 'Bakery',
|
||||||
}),
|
}),
|
||||||
];
|
];
|
||||||
mockedUseApiOnMount.mockReturnValue({
|
mockedUseMasterItemsQuery.mockReturnValue({
|
||||||
data: mockItems,
|
data: mockItems,
|
||||||
loading: false,
|
isLoading: false,
|
||||||
error: null,
|
error: null,
|
||||||
isRefetching: false,
|
} as any);
|
||||||
reset: vi.fn(),
|
|
||||||
});
|
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
const { result } = renderHook(() => useMasterItems(), { wrapper });
|
const { result } = renderHook(() => useMasterItems(), { wrapper });
|
||||||
@@ -95,13 +91,11 @@ describe('useMasterItems Hook and MasterItemsProvider', () => {
|
|||||||
it('should return an error state if the fetch fails', () => {
|
it('should return an error state if the fetch fails', () => {
|
||||||
// Arrange: Mock a failed data fetch.
|
// Arrange: Mock a failed data fetch.
|
||||||
const mockError = new Error('Failed to fetch master items');
|
const mockError = new Error('Failed to fetch master items');
|
||||||
mockedUseApiOnMount.mockReturnValue({
|
mockedUseMasterItemsQuery.mockReturnValue({
|
||||||
data: null,
|
data: undefined,
|
||||||
loading: false,
|
isLoading: false,
|
||||||
error: mockError,
|
error: mockError,
|
||||||
isRefetching: false,
|
} as any);
|
||||||
reset: vi.fn(),
|
|
||||||
});
|
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
const { result } = renderHook(() => useMasterItems(), { wrapper });
|
const { result } = renderHook(() => useMasterItems(), { wrapper });
|
||||||
|
|||||||
@@ -5,7 +5,8 @@ import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|||||||
import { useUserData } from './useUserData';
|
import { useUserData } from './useUserData';
|
||||||
import { useAuth } from './useAuth';
|
import { useAuth } from './useAuth';
|
||||||
import { UserDataProvider } from '../providers/UserDataProvider';
|
import { UserDataProvider } from '../providers/UserDataProvider';
|
||||||
import { useApiOnMount } from './useApiOnMount';
|
import { useWatchedItemsQuery } from './queries/useWatchedItemsQuery';
|
||||||
|
import { useShoppingListsQuery } from './queries/useShoppingListsQuery';
|
||||||
import type { UserProfile } from '../types';
|
import type { UserProfile } from '../types';
|
||||||
import {
|
import {
|
||||||
createMockMasterGroceryItem,
|
createMockMasterGroceryItem,
|
||||||
@@ -15,11 +16,13 @@ import {
|
|||||||
|
|
||||||
// 1. Mock the hook's dependencies
|
// 1. Mock the hook's dependencies
|
||||||
vi.mock('../hooks/useAuth');
|
vi.mock('../hooks/useAuth');
|
||||||
vi.mock('./useApiOnMount');
|
vi.mock('./queries/useWatchedItemsQuery');
|
||||||
|
vi.mock('./queries/useShoppingListsQuery');
|
||||||
|
|
||||||
// 2. Create typed mocks for type safety and autocompletion
|
// 2. Create typed mocks for type safety and autocompletion
|
||||||
const mockedUseAuth = vi.mocked(useAuth);
|
const mockedUseAuth = vi.mocked(useAuth);
|
||||||
const mockedUseApiOnMount = vi.mocked(useApiOnMount);
|
const mockedUseWatchedItemsQuery = vi.mocked(useWatchedItemsQuery);
|
||||||
|
const mockedUseShoppingListsQuery = vi.mocked(useShoppingListsQuery);
|
||||||
|
|
||||||
// 3. A simple wrapper component that renders our provider.
|
// 3. A simple wrapper component that renders our provider.
|
||||||
// This is necessary because the useUserData hook needs to be a child of UserDataProvider.
|
// This is necessary because the useUserData hook needs to be a child of UserDataProvider.
|
||||||
@@ -71,13 +74,16 @@ describe('useUserData Hook and UserDataProvider', () => {
|
|||||||
updateProfile: vi.fn(),
|
updateProfile: vi.fn(),
|
||||||
});
|
});
|
||||||
// Arrange: Mock the return value of the inner hooks.
|
// Arrange: Mock the return value of the inner hooks.
|
||||||
mockedUseApiOnMount.mockReturnValue({
|
mockedUseWatchedItemsQuery.mockReturnValue({
|
||||||
data: null,
|
data: [],
|
||||||
loading: false,
|
isLoading: false,
|
||||||
error: null,
|
error: null,
|
||||||
isRefetching: false,
|
} as any);
|
||||||
reset: vi.fn(),
|
mockedUseShoppingListsQuery.mockReturnValue({
|
||||||
});
|
data: [],
|
||||||
|
isLoading: false,
|
||||||
|
error: null,
|
||||||
|
} as any);
|
||||||
|
|
||||||
// Act: Render the hook within the provider wrapper.
|
// Act: Render the hook within the provider wrapper.
|
||||||
const { result } = renderHook(() => useUserData(), { wrapper });
|
const { result } = renderHook(() => useUserData(), { wrapper });
|
||||||
@@ -87,10 +93,9 @@ describe('useUserData Hook and UserDataProvider', () => {
|
|||||||
expect(result.current.watchedItems).toEqual([]);
|
expect(result.current.watchedItems).toEqual([]);
|
||||||
expect(result.current.shoppingLists).toEqual([]);
|
expect(result.current.shoppingLists).toEqual([]);
|
||||||
expect(result.current.error).toBeNull();
|
expect(result.current.error).toBeNull();
|
||||||
// Assert: Check that useApiOnMount was called with `enabled: false`.
|
// Assert: Check that queries were disabled (called with false)
|
||||||
expect(mockedUseApiOnMount).toHaveBeenCalledWith(expect.any(Function), [null], {
|
expect(mockedUseWatchedItemsQuery).toHaveBeenCalledWith(false);
|
||||||
enabled: false,
|
expect(mockedUseShoppingListsQuery).toHaveBeenCalledWith(false);
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return loading state when user is authenticated and data is fetching', () => {
|
it('should return loading state when user is authenticated and data is fetching', () => {
|
||||||
@@ -104,21 +109,16 @@ describe('useUserData Hook and UserDataProvider', () => {
|
|||||||
updateProfile: vi.fn(),
|
updateProfile: vi.fn(),
|
||||||
});
|
});
|
||||||
// Arrange: Mock one of the inner hooks to be in a loading state.
|
// Arrange: Mock one of the inner hooks to be in a loading state.
|
||||||
mockedUseApiOnMount
|
mockedUseWatchedItemsQuery.mockReturnValue({
|
||||||
.mockReturnValueOnce({
|
data: undefined,
|
||||||
data: null,
|
isLoading: true,
|
||||||
loading: true,
|
error: null,
|
||||||
error: null,
|
} as any);
|
||||||
isRefetching: false,
|
mockedUseShoppingListsQuery.mockReturnValue({
|
||||||
reset: vi.fn(),
|
data: undefined,
|
||||||
}) // watched items
|
isLoading: false,
|
||||||
.mockReturnValueOnce({
|
error: null,
|
||||||
data: null,
|
} as any);
|
||||||
loading: false,
|
|
||||||
error: null,
|
|
||||||
isRefetching: false,
|
|
||||||
reset: vi.fn(),
|
|
||||||
}); // shopping lists
|
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
const { result } = renderHook(() => useUserData(), { wrapper });
|
const { result } = renderHook(() => useUserData(), { wrapper });
|
||||||
@@ -138,21 +138,16 @@ describe('useUserData Hook and UserDataProvider', () => {
|
|||||||
updateProfile: vi.fn(),
|
updateProfile: vi.fn(),
|
||||||
});
|
});
|
||||||
// Arrange: Mock successful data fetches for both inner hooks.
|
// Arrange: Mock successful data fetches for both inner hooks.
|
||||||
mockedUseApiOnMount
|
mockedUseWatchedItemsQuery.mockReturnValue({
|
||||||
.mockReturnValueOnce({
|
data: mockWatchedItems,
|
||||||
data: mockWatchedItems,
|
isLoading: false,
|
||||||
loading: false,
|
error: null,
|
||||||
error: null,
|
} as any);
|
||||||
isRefetching: false,
|
mockedUseShoppingListsQuery.mockReturnValue({
|
||||||
reset: vi.fn(),
|
data: mockShoppingLists,
|
||||||
})
|
isLoading: false,
|
||||||
.mockReturnValueOnce({
|
error: null,
|
||||||
data: mockShoppingLists,
|
} as any);
|
||||||
loading: false,
|
|
||||||
error: null,
|
|
||||||
isRefetching: false,
|
|
||||||
reset: vi.fn(),
|
|
||||||
});
|
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
const { result } = renderHook(() => useUserData(), { wrapper });
|
const { result } = renderHook(() => useUserData(), { wrapper });
|
||||||
@@ -178,55 +173,16 @@ describe('useUserData Hook and UserDataProvider', () => {
|
|||||||
});
|
});
|
||||||
const mockError = new Error('Failed to fetch watched items');
|
const mockError = new Error('Failed to fetch watched items');
|
||||||
|
|
||||||
// Arrange: Mock the behavior persistently to handle re-renders.
|
mockedUseWatchedItemsQuery.mockReturnValue({
|
||||||
// We use mockImplementation to return based on call order in a loop or similar,
|
data: undefined,
|
||||||
// OR just use mockReturnValueOnce enough times.
|
isLoading: false,
|
||||||
// Since we don't know exact render count, mockImplementation is safer if valid.
|
error: mockError,
|
||||||
// But simplified: assuming 2 hooks called per render.
|
} as any);
|
||||||
|
mockedUseShoppingListsQuery.mockReturnValue({
|
||||||
// reset mocks to be sure
|
data: mockShoppingLists,
|
||||||
mockedUseApiOnMount.mockReset();
|
isLoading: false,
|
||||||
|
error: null,
|
||||||
// Define the sequence: 1st call (Watched) -> Error, 2nd call (Shopping) -> Success
|
} as any);
|
||||||
// We want this to persist for multiple renders.
|
|
||||||
mockedUseApiOnMount.mockImplementation((_fn) => {
|
|
||||||
// We can't easily distinguish based on 'fn' arg without inspecting it,
|
|
||||||
// but we know the order is Watched then Shopping in the provider.
|
|
||||||
// A simple toggle approach works if strict order is maintained.
|
|
||||||
// However, stateless mocks are better.
|
|
||||||
// Let's fallback to setting up "many" return values.
|
|
||||||
return { data: null, loading: false, error: null, isRefetching: false, reset: vi.fn() };
|
|
||||||
});
|
|
||||||
|
|
||||||
mockedUseApiOnMount
|
|
||||||
.mockReturnValueOnce({
|
|
||||||
data: null,
|
|
||||||
loading: false,
|
|
||||||
error: mockError,
|
|
||||||
isRefetching: false,
|
|
||||||
reset: vi.fn(),
|
|
||||||
}) // 1st render: Watched
|
|
||||||
.mockReturnValueOnce({
|
|
||||||
data: mockShoppingLists,
|
|
||||||
loading: false,
|
|
||||||
error: null,
|
|
||||||
isRefetching: false,
|
|
||||||
reset: vi.fn(),
|
|
||||||
}) // 1st render: Shopping
|
|
||||||
.mockReturnValueOnce({
|
|
||||||
data: null,
|
|
||||||
loading: false,
|
|
||||||
error: mockError,
|
|
||||||
isRefetching: false,
|
|
||||||
reset: vi.fn(),
|
|
||||||
}) // 2nd render: Watched
|
|
||||||
.mockReturnValueOnce({
|
|
||||||
data: mockShoppingLists,
|
|
||||||
loading: false,
|
|
||||||
error: null,
|
|
||||||
isRefetching: false,
|
|
||||||
reset: vi.fn(),
|
|
||||||
}); // 2nd render: Shopping
|
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
const { result } = renderHook(() => useUserData(), { wrapper });
|
const { result } = renderHook(() => useUserData(), { wrapper });
|
||||||
@@ -252,21 +208,16 @@ describe('useUserData Hook and UserDataProvider', () => {
|
|||||||
logout: vi.fn(),
|
logout: vi.fn(),
|
||||||
updateProfile: vi.fn(),
|
updateProfile: vi.fn(),
|
||||||
});
|
});
|
||||||
mockedUseApiOnMount
|
mockedUseWatchedItemsQuery.mockReturnValue({
|
||||||
.mockReturnValueOnce({
|
data: mockWatchedItems,
|
||||||
data: mockWatchedItems,
|
isLoading: false,
|
||||||
loading: false,
|
error: null,
|
||||||
error: null,
|
} as any);
|
||||||
isRefetching: false,
|
mockedUseShoppingListsQuery.mockReturnValue({
|
||||||
reset: vi.fn(),
|
data: mockShoppingLists,
|
||||||
})
|
isLoading: false,
|
||||||
.mockReturnValueOnce({
|
error: null,
|
||||||
data: mockShoppingLists,
|
} as any);
|
||||||
loading: false,
|
|
||||||
error: null,
|
|
||||||
isRefetching: false,
|
|
||||||
reset: vi.fn(),
|
|
||||||
});
|
|
||||||
const { result, rerender } = renderHook(() => useUserData(), { wrapper });
|
const { result, rerender } = renderHook(() => useUserData(), { wrapper });
|
||||||
await waitFor(() => expect(result.current.watchedItems).not.toEqual([]));
|
await waitFor(() => expect(result.current.watchedItems).not.toEqual([]));
|
||||||
|
|
||||||
@@ -279,6 +230,18 @@ describe('useUserData Hook and UserDataProvider', () => {
|
|||||||
logout: vi.fn(),
|
logout: vi.fn(),
|
||||||
updateProfile: vi.fn(),
|
updateProfile: vi.fn(),
|
||||||
});
|
});
|
||||||
|
// Update mocks to return empty data for the logged out state
|
||||||
|
mockedUseWatchedItemsQuery.mockReturnValue({
|
||||||
|
data: [],
|
||||||
|
isLoading: false,
|
||||||
|
error: null,
|
||||||
|
} as any);
|
||||||
|
mockedUseShoppingListsQuery.mockReturnValue({
|
||||||
|
data: [],
|
||||||
|
isLoading: false,
|
||||||
|
error: null,
|
||||||
|
} as any);
|
||||||
|
|
||||||
rerender();
|
rerender();
|
||||||
|
|
||||||
// Assert: The data should now be cleared.
|
// Assert: The data should now be cleared.
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
ValidationError,
|
ValidationError,
|
||||||
NotFoundError,
|
NotFoundError,
|
||||||
} from '../services/db/errors.db';
|
} from '../services/db/errors.db';
|
||||||
|
import { createMockRequest } from '../tests/utils/createMockRequest';
|
||||||
import type { Logger } from 'pino';
|
import type { Logger } from 'pino';
|
||||||
|
|
||||||
// Create a mock logger that we can inject into requests and assert against.
|
// Create a mock logger that we can inject into requests and assert against.
|
||||||
@@ -271,7 +272,7 @@ describe('errorHandler Middleware', () => {
|
|||||||
it('should call next(err) if headers have already been sent', () => {
|
it('should call next(err) if headers have already been sent', () => {
|
||||||
// Supertest doesn't easily allow simulating res.headersSent = true mid-request
|
// Supertest doesn't easily allow simulating res.headersSent = true mid-request
|
||||||
// We need to mock the express response object directly for this specific test.
|
// We need to mock the express response object directly for this specific test.
|
||||||
const mockRequestDirect: Partial<Request> = { path: '/headers-sent-error', method: 'GET' };
|
const mockRequestDirect = createMockRequest({ path: '/headers-sent-error', method: 'GET' });
|
||||||
const mockResponseDirect: Partial<Response> = {
|
const mockResponseDirect: Partial<Response> = {
|
||||||
status: vi.fn().mockReturnThis(),
|
status: vi.fn().mockReturnThis(),
|
||||||
json: vi.fn(),
|
json: vi.fn(),
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { describe, it, expect, vi, beforeEach, type Mock } from 'vitest';
|
|||||||
import { Request, Response, NextFunction } from 'express';
|
import { Request, Response, NextFunction } from 'express';
|
||||||
import { requireFileUpload } from './fileUpload.middleware';
|
import { requireFileUpload } from './fileUpload.middleware';
|
||||||
import { ValidationError } from '../services/db/errors.db';
|
import { ValidationError } from '../services/db/errors.db';
|
||||||
|
import { createMockRequest } from '../tests/utils/createMockRequest';
|
||||||
|
|
||||||
describe('requireFileUpload Middleware', () => {
|
describe('requireFileUpload Middleware', () => {
|
||||||
let mockRequest: Partial<Request>;
|
let mockRequest: Partial<Request>;
|
||||||
@@ -11,7 +12,7 @@ describe('requireFileUpload Middleware', () => {
|
|||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
// Reset mocks before each test
|
// Reset mocks before each test
|
||||||
mockRequest = {};
|
mockRequest = createMockRequest();
|
||||||
mockResponse = {
|
mockResponse = {
|
||||||
status: vi.fn().mockReturnThis(),
|
status: vi.fn().mockReturnThis(),
|
||||||
json: vi.fn(),
|
json: vi.fn(),
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import type { Request, Response, NextFunction } from 'express';
|
|||||||
import { createUploadMiddleware, handleMulterError } from './multer.middleware';
|
import { createUploadMiddleware, handleMulterError } from './multer.middleware';
|
||||||
import { createMockUserProfile } from '../tests/utils/mockFactories';
|
import { createMockUserProfile } from '../tests/utils/mockFactories';
|
||||||
import { ValidationError } from '../services/db/errors.db';
|
import { ValidationError } from '../services/db/errors.db';
|
||||||
|
import { createMockRequest } from '../tests/utils/createMockRequest';
|
||||||
|
|
||||||
// 1. Hoist the mocks so they can be referenced inside vi.mock factories.
|
// 1. Hoist the mocks so they can be referenced inside vi.mock factories.
|
||||||
const mocks = vi.hoisted(() => ({
|
const mocks = vi.hoisted(() => ({
|
||||||
@@ -125,7 +126,7 @@ describe('createUploadMiddleware', () => {
|
|||||||
createUploadMiddleware({ storageType: 'avatar' });
|
createUploadMiddleware({ storageType: 'avatar' });
|
||||||
const storageOptions = vi.mocked(multer.diskStorage).mock.calls[0][0];
|
const storageOptions = vi.mocked(multer.diskStorage).mock.calls[0][0];
|
||||||
const cb = vi.fn();
|
const cb = vi.fn();
|
||||||
const mockReq = { user: mockUser } as unknown as Request;
|
const mockReq = createMockRequest({ user: mockUser });
|
||||||
|
|
||||||
storageOptions.filename!(mockReq, mockFile, cb);
|
storageOptions.filename!(mockReq, mockFile, cb);
|
||||||
|
|
||||||
@@ -138,7 +139,7 @@ describe('createUploadMiddleware', () => {
|
|||||||
createUploadMiddleware({ storageType: 'avatar' });
|
createUploadMiddleware({ storageType: 'avatar' });
|
||||||
const storageOptions = vi.mocked(multer.diskStorage).mock.calls[0][0];
|
const storageOptions = vi.mocked(multer.diskStorage).mock.calls[0][0];
|
||||||
const cb = vi.fn();
|
const cb = vi.fn();
|
||||||
const mockReq = {} as Request; // No user on request
|
const mockReq = createMockRequest(); // No user on request
|
||||||
|
|
||||||
storageOptions.filename!(mockReq, mockFile, cb);
|
storageOptions.filename!(mockReq, mockFile, cb);
|
||||||
|
|
||||||
@@ -153,7 +154,7 @@ describe('createUploadMiddleware', () => {
|
|||||||
createUploadMiddleware({ storageType: 'avatar' });
|
createUploadMiddleware({ storageType: 'avatar' });
|
||||||
const storageOptions = vi.mocked(multer.diskStorage).mock.calls[0][0];
|
const storageOptions = vi.mocked(multer.diskStorage).mock.calls[0][0];
|
||||||
const cb = vi.fn();
|
const cb = vi.fn();
|
||||||
const mockReq = { user: mockUser } as unknown as Request;
|
const mockReq = createMockRequest({ user: mockUser });
|
||||||
|
|
||||||
storageOptions.filename!(mockReq, mockFile, cb);
|
storageOptions.filename!(mockReq, mockFile, cb);
|
||||||
|
|
||||||
@@ -171,7 +172,7 @@ describe('createUploadMiddleware', () => {
|
|||||||
createUploadMiddleware({ storageType: 'flyer' });
|
createUploadMiddleware({ storageType: 'flyer' });
|
||||||
const storageOptions = vi.mocked(multer.diskStorage).mock.calls[0][0];
|
const storageOptions = vi.mocked(multer.diskStorage).mock.calls[0][0];
|
||||||
const cb = vi.fn();
|
const cb = vi.fn();
|
||||||
const mockReq = {} as Request;
|
const mockReq = createMockRequest();
|
||||||
|
|
||||||
storageOptions.filename!(mockReq, mockFlyerFile, cb);
|
storageOptions.filename!(mockReq, mockFlyerFile, cb);
|
||||||
|
|
||||||
@@ -191,7 +192,7 @@ describe('createUploadMiddleware', () => {
|
|||||||
createUploadMiddleware({ storageType: 'flyer' });
|
createUploadMiddleware({ storageType: 'flyer' });
|
||||||
const storageOptions = vi.mocked(multer.diskStorage).mock.calls[0][0];
|
const storageOptions = vi.mocked(multer.diskStorage).mock.calls[0][0];
|
||||||
const cb = vi.fn();
|
const cb = vi.fn();
|
||||||
const mockReq = {} as Request;
|
const mockReq = createMockRequest();
|
||||||
|
|
||||||
storageOptions.filename!(mockReq, mockFlyerFile, cb);
|
storageOptions.filename!(mockReq, mockFlyerFile, cb);
|
||||||
|
|
||||||
@@ -206,7 +207,7 @@ describe('createUploadMiddleware', () => {
|
|||||||
const cb = vi.fn();
|
const cb = vi.fn();
|
||||||
const mockImageFile = { mimetype: 'image/png' } as Express.Multer.File;
|
const mockImageFile = { mimetype: 'image/png' } as Express.Multer.File;
|
||||||
|
|
||||||
multerOptions!.fileFilter!({} as Request, mockImageFile, cb);
|
multerOptions!.fileFilter!(createMockRequest(), mockImageFile, cb);
|
||||||
|
|
||||||
expect(cb).toHaveBeenCalledWith(null, true);
|
expect(cb).toHaveBeenCalledWith(null, true);
|
||||||
});
|
});
|
||||||
@@ -217,7 +218,7 @@ describe('createUploadMiddleware', () => {
|
|||||||
const cb = vi.fn();
|
const cb = vi.fn();
|
||||||
const mockTextFile = { mimetype: 'text/plain' } as Express.Multer.File;
|
const mockTextFile = { mimetype: 'text/plain' } as Express.Multer.File;
|
||||||
|
|
||||||
multerOptions!.fileFilter!({} as Request, { ...mockTextFile, fieldname: 'test' }, cb);
|
multerOptions!.fileFilter!(createMockRequest(), { ...mockTextFile, fieldname: 'test' }, cb);
|
||||||
|
|
||||||
const error = (cb as Mock).mock.calls[0][0];
|
const error = (cb as Mock).mock.calls[0][0];
|
||||||
expect(error).toBeInstanceOf(ValidationError);
|
expect(error).toBeInstanceOf(ValidationError);
|
||||||
@@ -232,7 +233,7 @@ describe('handleMulterError Middleware', () => {
|
|||||||
let mockNext: NextFunction;
|
let mockNext: NextFunction;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
mockRequest = {};
|
mockRequest = createMockRequest();
|
||||||
mockResponse = {
|
mockResponse = {
|
||||||
status: vi.fn().mockReturnThis(),
|
status: vi.fn().mockReturnThis(),
|
||||||
json: vi.fn(),
|
json: vi.fn(),
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { Request, Response, NextFunction } from 'express';
|
|||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { validateRequest } from './validation.middleware';
|
import { validateRequest } from './validation.middleware';
|
||||||
import { ValidationError } from '../services/db/errors.db';
|
import { ValidationError } from '../services/db/errors.db';
|
||||||
|
import { createMockRequest } from '../tests/utils/createMockRequest';
|
||||||
|
|
||||||
describe('validateRequest Middleware', () => {
|
describe('validateRequest Middleware', () => {
|
||||||
let mockRequest: Partial<Request>;
|
let mockRequest: Partial<Request>;
|
||||||
@@ -16,11 +17,11 @@ describe('validateRequest Middleware', () => {
|
|||||||
// This more accurately mimics the behavior of Express's request objects
|
// This more accurately mimics the behavior of Express's request objects
|
||||||
// and prevents issues with inherited properties when the middleware
|
// and prevents issues with inherited properties when the middleware
|
||||||
// attempts to delete keys before merging validated data.
|
// attempts to delete keys before merging validated data.
|
||||||
mockRequest = {
|
mockRequest = createMockRequest({
|
||||||
params: Object.create(null),
|
params: Object.create(null),
|
||||||
query: Object.create(null),
|
query: Object.create(null),
|
||||||
body: {},
|
body: {},
|
||||||
};
|
});
|
||||||
mockResponse = {
|
mockResponse = {
|
||||||
status: vi.fn().mockReturnThis(),
|
status: vi.fn().mockReturnThis(),
|
||||||
json: vi.fn(),
|
json: vi.fn(),
|
||||||
|
|||||||
@@ -94,33 +94,17 @@ vi.mock('../services/emailService.server', () => ({
|
|||||||
import authRouter from './auth.routes';
|
import authRouter from './auth.routes';
|
||||||
|
|
||||||
import { UniqueConstraintError } from '../services/db/errors.db'; // Import actual class for instanceof checks
|
import { UniqueConstraintError } from '../services/db/errors.db'; // Import actual class for instanceof checks
|
||||||
|
import { createTestApp } from '../tests/utils/createTestApp';
|
||||||
|
|
||||||
// --- 4. App Setup ---
|
// --- 4. App Setup using createTestApp ---
|
||||||
// We need to inject cookie-parser BEFORE the router is mounted.
|
const app = createTestApp({
|
||||||
// Since createTestApp mounts the router immediately, we pass middleware to it if supported,
|
router: authRouter,
|
||||||
// or we construct the app manually here to ensure correct order.
|
basePath: '/api/auth',
|
||||||
// Assuming createTestApp doesn't support pre-middleware injection easily, we will
|
// Inject cookieParser via the new middleware option
|
||||||
// create a standard express app here for full control, or modify createTestApp usage if possible.
|
middleware: [cookieParser()],
|
||||||
// Looking at createTestApp.ts (inferred), it likely doesn't take middleware.
|
|
||||||
// Let's manually build the app for this test file to ensure cookieParser runs first.
|
|
||||||
|
|
||||||
import express from 'express';
|
|
||||||
import { errorHandler } from '../middleware/errorHandler'; // Assuming this exists
|
|
||||||
|
|
||||||
const { mockLogger } = await import('../tests/utils/mockLogger');
|
|
||||||
|
|
||||||
const app = express();
|
|
||||||
app.use(express.json());
|
|
||||||
app.use(cookieParser()); // Mount BEFORE router
|
|
||||||
|
|
||||||
// Middleware to inject the mock logger into req
|
|
||||||
app.use((req, res, next) => {
|
|
||||||
req.log = mockLogger;
|
|
||||||
next();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
app.use('/api/auth', authRouter);
|
const { mockLogger } = await import('../tests/utils/mockLogger');
|
||||||
app.use(errorHandler); // Mount AFTER router
|
|
||||||
|
|
||||||
// --- 5. Tests ---
|
// --- 5. Tests ---
|
||||||
describe('Auth Routes (/api/auth)', () => {
|
describe('Auth Routes (/api/auth)', () => {
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ import {
|
|||||||
createMockUserProfile,
|
createMockUserProfile,
|
||||||
createMockUserWithPasswordHash,
|
createMockUserWithPasswordHash,
|
||||||
} from '../tests/utils/mockFactories';
|
} from '../tests/utils/mockFactories';
|
||||||
|
import { createMockRequest } from '../tests/utils/createMockRequest';
|
||||||
|
|
||||||
// Mock dependencies before importing the passport configuration
|
// Mock dependencies before importing the passport configuration
|
||||||
vi.mock('../services/db/index.db', () => ({
|
vi.mock('../services/db/index.db', () => ({
|
||||||
@@ -112,7 +113,7 @@ describe('Passport Configuration', () => {
|
|||||||
|
|
||||||
describe('LocalStrategy (Isolated Callback Logic)', () => {
|
describe('LocalStrategy (Isolated Callback Logic)', () => {
|
||||||
// FIX: mockReq needs a 'log' property because the implementation uses req.log
|
// FIX: mockReq needs a 'log' property because the implementation uses req.log
|
||||||
const mockReq = { ip: '127.0.0.1', log: logger } as unknown as Request;
|
const mockReq = createMockRequest({ ip: '127.0.0.1' });
|
||||||
const done = vi.fn();
|
const done = vi.fn();
|
||||||
|
|
||||||
it('should call done(null, user) on successful authentication', async () => {
|
it('should call done(null, user) on successful authentication', async () => {
|
||||||
@@ -454,12 +455,12 @@ describe('Passport Configuration', () => {
|
|||||||
|
|
||||||
it('should call next() if user has "admin" role', () => {
|
it('should call next() if user has "admin" role', () => {
|
||||||
// Arrange
|
// Arrange
|
||||||
const mockReq: Partial<Request> = {
|
const mockReq = createMockRequest({
|
||||||
user: createMockUserProfile({
|
user: createMockUserProfile({
|
||||||
role: 'admin',
|
role: 'admin',
|
||||||
user: { user_id: 'admin-id', email: 'admin@test.com' },
|
user: { user_id: 'admin-id', email: 'admin@test.com' },
|
||||||
}),
|
}),
|
||||||
};
|
});
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
isAdmin(mockReq as Request, mockRes as Response, mockNext);
|
isAdmin(mockReq as Request, mockRes as Response, mockNext);
|
||||||
@@ -471,12 +472,12 @@ describe('Passport Configuration', () => {
|
|||||||
|
|
||||||
it('should call next with a ForbiddenError if user does not have "admin" role', () => {
|
it('should call next with a ForbiddenError if user does not have "admin" role', () => {
|
||||||
// Arrange
|
// Arrange
|
||||||
const mockReq: Partial<Request> = {
|
const mockReq = createMockRequest({
|
||||||
user: createMockUserProfile({
|
user: createMockUserProfile({
|
||||||
role: 'user',
|
role: 'user',
|
||||||
user: { user_id: 'user-id', email: 'user@test.com' },
|
user: { user_id: 'user-id', email: 'user@test.com' },
|
||||||
}),
|
}),
|
||||||
};
|
});
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
isAdmin(mockReq as Request, mockRes as Response, mockNext);
|
isAdmin(mockReq as Request, mockRes as Response, mockNext);
|
||||||
@@ -488,7 +489,7 @@ describe('Passport Configuration', () => {
|
|||||||
|
|
||||||
it('should call next with a ForbiddenError if req.user is missing', () => {
|
it('should call next with a ForbiddenError if req.user is missing', () => {
|
||||||
// Arrange
|
// Arrange
|
||||||
const mockReq = {} as Request; // No req.user
|
const mockReq = createMockRequest(); // No req.user
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
isAdmin(mockReq, mockRes as Response, mockNext);
|
isAdmin(mockReq, mockRes as Response, mockNext);
|
||||||
@@ -500,12 +501,12 @@ describe('Passport Configuration', () => {
|
|||||||
|
|
||||||
it('should log a warning when a non-admin user tries to access an admin route', () => {
|
it('should log a warning when a non-admin user tries to access an admin route', () => {
|
||||||
// Arrange
|
// Arrange
|
||||||
const mockReq: Partial<Request> = {
|
const mockReq = createMockRequest({
|
||||||
user: createMockUserProfile({
|
user: createMockUserProfile({
|
||||||
role: 'user',
|
role: 'user',
|
||||||
user: { user_id: 'user-id-123', email: 'user@test.com' },
|
user: { user_id: 'user-id-123', email: 'user@test.com' },
|
||||||
}),
|
}),
|
||||||
};
|
});
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
isAdmin(mockReq as Request, mockRes as Response, mockNext);
|
isAdmin(mockReq as Request, mockRes as Response, mockNext);
|
||||||
@@ -516,7 +517,7 @@ describe('Passport Configuration', () => {
|
|||||||
|
|
||||||
it('should log a warning with "unknown" user when req.user is missing', () => {
|
it('should log a warning with "unknown" user when req.user is missing', () => {
|
||||||
// Arrange
|
// Arrange
|
||||||
const mockReq = {} as Request; // No req.user
|
const mockReq = createMockRequest(); // No req.user
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
isAdmin(mockReq, mockRes as Response, mockNext);
|
isAdmin(mockReq, mockRes as Response, mockNext);
|
||||||
@@ -533,37 +534,37 @@ describe('Passport Configuration', () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Case 1: user is not an object (e.g., a string)
|
// Case 1: user is not an object (e.g., a string)
|
||||||
const req1 = { user: 'not-an-object' } as unknown as Request;
|
const req1 = createMockRequest({ user: 'not-an-object' } as any);
|
||||||
isAdmin(req1, mockRes as Response, mockNext);
|
isAdmin(req1, mockRes as Response, mockNext);
|
||||||
expect(mockNext).toHaveBeenLastCalledWith(expect.any(ForbiddenError));
|
expect(mockNext).toHaveBeenLastCalledWith(expect.any(ForbiddenError));
|
||||||
expect(mockRes.status).not.toHaveBeenCalled();
|
expect(mockRes.status).not.toHaveBeenCalled();
|
||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
|
|
||||||
// Case 2: user is null
|
// Case 2: user is null
|
||||||
const req2 = { user: null } as unknown as Request;
|
const req2 = createMockRequest({ user: null } as any);
|
||||||
isAdmin(req2, mockRes as Response, mockNext);
|
isAdmin(req2, mockRes as Response, mockNext);
|
||||||
expect(mockNext).toHaveBeenLastCalledWith(expect.any(ForbiddenError));
|
expect(mockNext).toHaveBeenLastCalledWith(expect.any(ForbiddenError));
|
||||||
expect(mockRes.status).not.toHaveBeenCalled();
|
expect(mockRes.status).not.toHaveBeenCalled();
|
||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
|
|
||||||
// Case 3: user object is missing 'user' property
|
// Case 3: user object is missing 'user' property
|
||||||
const req3 = { user: { role: 'admin' } } as unknown as Request;
|
const req3 = createMockRequest({ user: { role: 'admin' } } as any);
|
||||||
isAdmin(req3, mockRes as Response, mockNext);
|
isAdmin(req3, mockRes as Response, mockNext);
|
||||||
expect(mockNext).toHaveBeenLastCalledWith(expect.any(ForbiddenError));
|
expect(mockNext).toHaveBeenLastCalledWith(expect.any(ForbiddenError));
|
||||||
expect(mockRes.status).not.toHaveBeenCalled();
|
expect(mockRes.status).not.toHaveBeenCalled();
|
||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
|
|
||||||
// Case 4: user.user is not an object
|
// Case 4: user.user is not an object
|
||||||
const req4 = { user: { role: 'admin', user: 'not-an-object' } } as unknown as Request;
|
const req4 = createMockRequest({ user: { role: 'admin', user: 'not-an-object' } } as any);
|
||||||
isAdmin(req4, mockRes as Response, mockNext);
|
isAdmin(req4, mockRes as Response, mockNext);
|
||||||
expect(mockNext).toHaveBeenLastCalledWith(expect.any(ForbiddenError));
|
expect(mockNext).toHaveBeenLastCalledWith(expect.any(ForbiddenError));
|
||||||
expect(mockRes.status).not.toHaveBeenCalled();
|
expect(mockRes.status).not.toHaveBeenCalled();
|
||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
|
|
||||||
// Case 5: user.user is missing 'user_id'
|
// Case 5: user.user is missing 'user_id'
|
||||||
const req5 = {
|
const req5 = createMockRequest({
|
||||||
user: { role: 'admin', user: { email: 'test@test.com' } },
|
user: { role: 'admin', user: { email: 'test@test.com' } } as any,
|
||||||
} as unknown as Request;
|
});
|
||||||
isAdmin(req5, mockRes as Response, mockNext);
|
isAdmin(req5, mockRes as Response, mockNext);
|
||||||
expect(mockNext).toHaveBeenLastCalledWith(expect.any(ForbiddenError));
|
expect(mockNext).toHaveBeenLastCalledWith(expect.any(ForbiddenError));
|
||||||
expect(mockRes.status).not.toHaveBeenCalled();
|
expect(mockRes.status).not.toHaveBeenCalled();
|
||||||
@@ -575,12 +576,12 @@ describe('Passport Configuration', () => {
|
|||||||
|
|
||||||
it('should call next with a ForbiddenError if req.user is not a valid UserProfile object', () => {
|
it('should call next with a ForbiddenError if req.user is not a valid UserProfile object', () => {
|
||||||
// Arrange
|
// Arrange
|
||||||
const mockReq: Partial<Request> = {
|
const mockReq = createMockRequest({
|
||||||
// An object that is not a valid UserProfile (e.g., missing 'role')
|
// An object that is not a valid UserProfile (e.g., missing 'role')
|
||||||
user: {
|
user: {
|
||||||
user: { user_id: 'invalid-user-id' }, // Missing 'role' property
|
user: { user_id: 'invalid-user-id' }, // Missing 'role' property
|
||||||
} as unknown as UserProfile, // Cast to UserProfile to satisfy req.user type, but it's intentionally malformed
|
} as unknown as UserProfile, // Cast to UserProfile to satisfy req.user type, but it's intentionally malformed
|
||||||
};
|
});
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
isAdmin(mockReq as Request, mockRes as Response, mockNext);
|
isAdmin(mockReq as Request, mockRes as Response, mockNext);
|
||||||
@@ -601,7 +602,7 @@ describe('Passport Configuration', () => {
|
|||||||
|
|
||||||
it('should populate req.user and call next() if authentication succeeds', () => {
|
it('should populate req.user and call next() if authentication succeeds', () => {
|
||||||
// Arrange
|
// Arrange
|
||||||
const mockReq = {} as Request;
|
const mockReq = createMockRequest();
|
||||||
const mockUser = createMockUserProfile({
|
const mockUser = createMockUserProfile({
|
||||||
role: 'admin',
|
role: 'admin',
|
||||||
user: { user_id: 'admin-id', email: 'admin@test.com' },
|
user: { user_id: 'admin-id', email: 'admin@test.com' },
|
||||||
@@ -621,7 +622,7 @@ describe('Passport Configuration', () => {
|
|||||||
|
|
||||||
it('should not populate req.user and still call next() if authentication fails', () => {
|
it('should not populate req.user and still call next() if authentication fails', () => {
|
||||||
// Arrange
|
// Arrange
|
||||||
const mockReq = {} as Request;
|
const mockReq = createMockRequest();
|
||||||
vi.mocked(passport.authenticate).mockImplementation(
|
vi.mocked(passport.authenticate).mockImplementation(
|
||||||
(_strategy, _options, callback) => () => callback?.(null, false, undefined),
|
(_strategy, _options, callback) => () => callback?.(null, false, undefined),
|
||||||
);
|
);
|
||||||
@@ -634,7 +635,7 @@ describe('Passport Configuration', () => {
|
|||||||
|
|
||||||
it('should log info and call next() if authentication provides an info message', () => {
|
it('should log info and call next() if authentication provides an info message', () => {
|
||||||
// Arrange
|
// Arrange
|
||||||
const mockReq = {} as Request;
|
const mockReq = createMockRequest();
|
||||||
const mockInfo = { message: 'Token expired' };
|
const mockInfo = { message: 'Token expired' };
|
||||||
// Mock passport.authenticate to call its callback with an info object
|
// Mock passport.authenticate to call its callback with an info object
|
||||||
vi.mocked(passport.authenticate).mockImplementation(
|
vi.mocked(passport.authenticate).mockImplementation(
|
||||||
@@ -652,7 +653,7 @@ describe('Passport Configuration', () => {
|
|||||||
|
|
||||||
it('should log info and call next() if authentication provides an info Error object', () => {
|
it('should log info and call next() if authentication provides an info Error object', () => {
|
||||||
// Arrange
|
// Arrange
|
||||||
const mockReq = {} as Request;
|
const mockReq = createMockRequest();
|
||||||
const mockInfoError = new Error('Token is malformed');
|
const mockInfoError = new Error('Token is malformed');
|
||||||
// Mock passport.authenticate to call its callback with an info object
|
// Mock passport.authenticate to call its callback with an info object
|
||||||
vi.mocked(passport.authenticate).mockImplementation(
|
vi.mocked(passport.authenticate).mockImplementation(
|
||||||
@@ -673,7 +674,7 @@ describe('Passport Configuration', () => {
|
|||||||
|
|
||||||
it('should log info.toString() if info object has no message property', () => {
|
it('should log info.toString() if info object has no message property', () => {
|
||||||
// Arrange
|
// Arrange
|
||||||
const mockReq = {} as Request;
|
const mockReq = createMockRequest();
|
||||||
const mockInfo = { custom: 'some info' };
|
const mockInfo = { custom: 'some info' };
|
||||||
// Mock passport.authenticate to call its callback with a custom info object
|
// Mock passport.authenticate to call its callback with a custom info object
|
||||||
vi.mocked(passport.authenticate).mockImplementation(
|
vi.mocked(passport.authenticate).mockImplementation(
|
||||||
@@ -693,7 +694,7 @@ describe('Passport Configuration', () => {
|
|||||||
|
|
||||||
it('should call next() and not populate user if passport returns an error', () => {
|
it('should call next() and not populate user if passport returns an error', () => {
|
||||||
// Arrange
|
// Arrange
|
||||||
const mockReq = {} as Request;
|
const mockReq = createMockRequest();
|
||||||
const authError = new Error('Malformed token');
|
const authError = new Error('Malformed token');
|
||||||
// Mock passport.authenticate to call its callback with an error
|
// Mock passport.authenticate to call its callback with an error
|
||||||
vi.mocked(passport.authenticate).mockImplementation(
|
vi.mocked(passport.authenticate).mockImplementation(
|
||||||
@@ -729,7 +730,7 @@ describe('Passport Configuration', () => {
|
|||||||
it('should attach a mock admin user to req when NODE_ENV is "test"', () => {
|
it('should attach a mock admin user to req when NODE_ENV is "test"', () => {
|
||||||
// Arrange
|
// Arrange
|
||||||
vi.stubEnv('NODE_ENV', 'test');
|
vi.stubEnv('NODE_ENV', 'test');
|
||||||
const mockReq = {} as Request;
|
const mockReq = createMockRequest();
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
mockAuth(mockReq, mockRes as Response, mockNext);
|
mockAuth(mockReq, mockRes as Response, mockNext);
|
||||||
@@ -743,7 +744,7 @@ describe('Passport Configuration', () => {
|
|||||||
it('should do nothing and call next() when NODE_ENV is not "test"', () => {
|
it('should do nothing and call next() when NODE_ENV is not "test"', () => {
|
||||||
// Arrange
|
// Arrange
|
||||||
vi.stubEnv('NODE_ENV', 'production');
|
vi.stubEnv('NODE_ENV', 'production');
|
||||||
const mockReq = {} as Request;
|
const mockReq = createMockRequest();
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
mockAuth(mockReq, mockRes as Response, mockNext);
|
mockAuth(mockReq, mockRes as Response, mockNext);
|
||||||
|
|||||||
8
src/tests/setup/global.ts
Normal file
8
src/tests/setup/global.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import { vi } from 'vitest';
|
||||||
|
import { mockLogger } from '../utils/mockLogger';
|
||||||
|
|
||||||
|
// Globally mock the logger service so individual test files don't have to.
|
||||||
|
// This ensures 'import { logger } from ...' always returns the mock.
|
||||||
|
vi.mock('../../services/logger.server', () => ({
|
||||||
|
logger: mockLogger,
|
||||||
|
}));
|
||||||
9
src/tests/utils/createMockRequest.ts
Normal file
9
src/tests/utils/createMockRequest.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import type { Request } from 'express';
|
||||||
|
import { mockLogger } from './mockLogger';
|
||||||
|
|
||||||
|
export const createMockRequest = (overrides: Partial<Request> = {}): Request => {
|
||||||
|
return {
|
||||||
|
log: mockLogger,
|
||||||
|
...overrides,
|
||||||
|
} as unknown as Request;
|
||||||
|
};
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
// src/tests/utils/createTestApp.ts
|
// src/tests/utils/createTestApp.ts
|
||||||
import express, { type Router } from 'express';
|
import express, { type Router, type RequestHandler } from 'express';
|
||||||
import type { Logger } from 'pino';
|
import type { Logger } from 'pino';
|
||||||
import { errorHandler } from '../../middleware/errorHandler';
|
import { errorHandler } from '../../middleware/errorHandler';
|
||||||
import { mockLogger } from './mockLogger';
|
import { mockLogger } from './mockLogger';
|
||||||
@@ -17,6 +17,7 @@ interface CreateAppOptions {
|
|||||||
router: Router;
|
router: Router;
|
||||||
basePath: string;
|
basePath: string;
|
||||||
authenticatedUser?: UserProfile;
|
authenticatedUser?: UserProfile;
|
||||||
|
middleware?: RequestHandler[];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -24,10 +25,20 @@ interface CreateAppOptions {
|
|||||||
* It includes JSON parsing, a mock logger, an optional authenticated user,
|
* It includes JSON parsing, a mock logger, an optional authenticated user,
|
||||||
* the specified router, and the global error handler.
|
* the specified router, and the global error handler.
|
||||||
*/
|
*/
|
||||||
export const createTestApp = ({ router, basePath, authenticatedUser }: CreateAppOptions) => {
|
export const createTestApp = ({
|
||||||
|
router,
|
||||||
|
basePath,
|
||||||
|
authenticatedUser,
|
||||||
|
middleware = [],
|
||||||
|
}: CreateAppOptions) => {
|
||||||
const app = express();
|
const app = express();
|
||||||
app.use(express.json({ strict: false }));
|
app.use(express.json({ strict: false }));
|
||||||
|
|
||||||
|
// Apply custom middleware (e.g. cookieParser)
|
||||||
|
if (middleware.length > 0) {
|
||||||
|
app.use(middleware);
|
||||||
|
}
|
||||||
|
|
||||||
// Inject the mock logger and authenticated user into every request.
|
// Inject the mock logger and authenticated user into every request.
|
||||||
app.use((req, res, next) => {
|
app.use((req, res, next) => {
|
||||||
req.log = mockLogger;
|
req.log = mockLogger;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||||
import type { Request } from 'express';
|
import { createMockRequest } from '../tests/utils/createMockRequest';
|
||||||
|
|
||||||
describe('rateLimit utils', () => {
|
describe('rateLimit utils', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@@ -16,7 +16,7 @@ describe('rateLimit utils', () => {
|
|||||||
vi.stubEnv('NODE_ENV', 'production');
|
vi.stubEnv('NODE_ENV', 'production');
|
||||||
const { shouldSkipRateLimit } = await import('./rateLimit');
|
const { shouldSkipRateLimit } = await import('./rateLimit');
|
||||||
|
|
||||||
const req = { headers: {} } as Request;
|
const req = createMockRequest({ headers: {} });
|
||||||
expect(shouldSkipRateLimit(req)).toBe(false);
|
expect(shouldSkipRateLimit(req)).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -24,7 +24,7 @@ describe('rateLimit utils', () => {
|
|||||||
vi.stubEnv('NODE_ENV', 'development');
|
vi.stubEnv('NODE_ENV', 'development');
|
||||||
const { shouldSkipRateLimit } = await import('./rateLimit');
|
const { shouldSkipRateLimit } = await import('./rateLimit');
|
||||||
|
|
||||||
const req = { headers: {} } as Request;
|
const req = createMockRequest({ headers: {} });
|
||||||
expect(shouldSkipRateLimit(req)).toBe(false);
|
expect(shouldSkipRateLimit(req)).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -32,7 +32,7 @@ describe('rateLimit utils', () => {
|
|||||||
vi.stubEnv('NODE_ENV', 'test');
|
vi.stubEnv('NODE_ENV', 'test');
|
||||||
const { shouldSkipRateLimit } = await import('./rateLimit');
|
const { shouldSkipRateLimit } = await import('./rateLimit');
|
||||||
|
|
||||||
const req = { headers: {} } as Request;
|
const req = createMockRequest({ headers: {} });
|
||||||
expect(shouldSkipRateLimit(req)).toBe(true);
|
expect(shouldSkipRateLimit(req)).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -40,9 +40,9 @@ describe('rateLimit utils', () => {
|
|||||||
vi.stubEnv('NODE_ENV', 'test');
|
vi.stubEnv('NODE_ENV', 'test');
|
||||||
const { shouldSkipRateLimit } = await import('./rateLimit');
|
const { shouldSkipRateLimit } = await import('./rateLimit');
|
||||||
|
|
||||||
const req = {
|
const req = createMockRequest({
|
||||||
headers: { 'x-test-rate-limit-enable': 'true' },
|
headers: { 'x-test-rate-limit-enable': 'true' },
|
||||||
} as unknown as Request;
|
});
|
||||||
expect(shouldSkipRateLimit(req)).toBe(false);
|
expect(shouldSkipRateLimit(req)).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -50,9 +50,9 @@ describe('rateLimit utils', () => {
|
|||||||
vi.stubEnv('NODE_ENV', 'test');
|
vi.stubEnv('NODE_ENV', 'test');
|
||||||
const { shouldSkipRateLimit } = await import('./rateLimit');
|
const { shouldSkipRateLimit } = await import('./rateLimit');
|
||||||
|
|
||||||
const req = {
|
const req = createMockRequest({
|
||||||
headers: { 'x-test-rate-limit-enable': 'false' },
|
headers: { 'x-test-rate-limit-enable': 'false' },
|
||||||
} as unknown as Request;
|
});
|
||||||
expect(shouldSkipRateLimit(req)).toBe(true);
|
expect(shouldSkipRateLimit(req)).toBe(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ export default defineConfig({
|
|||||||
globalSetup: './src/tests/setup/global-setup.ts',
|
globalSetup: './src/tests/setup/global-setup.ts',
|
||||||
// The globalApiMock MUST come first to ensure it's applied before other mocks that might depend on it.
|
// The globalApiMock MUST come first to ensure it's applied before other mocks that might depend on it.
|
||||||
setupFiles: [
|
setupFiles: [
|
||||||
|
'./src/tests/setup/global.ts',
|
||||||
'./src/tests/setup/globalApiMock.ts',
|
'./src/tests/setup/globalApiMock.ts',
|
||||||
'./src/tests/setup/tests-setup-unit.ts',
|
'./src/tests/setup/tests-setup-unit.ts',
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ const finalConfig = mergeConfig(
|
|||||||
},
|
},
|
||||||
// This setup script starts the backend server before tests run.
|
// This setup script starts the backend server before tests run.
|
||||||
globalSetup: './src/tests/setup/integration-global-setup.ts',
|
globalSetup: './src/tests/setup/integration-global-setup.ts',
|
||||||
|
setupFiles: ['./src/tests/setup/global.ts'],
|
||||||
// The default timeout is 5000ms (5 seconds)
|
// The default timeout is 5000ms (5 seconds)
|
||||||
testTimeout: 60000, // Increased timeout for server startup and API calls, especially AI services.
|
testTimeout: 60000, // Increased timeout for server startup and API calls, especially AI services.
|
||||||
hookTimeout: 60000,
|
hookTimeout: 60000,
|
||||||
|
|||||||
Reference in New Issue
Block a user