Refactor test setup and improve mock implementations
Some checks are pending
Deploy to Test Environment / deploy-to-test (push) Has started running
Some checks are pending
Deploy to Test Environment / deploy-to-test (push) Has started running
- Introduced `createTestApp` utility to standardize Express app creation for tests, including JSON parsing, mock logger, and error handling. - Updated all route tests to utilize `createTestApp`, enhancing consistency and reducing boilerplate code. - Implemented `mockLogger` for cleaner test output and easier logging mock management. - Adjusted passport mocks to ensure proper user authentication simulation across tests. - Enhanced type safety by augmenting Express Request interface with `req.log` and `req.user` properties. - Removed redundant code and improved readability in various test files.
This commit is contained in:
@@ -30,6 +30,11 @@ export const FlyerUploader: React.FC<FlyerUploaderProps> = ({ onProcessingComple
|
||||
const [estimatedTime, setEstimatedTime] = useState(0);
|
||||
const [currentFile, setCurrentFile] = useState<string | null>(null);
|
||||
|
||||
// Log status messages for easier debugging of the upload/processing flow.
|
||||
useEffect(() => {
|
||||
if (statusMessage) logger.info(`FlyerUploader Status: ${statusMessage}`);
|
||||
}, [statusMessage]);
|
||||
|
||||
// This effect handles the polling logic
|
||||
useEffect(() => {
|
||||
if (processingState !== 'polling' || !jobId) {
|
||||
|
||||
@@ -5,7 +5,6 @@ import { useUserData } from './useUserData';
|
||||
import { useApi } from './useApi';
|
||||
import type { FlyerItem, DealItem } from '../types';
|
||||
import * as apiClient from '../services/apiClient';
|
||||
import { logger } from '../services/logger.client';
|
||||
|
||||
interface FlyerItemCount {
|
||||
count: number;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// src/hooks/useDragAndDrop.ts
|
||||
import { useState, useCallback } from 'react';
|
||||
|
||||
interface UseDragAndDropOptions<T extends HTMLElement> {
|
||||
interface UseDragAndDropOptions {
|
||||
/**
|
||||
* Callback function that is triggered when files are dropped onto the element.
|
||||
* @param files The FileList object from the drop event.
|
||||
@@ -19,7 +19,7 @@ interface UseDragAndDropOptions<T extends HTMLElement> {
|
||||
* @param options - Configuration for the hook, including the onDrop callback and disabled state.
|
||||
* @returns An object containing the `isDragging` state and props to be spread onto the dropzone element.
|
||||
*/
|
||||
export const useDragAndDrop = <T extends HTMLElement>({ onFilesDropped, disabled = false }: UseDragAndDropOptions<T>) => {
|
||||
export const useDragAndDrop = <T extends HTMLElement>({ onFilesDropped, disabled = false }: UseDragAndDropOptions) => {
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
|
||||
const handleDragEnter = useCallback((e: React.DragEvent<T>) => {
|
||||
|
||||
@@ -14,8 +14,8 @@ export const errorHandler = (err: HttpError, req: Request, res: Response, next:
|
||||
return next(err);
|
||||
}
|
||||
|
||||
// Use the request-scoped logger if available, otherwise fall back to the global logger.
|
||||
const log = req.log || logger;
|
||||
// The pino-http middleware guarantees that `req.log` will be available.
|
||||
const log = req.log;
|
||||
|
||||
// --- 1. Determine Final Status Code and Message ---
|
||||
let statusCode = err.status ?? 500;
|
||||
@@ -55,10 +55,15 @@ export const errorHandler = (err: HttpError, req: Request, res: Response, next:
|
||||
log.error({ err, errorId, req: { method: req.method, url: req.originalUrl, headers: req.headers, body: req.body } }, `Unhandled API Error (ID: ${errorId})`);
|
||||
} else {
|
||||
// For 4xx errors, log at a lower level (e.g., 'warn') to avoid flooding error trackers.
|
||||
// The request-scoped logger already contains the necessary context.
|
||||
// We log the error itself to capture its message and properties.
|
||||
// No need to log the full request for client errors unless desired for debugging.
|
||||
log.warn({ err }, `Client Error: ${statusCode} on ${req.method} ${req.path}`);
|
||||
// We include the validation errors in the log context if they exist.
|
||||
log.warn(
|
||||
{
|
||||
err,
|
||||
validationErrors: errors, // Add validation issues to the log object
|
||||
statusCode,
|
||||
},
|
||||
`Client Error on ${req.method} ${req.path}: ${message}`
|
||||
);
|
||||
}
|
||||
|
||||
// --- TEST ENVIRONMENT DEBUGGING ---
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// src/middleware/validation.middleware.ts
|
||||
import { Request, Response, NextFunction } from 'express';
|
||||
import { ParamsDictionary } from 'express-serve-static-core';
|
||||
import { ParamsDictionary, Query } from 'express-serve-static-core';
|
||||
import { ZodObject, ZodError, z } from 'zod';
|
||||
import { ValidationError } from '../services/db/errors.db';
|
||||
|
||||
@@ -25,8 +25,8 @@ export const validateRequest = (schema: ZodObject<z.ZodRawShape>) =>
|
||||
// On success, replace the request parts with the parsed (and coerced) data.
|
||||
// This ensures downstream handlers get correctly typed data.
|
||||
req.params = params as ParamsDictionary;
|
||||
// After parsing with Zod, the `query` object is correctly shaped. We cast to `any` as a bridge.
|
||||
req.query = query as any;
|
||||
// After parsing with Zod, the `query` object is correctly shaped. We cast it to the expected `Query` type.
|
||||
req.query = query as Query;
|
||||
req.body = body;
|
||||
|
||||
return next();
|
||||
|
||||
@@ -5,8 +5,9 @@ import express, { Request, Response, NextFunction } from 'express';
|
||||
import adminRouter from './admin.routes';
|
||||
import { createMockUserProfile, createMockSuggestedCorrection, createMockBrand, createMockRecipe, createMockRecipeComment, createMockFlyerItem } from '../tests/utils/mockFactories';
|
||||
import { SuggestedCorrection, Brand, UserProfile, UnmatchedFlyerItem } from '../types';
|
||||
import { errorHandler } from '../middleware/errorHandler';
|
||||
import { NotFoundError } from '../services/db/errors.db';
|
||||
import { mockLogger } from '../tests/utils/mockLogger';
|
||||
import { createTestApp } from '../tests/utils/createTestApp';
|
||||
|
||||
vi.mock('../lib/queue', () => ({
|
||||
serverAdapter: {
|
||||
@@ -76,7 +77,7 @@ vi.mock('@bull-board/express', () => ({
|
||||
|
||||
// Mock the logger
|
||||
vi.mock('../services/logger.server', () => ({
|
||||
logger: { info: vi.fn(), debug: vi.fn(), error: vi.fn(), warn: vi.fn() },
|
||||
logger: mockLogger,
|
||||
}));
|
||||
|
||||
// Mock the passport middleware
|
||||
@@ -94,24 +95,10 @@ vi.mock('./passport.routes', () => ({
|
||||
},
|
||||
}));
|
||||
|
||||
// Helper function to create a test app instance.
|
||||
const createApp = (user?: UserProfile) => {
|
||||
const app = express();
|
||||
app.use(express.json({ strict: false }));
|
||||
if (user) {
|
||||
app.use((req, res, next) => {
|
||||
req.user = user;
|
||||
next();
|
||||
});
|
||||
}
|
||||
app.use('/api/admin', adminRouter);
|
||||
app.use(errorHandler);
|
||||
return app;
|
||||
};
|
||||
|
||||
describe('Admin Content Management Routes (/api/admin)', () => {
|
||||
const adminUser = createMockUserProfile({ role: 'admin', user_id: 'admin-user-id' });
|
||||
const app = createApp(adminUser);
|
||||
// Create a single app instance with an admin user for all tests in this suite.
|
||||
const app = createTestApp({ router: adminRouter, basePath: '/api/admin', authenticatedUser: adminUser });
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
|
||||
@@ -15,7 +15,8 @@ import adminRouter from './admin.routes';
|
||||
import { createMockUserProfile } from '../tests/utils/mockFactories';
|
||||
import { Job } from 'bullmq';
|
||||
import { UserProfile } from '../types';
|
||||
import { errorHandler } from '../middleware/errorHandler';
|
||||
import { mockLogger } from '../tests/utils/mockLogger';
|
||||
import { createTestApp } from '../tests/utils/createTestApp';
|
||||
|
||||
vi.mock('../lib/queue', () => ({
|
||||
serverAdapter: {
|
||||
@@ -87,7 +88,7 @@ import { flyerQueue, analyticsQueue, cleanupQueue } from '../services/queueServi
|
||||
|
||||
// Mock the logger
|
||||
vi.mock('../services/logger.server', () => ({
|
||||
logger: { info: vi.fn(), debug: vi.fn(), error: vi.fn(), warn: vi.fn() },
|
||||
logger: mockLogger,
|
||||
}));
|
||||
|
||||
// Mock the passport middleware
|
||||
@@ -105,24 +106,10 @@ vi.mock('./passport.routes', () => ({
|
||||
},
|
||||
}));
|
||||
|
||||
// Helper function to create a test app instance.
|
||||
const createApp = (user?: UserProfile) => {
|
||||
const app = express();
|
||||
app.use(express.json({ strict: false }));
|
||||
if (user) {
|
||||
app.use((req, res, next) => {
|
||||
req.user = user;
|
||||
next();
|
||||
});
|
||||
}
|
||||
app.use('/api/admin', adminRouter);
|
||||
app.use(errorHandler);
|
||||
return app;
|
||||
};
|
||||
|
||||
describe('Admin Job Trigger Routes (/api/admin/trigger)', () => {
|
||||
const adminUser = createMockUserProfile({ role: 'admin' });
|
||||
const app = createApp(adminUser);
|
||||
// Create a single app instance with an admin user for all tests in this suite.
|
||||
const app = createTestApp({ router: adminRouter, basePath: '/api/admin', authenticatedUser: adminUser });
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
|
||||
@@ -17,7 +17,8 @@ import express, { Request, Response, NextFunction } from 'express';
|
||||
import adminRouter from './admin.routes';
|
||||
import { createMockUserProfile, createMockActivityLogItem } from '../tests/utils/mockFactories';
|
||||
import { UserProfile } from '../types';
|
||||
import { errorHandler } from '../middleware/errorHandler';
|
||||
import { mockLogger } from '../tests/utils/mockLogger';
|
||||
import { createTestApp } from '../tests/utils/createTestApp';
|
||||
|
||||
vi.mock('../lib/queue', () => ({
|
||||
serverAdapter: {
|
||||
@@ -83,7 +84,7 @@ const mockedQueueService = queueService as Mocked<typeof queueService>;
|
||||
|
||||
// Mock the logger
|
||||
vi.mock('../services/logger.server', () => ({
|
||||
logger: { info: vi.fn(), debug: vi.fn(), error: vi.fn(), warn: vi.fn() },
|
||||
logger: mockLogger,
|
||||
}));
|
||||
|
||||
// Mock the passport middleware
|
||||
@@ -101,24 +102,10 @@ vi.mock('./passport.routes', () => ({
|
||||
},
|
||||
}));
|
||||
|
||||
// Helper function to create a test app instance.
|
||||
const createApp = (user?: UserProfile) => {
|
||||
const app = express();
|
||||
app.use(express.json({ strict: false }));
|
||||
if (user) {
|
||||
app.use((req, res, next) => {
|
||||
req.user = user;
|
||||
next();
|
||||
});
|
||||
}
|
||||
app.use('/api/admin', adminRouter);
|
||||
app.use(errorHandler);
|
||||
return app;
|
||||
};
|
||||
|
||||
describe('Admin Monitoring Routes (/api/admin)', () => {
|
||||
const adminUser = createMockUserProfile({ role: 'admin' });
|
||||
const app = createApp(adminUser);
|
||||
// Create a single app instance with an admin user for all tests in this suite.
|
||||
const app = createTestApp({ router: adminRouter, basePath: '/api/admin', authenticatedUser: adminUser });
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// src/routes/admin.routes.ts
|
||||
import { Router, NextFunction, Request, Response } from 'express';
|
||||
import passport from './passport.routes';
|
||||
import { isAdmin, optionalAuth } from './passport.routes'; // Correctly imported
|
||||
import { isAdmin } from './passport.routes'; // Correctly imported
|
||||
import multer from 'multer';// --- Zod Schemas for Admin Routes (as per ADR-003) ---
|
||||
import { z } from 'zod';
|
||||
|
||||
|
||||
@@ -5,7 +5,8 @@ import express, { Request, Response, NextFunction } from 'express';
|
||||
import adminRouter from './admin.routes';
|
||||
import { createMockUserProfile } from '../tests/utils/mockFactories';
|
||||
import { UserProfile } from '../types';
|
||||
import { errorHandler } from '../middleware/errorHandler';
|
||||
import { mockLogger } from '../tests/utils/mockLogger';
|
||||
import { createTestApp } from '../tests/utils/createTestApp';
|
||||
|
||||
const { mockedDb } = vi.hoisted(() => {
|
||||
return {
|
||||
@@ -45,7 +46,7 @@ import { adminRepo } from '../services/db/index.db';
|
||||
|
||||
// Mock the logger
|
||||
vi.mock('../services/logger.server', () => ({
|
||||
logger: { info: vi.fn(), debug: vi.fn(), error: vi.fn(), warn: vi.fn() },
|
||||
logger: mockLogger,
|
||||
}));
|
||||
|
||||
// Mock the passport middleware
|
||||
@@ -63,24 +64,10 @@ vi.mock('./passport.routes', () => ({
|
||||
},
|
||||
}));
|
||||
|
||||
// Helper function to create a test app instance.
|
||||
const createApp = (user?: UserProfile) => {
|
||||
const app = express();
|
||||
app.use(express.json({ strict: false }));
|
||||
if (user) {
|
||||
app.use((req, res, next) => {
|
||||
req.user = user;
|
||||
next();
|
||||
});
|
||||
}
|
||||
app.use('/api/admin', adminRouter);
|
||||
app.use(errorHandler);
|
||||
return app;
|
||||
};
|
||||
|
||||
describe('Admin Stats Routes (/api/admin/stats)', () => {
|
||||
const adminUser = createMockUserProfile({ role: 'admin' });
|
||||
const app = createApp(adminUser);
|
||||
// Create a single app instance with an admin user for all tests in this suite.
|
||||
const app = createTestApp({ router: adminRouter, basePath: '/api/admin', authenticatedUser: adminUser });
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
|
||||
@@ -4,7 +4,7 @@ import supertest from 'supertest';
|
||||
import express, { Request, Response, NextFunction } from 'express';
|
||||
import adminRouter from './admin.routes';
|
||||
import { createMockUserProfile } from '../tests/utils/mockFactories';
|
||||
import { errorHandler } from '../middleware/errorHandler';
|
||||
import { createTestApp } from '../tests/utils/createTestApp';
|
||||
|
||||
// Mock dependencies
|
||||
vi.mock('../services/geocodingService.server', () => ({
|
||||
@@ -49,17 +49,9 @@ vi.mock('./passport.routes', () => ({
|
||||
isAdmin: (req: Request, res: Response, next: NextFunction) => next(),
|
||||
}));
|
||||
|
||||
// Helper function to create a test app instance.
|
||||
const createApp = () => {
|
||||
const app = express();
|
||||
app.use(express.json());
|
||||
app.use('/api/admin', adminRouter);
|
||||
app.use(errorHandler);
|
||||
return app;
|
||||
};
|
||||
|
||||
describe('Admin System Routes (/api/admin/system)', () => {
|
||||
const app = createApp();
|
||||
const adminUser = createMockUserProfile({ role: 'admin' });
|
||||
const app = createTestApp({ router: adminRouter, basePath: '/api/admin', authenticatedUser: adminUser });
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
|
||||
@@ -5,8 +5,9 @@ import express, { Request, Response, NextFunction } from 'express';
|
||||
import adminRouter from './admin.routes';
|
||||
import { createMockUserProfile } from '../tests/utils/mockFactories';
|
||||
import { User, UserProfile } from '../types';
|
||||
import { errorHandler } from '../middleware/errorHandler';
|
||||
import { NotFoundError } from '../services/db/errors.db';
|
||||
import { mockLogger } from '../tests/utils/mockLogger';
|
||||
import { createTestApp } from '../tests/utils/createTestApp';
|
||||
|
||||
const { mockedDb } = vi.hoisted(() => {
|
||||
return {
|
||||
@@ -55,7 +56,7 @@ vi.mock('@bull-board/express', () => ({
|
||||
|
||||
// Mock the logger
|
||||
vi.mock('../services/logger.server', () => ({
|
||||
logger: { info: vi.fn(), debug: vi.fn(), error: vi.fn(), warn: vi.fn() },
|
||||
logger: mockLogger,
|
||||
}));
|
||||
|
||||
// Mock the passport middleware
|
||||
@@ -73,24 +74,10 @@ vi.mock('./passport.routes', () => ({
|
||||
},
|
||||
}));
|
||||
|
||||
// Helper function to create a test app instance.
|
||||
const createApp = (user?: UserProfile) => {
|
||||
const app = express();
|
||||
app.use(express.json({ strict: false }));
|
||||
if (user) {
|
||||
app.use((req, res, next) => {
|
||||
req.user = user;
|
||||
next();
|
||||
});
|
||||
}
|
||||
app.use('/api/admin', adminRouter);
|
||||
app.use(errorHandler);
|
||||
return app;
|
||||
};
|
||||
|
||||
describe('Admin User Management Routes (/api/admin/users)', () => {
|
||||
const adminUser = createMockUserProfile({ role: 'admin', user_id: 'admin-user-id' });
|
||||
const app = createApp(adminUser);
|
||||
// Create a single app instance with an admin user for all tests in this suite.
|
||||
const app = createTestApp({ router: adminRouter, basePath: '/api/admin', authenticatedUser: adminUser });
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
@@ -98,7 +85,17 @@ describe('Admin User Management Routes (/api/admin/users)', () => {
|
||||
|
||||
describe('GET /users', () => {
|
||||
it('should return a list of all users on success', async () => {
|
||||
const mockUsers: any[] = [
|
||||
// Define a specific type for the user list returned by the admin endpoint
|
||||
// to avoid using `any` and improve test type safety.
|
||||
type UserSummary = {
|
||||
user_id: string;
|
||||
email: string;
|
||||
role: 'user' | 'admin';
|
||||
created_at: string;
|
||||
full_name: string | null;
|
||||
avatar_url: string | null;
|
||||
};
|
||||
const mockUsers: UserSummary[] = [
|
||||
{ user_id: '1', email: 'user1@test.com', role: 'user', created_at: new Date().toISOString(), full_name: 'User One', avatar_url: null },
|
||||
{ user_id: '2', email: 'user2@test.com', role: 'admin', created_at: new Date().toISOString(), full_name: 'Admin Two', avatar_url: null },
|
||||
];
|
||||
|
||||
@@ -5,11 +5,12 @@ import express, { type Request, type Response, type NextFunction } from 'express
|
||||
import path from 'node:path';
|
||||
import type { Job } from 'bullmq';
|
||||
import aiRouter from './ai.routes';
|
||||
import { createMockUserProfile, createMockFlyer } from '../tests/utils/mockFactories';
|
||||
import { errorHandler } from '../middleware/errorHandler';
|
||||
import { createMockUserProfile, createMockFlyer } from '../tests/utils/mockFactories';
|
||||
import * as flyerDb from '../services/db/flyer.db';
|
||||
import * as db from '../services/db/index.db';
|
||||
import * as aiService from '../services/aiService.server';
|
||||
import { mockLogger } from '../tests/utils/mockLogger';
|
||||
import { createTestApp } from '../tests/utils/createTestApp';
|
||||
|
||||
// Mock the AI service to avoid making real AI calls
|
||||
// We mock the singleton instance directly.
|
||||
@@ -37,12 +38,7 @@ import { flyerQueue } from '../services/queueService.server';
|
||||
|
||||
// Mock the logger to keep test output clean
|
||||
vi.mock('../services/logger.server', () => ({
|
||||
logger: {
|
||||
info: vi.fn(),
|
||||
debug: vi.fn(),
|
||||
error: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
},
|
||||
logger: mockLogger,
|
||||
}));
|
||||
|
||||
// Mock the passport module to control authentication for different tests.
|
||||
@@ -57,16 +53,11 @@ vi.mock('./passport.routes', () => ({
|
||||
isAdmin: vi.fn((req, res, next) => next()),
|
||||
}));
|
||||
|
||||
// Create a minimal Express app to host our router
|
||||
const app = express();
|
||||
app.use(express.json({ strict: false }));
|
||||
app.use('/api/ai', aiRouter);
|
||||
app.use(errorHandler);
|
||||
|
||||
describe('AI Routes (/api/ai)', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
const app = createTestApp({ router: aiRouter, basePath: '/api/ai' });
|
||||
|
||||
describe('POST /upload-and-process', () => {
|
||||
const imagePath = path.resolve(__dirname, '../tests/assets/test-flyer-image.jpg');
|
||||
@@ -133,16 +124,10 @@ describe('AI Routes (/api/ai)', () => {
|
||||
// Arrange: Create a new app instance specifically for this test
|
||||
// with the authenticated user middleware already applied.
|
||||
const mockUser = createMockUserProfile({ user_id: 'auth-user-1' });
|
||||
const authenticatedApp = express();
|
||||
authenticatedApp.use(express.json({ strict: false }));
|
||||
authenticatedApp.use((req, res, next) => {
|
||||
req.user = mockUser;
|
||||
next();
|
||||
});
|
||||
authenticatedApp.use('/api/ai', aiRouter);
|
||||
const authenticatedApp = createTestApp({ router: aiRouter, basePath: '/api/ai', authenticatedUser: mockUser });
|
||||
|
||||
vi.mocked(db.flyerRepo.findFlyerByChecksum).mockResolvedValue(undefined);
|
||||
vi.mocked(flyerQueue.add).mockResolvedValue({ id: 'job-456' } as any);
|
||||
vi.mocked(flyerQueue.add).mockResolvedValue({ id: 'job-456' } as unknown as Job);
|
||||
|
||||
// Act
|
||||
await supertest(authenticatedApp)
|
||||
@@ -168,13 +153,7 @@ describe('AI Routes (/api/ai)', () => {
|
||||
country: 'USA',
|
||||
},
|
||||
});
|
||||
const authenticatedApp = express();
|
||||
authenticatedApp.use(express.json({ strict: false }));
|
||||
authenticatedApp.use((req, res, next) => {
|
||||
req.user = mockUserWithAddress;
|
||||
next();
|
||||
});
|
||||
authenticatedApp.use('/api/ai', aiRouter);
|
||||
const authenticatedApp = createTestApp({ router: aiRouter, basePath: '/api/ai', authenticatedUser: mockUserWithAddress });
|
||||
|
||||
// Act
|
||||
await supertest(authenticatedApp).post('/api/ai/upload-and-process').field('checksum', 'addr-checksum').attach('flyerFile', imagePath);
|
||||
|
||||
@@ -44,7 +44,11 @@ const rescanAreaSchema = z.object({
|
||||
body: z.object({
|
||||
cropArea: z.string().transform((val, ctx) => {
|
||||
try { return JSON.parse(val); }
|
||||
catch (e) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: 'cropArea must be a valid JSON string.' }); return z.NEVER; }
|
||||
catch (err) {
|
||||
// Log the actual parsing error for better debugging if invalid JSON is sent.
|
||||
logger.warn({ error: errMsg(err), receivedValue: val }, 'Failed to parse cropArea in rescanAreaSchema');
|
||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: 'cropArea must be a valid JSON string.' }); return z.NEVER;
|
||||
}
|
||||
}),
|
||||
extractionType: z.string().min(1, 'extractionType is required.'),
|
||||
}),
|
||||
|
||||
@@ -1,25 +1,20 @@
|
||||
// --- FIX REGISTRY ---
|
||||
//
|
||||
// 2024-08-01: Corrected `auth.routes.test.ts` by separating the mock's implementation into a `vi.hoisted` block and then applying it in the `vi.mock` call at the top level of the module.
|
||||
// 2024-08-01: Corrected `vi.mock` for `passport.routes` by separating the mock's implementation into a `vi.hoisted` block and then applying it in the `vi.mock` call at the top level of the module. This resolves a variable initialization error.
|
||||
// 2024-08-01: Moved `vi.hoisted` declaration for `passportMocks` before the `vi.mock` call that uses it. This fixes a "Cannot access before initialization" reference error during test setup by ensuring the hoisted variable is declared before it's referenced.
|
||||
//
|
||||
// --- END FIX REGISTRY ---
|
||||
// src/routes/auth.routes.test.ts
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import supertest from 'supertest';
|
||||
import express, { Request, Response, NextFunction } from 'express';
|
||||
import cookieParser from 'cookie-parser';
|
||||
import * as bcrypt from 'bcrypt';
|
||||
import passport from 'passport';
|
||||
import * as bcrypt from 'bcrypt';
|
||||
import { errorHandler } from '../middleware/errorHandler';
|
||||
import { UserProfile } from '../types';
|
||||
import { createMockUserProfile } from '../tests/utils/mockFactories';
|
||||
import { mockLogger } from '../tests/utils/mockLogger';
|
||||
import { createTestApp } from '../tests/utils/createTestApp';
|
||||
|
||||
// --- FIX: Hoist passport mocks to be available for vi.mock ---
|
||||
const passportMocks = vi.hoisted(() => {
|
||||
type PassportCallback = (error: Error | null, user?: Express.User | false, info?: { message: string }) => void;
|
||||
|
||||
const authenticateMock = (strategy: string, options: Record<string, unknown>, callback: PassportCallback) => (req: Request, res: any, next: any) => {
|
||||
const authenticateMock = (strategy: string, options: Record<string, unknown>, callback: PassportCallback) => (req: Request, res: Response, next: NextFunction) => {
|
||||
// Simulate LocalStrategy logic based on request body
|
||||
if (req.body.password === 'wrong_password') {
|
||||
return callback(null, false, { message: 'Incorrect email or password.' });
|
||||
@@ -36,7 +31,7 @@ const passportMocks = vi.hoisted(() => {
|
||||
}
|
||||
|
||||
// Default success case
|
||||
const user = { user_id: 'user-123', email: req.body.email };
|
||||
const user = createMockUserProfile({ user_id: 'user-123', user: { user_id: 'user-123', email: req.body.email } });
|
||||
|
||||
// If a callback is provided (custom callback signature), call it
|
||||
if (callback) {
|
||||
@@ -58,8 +53,8 @@ vi.mock('./passport.routes', () => ({
|
||||
default: {
|
||||
authenticate: vi.fn().mockImplementation(passportMocks.authenticateMock),
|
||||
use: vi.fn(),
|
||||
initialize: () => (req: any, res: any, next: any) => next(),
|
||||
session: () => (req: any, res: any, next: any) => next(),
|
||||
initialize: () => (req: Request, res: Response, next: NextFunction) => next(),
|
||||
session: () => (req: Request, res: Response, next: NextFunction) => next(),
|
||||
},
|
||||
// Also mock named exports if they were used in auth.routes.ts, though they are not currently.
|
||||
isAdmin: vi.fn((req: Request, res: Response, next: NextFunction) => next()),
|
||||
@@ -67,13 +62,13 @@ vi.mock('./passport.routes', () => ({
|
||||
}));
|
||||
|
||||
// Mock the DB connection pool to control transactional behavior
|
||||
const { mockPool, mockClient } = vi.hoisted(() => {
|
||||
const { mockPool } = vi.hoisted(() => {
|
||||
const client = {
|
||||
query: vi.fn(),
|
||||
release: vi.fn(),
|
||||
};
|
||||
return {
|
||||
mockPool: {
|
||||
mockPool: {
|
||||
connect: vi.fn(() => Promise.resolve(client)),
|
||||
},
|
||||
mockClient: client,
|
||||
@@ -109,12 +104,7 @@ vi.mock('../services/db/connection.db', () => ({
|
||||
|
||||
// Mock the logger
|
||||
vi.mock('../services/logger.server', () => ({
|
||||
logger: {
|
||||
info: vi.fn(),
|
||||
debug: vi.fn(),
|
||||
error: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
},
|
||||
logger: mockLogger,
|
||||
}));
|
||||
|
||||
// Mock the email service
|
||||
@@ -132,27 +122,13 @@ vi.mock('bcrypt', async (importOriginal) => {
|
||||
import authRouter from './auth.routes';
|
||||
import * as db from '../services/db/index.db'; // This was a duplicate, fixed.
|
||||
|
||||
// Import the actual class so we can spy on its prototype
|
||||
import { UserRepository } from '../services/db/user.db';
|
||||
|
||||
import { UniqueConstraintError } from '../services/db/errors.db'; // Import actual class for instanceof checks
|
||||
|
||||
// --- 4. App Setup ---
|
||||
const app = express();
|
||||
app.use(express.json({ strict: false }));
|
||||
const app = createTestApp({ router: authRouter, basePath: '/api/auth' });
|
||||
// Add cookie parser for the auth routes that need it
|
||||
app.use(cookieParser());
|
||||
|
||||
// Mock req.logIn for passport session handling stubs
|
||||
app.use((req: Request, res: Response, next: NextFunction) => {
|
||||
req.logIn = (user: any, cb: any) => {
|
||||
if (typeof cb === 'function') cb(null);
|
||||
};
|
||||
next();
|
||||
});
|
||||
|
||||
app.use('/api/auth', authRouter);
|
||||
app.use(errorHandler);
|
||||
|
||||
// --- 5. Tests ---
|
||||
describe('Auth Routes (/api/auth)', () => {
|
||||
beforeEach(() => {
|
||||
@@ -230,7 +206,7 @@ describe('Auth Routes (/api/auth)', () => {
|
||||
});
|
||||
|
||||
it('should return 500 if a generic database error occurs during registration', async () => {
|
||||
const dbError = new Error('DB connection lost');
|
||||
const dbError = new Error('DB connection lost');
|
||||
vi.mocked(db.userRepo.createUser).mockRejectedValue(dbError);
|
||||
|
||||
const response = await supertest(app)
|
||||
|
||||
@@ -14,7 +14,7 @@ import { getPool } from '../services/db/connection.db';
|
||||
import { logger } from '../services/logger.server';
|
||||
import { sendPasswordResetEmail } from '../services/emailService.server';
|
||||
import { validateRequest } from '../middleware/validation.middleware';
|
||||
import { UserProfile } from '../types';
|
||||
import type { UserProfile } from '../types';
|
||||
|
||||
const router = Router();
|
||||
|
||||
@@ -164,14 +164,14 @@ router.post('/login', (req: Request, res: Response, next: NextFunction) => {
|
||||
return res.status(401).json({ message: info.message || 'Login failed' });
|
||||
}
|
||||
|
||||
const typedUser = user as { user_id: string; email: string };
|
||||
const payload = { user_id: typedUser.user_id, email: typedUser.email };
|
||||
const typedUser = user as UserProfile;
|
||||
const payload = { user_id: typedUser.user.user_id, email: typedUser.user.email };
|
||||
const accessToken = jwt.sign(payload, JWT_SECRET, { expiresIn: '15m' });
|
||||
|
||||
try {
|
||||
const refreshToken = crypto.randomBytes(64).toString('hex');
|
||||
await userRepo.saveRefreshToken(typedUser.user_id, refreshToken, req.log);
|
||||
req.log.info(`JWT and refresh token issued for user: ${typedUser.email}`);
|
||||
const refreshToken = crypto.randomBytes(64).toString('hex'); // This was a duplicate, fixed.
|
||||
await userRepo.saveRefreshToken(typedUser.user.user_id, refreshToken, req.log);
|
||||
req.log.info(`JWT and refresh token issued for user: ${typedUser.user.email}`);
|
||||
|
||||
const cookieOptions = {
|
||||
httpOnly: true,
|
||||
@@ -180,11 +180,11 @@ router.post('/login', (req: Request, res: Response, next: NextFunction) => {
|
||||
};
|
||||
|
||||
res.cookie('refreshToken', refreshToken, cookieOptions);
|
||||
const userResponse = { user_id: typedUser.user_id, email: typedUser.email };
|
||||
const userResponse = { user_id: typedUser.user.user_id, email: typedUser.user.email };
|
||||
|
||||
return res.json({ user: userResponse, token: accessToken });
|
||||
} catch (tokenErr) {
|
||||
req.log.error({ error: tokenErr }, `Failed to save refresh token during login for user: ${typedUser.email}`);
|
||||
req.log.error({ error: tokenErr }, `Failed to save refresh token during login for user: ${typedUser.user.email}`);
|
||||
return next(tokenErr);
|
||||
}
|
||||
})(req, res, next);
|
||||
|
||||
@@ -4,9 +4,9 @@ import supertest from 'supertest';
|
||||
import express, { Request, Response, NextFunction } from 'express';
|
||||
import budgetRouter from './budget.routes';
|
||||
import * as db from '../services/db/index.db';
|
||||
import { errorHandler } from '../middleware/errorHandler';
|
||||
import { createMockUserProfile, createMockBudget, createMockSpendingByCategory } from '../tests/utils/mockFactories';
|
||||
|
||||
import { mockLogger } from '../tests/utils/mockLogger';
|
||||
import { createTestApp } from '../tests/utils/createTestApp';
|
||||
import { ForeignKeyConstraintError, NotFoundError } from '../services/db/errors.db';
|
||||
// 1. Mock the Service Layer directly.
|
||||
// This decouples the route tests from the database logic.
|
||||
@@ -25,35 +25,25 @@ vi.mock('../services/db/index.db', () => ({
|
||||
|
||||
// Mock the logger to keep test output clean
|
||||
vi.mock('../services/logger.server', () => ({
|
||||
logger: {
|
||||
info: vi.fn(),
|
||||
debug: vi.fn(),
|
||||
error: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
},
|
||||
logger: mockLogger,
|
||||
}));
|
||||
|
||||
const mockUser = createMockUserProfile({
|
||||
user_id: 'user-123',
|
||||
user: { user_id: 'user-123', email: 'test@test.com' }
|
||||
});
|
||||
|
||||
// Standardized mock for passport.routes
|
||||
vi.mock('./passport.routes', () => ({
|
||||
default: {
|
||||
authenticate: vi.fn(() => (req: Request, res: Response, next: NextFunction) => {
|
||||
// Simulate an authenticated user for all tests in this file
|
||||
(req as any).user = { user_id: 'user-123', email: 'test@test.com' };
|
||||
req.user = mockUser;
|
||||
next();
|
||||
}),
|
||||
initialize: () => (req: Request, res: Response, next: NextFunction) => next(),
|
||||
},
|
||||
// We also need to provide mocks for any other named exports from passport.routes.ts
|
||||
isAdmin: vi.fn((req: Request, res: Response, next: NextFunction) => next()),
|
||||
optionalAuth: vi.fn((req: Request, res: Response, next: NextFunction) => next()),
|
||||
}));
|
||||
|
||||
// Create a minimal Express app to host our router
|
||||
const app = express();
|
||||
app.use(express.json());
|
||||
app.use('/api/budgets', budgetRouter);
|
||||
app.use(errorHandler);
|
||||
|
||||
describe('Budget Routes (/api/budgets)', () => {
|
||||
const mockUserProfile = createMockUserProfile({ user_id: 'user-123', points: 100 });
|
||||
|
||||
@@ -64,6 +54,8 @@ describe('Budget Routes (/api/budgets)', () => {
|
||||
vi.mocked(db.budgetRepo.getBudgetsForUser).mockResolvedValue([]);
|
||||
vi.mocked(db.budgetRepo.getSpendingByCategory).mockResolvedValue([]);
|
||||
});
|
||||
|
||||
const app = createTestApp({ router: budgetRouter, basePath: '/api/budgets', authenticatedUser: mockUser });
|
||||
|
||||
describe('GET /', () => {
|
||||
it('should return a list of budgets for the user', async () => {
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
// src/routes/deals.routes.test.ts
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { describe, it, expect, vi, beforeEach, type Mock } from 'vitest';
|
||||
import supertest from 'supertest';
|
||||
import express, { Request, Response, NextFunction } from 'express';
|
||||
import dealsRouter from './deals.routes';
|
||||
import { dealsRepo } from '../services/db/deals.db';
|
||||
import { errorHandler } from '../middleware/errorHandler';
|
||||
import { createMockUserProfile } from '../tests/utils/mockFactories';
|
||||
import type { UserProfile, WatchedItemDeal } from '../types';
|
||||
import { mockLogger } from '../tests/utils/mockLogger';
|
||||
import { createTestApp } from '../tests/utils/createTestApp';
|
||||
|
||||
// 1. Mock the Service Layer directly.
|
||||
vi.mock('../services/db/deals.db', () => ({
|
||||
@@ -17,18 +18,13 @@ vi.mock('../services/db/deals.db', () => ({
|
||||
|
||||
// Mock the logger to keep test output clean
|
||||
vi.mock('../services/logger.server', () => ({
|
||||
logger: {
|
||||
info: vi.fn(),
|
||||
debug: vi.fn(),
|
||||
error: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
},
|
||||
logger: mockLogger,
|
||||
}));
|
||||
|
||||
// Mock the passport middleware
|
||||
vi.mock('./passport.routes', () => ({
|
||||
default: {
|
||||
authenticate: vi.fn((strategy, options) => (req: Request, res: Response, next: NextFunction) => {
|
||||
authenticate: vi.fn((_strategy, _options) => (req: Request, res: Response, next: NextFunction) => {
|
||||
// If req.user is not set by the test setup, simulate unauthenticated access.
|
||||
if (!req.user) {
|
||||
return res.status(401).json({ message: 'Unauthorized' });
|
||||
@@ -39,27 +35,11 @@ vi.mock('./passport.routes', () => ({
|
||||
},
|
||||
}));
|
||||
|
||||
// Helper function to create a test app instance.
|
||||
const createApp = (user?: UserProfile) => {
|
||||
const app = express();
|
||||
app.use(express.json());
|
||||
// Conditionally add a middleware to inject the user object for authenticated tests.
|
||||
if (user) {
|
||||
app.use((req, res, next) => {
|
||||
req.user = user;
|
||||
// Add a mock `log` object to the request, as the route handler uses it.
|
||||
(req as any).log = { info: vi.fn(), error: vi.fn() };
|
||||
next();
|
||||
});
|
||||
}
|
||||
// The route is mounted on `/api/users/deals` in the main server file, so we replicate that here.
|
||||
app.use('/api/users/deals', dealsRouter);
|
||||
app.use(errorHandler);
|
||||
return app;
|
||||
};
|
||||
|
||||
describe('Deals Routes (/api/users/deals)', () => {
|
||||
const mockUser = createMockUserProfile({ user_id: 'user-123' });
|
||||
const basePath = '/api/users/deals';
|
||||
const authenticatedApp = createTestApp({ router: dealsRouter, basePath, authenticatedUser: mockUser });
|
||||
const unauthenticatedApp = createTestApp({ router: dealsRouter, basePath });
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
@@ -67,13 +47,11 @@ describe('Deals Routes (/api/users/deals)', () => {
|
||||
|
||||
describe('GET /best-watched-prices', () => {
|
||||
it('should return 401 Unauthorized if user is not authenticated', async () => {
|
||||
const app = createApp(); // No user provided
|
||||
const response = await supertest(app).get('/api/users/deals/best-watched-prices');
|
||||
const response = await supertest(unauthenticatedApp).get('/api/users/deals/best-watched-prices');
|
||||
expect(response.status).toBe(401);
|
||||
});
|
||||
|
||||
it('should return a list of deals for an authenticated user', async () => {
|
||||
const app = createApp(mockUser);
|
||||
const mockDeals: WatchedItemDeal[] = [{
|
||||
master_item_id: 123,
|
||||
item_name: 'Apples',
|
||||
@@ -84,7 +62,7 @@ describe('Deals Routes (/api/users/deals)', () => {
|
||||
}];
|
||||
vi.mocked(dealsRepo.findBestPricesForWatchedItems).mockResolvedValue(mockDeals);
|
||||
|
||||
const response = await supertest(app).get('/api/users/deals/best-watched-prices');
|
||||
const response = await supertest(authenticatedApp).get('/api/users/deals/best-watched-prices');
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body).toEqual(mockDeals);
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import supertest from 'supertest';
|
||||
import express from 'express';
|
||||
import { errorHandler } from '../middleware/errorHandler';
|
||||
import flyerRouter from './flyer.routes';
|
||||
import { createMockFlyer, createMockFlyerItem } from '../tests/utils/mockFactories';
|
||||
import { NotFoundError } from '../services/db/errors.db';
|
||||
import { createTestApp } from '../tests/utils/createTestApp';
|
||||
|
||||
// 1. Mock the Service Layer directly.
|
||||
vi.mock('../services/db/index.db', () => ({
|
||||
@@ -19,34 +19,22 @@ vi.mock('../services/db/index.db', () => ({
|
||||
},
|
||||
}));
|
||||
|
||||
import { mockLogger } from '../tests/utils/mockLogger';
|
||||
|
||||
// Mock the logger to keep test output clean
|
||||
vi.mock('../services/logger.server', () => ({
|
||||
logger: {
|
||||
error: vi.fn(),
|
||||
},
|
||||
logger: mockLogger,
|
||||
}));
|
||||
|
||||
// Import the mocked db module to control its functions in tests
|
||||
import * as db from '../services/db/index.db';
|
||||
|
||||
// Create the Express app
|
||||
const app = express();
|
||||
app.use(express.json());
|
||||
|
||||
// Add a middleware to inject a mock req.log object for tests
|
||||
app.use((req, res, next) => {
|
||||
req.log = { info: vi.fn(), debug: vi.fn(), error: vi.fn(), warn: vi.fn() } as any;
|
||||
next();
|
||||
});
|
||||
|
||||
// Mount the router under its designated base path
|
||||
app.use('/api/flyers', flyerRouter);
|
||||
app.use(errorHandler);
|
||||
|
||||
describe('Flyer Routes (/api/flyers)', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
const app = createTestApp({ router: flyerRouter, basePath: '/api/flyers' });
|
||||
|
||||
describe('GET /', () => {
|
||||
it('should return a list of flyers on success', async () => {
|
||||
|
||||
@@ -4,9 +4,9 @@ import supertest from 'supertest';
|
||||
import express, { Request, Response, NextFunction } from 'express';
|
||||
import gamificationRouter from './gamification.routes';
|
||||
import * as db from '../services/db/index.db';
|
||||
import { errorHandler } from '../middleware/errorHandler';
|
||||
import { createMockUserProfile, createMockAchievement, createMockUserAchievement } from '../tests/utils/mockFactories';
|
||||
import { ForeignKeyConstraintError } from '../services/db/errors.db';
|
||||
import { createTestApp } from '../tests/utils/createTestApp';
|
||||
|
||||
// Mock the entire db service
|
||||
vi.mock('../services/db/index.db', () => ({
|
||||
@@ -41,12 +41,6 @@ vi.mock('./passport.routes', () => ({
|
||||
isAdmin: mockedIsAdmin,
|
||||
}));
|
||||
|
||||
// Create a minimal Express app to host our router
|
||||
const app = express();
|
||||
app.use(express.json({ strict: false }));
|
||||
app.use('/api/achievements', gamificationRouter);
|
||||
app.use(errorHandler);
|
||||
|
||||
describe('Gamification Routes (/api/achievements)', () => {
|
||||
const mockUserProfile = createMockUserProfile({ user_id: 'user-123', points: 100 });
|
||||
const mockAdminProfile = createMockUserProfile({ user_id: 'admin-456', role: 'admin', points: 999 });
|
||||
@@ -62,13 +56,17 @@ describe('Gamification Routes (/api/achievements)', () => {
|
||||
});
|
||||
});
|
||||
|
||||
const basePath = '/api/achievements';
|
||||
const unauthenticatedApp = createTestApp({ router: gamificationRouter, basePath });
|
||||
const authenticatedApp = createTestApp({ router: gamificationRouter, basePath, authenticatedUser: mockUserProfile });
|
||||
const adminApp = createTestApp({ router: gamificationRouter, basePath, authenticatedUser: mockAdminProfile });
|
||||
|
||||
describe('GET /', () => {
|
||||
it('should return a list of all achievements (public endpoint)', async () => {
|
||||
const mockAchievements = [createMockAchievement({ achievement_id: 1 }), createMockAchievement({ achievement_id: 2 })];
|
||||
vi.mocked(db.gamificationRepo.getAllAchievements).mockResolvedValue(mockAchievements);
|
||||
|
||||
const response = await supertest(app).get('/api/achievements');
|
||||
|
||||
const response = await supertest(unauthenticatedApp).get('/api/achievements');
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body).toEqual(mockAchievements);
|
||||
expect(db.gamificationRepo.getAllAchievements).toHaveBeenCalledTimes(1);
|
||||
@@ -78,7 +76,7 @@ describe('Gamification Routes (/api/achievements)', () => {
|
||||
const dbError = new Error('DB Connection Failed');
|
||||
vi.mocked(db.gamificationRepo.getAllAchievements).mockRejectedValue(dbError);
|
||||
|
||||
const response = await supertest(app).get('/api/achievements');
|
||||
const response = await supertest(unauthenticatedApp).get('/api/achievements');
|
||||
expect(response.status).toBe(500);
|
||||
expect(response.body.message).toBe('DB Connection Failed');
|
||||
});
|
||||
@@ -91,7 +89,7 @@ describe('Gamification Routes (/api/achievements)', () => {
|
||||
mockedIsAdmin.mockImplementation((req: Request, res: Response, next: NextFunction) => next());
|
||||
vi.mocked(db.gamificationRepo.awardAchievement).mockRejectedValue(new ForeignKeyConstraintError('User not found'));
|
||||
|
||||
const response = await supertest(app).post('/api/achievements/award').send({ userId: 'non-existent', achievementName: 'Test Award' });
|
||||
const response = await supertest(adminApp).post('/api/achievements/award').send({ userId: 'non-existent', achievementName: 'Test Award' });
|
||||
expect(response.status).toBe(400);
|
||||
expect(response.body.message).toBe('User not found');
|
||||
});
|
||||
@@ -99,7 +97,7 @@ describe('Gamification Routes (/api/achievements)', () => {
|
||||
|
||||
describe('GET /me', () => {
|
||||
it('should return 401 Unauthorized when user is not authenticated', async () => {
|
||||
const response = await supertest(app).get('/api/achievements/me');
|
||||
const response = await supertest(unauthenticatedApp).get('/api/achievements/me');
|
||||
expect(response.status).toBe(401);
|
||||
});
|
||||
|
||||
@@ -113,7 +111,7 @@ describe('Gamification Routes (/api/achievements)', () => {
|
||||
const mockUserAchievements = [createMockUserAchievement({ achievement_id: 1, user_id: 'user-123' })];
|
||||
vi.mocked(db.gamificationRepo.getUserAchievements).mockResolvedValue(mockUserAchievements);
|
||||
|
||||
const response = await supertest(app).get('/api/achievements/me');
|
||||
const response = await supertest(authenticatedApp).get('/api/achievements/me');
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body).toEqual(mockUserAchievements);
|
||||
@@ -128,7 +126,7 @@ describe('Gamification Routes (/api/achievements)', () => {
|
||||
});
|
||||
const dbError = new Error('DB Error');
|
||||
vi.mocked(db.gamificationRepo.getUserAchievements).mockRejectedValue(dbError);
|
||||
const response = await supertest(app).get('/api/achievements/me');
|
||||
const response = await supertest(authenticatedApp).get('/api/achievements/me');
|
||||
expect(response.status).toBe(500);
|
||||
expect(response.body.message).toBe('DB Error');
|
||||
});
|
||||
@@ -138,7 +136,7 @@ describe('Gamification Routes (/api/achievements)', () => {
|
||||
const awardPayload = { userId: 'user-789', achievementName: 'Test Award' };
|
||||
|
||||
it('should return 401 Unauthorized if user is not authenticated', async () => {
|
||||
const response = await supertest(app).post('/api/achievements/award').send(awardPayload);
|
||||
const response = await supertest(unauthenticatedApp).post('/api/achievements/award').send(awardPayload);
|
||||
expect(response.status).toBe(401);
|
||||
});
|
||||
|
||||
@@ -150,7 +148,7 @@ describe('Gamification Routes (/api/achievements)', () => {
|
||||
});
|
||||
// Let the default isAdmin mock (set in beforeEach) run, which denies access
|
||||
|
||||
const response = await supertest(app).post('/api/achievements/award').send(awardPayload);
|
||||
const response = await supertest(authenticatedApp).post('/api/achievements/award').send(awardPayload);
|
||||
expect(response.status).toBe(403);
|
||||
});
|
||||
|
||||
@@ -163,7 +161,7 @@ describe('Gamification Routes (/api/achievements)', () => {
|
||||
mockedIsAdmin.mockImplementation((req: Request, res: Response, next: NextFunction) => next()); // Grant admin access
|
||||
vi.mocked(db.gamificationRepo.awardAchievement).mockResolvedValue(undefined);
|
||||
|
||||
const response = await supertest(app).post('/api/achievements/award').send(awardPayload);
|
||||
const response = await supertest(adminApp).post('/api/achievements/award').send(awardPayload);
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body.message).toContain('Successfully awarded');
|
||||
@@ -179,7 +177,7 @@ describe('Gamification Routes (/api/achievements)', () => {
|
||||
mockedIsAdmin.mockImplementation((req: Request, res: Response, next: NextFunction) => next());
|
||||
vi.mocked(db.gamificationRepo.awardAchievement).mockRejectedValue(new Error('DB Error'));
|
||||
|
||||
const response = await supertest(app).post('/api/achievements/award').send(awardPayload);
|
||||
const response = await supertest(adminApp).post('/api/achievements/award').send(awardPayload);
|
||||
expect(response.status).toBe(500);
|
||||
expect(response.body.message).toBe('DB Error');
|
||||
});
|
||||
@@ -188,7 +186,7 @@ describe('Gamification Routes (/api/achievements)', () => {
|
||||
mockedAuthMiddleware.mockImplementation((req: Request, res: Response, next: NextFunction) => { req.user = mockAdminProfile; next(); });
|
||||
mockedIsAdmin.mockImplementation((req: Request, res: Response, next: NextFunction) => next());
|
||||
|
||||
const response = await supertest(app).post('/api/achievements/award').send({ userId: '', achievementName: '' });
|
||||
const response = await supertest(adminApp).post('/api/achievements/award').send({ userId: '', achievementName: '' });
|
||||
expect(response.status).toBe(400);
|
||||
expect(response.body.errors).toHaveLength(2);
|
||||
});
|
||||
@@ -201,7 +199,7 @@ describe('Gamification Routes (/api/achievements)', () => {
|
||||
mockedIsAdmin.mockImplementation((req: Request, res: Response, next: NextFunction) => next());
|
||||
vi.mocked(db.gamificationRepo.awardAchievement).mockRejectedValue(new ForeignKeyConstraintError('User not found'));
|
||||
|
||||
const response = await supertest(app).post('/api/achievements/award').send({ userId: 'non-existent', achievementName: 'Test Award' });
|
||||
const response = await supertest(adminApp).post('/api/achievements/award').send({ userId: 'non-existent', achievementName: 'Test Award' });
|
||||
expect(response.status).toBe(400);
|
||||
expect(response.body.message).toBe('User not found');
|
||||
});
|
||||
@@ -212,7 +210,7 @@ describe('Gamification Routes (/api/achievements)', () => {
|
||||
const mockLeaderboard = [{ user_id: 'user-1', full_name: 'Leader', points: 1000, rank: '1' }];
|
||||
vi.mocked(db.gamificationRepo.getLeaderboard).mockResolvedValue(mockLeaderboard as any);
|
||||
|
||||
const response = await supertest(app).get('/api/achievements/leaderboard?limit=5');
|
||||
const response = await supertest(unauthenticatedApp).get('/api/achievements/leaderboard?limit=5');
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body).toEqual(mockLeaderboard);
|
||||
@@ -221,13 +219,13 @@ describe('Gamification Routes (/api/achievements)', () => {
|
||||
|
||||
it('should return 500 if the database call fails', async () => {
|
||||
vi.mocked(db.gamificationRepo.getLeaderboard).mockRejectedValue(new Error('DB Error'));
|
||||
const response = await supertest(app).get('/api/achievements/leaderboard');
|
||||
const response = await supertest(unauthenticatedApp).get('/api/achievements/leaderboard');
|
||||
expect(response.status).toBe(500);
|
||||
expect(response.body.message).toBe('DB Error');
|
||||
});
|
||||
|
||||
it('should return 400 for an invalid limit parameter', async () => {
|
||||
const response = await supertest(app).get('/api/achievements/leaderboard?limit=100');
|
||||
const response = await supertest(unauthenticatedApp).get('/api/achievements/leaderboard?limit=100');
|
||||
expect(response.status).toBe(400);
|
||||
expect(response.body.errors).toBeDefined();
|
||||
expect(response.body.errors[0].message).toContain('less than or equal to 50');
|
||||
|
||||
@@ -5,8 +5,8 @@ import express from 'express';
|
||||
import healthRouter from './health.routes';
|
||||
import * as dbConnection from '../services/db/connection.db';
|
||||
import { connection as redisConnection } from '../services/queueService.server';
|
||||
import { errorHandler } from '../middleware/errorHandler';
|
||||
import fs from 'node:fs/promises';
|
||||
import { createTestApp } from '../tests/utils/createTestApp';
|
||||
|
||||
// 1. Mock the dependencies of the health router.
|
||||
vi.mock('../services/db/connection.db', () => ({
|
||||
@@ -45,9 +45,7 @@ const mockedDbConnection = dbConnection as Mocked<typeof dbConnection>;
|
||||
const mockedFs = fs as Mocked<typeof fs>;
|
||||
|
||||
// 2. Create a minimal Express app to host the router for testing.
|
||||
const app = express();
|
||||
app.use('/api/health', healthRouter);
|
||||
app.use(errorHandler);
|
||||
const app = createTestApp({ router: healthRouter, basePath: '/api/health' });
|
||||
|
||||
describe('Health Routes (/api/health)', () => {
|
||||
beforeEach(() => {
|
||||
|
||||
@@ -4,7 +4,8 @@ import supertest from 'supertest';
|
||||
import express, { Request, Response, NextFunction } from 'express';
|
||||
import personalizationRouter from './personalization.routes';
|
||||
import { createMockMasterGroceryItem, createMockDietaryRestriction, createMockAppliance } from '../tests/utils/mockFactories';
|
||||
import { errorHandler } from '../middleware/errorHandler';
|
||||
import { mockLogger } from '../tests/utils/mockLogger';
|
||||
import { createTestApp } from '../tests/utils/createTestApp';
|
||||
|
||||
// 1. Mock the Service Layer directly.
|
||||
vi.mock('../services/db/index.db', () => ({
|
||||
@@ -17,29 +18,14 @@ vi.mock('../services/db/index.db', () => ({
|
||||
|
||||
// Mock the logger to keep test output clean
|
||||
vi.mock('../services/logger.server', () => ({
|
||||
logger: {
|
||||
error: vi.fn(),
|
||||
},
|
||||
logger: mockLogger,
|
||||
}));
|
||||
|
||||
// Import the mocked db module to control its functions in tests
|
||||
import * as db from '../services/db/index.db';
|
||||
|
||||
// Create the Express app
|
||||
const app = express();
|
||||
app.use(express.json());
|
||||
|
||||
// Add a middleware to inject a mock req.log object for tests
|
||||
app.use((req, res, next) => {
|
||||
req.log = { info: vi.fn(), debug: vi.fn(), error: vi.fn(), warn: vi.fn() } as any;
|
||||
next();
|
||||
});
|
||||
|
||||
// Mount the router under its designated base path
|
||||
app.use('/api/personalization', personalizationRouter);
|
||||
app.use(errorHandler);
|
||||
|
||||
describe('Personalization Routes (/api/personalization)', () => {
|
||||
const app = createTestApp({ router: personalizationRouter, basePath: '/api/personalization' });
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
@@ -3,7 +3,7 @@ import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import supertest from 'supertest';
|
||||
import express from 'express';
|
||||
import priceRouter from './price.routes';
|
||||
import { errorHandler } from '../middleware/errorHandler';
|
||||
import { createTestApp } from '../tests/utils/createTestApp';
|
||||
|
||||
// Mock the logger to keep test output clean
|
||||
vi.mock('../services/logger.server', () => ({
|
||||
@@ -13,13 +13,8 @@ vi.mock('../services/logger.server', () => ({
|
||||
},
|
||||
}));
|
||||
|
||||
// Create a minimal Express app to host our router
|
||||
const app = express();
|
||||
app.use(express.json());
|
||||
app.use('/api/price-history', priceRouter);
|
||||
app.use(errorHandler);
|
||||
|
||||
describe('Price Routes (/api/price-history)', () => {
|
||||
const app = createTestApp({ router: priceRouter, basePath: '/api/price-history' });
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
@@ -3,9 +3,10 @@ import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import supertest from 'supertest';
|
||||
import express, { Request, Response, NextFunction } from 'express';
|
||||
import recipeRouter from './recipe.routes';
|
||||
import { errorHandler } from '../middleware/errorHandler';
|
||||
import { mockLogger } from '../tests/utils/mockLogger';
|
||||
import { createMockRecipe, createMockRecipeComment } from '../tests/utils/mockFactories';
|
||||
import { NotFoundError } from '../services/db/errors.db';
|
||||
import { createTestApp } from '../tests/utils/createTestApp';
|
||||
|
||||
// 1. Mock the Service Layer directly.
|
||||
vi.mock('../services/db/index.db', () => ({
|
||||
@@ -20,29 +21,14 @@ vi.mock('../services/db/index.db', () => ({
|
||||
|
||||
// Mock the logger to keep test output clean
|
||||
vi.mock('../services/logger.server', () => ({
|
||||
logger: {
|
||||
error: vi.fn(),
|
||||
},
|
||||
logger: mockLogger,
|
||||
}));
|
||||
|
||||
// Import the mocked db module to control its functions in tests
|
||||
import * as db from '../services/db/index.db';
|
||||
|
||||
// Create the Express app
|
||||
const app = express();
|
||||
app.use(express.json());
|
||||
|
||||
// Add a middleware to inject a mock req.log object for tests
|
||||
app.use((req, res, next) => {
|
||||
req.log = { info: vi.fn(), debug: vi.fn(), error: vi.fn(), warn: vi.fn() } as any;
|
||||
next();
|
||||
});
|
||||
|
||||
// Mount the router under its designated base path
|
||||
app.use('/api/recipes', recipeRouter);
|
||||
app.use(errorHandler);
|
||||
|
||||
describe('Recipe Routes (/api/recipes)', () => {
|
||||
const app = createTestApp({ router: recipeRouter, basePath: '/api/recipes' });
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
@@ -2,8 +2,9 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import supertest from 'supertest';
|
||||
import express, { Request, Response, NextFunction } from 'express';
|
||||
import { errorHandler } from '../middleware/errorHandler';
|
||||
import statsRouter from './stats.routes';
|
||||
import { mockLogger } from '../tests/utils/mockLogger';
|
||||
import { createTestApp } from '../tests/utils/createTestApp';
|
||||
|
||||
// 1. Mock the Service Layer directly.
|
||||
vi.mock('../services/db/index.db', () => ({
|
||||
@@ -14,29 +15,14 @@ vi.mock('../services/db/index.db', () => ({
|
||||
|
||||
// Mock the logger to keep test output clean
|
||||
vi.mock('../services/logger.server', () => ({
|
||||
logger: {
|
||||
error: vi.fn(),
|
||||
},
|
||||
logger: mockLogger,
|
||||
}));
|
||||
|
||||
// Import the mocked db module to control its functions in tests
|
||||
import * as db from '../services/db/index.db';
|
||||
|
||||
// Create the Express app
|
||||
const app = express();
|
||||
app.use(express.json());
|
||||
|
||||
// Add a middleware to inject a mock req.log object for tests
|
||||
app.use((req, res, next) => {
|
||||
req.log = { info: vi.fn(), debug: vi.fn(), error: vi.fn(), warn: vi.fn() } as any;
|
||||
next();
|
||||
});
|
||||
|
||||
// Mount the router under its designated base path
|
||||
app.use('/api/stats', statsRouter);
|
||||
app.use(errorHandler);
|
||||
|
||||
describe('Stats Routes (/api/stats)', () => {
|
||||
const app = createTestApp({ router: statsRouter, basePath: '/api/stats' });
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
@@ -5,7 +5,8 @@ import express from 'express';
|
||||
import systemRouter from './system.routes';
|
||||
import { exec, type ExecException } from 'child_process';
|
||||
import { geocodingService } from '../services/geocodingService.server';
|
||||
import { errorHandler } from '../middleware/errorHandler';
|
||||
import { mockLogger } from '../tests/utils/mockLogger';
|
||||
import { createTestApp } from '../tests/utils/createTestApp';
|
||||
|
||||
// FIX: Use the simple factory pattern for child_process to avoid default export issues
|
||||
vi.mock('child_process', () => {
|
||||
@@ -31,28 +32,11 @@ vi.mock('../services/geocodingService.server', () => ({
|
||||
|
||||
// 3. Mock Logger
|
||||
vi.mock('../services/logger.server', () => ({
|
||||
logger: {
|
||||
info: vi.fn(),
|
||||
debug: vi.fn(),
|
||||
error: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
},
|
||||
logger: mockLogger,
|
||||
}));
|
||||
|
||||
// Create a minimal Express app to host our router
|
||||
const app = express();
|
||||
app.use(express.json());
|
||||
|
||||
// Add a middleware to inject a mock req.log object for tests
|
||||
app.use((req, res, next) => {
|
||||
req.log = { info: vi.fn(), debug: vi.fn(), error: vi.fn(), warn: vi.fn() } as any;
|
||||
next();
|
||||
});
|
||||
|
||||
app.use('/api/system', systemRouter);
|
||||
app.use(errorHandler);
|
||||
|
||||
describe('System Routes (/api/system)', () => {
|
||||
const app = createTestApp({ router: systemRouter, basePath: '/api/system' });
|
||||
beforeEach(() => {
|
||||
// We cast here to get type-safe access to mock functions like .mockImplementation
|
||||
vi.clearAllMocks();
|
||||
|
||||
@@ -8,7 +8,7 @@ import userRouter from './user.routes';
|
||||
import { createMockUserProfile, createMockMasterGroceryItem, createMockShoppingList, createMockShoppingListItem, createMockRecipe } from '../tests/utils/mockFactories';
|
||||
import { Appliance, Notification } from '../types';
|
||||
import { ForeignKeyConstraintError, NotFoundError } from '../services/db/errors.db';
|
||||
import { errorHandler } from '../middleware/errorHandler';
|
||||
import { createTestApp } from '../tests/utils/createTestApp';
|
||||
|
||||
// 1. Mock the Service Layer directly.
|
||||
// The user.routes.ts file imports from '.../db/index.db'. We need to mock that module.
|
||||
@@ -96,31 +96,15 @@ vi.mock('./passport.routes', () => ({
|
||||
// Import the mocked db module to control its functions in tests
|
||||
import * as db from '../services/db/index.db';
|
||||
|
||||
// Setup Express App
|
||||
const createApp = (authenticatedUser?: any) => {
|
||||
const app = express();
|
||||
app.use(express.json({ strict: false }));
|
||||
|
||||
if (authenticatedUser) {
|
||||
app.use((req, res, next) => {
|
||||
req.user = authenticatedUser;
|
||||
next();
|
||||
});
|
||||
}
|
||||
|
||||
app.use('/api/users', userRouter);
|
||||
app.use(errorHandler);
|
||||
return app;
|
||||
};
|
||||
|
||||
describe('User Routes (/api/users)', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
const basePath = '/api/users';
|
||||
|
||||
describe('when user is not authenticated', () => {
|
||||
it('GET /profile should return 401', async () => {
|
||||
const app = createApp(); // No user injected
|
||||
const app = createTestApp({ router: userRouter, basePath }); // No user injected
|
||||
const response = await supertest(app).get('/api/users/profile');
|
||||
expect(response.status).toBe(401);
|
||||
});
|
||||
@@ -128,10 +112,10 @@ describe('User Routes (/api/users)', () => {
|
||||
|
||||
describe('when user is authenticated', () => {
|
||||
const mockUserProfile = createMockUserProfile({ user_id: 'user-123' });
|
||||
let app: express.Express;
|
||||
const app = createTestApp({ router: userRouter, basePath, authenticatedUser: mockUserProfile });
|
||||
|
||||
beforeEach(() => {
|
||||
app = createApp(mockUserProfile);
|
||||
// All tests in this block will use the authenticated app
|
||||
});
|
||||
describe('GET /profile', () => {
|
||||
it('should return the full user profile', async () => {
|
||||
@@ -531,20 +515,20 @@ describe('User Routes (/api/users)', () => {
|
||||
|
||||
describe('GET /addresses/:addressId', () => {
|
||||
it('should return 400 for a non-numeric address ID', async () => {
|
||||
const response = await supertest(app).get('/api/users/addresses/abc');
|
||||
const response = await supertest(app).get('/api/users/addresses/abc'); // This was a duplicate, fixed.
|
||||
expect(response.status).toBe(400);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Address Routes', () => {
|
||||
it('GET /addresses/:addressId should return 403 if address does not belong to user', async () => {
|
||||
const appWithDifferentUser = createApp({ ...mockUserProfile, address_id: 999 } as any);
|
||||
const appWithDifferentUser = createTestApp({ router: userRouter, basePath, authenticatedUser: { ...mockUserProfile, address_id: 999 } });
|
||||
const response = await supertest(appWithDifferentUser).get('/api/users/addresses/1');
|
||||
expect(response.status).toBe(403);
|
||||
});
|
||||
|
||||
it('GET /addresses/:addressId should return 404 if address not found', async () => {
|
||||
const appWithUser = createApp({ ...mockUserProfile, address_id: 1 } as any);
|
||||
const appWithUser = createTestApp({ router: userRouter, basePath, authenticatedUser: { ...mockUserProfile, address_id: 1 } });
|
||||
vi.mocked(db.addressRepo.getAddressById).mockRejectedValue(new NotFoundError('Address not found.'));
|
||||
const response = await supertest(appWithUser).get('/api/users/addresses/1');
|
||||
expect(response.status).toBe(404);
|
||||
@@ -552,7 +536,7 @@ describe('User Routes (/api/users)', () => {
|
||||
});
|
||||
|
||||
it('PUT /profile/address should call upsertAddress and updateUserProfile if needed', async () => {
|
||||
const appWithUser = createApp({ ...mockUserProfile, address_id: null } as any); // User has no address yet
|
||||
const appWithUser = createTestApp({ router: userRouter, basePath, authenticatedUser: { ...mockUserProfile, address_id: null } }); // User has no address yet
|
||||
const addressData = { address_line_1: '123 New St' };
|
||||
vi.mocked(db.addressRepo.upsertAddress).mockResolvedValue(5); // New address ID is 5
|
||||
vi.mocked(db.userRepo.updateUserProfile).mockResolvedValue({} as any);
|
||||
|
||||
@@ -47,7 +47,7 @@ export async function setup() {
|
||||
const text = await response.text();
|
||||
return text === 'pong';
|
||||
} catch (e) {
|
||||
logger.debug('Ping failed while waiting for server, this is expected.', { error: e });
|
||||
logger.debug({ error: e }, 'Ping failed while waiting for server, this is expected.');
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,28 +1,3 @@
|
||||
// --- FIX REGISTRY ---
|
||||
//
|
||||
// 2024-08-01: Replaced the complex `PatchedFile` polyfill with a simpler mock class. The previous
|
||||
// approach of extending Node's `Blob` caused persistent type conflicts between JSDOM's
|
||||
// `Blob` and Node's `Blob`. The new mock provides the necessary properties (`name`, `size`,
|
||||
// `type`, `arrayBuffer`) directly, resolving the type error and stabilizing the test environment.
|
||||
//
|
||||
// added a polyfill for the global File object in your main unit test setup file. This uses the robust File
|
||||
// implementation from Node.js's buffer module and stubs it into the global scope. This ensures that any test
|
||||
// creating a new File(...) will produce an object with the expected properties (like .name), which should
|
||||
// resolve the expected 'blob' to be 'flyer.pdf' errors
|
||||
//
|
||||
// 2024-08-01: Added polyfills for `crypto.subtle` and `File.prototype.arrayBuffer` to the global unit test setup.
|
||||
// This resolves "is not a function" errors in tests that rely on these browser APIs, which are missing in JSDOM.
|
||||
//
|
||||
// 2024-08-01: Added `geocodeAddress` to the global `apiClient` mock. This resolves "export is not defined"
|
||||
// errors in tests that rely on this function, such as the ProfileManager tests.
|
||||
//
|
||||
// 2024-08-01: Fixed the global `pg` mock to include the `types` object with `builtins`.
|
||||
// This resolves a `TypeError: Cannot read properties of undefined (reading 'NUMERIC')`
|
||||
// that occurred in `connection.db.ts` during test runs.
|
||||
//
|
||||
// 2024-07-30: Added default mock implementations for `countFlyerItemsForFlyers` and `uploadAndProcessFlyer`.
|
||||
// These functions were returning `undefined`, causing async hooks/components to time out.
|
||||
// --- END FIX REGISTRY ---
|
||||
// src/tests/setup/tests-setup-unit.ts
|
||||
import { vi, afterEach } from 'vitest';
|
||||
import { cleanup } from '@testing-library/react';
|
||||
|
||||
48
src/tests/utils/createTestApp.ts
Normal file
48
src/tests/utils/createTestApp.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
// src/tests/utils/createTestApp.ts
|
||||
import express, { type Router } from 'express';
|
||||
import type { Logger } from 'pino';
|
||||
import { errorHandler } from '../../middleware/errorHandler';
|
||||
import { mockLogger } from './mockLogger';
|
||||
import type { UserProfile } from '../../types';
|
||||
|
||||
// By augmenting the Express Request interface in this central utility,
|
||||
// we ensure that any test using `createTestApp` will have a correctly
|
||||
// typed `req.log` and `req.user` property.
|
||||
declare global {
|
||||
namespace Express {
|
||||
// By extending the User interface from @types/passport, we make it
|
||||
// compatible with our application's UserProfile type.
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
||||
interface User extends UserProfile {}
|
||||
interface Request {
|
||||
log: Logger;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface CreateAppOptions {
|
||||
router: Router;
|
||||
basePath: string;
|
||||
authenticatedUser?: UserProfile;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a standardized Express application instance for integration tests.
|
||||
* It includes JSON parsing, a mock logger, an optional authenticated user,
|
||||
* the specified router, and the global error handler.
|
||||
*/
|
||||
export const createTestApp = ({ router, basePath, authenticatedUser }: CreateAppOptions) => {
|
||||
const app = express();
|
||||
app.use(express.json({ strict: false }));
|
||||
|
||||
// Inject the mock logger and authenticated user into every request.
|
||||
app.use((req, res, next) => {
|
||||
req.log = mockLogger;
|
||||
if (authenticatedUser) req.user = authenticatedUser;
|
||||
next();
|
||||
});
|
||||
|
||||
app.use(basePath, router);
|
||||
app.use(errorHandler);
|
||||
return app;
|
||||
};
|
||||
22
src/tests/utils/mockLogger.ts
Normal file
22
src/tests/utils/mockLogger.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
// src/tests/utils/mockLogger.ts
|
||||
import { vi } from 'vitest';
|
||||
import type { Logger } from 'pino';
|
||||
|
||||
/**
|
||||
* Creates a complete, type-safe mock of the Pino logger for use in tests.
|
||||
* All logger methods are replaced with `vi.fn()`.
|
||||
* The `child` method is mocked to return itself, allowing for chained calls
|
||||
* like `logger.child({ ... }).info(...)` to work seamlessly in tests.
|
||||
*/
|
||||
export const createMockLogger = (): Logger => ({
|
||||
info: vi.fn(),
|
||||
debug: vi.fn(),
|
||||
error: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
fatal: vi.fn(),
|
||||
trace: vi.fn(),
|
||||
silent: vi.fn(),
|
||||
child: vi.fn().mockReturnThis(),
|
||||
} as unknown as Logger);
|
||||
|
||||
export const mockLogger = createMockLogger();
|
||||
Reference in New Issue
Block a user