consolidate some testing functions
This commit is contained in:
@@ -15,6 +15,11 @@ import { NotFoundError } from '../services/db/errors.db'; // This can stay, it's
|
||||
import { createTestApp } from '../tests/utils/createTestApp';
|
||||
import { mockLogger } from '../tests/utils/mockLogger';
|
||||
|
||||
// Mock the file upload middleware to allow testing the controller's internal check
|
||||
vi.mock('../middleware/fileUpload.middleware', () => ({
|
||||
requireFileUpload: () => (req: Request, res: Response, next: NextFunction) => next(),
|
||||
}));
|
||||
|
||||
vi.mock('../lib/queue', () => ({
|
||||
serverAdapter: {
|
||||
getRouter: () => (req: Request, res: Response, next: NextFunction) => next(), // Return a dummy express handler
|
||||
@@ -256,7 +261,7 @@ describe('Admin Content Management Routes (/api/admin)', () => {
|
||||
const response = await supertest(app).post('/api/admin/brands/55/logo');
|
||||
expect(response.status).toBe(400);
|
||||
expect(response.body.message).toMatch(
|
||||
/Logo image file is required|The request data is invalid/,
|
||||
/Logo image file is required|The request data is invalid|Logo image file is missing./,
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -242,6 +242,17 @@ describe('Admin Job Trigger Routes (/api/admin/trigger)', () => {
|
||||
expect(response.status).toBe(400);
|
||||
});
|
||||
|
||||
it('should return 404 if the queue name is valid but not in the retry map', async () => {
|
||||
const queueName = 'weekly-analytics-reporting'; // This is in the Zod enum but not the queueMap
|
||||
const jobId = 'some-job-id';
|
||||
|
||||
const response = await supertest(app).post(`/api/admin/jobs/${queueName}/${jobId}/retry`);
|
||||
|
||||
// The route throws a NotFoundError, which the error handler should convert to a 404.
|
||||
expect(response.status).toBe(404);
|
||||
expect(response.body.message).toBe(`Queue 'weekly-analytics-reporting' not found.`);
|
||||
});
|
||||
|
||||
it('should return 404 if the job ID is not found in the queue', async () => {
|
||||
vi.mocked(flyerQueue.getJob).mockResolvedValue(undefined);
|
||||
const response = await supertest(app).post(
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { Router, NextFunction, Request, Response } 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) ---
|
||||
import multer from 'multer';
|
||||
import { z } from 'zod';
|
||||
|
||||
import * as db from '../services/db/index.db';
|
||||
@@ -32,7 +32,12 @@ import {
|
||||
weeklyAnalyticsWorker,
|
||||
} from '../services/queueService.server'; // Import your queues
|
||||
import { getSimpleWeekAndYear } from '../utils/dateUtils';
|
||||
import { requiredString, numericIdParam, uuidParamSchema } from '../utils/zodUtils';
|
||||
import {
|
||||
requiredString,
|
||||
numericIdParam,
|
||||
uuidParamSchema,
|
||||
optionalNumeric,
|
||||
} from '../utils/zodUtils';
|
||||
import { logger } from '../services/logger.server';
|
||||
|
||||
const updateCorrectionSchema = numericIdParam('id').extend({
|
||||
@@ -61,8 +66,8 @@ const updateUserRoleSchema = uuidParamSchema('id', 'A valid user ID is required.
|
||||
|
||||
const activityLogSchema = z.object({
|
||||
query: z.object({
|
||||
limit: z.coerce.number().int().positive().optional().default(50),
|
||||
offset: z.coerce.number().int().nonnegative().optional().default(0),
|
||||
limit: optionalNumeric({ default: 50, integer: true, positive: true }),
|
||||
offset: optionalNumeric({ default: 0, integer: true, nonnegative: true }),
|
||||
}),
|
||||
});
|
||||
|
||||
|
||||
@@ -79,12 +79,6 @@ describe('Admin System Routes (/api/admin/system)', () => {
|
||||
authenticatedUser: adminUser,
|
||||
});
|
||||
|
||||
// Add a basic error handler to capture errors passed to next(err) and return JSON.
|
||||
// This prevents unhandled error crashes in tests and ensures we get the 500 response we expect.
|
||||
app.use((err: any, req: any, res: any, next: any) => {
|
||||
res.status(err.status || 500).json({ message: err.message, errors: err.errors });
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
@@ -78,6 +78,7 @@ describe('AI Routes (/api/ai)', () => {
|
||||
vi.mocked(mockLogger.info).mockImplementation(() => {});
|
||||
vi.mocked(mockLogger.error).mockImplementation(() => {});
|
||||
vi.mocked(mockLogger.warn).mockImplementation(() => {});
|
||||
vi.mocked(mockLogger.debug).mockImplementation(() => {}); // Ensure debug is also mocked
|
||||
});
|
||||
const app = createTestApp({ router: aiRouter, basePath: '/api/ai' });
|
||||
|
||||
@@ -111,10 +112,55 @@ describe('AI Routes (/api/ai)', () => {
|
||||
});
|
||||
});
|
||||
|
||||
// Add a basic error handler to capture errors passed to next(err) and return JSON.
|
||||
// This prevents unhandled error crashes in tests and ensures we get the 500 response we expect.
|
||||
app.use((err: any, req: any, res: any, next: any) => {
|
||||
res.status(err.status || 500).json({ message: err.message, errors: err.errors });
|
||||
// New test to cover the router.use diagnostic middleware's catch block and errMsg branches
|
||||
describe('Diagnostic Middleware Error Handling', () => {
|
||||
it('should log an error if logger.debug throws an object with a message property', async () => {
|
||||
const mockErrorObject = { message: 'Mock debug error' };
|
||||
vi.mocked(mockLogger.debug).mockImplementationOnce(() => {
|
||||
throw mockErrorObject;
|
||||
});
|
||||
|
||||
// Make any request to trigger the middleware
|
||||
const response = await supertest(app).get('/api/ai/jobs/job-123/status');
|
||||
|
||||
expect(mockLogger.error).toHaveBeenCalledWith(
|
||||
{ error: mockErrorObject.message }, // errMsg should extract the message
|
||||
'Failed to log incoming AI request headers',
|
||||
);
|
||||
// The request should still proceed, but might fail later if the original flow was interrupted.
|
||||
// Here, it will likely hit the 404 for job not found.
|
||||
expect(response.status).toBe(404);
|
||||
});
|
||||
|
||||
it('should log an error if logger.debug throws a primitive string', async () => {
|
||||
const mockErrorString = 'Mock debug error string';
|
||||
vi.mocked(mockLogger.debug).mockImplementationOnce(() => {
|
||||
throw mockErrorString;
|
||||
});
|
||||
|
||||
// Make any request to trigger the middleware
|
||||
const response = await supertest(app).get('/api/ai/jobs/job-123/status');
|
||||
|
||||
expect(mockLogger.error).toHaveBeenCalledWith(
|
||||
{ error: mockErrorString }, // errMsg should convert to string
|
||||
'Failed to log incoming AI request headers',
|
||||
);
|
||||
expect(response.status).toBe(404);
|
||||
});
|
||||
|
||||
it('should log an error if logger.debug throws null/undefined', async () => {
|
||||
vi.mocked(mockLogger.debug).mockImplementationOnce(() => {
|
||||
throw null; // Simulate throwing null
|
||||
});
|
||||
|
||||
const response = await supertest(app).get('/api/ai/jobs/job-123/status');
|
||||
|
||||
expect(mockLogger.error).toHaveBeenCalledWith(
|
||||
{ error: 'An unknown error occurred.' }, // errMsg should handle null/undefined
|
||||
'Failed to log incoming AI request headers',
|
||||
);
|
||||
expect(response.status).toBe(404);
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /upload-and-process', () => {
|
||||
@@ -423,6 +469,52 @@ describe('AI Routes (/api/ai)', () => {
|
||||
expect(mockedDb.createFlyerAndItems).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should handle payload where extractedData is null', async () => {
|
||||
const payloadWithNullExtractedData = {
|
||||
checksum: 'null-extracted-data-checksum',
|
||||
originalFileName: 'flyer-null.jpg',
|
||||
extractedData: null,
|
||||
};
|
||||
|
||||
const response = await supertest(app)
|
||||
.post('/api/ai/flyers/process')
|
||||
.field('data', JSON.stringify(payloadWithNullExtractedData))
|
||||
.attach('flyerImage', imagePath);
|
||||
|
||||
expect(response.status).toBe(201);
|
||||
expect(mockedDb.createFlyerAndItems).toHaveBeenCalledTimes(1);
|
||||
// Verify that extractedData was correctly defaulted to an empty object
|
||||
const flyerDataArg = vi.mocked(mockedDb.createFlyerAndItems).mock.calls[0][0];
|
||||
expect(flyerDataArg.store_name).toContain('Unknown Store'); // Fallback should be used
|
||||
expect(mockLogger.warn).toHaveBeenCalledWith(
|
||||
{ bodyData: expect.any(Object) },
|
||||
'Missing extractedData in /api/ai/flyers/process payload.',
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle payload where extractedData is a string', async () => {
|
||||
const payloadWithStringExtractedData = {
|
||||
checksum: 'string-extracted-data-checksum',
|
||||
originalFileName: 'flyer-string.jpg',
|
||||
extractedData: 'not-an-object',
|
||||
};
|
||||
|
||||
const response = await supertest(app)
|
||||
.post('/api/ai/flyers/process')
|
||||
.field('data', JSON.stringify(payloadWithStringExtractedData))
|
||||
.attach('flyerImage', imagePath);
|
||||
|
||||
expect(response.status).toBe(201);
|
||||
expect(mockedDb.createFlyerAndItems).toHaveBeenCalledTimes(1);
|
||||
// Verify that extractedData was correctly defaulted to an empty object
|
||||
const flyerDataArg = vi.mocked(mockedDb.createFlyerAndItems).mock.calls[0][0];
|
||||
expect(flyerDataArg.store_name).toContain('Unknown Store'); // Fallback should be used
|
||||
expect(mockLogger.warn).toHaveBeenCalledWith(
|
||||
{ bodyData: expect.any(Object) },
|
||||
'Missing extractedData in /api/ai/flyers/process payload.',
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle payload where extractedData is at the root of the body', async () => {
|
||||
// This simulates a client sending multipart fields for each property of extractedData
|
||||
const response = await supertest(app)
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
// src/routes/auth.routes.ts
|
||||
import { Router, Request, Response, NextFunction } from 'express';
|
||||
import * as bcrypt from 'bcrypt';
|
||||
import zxcvbn from 'zxcvbn';
|
||||
import { z } from 'zod';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import crypto from 'crypto';
|
||||
@@ -44,8 +43,6 @@ const resetPasswordLimiter = rateLimit({
|
||||
skip: () => isTestEnv, // Skip this middleware if in test environment
|
||||
});
|
||||
|
||||
// --- Zod Schemas for Auth Routes (as per ADR-003) ---
|
||||
|
||||
const registerSchema = z.object({
|
||||
body: z.object({
|
||||
email: z.string().email('A valid email is required.'),
|
||||
|
||||
@@ -54,13 +54,6 @@ describe('Deals Routes (/api/users/deals)', () => {
|
||||
authenticatedUser: mockUser,
|
||||
});
|
||||
const unauthenticatedApp = createTestApp({ router: dealsRouter, basePath });
|
||||
const errorHandler = (err: any, req: any, res: any, next: any) => {
|
||||
res.status(err.status || 500).json({ message: err.message, errors: err.errors });
|
||||
};
|
||||
|
||||
// Apply the handler to both app instances
|
||||
authenticatedApp.use(errorHandler);
|
||||
unauthenticatedApp.use(errorHandler);
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Router } from 'express';
|
||||
import * as db from '../services/db/index.db';
|
||||
import { z } from 'zod';
|
||||
import { validateRequest } from '../middleware/validation.middleware';
|
||||
import { optionalNumeric } from '../utils/zodUtils';
|
||||
|
||||
const router = Router();
|
||||
|
||||
@@ -10,8 +11,8 @@ const router = Router();
|
||||
|
||||
const getFlyersSchema = z.object({
|
||||
query: z.object({
|
||||
limit: z.coerce.number().int().positive().optional().default(20),
|
||||
offset: z.coerce.number().int().nonnegative().optional().default(0),
|
||||
limit: optionalNumeric({ default: 20, integer: true, positive: true }),
|
||||
offset: optionalNumeric({ default: 0, integer: true, nonnegative: true }),
|
||||
}),
|
||||
});
|
||||
|
||||
|
||||
@@ -86,12 +86,6 @@ describe('Gamification Routes (/api/achievements)', () => {
|
||||
basePath,
|
||||
authenticatedUser: mockAdminProfile,
|
||||
});
|
||||
const errorHandler = (err: any, req: any, res: any, next: any) => {
|
||||
res.status(err.status || 500).json({ message: err.message, errors: err.errors });
|
||||
};
|
||||
unauthenticatedApp.use(errorHandler);
|
||||
authenticatedApp.use(errorHandler);
|
||||
adminApp.use(errorHandler);
|
||||
|
||||
describe('GET /', () => {
|
||||
it('should return a list of all achievements (public endpoint)', async () => {
|
||||
|
||||
@@ -7,7 +7,7 @@ import { logger } from '../services/logger.server';
|
||||
import { UserProfile } from '../types';
|
||||
import { ForeignKeyConstraintError } from '../services/db/errors.db';
|
||||
import { validateRequest } from '../middleware/validation.middleware';
|
||||
import { requiredString } from '../utils/zodUtils';
|
||||
import { requiredString, optionalNumeric } from '../utils/zodUtils';
|
||||
|
||||
const router = express.Router();
|
||||
const adminGamificationRouter = express.Router(); // Create a new router for admin-only routes.
|
||||
@@ -16,7 +16,7 @@ const adminGamificationRouter = express.Router(); // Create a new router for adm
|
||||
|
||||
const leaderboardSchema = z.object({
|
||||
query: z.object({
|
||||
limit: z.coerce.number().int().positive().max(50).optional().default(10),
|
||||
limit: optionalNumeric({ default: 10, integer: true, positive: true, max: 50 }),
|
||||
}),
|
||||
});
|
||||
|
||||
|
||||
@@ -22,10 +22,6 @@ import { priceRepo } from '../services/db/price.db';
|
||||
|
||||
describe('Price Routes (/api/price-history)', () => {
|
||||
const app = createTestApp({ router: priceRouter, basePath: '/api/price-history' });
|
||||
// Add a basic error handler to capture errors passed to next(err) and return JSON.
|
||||
app.use((err: any, req: any, res: any, next: any) => {
|
||||
res.status(err.status || 500).json({ message: err.message, errors: err.errors });
|
||||
});
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
@@ -100,7 +96,7 @@ describe('Price Routes (/api/price-history)', () => {
|
||||
.send({ masterItemIds: 'not-an-array' });
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
expect(response.body.errors[0].message).toBe('Expected array, received string');
|
||||
expect(response.body.errors[0].message).toContain('Expected array, received string');
|
||||
});
|
||||
|
||||
it('should return 400 if masterItemIds contains non-positive integers', async () => {
|
||||
@@ -127,7 +123,7 @@ describe('Price Routes (/api/price-history)', () => {
|
||||
expect(response.status).toBe(400);
|
||||
expect(response.body.errors).toHaveLength(2);
|
||||
expect(response.body.errors[0].message).toBe('Number must be greater than 0');
|
||||
expect(response.body.errors[1].message).toContain('Expected number, received string');
|
||||
expect(response.body.errors[1].message).toBe('Expected number, received string');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,16 +3,19 @@ import { Router, Request, Response, NextFunction } from 'express';
|
||||
import { z } from 'zod';
|
||||
import { validateRequest } from '../middleware/validation.middleware';
|
||||
import { priceRepo } from '../services/db/price.db';
|
||||
import { optionalNumeric } from '../utils/zodUtils';
|
||||
|
||||
const router = Router();
|
||||
|
||||
const priceHistorySchema = z.object({
|
||||
body: z.object({
|
||||
masterItemIds: z.array(z.number().int().positive()).nonempty({
|
||||
message: 'masterItemIds must be a non-empty array of positive integers.',
|
||||
}),
|
||||
limit: z.coerce.number().int().positive().optional().default(1000),
|
||||
offset: z.coerce.number().int().nonnegative().optional().default(0),
|
||||
masterItemIds: z
|
||||
.array(z.number().int().positive('Number must be greater than 0'))
|
||||
.nonempty({
|
||||
message: 'masterItemIds must be a non-empty array of positive integers.',
|
||||
}),
|
||||
limit: optionalNumeric({ default: 1000, integer: true, positive: true }),
|
||||
offset: optionalNumeric({ default: 0, integer: true, nonnegative: true }),
|
||||
}),
|
||||
});
|
||||
|
||||
|
||||
@@ -3,21 +3,19 @@ import { Router } from 'express';
|
||||
import { z } from 'zod';
|
||||
import * as db from '../services/db/index.db';
|
||||
import { validateRequest } from '../middleware/validation.middleware';
|
||||
import { requiredString, numericIdParam } from '../utils/zodUtils';
|
||||
import { requiredString, numericIdParam, optionalNumeric } from '../utils/zodUtils';
|
||||
|
||||
const router = Router();
|
||||
|
||||
// --- Zod Schemas for Recipe Routes (as per ADR-003) ---
|
||||
|
||||
const bySalePercentageSchema = z.object({
|
||||
query: z.object({
|
||||
minPercentage: z.coerce.number().min(0).max(100).optional().default(50),
|
||||
minPercentage: optionalNumeric({ default: 50, min: 0, max: 100 }),
|
||||
}),
|
||||
});
|
||||
|
||||
const bySaleIngredientsSchema = z.object({
|
||||
query: z.object({
|
||||
minIngredients: z.coerce.number().int().positive().optional().default(3),
|
||||
minIngredients: optionalNumeric({ default: 3, integer: true, positive: true }),
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -40,7 +38,7 @@ router.get(
|
||||
try {
|
||||
// Explicitly parse req.query to apply coercion (string -> number) and default values
|
||||
const { query } = bySalePercentageSchema.parse({ query: req.query });
|
||||
const recipes = await db.recipeRepo.getRecipesBySalePercentage(query.minPercentage, req.log);
|
||||
const recipes = await db.recipeRepo.getRecipesBySalePercentage(query.minPercentage!, req.log);
|
||||
res.json(recipes);
|
||||
} catch (error) {
|
||||
req.log.error({ error }, 'Error fetching recipes in /api/recipes/by-sale-percentage:');
|
||||
@@ -60,7 +58,7 @@ router.get(
|
||||
// Explicitly parse req.query to apply coercion (string -> number) and default values
|
||||
const { query } = bySaleIngredientsSchema.parse({ query: req.query });
|
||||
const recipes = await db.recipeRepo.getRecipesByMinSaleIngredients(
|
||||
query.minIngredients,
|
||||
query.minIngredients!,
|
||||
req.log,
|
||||
);
|
||||
res.json(recipes);
|
||||
|
||||
@@ -28,12 +28,6 @@ const expectLogger = expect.objectContaining({
|
||||
describe('Stats Routes (/api/stats)', () => {
|
||||
const app = createTestApp({ router: statsRouter, basePath: '/api/stats' });
|
||||
|
||||
// Add a basic error handler to capture errors passed to next(err) and return JSON.
|
||||
// This prevents unhandled error crashes in tests and ensures we get the 500 response we expect.
|
||||
app.use((err: any, req: any, res: any, next: any) => {
|
||||
res.status(err.status || 500).json({ message: err.message, errors: err.errors });
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Router, Request, Response, NextFunction } from 'express';
|
||||
import { z } from 'zod';
|
||||
import * as db from '../services/db/index.db';
|
||||
import { validateRequest } from '../middleware/validation.middleware';
|
||||
import { optionalNumeric } from '../utils/zodUtils';
|
||||
|
||||
const router = Router();
|
||||
|
||||
@@ -10,8 +11,8 @@ const router = Router();
|
||||
|
||||
// Define the query schema separately so we can use it to parse req.query in the handler
|
||||
const statsQuerySchema = z.object({
|
||||
days: z.coerce.number().int().min(1).max(365).optional().default(30),
|
||||
limit: z.coerce.number().int().min(1).max(50).optional().default(10),
|
||||
days: optionalNumeric({ default: 30, min: 1, max: 365, integer: true }),
|
||||
limit: optionalNumeric({ default: 10, min: 1, max: 50, integer: true }),
|
||||
});
|
||||
|
||||
const mostFrequentSalesSchema = z.object({
|
||||
@@ -31,7 +32,7 @@ router.get(
|
||||
// Even though validateRequest checks validity, it may not mutate req.query with the parsed result.
|
||||
const { days, limit } = statsQuerySchema.parse(req.query);
|
||||
|
||||
const items = await db.adminRepo.getMostFrequentSaleItems(days, limit, req.log);
|
||||
const items = await db.adminRepo.getMostFrequentSaleItems(days!, limit!, req.log);
|
||||
res.json(items);
|
||||
} catch (error) {
|
||||
req.log.error(
|
||||
|
||||
@@ -877,20 +877,41 @@ describe('User Routes (/api/users)', () => {
|
||||
});
|
||||
|
||||
describe('Notification Routes', () => {
|
||||
it('GET /notifications should return notifications for the user', async () => {
|
||||
it('GET /notifications should return only unread notifications by default', async () => {
|
||||
const mockNotifications: Notification[] = [
|
||||
createMockNotification({ user_id: 'user-123', content: 'Test' }),
|
||||
];
|
||||
vi.mocked(db.notificationRepo.getNotificationsForUser).mockResolvedValue(mockNotifications);
|
||||
|
||||
const response = await supertest(app).get('/api/users/notifications?limit=10&offset=0');
|
||||
const response = await supertest(app).get('/api/users/notifications?limit=10');
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body).toEqual(mockNotifications);
|
||||
expect(db.notificationRepo.getNotificationsForUser).toHaveBeenCalledWith(
|
||||
'user-123',
|
||||
10,
|
||||
0,
|
||||
0, // default offset
|
||||
false, // default includeRead
|
||||
expectLogger,
|
||||
);
|
||||
});
|
||||
|
||||
it('GET /notifications?includeRead=true should return all notifications', async () => {
|
||||
const mockNotifications: Notification[] = [
|
||||
createMockNotification({ user_id: 'user-123', content: 'Read', is_read: true }),
|
||||
createMockNotification({ user_id: 'user-123', content: 'Unread', is_read: false }),
|
||||
];
|
||||
vi.mocked(db.notificationRepo.getNotificationsForUser).mockResolvedValue(mockNotifications);
|
||||
|
||||
const response = await supertest(app).get('/api/users/notifications?includeRead=true');
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body).toEqual(mockNotifications);
|
||||
expect(db.notificationRepo.getNotificationsForUser).toHaveBeenCalledWith(
|
||||
'user-123',
|
||||
20, // default limit
|
||||
0, // default offset
|
||||
true, // includeRead from query param
|
||||
expectLogger,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -4,8 +4,7 @@ import passport from './passport.routes';
|
||||
import multer from 'multer';
|
||||
import path from 'path';
|
||||
import fs from 'node:fs/promises';
|
||||
import * as bcrypt from 'bcrypt';
|
||||
import zxcvbn from 'zxcvbn';
|
||||
import * as bcrypt from 'bcrypt'; // This was a duplicate, fixed.
|
||||
import { z } from 'zod';
|
||||
import { logger } from '../services/logger.server';
|
||||
import { UserProfile } from '../types';
|
||||
@@ -13,7 +12,12 @@ import { userService } from '../services/userService';
|
||||
import { ForeignKeyConstraintError } from '../services/db/errors.db';
|
||||
import { validateRequest } from '../middleware/validation.middleware';
|
||||
import { validatePasswordStrength } from '../utils/authUtils';
|
||||
import { requiredString, numericIdParam } from '../utils/zodUtils';
|
||||
import {
|
||||
requiredString,
|
||||
numericIdParam,
|
||||
optionalNumeric,
|
||||
optionalBoolean,
|
||||
} from '../utils/zodUtils';
|
||||
import * as db from '../services/db/index.db';
|
||||
|
||||
const router = express.Router();
|
||||
@@ -56,8 +60,9 @@ const createShoppingListSchema = z.object({
|
||||
// Apply the JWT authentication middleware to all routes in this file.
|
||||
const notificationQuerySchema = z.object({
|
||||
query: z.object({
|
||||
limit: z.coerce.number().int().positive().optional().default(20),
|
||||
offset: z.coerce.number().int().nonnegative().optional().default(0),
|
||||
limit: optionalNumeric({ default: 20, integer: true, positive: true }),
|
||||
offset: optionalNumeric({ default: 0, integer: true, nonnegative: true }),
|
||||
includeRead: optionalBoolean({ default: false }),
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -136,13 +141,12 @@ router.get(
|
||||
// Apply ADR-003 pattern for type safety
|
||||
try {
|
||||
const { query } = req as unknown as GetNotificationsRequest;
|
||||
// Explicitly convert to numbers to ensure the repo receives correct types
|
||||
const limit = query.limit ? Number(query.limit) : 20;
|
||||
const offset = query.offset ? Number(query.offset) : 0;
|
||||
const parsedQuery = notificationQuerySchema.parse({ query: req.query }).query;
|
||||
const notifications = await db.notificationRepo.getNotificationsForUser(
|
||||
userProfile.user.user_id,
|
||||
limit,
|
||||
offset,
|
||||
parsedQuery.limit!,
|
||||
parsedQuery.offset!,
|
||||
parsedQuery.includeRead!,
|
||||
req.log,
|
||||
);
|
||||
res.json(notifications);
|
||||
|
||||
Reference in New Issue
Block a user