From 6540a19ee99e8ceb9ae44de624f0bef00d8ba93f Mon Sep 17 00:00:00 2001 From: Torben Sorensen Date: Thu, 4 Dec 2025 23:12:07 -0800 Subject: [PATCH] Fix: TypeError: JwtStrategy requires a secret or key --- .gitea/workflows/deploy.yml | 5 +- src/routes/admin.test.ts | 135 ++++++++++++++++--------------- src/routes/ai.test.ts | 46 ++++++----- src/routes/auth.test.ts | 40 ++++----- src/routes/budget.test.ts | 18 ++--- src/routes/gamification.test.ts | 6 +- src/routes/public.routes.test.ts | 39 +++++---- src/routes/user.test.ts | 54 +++++++------ 8 files changed, 183 insertions(+), 160 deletions(-) diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml index 178c5797..9baf734b 100644 --- a/.gitea/workflows/deploy.yml +++ b/.gitea/workflows/deploy.yml @@ -94,6 +94,9 @@ jobs: FRONTEND_URL: "http://localhost:3000" VITE_API_BASE_URL: "http://localhost:3001/api" GEMINI_API_KEY: ${{ secrets.VITE_GOOGLE_GENAI_API_KEY }} + + # --- JWT Secret for Passport authentication in tests --- + JWT_SECRET: ${{ secrets.JWT_SECRET }} # --- Increase Node.js memory limit to prevent heap out of memory errors --- # This is crucial for memory-intensive tasks like running tests and coverage. @@ -101,7 +104,7 @@ jobs: run: | # Fail-fast check to ensure secrets are configured in Gitea for testing. - if [ -z "$DB_HOST" ] || [ -z "$DB_USER" ] || [ -z "$DB_PASSWORD" ] || [ -z "$DB_NAME" ] || [ -z "$GEMINI_API_KEY" ] || [ -z "$REDIS_PASSWORD" ]; then + if [ -z "$DB_HOST" ] || [ -z "$DB_USER" ] || [ -z "$DB_PASSWORD" ] || [ -z "$DB_NAME" ] || [ -z "$GEMINI_API_KEY" ] || [ -z "$REDIS_PASSWORD" ] || [ -z "$JWT_SECRET" ]; then echo "ERROR: One or more test secrets (DB_*, GEMINI_API_KEY, REDIS_PASSWORD_TEST) are not set in Gitea repository secrets." exit 1 fi diff --git a/src/routes/admin.test.ts b/src/routes/admin.test.ts index 81745f1d..fb3d507c 100644 --- a/src/routes/admin.test.ts +++ b/src/routes/admin.test.ts @@ -5,12 +5,19 @@ import express, { Request, Response, NextFunction } from 'express'; import path from 'node:path'; import fs from 'node:fs/promises'; import adminRouter from './admin'; // Correctly imported -import * as db from '../services/db/index.db'; import { UserProfile } from '../types'; -// Mock the entire db service -vi.mock('../services/db'); -const mockedDb = db as Mocked; +// Mock the specific DB modules used by the admin router. +// This is more robust than mocking the barrel file ('../services/db'). +vi.mock('../services/db/admin.db'); +vi.mock('../services/db/flyer.db'); +vi.mock('../services/db/recipe.db'); + +// Import the mocked modules to control them in tests. +import * as adminDb from '../services/db/admin.db'; +import * as flyerDb from '../services/db/flyer.db'; +import * as recipeDb from '../services/db/recipe.db'; +const mockedDb = { ...adminDb, ...flyerDb, ...recipeDb } as Mocked; // Mock the logger to keep test output clean vi.mock('../services/logger.server', () => ({ @@ -112,7 +119,7 @@ describe('Admin Routes (/api/admin)', () => { it('GET /corrections should return corrections data', async () => { // Arrange const mockCorrections: Awaited> = [{ suggested_correction_id: 1, flyer_item_id: 1, user_id: '1', correction_type: 'price', suggested_value: 'New Price', status: 'pending', created_at: new Date().toISOString() }]; - vi.mocked(mockedDb.getSuggestedCorrections).mockResolvedValue(mockCorrections); + vi.mocked(adminDb.getSuggestedCorrections).mockResolvedValue(mockCorrections); // Act const response = await supertest(app).get('/api/admin/corrections'); @@ -120,7 +127,7 @@ describe('Admin Routes (/api/admin)', () => { // Assert expect(response.status).toBe(200); expect(response.body).toEqual(mockCorrections); - expect(mockedDb.getSuggestedCorrections).toHaveBeenCalledTimes(1); + expect(adminDb.getSuggestedCorrections).toHaveBeenCalledTimes(1); }); describe('GET /brands', () => { @@ -130,7 +137,7 @@ describe('Admin Routes (/api/admin)', () => { { brand_id: 1, name: 'Brand A', logo_url: '/path/a.png' }, { brand_id: 2, name: 'Brand B', logo_url: '/path/b.png' }, ]; - vi.mocked(mockedDb.getAllBrands).mockResolvedValue(mockBrands); + vi.mocked(flyerDb.getAllBrands).mockResolvedValue(mockBrands); // Act const response = await supertest(app).get('/api/admin/brands'); @@ -138,11 +145,11 @@ describe('Admin Routes (/api/admin)', () => { // Assert expect(response.status).toBe(200); expect(response.body).toEqual(mockBrands); - expect(mockedDb.getAllBrands).toHaveBeenCalledTimes(1); + expect(flyerDb.getAllBrands).toHaveBeenCalledTimes(1); }); it('should return a 500 error if the database call fails', async () => { - mockedDb.getAllBrands.mockRejectedValue(new Error('Failed to fetch brands')); + vi.mocked(flyerDb.getAllBrands).mockRejectedValue(new Error('Failed to fetch brands')); const response = await supertest(app).get('/api/admin/brands'); expect(response.status).toBe(500); }); @@ -158,7 +165,7 @@ describe('Admin Routes (/api/admin)', () => { storeCount: 12, pendingCorrectionCount: 5, }; - mockedDb.getApplicationStats.mockResolvedValue(mockStats); + vi.mocked(adminDb.getApplicationStats).mockResolvedValue(mockStats); // Act const response = await supertest(app).get('/api/admin/stats'); @@ -166,11 +173,11 @@ describe('Admin Routes (/api/admin)', () => { // Assert expect(response.status).toBe(200); expect(response.body).toEqual(mockStats); - expect(mockedDb.getApplicationStats).toHaveBeenCalledTimes(1); + expect(adminDb.getApplicationStats).toHaveBeenCalledTimes(1); }); it('should return a 500 error if the database call fails', async () => { - mockedDb.getApplicationStats.mockRejectedValue(new Error('Failed to fetch stats')); + vi.mocked(adminDb.getApplicationStats).mockRejectedValue(new Error('Failed to fetch stats')); const response = await supertest(app).get('/api/admin/stats'); expect(response.status).toBe(500); }); @@ -183,7 +190,7 @@ describe('Admin Routes (/api/admin)', () => { { date: '2024-01-01', new_users: 5, new_flyers: 10 }, { date: '2024-01-02', new_users: 3, new_flyers: 8 }, ] as Awaited>; - vi.mocked(mockedDb.getDailyStatsForLast30Days).mockResolvedValue(mockDailyStats); + vi.mocked(adminDb.getDailyStatsForLast30Days).mockResolvedValue(mockDailyStats); // Act const response = await supertest(app).get('/api/admin/stats/daily'); @@ -191,11 +198,11 @@ describe('Admin Routes (/api/admin)', () => { // Assert expect(response.status).toBe(200); expect(response.body).toEqual(mockDailyStats); - expect(mockedDb.getDailyStatsForLast30Days).toHaveBeenCalledTimes(1); + expect(adminDb.getDailyStatsForLast30Days).toHaveBeenCalledTimes(1); }); it('should return a 500 error if the database call fails', async () => { - mockedDb.getDailyStatsForLast30Days.mockRejectedValue(new Error('Failed to fetch daily stats')); + vi.mocked(adminDb.getDailyStatsForLast30Days).mockRejectedValue(new Error('Failed to fetch daily stats')); const response = await supertest(app).get('/api/admin/stats/daily'); expect(response.status).toBe(500); }); @@ -208,7 +215,7 @@ describe('Admin Routes (/api/admin)', () => { { unmatched_flyer_item_id: 1, status: 'pending', created_at: new Date().toISOString(), flyer_item_id: 101, flyer_item_name: 'Ketchup Chips', price_display: '$3.00', flyer_id: 1, store_name: 'Test Store' }, { unmatched_flyer_item_id: 2, status: 'pending', created_at: new Date().toISOString(), flyer_item_id: 102, flyer_item_name: 'Mystery Soda', price_display: '2 for $4.00', flyer_id: 1, store_name: 'Test Store' }, ]; - vi.mocked(mockedDb.getUnmatchedFlyerItems).mockResolvedValue(mockUnmatchedItems); + vi.mocked(adminDb.getUnmatchedFlyerItems).mockResolvedValue(mockUnmatchedItems); // Act const response = await supertest(app).get('/api/admin/unmatched-items'); @@ -216,11 +223,11 @@ describe('Admin Routes (/api/admin)', () => { // Assert expect(response.status).toBe(200); expect(response.body).toEqual(mockUnmatchedItems); - expect(mockedDb.getUnmatchedFlyerItems).toHaveBeenCalledTimes(1); + expect(adminDb.getUnmatchedFlyerItems).toHaveBeenCalledTimes(1); }); it('should return a 500 error if the database call fails', async () => { - mockedDb.getUnmatchedFlyerItems.mockRejectedValue(new Error('Failed to fetch unmatched items')); + vi.mocked(adminDb.getUnmatchedFlyerItems).mockRejectedValue(new Error('Failed to fetch unmatched items')); const response = await supertest(app).get('/api/admin/unmatched-items'); expect(response.status).toBe(500); }); @@ -230,7 +237,7 @@ describe('Admin Routes (/api/admin)', () => { it('should approve a correction and return a success message', async () => { // Arrange const correctionId = 123; - mockedDb.approveCorrection.mockResolvedValue(undefined); // Mock the DB call to succeed + vi.mocked(adminDb.approveCorrection).mockResolvedValue(undefined); // Mock the DB call to succeed // Act const response = await supertest(app).post(`/api/admin/corrections/${correctionId}/approve`); @@ -238,14 +245,14 @@ describe('Admin Routes (/api/admin)', () => { // Assert expect(response.status).toBe(200); expect(response.body).toEqual({ message: 'Correction approved successfully.' }); - expect(mockedDb.approveCorrection).toHaveBeenCalledTimes(1); - expect(mockedDb.approveCorrection).toHaveBeenCalledWith(correctionId); + expect(adminDb.approveCorrection).toHaveBeenCalledTimes(1); + expect(adminDb.approveCorrection).toHaveBeenCalledWith(correctionId); }); it('should return a 500 error if the database call fails', async () => { // Arrange const correctionId = 456; - mockedDb.approveCorrection.mockRejectedValue(new Error('Database failure')); + vi.mocked(adminDb.approveCorrection).mockRejectedValue(new Error('Database failure')); // Act const response = await supertest(app).post(`/api/admin/corrections/${correctionId}/approve`); @@ -261,7 +268,7 @@ describe('Admin Routes (/api/admin)', () => { // Assert expect(response.status).toBe(400); expect(response.body.message).toBe('Invalid correction ID provided.'); - expect(mockedDb.approveCorrection).not.toHaveBeenCalled(); + expect(adminDb.approveCorrection).not.toHaveBeenCalled(); }); }); @@ -269,7 +276,7 @@ describe('Admin Routes (/api/admin)', () => { it('should reject a correction and return a success message', async () => { // Arrange const correctionId = 789; - mockedDb.rejectCorrection.mockResolvedValue(undefined); // Mock the DB call to succeed + vi.mocked(adminDb.rejectCorrection).mockResolvedValue(undefined); // Mock the DB call to succeed // Act const response = await supertest(app).post(`/api/admin/corrections/${correctionId}/reject`); @@ -277,14 +284,14 @@ describe('Admin Routes (/api/admin)', () => { // Assert expect(response.status).toBe(200); expect(response.body).toEqual({ message: 'Correction rejected successfully.' }); - expect(mockedDb.rejectCorrection).toHaveBeenCalledTimes(1); - expect(mockedDb.rejectCorrection).toHaveBeenCalledWith(correctionId); + expect(adminDb.rejectCorrection).toHaveBeenCalledTimes(1); + expect(adminDb.rejectCorrection).toHaveBeenCalledWith(correctionId); }); it('should return a 500 error if the database call fails', async () => { // Arrange const correctionId = 987; - mockedDb.rejectCorrection.mockRejectedValue(new Error('Database failure')); + vi.mocked(adminDb.rejectCorrection).mockRejectedValue(new Error('Database failure')); // Act const response = await supertest(app).post(`/api/admin/corrections/${correctionId}/reject`); @@ -300,7 +307,7 @@ describe('Admin Routes (/api/admin)', () => { const correctionId = 101; const requestBody = { suggested_value: 'A new corrected value' }; const mockUpdatedCorrection: Awaited> = { suggested_correction_id: correctionId, flyer_item_id: 1, user_id: '1', correction_type: 'price', status: 'pending', created_at: new Date().toISOString(), ...requestBody }; - vi.mocked(mockedDb.updateSuggestedCorrection).mockResolvedValue(mockUpdatedCorrection); + vi.mocked(adminDb.updateSuggestedCorrection).mockResolvedValue(mockUpdatedCorrection); // Act: Use .send() to include a request body const response = await supertest(app) @@ -310,8 +317,8 @@ describe('Admin Routes (/api/admin)', () => { // Assert expect(response.status).toBe(200); expect(response.body).toEqual(mockUpdatedCorrection); - expect(mockedDb.updateSuggestedCorrection).toHaveBeenCalledTimes(1); - expect(mockedDb.updateSuggestedCorrection).toHaveBeenCalledWith(correctionId, requestBody.suggested_value); + expect(adminDb.updateSuggestedCorrection).toHaveBeenCalledTimes(1); + expect(adminDb.updateSuggestedCorrection).toHaveBeenCalledWith(correctionId, requestBody.suggested_value); }); it('should return a 400 error if suggested_value is missing from the body', async () => { @@ -327,7 +334,7 @@ describe('Admin Routes (/api/admin)', () => { expect(response.status).toBe(400); expect(response.body.message).toBe('A new suggested_value is required.'); // Ensure the database was not called - expect(mockedDb.updateSuggestedCorrection).not.toHaveBeenCalled(); + expect(adminDb.updateSuggestedCorrection).not.toHaveBeenCalled(); }); it('should return a 404 error if the correction to update is not found', async () => { @@ -335,7 +342,7 @@ describe('Admin Routes (/api/admin)', () => { const correctionId = 999; // A non-existent ID const requestBody = { suggested_value: 'This will fail' }; // Mock the DB function to throw a "not found" error, simulating the real DB behavior. - mockedDb.updateSuggestedCorrection.mockRejectedValue(new Error(`Correction with ID ${correctionId} not found.`)); + vi.mocked(adminDb.updateSuggestedCorrection).mockRejectedValue(new Error(`Correction with ID ${correctionId} not found.`)); // Act const response = await supertest(app) @@ -352,7 +359,7 @@ describe('Admin Routes (/api/admin)', () => { it('should upload a logo and update the brand', async () => { // Arrange const brandId = 55; - mockedDb.updateBrandLogo.mockResolvedValue(undefined); // Mock the DB call + vi.mocked(adminDb.updateBrandLogo).mockResolvedValue(undefined); // Mock the DB call // Create a dummy file for supertest to attach. // supertest needs a real file path to stream from. @@ -371,8 +378,8 @@ describe('Admin Routes (/api/admin)', () => { expect(response.body.logoUrl).toMatch(/^\/assets\/logoImage-/); // Check for the generated URL format // Verify the database was updated with the correct brand ID and a generated URL - expect(mockedDb.updateBrandLogo).toHaveBeenCalledTimes(1); - expect(mockedDb.updateBrandLogo).toHaveBeenCalledWith(brandId, expect.stringContaining('/assets/')); + expect(adminDb.updateBrandLogo).toHaveBeenCalledTimes(1); + expect(adminDb.updateBrandLogo).toHaveBeenCalledWith(brandId, expect.stringContaining('/assets/')); // Clean up the dummy file await fs.unlink(dummyFilePath); @@ -409,7 +416,7 @@ describe('Admin Routes (/api/admin)', () => { const recipeId = 201; const requestBody = { status: 'public' as const }; const mockUpdatedRecipe: Awaited> = { recipe_id: recipeId, status: 'public', name: 'Test Recipe', avg_rating: 0, rating_count: 0, fork_count: 0, created_at: new Date().toISOString() }; - vi.mocked(mockedDb.updateRecipeStatus).mockResolvedValue(mockUpdatedRecipe); + vi.mocked(adminDb.updateRecipeStatus).mockResolvedValue(mockUpdatedRecipe); // Act const response = await supertest(app) @@ -419,8 +426,8 @@ describe('Admin Routes (/api/admin)', () => { // Assert expect(response.status).toBe(200); expect(response.body).toEqual(mockUpdatedRecipe); - expect(mockedDb.updateRecipeStatus).toHaveBeenCalledTimes(1); - expect(mockedDb.updateRecipeStatus).toHaveBeenCalledWith(recipeId, 'public'); + expect(adminDb.updateRecipeStatus).toHaveBeenCalledTimes(1); + expect(adminDb.updateRecipeStatus).toHaveBeenCalledWith(recipeId, 'public'); }); it('should return a 400 error for an invalid status value', async () => { @@ -436,7 +443,7 @@ describe('Admin Routes (/api/admin)', () => { // Assert expect(response.status).toBe(400); expect(response.body.message).toBe('A valid status (private, pending_review, public, rejected) is required.'); - expect(mockedDb.updateRecipeStatus).not.toHaveBeenCalled(); + expect(adminDb.updateRecipeStatus).not.toHaveBeenCalled(); }); }); @@ -446,7 +453,7 @@ describe('Admin Routes (/api/admin)', () => { const commentId = 301; const requestBody = { status: 'hidden' as const }; const mockUpdatedComment: Awaited> = { recipe_comment_id: commentId, recipe_id: 1, user_id: '1', status: 'hidden', content: 'Test Comment', created_at: new Date().toISOString() }; - vi.mocked(mockedDb.updateRecipeCommentStatus).mockResolvedValue(mockUpdatedComment); + vi.mocked(adminDb.updateRecipeCommentStatus).mockResolvedValue(mockUpdatedComment); // Act const response = await supertest(app) @@ -456,15 +463,15 @@ describe('Admin Routes (/api/admin)', () => { // Assert expect(response.status).toBe(200); expect(response.body).toEqual(mockUpdatedComment); - expect(mockedDb.updateRecipeCommentStatus).toHaveBeenCalledTimes(1); - expect(mockedDb.updateRecipeCommentStatus).toHaveBeenCalledWith(commentId, 'hidden'); + expect(adminDb.updateRecipeCommentStatus).toHaveBeenCalledTimes(1); + expect(adminDb.updateRecipeCommentStatus).toHaveBeenCalledWith(commentId, 'hidden'); }); it('should return a 400 error for an invalid status value', async () => { const response = await supertest(app).put('/api/admin/comments/302/status').send({ status: 'invalid' }); expect(response.status).toBe(400); expect(response.body.message).toBe('A valid status (visible, hidden, reported) is required.'); - expect(mockedDb.updateRecipeCommentStatus).not.toHaveBeenCalled(); + expect(adminDb.updateRecipeCommentStatus).not.toHaveBeenCalled(); }); }); @@ -475,7 +482,7 @@ describe('Admin Routes (/api/admin)', () => { { user_id: '1', email: 'user1@test.com', role: 'user', created_at: new Date().toISOString(), full_name: 'User One', avatar_url: null }, { user_id: '2', email: 'user2@test.com', role: 'admin', created_at: new Date().toISOString(), full_name: 'Admin Two', avatar_url: null }, ]; - vi.mocked(mockedDb.getAllUsers).mockResolvedValue(mockUsers); + vi.mocked(adminDb.getAllUsers).mockResolvedValue(mockUsers); // Act const response = await supertest(app).get('/api/admin/users'); @@ -483,11 +490,11 @@ describe('Admin Routes (/api/admin)', () => { // Assert expect(response.status).toBe(200); expect(response.body).toEqual(mockUsers); - expect(mockedDb.getAllUsers).toHaveBeenCalledTimes(1); + expect(adminDb.getAllUsers).toHaveBeenCalledTimes(1); }); it('should return a 500 error if the database call fails', async () => { - vi.mocked(mockedDb.getAllUsers).mockRejectedValue(new Error('Failed to fetch users')); + vi.mocked(adminDb.getAllUsers).mockRejectedValue(new Error('Failed to fetch users')); const response = await supertest(app).get('/api/admin/users'); expect(response.status).toBe(500); }); @@ -497,7 +504,7 @@ describe('Admin Routes (/api/admin)', () => { it('should return a list of activity logs with default pagination', async () => { // Arrange const mockLogs: Awaited> = [{ activity_log_id: 1, action: 'user_registered', display_text: 'test', created_at: new Date().toISOString(), user_id: '1', details: { full_name: 'test', user_avatar_url: 'test', user_full_name: 'test' } }]; - vi.mocked(mockedDb.getActivityLog).mockResolvedValue(mockLogs); + vi.mocked(adminDb.getActivityLog).mockResolvedValue(mockLogs); // Act const response = await supertest(app).get('/api/admin/activity-log'); @@ -505,34 +512,34 @@ describe('Admin Routes (/api/admin)', () => { // Assert expect(response.status).toBe(200); expect(response.body).toEqual(mockLogs); - expect(mockedDb.getActivityLog).toHaveBeenCalledTimes(1); + expect(adminDb.getActivityLog).toHaveBeenCalledTimes(1); // Check that default pagination values were used // This makes the test more robust by verifying the correct parameters were passed. - expect(mockedDb.getActivityLog).toHaveBeenCalledWith(50, 0); + expect(adminDb.getActivityLog).toHaveBeenCalledWith(50, 0); }); it('should use limit and offset query parameters when provided', async () => { - mockedDb.getActivityLog.mockResolvedValue([]); + vi.mocked(adminDb.getActivityLog).mockResolvedValue([]); await supertest(app).get('/api/admin/activity-log?limit=10&offset=20'); - expect(mockedDb.getActivityLog).toHaveBeenCalledTimes(1); - expect(mockedDb.getActivityLog).toHaveBeenCalledWith(10, 20); + expect(adminDb.getActivityLog).toHaveBeenCalledTimes(1); + expect(adminDb.getActivityLog).toHaveBeenCalledWith(10, 20); }); it('should handle invalid pagination parameters gracefully', async () => { - mockedDb.getActivityLog.mockResolvedValue([]); + vi.mocked(adminDb.getActivityLog).mockResolvedValue([]); // Act: Send non-numeric query parameters await supertest(app).get('/api/admin/activity-log?limit=abc&offset=xyz'); // Assert: The route should fall back to the default values - expect(mockedDb.getActivityLog).toHaveBeenCalledWith(50, 0); + expect(adminDb.getActivityLog).toHaveBeenCalledWith(50, 0); }); it('should return a 500 error if the database call fails', async () => { // Arrange - mockedDb.getActivityLog.mockRejectedValue(new Error('DB connection error')); + vi.mocked(adminDb.getActivityLog).mockRejectedValue(new Error('DB connection error')); // Act const response = await supertest(app).get('/api/admin/activity-log'); @@ -546,7 +553,7 @@ describe('Admin Routes (/api/admin)', () => { it('should fetch a single user successfully', async () => { // Arrange const mockUser: Awaited> = { user_id: 'user-123', role: 'user', points: 0 }; - vi.mocked(mockedDb.findUserProfileById).mockResolvedValue(mockUser); + vi.mocked(db.findUserProfileById).mockResolvedValue(mockUser); // Act const response = await supertest(app).get('/api/admin/users/user-123'); @@ -554,12 +561,12 @@ describe('Admin Routes (/api/admin)', () => { // Assert expect(response.status).toBe(200); expect(response.body).toEqual(mockUser); - expect(mockedDb.findUserProfileById).toHaveBeenCalledWith('user-123'); + expect(db.findUserProfileById).toHaveBeenCalledWith('user-123'); }); it('should return 404 for a non-existent user', async () => { // Arrange - vi.mocked(mockedDb.findUserProfileById).mockResolvedValue(undefined); + vi.mocked(db.findUserProfileById).mockResolvedValue(undefined); // Act const response = await supertest(app).get('/api/admin/users/non-existent-id'); @@ -574,7 +581,7 @@ describe('Admin Routes (/api/admin)', () => { it('should update a user role successfully', async () => { // Arrange const updatedUser: Awaited> = { user_id: 'user-to-update', email: 'test@test.com' }; - vi.mocked(mockedDb.updateUserRole).mockResolvedValue(updatedUser); + vi.mocked(adminDb.updateUserRole).mockResolvedValue(updatedUser); // Act const response = await supertest(app) @@ -584,11 +591,11 @@ describe('Admin Routes (/api/admin)', () => { // Assert expect(response.status).toBe(200); expect(response.body).toEqual(updatedUser); - expect(mockedDb.updateUserRole).toHaveBeenCalledWith('user-to-update', 'admin'); + expect(adminDb.updateUserRole).toHaveBeenCalledWith('user-to-update', 'admin'); }); it('should return 404 for a non-existent user', async () => { - vi.mocked(mockedDb.updateUserRole).mockRejectedValue(new Error('User with ID non-existent not found.')); + vi.mocked(adminDb.updateUserRole).mockRejectedValue(new Error('User with ID non-existent not found.')); const response = await supertest(app).put('/api/admin/users/non-existent').send({ role: 'user' }); expect(response.status).toBe(404); }); @@ -602,10 +609,10 @@ describe('Admin Routes (/api/admin)', () => { describe('DELETE /users/:id', () => { it('should successfully delete a user', async () => { - vi.mocked(mockedDb.deleteUserById).mockResolvedValue(undefined); + vi.mocked(db.deleteUserById).mockResolvedValue(undefined); const response = await supertest(app).delete('/api/admin/users/user-to-delete'); expect(response.status).toBe(204); - expect(mockedDb.deleteUserById).toHaveBeenCalledWith('user-to-delete'); + expect(db.deleteUserById).toHaveBeenCalledWith('user-to-delete'); }); it('should prevent an admin from deleting their own account', async () => { @@ -613,7 +620,7 @@ describe('Admin Routes (/api/admin)', () => { const response = await supertest(app).delete('/api/admin/users/admin-user-id'); expect(response.status).toBe(400); expect(response.body.message).toBe('Admins cannot delete their own account.'); - expect(mockedDb.deleteUserById).not.toHaveBeenCalled(); + expect(db.deleteUserById).not.toHaveBeenCalled(); }); }); }); diff --git a/src/routes/ai.test.ts b/src/routes/ai.test.ts index c68c9543..dabc7433 100644 --- a/src/routes/ai.test.ts +++ b/src/routes/ai.test.ts @@ -5,14 +5,16 @@ import express, { type Request, type Response, type NextFunction } from 'express import path from 'node:path'; import aiRouter from './ai'; import { UserProfile } from '../types'; -import * as db from '../services/db/index.db'; +import * as flyerDb from '../services/db/flyer.db'; +import * as adminDb from '../services/db/admin.db'; // Mock the AI service to avoid making real AI calls vi.mock('../services/aiService.server'); -// Mock the entire db service, as the /flyers/process route uses it. -vi.mock('../services/db'); // Keep this mock, as db is used by the route -const mockedDb = db as Mocked; +// Mock the specific DB modules used by the AI router. +vi.mock('../services/db/flyer.db'); +vi.mock('../services/db/admin.db'); +const mockedDb = { ...flyerDb, ...adminDb } as Mocked; // Mock the logger to keep test output clean vi.mock('../services/logger.server', () => ({ @@ -66,16 +68,16 @@ describe('AI Routes (/api/ai)', () => { it('should save a flyer and return 201 on success', async () => { // Arrange - mockedDb.findFlyerByChecksum.mockResolvedValue(undefined); // No duplicate - mockedDb.createFlyerAndItems.mockResolvedValue({ + vi.mocked(flyerDb.findFlyerByChecksum).mockResolvedValue(undefined); // No duplicate + vi.mocked(flyerDb.createFlyerAndItems).mockResolvedValue({ flyer_id: 1, created_at: new Date().toISOString(), file_name: mockDataPayload.originalFileName, image_url: '/assets/some-image.jpg', - ...mockDataPayload.extractedData, + ...(mockDataPayload.extractedData as any), item_count: 0, // Add missing property to satisfy the Flyer type }); - mockedDb.logActivity.mockResolvedValue(); + vi.mocked(adminDb.logActivity).mockResolvedValue(); // Act const response = await supertest(app) @@ -86,12 +88,12 @@ describe('AI Routes (/api/ai)', () => { // Assert expect(response.status).toBe(201); expect(response.body.message).toBe('Flyer processed and saved successfully.'); - expect(mockedDb.createFlyerAndItems).toHaveBeenCalledTimes(1); + expect(flyerDb.createFlyerAndItems).toHaveBeenCalledTimes(1); }); it('should return 409 Conflict if flyer checksum already exists', async () => { // Arrange - mockedDb.findFlyerByChecksum.mockResolvedValue({ flyer_id: 99 } as Awaited>); // Duplicate found + vi.mocked(flyerDb.findFlyerByChecksum).mockResolvedValue({ flyer_id: 99 } as Awaited>); // Duplicate found // Act const response = await supertest(app) @@ -102,7 +104,7 @@ describe('AI Routes (/api/ai)', () => { // Assert expect(response.status).toBe(409); expect(response.body.message).toBe('This flyer has already been processed.'); - expect(mockedDb.createFlyerAndItems).not.toHaveBeenCalled(); + expect(flyerDb.createFlyerAndItems).not.toHaveBeenCalled(); }); it('should return 400 if no image file is provided', async () => { @@ -124,7 +126,7 @@ describe('AI Routes (/api/ai)', () => { expect(response.status).toBe(400); expect(response.body.message).toBe('Invalid request: extractedData is required.'); - expect(mockedDb.createFlyerAndItems).not.toHaveBeenCalled(); + expect(flyerDb.createFlyerAndItems).not.toHaveBeenCalled(); }); it('should accept payload when extractedData.items is missing and save with empty items', async () => { @@ -135,13 +137,13 @@ describe('AI Routes (/api/ai)', () => { extractedData: { store_name: 'Partial Store' } // no items key }; - mockedDb.findFlyerByChecksum.mockResolvedValue(undefined); - mockedDb.createFlyerAndItems.mockResolvedValue({ + vi.mocked(flyerDb.findFlyerByChecksum).mockResolvedValue(undefined); + vi.mocked(flyerDb.createFlyerAndItems).mockResolvedValue({ flyer_id: 2, created_at: new Date().toISOString(), file_name: partialPayload.originalFileName, image_url: '/flyer-images/flyer2.jpg', - item_count: 0, // Add missing required property + item_count: 0, }); const response = await supertest(app) @@ -150,9 +152,9 @@ describe('AI Routes (/api/ai)', () => { .attach('flyerImage', imagePath); expect(response.status).toBe(201); - expect(mockedDb.createFlyerAndItems).toHaveBeenCalledTimes(1); + expect(flyerDb.createFlyerAndItems).toHaveBeenCalledTimes(1); // verify the items array passed to DB was an empty array - const callArgs = mockedDb.createFlyerAndItems.mock.calls[0]?.[1]; + const callArgs = vi.mocked(flyerDb.createFlyerAndItems).mock.calls[0]?.[1]; expect(callArgs).toBeDefined(); expect(Array.isArray(callArgs)).toBe(true); // use non-null assertion for the runtime-checked variable so TypeScript is satisfied @@ -166,13 +168,13 @@ describe('AI Routes (/api/ai)', () => { extractedData: { items: [] } // store_name missing }; - mockedDb.findFlyerByChecksum.mockResolvedValue(undefined); - mockedDb.createFlyerAndItems.mockResolvedValue({ + vi.mocked(flyerDb.findFlyerByChecksum).mockResolvedValue(undefined); + vi.mocked(flyerDb.createFlyerAndItems).mockResolvedValue({ flyer_id: 3, created_at: new Date().toISOString(), file_name: payloadNoStore.originalFileName, image_url: '/flyer-images/flyer3.jpg', - item_count: 0, // Add missing required property + item_count: 0, }); const response = await supertest(app) @@ -181,9 +183,9 @@ describe('AI Routes (/api/ai)', () => { .attach('flyerImage', imagePath); expect(response.status).toBe(201); - expect(mockedDb.createFlyerAndItems).toHaveBeenCalledTimes(1); + expect(flyerDb.createFlyerAndItems).toHaveBeenCalledTimes(1); // verify the flyerData.store_name passed to DB was the fallback string - const flyerDataArg = mockedDb.createFlyerAndItems.mock.calls[0][0]; + const flyerDataArg = vi.mocked(flyerDb.createFlyerAndItems).mock.calls[0][0]; expect(flyerDataArg.store_name).toContain('Unknown Store'); }); }); diff --git a/src/routes/auth.test.ts b/src/routes/auth.test.ts index 71e9dd3e..5f5a67d2 100644 --- a/src/routes/auth.test.ts +++ b/src/routes/auth.test.ts @@ -5,13 +5,15 @@ import express, { Request } from 'express'; import cookieParser from 'cookie-parser'; import * as bcrypt from 'bcrypt'; import authRouter from './auth'; -import * as db from '../services/db/index.db'; +import * as userDb from '../services/db/user.db'; +import * as adminDb from '../services/db/admin.db'; import { UserProfile } from '../types'; // 1. Mock the Service Layer directly. // This decouples the route tests from the SQL implementation details. -vi.mock('../services/db'); - +vi.mock('../services/db/user.db'); +vi.mock('../services/db/admin.db'); +const mockedDb = { ...userDb, ...adminDb } as Mocked; // Mock the logger to keep test output clean vi.mock('../services/logger.server', () => ({ logger: { @@ -88,10 +90,10 @@ describe('Auth Routes (/api/auth)', () => { preferences: {} }; - vi.mocked(db.findUserByEmail).mockResolvedValue(undefined); // No existing user - vi.mocked(db.createUser).mockResolvedValue(mockNewUser); - vi.mocked(db.saveRefreshToken).mockResolvedValue(undefined); - vi.mocked(db.logActivity).mockResolvedValue(undefined); + vi.mocked(userDb.findUserByEmail).mockResolvedValue(undefined); // No existing user + vi.mocked(userDb.createUser).mockResolvedValue(mockNewUser); + vi.mocked(userDb.saveRefreshToken).mockResolvedValue(undefined); + vi.mocked(adminDb.logActivity).mockResolvedValue(undefined); // Act const response = await supertest(app) @@ -125,7 +127,7 @@ describe('Auth Routes (/api/auth)', () => { it('should reject registration if the email already exists', async () => { // Arrange: Mock that the user exists - vi.mocked(db.findUserByEmail).mockResolvedValue({ + vi.mocked(userDb.findUserByEmail).mockResolvedValue({ user_id: 'existing', email: newUserEmail, password_hash: 'some_hash', @@ -158,7 +160,7 @@ describe('Auth Routes (/api/auth)', () => { it('should successfully log in a user and return a token and cookie', async () => { // Arrange: const loginCredentials = { email: 'test@test.com', password: 'password123' }; - vi.mocked(db.saveRefreshToken).mockResolvedValue(undefined); + vi.mocked(userDb.saveRefreshToken).mockResolvedValue(undefined); // Act const response = await supertest(app) @@ -202,14 +204,14 @@ describe('Auth Routes (/api/auth)', () => { describe('POST /forgot-password', () => { it('should send a reset link if the user exists', async () => { // Arrange - vi.mocked(db.findUserByEmail).mockResolvedValue({ + vi.mocked(userDb.findUserByEmail).mockResolvedValue({ user_id: 'user-123', email: 'test@test.com', password_hash: 'some_hash', failed_login_attempts: 0, last_failed_login: null, }); - vi.mocked(db.createPasswordResetToken).mockResolvedValue(undefined); + vi.mocked(userDb.createPasswordResetToken).mockResolvedValue(undefined); // Act const response = await supertest(app) @@ -225,7 +227,7 @@ describe('Auth Routes (/api/auth)', () => { it('should return a generic success message even if the user does not exist', async () => { // Arrange - vi.mocked(db.findUserByEmail).mockResolvedValue(undefined); + vi.mocked(userDb.findUserByEmail).mockResolvedValue(undefined); // Act const response = await supertest(app) @@ -242,11 +244,11 @@ describe('Auth Routes (/api/auth)', () => { it('should reset the password with a valid token and strong password', async () => { // Arrange const tokenRecord = { user_id: 'user-123', token_hash: 'hashed-token', expires_at: new Date(Date.now() + 3600000) }; - vi.mocked(db.getValidResetTokens).mockResolvedValue([tokenRecord]); + vi.mocked(userDb.getValidResetTokens).mockResolvedValue([tokenRecord]); vi.mocked(bcrypt.compare).mockResolvedValue(true as never); // Token matches - vi.mocked(db.updateUserPassword).mockResolvedValue(undefined); - vi.mocked(db.deleteResetToken).mockResolvedValue(undefined); - vi.mocked(db.logActivity).mockResolvedValue(undefined); + vi.mocked(userDb.updateUserPassword).mockResolvedValue(undefined); + vi.mocked(userDb.deleteResetToken).mockResolvedValue(undefined); + vi.mocked(adminDb.logActivity).mockResolvedValue(undefined); // Act const response = await supertest(app) @@ -260,7 +262,7 @@ describe('Auth Routes (/api/auth)', () => { it('should reject with an invalid or expired token', async () => { // Arrange - vi.mocked(db.getValidResetTokens).mockResolvedValue([]); // No valid tokens found + vi.mocked(userDb.getValidResetTokens).mockResolvedValue([]); // No valid tokens found // Act const response = await supertest(app) @@ -283,7 +285,7 @@ describe('Auth Routes (/api/auth)', () => { failed_login_attempts: 0, last_failed_login: null, }; - vi.mocked(db.findUserByRefreshToken).mockResolvedValue(mockUser); + vi.mocked(userDb.findUserByRefreshToken).mockResolvedValue(mockUser); // Act const response = await supertest(app) @@ -302,7 +304,7 @@ describe('Auth Routes (/api/auth)', () => { }); it('should return 403 if refresh token is invalid', async () => { - vi.mocked(db.findUserByRefreshToken).mockResolvedValue(undefined); + vi.mocked(userDb.findUserByRefreshToken).mockResolvedValue(undefined); const response = await supertest(app) .post('/api/auth/refresh-token') diff --git a/src/routes/budget.test.ts b/src/routes/budget.test.ts index 6938a1f3..03455b7a 100644 --- a/src/routes/budget.test.ts +++ b/src/routes/budget.test.ts @@ -3,12 +3,12 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'; import supertest from 'supertest'; import express, { Request, Response, NextFunction } from 'express'; import budgetRouter from './budget'; -import * as db from '../services/db/index.db'; +import * as budgetDb from '../services/db/budget.db'; import { UserProfile, Budget, SpendingByCategory } from '../types'; // 1. Mock the Service Layer directly. // This decouples the route tests from the database logic. -vi.mock('../services/db'); +vi.mock('../services/db/budget.db'); // Mock the logger to keep test output clean vi.mock('../services/logger.server', () => ({ @@ -99,7 +99,7 @@ describe('Budget Routes (/api/budgets)', () => { it('should return a list of budgets for the user', async () => { const mockBudgets: Budget[] = [{ budget_id: 1, user_id: 'user-123', name: 'Groceries', amount_cents: 50000, period: 'monthly', start_date: '2024-01-01' }]; // Mock the service function directly - vi.mocked(db.getBudgetsForUser).mockResolvedValue(mockBudgets); + vi.mocked(budgetDb.getBudgetsForUser).mockResolvedValue(mockBudgets); const response = await supertest(app).get('/api/budgets'); @@ -114,7 +114,7 @@ describe('Budget Routes (/api/budgets)', () => { const newBudgetData = { name: 'Entertainment', amount_cents: 10000, period: 'monthly' as const, start_date: '2024-01-01' }; const mockCreatedBudget: Budget = { budget_id: 2, user_id: 'user-123', ...newBudgetData }; // Mock the service function - vi.mocked(db.createBudget).mockResolvedValue(mockCreatedBudget); + vi.mocked(budgetDb.createBudget).mockResolvedValue(mockCreatedBudget); const response = await supertest(app).post('/api/budgets').send(newBudgetData); @@ -128,7 +128,7 @@ describe('Budget Routes (/api/budgets)', () => { const budgetUpdates = { amount_cents: 60000 }; const mockUpdatedBudget: Budget = { budget_id: 1, user_id: 'user-123', name: 'Groceries', amount_cents: 60000, period: 'monthly', start_date: '2024-01-01' }; // Mock the service function - vi.mocked(db.updateBudget).mockResolvedValue(mockUpdatedBudget); + vi.mocked(budgetDb.updateBudget).mockResolvedValue(mockUpdatedBudget); const response = await supertest(app).put('/api/budgets/1').send(budgetUpdates); @@ -140,12 +140,12 @@ describe('Budget Routes (/api/budgets)', () => { describe('DELETE /:id', () => { it('should delete a budget', async () => { // Mock the service function to resolve (void) - vi.mocked(db.deleteBudget).mockResolvedValue(undefined); + vi.mocked(budgetDb.deleteBudget).mockResolvedValue(undefined); const response = await supertest(app).delete('/api/budgets/1'); expect(response.status).toBe(204); - expect(db.deleteBudget).toHaveBeenCalledWith(1, mockUserProfile.user_id); + expect(budgetDb.deleteBudget).toHaveBeenCalledWith(1, mockUserProfile.user_id); }); }); @@ -153,7 +153,7 @@ describe('Budget Routes (/api/budgets)', () => { it('should return spending analysis data for a valid date range', async () => { const mockSpendingData: SpendingByCategory[] = [{ category_id: 1, category_name: 'Produce', total_spent_cents: 12345 }]; // Mock the service function - vi.mocked(db.getSpendingByCategory).mockResolvedValue(mockSpendingData); + vi.mocked(budgetDb.getSpendingByCategory).mockResolvedValue(mockSpendingData); const response = await supertest(app).get('/api/budgets/spending-analysis?startDate=2024-01-01&endDate=2024-01-31'); @@ -170,7 +170,7 @@ describe('Budget Routes (/api/budgets)', () => { it('should return 500 if the database call fails', async () => { // Mock the service function to throw - vi.mocked(db.getSpendingByCategory).mockRejectedValue(new Error('DB Error')); + vi.mocked(budgetDb.getSpendingByCategory).mockRejectedValue(new Error('DB Error')); const response = await supertest(app).get('/api/budgets/spending-analysis?startDate=2024-01-01&endDate=2024-01-31'); diff --git a/src/routes/gamification.test.ts b/src/routes/gamification.test.ts index d9b9f803..d7ae579f 100644 --- a/src/routes/gamification.test.ts +++ b/src/routes/gamification.test.ts @@ -3,12 +3,12 @@ import { describe, it, expect, vi, beforeEach, type Mocked } from 'vitest'; import supertest from 'supertest'; import express, { Request, Response, NextFunction } from 'express'; import gamificationRouter from './gamification'; -import * as db from '../services/db/index.db'; +import * as gamificationDb from '../services/db/gamification.db'; import { UserProfile, Achievement, UserAchievement } from '../types'; // Mock the entire db service -vi.mock('../services/db'); -const mockedDb = db as Mocked; +vi.mock('../services/db/gamification.db'); +const mockedDb = gamificationDb as Mocked; // Mock the logger to keep test output clean vi.mock('../services/logger.server', () => ({ diff --git a/src/routes/public.routes.test.ts b/src/routes/public.routes.test.ts index c572a0b9..6588c791 100644 --- a/src/routes/public.routes.test.ts +++ b/src/routes/public.routes.test.ts @@ -3,13 +3,18 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'; import supertest from 'supertest'; import express from 'express'; import publicRouter from './public'; // Import the router we want to test -import * as db from '../services/db/index.db'; +import * as connectionDb from '../services/db/connection.db'; +import * as flyerDb from '../services/db/flyer.db'; +import * as recipeDb from '../services/db/recipe.db'; +import * as adminDb from '../services/db/admin.db'; import * as fs from 'fs/promises'; import { Flyer, Recipe } from '../types'; // 1. Mock the Service Layer directly. // This decouples the route tests from the SQL implementation details. -vi.mock('../services/db'); +vi.mock('../services/db/connection.db'); +vi.mock('../services/db/flyer.db'); +vi.mock('../services/db/recipe.db'); // 2. Mock fs/promises. // We provide both named exports and a default export to support different import styles. @@ -59,7 +64,7 @@ describe('Public Routes (/api)', () => { describe('GET /health/db-schema', () => { it('should return 200 OK if all tables exist', async () => { // Mock the service function to return an empty array (no missing tables) - vi.mocked(db.checkTablesExist).mockResolvedValue([]); + vi.mocked(connectionDb.checkTablesExist).mockResolvedValue([]); const response = await supertest(app).get('/api/health/db-schema'); @@ -69,7 +74,7 @@ describe('Public Routes (/api)', () => { it('should return 500 if tables are missing', async () => { // Mock the service function to return missing table names - vi.mocked(db.checkTablesExist).mockResolvedValue(['missing_table']); + vi.mocked(connectionDb.checkTablesExist).mockResolvedValue(['missing_table']); const response = await supertest(app).get('/api/health/db-schema'); @@ -104,7 +109,7 @@ describe('Public Routes (/api)', () => { describe('GET /health/db-pool', () => { it('should return 200 OK for a healthy pool', async () => { // Mock the simple synchronous status function - vi.mocked(db.getPoolStatus).mockReturnValue({ totalCount: 10, idleCount: 5, waitingCount: 0 }); + vi.mocked(connectionDb.getPoolStatus).mockReturnValue({ totalCount: 10, idleCount: 5, waitingCount: 0 }); const response = await supertest(app).get('/api/health/db-pool'); @@ -120,7 +125,7 @@ describe('Public Routes (/api)', () => { { flyer_id: 2, file_name: 'flyer_b.jpg', image_url: '/b.jpg', created_at: new Date().toISOString(), item_count: 20 }, ]; // Mock the service function - vi.mocked(db.getFlyers).mockResolvedValue(mockFlyers); + vi.mocked(flyerDb.getFlyers).mockResolvedValue(mockFlyers); const response = await supertest(app).get('/api/flyers'); @@ -129,7 +134,7 @@ describe('Public Routes (/api)', () => { }); it('should handle database errors gracefully', async () => { - vi.mocked(db.getFlyers).mockRejectedValue(new Error('DB Error')); + vi.mocked(flyerDb.getFlyers).mockRejectedValue(new Error('DB Error')); const response = await supertest(app).get('/api/flyers'); @@ -141,7 +146,7 @@ describe('Public Routes (/api)', () => { describe('GET /master-items', () => { it('should return a list of master items', async () => { const mockItems = [{ master_grocery_item_id: 1, name: 'Milk', created_at: new Date().toISOString() }]; - vi.mocked(db.getAllMasterItems).mockResolvedValue(mockItems); + vi.mocked(flyerDb.getAllMasterItems).mockResolvedValue(mockItems); const response = await supertest(app).get('/api/master-items'); @@ -155,7 +160,7 @@ describe('Public Routes (/api)', () => { const mockFlyerItems = [ { flyer_item_id: 1, flyer_id: 123, item: 'Cheese', price_display: '$5', price_in_cents: 500, created_at: new Date().toISOString(), view_count: 0, click_count: 0, updated_at: new Date().toISOString(), quantity: '500g' } ]; - vi.mocked(db.getFlyerItems).mockResolvedValue(mockFlyerItems); + vi.mocked(flyerDb.getFlyerItems).mockResolvedValue(mockFlyerItems); const response = await supertest(app).get('/api/flyers/123/items'); @@ -169,7 +174,7 @@ describe('Public Routes (/api)', () => { const mockFlyerItems = [ { flyer_item_id: 1, flyer_id: 1, item: 'Bread', price_display: '$2', price_in_cents: 200, created_at: new Date().toISOString(), view_count: 0, click_count: 0, updated_at: new Date().toISOString(), quantity: '1 loaf' } ]; - vi.mocked(db.getFlyerItemsForFlyers).mockResolvedValue(mockFlyerItems); + vi.mocked(flyerDb.getFlyerItemsForFlyers).mockResolvedValue(mockFlyerItems); const response = await supertest(app) .post('/api/flyer-items/batch-fetch') @@ -192,7 +197,7 @@ describe('Public Routes (/api)', () => { const mockRecipes: Recipe[] = [ { recipe_id: 1, name: 'Pasta', description: null, instructions: null, avg_rating: 0, rating_count: 0, fork_count: 0, status: 'public', created_at: new Date().toISOString() }, ]; - vi.mocked(db.getRecipesBySalePercentage).mockResolvedValue(mockRecipes); + vi.mocked(recipeDb.getRecipesBySalePercentage).mockResolvedValue(mockRecipes); const response = await supertest(app).get('/api/recipes/by-sale-percentage?minPercentage=75'); @@ -203,7 +208,7 @@ describe('Public Routes (/api)', () => { describe('POST /flyer-items/batch-count', () => { it('should return the count of items for multiple flyers', async () => { - vi.mocked(db.countFlyerItemsForFlyers).mockResolvedValue(42); + vi.mocked(flyerDb.countFlyerItemsForFlyers).mockResolvedValue(42); const response = await supertest(app) .post('/api/flyer-items/batch-count') @@ -225,7 +230,7 @@ describe('Public Routes (/api)', () => { describe('GET /recipes/by-sale-ingredients', () => { it('should return recipes with default minIngredients', async () => { - vi.mocked(db.getRecipesByMinSaleIngredients).mockResolvedValue([]); + vi.mocked(recipeDb.getRecipesByMinSaleIngredients).mockResolvedValue([]); const response = await supertest(app).get('/api/recipes/by-sale-ingredients'); expect(response.status).toBe(200); }); @@ -242,7 +247,7 @@ describe('Public Routes (/api)', () => { const mockRecipes: Recipe[] = [ { recipe_id: 2, name: 'Chicken Tacos', description: null, instructions: null, avg_rating: 0, rating_count: 0, fork_count: 0, status: 'public', created_at: new Date().toISOString() }, ]; - vi.mocked(db.findRecipesByIngredientAndTag).mockResolvedValue(mockRecipes); + vi.mocked(recipeDb.findRecipesByIngredientAndTag).mockResolvedValue(mockRecipes); const response = await supertest(app).get('/api/recipes/by-ingredient-and-tag?ingredient=chicken&tag=quick'); @@ -259,15 +264,15 @@ describe('Public Routes (/api)', () => { describe('GET /stats/most-frequent-sales', () => { it('should return most frequent sale items with default parameters', async () => { - vi.mocked(db.getMostFrequentSaleItems).mockResolvedValue([]); + vi.mocked(adminDb.getMostFrequentSaleItems).mockResolvedValue([]); const response = await supertest(app).get('/api/stats/most-frequent-sales'); expect(response.status).toBe(200); }); it('should use provided query parameters', async () => { - vi.mocked(db.getMostFrequentSaleItems).mockResolvedValue([]); + vi.mocked(adminDb.getMostFrequentSaleItems).mockResolvedValue([]); await supertest(app).get('/api/stats/most-frequent-sales?days=90&limit=5'); - expect(db.getMostFrequentSaleItems).toHaveBeenCalledWith(90, 5); + expect(adminDb.getMostFrequentSaleItems).toHaveBeenCalledWith(90, 5); }); it('should return 400 for an invalid "days" parameter', async () => { diff --git a/src/routes/user.test.ts b/src/routes/user.test.ts index 46b2ffb8..224f73ff 100644 --- a/src/routes/user.test.ts +++ b/src/routes/user.test.ts @@ -4,11 +4,15 @@ import express from 'express'; // Use * as bcrypt to match the implementation's import style and ensure mocks align. import * as bcrypt from 'bcrypt'; import userRouter from './user'; -import * as db from '../services/db/index.db'; +import * as userDb from '../services/db/user.db'; +import * as personalizationDb from '../services/db/personalization.db'; +import * as shoppingDb from '../services/db/shopping.db'; import { UserProfile, MasterGroceryItem, ShoppingList, ShoppingListItem, Appliance } from '../types'; // 1. Mock the Service Layer directly. -vi.mock('../services/db'); +vi.mock('../services/db/user.db'); +vi.mock('../services/db/personalization.db'); +vi.mock('../services/db/shopping.db'); // 2. Mock bcrypt. // We return an object that satisfies both default and named imports to be safe. @@ -92,7 +96,7 @@ describe('User Routes (/api/users)', () => { describe('GET /profile', () => { it('should return the full user profile', async () => { // Arrange - vi.mocked(db.findUserProfileById).mockResolvedValue(mockUserProfile); + vi.mocked(userDb.findUserProfileById).mockResolvedValue(mockUserProfile); // Act const response = await supertest(app).get('/api/users/profile'); @@ -100,12 +104,12 @@ describe('User Routes (/api/users)', () => { // Assert expect(response.status).toBe(200); expect(response.body).toEqual(mockUserProfile); - expect(db.findUserProfileById).toHaveBeenCalledWith(mockUserProfile.user_id); + expect(userDb.findUserProfileById).toHaveBeenCalledWith(mockUserProfile.user_id); }); it('should return 404 if profile is not found in DB', async () => { // Arrange - vi.mocked(db.findUserProfileById).mockResolvedValue(undefined); + vi.mocked(userDb.findUserProfileById).mockResolvedValue(undefined); // Act const response = await supertest(app).get('/api/users/profile'); @@ -126,7 +130,7 @@ describe('User Routes (/api/users)', () => { category_id: 1, // Add missing properties category_name: 'Dairy & Eggs' }]; - vi.mocked(db.getWatchedItems).mockResolvedValue(mockItems); + vi.mocked(personalizationDb.getWatchedItems).mockResolvedValue(mockItems); // Act const response = await supertest(app).get('/api/users/watched-items'); @@ -148,7 +152,7 @@ describe('User Routes (/api/users)', () => { category_id: 1, // Add missing properties category_name: 'Produce' }; - vi.mocked(db.addWatchedItem).mockResolvedValue(mockAddedItem); + vi.mocked(personalizationDb.addWatchedItem).mockResolvedValue(mockAddedItem); // Act const response = await supertest(app) @@ -164,21 +168,21 @@ describe('User Routes (/api/users)', () => { describe('DELETE /watched-items/:masterItemId', () => { it('should remove an item from the watchlist', async () => { // Arrange - vi.mocked(db.removeWatchedItem).mockResolvedValue(undefined); + vi.mocked(personalizationDb.removeWatchedItem).mockResolvedValue(undefined); // Act const response = await supertest(app).delete(`/api/users/watched-items/99`); // Assert expect(response.status).toBe(204); - expect(db.removeWatchedItem).toHaveBeenCalledWith(mockUserProfile.user_id, 99); + expect(personalizationDb.removeWatchedItem).toHaveBeenCalledWith(mockUserProfile.user_id, 99); }); }); describe('Shopping List Routes', () => { it('GET /shopping-lists should return all shopping lists for the user', async () => { const mockLists: ShoppingList[] = [{ shopping_list_id: 1, user_id: mockUserProfile.user_id, name: 'Weekly Groceries', created_at: new Date().toISOString(), items: [] }]; - vi.mocked(db.getShoppingLists).mockResolvedValue(mockLists); + vi.mocked(shoppingDb.getShoppingLists).mockResolvedValue(mockLists); const response = await supertest(app).get('/api/users/shopping-lists'); @@ -188,7 +192,7 @@ describe('User Routes (/api/users)', () => { it('POST /shopping-lists should create a new list', async () => { const mockNewList: ShoppingList = { shopping_list_id: 2, user_id: mockUserProfile.user_id, name: 'Party Supplies', created_at: new Date().toISOString(), items: [] }; - vi.mocked(db.createShoppingList).mockResolvedValue(mockNewList); + vi.mocked(shoppingDb.createShoppingList).mockResolvedValue(mockNewList); const response = await supertest(app) .post('/api/users/shopping-lists') @@ -199,7 +203,7 @@ describe('User Routes (/api/users)', () => { }); it('DELETE /shopping-lists/:listId should delete a list', async () => { - vi.mocked(db.deleteShoppingList).mockResolvedValue(undefined); + vi.mocked(shoppingDb.deleteShoppingList).mockResolvedValue(undefined); const response = await supertest(app).delete('/api/users/shopping-lists/1'); expect(response.status).toBe(204); }); @@ -217,7 +221,7 @@ describe('User Routes (/api/users)', () => { added_at: new Date().toISOString(), ...itemData }; - vi.mocked(db.addShoppingListItem).mockResolvedValue(mockAddedItem); + vi.mocked(shoppingDb.addShoppingListItem).mockResolvedValue(mockAddedItem); const response = await supertest(app) .post(`/api/users/shopping-lists/${listId}/items`) @@ -237,7 +241,7 @@ describe('User Routes (/api/users)', () => { custom_item_name: 'Item', // Add missing property ...updates }; - vi.mocked(db.updateShoppingListItem).mockResolvedValue(mockUpdatedItem); + vi.mocked(shoppingDb.updateShoppingListItem).mockResolvedValue(mockUpdatedItem); const response = await supertest(app) .put(`/api/users/shopping-lists/items/${itemId}`) @@ -248,7 +252,7 @@ describe('User Routes (/api/users)', () => { }); it('DELETE /shopping-lists/items/:itemId should delete an item', async () => { - vi.mocked(db.removeShoppingListItem).mockResolvedValue(undefined); + vi.mocked(shoppingDb.removeShoppingListItem).mockResolvedValue(undefined); const response = await supertest(app).delete('/api/users/shopping-lists/items/101'); expect(response.status).toBe(204); }); @@ -258,7 +262,7 @@ describe('User Routes (/api/users)', () => { it('should update the user profile successfully', async () => { const profileUpdates = { full_name: 'New Name' }; const updatedProfile = { ...mockUserProfile, ...profileUpdates }; - vi.mocked(db.updateUserProfile).mockResolvedValue(updatedProfile); + vi.mocked(userDb.updateUserProfile).mockResolvedValue(updatedProfile); const response = await supertest(app) .put('/api/users/profile') @@ -281,7 +285,7 @@ describe('User Routes (/api/users)', () => { describe('PUT /profile/password', () => { it('should update the password successfully with a strong password', async () => { vi.mocked(bcrypt.hash).mockResolvedValue('hashed-password' as never); - vi.mocked(db.updateUserPassword).mockResolvedValue(undefined); + vi.mocked(userDb.updateUserPassword).mockResolvedValue(undefined); const response = await supertest(app) .put('/api/users/profile/password') @@ -310,8 +314,8 @@ describe('User Routes (/api/users)', () => { failed_login_attempts: 0, // Add missing properties last_failed_login: null }; - vi.mocked(db.findUserWithPasswordHashById).mockResolvedValue(userWithHash); - vi.mocked(db.deleteUserById).mockResolvedValue(undefined); + vi.mocked(userDb.findUserWithPasswordHashById).mockResolvedValue(userWithHash); + vi.mocked(userDb.deleteUserById).mockResolvedValue(undefined); vi.mocked(bcrypt.compare).mockResolvedValue(true as never); // Act @@ -331,7 +335,7 @@ describe('User Routes (/api/users)', () => { failed_login_attempts: 0, // Add missing properties last_failed_login: null }; - vi.mocked(db.findUserWithPasswordHashById).mockResolvedValue(userWithHash); + vi.mocked(userDb.findUserWithPasswordHashById).mockResolvedValue(userWithHash); vi.mocked(bcrypt.compare).mockResolvedValue(false as never); const response = await supertest(app) @@ -351,7 +355,7 @@ describe('User Routes (/api/users)', () => { ...mockUserProfile, preferences: { ...mockUserProfile.preferences, ...preferencesUpdate } }; - vi.mocked(db.updateUserPreferences).mockResolvedValue(updatedProfile); + vi.mocked(userDb.updateUserPreferences).mockResolvedValue(updatedProfile); const response = await supertest(app) .put('/api/users/profile/preferences') @@ -375,7 +379,7 @@ describe('User Routes (/api/users)', () => { describe('GET and PUT /users/me/dietary-restrictions', () => { it('GET should return a list of restriction IDs', async () => { const mockRestrictions = [{ dietary_restriction_id: 1, name: 'Gluten-Free', type: 'diet' as const }]; - vi.mocked(db.getUserDietaryRestrictions).mockResolvedValue(mockRestrictions); + vi.mocked(personalizationDb.getUserDietaryRestrictions).mockResolvedValue(mockRestrictions); const response = await supertest(app).get('/api/users/me/dietary-restrictions'); @@ -384,7 +388,7 @@ describe('User Routes (/api/users)', () => { }); it('PUT should successfully set the restrictions', async () => { - vi.mocked(db.setUserDietaryRestrictions).mockResolvedValue(undefined); + vi.mocked(personalizationDb.setUserDietaryRestrictions).mockResolvedValue(undefined); const restrictionIds = [1, 3, 5]; const response = await supertest(app) @@ -398,7 +402,7 @@ describe('User Routes (/api/users)', () => { describe('GET and PUT /users/me/appliances', () => { it('GET should return a list of appliance IDs', async () => { const mockAppliances: Appliance[] = [{ appliance_id: 2, name: 'Air Fryer' }]; - vi.mocked(db.getUserAppliances).mockResolvedValue(mockAppliances); + vi.mocked(personalizationDb.getUserAppliances).mockResolvedValue(mockAppliances); const response = await supertest(app).get('/api/users/me/appliances'); @@ -408,7 +412,7 @@ describe('User Routes (/api/users)', () => { it('PUT should successfully set the appliances', async () => { // Pass an empty array to match the expected return type UserAppliance[] - vi.mocked(db.setUserAppliances).mockResolvedValue([]); + vi.mocked(personalizationDb.setUserAppliances).mockResolvedValue([]); const applianceIds = [2, 4, 6]; const response = await supertest(app).put('/api/users/me/appliances').send({ applianceIds }); expect(response.status).toBe(204);