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