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
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:
@@ -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';
|
||||
|
||||
@@ -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`);
|
||||
|
||||
|
||||
@@ -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) ---
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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');
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user