Refactor MainLayout to use new hooks for flyers and master items; consolidate error handling
Some checks failed
Deploy to Test Environment / deploy-to-test (push) Has been cancelled

Update error handling tests to ensure proper status assignment for errors
This commit is contained in:
2025-12-14 11:05:04 -08:00
parent 571ca59e82
commit 9757f9dd9f
40 changed files with 348 additions and 207 deletions

View File

@@ -1,16 +1,5 @@
// --- FIX REGISTRY ---
//
// 1) Updated `vi.mock` for `../services/db/index.db` to use `vi.hoisted` and `importOriginal`.
// This preserves named exports (like repository classes) from the original module,
// fixing 'undefined' errors when other modules tried to import them from the mock.
//
// 2) Added a `default` export to the `node:fs/promises` mock. This resolves import errors
// in modules that use the `import fs from 'node:fs/promises'` syntax.
//
// --- END FIX REGISTRY ---
// src/routes/admin.content.routes.test.ts
import { describe, it, expect, vi, beforeEach, type Mocked } from 'vitest';
import { describe, it, expect, vi, beforeEach } from 'vitest';
import supertest from 'supertest';
import express, { Request, Response, NextFunction } from 'express';
import adminRouter from './admin.routes';

View File

@@ -194,7 +194,7 @@ describe('Admin Job Trigger Routes (/api/admin/trigger)', () => {
getState: vi.fn().mockResolvedValue('failed'),
retry: vi.fn().mockResolvedValue(undefined),
};
vi.mocked(flyerQueue.getJob).mockResolvedValue(mockJob as any);
vi.mocked(flyerQueue.getJob).mockResolvedValue(mockJob as unknown as Job);
// Act
const response = await supertest(app).post(`/api/admin/jobs/${queueName}/${jobId}/retry`);
@@ -224,7 +224,7 @@ describe('Admin Job Trigger Routes (/api/admin/trigger)', () => {
getState: vi.fn().mockResolvedValue('completed'),
retry: vi.fn(),
};
vi.mocked(flyerQueue.getJob).mockResolvedValue(mockJob as any);
vi.mocked(flyerQueue.getJob).mockResolvedValue(mockJob as unknown as Job);
const response = await supertest(app).post(`/api/admin/jobs/${queueName}/${jobId}/retry`);
@@ -239,7 +239,7 @@ describe('Admin Job Trigger Routes (/api/admin/trigger)', () => {
getState: vi.fn().mockResolvedValue('failed'),
retry: vi.fn().mockRejectedValue(new Error('Cannot retry job')),
};
vi.mocked(flyerQueue.getJob).mockResolvedValue(mockJob as any);
vi.mocked(flyerQueue.getJob).mockResolvedValue(mockJob as unknown as Job);
const response = await supertest(app).post(`/api/admin/jobs/${queueName}/${jobId}/retry`);

View File

@@ -1,5 +1,5 @@
// src/routes/admin.routes.ts
import { Router, Request, Response, NextFunction } from 'express';
import { Router, NextFunction } from 'express';
import passport from './passport.routes';
import { isAdmin } from './passport.routes'; // Correctly imported
import multer from 'multer';// --- Zod Schemas for Admin Routes (as per ADR-003) ---

View File

@@ -1,12 +1,5 @@
// --- FIX REGISTRY ---
//
// 2024-07-29: Updated `vi.mock` for `../services/db/index.db` to use `vi.hoisted` and `importOriginal`.
// This preserves named exports (like repository classes) from the original module,
// fixing 'undefined' errors when other modules tried to import them from the mock.
// --- END FIX REGISTRY ---
// src/routes/admin.stats.routes.test.ts
import { describe, it, expect, vi, beforeEach, type Mocked } from 'vitest';
import { describe, it, expect, vi, beforeEach } from 'vitest';
import supertest from 'supertest';
import express, { Request, Response, NextFunction } from 'express';
import adminRouter from './admin.routes';

View File

@@ -1,10 +1,9 @@
// src/routes/admin.system.routes.test.ts
import { describe, it, expect, vi, beforeEach, type Mocked } from 'vitest';
import { describe, it, expect, vi, beforeEach } from 'vitest';
import supertest from 'supertest';
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';
// Mock dependencies

View File

@@ -1,12 +1,5 @@
// --- FIX REGISTRY ---
//
// 2024-07-29: Updated `vi.mock` for `../services/db/index.db` to use `vi.hoisted` and `importOriginal`.
// This preserves named exports (like repository classes) from the original module,
// fixing 'undefined' errors when other modules tried to import them from the mock.
// --- END FIX REGISTRY ---
// src/routes/admin.users.routes.test.ts
import { describe, it, expect, vi, beforeEach, type Mocked } from 'vitest';
import { describe, it, expect, vi, beforeEach } from 'vitest';
import supertest from 'supertest';
import express, { Request, Response, NextFunction } from 'express';
import adminRouter from './admin.routes';

View File

@@ -3,6 +3,7 @@ import { describe, it, expect, vi, beforeEach } from 'vitest';
import supertest from 'supertest';
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';
@@ -72,7 +73,7 @@ describe('AI Routes (/api/ai)', () => {
it('should enqueue a job and return 202 on success', async () => {
vi.mocked(db.flyerRepo.findFlyerByChecksum).mockResolvedValue(undefined);
vi.mocked(flyerQueue.add).mockResolvedValue({ id: 'job-123' } as any);
vi.mocked(flyerQueue.add).mockResolvedValue({ id: 'job-123' } as unknown as Job);
const response = await supertest(app)
.post('/api/ai/upload-and-process')
@@ -187,7 +188,7 @@ describe('AI Routes (/api/ai)', () => {
it('should return job status if job is found', async () => {
const mockJob = { id: 'job-123', getState: async () => 'completed', progress: 100, returnvalue: { flyerId: 1 } };
vi.mocked(flyerQueue.getJob).mockResolvedValue(mockJob as any);
vi.mocked(flyerQueue.getJob).mockResolvedValue(mockJob as unknown as Job);
const response = await supertest(app).get('/api/ai/jobs/job-123/status');

View File

@@ -1,6 +1,5 @@
// src/routes/ai.ts
import { Router, Request, Response, NextFunction } from 'express';
import crypto from 'crypto';
import multer from 'multer';
import path from 'path';
import fs from 'fs';

View File

@@ -459,7 +459,7 @@ describe('Auth Routes (/api/auth)', () => {
});
it('should return 403 if refresh token is invalid', async () => {
vi.mocked(db.userRepo.findUserByRefreshToken).mockResolvedValue(undefined);
vi.mocked(db.userRepo.findUserByRefreshToken).mockRejectedValue(new Error('Invalid or expired refresh token.'));
const response = await supertest(app)
.post('/api/auth/refresh-token')

View File

@@ -99,7 +99,7 @@ router.post('/register', validateRequest(registerSchema), async (req, res, next)
logger.info(`Hashing password for new user: ${email}`);
// The createUser method in UserRepository now handles its own transaction.
const newUser = await userRepo.createUser(email, hashedPassword, { full_name, avatar_url });
const newUser = await userRepo.createUser(email, hashedPassword, { full_name, avatar_url }, req.log);
const userEmail = newUser.user.email || 'unknown';
const userId = newUser.user_id || 'unknown';
@@ -111,13 +111,13 @@ router.post('/register', validateRequest(registerSchema), async (req, res, next)
action: 'user_registered',
displayText: `${userEmail} has registered.`,
icon: 'user-plus',
});
}, req.log);
const payload = { user_id: newUser.user_id, email: userEmail };
const token = jwt.sign(payload, JWT_SECRET, { expiresIn: '1h' });
const refreshToken = crypto.randomBytes(64).toString('hex');
await userRepo.saveRefreshToken(newUser.user_id, refreshToken);
await userRepo.saveRefreshToken(newUser.user_id, refreshToken, req.log);
res.cookie('refreshToken', refreshToken, {
httpOnly: true,
@@ -131,7 +131,7 @@ router.post('/register', validateRequest(registerSchema), async (req, res, next)
return res.status(409).json({ message: error.message });
}
// The createUser method now handles its own transaction logging, so we just log the route failure.
logger.error(`User registration route failed for email: ${email}.`, { error });
logger.error({ error }, `User registration route failed for email: ${email}.`);
return next(error);
}
});
@@ -140,23 +140,23 @@ router.post('/register', validateRequest(registerSchema), async (req, res, next)
router.post('/login', (req: Request, res: Response, next: NextFunction) => {
passport.authenticate('local', { session: false }, async (err: Error, user: Express.User | false, info: { message: string }) => {
// --- LOGIN ROUTE DEBUG LOGGING ---
logger.debug(`[API /login] Received login request for email: ${req.body.email}`);
if (err) logger.error('[API /login] Passport reported an error.', { err });
if (!user) logger.warn('[API /login] Passport reported NO USER found.', { info });
if (user) logger.debug('[API /login] Passport user object:', { user }); // Log the user object passport returns
if (user) logger.info('[API /login] Passport reported USER FOUND.', { user });
req.log.debug(`[API /login] Received login request for email: ${req.body.email}`);
if (err) req.log.error({ err }, '[API /login] Passport reported an error.');
if (!user) req.log.warn({ info }, '[API /login] Passport reported NO USER found.');
if (user) req.log.debug({ user }, '[API /login] Passport user object:'); // Log the user object passport returns
if (user) req.log.info({ user }, '[API /login] Passport reported USER FOUND.');
try {
const allUsersInDb = await getPool().query('SELECT u.user_id, u.email, p.role FROM public.users u JOIN public.profiles p ON u.user_id = p.user_id');
logger.debug('[API /login] Current users in DB from SERVER perspective:');
req.log.debug('[API /login] Current users in DB from SERVER perspective:');
console.table(allUsersInDb.rows);
} catch (dbError) {
logger.error('[API /login] Could not query users table for debugging.', { dbError });
req.log.error({ dbError }, '[API /login] Could not query users table for debugging.');
}
// --- END DEBUG LOGGING ---
const { rememberMe } = req.body;
if (err) {
logger.error(`Login authentication error in /login route for email: ${req.body.email}`, { error: err });
req.log.error({ error: err }, `Login authentication error in /login route for email: ${req.body.email}`);
return next(err);
}
if (!user) {
@@ -169,8 +169,8 @@ router.post('/login', (req: Request, res: Response, next: NextFunction) => {
try {
const refreshToken = crypto.randomBytes(64).toString('hex');
await userRepo.saveRefreshToken(typedUser.user_id, refreshToken);
logger.info(`JWT and refresh token issued for user: ${typedUser.email}`);
await userRepo.saveRefreshToken(typedUser.user_id, refreshToken, req.log);
req.log.info(`JWT and refresh token issued for user: ${typedUser.email}`);
const cookieOptions = {
httpOnly: true,
@@ -183,7 +183,7 @@ router.post('/login', (req: Request, res: Response, next: NextFunction) => {
return res.json({ user: userResponse, token: accessToken });
} catch (tokenErr) {
logger.error(`Failed to save refresh token during login for user: ${typedUser.email}`, { error: tokenErr });
req.log.error({ error: tokenErr }, `Failed to save refresh token during login for user: ${typedUser.email}`);
return next(tokenErr);
}
})(req, res, next);
@@ -194,10 +194,10 @@ router.post('/forgot-password', forgotPasswordLimiter, validateRequest(forgotPas
const { email } = req.body;
try {
logger.debug(`[API /forgot-password] Received request for email: ${email}`);
const user = await userRepo.findUserByEmail(email);
req.log.debug(`[API /forgot-password] Received request for email: ${email}`);
const user = await userRepo.findUserByEmail(email, req.log);
let token: string | undefined;
logger.debug(`[API /forgot-password] Database search result for ${email}:`, { user: user ? { user_id: user.user_id, email: user.email } : 'NOT FOUND' });
req.log.debug({ user: user ? { user_id: user.user_id, email: user.email } : 'NOT FOUND' }, `[API /forgot-password] Database search result for ${email}:`);
if (user) {
token = crypto.randomBytes(32).toString('hex');
@@ -205,17 +205,17 @@ router.post('/forgot-password', forgotPasswordLimiter, validateRequest(forgotPas
const tokenHash = await bcrypt.hash(token, saltRounds);
const expiresAt = new Date(Date.now() + 3600000); // 1 hour
await userRepo.createPasswordResetToken(user.user_id, tokenHash, expiresAt);
await userRepo.createPasswordResetToken(user.user_id, tokenHash, expiresAt, req.log);
const resetLink = `${process.env.FRONTEND_URL}/reset-password/${token}`;
try {
await sendPasswordResetEmail(email, resetLink);
await sendPasswordResetEmail(email, resetLink, req.log);
} catch (emailError) {
logger.error(`Email send failure during password reset for user: ${emailError}`);
req.log.error({ emailError }, `Email send failure during password reset for user`);
}
} else {
logger.warn(`Password reset requested for non-existent email: ${email}`);
req.log.warn(`Password reset requested for non-existent email: ${email}`);
}
// For testability, return the token in the response only in the test environment.
@@ -225,7 +225,7 @@ router.post('/forgot-password', forgotPasswordLimiter, validateRequest(forgotPas
if (process.env.NODE_ENV === 'test' && user) responsePayload.token = token;
res.status(200).json(responsePayload);
} catch (error) {
logger.error(`An error occurred during /forgot-password for email: ${email}`, { error });
req.log.error({ error }, `An error occurred during /forgot-password for email: ${email}`);
next(error);
}
});
@@ -235,7 +235,7 @@ router.post('/reset-password', resetPasswordLimiter, validateRequest(resetPasswo
const { token, newPassword } = req.body;
try {
const validTokens = await userRepo.getValidResetTokens();
const validTokens = await userRepo.getValidResetTokens(req.log);
let tokenRecord;
for (const record of validTokens) {
const isMatch = await bcrypt.compare(token, record.token_hash);
@@ -252,8 +252,8 @@ router.post('/reset-password', resetPasswordLimiter, validateRequest(resetPasswo
const saltRounds = 10;
const hashedPassword = await bcrypt.hash(newPassword, saltRounds);
await userRepo.updateUserPassword(tokenRecord.user_id, hashedPassword);
await userRepo.deleteResetToken(tokenRecord.token_hash);
await userRepo.updateUserPassword(tokenRecord.user_id, hashedPassword, req.log);
await userRepo.deleteResetToken(tokenRecord.token_hash, req.log);
// Log this security event after a successful password reset.
await adminRepo.logActivity({
@@ -262,11 +262,11 @@ router.post('/reset-password', resetPasswordLimiter, validateRequest(resetPasswo
displayText: `User ID ${tokenRecord.user_id} has reset their password.`,
icon: 'key',
details: { source_ip: req.ip ?? null }
});
}, req.log);
res.status(200).json({ message: 'Password has been reset successfully.' });
} catch (error) {
logger.error(`An error occurred during password reset.`, { error });
req.log.error({ error }, `An error occurred during password reset.`);
next(error);
}
});
@@ -279,7 +279,7 @@ router.post('/refresh-token', async (req: Request, res: Response, next: NextFunc
}
try {
const user = await userRepo.findUserByRefreshToken(refreshToken);
const user = await userRepo.findUserByRefreshToken(refreshToken, req.log);
if (!user) {
return res.status(403).json({ message: 'Invalid or expired refresh token.' });
}
@@ -289,7 +289,7 @@ router.post('/refresh-token', async (req: Request, res: Response, next: NextFunc
res.json({ token: newAccessToken });
} catch (error) {
logger.error('An error occurred during /refresh-token.', { error });
req.log.error({ error }, 'An error occurred during /refresh-token.');
next(error);
}
});
@@ -304,8 +304,8 @@ router.post('/logout', async (req: Request, res: Response) => {
if (refreshToken) {
// Invalidate the token in the database so it cannot be used again.
// We don't need to wait for this to finish to respond to the user.
userRepo.deleteRefreshToken(refreshToken).catch((err: Error) => {
logger.error('Failed to delete refresh token from DB during logout.', { error: err });
userRepo.deleteRefreshToken(refreshToken, req.log).catch((err: Error) => {
req.log.error({ error: err }, 'Failed to delete refresh token from DB during logout.');
});
}
// Instruct the browser to clear the cookie by setting its expiration to the past.

View File

@@ -1,9 +1,8 @@
// src/routes/budget.ts
import express, { Request, Response, NextFunction } from 'express';
import express, { NextFunction } from 'express';
import { z } from 'zod';
import passport from './passport.routes';
import { budgetRepo } from '../services/db/index.db';
import { logger } from '../services/logger.server';
import type { UserProfile } from '../types';
import { validateRequest } from '../middleware/validation.middleware';

View File

@@ -1,7 +1,7 @@
// src/routes/flyer.routes.test.ts
import { describe, it, expect, vi, beforeEach } from 'vitest';
import supertest from 'supertest';
import express, { Request, Response, NextFunction } from 'express';
import express from 'express';
import { errorHandler } from '../middleware/errorHandler';
import flyerRouter from './flyer.routes';
import { createMockFlyer, createMockFlyerItem } from '../tests/utils/mockFactories';

View File

@@ -1,6 +1,5 @@
// src/routes/flyer.routes.ts
import { Router, Request, Response, NextFunction } from 'express';
import crypto from 'crypto';
import * as db from '../services/db/index.db';
import { z } from 'zod';
import { logger } from '../services/logger.server';
@@ -45,10 +44,10 @@ const trackItemSchema = z.object({
router.get('/', validateRequest(getFlyersSchema), async (req, res, next: NextFunction) => {
try {
const { limit, offset } = req.query as unknown as { limit: number; offset: number };
const flyers = await db.flyerRepo.getFlyers(limit, offset);
const flyers = await db.flyerRepo.getFlyers(req.log, limit, offset);
res.json(flyers);
} catch (error) {
logger.error('Error fetching flyers in /api/flyers:', { error });
req.log.error({ error }, 'Error fetching flyers in /api/flyers:');
next(error);
}
});
@@ -59,7 +58,7 @@ router.get('/', validateRequest(getFlyersSchema), async (req, res, next: NextFun
router.get('/:id', validateRequest(flyerIdParamSchema), async (req, res, next: NextFunction) => {
try {
const flyerId = req.params.id as unknown as number;
const flyer = await db.flyerRepo.getFlyerById(flyerId);
const flyer = await db.flyerRepo.getFlyerById(flyerId, req.log);
res.json(flyer);
} catch (error) {
next(error);
@@ -72,10 +71,10 @@ router.get('/:id', validateRequest(flyerIdParamSchema), async (req, res, next: N
router.get('/:id/items', validateRequest(flyerIdParamSchema), async (req, res, next: NextFunction) => {
try {
const flyerId = req.params.id as unknown as number;
const items = await db.flyerRepo.getFlyerItems(flyerId);
const items = await db.flyerRepo.getFlyerItems(flyerId, req.log);
res.json(items);
} catch (error) {
logger.error('Error fetching flyer items in /api/flyers/:id/items:', { error });
req.log.error({ error }, 'Error fetching flyer items in /api/flyers/:id/items:');
next(error);
}
});
@@ -86,7 +85,7 @@ router.get('/:id/items', validateRequest(flyerIdParamSchema), async (req, res, n
router.post('/items/batch-fetch', validateRequest(batchFetchSchema), async (req, res, next: NextFunction) => {
const { flyerIds } = req.body as { flyerIds: number[] };
try {
const items = await db.flyerRepo.getFlyerItemsForFlyers(flyerIds);
const items = await db.flyerRepo.getFlyerItemsForFlyers(flyerIds, req.log);
res.json(items);
} catch (error) {
next(error);
@@ -100,7 +99,7 @@ router.post('/items/batch-count', validateRequest(batchFetchSchema.partial()), a
const { flyerIds } = req.body as { flyerIds?: number[] };
try {
// The DB function handles an empty array, so we can simplify.
const count = await db.flyerRepo.countFlyerItemsForFlyers(flyerIds ?? []);
const count = await db.flyerRepo.countFlyerItemsForFlyers(flyerIds ?? [], req.log);
res.json({ count });
} catch (error) {
next(error);
@@ -113,7 +112,7 @@ router.post('/items/batch-count', validateRequest(batchFetchSchema.partial()), a
router.post('/items/:itemId/track', validateRequest(trackItemSchema), (req: Request, res: Response) => {
const itemId = req.params.itemId as unknown as number;
const { type } = req.body as { type: 'view' | 'click' };
db.flyerRepo.trackFlyerItemInteraction(itemId, type);
db.flyerRepo.trackFlyerItemInteraction(itemId, type, req.log);
res.status(202).send();
});

View File

@@ -1,7 +1,7 @@
// src/routes/health.routes.test.ts
import { describe, it, expect, vi, beforeEach, afterEach, type Mocked } from 'vitest';
import supertest from 'supertest';
import express, { Request, Response, NextFunction } from 'express';
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';

View File

@@ -252,7 +252,10 @@ describe('Passport Configuration', () => {
it('should call done(null, false) when user is not found', async () => {
// Arrange
const jwtPayload = { user_id: 'non-existent-user' };
vi.mocked(mockedDb.userRepo.findUserProfileById).mockResolvedValue(undefined);
// Per ADR-001, the repository method throws an error when the user is not found.
// The JWT strategy's catch block will then handle this and call done(err, false).
const notFoundError = new Error('User not found');
vi.mocked(mockedDb.userRepo.findUserProfileById).mockRejectedValue(notFoundError);
const done = vi.fn();
// Act
@@ -261,7 +264,8 @@ describe('Passport Configuration', () => {
}
// Assert
expect(done).toHaveBeenCalledWith(null, false);
// The strategy's catch block passes the error to done().
expect(done).toHaveBeenCalledWith(notFoundError, false);
});
it('should call done(err) if the database lookup fails', async () => {

View File

@@ -10,7 +10,6 @@ import { Request, Response, NextFunction } from 'express';
import * as db from '../services/db/index.db';
import { logger } from '../services/logger.server';
import { UserProfile } from '../types';
import { omit } from '../utils/objectUtils';
import { createMockUserProfile } from '../tests/utils/mockFactories';
const JWT_SECRET = process.env.JWT_SECRET!;
@@ -37,7 +36,7 @@ passport.use(new LocalStrategy(
async (req: Request, email, password, done) => {
try {
// 1. Find the user by email, including their profile data for the JWT payload.
const user = await db.userRepo.findUserWithProfileByEmail(email);
const user = await db.userRepo.findUserWithProfileByEmail(email, req.log);
if (!user) {
// User not found
@@ -73,7 +72,7 @@ passport.use(new LocalStrategy(
logger.warn(`Login attempt failed for user ${email} due to incorrect password.`);
// Increment failed attempts
await db.adminRepo.incrementFailedLoginAttempts(user.user_id);
await db.adminRepo.incrementFailedLoginAttempts(user.user_id, req.log);
// Log this security event.
await db.adminRepo.logActivity({
@@ -81,22 +80,22 @@ passport.use(new LocalStrategy(
action: 'login_failed_password',
displayText: `Failed login attempt for user ${user.email}.`,
icon: 'shield-alert',
details: { source_ip: req.ip ?? null }
});
details: { source_ip: req.ip ?? null },
}, req.log);
return done(null, false, { message: 'Incorrect email or password.' });
}
// 3. Success! Return the user object (without password_hash for security).
// Reset failed login attempts upon successful login.
await db.adminRepo.resetFailedLoginAttempts(user.user_id, req.ip ?? 'unknown');
await db.adminRepo.resetFailedLoginAttempts(user.user_id, req.ip ?? 'unknown', req.log);
logger.info(`User successfully authenticated: ${email}`);
// The user object from `findUserWithProfileByEmail` already excludes the password hash.
// This object will be passed to the /login route handler.
const userWithoutHash = user;
return done(null, userWithoutHash);
} catch (err) {
logger.error('Error during local authentication strategy:', { error: err });
} catch (err: unknown) {
req.log.error({ error: err }, 'Error during local authentication strategy:');
return done(err);
}
}
@@ -213,11 +212,11 @@ const jwtOptions = {
};
passport.use(new JwtStrategy(jwtOptions, async (jwt_payload, done) => {
logger.debug('[JWT Strategy] Verifying token payload:', { jwt_payload: jwt_payload ? { user_id: jwt_payload.user_id } : 'null' });
logger.debug({ jwt_payload: jwt_payload ? { user_id: jwt_payload.user_id } : 'null' }, '[JWT Strategy] Verifying token payload:');
try {
// The jwt_payload contains the data you put into the token during login (e.g., { user_id: user.user_id, email: user.email }).
// We re-fetch the user from the database here to ensure they are still active and valid.
const userProfile = await db.userRepo.findUserProfileById(jwt_payload.user_id);
const userProfile = await db.userRepo.findUserProfileById(jwt_payload.user_id, logger);
// --- JWT STRATEGY DEBUG LOGGING ---
logger.debug(`[JWT Strategy] DB lookup for user ID ${jwt_payload.user_id} result: ${userProfile ? 'FOUND' : 'NOT FOUND'}`);
@@ -228,8 +227,8 @@ passport.use(new JwtStrategy(jwtOptions, async (jwt_payload, done) => {
logger.warn(`JWT authentication failed: user with ID ${jwt_payload.user_id} not found.`);
return done(null, false); // User not found or invalid token
}
} catch (err) {
logger.error('Error during JWT authentication strategy:', { error: err });
} catch (err: unknown) {
logger.error({ error: err }, 'Error during JWT authentication strategy:');
return done(err, false);
}
}));
@@ -259,9 +258,9 @@ export const optionalAuth = (req: Request, res: Response, next: NextFunction) =>
// The custom callback for passport.authenticate gives us access to `err`, `user`, and `info`.
passport.authenticate('jwt', { session: false }, (err: Error | null, user: Express.User | false, info: { message: string } | Error) => {
// If there's an authentication error (e.g., malformed token), log it but don't block the request.
if (info) {
logger.info('Optional auth info:', { info: info.message || info.toString() });
}
if (info) { // The patch requested this specific error handling.
logger.info({ info: info.message || info.toString() }, 'Optional auth info:');
} // The patch requested this specific error handling.
if (user) (req as Express.Request).user = user; // Attach user if authentication succeeds
next(); // Always proceed to the next middleware

View File

@@ -1,7 +1,6 @@
// src/routes/personalization.routes.ts
import { Router, Request, Response, NextFunction } from 'express';
import * as db from '../services/db/index.db';
import { logger } from '../services/logger.server';
const router = Router();

View File

@@ -2,7 +2,6 @@
import { Router, type Request, type Response, type NextFunction } from 'express';
import { z } from 'zod';
import * as db from '../services/db/index.db';
import { logger } from '../services/logger.server';
import { validateRequest } from '../middleware/validation.middleware';
const router = Router();

View File

@@ -3,7 +3,7 @@ import { describe, it, expect, vi, beforeEach } from 'vitest';
import supertest from 'supertest';
import express from 'express';
import systemRouter from './system.routes';
import { exec } from 'child_process';
import { exec, type ExecException } from 'child_process';
import { geocodingService } from '../services/geocodingService.server';
import { errorHandler } from '../middleware/errorHandler';
@@ -60,11 +60,16 @@ describe('System Routes (/api/system)', () => {
└───────────┴───────────┘
`;
type ExecCallback = (error: ExecException | null, stdout: string, stderr: string) => void;
// Strict implementation that finds the callback (last argument)
vi.mocked(exec).mockImplementation((...args: any[]) => {
const callback = args.find(arg => typeof arg === 'function');
callback(null, pm2OnlineOutput, '');
return {} as any;
vi.mocked(exec).mockImplementation((command: string, ...args: any[]) => {
const callback = args.find(arg => typeof arg === 'function') as ExecCallback | undefined;
if (callback) {
callback(null, pm2OnlineOutput, '');
}
// Return a minimal object that satisfies the ChildProcess type for .unref()
return { unref: () => {} } as ReturnType<typeof exec>;
});
// Act
@@ -78,10 +83,12 @@ describe('System Routes (/api/system)', () => {
it('should return success: false when pm2 process is stopped or errored', async () => {
const pm2StoppedOutput = `│ status │ stopped │`;
vi.mocked(exec).mockImplementation((...args: any[]) => {
const callback = args.find(arg => typeof arg === 'function');
callback(null, pm2StoppedOutput, '');
return {} as any;
vi.mocked(exec).mockImplementation((command: string, ...args: any[]) => {
const callback = args.find(arg => typeof arg === 'function') as ((error: ExecException | null, stdout: string, stderr: string) => void) | undefined;
if (callback) {
callback(null, pm2StoppedOutput, '');
}
return { unref: () => {} } as ReturnType<typeof exec>;
});
const response = await supertest(app).get('/api/system/pm2-status');
@@ -92,11 +99,12 @@ describe('System Routes (/api/system)', () => {
});
it('should return 500 on a generic exec error', async () => {
vi.mocked(exec).mockImplementation((...args: any[]) => {
const callback = args.find(arg => typeof arg === 'function');
// Generic system error (not PM2 specific)
callback(new Error('System error'), '', 'stderr output');
return {} as any;
vi.mocked(exec).mockImplementation((command: string, ...args: any[]) => {
const callback = args.find(arg => typeof arg === 'function') as ((error: ExecException | null, stdout: string, stderr: string) => void) | undefined;
if (callback) {
callback(new Error('System error') as ExecException, '', 'stderr output');
}
return { unref: () => {} } as ReturnType<typeof exec>;
});
// Act