fix tests ugh
All checks were successful
Deploy to Web Server flyer-crawler.projectium.com / deploy (push) Successful in 5m55s

This commit is contained in:
2025-12-09 18:18:56 -08:00
parent b3372b6fa3
commit 4674309ea1
6 changed files with 67 additions and 89 deletions

View File

@@ -49,6 +49,7 @@ import { UserProfile } from '../types';
vi.mock('../services/db/index.db', () => ({
userRepo: {
findUserByEmail: vi.fn(),
findUserWithProfileByEmail: vi.fn(), // ADD THIS
findUserProfileById: vi.fn(),
},
adminRepo: {
@@ -99,8 +100,16 @@ describe('Passport Configuration', () => {
it('should call done(null, user) on successful authentication', async () => {
// Arrange
const mockUser = { user_id: 'user-123', email: 'test@test.com', password_hash: 'hashed_password', failed_login_attempts: 0, last_failed_login: null };
vi.mocked(mockedDb.userRepo.findUserByEmail).mockResolvedValue(mockUser);
const mockUser = {
user_id: 'user-123',
email: 'test@test.com',
password_hash: 'hashed_password',
failed_login_attempts: 0,
last_failed_login: null,
points: 0,
role: 'user' as const
};
vi.mocked(mockedDb.userRepo.findUserWithProfileByEmail).mockResolvedValue(mockUser);
vi.mocked(bcrypt.compare).mockResolvedValue(true as never);
// Act
@@ -109,14 +118,14 @@ describe('Passport Configuration', () => {
}
// Assert
expect(mockedDb.userRepo.findUserByEmail).toHaveBeenCalledWith('test@test.com');
expect(mockedDb.userRepo.findUserWithProfileByEmail).toHaveBeenCalledWith('test@test.com');
expect(bcrypt.compare).toHaveBeenCalledWith('password', 'hashed_password');
expect(mockedDb.adminRepo.resetFailedLoginAttempts).toHaveBeenCalledWith('user-123', '127.0.0.1');
expect(done).toHaveBeenCalledWith(null, { user_id: 'user-123', email: 'test@test.com', failed_login_attempts: 0, last_failed_login: null });
expect(done).toHaveBeenCalledWith(null, mockUser);
});
it('should call done(null, false) if user is not found', async () => {
vi.mocked(mockedDb.userRepo.findUserByEmail).mockResolvedValue(undefined);
vi.mocked(mockedDb.userRepo.findUserWithProfileByEmail).mockResolvedValue(undefined);
if (localStrategyCallbackWrapper.callback) {
await localStrategyCallbackWrapper.callback(mockReq, 'notfound@test.com', 'password', done);
@@ -126,8 +135,16 @@ describe('Passport Configuration', () => {
});
it('should call done(null, false) and increment failed attempts on password mismatch', async () => {
const mockUser = { user_id: 'user-123', email: 'test@test.com', password_hash: 'hashed_password', failed_login_attempts: 1, last_failed_login: null };
vi.mocked(mockedDb.userRepo.findUserByEmail).mockResolvedValue(mockUser);
const mockUser = {
user_id: 'user-123',
email: 'test@test.com',
password_hash: 'hashed_password',
failed_login_attempts: 1,
last_failed_login: null,
points: 0,
role: 'user' as const
};
vi.mocked(mockedDb.userRepo.findUserWithProfileByEmail).mockResolvedValue(mockUser);
vi.mocked(bcrypt.compare).mockResolvedValue(false as never);
if (localStrategyCallbackWrapper.callback) {
@@ -140,8 +157,16 @@ describe('Passport Configuration', () => {
});
it('should call done(null, false) for an OAuth user (no password hash)', async () => {
const mockUser = { user_id: 'oauth-user', email: 'oauth@test.com', password_hash: null, failed_login_attempts: 0, last_failed_login: null };
vi.mocked(mockedDb.userRepo.findUserByEmail).mockResolvedValue(mockUser);
const mockUser = {
user_id: 'oauth-user',
email: 'oauth@test.com',
password_hash: null,
failed_login_attempts: 0,
last_failed_login: null,
points: 0,
role: 'user' as const
};
vi.mocked(mockedDb.userRepo.findUserWithProfileByEmail).mockResolvedValue(mockUser);
if (localStrategyCallbackWrapper.callback) {
await localStrategyCallbackWrapper.callback(mockReq, 'oauth@test.com', 'any_password', done);
@@ -156,9 +181,11 @@ describe('Passport Configuration', () => {
email: 'locked@test.com',
password_hash: 'hashed_password',
failed_login_attempts: 5,
last_failed_login: new Date().toISOString(), // Recently locked
last_failed_login: new Date().toISOString(),
points: 0,
role: 'user' as const
};
vi.mocked(mockedDb.userRepo.findUserByEmail).mockResolvedValue(mockUser);
vi.mocked(mockedDb.userRepo.findUserWithProfileByEmail).mockResolvedValue(mockUser);
if (localStrategyCallbackWrapper.callback) {
await localStrategyCallbackWrapper.callback(mockReq, 'locked@test.com', 'any_password', done);
@@ -173,9 +200,11 @@ describe('Passport Configuration', () => {
email: 'expired@test.com',
password_hash: 'hashed_password',
failed_login_attempts: 5,
last_failed_login: new Date(Date.now() - 20 * 60 * 1000).toISOString(), // Locked 20 mins ago
last_failed_login: new Date(Date.now() - 20 * 60 * 1000).toISOString(),
points: 0,
role: 'user' as const
};
vi.mocked(mockedDb.userRepo.findUserByEmail).mockResolvedValue(mockUser);
vi.mocked(mockedDb.userRepo.findUserWithProfileByEmail).mockResolvedValue(mockUser);
vi.mocked(bcrypt.compare).mockResolvedValue(true as never); // Correct password
if (localStrategyCallbackWrapper.callback) {
@@ -189,7 +218,7 @@ describe('Passport Configuration', () => {
it('should call done(err) if the database lookup fails', async () => {
const dbError = new Error('DB connection failed');
vi.mocked(mockedDb.userRepo.findUserByEmail).mockRejectedValue(dbError);
vi.mocked(mockedDb.userRepo.findUserWithProfileByEmail).mockRejectedValue(dbError);
if (localStrategyCallbackWrapper.callback) {
await localStrategyCallbackWrapper.callback(mockReq, 'any@test.com', 'any_password', done);

View File

@@ -402,17 +402,8 @@ describe('API Client', () => {
it('updateUserPreferences should send a PUT request with preferences data', async () => {
const preferences = { darkMode: true };
let capturedBody: typeof preferences | null = null;
// Restore the original fetch so MSW can intercept this request.
// The global fetch spy from beforeEach would otherwise capture this call.
vi.mocked(global.fetch).mockRestore();
server.use(
http.put('http://localhost/api/users/profile/preferences', async ({ request }) => {
capturedBody = await request.json() as typeof preferences;
return HttpResponse.json({ success: true });
})
);
await apiClient.updateUserPreferences(preferences);
expect(capturedUrl?.pathname).toBe('/api/users/profile/preferences');
expect(capturedBody).toEqual(preferences);
});

View File

@@ -269,11 +269,11 @@ export class PersonalizationRepository {
await client.query('COMMIT');
return newAppliances;
} catch (error) {
await client.query('ROLLBACK');
// The patch requested this specific error handling.
if ((error as any).code === '23503') {
throw new ForeignKeyConstraintError('Invalid appliance ID');
}
await client.query('ROLLBACK');
logger.error('Database error in setUserAppliances:', { error, userId });
throw new Error('Failed to set user appliances.');
} finally {

View File

@@ -205,7 +205,7 @@ describe('Shopping DB Service', () => {
it('should throw an error if the item to update is not found', async () => {
mockPoolInstance.query.mockResolvedValue({ rowCount: 0, rows: [], command: 'UPDATE' });
await expect(shoppingRepo.updateShoppingListItem(999, { quantity: 5 })).rejects.toThrow('Failed to update shopping list item.');
await expect(shoppingRepo.updateShoppingListItem(999, { quantity: 5 })).rejects.toThrow('Shopping list item not found.');
});
it('should throw an error if no valid fields are provided to update', async () => {

View File

@@ -1,15 +1,25 @@
// src/services/flyerProcessingService.server.test.ts
// --- FIX REGISTRY ---
//
// 2024-07-30: Fixed `FlyerDataTransformer` mock to be a constructible class. The previous mock was not a constructor,
// causing a `TypeError` when `FlyerProcessingService` tried to instantiate it with `new`.
// 2024-12-09: Fixed duplicate imports of FlyerProcessingService and FlyerJobData. Consolidated imports to use
// FlyerJobData from types file and FlyerProcessingService from server file.
// 2024-12-09: Removed duplicate _saveProcessedFlyerData test suite. Fixed assertion to match actual logActivity call
// signature which includes displayText and userId fields.
// --- END FIX REGISTRY ---
// src/services/flyerProcessingService.server.test.ts
import { describe, it, expect, vi, beforeEach, type Mocked } from 'vitest';
import { Job } from 'bullmq';
import type { Dirent } from 'node:fs';
import type { FlyerJobData } from './flyerProcessingService.types';
export interface FlyerJobData {
filePath: string;
originalFileName: string;
checksum: string;
userId?: string;
submitterIp?: string;
userProfileAddress?: string;
}
// 1. Create hoisted mocks FIRST
const mocks = vi.hoisted(() => ({
@@ -227,6 +237,7 @@ describe('FlyerProcessingService', () => {
};
const mockImagePaths = [{ path: '/tmp/flyer.jpg', mimetype: 'image/jpeg' }];
const mockJobData = {
filePath: '/tmp/flyer.jpg',
originalFileName: 'flyer.jpg',
checksum: 'checksum-123',
userId: 'user-abc',
@@ -236,7 +247,7 @@ describe('FlyerProcessingService', () => {
const transformerSpy = vi.spyOn(FlyerDataTransformer.prototype, 'transform');
// The DB create function is also mocked in beforeEach.
const mockNewFlyer = { flyer_id: 1, file_name: 'flyer.jpg', store_name: 'Test Store' };
const mockNewFlyer = { flyer_id: 1, file_name: 'flyer.jpg', store_name: 'Mock Store' };
vi.mocked(createFlyerAndItems).mockResolvedValue({ flyer: mockNewFlyer, items: [] } as any);
// Act: Access and call the private method for testing
@@ -250,11 +261,13 @@ describe('FlyerProcessingService', () => {
const transformedData = await transformerSpy.mock.results[0].value;
expect(createFlyerAndItems).toHaveBeenCalledWith(transformedData.flyerData, transformedData.itemsForDb);
// 3. Activity was logged
expect(mockedDb.adminRepo.logActivity).toHaveBeenCalledWith(expect.objectContaining({
// 3. Activity was logged with all expected fields
expect(mockedDb.adminRepo.logActivity).toHaveBeenCalledWith({
userId: 'user-abc',
action: 'flyer_processed',
details: { flyerId: mockNewFlyer.flyer_id, storeName: mockNewFlyer.store_name }
}));
displayText: 'Processed a new flyer for Mock Store.',
details: { flyerId: 1, storeName: 'Mock Store' }
});
// 4. The method returned the new flyer
expect(result).toEqual(mockNewFlyer);
@@ -305,50 +318,4 @@ describe('FlyerProcessingService', () => {
.rejects.toThrow(commandError);
});
});
describe('_saveProcessedFlyerData (private method)', () => {
it('should transform data, create flyer in DB, and log activity', async () => {
// Arrange
const mockExtractedData = {
store_name: 'Test Store',
valid_from: '2024-01-01',
valid_to: '2024-01-07',
store_address: '123 Mock St',
items: [{ item: 'Test Item', price_display: '$1.99', price_in_cents: 199, quantity: 'each', category_name: 'Test Category', master_item_id: 1 }],
};
const mockImagePaths = [{ path: '/tmp/flyer.jpg', mimetype: 'image/jpeg' }];
const mockJobData = {
originalFileName: 'flyer.jpg',
checksum: 'checksum-123',
userId: 'user-abc',
};
// The transformer is already spied on in beforeEach, we can just check its call.
const transformerSpy = vi.spyOn(FlyerDataTransformer.prototype, 'transform');
// The DB create function is also mocked in beforeEach.
const mockNewFlyer = { flyer_id: 1, file_name: 'flyer.jpg', store_name: 'Test Store' };
vi.mocked(createFlyerAndItems).mockResolvedValue({ flyer: mockNewFlyer, items: [] } as any);
// Act: Access and call the private method for testing
const result = await (service as any)._saveProcessedFlyerData(mockExtractedData, mockImagePaths, mockJobData);
// Assert
// 1. Transformer was called correctly
expect(transformerSpy).toHaveBeenCalledWith(mockExtractedData, mockImagePaths, mockJobData.originalFileName, mockJobData.checksum, mockJobData.userId);
// 2. DB function was called with the transformed data
const transformedData = await transformerSpy.mock.results[0].value;
expect(createFlyerAndItems).toHaveBeenCalledWith(transformedData.flyerData, transformedData.itemsForDb);
// 3. Activity was logged
expect(mockedDb.adminRepo.logActivity).toHaveBeenCalledWith(expect.objectContaining({
action: 'flyer_processed',
details: { flyerId: mockNewFlyer.flyer_id, storeName: mockNewFlyer.store_name }
}));
// 4. The method returned the new flyer
expect(result).toEqual(mockNewFlyer);
});
});
});
});

View File

@@ -1,9 +0,0 @@
// src/services/flyerProcessingService.types.ts
export interface FlyerJobData {
filePath: string;
originalFileName: string;
checksum: string;
userId?: string;
submitterIp?: string;
userProfileAddress?: string;
}