lootsa tests fixes
All checks were successful
Deploy to Web Server flyer-crawler.projectium.com / deploy (push) Successful in 4m35s
All checks were successful
Deploy to Web Server flyer-crawler.projectium.com / deploy (push) Successful in 4m35s
This commit is contained in:
@@ -5,7 +5,9 @@ import express, { Request, Response, NextFunction } from 'express';
|
||||
import path from 'path';
|
||||
import fs from 'node:fs/promises';
|
||||
import adminRouter from './admin.routes'; // Correctly imported
|
||||
import { UserProfile } from '../types';
|
||||
import { createMockUserProfile, createMockSuggestedCorrection, createMockBrand, createMockRecipe, createMockRecipeComment, createMockActivityLogItem } from '../tests/utils/mockFactories';
|
||||
import { Job } from 'bullmq';
|
||||
import { UserProfile, SuggestedCorrection, Brand, Recipe, RecipeComment, User, UnmatchedFlyerItem } from '../types';
|
||||
|
||||
// Mock the specific DB modules used by the admin router.
|
||||
// This is more robust than mocking the barrel file ('../services/db').
|
||||
@@ -14,12 +16,29 @@ vi.mock('../services/db/flyer.db');
|
||||
vi.mock('../services/db/recipe.db');
|
||||
vi.mock('../services/db/user.db'); // Add mock for user.db
|
||||
|
||||
// Mock the background job service to test the trigger endpoint.
|
||||
vi.mock('../services/backgroundJobService');
|
||||
|
||||
// Mock the geocoding service to test the cache clear endpoint.
|
||||
vi.mock('../services/geocodingService.server');
|
||||
|
||||
// Mock the queue service to test job enqueuing endpoints.
|
||||
vi.mock('../services/queueService.server', () => ({
|
||||
flyerQueue: { add: vi.fn() },
|
||||
emailQueue: { add: vi.fn() },
|
||||
analyticsQueue: { add: vi.fn() },
|
||||
cleanupQueue: { add: vi.fn() },
|
||||
}));
|
||||
|
||||
// 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';
|
||||
import * as userDb from '../services/db/user.db'; // Import the mocked user.db
|
||||
const mockedDb = { ...adminDb, ...flyerDb, ...recipeDb, ...userDb } as Mocked<typeof adminDb & typeof flyerDb & typeof recipeDb & typeof userDb>;
|
||||
import { runDailyDealCheck } from '../services/backgroundJobService';
|
||||
import { analyticsQueue, cleanupQueue } from '../services/queueService.server';
|
||||
import { clearGeocodeCache } from '../services/geocodingService.server';
|
||||
|
||||
// Mock the logger to keep test output clean
|
||||
vi.mock('../services/logger.server', () => ({
|
||||
@@ -34,7 +53,7 @@ vi.mock('../services/logger.server', () => ({
|
||||
// Use vi.hoisted to create a mutable mock function reference that can be controlled in tests.
|
||||
const mockedIsAdmin = vi.hoisted(() => vi.fn());
|
||||
|
||||
vi.mock('./passport', () => ({
|
||||
vi.mock('./passport.routes', () => ({
|
||||
// Mock the default export (the passport instance)
|
||||
default: {
|
||||
// The 'authenticate' method returns a middleware function. We mock that.
|
||||
@@ -99,22 +118,18 @@ describe('Admin Routes (/api/admin)', () => {
|
||||
beforeEach(() => {
|
||||
// Arrange: For all tests in this block, simulate a logged-in admin user.
|
||||
mockedIsAdmin.mockImplementation((req: Request, res: Response, next: NextFunction) => {
|
||||
// Attach a mock admin user profile to the request object.
|
||||
// The UserProfile type has a nested `user` object.
|
||||
req.user = {
|
||||
user: { user_id: 'admin-user-id', email: 'admin@test.com' },
|
||||
role: 'admin',
|
||||
// Add missing properties to align with the UserProfile type
|
||||
points: 0,
|
||||
} as UserProfile;
|
||||
// Use the factory to create a mock admin user.
|
||||
req.user = createMockUserProfile({ role: 'admin', user_id: 'admin-user-id' });
|
||||
next(); // Grant access
|
||||
});
|
||||
});
|
||||
|
||||
it('GET /corrections should return corrections data', async () => {
|
||||
// Arrange
|
||||
const mockCorrections: Awaited<ReturnType<typeof mockedDb.getSuggestedCorrections>> = [{ 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(adminDb.getSuggestedCorrections).mockResolvedValue(mockCorrections);
|
||||
const mockCorrections: SuggestedCorrection[] = [
|
||||
createMockSuggestedCorrection({ suggested_correction_id: 1 }),
|
||||
];
|
||||
mockedDb.getSuggestedCorrections.mockResolvedValue(mockCorrections);
|
||||
|
||||
// Act
|
||||
const response = await supertest(app).get('/api/admin/corrections');
|
||||
@@ -122,17 +137,17 @@ describe('Admin Routes (/api/admin)', () => {
|
||||
// Assert
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body).toEqual(mockCorrections);
|
||||
expect(adminDb.getSuggestedCorrections).toHaveBeenCalledTimes(1);
|
||||
expect(mockedDb.getSuggestedCorrections).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
describe('GET /brands', () => {
|
||||
it('should return a list of all brands on success', async () => {
|
||||
// Arrange
|
||||
const mockBrands: Awaited<ReturnType<typeof mockedDb.getAllBrands>> = [
|
||||
{ brand_id: 1, name: 'Brand A', logo_url: '/path/a.png' },
|
||||
{ brand_id: 2, name: 'Brand B', logo_url: '/path/b.png' },
|
||||
const mockBrands: Brand[] = [
|
||||
createMockBrand({ brand_id: 1, name: 'Brand A' }),
|
||||
createMockBrand({ brand_id: 2, name: 'Brand B' }),
|
||||
];
|
||||
vi.mocked(flyerDb.getAllBrands).mockResolvedValue(mockBrands);
|
||||
mockedDb.getAllBrands.mockResolvedValue(mockBrands);
|
||||
|
||||
// Act
|
||||
const response = await supertest(app).get('/api/admin/brands');
|
||||
@@ -140,11 +155,11 @@ describe('Admin Routes (/api/admin)', () => {
|
||||
// Assert
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body).toEqual(mockBrands);
|
||||
expect(flyerDb.getAllBrands).toHaveBeenCalledTimes(1);
|
||||
expect(mockedDb.getAllBrands).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should return a 500 error if the database call fails', async () => {
|
||||
vi.mocked(flyerDb.getAllBrands).mockRejectedValue(new Error('Failed to fetch brands'));
|
||||
mockedDb.getAllBrands.mockRejectedValue(new Error('Failed to fetch brands'));
|
||||
const response = await supertest(app).get('/api/admin/brands');
|
||||
expect(response.status).toBe(500);
|
||||
});
|
||||
@@ -160,7 +175,7 @@ describe('Admin Routes (/api/admin)', () => {
|
||||
storeCount: 12,
|
||||
pendingCorrectionCount: 5,
|
||||
};
|
||||
vi.mocked(adminDb.getApplicationStats).mockResolvedValue(mockStats);
|
||||
mockedDb.getApplicationStats.mockResolvedValue(mockStats);
|
||||
|
||||
// Act
|
||||
const response = await supertest(app).get('/api/admin/stats');
|
||||
@@ -168,11 +183,11 @@ describe('Admin Routes (/api/admin)', () => {
|
||||
// Assert
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body).toEqual(mockStats);
|
||||
expect(adminDb.getApplicationStats).toHaveBeenCalledTimes(1);
|
||||
expect(mockedDb.getApplicationStats).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should return a 500 error if the database call fails', async () => {
|
||||
vi.mocked(adminDb.getApplicationStats).mockRejectedValue(new Error('Failed to fetch stats'));
|
||||
mockedDb.getApplicationStats.mockRejectedValue(new Error('Failed to fetch stats'));
|
||||
const response = await supertest(app).get('/api/admin/stats');
|
||||
expect(response.status).toBe(500);
|
||||
});
|
||||
@@ -184,8 +199,8 @@ describe('Admin Routes (/api/admin)', () => {
|
||||
const mockDailyStats = [
|
||||
{ date: '2024-01-01', new_users: 5, new_flyers: 10 },
|
||||
{ date: '2024-01-02', new_users: 3, new_flyers: 8 },
|
||||
] as Awaited<ReturnType<typeof mockedDb.getDailyStatsForLast30Days>>;
|
||||
vi.mocked(adminDb.getDailyStatsForLast30Days).mockResolvedValue(mockDailyStats);
|
||||
];
|
||||
mockedDb.getDailyStatsForLast30Days.mockResolvedValue(mockDailyStats);
|
||||
|
||||
// Act
|
||||
const response = await supertest(app).get('/api/admin/stats/daily');
|
||||
@@ -193,11 +208,11 @@ describe('Admin Routes (/api/admin)', () => {
|
||||
// Assert
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body).toEqual(mockDailyStats);
|
||||
expect(adminDb.getDailyStatsForLast30Days).toHaveBeenCalledTimes(1);
|
||||
expect(mockedDb.getDailyStatsForLast30Days).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should return a 500 error if the database call fails', async () => {
|
||||
vi.mocked(adminDb.getDailyStatsForLast30Days).mockRejectedValue(new Error('Failed to fetch daily stats'));
|
||||
mockedDb.getDailyStatsForLast30Days.mockRejectedValue(new Error('Failed to fetch daily stats'));
|
||||
const response = await supertest(app).get('/api/admin/stats/daily');
|
||||
expect(response.status).toBe(500);
|
||||
});
|
||||
@@ -206,11 +221,11 @@ describe('Admin Routes (/api/admin)', () => {
|
||||
describe('GET /unmatched-items', () => {
|
||||
it('should return a list of unmatched items on success', async () => {
|
||||
// Arrange
|
||||
const mockUnmatchedItems: Awaited<ReturnType<typeof mockedDb.getUnmatchedFlyerItems>> = [
|
||||
const mockUnmatchedItems: UnmatchedFlyerItem[] = [
|
||||
{ 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(adminDb.getUnmatchedFlyerItems).mockResolvedValue(mockUnmatchedItems);
|
||||
mockedDb.getUnmatchedFlyerItems.mockResolvedValue(mockUnmatchedItems);
|
||||
|
||||
// Act
|
||||
const response = await supertest(app).get('/api/admin/unmatched-items');
|
||||
@@ -218,11 +233,11 @@ describe('Admin Routes (/api/admin)', () => {
|
||||
// Assert
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body).toEqual(mockUnmatchedItems);
|
||||
expect(adminDb.getUnmatchedFlyerItems).toHaveBeenCalledTimes(1);
|
||||
expect(mockedDb.getUnmatchedFlyerItems).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should return a 500 error if the database call fails', async () => {
|
||||
vi.mocked(adminDb.getUnmatchedFlyerItems).mockRejectedValue(new Error('Failed to fetch unmatched items'));
|
||||
mockedDb.getUnmatchedFlyerItems.mockRejectedValue(new Error('Failed to fetch unmatched items'));
|
||||
const response = await supertest(app).get('/api/admin/unmatched-items');
|
||||
expect(response.status).toBe(500);
|
||||
});
|
||||
@@ -232,7 +247,7 @@ describe('Admin Routes (/api/admin)', () => {
|
||||
it('should approve a correction and return a success message', async () => {
|
||||
// Arrange
|
||||
const correctionId = 123;
|
||||
vi.mocked(adminDb.approveCorrection).mockResolvedValue(undefined); // Mock the DB call to succeed
|
||||
mockedDb.approveCorrection.mockResolvedValue(undefined); // Mock the DB call to succeed
|
||||
|
||||
// Act
|
||||
const response = await supertest(app).post(`/api/admin/corrections/${correctionId}/approve`);
|
||||
@@ -240,14 +255,14 @@ describe('Admin Routes (/api/admin)', () => {
|
||||
// Assert
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body).toEqual({ message: 'Correction approved successfully.' });
|
||||
expect(adminDb.approveCorrection).toHaveBeenCalledTimes(1);
|
||||
expect(adminDb.approveCorrection).toHaveBeenCalledWith(correctionId);
|
||||
expect(mockedDb.approveCorrection).toHaveBeenCalledTimes(1);
|
||||
expect(mockedDb.approveCorrection).toHaveBeenCalledWith(correctionId);
|
||||
});
|
||||
|
||||
it('should return a 500 error if the database call fails', async () => {
|
||||
// Arrange
|
||||
const correctionId = 456;
|
||||
vi.mocked(adminDb.approveCorrection).mockRejectedValue(new Error('Database failure'));
|
||||
mockedDb.approveCorrection.mockRejectedValue(new Error('Database failure'));
|
||||
|
||||
// Act
|
||||
const response = await supertest(app).post(`/api/admin/corrections/${correctionId}/approve`);
|
||||
@@ -263,7 +278,7 @@ describe('Admin Routes (/api/admin)', () => {
|
||||
// Assert
|
||||
expect(response.status).toBe(400);
|
||||
expect(response.body.message).toBe('Invalid correction ID provided.');
|
||||
expect(adminDb.approveCorrection).not.toHaveBeenCalled();
|
||||
expect(mockedDb.approveCorrection).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -271,7 +286,7 @@ describe('Admin Routes (/api/admin)', () => {
|
||||
it('should reject a correction and return a success message', async () => {
|
||||
// Arrange
|
||||
const correctionId = 789;
|
||||
vi.mocked(adminDb.rejectCorrection).mockResolvedValue(undefined); // Mock the DB call to succeed
|
||||
mockedDb.rejectCorrection.mockResolvedValue(undefined); // Mock the DB call to succeed
|
||||
|
||||
// Act
|
||||
const response = await supertest(app).post(`/api/admin/corrections/${correctionId}/reject`);
|
||||
@@ -279,14 +294,14 @@ describe('Admin Routes (/api/admin)', () => {
|
||||
// Assert
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body).toEqual({ message: 'Correction rejected successfully.' });
|
||||
expect(adminDb.rejectCorrection).toHaveBeenCalledTimes(1);
|
||||
expect(adminDb.rejectCorrection).toHaveBeenCalledWith(correctionId);
|
||||
expect(mockedDb.rejectCorrection).toHaveBeenCalledTimes(1);
|
||||
expect(mockedDb.rejectCorrection).toHaveBeenCalledWith(correctionId);
|
||||
});
|
||||
|
||||
it('should return a 500 error if the database call fails', async () => {
|
||||
// Arrange
|
||||
const correctionId = 987;
|
||||
vi.mocked(adminDb.rejectCorrection).mockRejectedValue(new Error('Database failure'));
|
||||
mockedDb.rejectCorrection.mockRejectedValue(new Error('Database failure'));
|
||||
|
||||
// Act
|
||||
const response = await supertest(app).post(`/api/admin/corrections/${correctionId}/reject`);
|
||||
@@ -301,8 +316,8 @@ describe('Admin Routes (/api/admin)', () => {
|
||||
// Arrange
|
||||
const correctionId = 101;
|
||||
const requestBody = { suggested_value: 'A new corrected value' };
|
||||
const mockUpdatedCorrection: Awaited<ReturnType<typeof mockedDb.updateSuggestedCorrection>> = { suggested_correction_id: correctionId, flyer_item_id: 1, user_id: '1', correction_type: 'price', status: 'pending', created_at: new Date().toISOString(), ...requestBody };
|
||||
vi.mocked(adminDb.updateSuggestedCorrection).mockResolvedValue(mockUpdatedCorrection);
|
||||
const mockUpdatedCorrection = createMockSuggestedCorrection({ suggested_correction_id: correctionId, ...requestBody });
|
||||
mockedDb.updateSuggestedCorrection.mockResolvedValue(mockUpdatedCorrection);
|
||||
|
||||
// Act: Use .send() to include a request body
|
||||
const response = await supertest(app)
|
||||
@@ -312,8 +327,8 @@ describe('Admin Routes (/api/admin)', () => {
|
||||
// Assert
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body).toEqual(mockUpdatedCorrection);
|
||||
expect(adminDb.updateSuggestedCorrection).toHaveBeenCalledTimes(1);
|
||||
expect(adminDb.updateSuggestedCorrection).toHaveBeenCalledWith(correctionId, requestBody.suggested_value);
|
||||
expect(mockedDb.updateSuggestedCorrection).toHaveBeenCalledTimes(1);
|
||||
expect(mockedDb.updateSuggestedCorrection).toHaveBeenCalledWith(correctionId, requestBody.suggested_value);
|
||||
});
|
||||
|
||||
it('should return a 400 error if suggested_value is missing from the body', async () => {
|
||||
@@ -329,7 +344,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(adminDb.updateSuggestedCorrection).not.toHaveBeenCalled();
|
||||
expect(mockedDb.updateSuggestedCorrection).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should return a 404 error if the correction to update is not found', async () => {
|
||||
@@ -337,7 +352,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.
|
||||
vi.mocked(adminDb.updateSuggestedCorrection).mockRejectedValue(new Error(`Correction with ID ${correctionId} not found.`));
|
||||
mockedDb.updateSuggestedCorrection.mockRejectedValue(new Error(`Correction with ID ${correctionId} not found.`));
|
||||
|
||||
// Act
|
||||
const response = await supertest(app)
|
||||
@@ -354,7 +369,7 @@ describe('Admin Routes (/api/admin)', () => {
|
||||
it('should upload a logo and update the brand', async () => {
|
||||
// Arrange
|
||||
const brandId = 55;
|
||||
vi.mocked(adminDb.updateBrandLogo).mockResolvedValue(undefined); // Mock the DB call
|
||||
mockedDb.updateBrandLogo.mockResolvedValue(undefined); // Mock the DB call
|
||||
|
||||
// Create a dummy file for supertest to attach.
|
||||
// supertest needs a real file path to stream from.
|
||||
@@ -373,8 +388,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(adminDb.updateBrandLogo).toHaveBeenCalledTimes(1);
|
||||
expect(adminDb.updateBrandLogo).toHaveBeenCalledWith(brandId, expect.stringContaining('/assets/'));
|
||||
expect(mockedDb.updateBrandLogo).toHaveBeenCalledTimes(1);
|
||||
expect(mockedDb.updateBrandLogo).toHaveBeenCalledWith(brandId, expect.stringContaining('/assets/'));
|
||||
|
||||
// Clean up the dummy file
|
||||
await fs.unlink(dummyFilePath);
|
||||
@@ -410,8 +425,8 @@ describe('Admin Routes (/api/admin)', () => {
|
||||
// Arrange
|
||||
const recipeId = 201;
|
||||
const requestBody = { status: 'public' as const };
|
||||
const mockUpdatedRecipe: Awaited<ReturnType<typeof mockedDb.updateRecipeStatus>> = { recipe_id: recipeId, status: 'public', name: 'Test Recipe', avg_rating: 0, rating_count: 0, fork_count: 0, created_at: new Date().toISOString() };
|
||||
vi.mocked(adminDb.updateRecipeStatus).mockResolvedValue(mockUpdatedRecipe);
|
||||
const mockUpdatedRecipe = createMockRecipe({ recipe_id: recipeId, status: 'public' });
|
||||
mockedDb.updateRecipeStatus.mockResolvedValue(mockUpdatedRecipe);
|
||||
|
||||
// Act
|
||||
const response = await supertest(app)
|
||||
@@ -421,8 +436,8 @@ describe('Admin Routes (/api/admin)', () => {
|
||||
// Assert
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body).toEqual(mockUpdatedRecipe);
|
||||
expect(adminDb.updateRecipeStatus).toHaveBeenCalledTimes(1);
|
||||
expect(adminDb.updateRecipeStatus).toHaveBeenCalledWith(recipeId, 'public');
|
||||
expect(mockedDb.updateRecipeStatus).toHaveBeenCalledTimes(1);
|
||||
expect(mockedDb.updateRecipeStatus).toHaveBeenCalledWith(recipeId, 'public');
|
||||
});
|
||||
|
||||
it('should return a 400 error for an invalid status value', async () => {
|
||||
@@ -438,7 +453,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(adminDb.updateRecipeStatus).not.toHaveBeenCalled();
|
||||
expect(mockedDb.updateRecipeStatus).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -447,8 +462,8 @@ describe('Admin Routes (/api/admin)', () => {
|
||||
// Arrange
|
||||
const commentId = 301;
|
||||
const requestBody = { status: 'hidden' as const };
|
||||
const mockUpdatedComment: Awaited<ReturnType<typeof mockedDb.updateRecipeCommentStatus>> = { recipe_comment_id: commentId, recipe_id: 1, user_id: '1', status: 'hidden', content: 'Test Comment', created_at: new Date().toISOString() };
|
||||
vi.mocked(adminDb.updateRecipeCommentStatus).mockResolvedValue(mockUpdatedComment);
|
||||
const mockUpdatedComment = createMockRecipeComment({ recipe_comment_id: commentId, status: 'hidden' });
|
||||
mockedDb.updateRecipeCommentStatus.mockResolvedValue(mockUpdatedComment);
|
||||
|
||||
// Act
|
||||
const response = await supertest(app)
|
||||
@@ -458,26 +473,26 @@ describe('Admin Routes (/api/admin)', () => {
|
||||
// Assert
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body).toEqual(mockUpdatedComment);
|
||||
expect(adminDb.updateRecipeCommentStatus).toHaveBeenCalledTimes(1);
|
||||
expect(adminDb.updateRecipeCommentStatus).toHaveBeenCalledWith(commentId, 'hidden');
|
||||
expect(mockedDb.updateRecipeCommentStatus).toHaveBeenCalledTimes(1);
|
||||
expect(mockedDb.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(adminDb.updateRecipeCommentStatus).not.toHaveBeenCalled();
|
||||
expect(mockedDb.updateRecipeCommentStatus).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /users', () => {
|
||||
it('should return a list of all users on success', async () => {
|
||||
// Arrange
|
||||
const mockUsers: Awaited<ReturnType<typeof mockedDb.getAllUsers>> = [
|
||||
{ user_id: '1', email: 'user1@test.com', role: 'user', created_at: new Date().toISOString(), full_name: 'User One', avatar_url: null },
|
||||
const mockUsers: (User & { role: 'user' | 'admin', created_at: string, full_name: string | null, avatar_url: string | null })[] = [
|
||||
{ user_id: '1', email: 'user1@test.com', role: 'user' as const, 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(adminDb.getAllUsers).mockResolvedValue(mockUsers);
|
||||
mockedDb.getAllUsers.mockResolvedValue(mockUsers);
|
||||
|
||||
// Act
|
||||
const response = await supertest(app).get('/api/admin/users');
|
||||
@@ -485,11 +500,11 @@ describe('Admin Routes (/api/admin)', () => {
|
||||
// Assert
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body).toEqual(mockUsers);
|
||||
expect(adminDb.getAllUsers).toHaveBeenCalledTimes(1);
|
||||
expect(mockedDb.getAllUsers).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should return a 500 error if the database call fails', async () => {
|
||||
vi.mocked(adminDb.getAllUsers).mockRejectedValue(new Error('Failed to fetch users'));
|
||||
mockedDb.getAllUsers.mockRejectedValue(new Error('Failed to fetch users'));
|
||||
const response = await supertest(app).get('/api/admin/users');
|
||||
expect(response.status).toBe(500);
|
||||
});
|
||||
@@ -498,8 +513,8 @@ describe('Admin Routes (/api/admin)', () => {
|
||||
describe('GET /activity-log', () => {
|
||||
it('should return a list of activity logs with default pagination', async () => {
|
||||
// Arrange
|
||||
const mockLogs: Awaited<ReturnType<typeof mockedDb.getActivityLog>> = [{ 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(adminDb.getActivityLog).mockResolvedValue(mockLogs);
|
||||
const mockLogs = [createMockActivityLogItem({ action: 'flyer_processed' })];
|
||||
mockedDb.getActivityLog.mockResolvedValue(mockLogs);
|
||||
|
||||
// Act
|
||||
const response = await supertest(app).get('/api/admin/activity-log');
|
||||
@@ -507,34 +522,34 @@ describe('Admin Routes (/api/admin)', () => {
|
||||
// Assert
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body).toEqual(mockLogs);
|
||||
expect(adminDb.getActivityLog).toHaveBeenCalledTimes(1);
|
||||
expect(mockedDb.getActivityLog).toHaveBeenCalledTimes(1);
|
||||
// Check that default pagination values were used
|
||||
// This makes the test more robust by verifying the correct parameters were passed.
|
||||
expect(adminDb.getActivityLog).toHaveBeenCalledWith(50, 0);
|
||||
expect(mockedDb.getActivityLog).toHaveBeenCalledWith(50, 0);
|
||||
});
|
||||
|
||||
it('should use limit and offset query parameters when provided', async () => {
|
||||
vi.mocked(adminDb.getActivityLog).mockResolvedValue([]);
|
||||
mockedDb.getActivityLog.mockResolvedValue([]);
|
||||
|
||||
await supertest(app).get('/api/admin/activity-log?limit=10&offset=20');
|
||||
|
||||
expect(adminDb.getActivityLog).toHaveBeenCalledTimes(1);
|
||||
expect(adminDb.getActivityLog).toHaveBeenCalledWith(10, 20);
|
||||
expect(mockedDb.getActivityLog).toHaveBeenCalledTimes(1);
|
||||
expect(mockedDb.getActivityLog).toHaveBeenCalledWith(10, 20);
|
||||
});
|
||||
|
||||
it('should handle invalid pagination parameters gracefully', async () => {
|
||||
vi.mocked(adminDb.getActivityLog).mockResolvedValue([]);
|
||||
mockedDb.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(adminDb.getActivityLog).toHaveBeenCalledWith(50, 0);
|
||||
expect(mockedDb.getActivityLog).toHaveBeenCalledWith(50, 0);
|
||||
});
|
||||
|
||||
it('should return a 500 error if the database call fails', async () => {
|
||||
// Arrange
|
||||
vi.mocked(adminDb.getActivityLog).mockRejectedValue(new Error('DB connection error'));
|
||||
mockedDb.getActivityLog.mockRejectedValue(new Error('DB connection error'));
|
||||
|
||||
// Act
|
||||
const response = await supertest(app).get('/api/admin/activity-log');
|
||||
@@ -547,8 +562,8 @@ describe('Admin Routes (/api/admin)', () => {
|
||||
describe('GET /users/:id', () => {
|
||||
it('should fetch a single user successfully', async () => {
|
||||
// Arrange
|
||||
const mockUser: Awaited<ReturnType<typeof mockedDb.findUserProfileById>> = { user_id: 'user-123', role: 'user', points: 0 };
|
||||
vi.mocked(mockedDb.findUserProfileById).mockResolvedValue(mockUser);
|
||||
const mockUser = createMockUserProfile({ user_id: 'user-123' });
|
||||
mockedDb.findUserProfileById.mockResolvedValue(mockUser);
|
||||
|
||||
// Act
|
||||
const response = await supertest(app).get('/api/admin/users/user-123');
|
||||
@@ -561,7 +576,7 @@ describe('Admin Routes (/api/admin)', () => {
|
||||
|
||||
it('should return 404 for a non-existent user', async () => {
|
||||
// Arrange
|
||||
vi.mocked(mockedDb.findUserProfileById).mockResolvedValue(undefined);
|
||||
mockedDb.findUserProfileById.mockResolvedValue(undefined);
|
||||
|
||||
// Act
|
||||
const response = await supertest(app).get('/api/admin/users/non-existent-id');
|
||||
@@ -575,8 +590,8 @@ describe('Admin Routes (/api/admin)', () => {
|
||||
describe('PUT /users/:id', () => {
|
||||
it('should update a user role successfully', async () => {
|
||||
// Arrange
|
||||
const updatedUser: Awaited<ReturnType<typeof mockedDb.updateUserRole>> = { user_id: 'user-to-update', email: 'test@test.com' };
|
||||
vi.mocked(mockedDb.updateUserRole).mockResolvedValue(updatedUser);
|
||||
const updatedUser: User = { user_id: 'user-to-update', email: 'test@test.com' };
|
||||
mockedDb.updateUserRole.mockResolvedValue(updatedUser);
|
||||
|
||||
// Act
|
||||
const response = await supertest(app)
|
||||
@@ -586,11 +601,11 @@ describe('Admin Routes (/api/admin)', () => {
|
||||
// Assert
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body).toEqual(updatedUser); // The actual route returns the updated user, not just a message
|
||||
expect(adminDb.updateUserRole).toHaveBeenCalledWith('user-to-update', 'admin');
|
||||
expect(mockedDb.updateUserRole).toHaveBeenCalledWith('user-to-update', 'admin');
|
||||
});
|
||||
|
||||
it('should return 404 for a non-existent user', async () => {
|
||||
vi.mocked(adminDb.updateUserRole).mockRejectedValue(new Error('User with ID non-existent not found.'));
|
||||
mockedDb.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);
|
||||
});
|
||||
@@ -604,10 +619,10 @@ describe('Admin Routes (/api/admin)', () => {
|
||||
|
||||
describe('DELETE /users/:id', () => {
|
||||
it('should successfully delete a user', async () => {
|
||||
vi.mocked(userDb.deleteUserById).mockResolvedValue(undefined);
|
||||
mockedDb.deleteUserById.mockResolvedValue(undefined);
|
||||
const response = await supertest(app).delete('/api/admin/users/user-to-delete');
|
||||
expect(response.status).toBe(204);
|
||||
expect(userDb.deleteUserById).toHaveBeenCalledWith('user-to-delete');
|
||||
expect(mockedDb.deleteUserById).toHaveBeenCalledWith('user-to-delete');
|
||||
});
|
||||
|
||||
it('should prevent an admin from deleting their own account', async () => {
|
||||
@@ -615,7 +630,109 @@ 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(userDb.deleteUserById).not.toHaveBeenCalled();
|
||||
expect(mockedDb.deleteUserById).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /trigger/daily-deal-check', () => {
|
||||
it('should trigger the daily deal check job and return 202 Accepted', async () => {
|
||||
// Arrange
|
||||
// The runDailyDealCheck function is mocked at the top level of the file.
|
||||
// We can simply check if it was called.
|
||||
vi.mocked(runDailyDealCheck).mockImplementation(async () => {}); // It returns Promise<void>
|
||||
|
||||
// Act
|
||||
const response = await supertest(app).post('/api/admin/trigger/daily-deal-check');
|
||||
|
||||
// Assert
|
||||
expect(response.status).toBe(202);
|
||||
expect(response.body.message).toBe('Daily deal check job has been triggered successfully. It will run in the background.');
|
||||
expect(runDailyDealCheck).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /trigger/failing-job', () => {
|
||||
it('should enqueue a job designed to fail and return 202 Accepted', async () => {
|
||||
// Arrange
|
||||
const mockJob = { id: 'failing-job-id-456' } as Job;
|
||||
vi.mocked(analyticsQueue.add).mockResolvedValue(mockJob);
|
||||
|
||||
// Act
|
||||
const response = await supertest(app).post('/api/admin/trigger/failing-job');
|
||||
|
||||
// Assert
|
||||
expect(response.status).toBe(202);
|
||||
expect(response.body.message).toContain('Failing test job has been enqueued successfully.');
|
||||
expect(response.body.jobId).toBe(mockJob.id);
|
||||
expect(analyticsQueue.add).toHaveBeenCalledTimes(1);
|
||||
// Verify it was called with the specific payload that the worker recognizes as a failure trigger.
|
||||
expect(analyticsQueue.add).toHaveBeenCalledWith('generate-daily-report', { reportDate: 'FAIL' });
|
||||
});
|
||||
|
||||
it('should return 500 if the queue service fails to add the job', async () => {
|
||||
// Arrange
|
||||
vi.mocked(analyticsQueue.add).mockRejectedValue(new Error('Redis connection lost'));
|
||||
|
||||
// Act
|
||||
const response = await supertest(app).post('/api/admin/trigger/failing-job');
|
||||
|
||||
// Assert
|
||||
expect(response.status).toBe(500);
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /flyers/:flyerId/cleanup', () => {
|
||||
it('should enqueue a cleanup job for a valid flyer ID', async () => {
|
||||
// Arrange
|
||||
const flyerId = 789;
|
||||
const mockJob = { id: `cleanup-job-${flyerId}` } as Job;
|
||||
vi.mocked(cleanupQueue.add).mockResolvedValue(mockJob as Job);
|
||||
|
||||
// Act
|
||||
const response = await supertest(app).post(`/api/admin/flyers/${flyerId}/cleanup`);
|
||||
|
||||
// Assert
|
||||
expect(response.status).toBe(202);
|
||||
expect(response.body.message).toBe(`File cleanup job for flyer ID ${flyerId} has been enqueued.`);
|
||||
expect(cleanupQueue.add).toHaveBeenCalledTimes(1);
|
||||
expect(cleanupQueue.add).toHaveBeenCalledWith('cleanup-flyer-files', { flyerId });
|
||||
});
|
||||
|
||||
it('should return 400 for an invalid flyer ID', async () => {
|
||||
// Act
|
||||
const response = await supertest(app).post('/api/admin/flyers/invalid-id/cleanup');
|
||||
|
||||
// Assert
|
||||
expect(response.status).toBe(400);
|
||||
expect(response.body.message).toBe('A valid flyer ID is required.');
|
||||
expect(cleanupQueue.add).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /system/clear-geocode-cache', () => {
|
||||
it('should clear the geocode cache and return a success message', async () => {
|
||||
// Arrange
|
||||
const deletedKeysCount = 42;
|
||||
vi.mocked(clearGeocodeCache).mockResolvedValue(deletedKeysCount);
|
||||
|
||||
// Act
|
||||
const response = await supertest(app).post('/api/admin/system/clear-geocode-cache');
|
||||
|
||||
// Assert
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body.message).toBe(`Successfully cleared the geocode cache. ${deletedKeysCount} keys were removed.`);
|
||||
expect(clearGeocodeCache).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should return 500 if clearing the cache fails', async () => {
|
||||
// Arrange
|
||||
vi.mocked(clearGeocodeCache).mockRejectedValue(new Error('Redis connection failed'));
|
||||
|
||||
// Act
|
||||
const response = await supertest(app).post('/api/admin/system/clear-geocode-cache');
|
||||
|
||||
// Assert
|
||||
expect(response.status).toBe(500);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,7 +4,7 @@ import supertest from 'supertest';
|
||||
import express, { type Request, type Response, type NextFunction } from 'express';
|
||||
import path from 'node:path';
|
||||
import aiRouter from './ai.routes';
|
||||
import { UserProfile } from '../types';
|
||||
import { createMockUserProfile, createMockFlyer } from '../tests/utils/mockFactories';
|
||||
import * as flyerDb from '../services/db/flyer.db';
|
||||
import * as adminDb from '../services/db/admin.db';
|
||||
|
||||
@@ -67,15 +67,12 @@ describe('AI Routes (/api/ai)', () => {
|
||||
|
||||
it('should save a flyer and return 201 on success', async () => {
|
||||
// Arrange
|
||||
vi.mocked(flyerDb.findFlyerByChecksum).mockResolvedValue(undefined); // No duplicate
|
||||
vi.mocked(flyerDb.createFlyerAndItems).mockResolvedValue({
|
||||
const mockFlyer = createMockFlyer({
|
||||
flyer_id: 1,
|
||||
created_at: new Date().toISOString(),
|
||||
file_name: mockDataPayload.originalFileName,
|
||||
image_url: '/assets/some-image.jpg',
|
||||
...(mockDataPayload.extractedData as any),
|
||||
item_count: 0, // Add missing property to satisfy the Flyer type
|
||||
});
|
||||
vi.mocked(flyerDb.findFlyerByChecksum).mockResolvedValue(undefined); // No duplicate
|
||||
vi.mocked(flyerDb.createFlyerAndItems).mockResolvedValue(mockFlyer);
|
||||
vi.mocked(adminDb.logActivity).mockResolvedValue();
|
||||
|
||||
// Act
|
||||
@@ -92,7 +89,8 @@ describe('AI Routes (/api/ai)', () => {
|
||||
|
||||
it('should return 409 Conflict if flyer checksum already exists', async () => {
|
||||
// Arrange
|
||||
vi.mocked(flyerDb.findFlyerByChecksum).mockResolvedValue({ flyer_id: 99 } as Awaited<ReturnType<typeof flyerDb.findFlyerByChecksum>>); // Duplicate found
|
||||
const mockExistingFlyer = createMockFlyer({ flyer_id: 99 });
|
||||
vi.mocked(flyerDb.findFlyerByChecksum).mockResolvedValue(mockExistingFlyer); // Duplicate found
|
||||
|
||||
// Act
|
||||
const response = await supertest(app)
|
||||
@@ -106,28 +104,6 @@ describe('AI Routes (/api/ai)', () => {
|
||||
expect(flyerDb.createFlyerAndItems).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should return 400 if no image file is provided', async () => {
|
||||
const response = await supertest(app)
|
||||
.post('/api/ai/flyers/process')
|
||||
.field('data', JSON.stringify(mockDataPayload)); // No .attach()
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
expect(response.body.message).toBe('Flyer image file is required.');
|
||||
});
|
||||
|
||||
it('should return 400 if extractedData is missing from payload', async () => {
|
||||
const badPayload = { checksum: 'c2', originalFileName: 'noflyer.jpg' }; // no extractedData
|
||||
|
||||
const response = await supertest(app)
|
||||
.post('/api/ai/flyers/process')
|
||||
.field('data', JSON.stringify(badPayload))
|
||||
.attach('flyerImage', imagePath);
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
expect(response.body.message).toBe('Invalid request: extractedData is required.');
|
||||
expect(flyerDb.createFlyerAndItems).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should accept payload when extractedData.items is missing and save with empty items', async () => {
|
||||
// Arrange: extractedData present but items missing
|
||||
const partialPayload = {
|
||||
@@ -137,13 +113,11 @@ describe('AI Routes (/api/ai)', () => {
|
||||
};
|
||||
|
||||
vi.mocked(flyerDb.findFlyerByChecksum).mockResolvedValue(undefined);
|
||||
vi.mocked(flyerDb.createFlyerAndItems).mockResolvedValue({
|
||||
const mockFlyer = createMockFlyer({
|
||||
flyer_id: 2,
|
||||
created_at: new Date().toISOString(),
|
||||
file_name: partialPayload.originalFileName,
|
||||
image_url: '/flyer-images/flyer2.jpg',
|
||||
item_count: 0,
|
||||
});
|
||||
vi.mocked(flyerDb.createFlyerAndItems).mockResolvedValue(mockFlyer);
|
||||
|
||||
const response = await supertest(app)
|
||||
.post('/api/ai/flyers/process')
|
||||
@@ -168,13 +142,11 @@ describe('AI Routes (/api/ai)', () => {
|
||||
};
|
||||
|
||||
vi.mocked(flyerDb.findFlyerByChecksum).mockResolvedValue(undefined);
|
||||
vi.mocked(flyerDb.createFlyerAndItems).mockResolvedValue({
|
||||
const mockFlyer = createMockFlyer({
|
||||
flyer_id: 3,
|
||||
created_at: new Date().toISOString(),
|
||||
file_name: payloadNoStore.originalFileName,
|
||||
image_url: '/flyer-images/flyer3.jpg',
|
||||
item_count: 0,
|
||||
});
|
||||
vi.mocked(flyerDb.createFlyerAndItems).mockResolvedValue(mockFlyer);
|
||||
|
||||
const response = await supertest(app)
|
||||
.post('/api/ai/flyers/process')
|
||||
@@ -190,13 +162,7 @@ describe('AI Routes (/api/ai)', () => {
|
||||
});
|
||||
|
||||
describe('when user is authenticated', () => {
|
||||
const mockUserProfile: UserProfile = {
|
||||
user_id: 'user-123',
|
||||
user: { user_id: 'user-123', email: 'test@test.com' },
|
||||
role: 'user',
|
||||
// Add missing properties to align with the UserProfile type
|
||||
points: 0,
|
||||
} as UserProfile;
|
||||
const mockUserProfile = createMockUserProfile({ user_id: 'user-123' });
|
||||
|
||||
beforeEach(() => {
|
||||
// For this block, simulate a logged-in user by having the middleware call next().
|
||||
|
||||
@@ -6,7 +6,7 @@ import jwt from 'jsonwebtoken';
|
||||
import crypto from 'crypto';
|
||||
import rateLimit from 'express-rate-limit';
|
||||
|
||||
import passport from './passport.routes';
|
||||
import passport from './passport.routes'; // Corrected import path
|
||||
import * as db from '../services/db/index.db';
|
||||
import { getPool } from '../services/db/connection.db';
|
||||
import { logger } from '../services/logger.server';
|
||||
|
||||
@@ -4,7 +4,8 @@ import supertest from 'supertest';
|
||||
import express, { Request, Response, NextFunction } from 'express';
|
||||
import budgetRouter from './budget.routes';
|
||||
import * as budgetDb from '../services/db/budget.db';
|
||||
import { UserProfile, Budget, SpendingByCategory } from '../types';
|
||||
import { createMockUserProfile, createMockBudget, createMockSpendingByCategory } from '../tests/utils/mockFactories';
|
||||
import { Budget, SpendingByCategory } from '../types';
|
||||
|
||||
// 1. Mock the Service Layer directly.
|
||||
// This decouples the route tests from the database logic.
|
||||
@@ -41,15 +42,7 @@ app.use((err: Error, req: Request, res: Response) => {
|
||||
});
|
||||
|
||||
describe('Budget Routes (/api/budgets)', () => {
|
||||
const mockUserProfile: UserProfile = {
|
||||
user_id: 'user-123',
|
||||
user: { user_id: 'user-123', email: 'test@test.com' },
|
||||
role: 'user',
|
||||
points: 100,
|
||||
full_name: 'Test User',
|
||||
avatar_url: null,
|
||||
preferences: {}
|
||||
};
|
||||
const mockUserProfile = createMockUserProfile({ user_id: 'user-123', points: 100 });
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
@@ -97,7 +90,7 @@ describe('Budget Routes (/api/budgets)', () => {
|
||||
|
||||
describe('GET /', () => {
|
||||
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' }];
|
||||
const mockBudgets = [createMockBudget({ budget_id: 1, user_id: 'user-123' })];
|
||||
// Mock the service function directly
|
||||
vi.mocked(budgetDb.getBudgetsForUser).mockResolvedValue(mockBudgets);
|
||||
|
||||
@@ -112,7 +105,7 @@ describe('Budget Routes (/api/budgets)', () => {
|
||||
describe('POST /', () => {
|
||||
it('should create a new budget and return it', async () => {
|
||||
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 };
|
||||
const mockCreatedBudget = createMockBudget({ budget_id: 2, user_id: 'user-123', ...newBudgetData });
|
||||
// Mock the service function
|
||||
vi.mocked(budgetDb.createBudget).mockResolvedValue(mockCreatedBudget);
|
||||
|
||||
@@ -126,7 +119,7 @@ describe('Budget Routes (/api/budgets)', () => {
|
||||
describe('PUT /:id', () => {
|
||||
it('should update an existing budget', async () => {
|
||||
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' };
|
||||
const mockUpdatedBudget = createMockBudget({ budget_id: 1, user_id: 'user-123', ...budgetUpdates });
|
||||
// Mock the service function
|
||||
vi.mocked(budgetDb.updateBudget).mockResolvedValue(mockUpdatedBudget);
|
||||
|
||||
@@ -151,7 +144,7 @@ describe('Budget Routes (/api/budgets)', () => {
|
||||
|
||||
describe('GET /spending-analysis', () => {
|
||||
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 }];
|
||||
const mockSpendingData = [createMockSpendingByCategory({ category_id: 1, category_name: 'Produce' })];
|
||||
// Mock the service function
|
||||
vi.mocked(budgetDb.getSpendingByCategory).mockResolvedValue(mockSpendingData);
|
||||
|
||||
|
||||
@@ -4,7 +4,8 @@ import supertest from 'supertest';
|
||||
import express, { Request, Response, NextFunction } from 'express';
|
||||
import gamificationRouter from './gamification.routes';
|
||||
import * as gamificationDb from '../services/db/gamification.db';
|
||||
import { UserProfile, Achievement, UserAchievement } from '../types';
|
||||
import { createMockUserProfile, createMockAchievement, createMockUserAchievement } from '../tests/utils/mockFactories';
|
||||
import { Achievement, UserAchievement } from '../types';
|
||||
|
||||
// Mock the entire db service
|
||||
vi.mock('../services/db/gamification.db');
|
||||
@@ -24,7 +25,7 @@ vi.mock('../services/logger.server', () => ({
|
||||
const mockedAuthMiddleware = vi.hoisted(() => vi.fn((req: Request, res: Response, next: NextFunction) => next()));
|
||||
const mockedIsAdmin = vi.hoisted(() => vi.fn());
|
||||
|
||||
vi.mock('./passport', () => ({
|
||||
vi.mock('./passport.routes', () => ({
|
||||
default: {
|
||||
// The authenticate method will now call our hoisted mock middleware.
|
||||
authenticate: vi.fn(() => mockedAuthMiddleware),
|
||||
@@ -39,18 +40,8 @@ app.use(express.json({ strict: false }));
|
||||
app.use('/api/achievements', gamificationRouter);
|
||||
|
||||
describe('Gamification Routes (/api/achievements)', () => {
|
||||
const mockUserProfile: UserProfile = {
|
||||
user_id: 'user-123',
|
||||
user: { user_id: 'user-123', email: 'test@test.com' },
|
||||
role: 'user',
|
||||
points: 100,
|
||||
};
|
||||
const mockAdminProfile: UserProfile = {
|
||||
user_id: 'admin-456',
|
||||
user: { user_id: 'admin-456', email: 'admin@test.com' },
|
||||
role: 'admin',
|
||||
points: 999,
|
||||
};
|
||||
const mockUserProfile = createMockUserProfile({ user_id: 'user-123', points: 100 });
|
||||
const mockAdminProfile = createMockUserProfile({ user_id: 'admin-456', role: 'admin', points: 999 });
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
@@ -65,10 +56,7 @@ describe('Gamification Routes (/api/achievements)', () => {
|
||||
|
||||
describe('GET /', () => {
|
||||
it('should return a list of all achievements (public endpoint)', async () => {
|
||||
const mockAchievements: Achievement[] = [
|
||||
{ achievement_id: 1, name: 'First Steps', description: '...', icon: 'footprints', points_value: 10 },
|
||||
{ achievement_id: 2, name: 'Budget Master', description: '...', icon: 'piggy-bank', points_value: 50 },
|
||||
];
|
||||
const mockAchievements = [createMockAchievement({ achievement_id: 1 }), createMockAchievement({ achievement_id: 2 })];
|
||||
mockedDb.getAllAchievements.mockResolvedValue(mockAchievements);
|
||||
|
||||
const response = await supertest(app).get('/api/achievements');
|
||||
@@ -92,7 +80,7 @@ describe('Gamification Routes (/api/achievements)', () => {
|
||||
next();
|
||||
});
|
||||
|
||||
const mockUserAchievements: (UserAchievement & Achievement)[] = [{ achievement_id: 1, user_id: 'user-123', achieved_at: '2024-01-01', name: 'First Steps', description: '...', icon: 'footprints', points_value: 10 }];
|
||||
const mockUserAchievements = [createMockUserAchievement({ achievement_id: 1, user_id: 'user-123' })];
|
||||
mockedDb.getUserAchievements.mockResolvedValue(mockUserAchievements);
|
||||
|
||||
const response = await supertest(app).get('/api/achievements/me');
|
||||
|
||||
73
src/routes/health.routes.test.ts
Normal file
73
src/routes/health.routes.test.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
// src/routes/health.routes.test.ts
|
||||
import { describe, it, expect, vi, beforeEach, type Mocked } from 'vitest';
|
||||
import supertest from 'supertest';
|
||||
import express from 'express';
|
||||
import healthRouter from './health.routes';
|
||||
import { connection as redisConnection } from '../services/queueService.server';
|
||||
|
||||
// 1. Mock the dependencies of the health router.
|
||||
// In this case, it's the redisConnection from the queueService.
|
||||
vi.mock('../services/queueService.server', () => ({
|
||||
// We need to mock the `connection` export which is an object with a `ping` method.
|
||||
connection: {
|
||||
ping: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
// Mock the logger to keep test output clean.
|
||||
vi.mock('../services/logger.server', () => ({
|
||||
logger: {
|
||||
info: vi.fn(),
|
||||
debug: vi.fn(),
|
||||
error: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
// Cast the mocked import to a Mocked type for type-safe access to mock functions.
|
||||
const mockedRedisConnection = redisConnection as Mocked<typeof redisConnection>;
|
||||
|
||||
// 2. Create a minimal Express app to host the router for testing.
|
||||
const app = express();
|
||||
app.use('/api/health', healthRouter);
|
||||
|
||||
describe('Health Routes (/api/health)', () => {
|
||||
beforeEach(() => {
|
||||
// Clear mock history before each test to ensure isolation.
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('GET /redis', () => {
|
||||
it('should return 200 OK if Redis ping is successful', async () => {
|
||||
// Arrange: Simulate a successful ping by having the mock resolve to 'PONG'.
|
||||
mockedRedisConnection.ping.mockResolvedValue('PONG');
|
||||
|
||||
// Act: Make a request to the endpoint.
|
||||
const response = await supertest(app).get('/api/health/redis');
|
||||
|
||||
// Assert: Check for the correct status and response body.
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body).toEqual({
|
||||
success: true,
|
||||
message: 'Redis connection is healthy.',
|
||||
});
|
||||
});
|
||||
|
||||
it('should return 500 if Redis ping fails', async () => {
|
||||
// Arrange: Simulate a failure by having the mock reject with an error.
|
||||
const redisError = new Error('Connection timed out');
|
||||
mockedRedisConnection.ping.mockRejectedValue(redisError);
|
||||
|
||||
// Act
|
||||
const response = await supertest(app).get('/api/health/redis');
|
||||
|
||||
// Assert
|
||||
expect(response.status).toBe(500);
|
||||
expect(response.body).toEqual({
|
||||
success: false,
|
||||
message: 'Failed to connect to Redis.',
|
||||
error: 'Connection timed out',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,4 +1,4 @@
|
||||
// src/routes/passport.ts
|
||||
// src/routes/passport.routes.ts
|
||||
import passport from 'passport';
|
||||
import { Strategy as LocalStrategy } from 'passport-local';
|
||||
//import { Strategy as GoogleStrategy } from 'passport-google-oauth20';
|
||||
@@ -11,6 +11,7 @@ import * as db from '../services/db/index.db';
|
||||
import { logger } from '../services/logger.server';
|
||||
import { UserProfile } from '../types';
|
||||
import { omit } from '../utils/objectUtils';
|
||||
import { createMockUserProfile } from '../tests/utils/mockFactories';
|
||||
|
||||
const JWT_SECRET = process.env.JWT_SECRET!;
|
||||
|
||||
@@ -201,7 +202,7 @@ const jwtOptions = {
|
||||
};
|
||||
|
||||
passport.use(new JwtStrategy(jwtOptions, async (jwt_payload, done) => {
|
||||
logger.debug('[JWT Strategy] Verifying token payload:', { jwt_payload });
|
||||
logger.debug('[JWT Strategy] Verifying token payload:', { jwt_payload: jwt_payload ? { user_id: jwt_payload.user_id } : 'null' });
|
||||
try {
|
||||
// The jwt_payload contains the data you put into the token during login (e.g., { user_id: user.user_id, email: user.email }).
|
||||
// We re-fetch the user from the database here to ensure they are still active and valid.
|
||||
@@ -243,15 +244,40 @@ export const isAdmin = (req: Request, res: Response, next: NextFunction) => {
|
||||
*/
|
||||
export const optionalAuth = (req: Request, res: Response, next: NextFunction) => {
|
||||
// The custom callback for passport.authenticate gives us access to `err`, `user`, and `info`.
|
||||
passport.authenticate('jwt', { session: false }, (err: Error, user: Express.User | false, info: Error | { message: string }) => {
|
||||
passport.authenticate('jwt', { session: false }, (err: Error | null, user: Express.User | false, info: { message: string } | Error) => {
|
||||
// If there's an authentication error (e.g., malformed token), log it but don't block the request.
|
||||
if (info) {
|
||||
logger.info('Optional auth info:', { info: info.message || info.toString() });
|
||||
}
|
||||
if (user) req.user = user; // Attach user if authentication succeeds
|
||||
if (user) (req as Express.Request).user = user; // Attach user if authentication succeeds
|
||||
|
||||
next(); // Always proceed to the next middleware
|
||||
})(req, res, next);
|
||||
};
|
||||
|
||||
/**
|
||||
* Mock Authentication Middleware for Testing
|
||||
*
|
||||
* This middleware is ONLY active when `process.env.NODE_ENV` is 'test'.
|
||||
* It bypasses the entire JWT authentication flow and directly injects a
|
||||
* mock user object into `req.user`. This is essential for integration tests,
|
||||
* allowing protected routes to be tested without needing to generate valid JWTs
|
||||
* or mock the passport strategy.
|
||||
*
|
||||
* In any environment other than 'test', it does nothing and immediately passes
|
||||
* control to the next middleware.
|
||||
*/
|
||||
export const mockAuth = (req: Request, res: Response, next: NextFunction) => {
|
||||
if (process.env.NODE_ENV === 'test') {
|
||||
// In a test environment, attach a mock user to the request.
|
||||
// We use the mock factory to create a consistent, type-safe user profile.
|
||||
// We override the default role to 'admin' for broad access in tests.
|
||||
req.user = createMockUserProfile({
|
||||
role: 'admin',
|
||||
});
|
||||
}
|
||||
// In production or development, this middleware does nothing.
|
||||
next();
|
||||
};
|
||||
|
||||
export default passport;
|
||||
@@ -7,7 +7,7 @@ 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 { createMockFlyer, createMockFlyerItem, createMockMasterGroceryItem, createMockRecipe } from '../tests/utils/mockFactories';
|
||||
import { Flyer, Recipe } from '../types';
|
||||
|
||||
// 1. Mock the Service Layer directly.
|
||||
@@ -15,6 +15,17 @@ import { Flyer, Recipe } from '../types';
|
||||
vi.mock('../services/db/connection.db');
|
||||
vi.mock('../services/db/flyer.db');
|
||||
vi.mock('../services/db/recipe.db');
|
||||
vi.mock('../services/db/admin.db');
|
||||
|
||||
// Mock the logger to keep test output clean
|
||||
vi.mock('../services/logger.server', () => ({
|
||||
logger: {
|
||||
info: vi.fn(),
|
||||
debug: vi.fn(),
|
||||
error: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
// 2. Mock fs/promises.
|
||||
// We provide both named exports and a default export to support different import styles.
|
||||
@@ -30,16 +41,7 @@ vi.mock('fs/promises', () => {
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
// Mock the logger to keep test output clean
|
||||
vi.mock('../services/logger.server', () => ({
|
||||
logger: {
|
||||
info: vi.fn(),
|
||||
debug: vi.fn(),
|
||||
error: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
},
|
||||
}));
|
||||
import * as fs from 'fs/promises';
|
||||
|
||||
// Create the Express app
|
||||
const app = express();
|
||||
@@ -120,10 +122,7 @@ describe('Public Routes (/api)', () => {
|
||||
|
||||
describe('GET /flyers', () => {
|
||||
it('should return a list of flyers on success', async () => {
|
||||
const mockFlyers: Flyer[] = [
|
||||
{ flyer_id: 1, file_name: 'flyer_a.jpg', image_url: '/a.jpg', created_at: new Date().toISOString(), item_count: 10 },
|
||||
{ flyer_id: 2, file_name: 'flyer_b.jpg', image_url: '/b.jpg', created_at: new Date().toISOString(), item_count: 20 },
|
||||
];
|
||||
const mockFlyers = [createMockFlyer({ flyer_id: 1 }), createMockFlyer({ flyer_id: 2 })];
|
||||
// Mock the service function
|
||||
vi.mocked(flyerDb.getFlyers).mockResolvedValue(mockFlyers);
|
||||
|
||||
@@ -145,7 +144,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() }];
|
||||
const mockItems = [createMockMasterGroceryItem({ master_grocery_item_id: 1, name: 'Milk' })];
|
||||
vi.mocked(flyerDb.getAllMasterItems).mockResolvedValue(mockItems);
|
||||
|
||||
const response = await supertest(app).get('/api/master-items');
|
||||
@@ -157,9 +156,7 @@ describe('Public Routes (/api)', () => {
|
||||
|
||||
describe('GET /flyers/:id/items', () => {
|
||||
it('should return items for a specific flyer', async () => {
|
||||
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' }
|
||||
];
|
||||
const mockFlyerItems = [createMockFlyerItem({ flyer_item_id: 1, flyer_id: 123 })];
|
||||
vi.mocked(flyerDb.getFlyerItems).mockResolvedValue(mockFlyerItems);
|
||||
|
||||
const response = await supertest(app).get('/api/flyers/123/items');
|
||||
@@ -171,9 +168,7 @@ describe('Public Routes (/api)', () => {
|
||||
|
||||
describe('POST /flyer-items/batch-fetch', () => {
|
||||
it('should return items for multiple flyers', async () => {
|
||||
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' }
|
||||
];
|
||||
const mockFlyerItems = [createMockFlyerItem({ flyer_item_id: 1, flyer_id: 1 })];
|
||||
vi.mocked(flyerDb.getFlyerItemsForFlyers).mockResolvedValue(mockFlyerItems);
|
||||
|
||||
const response = await supertest(app)
|
||||
@@ -194,9 +189,7 @@ describe('Public Routes (/api)', () => {
|
||||
|
||||
describe('GET /recipes/by-sale-percentage', () => {
|
||||
it('should return recipes based on sale percentage', async () => {
|
||||
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() },
|
||||
];
|
||||
const mockRecipes = [createMockRecipe({ recipe_id: 1, name: 'Pasta' })];
|
||||
vi.mocked(recipeDb.getRecipesBySalePercentage).mockResolvedValue(mockRecipes);
|
||||
|
||||
const response = await supertest(app).get('/api/recipes/by-sale-percentage?minPercentage=75');
|
||||
@@ -244,9 +237,7 @@ describe('Public Routes (/api)', () => {
|
||||
|
||||
describe('GET /recipes/by-ingredient-and-tag', () => {
|
||||
it('should return recipes for a given ingredient and tag', async () => {
|
||||
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() },
|
||||
];
|
||||
const mockRecipes = [createMockRecipe({ recipe_id: 2, name: 'Chicken Tacos' })];
|
||||
vi.mocked(recipeDb.findRecipesByIngredientAndTag).mockResolvedValue(mockRecipes);
|
||||
|
||||
const response = await supertest(app).get('/api/recipes/by-ingredient-and-tag?ingredient=chicken&tag=quick');
|
||||
|
||||
@@ -19,6 +19,9 @@ vi.mock('child_process', async (importOriginal) => {
|
||||
|
||||
import { exec } from 'child_process';
|
||||
|
||||
// Mock the geocoding service
|
||||
vi.mock('../services/geocodingService.server');
|
||||
import { geocodeAddress } from '../services/geocodingService.server';
|
||||
vi.mock('../services/logger.server', () => ({
|
||||
logger: {
|
||||
info: vi.fn(),
|
||||
@@ -138,4 +141,45 @@ describe('System Routes (/api/system)', () => {
|
||||
expect(response.status).toBe(500);
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /geocode', () => {
|
||||
it('should return geocoded coordinates for a valid address', async () => {
|
||||
// Arrange
|
||||
const mockCoordinates = { lat: 48.4284, lng: -123.3656 };
|
||||
vi.mocked(geocodeAddress).mockResolvedValue(mockCoordinates);
|
||||
|
||||
// Act
|
||||
const response = await supertest(app)
|
||||
.post('/api/system/geocode')
|
||||
.send({ address: 'Victoria, BC' });
|
||||
|
||||
// Assert
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body).toEqual(mockCoordinates);
|
||||
expect(geocodeAddress).toHaveBeenCalledWith('Victoria, BC');
|
||||
});
|
||||
|
||||
it('should return 404 if the address cannot be geocoded', async () => {
|
||||
// Arrange
|
||||
vi.mocked(geocodeAddress).mockResolvedValue(null);
|
||||
|
||||
// Act
|
||||
const response = await supertest(app)
|
||||
.post('/api/system/geocode')
|
||||
.send({ address: 'Invalid Address 12345' });
|
||||
|
||||
// Assert
|
||||
expect(response.status).toBe(404);
|
||||
expect(response.body.message).toBe('Could not geocode the provided address.');
|
||||
});
|
||||
|
||||
it('should return 400 if no address is provided', async () => {
|
||||
// Act
|
||||
const response = await supertest(app).post('/api/system/geocode').send({});
|
||||
|
||||
// Assert
|
||||
expect(response.status).toBe(400);
|
||||
expect(response.body.message).toBe('An address string is required.');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -7,7 +7,8 @@ import userRouter from './user.routes';
|
||||
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';
|
||||
import { createMockUserProfile, createMockMasterGroceryItem, createMockShoppingList, createMockShoppingListItem } from '../tests/utils/mockFactories';
|
||||
import { UserProfile, Appliance } from '../types';
|
||||
|
||||
// 1. Mock the Service Layer directly.
|
||||
vi.mock('../services/db/user.db');
|
||||
@@ -74,15 +75,7 @@ describe('User Routes (/api/users)', () => {
|
||||
});
|
||||
|
||||
describe('when user is authenticated', () => {
|
||||
const mockUserProfile: UserProfile = {
|
||||
user_id: 'user-123',
|
||||
user: { user_id: 'user-123', email: 'test@test.com' },
|
||||
role: 'user',
|
||||
full_name: 'Test User',
|
||||
avatar_url: null,
|
||||
points: 0,
|
||||
preferences: {},
|
||||
};
|
||||
const mockUserProfile = createMockUserProfile({ user_id: 'user-123' });
|
||||
|
||||
beforeEach(() => {
|
||||
// Simulate a logged-in user for this block of tests.
|
||||
@@ -122,13 +115,7 @@ describe('User Routes (/api/users)', () => {
|
||||
describe('GET /watched-items', () => {
|
||||
it('should return a list of watched items', async () => {
|
||||
// Arrange
|
||||
const mockItems: MasterGroceryItem[] = [{
|
||||
master_grocery_item_id: 1,
|
||||
name: 'Milk',
|
||||
created_at: new Date().toISOString(),
|
||||
category_id: 1, // Add missing properties
|
||||
category_name: 'Dairy & Eggs'
|
||||
}];
|
||||
const mockItems = [createMockMasterGroceryItem({ master_grocery_item_id: 1, name: 'Milk' })];
|
||||
vi.mocked(personalizationDb.getWatchedItems).mockResolvedValue(mockItems);
|
||||
|
||||
// Act
|
||||
@@ -144,13 +131,7 @@ describe('User Routes (/api/users)', () => {
|
||||
it('should add an item to the watchlist and return the new item', async () => {
|
||||
// Arrange
|
||||
const newItem = { itemName: 'Organic Bananas', category: 'Produce' };
|
||||
const mockAddedItem: MasterGroceryItem = {
|
||||
master_grocery_item_id: 99,
|
||||
name: 'Organic Bananas',
|
||||
created_at: new Date().toISOString(),
|
||||
category_id: 1, // Add missing properties
|
||||
category_name: 'Produce'
|
||||
};
|
||||
const mockAddedItem = createMockMasterGroceryItem({ master_grocery_item_id: 99, name: 'Organic Bananas', category_name: 'Produce' });
|
||||
vi.mocked(personalizationDb.addWatchedItem).mockResolvedValue(mockAddedItem);
|
||||
|
||||
// Act
|
||||
@@ -180,7 +161,7 @@ describe('User Routes (/api/users)', () => {
|
||||
|
||||
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: [] }];
|
||||
const mockLists = [createMockShoppingList({ shopping_list_id: 1, user_id: mockUserProfile.user_id })];
|
||||
vi.mocked(shoppingDb.getShoppingLists).mockResolvedValue(mockLists);
|
||||
|
||||
const response = await supertest(app).get('/api/users/shopping-lists');
|
||||
@@ -190,7 +171,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: [] };
|
||||
const mockNewList = createMockShoppingList({ shopping_list_id: 2, user_id: mockUserProfile.user_id, name: 'Party Supplies' });
|
||||
vi.mocked(shoppingDb.createShoppingList).mockResolvedValue(mockNewList);
|
||||
|
||||
const response = await supertest(app)
|
||||
@@ -212,14 +193,7 @@ describe('User Routes (/api/users)', () => {
|
||||
it('POST /shopping-lists/:listId/items should add an item to a list', async () => {
|
||||
const listId = 1;
|
||||
const itemData = { customItemName: 'Paper Towels' };
|
||||
const mockAddedItem: ShoppingListItem = {
|
||||
shopping_list_item_id: 101,
|
||||
shopping_list_id: listId,
|
||||
quantity: 1,
|
||||
is_purchased: false,
|
||||
added_at: new Date().toISOString(),
|
||||
...itemData
|
||||
};
|
||||
const mockAddedItem = createMockShoppingListItem({ shopping_list_item_id: 101, shopping_list_id: listId, ...itemData });
|
||||
vi.mocked(shoppingDb.addShoppingListItem).mockResolvedValue(mockAddedItem);
|
||||
|
||||
const response = await supertest(app)
|
||||
@@ -233,13 +207,7 @@ describe('User Routes (/api/users)', () => {
|
||||
it('PUT /shopping-lists/items/:itemId should update an item', async () => {
|
||||
const itemId = 101;
|
||||
const updates = { is_purchased: true, quantity: 2 };
|
||||
const mockUpdatedItem: ShoppingListItem = {
|
||||
shopping_list_item_id: itemId,
|
||||
shopping_list_id: 1,
|
||||
added_at: new Date().toISOString(),
|
||||
custom_item_name: 'Item', // Add missing property
|
||||
...updates
|
||||
};
|
||||
const mockUpdatedItem = createMockShoppingListItem({ shopping_list_item_id: itemId, shopping_list_id: 1, ...updates });
|
||||
vi.mocked(shoppingDb.updateShoppingListItem).mockResolvedValue(mockUpdatedItem);
|
||||
|
||||
const response = await supertest(app)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// src/services/db/admin.db.test.ts
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { mockPoolInstance } from '../../tests/setup/tests-setup-unit';
|
||||
import {
|
||||
getSuggestedCorrections,
|
||||
approveCorrection,
|
||||
@@ -16,20 +17,8 @@ import {
|
||||
updateRecipeStatus,
|
||||
updateReceiptStatus,
|
||||
} from './admin.db';
|
||||
import { getPool } from './connection.db';
|
||||
import type { SuggestedCorrection } from '../../types';
|
||||
|
||||
// Define test-local mock functions. These will be used to control the mock's behavior.
|
||||
const mockQuery = vi.fn();
|
||||
|
||||
// Mock the entire connection module.
|
||||
vi.mock('./connection', () => ({
|
||||
// The mock factory for getPool returns an object that uses our test-local mockQuery.
|
||||
getPool: () => ({
|
||||
query: mockQuery,
|
||||
}),
|
||||
}));
|
||||
|
||||
// Mock the logger to prevent console output during tests
|
||||
vi.mock('../logger', () => ({
|
||||
logger: {
|
||||
@@ -42,8 +31,7 @@ vi.mock('../logger', () => ({
|
||||
|
||||
describe('Admin DB Service', () => {
|
||||
beforeEach(() => {
|
||||
// FIX: Reset mocks
|
||||
mockQuery.mockReset();
|
||||
// Reset the global mock's call history before each test.
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
@@ -52,30 +40,30 @@ describe('Admin DB Service', () => {
|
||||
const mockCorrections: SuggestedCorrection[] = [
|
||||
{ suggested_correction_id: 1, flyer_item_id: 101, user_id: 'user-1', correction_type: 'WRONG_PRICE', suggested_value: '250', status: 'pending', created_at: new Date().toISOString() },
|
||||
];
|
||||
mockQuery.mockResolvedValue({ rows: mockCorrections });
|
||||
mockPoolInstance.query.mockResolvedValue({ rows: mockCorrections });
|
||||
|
||||
const result = await getSuggestedCorrections();
|
||||
|
||||
expect(getPool().query).toHaveBeenCalledWith(expect.stringContaining("FROM public.suggested_corrections sc"));
|
||||
expect(mockPoolInstance.query).toHaveBeenCalledWith(expect.stringContaining("FROM public.suggested_corrections sc"));
|
||||
expect(result).toEqual(mockCorrections);
|
||||
});
|
||||
});
|
||||
|
||||
describe('approveCorrection', () => {
|
||||
it('should call the approve_correction database function', async () => {
|
||||
mockQuery.mockResolvedValue({ rows: [] }); // Mock the function call
|
||||
mockPoolInstance.query.mockResolvedValue({ rows: [] }); // Mock the function call
|
||||
await approveCorrection(123);
|
||||
|
||||
expect(getPool().query).toHaveBeenCalledWith('SELECT public.approve_correction($1)', [123]);
|
||||
expect(mockPoolInstance.query).toHaveBeenCalledWith('SELECT public.approve_correction($1)', [123]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('rejectCorrection', () => {
|
||||
it('should update the correction status to rejected', async () => {
|
||||
mockQuery.mockResolvedValue({ rowCount: 1 });
|
||||
mockPoolInstance.query.mockResolvedValue({ rowCount: 1 });
|
||||
await rejectCorrection(123);
|
||||
|
||||
expect(getPool().query).toHaveBeenCalledWith(
|
||||
expect(mockPoolInstance.query).toHaveBeenCalledWith(
|
||||
expect.stringContaining("UPDATE public.suggested_corrections SET status = 'rejected'"),
|
||||
[123]
|
||||
);
|
||||
@@ -85,11 +73,11 @@ describe('Admin DB Service', () => {
|
||||
describe('updateSuggestedCorrection', () => {
|
||||
it('should update the suggested value and return the updated correction', async () => {
|
||||
const mockCorrection: SuggestedCorrection = { suggested_correction_id: 1, flyer_item_id: 101, user_id: 'user-1', correction_type: 'WRONG_PRICE', suggested_value: '300', status: 'pending', created_at: new Date().toISOString() };
|
||||
mockQuery.mockResolvedValue({ rows: [mockCorrection], rowCount: 1 });
|
||||
mockPoolInstance.query.mockResolvedValue({ rows: [mockCorrection], rowCount: 1 });
|
||||
|
||||
const result = await updateSuggestedCorrection(1, '300');
|
||||
|
||||
expect(getPool().query).toHaveBeenCalledWith(
|
||||
expect(mockPoolInstance.query).toHaveBeenCalledWith(
|
||||
expect.stringContaining("UPDATE public.suggested_corrections SET suggested_value = $1"),
|
||||
['300', 1]
|
||||
);
|
||||
@@ -100,7 +88,7 @@ describe('Admin DB Service', () => {
|
||||
describe('getApplicationStats', () => {
|
||||
it('should execute 5 parallel count queries and return the aggregated stats', async () => {
|
||||
// Mock responses for each of the 5 parallel queries
|
||||
mockQuery
|
||||
mockPoolInstance.query
|
||||
.mockResolvedValueOnce({ rows: [{ count: '10' }] }) // flyerCount
|
||||
.mockResolvedValueOnce({ rows: [{ count: '20' }] }) // userCount
|
||||
.mockResolvedValueOnce({ rows: [{ count: '300' }] }) // flyerItemCount
|
||||
@@ -109,7 +97,7 @@ describe('Admin DB Service', () => {
|
||||
|
||||
const stats = await getApplicationStats();
|
||||
|
||||
expect(getPool().query).toHaveBeenCalledTimes(5);
|
||||
expect(mockPoolInstance.query).toHaveBeenCalledTimes(5);
|
||||
expect(stats).toEqual({
|
||||
flyerCount: 10,
|
||||
userCount: 20,
|
||||
@@ -123,22 +111,22 @@ describe('Admin DB Service', () => {
|
||||
describe('getDailyStatsForLast30Days', () => {
|
||||
it('should execute the correct query to get daily stats', async () => {
|
||||
const mockStats = [{ date: '2023-01-01', new_users: 5, new_flyers: 2 }];
|
||||
mockQuery.mockResolvedValue({ rows: mockStats });
|
||||
mockPoolInstance.query.mockResolvedValue({ rows: mockStats });
|
||||
|
||||
const result = await getDailyStatsForLast30Days();
|
||||
|
||||
expect(getPool().query).toHaveBeenCalledWith(expect.stringContaining("WITH date_series AS"));
|
||||
expect(mockPoolInstance.query).toHaveBeenCalledWith(expect.stringContaining("WITH date_series AS"));
|
||||
expect(result).toEqual(mockStats);
|
||||
});
|
||||
});
|
||||
|
||||
describe('logActivity', () => {
|
||||
it('should insert a new activity log entry', async () => {
|
||||
mockQuery.mockResolvedValue({ rows: [] });
|
||||
mockPoolInstance.query.mockResolvedValue({ rows: [] });
|
||||
const logData = { userId: 'user-123', action: 'test_action', displayText: 'Test activity' };
|
||||
await logActivity(logData);
|
||||
|
||||
expect(getPool().query).toHaveBeenCalledWith(
|
||||
expect(mockPoolInstance.query).toHaveBeenCalledWith(
|
||||
expect.stringContaining("INSERT INTO public.activity_log"),
|
||||
[logData.userId, logData.action, logData.displayText, null, null]
|
||||
);
|
||||
@@ -147,47 +135,47 @@ describe('Admin DB Service', () => {
|
||||
|
||||
describe('getMostFrequentSaleItems', () => {
|
||||
it('should call the correct database function', async () => {
|
||||
mockQuery.mockResolvedValue({ rows: [] });
|
||||
mockPoolInstance.query.mockResolvedValue({ rows: [] });
|
||||
await getMostFrequentSaleItems(30, 10);
|
||||
expect(getPool().query).toHaveBeenCalledWith(expect.stringContaining('FROM public.flyer_items fi'), [30, 10]);
|
||||
expect(mockPoolInstance.query).toHaveBeenCalledWith(expect.stringContaining('FROM public.flyer_items fi'), [30, 10]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateRecipeCommentStatus', () => {
|
||||
it('should update the comment status and return the updated comment', async () => {
|
||||
const mockComment = { comment_id: 1, status: 'hidden' };
|
||||
mockQuery.mockResolvedValue({ rows: [mockComment], rowCount: 1 });
|
||||
mockPoolInstance.query.mockResolvedValue({ rows: [mockComment], rowCount: 1 });
|
||||
const result = await updateRecipeCommentStatus(1, 'hidden');
|
||||
expect(getPool().query).toHaveBeenCalledWith(expect.stringContaining('UPDATE public.recipe_comments'), ['hidden', 1]);
|
||||
expect(mockPoolInstance.query).toHaveBeenCalledWith(expect.stringContaining('UPDATE public.recipe_comments'), ['hidden', 1]);
|
||||
expect(result).toEqual(mockComment);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getUnmatchedFlyerItems', () => {
|
||||
it('should execute the correct query to get unmatched items', async () => {
|
||||
mockQuery.mockResolvedValue({ rows: [] });
|
||||
mockPoolInstance.query.mockResolvedValue({ rows: [] });
|
||||
await getUnmatchedFlyerItems();
|
||||
expect(getPool().query).toHaveBeenCalledWith(expect.stringContaining('FROM public.unmatched_flyer_items ufi'));
|
||||
expect(mockPoolInstance.query).toHaveBeenCalledWith(expect.stringContaining('FROM public.unmatched_flyer_items ufi'));
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateRecipeStatus', () => {
|
||||
it('should update the recipe status and return the updated recipe', async () => {
|
||||
const mockRecipe = { recipe_id: 1, status: 'public' };
|
||||
mockQuery.mockResolvedValue({ rows: [mockRecipe], rowCount: 1 });
|
||||
mockPoolInstance.query.mockResolvedValue({ rows: [mockRecipe], rowCount: 1 });
|
||||
const result = await updateRecipeStatus(1, 'public');
|
||||
expect(getPool().query).toHaveBeenCalledWith(expect.stringContaining('UPDATE public.recipes'), ['public', 1]);
|
||||
expect(mockPoolInstance.query).toHaveBeenCalledWith(expect.stringContaining('UPDATE public.recipes'), ['public', 1]);
|
||||
expect(result).toEqual(mockRecipe);
|
||||
});
|
||||
});
|
||||
|
||||
describe('incrementFailedLoginAttempts', () => {
|
||||
it('should execute an UPDATE query to increment failed attempts', async () => {
|
||||
mockQuery.mockResolvedValue({ rows: [] });
|
||||
mockPoolInstance.query.mockResolvedValue({ rows: [] });
|
||||
await incrementFailedLoginAttempts('user-123');
|
||||
|
||||
// Fix: Use regex to match query with variable whitespace
|
||||
expect(getPool().query).toHaveBeenCalledWith(
|
||||
expect(mockPoolInstance.query).toHaveBeenCalledWith(
|
||||
expect.stringMatching(/UPDATE\s+public\.users\s+SET\s+failed_login_attempts\s*=\s*failed_login_attempts\s*\+\s*1/),
|
||||
['user-123']
|
||||
);
|
||||
@@ -196,18 +184,18 @@ describe('Admin DB Service', () => {
|
||||
|
||||
describe('updateBrandLogo', () => {
|
||||
it('should execute an UPDATE query for the brand logo', async () => {
|
||||
mockQuery.mockResolvedValue({ rows: [] });
|
||||
mockPoolInstance.query.mockResolvedValue({ rows: [] });
|
||||
await updateBrandLogo(1, '/logo.png');
|
||||
expect(getPool().query).toHaveBeenCalledWith('UPDATE public.brands SET logo_url = $1 WHERE brand_id = $2', ['/logo.png', 1]);
|
||||
expect(mockPoolInstance.query).toHaveBeenCalledWith('UPDATE public.brands SET logo_url = $1 WHERE brand_id = $2', ['/logo.png', 1]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateReceiptStatus', () => {
|
||||
it('should update the receipt status and return the updated receipt', async () => {
|
||||
const mockReceipt = { receipt_id: 1, status: 'completed' };
|
||||
mockQuery.mockResolvedValue({ rows: [mockReceipt], rowCount: 1 });
|
||||
mockPoolInstance.query.mockResolvedValue({ rows: [mockReceipt], rowCount: 1 });
|
||||
const result = await updateReceiptStatus(1, 'completed');
|
||||
expect(getPool().query).toHaveBeenCalledWith(expect.stringContaining('UPDATE public.receipts'), ['completed', 1]);
|
||||
expect(mockPoolInstance.query).toHaveBeenCalledWith(expect.stringContaining('UPDATE public.receipts'), ['completed', 1]);
|
||||
expect(result).toEqual(mockReceipt);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// src/services/db/gamification.db.test.ts
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { mockPoolInstance } from '../../tests/setup/tests-setup-unit';
|
||||
import {
|
||||
getAllAchievements,
|
||||
getUserAchievements,
|
||||
@@ -7,17 +8,6 @@ import {
|
||||
getLeaderboard,
|
||||
} from './gamification.db';
|
||||
import type { Achievement, UserAchievement, LeaderboardUser } from '../../types';
|
||||
import { getPool } from './connection.db';
|
||||
|
||||
// Mock the getPool function to return a mocked pool object.
|
||||
const mockQuery = vi.fn();
|
||||
|
||||
// We mock the connection module to control the 'getPool' output directly.
|
||||
vi.mock('./connection', () => ({
|
||||
getPool: () => ({
|
||||
query: mockQuery,
|
||||
}),
|
||||
}));
|
||||
|
||||
// Mock the logger
|
||||
vi.mock('../logger', () => ({
|
||||
@@ -31,8 +21,8 @@ vi.mock('../logger', () => ({
|
||||
|
||||
describe('Gamification DB Service', () => {
|
||||
beforeEach(() => {
|
||||
// Reset the global mock's call history before each test.
|
||||
vi.clearAllMocks();
|
||||
console.log('[gamification.db.test.ts] Mocks cleared');
|
||||
});
|
||||
|
||||
describe('getAllAchievements', () => {
|
||||
@@ -40,11 +30,11 @@ describe('Gamification DB Service', () => {
|
||||
const mockAchievements: Achievement[] = [
|
||||
{ achievement_id: 1, name: 'First Steps', description: '...', icon: 'footprints', points_value: 10 },
|
||||
];
|
||||
mockQuery.mockResolvedValue({ rows: mockAchievements });
|
||||
mockPoolInstance.query.mockResolvedValue({ rows: mockAchievements });
|
||||
|
||||
const result = await getAllAchievements();
|
||||
|
||||
expect(getPool().query).toHaveBeenCalledWith('SELECT * FROM public.achievements ORDER BY points_value ASC, name ASC');
|
||||
expect(mockPoolInstance.query).toHaveBeenCalledWith('SELECT * FROM public.achievements ORDER BY points_value ASC, name ASC');
|
||||
expect(result).toEqual(mockAchievements);
|
||||
});
|
||||
});
|
||||
@@ -54,21 +44,21 @@ describe('Gamification DB Service', () => {
|
||||
const mockUserAchievements: (UserAchievement & Achievement)[] = [
|
||||
{ achievement_id: 1, user_id: 'user-123', achieved_at: '2024-01-01', name: 'First Steps', description: '...', icon: 'footprints', points_value: 10 },
|
||||
];
|
||||
mockQuery.mockResolvedValue({ rows: mockUserAchievements });
|
||||
mockPoolInstance.query.mockResolvedValue({ rows: mockUserAchievements });
|
||||
|
||||
const result = await getUserAchievements('user-123');
|
||||
|
||||
expect(getPool().query).toHaveBeenCalledWith(expect.stringContaining('FROM public.user_achievements ua'), ['user-123']);
|
||||
expect(mockPoolInstance.query).toHaveBeenCalledWith(expect.stringContaining('FROM public.user_achievements ua'), ['user-123']);
|
||||
expect(result).toEqual(mockUserAchievements);
|
||||
});
|
||||
});
|
||||
|
||||
describe('awardAchievement', () => {
|
||||
it('should call the award_achievement database function with the correct parameters', async () => {
|
||||
mockQuery.mockResolvedValue({ rows: [] }); // The function returns void
|
||||
mockPoolInstance.query.mockResolvedValue({ rows: [] }); // The function returns void
|
||||
await awardAchievement('user-123', 'Test Achievement');
|
||||
|
||||
expect(getPool().query).toHaveBeenCalledWith("SELECT public.award_achievement($1, $2)", ['user-123', 'Test Achievement']);
|
||||
expect(mockPoolInstance.query).toHaveBeenCalledWith("SELECT public.award_achievement($1, $2)", ['user-123', 'Test Achievement']);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -78,12 +68,12 @@ describe('Gamification DB Service', () => {
|
||||
{ user_id: 'user-1', full_name: 'User One', avatar_url: null, points: 500, rank: '1' },
|
||||
{ user_id: 'user-2', full_name: 'User Two', avatar_url: null, points: 450, rank: '2' },
|
||||
];
|
||||
mockQuery.mockResolvedValue({ rows: mockLeaderboard });
|
||||
mockPoolInstance.query.mockResolvedValue({ rows: mockLeaderboard });
|
||||
|
||||
const result = await getLeaderboard(10);
|
||||
|
||||
expect(getPool().query).toHaveBeenCalledTimes(1);
|
||||
expect(getPool().query).toHaveBeenCalledWith(expect.stringContaining('RANK() OVER (ORDER BY points DESC)'), [10]);
|
||||
expect(mockPoolInstance.query).toHaveBeenCalledTimes(1);
|
||||
expect(mockPoolInstance.query).toHaveBeenCalledWith(expect.stringContaining('RANK() OVER (ORDER BY points DESC)'), [10]);
|
||||
expect(result).toEqual(mockLeaderboard);
|
||||
});
|
||||
});
|
||||
|
||||
361
src/tests/utils/mockFactories.ts
Normal file
361
src/tests/utils/mockFactories.ts
Normal file
@@ -0,0 +1,361 @@
|
||||
// src/tests/utils/mockFactories.ts
|
||||
import { UserProfile, User, Flyer, Store, SuggestedCorrection, Brand, FlyerItem, MasterGroceryItem, ShoppingList, ShoppingListItem, Achievement, UserAchievement, Budget, SpendingByCategory, Recipe, RecipeComment, ActivityLogItem } from '../../types';
|
||||
|
||||
/**
|
||||
* Creates a mock UserProfile object for use in tests, ensuring type safety.
|
||||
*
|
||||
* @param overrides - An object containing properties to override the default mock values.
|
||||
* This allows for easy customization of the mock user for specific test cases.
|
||||
* For example: `createMockUserProfile({ role: 'admin', points: 500 })`
|
||||
* @returns A complete and type-safe UserProfile object.
|
||||
*/
|
||||
export const createMockUserProfile = (overrides: Partial<UserProfile & { user: Partial<User> }> = {}): UserProfile => {
|
||||
const userId = overrides.user_id ?? `user-${Math.random().toString(36).substring(2, 9)}`;
|
||||
|
||||
const defaultProfile: UserProfile = {
|
||||
user_id: userId,
|
||||
role: 'user',
|
||||
points: 0,
|
||||
full_name: 'Test User',
|
||||
avatar_url: null,
|
||||
preferences: {},
|
||||
address: null,
|
||||
user: {
|
||||
user_id: userId,
|
||||
email: `${userId}@example.com`,
|
||||
...overrides.user, // Apply nested user overrides
|
||||
},
|
||||
};
|
||||
|
||||
return { ...defaultProfile, ...overrides };
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a mock Flyer object for use in tests, ensuring type safety.
|
||||
*
|
||||
* @param overrides - An object containing properties to override the default mock values,
|
||||
* including nested properties for the `store`.
|
||||
* e.g., `createMockFlyer({ item_count: 50, store: { name: 'Walmart' } })`
|
||||
* @returns A complete and type-safe Flyer object.
|
||||
*/
|
||||
export const createMockFlyer = (overrides: Partial<Flyer & { store: Partial<Store> }> = {}): Flyer => {
|
||||
const flyerId = overrides.flyer_id ?? Math.floor(Math.random() * 1000);
|
||||
const storeId = overrides.store?.store_id ?? Math.floor(Math.random() * 100);
|
||||
|
||||
const defaultFlyer: Flyer = {
|
||||
flyer_id: flyerId,
|
||||
created_at: new Date().toISOString(),
|
||||
file_name: `flyer-${flyerId}.jpg`,
|
||||
image_url: `/flyer-images/flyer-${flyerId}.jpg`,
|
||||
icon_url: `/flyer-images/icons/icon-flyer-${flyerId}.webp`,
|
||||
checksum: `checksum-${flyerId}`,
|
||||
store_id: storeId,
|
||||
valid_from: new Date().toISOString().split('T')[0],
|
||||
valid_to: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString().split('T')[0], // 7 days from now
|
||||
store_address: '123 Main St, Anytown, USA',
|
||||
item_count: Math.floor(Math.random() * 100) + 10,
|
||||
uploaded_by: null,
|
||||
store: {
|
||||
store_id: storeId,
|
||||
created_at: new Date().toISOString(),
|
||||
name: 'Mock Store',
|
||||
logo_url: null,
|
||||
},
|
||||
};
|
||||
|
||||
// Deep merge the store object and then merge the top-level properties.
|
||||
return { ...defaultFlyer, ...overrides, store: { ...defaultFlyer.store, ...overrides.store } as Store };
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a mock SuggestedCorrection object for use in tests.
|
||||
* @param overrides - An object containing properties to override the default mock values.
|
||||
* @returns A complete and type-safe SuggestedCorrection object.
|
||||
*/
|
||||
export const createMockSuggestedCorrection = (overrides: Partial<SuggestedCorrection> = {}): SuggestedCorrection => {
|
||||
const defaultCorrection: SuggestedCorrection = {
|
||||
suggested_correction_id: Math.floor(Math.random() * 1000),
|
||||
flyer_item_id: Math.floor(Math.random() * 10000),
|
||||
user_id: `user-${Math.random().toString(36).substring(2, 9)}`,
|
||||
correction_type: 'price',
|
||||
suggested_value: '$9.99',
|
||||
status: 'pending',
|
||||
created_at: new Date().toISOString(),
|
||||
};
|
||||
|
||||
return { ...defaultCorrection, ...overrides };
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a mock Brand object for use in tests.
|
||||
* @param overrides - An object containing properties to override the default mock values.
|
||||
* @returns A complete and type-safe Brand object.
|
||||
*/
|
||||
export const createMockBrand = (overrides: Partial<Brand> = {}): Brand => {
|
||||
const brandId = overrides.brand_id ?? Math.floor(Math.random() * 100);
|
||||
|
||||
const defaultBrand: Brand = {
|
||||
brand_id: brandId,
|
||||
name: `Brand ${brandId}`,
|
||||
logo_url: null,
|
||||
store_id: null,
|
||||
store_name: null,
|
||||
};
|
||||
|
||||
return { ...defaultBrand, ...overrides };
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a mock FlyerItem object for use in tests.
|
||||
* @param overrides - An object containing properties to override the default mock values.
|
||||
* @returns A complete and type-safe FlyerItem object.
|
||||
*/
|
||||
export const createMockFlyerItem = (overrides: Partial<FlyerItem> = {}): FlyerItem => {
|
||||
const defaultItem: FlyerItem = {
|
||||
flyer_item_id: Math.floor(Math.random() * 10000),
|
||||
flyer_id: Math.floor(Math.random() * 1000),
|
||||
created_at: new Date().toISOString(),
|
||||
item: 'Mock Item',
|
||||
price_display: '$1.99',
|
||||
price_in_cents: 199,
|
||||
quantity: 'each',
|
||||
view_count: 0,
|
||||
click_count: 0,
|
||||
updated_at: new Date().toISOString(),
|
||||
};
|
||||
|
||||
return { ...defaultItem, ...overrides };
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a mock Recipe object for use in tests.
|
||||
* @param overrides - An object containing properties to override the default mock values.
|
||||
* @returns A complete and type-safe Recipe object.
|
||||
*/
|
||||
export const createMockRecipe = (overrides: Partial<Recipe> = {}): Recipe => {
|
||||
const recipeId = overrides.recipe_id ?? Math.floor(Math.random() * 1000);
|
||||
|
||||
const defaultRecipe: Recipe = {
|
||||
recipe_id: recipeId,
|
||||
user_id: `user-${Math.random().toString(36).substring(2, 9)}`,
|
||||
name: `Mock Recipe ${recipeId}`,
|
||||
description: 'A delicious mock recipe.',
|
||||
instructions: '1. Mock the ingredients. 2. Mock the cooking. 3. Enjoy!',
|
||||
avg_rating: Math.random() * 5,
|
||||
rating_count: Math.floor(Math.random() * 100),
|
||||
fork_count: Math.floor(Math.random() * 20),
|
||||
status: 'public',
|
||||
created_at: new Date().toISOString(),
|
||||
prep_time_minutes: 15,
|
||||
cook_time_minutes: 30,
|
||||
servings: 4,
|
||||
};
|
||||
|
||||
return { ...defaultRecipe, ...overrides };
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a mock RecipeComment object for use in tests.
|
||||
* @param overrides - An object containing properties to override the default mock values.
|
||||
* @returns A complete and type-safe RecipeComment object.
|
||||
*/
|
||||
export const createMockRecipeComment = (overrides: Partial<RecipeComment> = {}): RecipeComment => {
|
||||
const defaultComment: RecipeComment = {
|
||||
recipe_comment_id: Math.floor(Math.random() * 10000),
|
||||
recipe_id: Math.floor(Math.random() * 1000),
|
||||
user_id: `user-${Math.random().toString(36).substring(2, 9)}`,
|
||||
content: 'This is a mock comment.',
|
||||
status: 'visible',
|
||||
created_at: new Date().toISOString(),
|
||||
user_full_name: 'Mock User', // This was correct
|
||||
user_avatar_url: undefined,
|
||||
};
|
||||
|
||||
return { ...defaultComment, ...overrides };
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a mock ActivityLogItem object for use in tests.
|
||||
* This factory handles the discriminated union nature of the ActivityLogItem type.
|
||||
* By default, it creates a 'flyer_processed' log item. You can override the 'action'
|
||||
* and 'details' to create other types of log items.
|
||||
*
|
||||
* @param overrides - An object containing properties to override the default mock values.
|
||||
* e.g., `createMockActivityLogItem({ action: 'user_registered', details: { full_name: 'New User' } })`
|
||||
* @returns A complete and type-safe ActivityLogItem object.
|
||||
*/
|
||||
export const createMockActivityLogItem = (overrides: Partial<ActivityLogItem> = {}): ActivityLogItem => {
|
||||
const action = overrides.action ?? 'flyer_processed';
|
||||
|
||||
const baseLog = {
|
||||
activity_log_id: Math.floor(Math.random() * 10000),
|
||||
user_id: `user-${Math.random().toString(36).substring(2, 9)}`,
|
||||
created_at: new Date().toISOString(),
|
||||
};
|
||||
|
||||
let specificLog: ActivityLogItem;
|
||||
|
||||
// Create a default log based on the action, which can then be overridden.
|
||||
switch (action) {
|
||||
case 'recipe_created':
|
||||
specificLog = {
|
||||
...baseLog,
|
||||
action: 'recipe_created',
|
||||
display_text: 'Created a new recipe: Mock Recipe.',
|
||||
icon: 'chef-hat',
|
||||
details: { recipe_id: 1, recipe_name: 'Mock Recipe' },
|
||||
};
|
||||
break;
|
||||
case 'user_registered':
|
||||
specificLog = {
|
||||
...baseLog,
|
||||
action: 'user_registered',
|
||||
display_text: 'A new user has registered.',
|
||||
icon: 'user-plus',
|
||||
details: { full_name: 'New Mock User' },
|
||||
};
|
||||
break;
|
||||
case 'list_shared':
|
||||
specificLog = {
|
||||
...baseLog,
|
||||
action: 'list_shared',
|
||||
display_text: 'A shopping list was shared.',
|
||||
icon: 'share-2',
|
||||
details: { list_name: 'Mock List', shopping_list_id: 1, shared_with_name: 'Another User' },
|
||||
};
|
||||
break;
|
||||
case 'flyer_processed':
|
||||
default:
|
||||
specificLog = {
|
||||
...baseLog,
|
||||
action: 'flyer_processed',
|
||||
display_text: 'Processed a new flyer for Mock Store.',
|
||||
icon: 'file-check',
|
||||
details: { flyer_id: 1, store_name: 'Mock Store' },
|
||||
};
|
||||
break;
|
||||
}
|
||||
|
||||
// Merge the generated log with any specific overrides provided.
|
||||
// This allows for deep merging of the 'details' object.
|
||||
return { ...specificLog, ...overrides, details: { ...specificLog.details, ...overrides.details } } as ActivityLogItem;
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a mock Achievement object for use in tests.
|
||||
* @param overrides - An object containing properties to override the default mock values.
|
||||
* @returns A complete and type-safe Achievement object.
|
||||
*/
|
||||
export const createMockAchievement = (overrides: Partial<Achievement> = {}): Achievement => {
|
||||
const defaultAchievement: Achievement = {
|
||||
achievement_id: Math.floor(Math.random() * 100),
|
||||
name: 'Mock Achievement',
|
||||
description: 'A great accomplishment.',
|
||||
icon: 'star',
|
||||
points_value: 10,
|
||||
};
|
||||
return { ...defaultAchievement, ...overrides };
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a mock object representing a joined UserAchievement and Achievement for use in tests.
|
||||
* @param overrides - An object containing properties to override the default mock values.
|
||||
* @returns A complete and type-safe object representing the joined achievement data.
|
||||
*/
|
||||
export const createMockUserAchievement = (overrides: Partial<UserAchievement & Achievement> = {}): UserAchievement & Achievement => {
|
||||
const achievementId = overrides.achievement_id ?? Math.floor(Math.random() * 100);
|
||||
const defaultUserAchievement: UserAchievement & Achievement = {
|
||||
user_id: `user-${Math.random().toString(36).substring(2, 9)}`,
|
||||
achievement_id: achievementId,
|
||||
achieved_at: new Date().toISOString(),
|
||||
// from Achievement
|
||||
name: 'Mock User Achievement',
|
||||
description: 'An achievement someone earned.',
|
||||
icon: 'award',
|
||||
points_value: 20,
|
||||
};
|
||||
return { ...defaultUserAchievement, ...overrides };
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a mock Budget object for use in tests.
|
||||
* @param overrides - An object containing properties to override the default mock values.
|
||||
* @returns A complete and type-safe Budget object.
|
||||
*/
|
||||
export const createMockBudget = (overrides: Partial<Budget> = {}): Budget => {
|
||||
const defaultBudget: Budget = {
|
||||
budget_id: Math.floor(Math.random() * 100),
|
||||
user_id: `user-${Math.random().toString(36).substring(2, 9)}`,
|
||||
name: 'Monthly Groceries',
|
||||
amount_cents: 50000,
|
||||
period: 'monthly',
|
||||
start_date: new Date().toISOString().split('T')[0],
|
||||
};
|
||||
return { ...defaultBudget, ...overrides };
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a mock SpendingByCategory object for use in tests.
|
||||
* @param overrides - An object containing properties to override the default mock values.
|
||||
* @returns A complete and type-safe SpendingByCategory object.
|
||||
*/
|
||||
export const createMockSpendingByCategory = (overrides: Partial<SpendingByCategory> = {}): SpendingByCategory => {
|
||||
const defaultSpending: SpendingByCategory = {
|
||||
category_id: Math.floor(Math.random() * 20) + 1,
|
||||
category_name: 'Produce',
|
||||
total_spent_cents: Math.floor(Math.random() * 20000) + 1000,
|
||||
};
|
||||
return { ...defaultSpending, ...overrides };
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a mock MasterGroceryItem object for use in tests.
|
||||
* @param overrides - An object containing properties to override the default mock values.
|
||||
* @returns A complete and type-safe MasterGroceryItem object.
|
||||
*/
|
||||
export const createMockMasterGroceryItem = (overrides: Partial<MasterGroceryItem> = {}): MasterGroceryItem => {
|
||||
const defaultItem: MasterGroceryItem = {
|
||||
master_grocery_item_id: Math.floor(Math.random() * 10000),
|
||||
created_at: new Date().toISOString(),
|
||||
name: 'Mock Master Item',
|
||||
category_id: 1,
|
||||
category_name: 'Pantry & Dry Goods',
|
||||
};
|
||||
|
||||
return { ...defaultItem, ...overrides };
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a mock ShoppingList object for use in tests.
|
||||
* @param overrides - An object containing properties to override the default mock values.
|
||||
* @returns A complete and type-safe ShoppingList object.
|
||||
*/
|
||||
export const createMockShoppingList = (overrides: Partial<ShoppingList> = {}): ShoppingList => {
|
||||
const defaultList: ShoppingList = {
|
||||
shopping_list_id: Math.floor(Math.random() * 100),
|
||||
user_id: `user-${Math.random().toString(36).substring(2, 9)}`,
|
||||
name: 'My Mock List',
|
||||
created_at: new Date().toISOString(),
|
||||
items: [],
|
||||
};
|
||||
|
||||
return { ...defaultList, ...overrides };
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a mock ShoppingListItem object for use in tests.
|
||||
* @param overrides - An object containing properties to override the default mock values.
|
||||
* @returns A complete and type-safe ShoppingListItem object.
|
||||
*/
|
||||
export const createMockShoppingListItem = (overrides: Partial<ShoppingListItem> = {}): ShoppingListItem => {
|
||||
const defaultItem: ShoppingListItem = {
|
||||
shopping_list_item_id: Math.floor(Math.random() * 100000),
|
||||
shopping_list_id: Math.floor(Math.random() * 100),
|
||||
custom_item_name: 'Mock Shopping List Item',
|
||||
quantity: 1,
|
||||
is_purchased: false,
|
||||
added_at: new Date().toISOString(),
|
||||
};
|
||||
|
||||
return { ...defaultItem, ...overrides };
|
||||
};
|
||||
Reference in New Issue
Block a user