more refactor
Some checks failed
Deploy to Test Environment / deploy-to-test (push) Has been cancelled

This commit is contained in:
2025-12-21 21:14:22 -08:00
parent bc2c24bcff
commit de5425ef91
8 changed files with 40 additions and 42 deletions

View File

@@ -23,7 +23,6 @@ const wrapper = ({ children }: { children: ReactNode }) => <UserDataProvider>{ch
// 4. Mock data for testing // 4. Mock data for testing
const mockUser: UserProfile = createMockUserProfile({ const mockUser: UserProfile = createMockUserProfile({
user_id: 'user-123',
full_name: 'Test User', full_name: 'Test User',
points: 100, points: 100,
user: { user_id: 'user-123', email: 'test@example.com' }, user: { user_id: 'user-123', email: 'test@example.com' },

View File

@@ -30,7 +30,6 @@ const mockedApiClient = apiClient as Mocked<typeof apiClient>;
// --- Mock Data --- // --- Mock Data ---
const mockProfile: UserProfile = createMockUserProfile({ const mockProfile: UserProfile = createMockUserProfile({
user_id: 'user-123',
user: createMockUser({ user_id: 'user-123', email: 'test@example.com' }), user: createMockUser({ user_id: 'user-123', email: 'test@example.com' }),
full_name: 'Test User', full_name: 'Test User',
avatar_url: 'http://example.com/avatar.jpg', avatar_url: 'http://example.com/avatar.jpg',

View File

@@ -237,12 +237,12 @@ router.get('/unmatched-items', async (req, res, next: NextFunction) => {
* DELETE /api/admin/recipes/:recipeId - Admin endpoint to delete any recipe. * DELETE /api/admin/recipes/:recipeId - Admin endpoint to delete any recipe.
*/ */
router.delete('/recipes/:recipeId', validateRequest(numericIdParamSchema('recipeId')), async (req: Request, res: Response, next: NextFunction) => { router.delete('/recipes/:recipeId', validateRequest(numericIdParamSchema('recipeId')), async (req: Request, res: Response, next: NextFunction) => {
const adminUser = req.user as UserProfile; const userProfile = req.user as UserProfile;
// Infer the type directly from the schema generator function. // This was a duplicate, fixed. // Infer the type directly from the schema generator function. // This was a duplicate, fixed.
const { params } = req as unknown as z.infer<ReturnType<typeof numericIdParamSchema>>; const { params } = req as unknown as z.infer<ReturnType<typeof numericIdParamSchema>>;
try { try {
// The isAdmin flag bypasses the ownership check in the repository method. // The isAdmin flag bypasses the ownership check in the repository method.
await db.recipeRepo.deleteRecipe(params.recipeId, adminUser.user.user_id, true, req.log); await db.recipeRepo.deleteRecipe(params.recipeId, userProfile.user.user_id, true, req.log);
res.status(204).send(); res.status(204).send();
} catch (error: unknown) { } catch (error: unknown) {
next(error); next(error);
@@ -323,11 +323,11 @@ router.put('/users/:id', validateRequest(updateUserRoleSchema), async (req: Requ
}); });
router.delete('/users/:id', validateRequest(uuidParamSchema('id', 'A valid user ID is required.')), async (req: Request, res: Response, next: NextFunction) => { router.delete('/users/:id', validateRequest(uuidParamSchema('id', 'A valid user ID is required.')), async (req: Request, res: Response, next: NextFunction) => {
const adminUser = req.user as UserProfile; const userProfile = req.user as UserProfile;
// Apply ADR-003 pattern for type safety // Apply ADR-003 pattern for type safety
const { params } = req as unknown as z.infer<ReturnType<typeof uuidParamSchema>>; const { params } = req as unknown as z.infer<ReturnType<typeof uuidParamSchema>>;
try { try {
if (adminUser.user.user_id === params.id) { if (userProfile.user.user_id === params.id) {
throw new ValidationError([], 'Admins cannot delete their own account.'); throw new ValidationError([], 'Admins cannot delete their own account.');
} }
await db.userRepo.deleteUserById(params.id, req.log); await db.userRepo.deleteUserById(params.id, req.log);
@@ -342,8 +342,8 @@ router.delete('/users/:id', validateRequest(uuidParamSchema('id', 'A valid user
* This is useful for testing or forcing an update without waiting for the cron schedule. * This is useful for testing or forcing an update without waiting for the cron schedule.
*/ */
router.post('/trigger/daily-deal-check', async (req: Request, res: Response, next: NextFunction) => { router.post('/trigger/daily-deal-check', async (req: Request, res: Response, next: NextFunction) => {
const adminUser = req.user as UserProfile; const userProfile = req.user as UserProfile;
logger.info(`[Admin] Manual trigger for daily deal check received from user: ${adminUser.user.user_id}`); logger.info(`[Admin] Manual trigger for daily deal check received from user: ${userProfile.user.user_id}`);
try { try {
// We call the function but don't wait for it to finish (no `await`). // We call the function but don't wait for it to finish (no `await`).
@@ -361,8 +361,8 @@ router.post('/trigger/daily-deal-check', async (req: Request, res: Response, nex
* This is useful for testing or re-generating a report without waiting for the cron schedule. * This is useful for testing or re-generating a report without waiting for the cron schedule.
*/ */
router.post('/trigger/analytics-report', async (req: Request, res: Response, next: NextFunction) => { router.post('/trigger/analytics-report', async (req: Request, res: Response, next: NextFunction) => {
const adminUser = req.user as UserProfile; const userProfile = req.user as UserProfile;
logger.info(`[Admin] Manual trigger for analytics report generation received from user: ${adminUser.user.user_id}`); logger.info(`[Admin] Manual trigger for analytics report generation received from user: ${userProfile.user.user_id}`);
try { try {
const reportDate = new Date().toISOString().split('T')[0]; // YYYY-MM-DD const reportDate = new Date().toISOString().split('T')[0]; // YYYY-MM-DD
@@ -383,10 +383,10 @@ router.post('/trigger/analytics-report', async (req: Request, res: Response, nex
* This is triggered by an admin after they have verified the flyer processing was successful. * This is triggered by an admin after they have verified the flyer processing was successful.
*/ */
router.post('/flyers/:flyerId/cleanup', validateRequest(numericIdParamSchema('flyerId')), async (req: Request, res: Response, next: NextFunction) => { router.post('/flyers/:flyerId/cleanup', validateRequest(numericIdParamSchema('flyerId')), async (req: Request, res: Response, next: NextFunction) => {
const adminUser = req.user as UserProfile; const userProfile = req.user as UserProfile;
// Infer type from the schema generator for type safety, as per ADR-003. // Infer type from the schema generator for type safety, as per ADR-003.
const { params } = req as unknown as z.infer<ReturnType<typeof numericIdParamSchema>>; // This was a duplicate, fixed. const { params } = req as unknown as z.infer<ReturnType<typeof numericIdParamSchema>>; // This was a duplicate, fixed.
logger.info(`[Admin] Manual trigger for flyer file cleanup received from user: ${adminUser.user.user_id} for flyer ID: ${params.flyerId}`); logger.info(`[Admin] Manual trigger for flyer file cleanup received from user: ${userProfile.user.user_id} for flyer ID: ${params.flyerId}`);
// Enqueue the cleanup job. The worker will handle the file deletion. // Enqueue the cleanup job. The worker will handle the file deletion.
try { try {
@@ -402,8 +402,8 @@ router.post('/flyers/:flyerId/cleanup', validateRequest(numericIdParamSchema('fl
* This is for testing the retry mechanism and Bull Board UI. * This is for testing the retry mechanism and Bull Board UI.
*/ */
router.post('/trigger/failing-job', async (req: Request, res: Response, next: NextFunction) => { router.post('/trigger/failing-job', async (req: Request, res: Response, next: NextFunction) => {
const adminUser = req.user as UserProfile; const userProfile = req.user as UserProfile;
logger.info(`[Admin] Manual trigger for a failing job received from user: ${adminUser.user.user_id}`); logger.info(`[Admin] Manual trigger for a failing job received from user: ${userProfile.user.user_id}`);
try { try {
// Add a job with a special 'forceFail' flag that the worker will recognize. // Add a job with a special 'forceFail' flag that the worker will recognize.
@@ -419,8 +419,8 @@ router.post('/trigger/failing-job', async (req: Request, res: Response, next: Ne
* Requires admin privileges. * Requires admin privileges.
*/ */
router.post('/system/clear-geocode-cache', async (req: Request, res: Response, next: NextFunction) => { router.post('/system/clear-geocode-cache', async (req: Request, res: Response, next: NextFunction) => {
const adminUser = req.user as UserProfile; const userProfile = req.user as UserProfile;
logger.info(`[Admin] Manual trigger for geocode cache clear received from user: ${adminUser.user.user_id}`); logger.info(`[Admin] Manual trigger for geocode cache clear received from user: ${userProfile.user.user_id}`);
try { try {
const keysDeleted = await geocodingService.clearGeocodeCache(req.log); const keysDeleted = await geocodingService.clearGeocodeCache(req.log);
@@ -476,7 +476,7 @@ router.get('/queues/status', async (req: Request, res: Response, next: NextFunct
* POST /api/admin/jobs/:queueName/:jobId/retry - Retries a specific failed job. * POST /api/admin/jobs/:queueName/:jobId/retry - Retries a specific failed job.
*/ */
router.post('/jobs/:queueName/:jobId/retry', validateRequest(jobRetrySchema), async (req: Request, res: Response, next: NextFunction) => { router.post('/jobs/:queueName/:jobId/retry', validateRequest(jobRetrySchema), async (req: Request, res: Response, next: NextFunction) => {
const adminUser = req.user as UserProfile; const userProfile = req.user as UserProfile;
const { params: { queueName, jobId } } = req as unknown as z.infer<typeof jobRetrySchema>; const { params: { queueName, jobId } } = req as unknown as z.infer<typeof jobRetrySchema>;
const queueMap: { [key: string]: Queue } = { const queueMap: { [key: string]: Queue } = {
@@ -501,7 +501,7 @@ router.post('/jobs/:queueName/:jobId/retry', validateRequest(jobRetrySchema), as
if (jobState !== 'failed') throw new ValidationError([], `Job is not in a 'failed' state. Current state: ${jobState}.`); // This was a duplicate, fixed. if (jobState !== 'failed') throw new ValidationError([], `Job is not in a 'failed' state. Current state: ${jobState}.`); // This was a duplicate, fixed.
await job.retry(); await job.retry();
logger.info(`[Admin] User ${adminUser.user.user_id} manually retried job ${jobId} in queue ${queueName}.`); logger.info(`[Admin] User ${userProfile.user.user_id} manually retried job ${jobId} in queue ${queueName}.`);
res.status(200).json({ message: `Job ${jobId} has been successfully marked for retry.` }); res.status(200).json({ message: `Job ${jobId} has been successfully marked for retry.` });
} catch (error) { } catch (error) {
next(error); next(error);
@@ -512,8 +512,8 @@ router.post('/jobs/:queueName/:jobId/retry', validateRequest(jobRetrySchema), as
* POST /api/admin/trigger/weekly-analytics - Manually trigger the weekly analytics report job. * POST /api/admin/trigger/weekly-analytics - Manually trigger the weekly analytics report job.
*/ */
router.post('/trigger/weekly-analytics', async (req: Request, res: Response, next: NextFunction) => { router.post('/trigger/weekly-analytics', async (req: Request, res: Response, next: NextFunction) => {
const adminUser = req.user as UserProfile; // This was a duplicate, fixed. const userProfile = req.user as UserProfile; // This was a duplicate, fixed.
logger.info(`[Admin] Manual trigger for weekly analytics report received from user: ${adminUser.user.user_id}`); logger.info(`[Admin] Manual trigger for weekly analytics report received from user: ${userProfile.user.user_id}`);
try { try {
const { year: reportYear, week: reportWeek } = getSimpleWeekAndYear(); const { year: reportYear, week: reportWeek } = getSimpleWeekAndYear();

View File

@@ -79,7 +79,7 @@ describe('Admin Stats Routes (/api/admin/stats)', () => {
describe('GET /stats', () => { describe('GET /stats', () => {
it('should return application stats on success', async () => { it('should return application stats on success', async () => {
const mockStats = { flyerCount: 150, userCount: 42, flyerItemCount: 10000, storeCount: 12, pendingCorrectionCount: 5 }; const mockStats = { flyerCount: 150, userCount: 42, flyerItemCount: 10000, storeCount: 12, pendingCorrectionCount: 5, recipeCount: 50 };
vi.mocked(adminRepo.getApplicationStats).mockResolvedValue(mockStats); vi.mocked(adminRepo.getApplicationStats).mockResolvedValue(mockStats);
const response = await supertest(app).get('/api/admin/stats'); const response = await supertest(app).get('/api/admin/stats');
expect(response.status).toBe(200); expect(response.status).toBe(200);

View File

@@ -48,12 +48,12 @@ router.use(passport.authenticate('jwt', { session: false }));
* GET /api/budgets - Get all budgets for the authenticated user. * GET /api/budgets - Get all budgets for the authenticated user.
*/ */
router.get('/', async (req: Request, res: Response, next: NextFunction) => { router.get('/', async (req: Request, res: Response, next: NextFunction) => {
const user = req.user as UserProfile; const userProfile = req.user as UserProfile;
try { try {
const budgets = await budgetRepo.getBudgetsForUser(user.user_id, req.log); const budgets = await budgetRepo.getBudgetsForUser(userProfile.user.user_id, req.log);
res.json(budgets); res.json(budgets);
} catch (error) { } catch (error) {
req.log.error({ error, userId: user.user_id }, 'Error fetching budgets'); req.log.error({ error, userId: userProfile.user.user_id }, 'Error fetching budgets');
next(error); next(error);
} }
}); });
@@ -62,14 +62,14 @@ router.get('/', async (req: Request, res: Response, next: NextFunction) => {
* POST /api/budgets - Create a new budget for the authenticated user. * POST /api/budgets - Create a new budget for the authenticated user.
*/ */
router.post('/', validateRequest(createBudgetSchema), async (req: Request, res: Response, next: NextFunction) => { router.post('/', validateRequest(createBudgetSchema), async (req: Request, res: Response, next: NextFunction) => {
const user = req.user as UserProfile; const userProfile = req.user as UserProfile;
type CreateBudgetRequest = z.infer<typeof createBudgetSchema>; type CreateBudgetRequest = z.infer<typeof createBudgetSchema>;
const { body } = req as unknown as CreateBudgetRequest; const { body } = req as unknown as CreateBudgetRequest;
try { try {
const newBudget = await budgetRepo.createBudget(user.user_id, body, req.log); const newBudget = await budgetRepo.createBudget(userProfile.user.user_id, body, req.log);
res.status(201).json(newBudget); res.status(201).json(newBudget);
} catch (error: unknown) { } catch (error: unknown) {
req.log.error({ error, userId: user.user_id, body }, 'Error creating budget'); req.log.error({ error, userId: userProfile.user.user_id, body }, 'Error creating budget');
next(error); next(error);
} }
}); });
@@ -78,14 +78,14 @@ router.post('/', validateRequest(createBudgetSchema), async (req: Request, res:
* PUT /api/budgets/:id - Update an existing budget. * PUT /api/budgets/:id - Update an existing budget.
*/ */
router.put('/:id', validateRequest(updateBudgetSchema), async (req: Request, res: Response, next: NextFunction) => { router.put('/:id', validateRequest(updateBudgetSchema), async (req: Request, res: Response, next: NextFunction) => {
const user = req.user as UserProfile; const userProfile = req.user as UserProfile;
type UpdateBudgetRequest = z.infer<typeof updateBudgetSchema>; type UpdateBudgetRequest = z.infer<typeof updateBudgetSchema>;
const { params, body } = req as unknown as UpdateBudgetRequest; const { params, body } = req as unknown as UpdateBudgetRequest;
try { try {
const updatedBudget = await budgetRepo.updateBudget(params.id, user.user_id, body, req.log); const updatedBudget = await budgetRepo.updateBudget(params.id, userProfile.user.user_id, body, req.log);
res.json(updatedBudget); res.json(updatedBudget);
} catch (error: unknown) { } catch (error: unknown) {
req.log.error({ error, userId: user.user_id, budgetId: params.id }, 'Error updating budget'); req.log.error({ error, userId: userProfile.user.user_id, budgetId: params.id }, 'Error updating budget');
next(error); next(error);
} }
}); });
@@ -94,14 +94,14 @@ router.put('/:id', validateRequest(updateBudgetSchema), async (req: Request, res
* DELETE /api/budgets/:id - Delete a budget. * DELETE /api/budgets/:id - Delete a budget.
*/ */
router.delete('/:id', validateRequest(budgetIdParamSchema), async (req: Request, res: Response, next: NextFunction) => { router.delete('/:id', validateRequest(budgetIdParamSchema), async (req: Request, res: Response, next: NextFunction) => {
const user = req.user as UserProfile; const userProfile = req.user as UserProfile;
type DeleteBudgetRequest = z.infer<typeof budgetIdParamSchema>; type DeleteBudgetRequest = z.infer<typeof budgetIdParamSchema>;
const { params } = req as unknown as DeleteBudgetRequest; const { params } = req as unknown as DeleteBudgetRequest;
try { try {
await budgetRepo.deleteBudget(params.id, user.user_id, req.log); await budgetRepo.deleteBudget(params.id, userProfile.user.user_id, req.log);
res.status(204).send(); // No Content res.status(204).send(); // No Content
} catch (error: unknown) { } catch (error: unknown) {
req.log.error({ error, userId: user.user_id, budgetId: params.id }, 'Error deleting budget'); req.log.error({ error, userId: userProfile.user.user_id, budgetId: params.id }, 'Error deleting budget');
next(error); next(error);
} }
}); });
@@ -111,15 +111,15 @@ router.delete('/:id', validateRequest(budgetIdParamSchema), async (req: Request,
* Query params: startDate (YYYY-MM-DD), endDate (YYYY-MM-DD) * Query params: startDate (YYYY-MM-DD), endDate (YYYY-MM-DD)
*/ */
router.get('/spending-analysis', validateRequest(spendingAnalysisSchema), async (req: Request, res: Response, next: NextFunction) => { router.get('/spending-analysis', validateRequest(spendingAnalysisSchema), async (req: Request, res: Response, next: NextFunction) => {
const user = req.user as UserProfile; const userProfile = req.user as UserProfile;
type SpendingAnalysisRequest = z.infer<typeof spendingAnalysisSchema>; type SpendingAnalysisRequest = z.infer<typeof spendingAnalysisSchema>;
const { query: { startDate, endDate } } = req as unknown as SpendingAnalysisRequest; const { query: { startDate, endDate } } = req as unknown as SpendingAnalysisRequest;
try { try {
const spendingData = await budgetRepo.getSpendingByCategory(user.user_id, startDate, endDate, req.log); const spendingData = await budgetRepo.getSpendingByCategory(userProfile.user.user_id, startDate, endDate, req.log);
res.json(spendingData); res.json(spendingData);
} catch (error) { } catch (error) {
req.log.error({ error, userId: user.user_id, startDate, endDate }, 'Error fetching spending analysis'); req.log.error({ error, userId: userProfile.user.user_id, startDate, endDate }, 'Error fetching spending analysis');
next(error); next(error);
} }
}); });

View File

@@ -26,11 +26,11 @@ router.use(passport.authenticate('jwt', { session: false }));
* @access Private * @access Private
*/ */
router.get('/best-watched-prices', validateRequest(bestWatchedPricesSchema), async (req: Request, res: Response, next: NextFunction) => { router.get('/best-watched-prices', validateRequest(bestWatchedPricesSchema), async (req: Request, res: Response, next: NextFunction) => {
const user = req.user as UserProfile; const userProfile = req.user as UserProfile;
try { try {
// The controller logic is simple enough to be handled directly in the route, // The controller logic is simple enough to be handled directly in the route,
// consistent with other simple GET routes in the project. // consistent with other simple GET routes in the project.
const deals = await dealsRepo.findBestPricesForWatchedItems(user.user_id, req.log); const deals = await dealsRepo.findBestPricesForWatchedItems(userProfile.user.user_id, req.log);
req.log.info({ dealCount: deals.length }, 'Successfully fetched best watched item deals.'); req.log.info({ dealCount: deals.length }, 'Successfully fetched best watched item deals.');
res.status(200).json(deals); res.status(200).json(deals);
} catch (error) { } catch (error) {

View File

@@ -75,12 +75,12 @@ router.get(
'/me', '/me',
passport.authenticate('jwt', { session: false }), passport.authenticate('jwt', { session: false }),
async (req, res, next: NextFunction): Promise<void> => { async (req, res, next: NextFunction): Promise<void> => {
const user = req.user as UserProfile; const userProfile = req.user as UserProfile;
try { try {
const userAchievements = await gamificationRepo.getUserAchievements(user.user_id, req.log); const userAchievements = await gamificationRepo.getUserAchievements(userProfile.user.user_id, req.log);
res.json(userAchievements); res.json(userAchievements);
} catch (error) { } catch (error) {
logger.error({ error, userId: user.user_id }, 'Error fetching user achievements:'); logger.error({ error, userId: userProfile.user.user_id }, 'Error fetching user achievements:');
next(error); next(error);
} }
} }

View File

@@ -484,7 +484,7 @@ describe('Admin DB Service', () => {
describe('updateUserRole', () => { describe('updateUserRole', () => {
it('should update the user role and return the updated user', async () => { it('should update the user role and return the updated user', async () => {
const mockProfile: Profile = createMockProfile({ user_id: '1', role: 'admin' }); const mockProfile: Profile = createMockProfile({ role: 'admin' });
mockPoolInstance.query.mockResolvedValue({ rows: [mockProfile], rowCount: 1 }); mockPoolInstance.query.mockResolvedValue({ rows: [mockProfile], rowCount: 1 });
const result = await adminRepo.updateUserRole('1', 'admin', mockLogger); const result = await adminRepo.updateUserRole('1', 'admin', mockLogger);
expect(mockPoolInstance.query).toHaveBeenCalledWith('UPDATE public.profiles SET role = $1 WHERE user_id = $2 RETURNING *', ['admin', '1']); expect(mockPoolInstance.query).toHaveBeenCalledWith('UPDATE public.profiles SET role = $1 WHERE user_id = $2 RETURNING *', ['admin', '1']);