Refactor test setup and improve mock implementations
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:
2025-12-14 14:47:19 -08:00
parent edb0f8a38c
commit eb0e183f61
31 changed files with 244 additions and 419 deletions

View File

@@ -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) {

View File

@@ -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;

View File

@@ -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>) => {

View File

@@ -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 ---

View File

@@ -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();

View File

@@ -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();

View File

@@ -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();

View File

@@ -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();

View File

@@ -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';

View File

@@ -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();

View File

@@ -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();

View File

@@ -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 },
];

View File

@@ -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);

View File

@@ -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.'),
}),

View File

@@ -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)

View File

@@ -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);

View File

@@ -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 () => {

View File

@@ -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);

View File

@@ -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 () => {

View File

@@ -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');

View File

@@ -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(() => {

View File

@@ -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();
});

View File

@@ -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();
});

View File

@@ -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();
});

View File

@@ -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();
});

View File

@@ -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();

View File

@@ -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);

View File

@@ -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;
}
};

View File

@@ -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';

View 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;
};

View 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();