Refactor: Introduce requiredString helper for consistent validation across routes and services
All checks were successful
Deploy to Test Environment / deploy-to-test (push) Successful in 8m21s
All checks were successful
Deploy to Test Environment / deploy-to-test (push) Successful in 8m21s
This commit is contained in:
@@ -23,6 +23,9 @@ import { backgroundJobService } from '../services/backgroundJobService';
|
||||
import { flyerQueue, emailQueue, analyticsQueue, cleanupQueue, weeklyAnalyticsQueue, flyerWorker, emailWorker, analyticsWorker, cleanupWorker, weeklyAnalyticsWorker } from '../services/queueService.server'; // Import your queues
|
||||
import { getSimpleWeekAndYear } from '../utils/dateUtils';
|
||||
|
||||
// Helper for consistent required string validation (handles missing/null/empty)
|
||||
const requiredString = (message: string) => z.preprocess((val) => val ?? '', z.string().min(1, message));
|
||||
|
||||
/**
|
||||
* A factory for creating a Zod schema that validates a UUID in the request parameters.
|
||||
* @param key The name of the parameter key (e.g., 'userId').
|
||||
@@ -41,7 +44,7 @@ const numericIdParamSchema = (key: string, message = `Invalid ID for parameter '
|
||||
|
||||
const updateCorrectionSchema = numericIdParamSchema('id').extend({
|
||||
body: z.object({
|
||||
suggested_value: z.string().min(1, 'A new suggested_value is required.'),
|
||||
suggested_value: requiredString('A new suggested_value is required.'),
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -73,7 +76,7 @@ const activityLogSchema = z.object({
|
||||
const jobRetrySchema = z.object({
|
||||
params: z.object({
|
||||
queueName: z.enum(['flyer-processing', 'email-sending', 'analytics-reporting', 'file-cleanup', 'weekly-analytics-reporting']),
|
||||
jobId: z.string().min(1, 'A valid Job ID is required.'),
|
||||
jobId: requiredString('A valid Job ID is required.'),
|
||||
}),
|
||||
});
|
||||
|
||||
|
||||
@@ -15,7 +15,6 @@ import { logger } from '../services/logger.server';
|
||||
import { UserProfile, ExtractedCoreData } from '../types';
|
||||
import { flyerQueue } from '../services/queueService.server';
|
||||
import { validateRequest } from '../middleware/validation.middleware';
|
||||
import { NotFoundError } from '../services/db/errors.db';
|
||||
|
||||
const router = Router();
|
||||
|
||||
@@ -27,18 +26,18 @@ interface FlyerProcessPayload extends Partial<ExtractedCoreData> {
|
||||
}
|
||||
|
||||
// --- Zod Schemas for AI Routes (as per ADR-003) ---
|
||||
// Helper for consistent required string validation (handles missing/null/empty)
|
||||
const requiredString = (message: string) => z.preprocess((val) => val ?? '', z.string().min(1, message));
|
||||
|
||||
const uploadAndProcessSchema = z.object({
|
||||
body: z.object({
|
||||
checksum: z.string().refine(val => val && val.length > 0, {
|
||||
message: 'File checksum is required.',
|
||||
}),
|
||||
checksum: requiredString('File checksum is required.'),
|
||||
}),
|
||||
});
|
||||
|
||||
const jobIdParamSchema = z.object({
|
||||
params: z.object({
|
||||
jobId: z.string().min(1, 'A valid Job ID is required.'),
|
||||
jobId: requiredString('A valid Job ID is required.'),
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -51,9 +50,7 @@ const errMsg = (e: unknown) => {
|
||||
|
||||
const rescanAreaSchema = z.object({
|
||||
body: z.object({
|
||||
cropArea: z.string().refine(val => val && val.length > 0, {
|
||||
message: 'cropArea must be a valid JSON string.',
|
||||
}).transform((val, ctx) => {
|
||||
cropArea: requiredString('cropArea must be a valid JSON string.').transform((val, ctx) => {
|
||||
try { return JSON.parse(val); }
|
||||
catch (err) {
|
||||
// Log the actual parsing error for better debugging if invalid JSON is sent.
|
||||
@@ -61,9 +58,7 @@ const rescanAreaSchema = z.object({
|
||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: 'cropArea must be a valid JSON string.' }); return z.NEVER;
|
||||
}
|
||||
}),
|
||||
extractionType: z.string().refine(val => val && val.length > 0, {
|
||||
message: 'extractionType is required.',
|
||||
}),
|
||||
extractionType: requiredString('extractionType is required.'),
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -91,15 +86,15 @@ const planTripSchema = z.object({
|
||||
});
|
||||
|
||||
const generateImageSchema = z.object({
|
||||
body: z.object({ prompt: z.string().min(1) }),
|
||||
body: z.object({ prompt: requiredString('A prompt is required.') }),
|
||||
});
|
||||
|
||||
const generateSpeechSchema = z.object({
|
||||
body: z.object({ text: z.string().min(1) }),
|
||||
body: z.object({ text: requiredString('Text is required.') }),
|
||||
});
|
||||
|
||||
const searchWebSchema = z.object({
|
||||
body: z.object({ query: z.string().min(1, 'A search query is required.') }),
|
||||
body: z.object({ query: requiredString('A search query is required.') }),
|
||||
});
|
||||
|
||||
// --- Multer Configuration for File Uploads ---
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
// src/routes/auth.routes.test.ts
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import supertest from 'supertest';
|
||||
import { Request, Response, NextFunction, RequestHandler } from 'express';
|
||||
import { Request, Response, NextFunction } from 'express';
|
||||
import cookieParser from 'cookie-parser';
|
||||
import * as bcrypt from 'bcrypt';
|
||||
import { createMockUserProfile, createMockUserWithPasswordHash } 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(() => {
|
||||
@@ -199,7 +198,10 @@ describe('Auth Routes (/api/auth)', () => {
|
||||
expect(response.status).toBe(400);
|
||||
// The validation middleware returns errors in an array.
|
||||
// We check if any of the error messages contain the expected text.
|
||||
const errorMessages = response.body.errors?.map((e: any) => e.message).join(' ');
|
||||
interface ZodError {
|
||||
message: string;
|
||||
}
|
||||
const errorMessages = response.body.errors?.map((e: ZodError) => e.message).join(' ');
|
||||
expect(errorMessages).toMatch(/Password is too weak/i);
|
||||
});
|
||||
|
||||
@@ -468,11 +470,12 @@ describe('Auth Routes (/api/auth)', () => {
|
||||
});
|
||||
|
||||
it('should return 403 if refresh token is invalid', async () => {
|
||||
vi.mocked(db.userRepo.findUserByRefreshToken).mockRejectedValue(new Error('Invalid or expired refresh token.'));
|
||||
// Mock finding no user for this token, which should trigger the 403 logic
|
||||
vi.mocked(db.userRepo.findUserByRefreshToken).mockResolvedValue(undefined as any);
|
||||
|
||||
const response = await supertest(app)
|
||||
.post('/api/auth/refresh-token')
|
||||
.set('Cookie', 'refreshToken=invalid-token'); // This was a duplicate, fixed.
|
||||
.set('Cookie', 'refreshToken=invalid-token');
|
||||
|
||||
expect(response.status).toBe(403);
|
||||
});
|
||||
|
||||
@@ -37,6 +37,9 @@ const validatePasswordStrength = (password: string): { isValid: boolean; feedbac
|
||||
return { isValid: true };
|
||||
};
|
||||
|
||||
// Helper for consistent required string validation (handles missing/null/empty)
|
||||
const requiredString = (message: string) => z.preprocess((val) => val ?? '', z.string().min(1, message));
|
||||
|
||||
// Conditionally disable rate limiting for the test environment
|
||||
const isTestEnv = process.env.NODE_ENV === 'test';
|
||||
|
||||
@@ -79,7 +82,7 @@ const forgotPasswordSchema = z.object({
|
||||
|
||||
const resetPasswordSchema = z.object({
|
||||
body: z.object({
|
||||
token: z.string().min(1, 'Token is required.'),
|
||||
token: requiredString('Token is required.'),
|
||||
newPassword: z.string().min(8, 'Password must be at least 8 characters long.').superRefine((password, ctx) => {
|
||||
const strength = validatePasswordStrength(password);
|
||||
if (!strength.isValid) ctx.addIssue({ code: 'custom', message: strength.feedback });
|
||||
|
||||
@@ -8,6 +8,9 @@ import { validateRequest } from '../middleware/validation.middleware';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
// Helper for consistent required string validation (handles missing/null/empty)
|
||||
const requiredString = (message: string) => z.preprocess((val) => val ?? '', z.string().min(1, message));
|
||||
|
||||
// --- Zod Schemas for Budget Routes (as per ADR-003) ---
|
||||
|
||||
const budgetIdParamSchema = z.object({
|
||||
@@ -18,7 +21,7 @@ const budgetIdParamSchema = z.object({
|
||||
|
||||
const createBudgetSchema = z.object({
|
||||
body: z.object({
|
||||
name: z.string().min(1, 'Budget name is required.'),
|
||||
name: requiredString('Budget name is required.'),
|
||||
amount_cents: z.number().int().positive('Amount must be a positive integer.'),
|
||||
period: z.enum(['weekly', 'monthly']),
|
||||
start_date: z.string().date('Start date must be a valid date in YYYY-MM-DD format.'),
|
||||
|
||||
@@ -11,6 +11,9 @@ import { validateRequest } from '../middleware/validation.middleware';
|
||||
const router = express.Router();
|
||||
const adminGamificationRouter = express.Router(); // Create a new router for admin-only routes.
|
||||
|
||||
// Helper for consistent required string validation (handles missing/null/empty)
|
||||
const requiredString = (message: string) => z.preprocess((val) => val ?? '', z.string().min(1, message));
|
||||
|
||||
// --- Zod Schemas for Gamification Routes (as per ADR-003) ---
|
||||
|
||||
const leaderboardSchema = z.object({
|
||||
@@ -21,8 +24,8 @@ const leaderboardSchema = z.object({
|
||||
|
||||
const awardAchievementSchema = z.object({
|
||||
body: z.object({
|
||||
userId: z.string().min(1, 'userId is required.'),
|
||||
achievementName: z.string().min(1, 'achievementName is required.'),
|
||||
userId: requiredString('userId is required.'),
|
||||
achievementName: requiredString('achievementName is required.'),
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -46,7 +49,6 @@ router.get('/', async (req, res, next: NextFunction) => {
|
||||
* GET /api/achievements/leaderboard - Get the top users by points.
|
||||
* This is a public endpoint.
|
||||
*/
|
||||
type LeaderboardRequest = z.infer<typeof leaderboardSchema>;
|
||||
router.get('/leaderboard', validateRequest(leaderboardSchema), async (req, res, next: NextFunction): Promise<void> => {
|
||||
// Apply ADR-003 pattern for type safety.
|
||||
// Explicitly coerce query params to ensure numbers are passed to the repo,
|
||||
|
||||
@@ -6,6 +6,9 @@ import { validateRequest } from '../middleware/validation.middleware';
|
||||
|
||||
const router = Router();
|
||||
|
||||
// Helper for consistent required string validation (handles missing/null/empty)
|
||||
const requiredString = (message: string) => z.preprocess((val) => val ?? '', z.string().min(1, message));
|
||||
|
||||
// --- Zod Schemas for Recipe Routes (as per ADR-003) ---
|
||||
|
||||
const bySalePercentageSchema = z.object({
|
||||
@@ -22,8 +25,8 @@ const bySaleIngredientsSchema = z.object({
|
||||
|
||||
const byIngredientAndTagSchema = z.object({
|
||||
query: z.object({
|
||||
ingredient: z.string().min(1, 'Query parameter "ingredient" is required.'),
|
||||
tag: z.string().min(1, 'Query parameter "tag" is required.'),
|
||||
ingredient: requiredString('Query parameter "ingredient" is required.'),
|
||||
tag: requiredString('Query parameter "tag" is required.'),
|
||||
}),
|
||||
});
|
||||
|
||||
|
||||
@@ -8,9 +8,12 @@ import { validateRequest } from '../middleware/validation.middleware';
|
||||
|
||||
const router = Router();
|
||||
|
||||
// Helper for consistent required string validation (handles missing/null/empty)
|
||||
const requiredString = (message: string) => z.preprocess((val) => val ?? '', z.string().min(1, message));
|
||||
|
||||
const geocodeSchema = z.object({
|
||||
body: z.object({
|
||||
address: z.string().min(1, 'An address string is required.'),
|
||||
address: requiredString('An address string is required.'),
|
||||
}),
|
||||
});
|
||||
|
||||
|
||||
@@ -33,6 +33,9 @@ const validatePasswordStrength = (password: string): { isValid: boolean; feedbac
|
||||
return { isValid: true };
|
||||
};
|
||||
|
||||
// Helper for consistent required string validation (handles missing/null/empty)
|
||||
const requiredString = (message: string) => z.preprocess((val) => val ?? '', z.string().min(1, message));
|
||||
|
||||
// --- Zod Schemas for User Routes (as per ADR-003) ---
|
||||
|
||||
const numericIdParam = (key: string) => z.object({
|
||||
@@ -54,17 +57,17 @@ const updatePasswordSchema = z.object({
|
||||
});
|
||||
|
||||
const deleteAccountSchema = z.object({
|
||||
body: z.object({ password: z.string().min(1, "Field 'password' is required.") }),
|
||||
body: z.object({ password: requiredString("Field 'password' is required.") }),
|
||||
});
|
||||
|
||||
const addWatchedItemSchema = z.object({
|
||||
body: z.object({
|
||||
itemName: z.string().min(1, "Field 'itemName' is required."),
|
||||
category: z.string().min(1, "Field 'category' is required."),
|
||||
itemName: requiredString("Field 'itemName' is required."),
|
||||
category: requiredString("Field 'category' is required."),
|
||||
}),
|
||||
});
|
||||
|
||||
const createShoppingListSchema = z.object({ body: z.object({ name: z.string().min(1, "Field 'name' is required.") }) });
|
||||
const createShoppingListSchema = z.object({ body: z.object({ name: requiredString("Field 'name' is required.") }) });
|
||||
|
||||
// Apply the JWT authentication middleware to all routes in this file.
|
||||
const notificationQuerySchema = z.object({
|
||||
@@ -417,7 +420,7 @@ router.delete('/shopping-lists/:listId', validateRequest(shoppingListIdSchema),
|
||||
const addShoppingListItemSchema = shoppingListIdSchema.extend({
|
||||
body: z.object({
|
||||
masterItemId: z.number().int().positive().optional(),
|
||||
customItemName: z.string().min(1).optional(),
|
||||
customItemName: requiredString('customItemName required?'),
|
||||
}).refine(data => data.masterItemId || data.customItemName, { message: 'Either masterItemId or customItemName must be provided.' }),
|
||||
});
|
||||
type AddShoppingListItemRequest = z.infer<typeof addShoppingListItemSchema>;
|
||||
|
||||
Reference in New Issue
Block a user