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
const mockUser: UserProfile = createMockUserProfile({
user_id: 'user-123',
full_name: 'Test User',
points: 100,
user: { user_id: 'user-123', email: 'test@example.com' },

View File

@@ -30,7 +30,6 @@ const mockedApiClient = apiClient as Mocked<typeof apiClient>;
// --- Mock Data ---
const mockProfile: UserProfile = createMockUserProfile({
user_id: 'user-123',
user: createMockUser({ user_id: 'user-123', email: 'test@example.com' }),
full_name: 'Test User',
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.
*/
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.
const { params } = req as unknown as z.infer<ReturnType<typeof numericIdParamSchema>>;
try {
// 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();
} catch (error: unknown) {
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) => {
const adminUser = req.user as UserProfile;
const userProfile = req.user as UserProfile;
// Apply ADR-003 pattern for type safety
const { params } = req as unknown as z.infer<ReturnType<typeof uuidParamSchema>>;
try {
if (adminUser.user.user_id === params.id) {
if (userProfile.user.user_id === params.id) {
throw new ValidationError([], 'Admins cannot delete their own account.');
}
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.
*/
router.post('/trigger/daily-deal-check', async (req: Request, res: Response, next: NextFunction) => {
const adminUser = req.user as UserProfile;
logger.info(`[Admin] Manual trigger for daily deal check received from user: ${adminUser.user.user_id}`);
const userProfile = req.user as UserProfile;
logger.info(`[Admin] Manual trigger for daily deal check received from user: ${userProfile.user.user_id}`);
try {
// 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.
*/
router.post('/trigger/analytics-report', async (req: Request, res: Response, next: NextFunction) => {
const adminUser = req.user as UserProfile;
logger.info(`[Admin] Manual trigger for analytics report generation received from user: ${adminUser.user.user_id}`);
const userProfile = req.user as UserProfile;
logger.info(`[Admin] Manual trigger for analytics report generation received from user: ${userProfile.user.user_id}`);
try {
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.
*/
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.
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.
try {
@@ -402,8 +402,8 @@ router.post('/flyers/:flyerId/cleanup', validateRequest(numericIdParamSchema('fl
* This is for testing the retry mechanism and Bull Board UI.
*/
router.post('/trigger/failing-job', async (req: Request, res: Response, next: NextFunction) => {
const adminUser = req.user as UserProfile;
logger.info(`[Admin] Manual trigger for a failing job received from user: ${adminUser.user.user_id}`);
const userProfile = req.user as UserProfile;
logger.info(`[Admin] Manual trigger for a failing job received from user: ${userProfile.user.user_id}`);
try {
// 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.
*/
router.post('/system/clear-geocode-cache', async (req: Request, res: Response, next: NextFunction) => {
const adminUser = req.user as UserProfile;
logger.info(`[Admin] Manual trigger for geocode cache clear received from user: ${adminUser.user.user_id}`);
const userProfile = req.user as UserProfile;
logger.info(`[Admin] Manual trigger for geocode cache clear received from user: ${userProfile.user.user_id}`);
try {
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.
*/
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 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.
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.` });
} catch (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.
*/
router.post('/trigger/weekly-analytics', async (req: Request, res: Response, next: NextFunction) => {
const adminUser = 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}`);
const userProfile = req.user as UserProfile; // This was a duplicate, fixed.
logger.info(`[Admin] Manual trigger for weekly analytics report received from user: ${userProfile.user.user_id}`);
try {
const { year: reportYear, week: reportWeek } = getSimpleWeekAndYear();

View File

@@ -79,7 +79,7 @@ describe('Admin Stats Routes (/api/admin/stats)', () => {
describe('GET /stats', () => {
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);
const response = await supertest(app).get('/api/admin/stats');
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.
*/
router.get('/', async (req: Request, res: Response, next: NextFunction) => {
const user = req.user as UserProfile;
const userProfile = req.user as UserProfile;
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);
} 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);
}
});
@@ -62,14 +62,14 @@ router.get('/', async (req: Request, res: Response, next: NextFunction) => {
* POST /api/budgets - Create a new budget for the authenticated user.
*/
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>;
const { body } = req as unknown as CreateBudgetRequest;
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);
} 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);
}
});
@@ -78,14 +78,14 @@ router.post('/', validateRequest(createBudgetSchema), async (req: Request, res:
* PUT /api/budgets/:id - Update an existing budget.
*/
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>;
const { params, body } = req as unknown as UpdateBudgetRequest;
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);
} 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);
}
});
@@ -94,14 +94,14 @@ router.put('/:id', validateRequest(updateBudgetSchema), async (req: Request, res
* DELETE /api/budgets/:id - Delete a budget.
*/
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>;
const { params } = req as unknown as DeleteBudgetRequest;
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
} 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);
}
});
@@ -111,15 +111,15 @@ router.delete('/:id', validateRequest(budgetIdParamSchema), async (req: Request,
* Query params: startDate (YYYY-MM-DD), endDate (YYYY-MM-DD)
*/
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>;
const { query: { startDate, endDate } } = req as unknown as SpendingAnalysisRequest;
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);
} 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);
}
});

View File

@@ -26,11 +26,11 @@ router.use(passport.authenticate('jwt', { session: false }));
* @access Private
*/
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 {
// The controller logic is simple enough to be handled directly in the route,
// 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.');
res.status(200).json(deals);
} catch (error) {

View File

@@ -75,12 +75,12 @@ router.get(
'/me',
passport.authenticate('jwt', { session: false }),
async (req, res, next: NextFunction): Promise<void> => {
const user = req.user as UserProfile;
const userProfile = req.user as UserProfile;
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);
} 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);
}
}

View File

@@ -484,7 +484,7 @@ describe('Admin DB Service', () => {
describe('updateUserRole', () => {
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 });
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']);