Files
flyer-crawler.projectium.com/src/tests/setup/tests-setup-unit.ts
Torben Sorensen d4739b5784
Some checks failed
Deploy to Test Environment / deploy-to-test (push) Has been cancelled
Refactor and update various service and route tests for improved type safety and clarity
- Added a new notes file regarding the deprecation of the Google AI JavaScript SDK.
- Removed unused imports and fixed duplicate imports in admin and auth route tests.
- Enhanced type safety in error handling for unique constraint violations in auth routes.
- Simplified gamification route tests by removing unnecessary imports.
- Updated price route to improve type safety by casting request body.
- Improved mock implementations in system route tests for better type handling.
- Cleaned up user routes by removing unused imports.
- Enhanced AI API client tests with more robust type definitions for form data.
- Updated recipe database tests to remove unused error imports.
- Refactored flyer processing service tests for better type safety and clarity.
- Improved logger client to use `unknown` instead of `any` for better type safety.
- Cleaned up notification service tests to ensure proper type casting.
- Updated queue service tests to remove unnecessary imports and improve type handling.
- Refactored queue service workers tests for better type safety in job processors.
- Cleaned up user routes integration tests by removing unused imports.
- Enhanced tests setup for unit tests to improve type safety in mocked Express requests.
- Updated PDF converter tests for better type safety in mocked return values.
- Improved price parser tests to ensure proper handling of null and undefined inputs.
2025-12-14 18:02:16 -08:00

415 lines
14 KiB
TypeScript

// src/tests/setup/tests-setup-unit.ts
import { vi, afterEach } from 'vitest';
import { cleanup } from '@testing-library/react';
import type { Request, Response, NextFunction } from 'express';
import '@testing-library/jest-dom/vitest';
// Mock the GeolocationPositionError global that exists in browsers but not in JSDOM.
// This is necessary for tests that simulate geolocation failures.
if (typeof global.GeolocationPositionError === 'undefined') {
// Define a simple class that mimics the structure of the real GeolocationPositionError.
global.GeolocationPositionError = class GeolocationPositionError extends Error {
readonly code: number;
// Add static properties to match the browser's built-in type definition.
static readonly PERMISSION_DENIED = 1;
static readonly POSITION_UNAVAILABLE = 2;
static readonly TIMEOUT = 3;
readonly PERMISSION_DENIED = 1;
readonly POSITION_UNAVAILABLE = 2;
readonly TIMEOUT = 3;
// Make constructor arguments optional to match the built-in type definition.
constructor(message?: string, code?: number) {
super(message);
// Provide a default value (e.g., 0) if code is undefined to satisfy the type.
this.code = code ?? 0;
}
};
}
// Mock window.matchMedia, which is not implemented in JSDOM.
// This is necessary for components that check for the user's preferred color scheme.
Object.defineProperty(window, 'matchMedia', {
writable: true,
value: vi.fn().mockImplementation(query => ({
matches: false,
media: query,
onchange: null,
addListener: vi.fn(), // deprecated
removeListener: vi.fn(), // deprecated
addEventListener: vi.fn(),
removeEventListener: vi.fn(),
dispatchEvent: vi.fn(),
})),
});
// --- Polyfill for File constructor and prototype ---
// The `File` object in JSDOM is incomplete. It lacks `arrayBuffer` and its constructor
// can be unreliable across different Node versions. This polyfill ensures a consistent
// `File` object is available in all unit tests, resolving `instanceof` issues and
// errors where the `name` property is missing.
if (typeof global.File === 'undefined') {
// Create a simplified mock of the File class that has the properties our app uses.
// This avoids the complex and error-prone type mismatch between JSDOM's Blob and Node's Blob.
class MockFile {
name: string;
lastModified: number;
size: number;
type: string;
constructor(fileBits: BlobPart[], fileName: string, options?: FilePropertyBag) {
this.name = fileName;
this.lastModified = options?.lastModified ?? Date.now();
// FIX: Correctly calculate size by handling all BlobPart types.
// `ArrayBuffer` and `ArrayBufferView` have `byteLength`, while `Blob` has `size`.
this.size = fileBits.reduce((acc, part) => {
if (typeof part === 'string') {
return acc + part.length;
}
// `part.size` exists on `Blob`, while `part.byteLength` exists on `ArrayBuffer` and `ArrayBufferView`.
return acc + ((part as Blob).size ?? (part as ArrayBuffer).byteLength);
}, 0);
this.type = options?.type ?? '';
}
}
vi.stubGlobal('File', MockFile);
}
// --- Polyfill for crypto.subtle ---
// JSDOM environments do not include the 'crypto' module. We need to polyfill it
// for utilities like `generateFileChecksum` to work in tests.
import { webcrypto } from 'node:crypto';
if (!global.crypto) {
// Use `vi.stubGlobal` which correctly handles read-only properties.
vi.stubGlobal('crypto', webcrypto);
}
// --- Polyfill for File.prototype.arrayBuffer ---
// The `File` object in JSDOM does not have the `arrayBuffer` method, which is used
// by utilities like `generateFileChecksum` and `pdfConverter`. This polyfill adds it.
if (typeof File.prototype.arrayBuffer === 'undefined') {
File.prototype.arrayBuffer = function() {
return new Promise((resolve) => {
const fr = new FileReader();
fr.onload = () => {
// Ensure the result is an ArrayBuffer before resolving.
resolve(fr.result as ArrayBuffer);
};
fr.readAsArrayBuffer(this);
});
};
}
// Automatically run cleanup after each test case (e.g., clearing jsdom)
// This is specific to our jsdom-based unit tests.
afterEach(cleanup);
// --- Global Mocks for Unit Tests ---
// By placing mocks here, they are guaranteed to be hoisted and applied
// before any test files are executed, preventing initialization errors.
// --- Global Mocks ---
// 1. Define the mock pool instance logic OUTSIDE the factory so it can be used
// by the factory. However, we define the Constructor function INSIDE the factory
// or via a standard function expression to ensure it remains constructible.
const { mockPoolInstance } = vi.hoisted(() => {
//console.log('[DEBUG] tests-setup-unit.ts: Initializing hoisted mock variables');
const mockQuery = vi.fn().mockResolvedValue({ rows: [], rowCount: 0 });
const mockRelease = vi.fn();
const mockConnect = vi.fn().mockResolvedValue({
query: mockQuery,
release: mockRelease,
});
const instance = {
connect: mockConnect,
query: mockQuery,
end: vi.fn(),
on: vi.fn(),
totalCount: 10,
idleCount: 5,
waitingCount: 0,
};
return { mockPoolInstance: instance };
});
// Expose the mock instance globally so it can be imported by tests if needed,
// though typically they should import from 'pg' directly.
export { mockPoolInstance };
/**
* Mocks the `pg` module globally.
* We define the MockPool function INSIDE the factory to ensure it remains a standard function.
*/
vi.mock('pg', () => {
//console.log('[DEBUG] tests-setup-unit.ts: vi.mock("pg") factory executing');
// FIX: Use a standard function expression for the Mock Pool Constructor.
// This ensures `new Pool()` works at runtime.
const MockPool = function() {
//console.log('[DEBUG] tests-setup-unit.ts: MockPool constructor called via "new"!');
return mockPoolInstance;
};
return {
// Named export must be the constructor
Pool: MockPool,
// Default export often contains Pool as a property
default: { Pool: MockPool },
// FIX: Add the `types` object with `builtins` to the mock.
// This prevents `TypeError: Cannot read properties of undefined (reading 'NUMERIC')`
// when `connection.db.ts` is imported during tests.
types: {
builtins: { NUMERIC: 1700, INT8: 20 }, // Mocked enum values
setTypeParser: vi.fn(),
},
};
});
/**
* Mocks the Google Generative AI package.
*/
vi.mock('@google/genai', () => {
const mockGenerativeModel = {
generateContent: vi.fn().mockResolvedValue({
// The new SDK structure is slightly different
text: 'Mocked AI response',
candidates: [{ content: { parts: [{ text: 'Mocked AI response' }] } }],
}),
};
return {
// Mock the GoogleGenAI class constructor
GoogleGenAI: vi.fn(function() {
return {
getGenerativeModel: () => mockGenerativeModel,
models: mockGenerativeModel, // Also mock the `models` getter
};
}),
};
});
/**
* Mocks the entire apiClient module.
* This ensures that all test files that import from apiClient will get this mocked version.
*/
vi.mock('../../services/apiClient', () => ({
// --- Auth ---
registerUser: vi.fn(),
loginUser: vi.fn(),
getAuthenticatedUserProfile: vi.fn(),
requestPasswordReset: vi.fn(),
resetPassword: vi.fn(),
updateUserPassword: vi.fn(),
deleteUserAccount: vi.fn(),
updateUserPreferences: vi.fn(),
updateUserProfile: vi.fn(),
// --- Data Fetching & Manipulation ---
fetchFlyers: vi.fn(),
fetchFlyerItems: vi.fn(),
// Provide a default implementation that returns a valid Response object to prevent timeouts.
fetchFlyerItemsForFlyers: vi.fn(() => Promise.resolve(new Response(JSON.stringify([])))),
countFlyerItemsForFlyers: vi.fn(() => Promise.resolve(new Response(JSON.stringify({ count: 0 })))),
fetchMasterItems: vi.fn(),
fetchWatchedItems: vi.fn(),
addWatchedItem: vi.fn(),
removeWatchedItem: vi.fn(),
fetchShoppingLists: vi.fn(),
createShoppingList: vi.fn(),
deleteShoppingList: vi.fn(),
addShoppingListItem: vi.fn(),
updateShoppingListItem: vi.fn(),
removeShoppingListItem: vi.fn(),
fetchHistoricalPriceData: vi.fn(),
processFlyerFile: vi.fn(),
uploadLogoAndUpdateStore: vi.fn(),
exportUserData: vi.fn(),
// --- Address ---
getUserAddress: vi.fn(),
updateUserAddress: vi.fn(),
geocodeAddress: vi.fn(() => Promise.resolve(new Response(JSON.stringify({ lat: 0, lng: 0 })))),
// --- Admin ---
getSuggestedCorrections: vi.fn(),
fetchCategories: vi.fn(),
approveCorrection: vi.fn(),
rejectCorrection: vi.fn(),
updateSuggestedCorrection: vi.fn(),
getApplicationStats: vi.fn(),
fetchActivityLog: vi.fn(),
fetchAllBrands: vi.fn(),
uploadBrandLogo: vi.fn(),
// --- System ---
pingBackend: vi.fn(),
checkDbSchema: vi.fn(),
checkStorage: vi.fn(),
checkDbPoolHealth: vi.fn(),
checkRedisHealth: vi.fn(),
checkPm2Status: vi.fn(),
fetchLeaderboard: vi.fn(),
}));
// FIX: Mock the aiApiClient module as well, which is used by AnalysisPanel
vi.mock('../../services/aiApiClient', () => ({
// Provide a default implementation that returns a valid Response object to prevent timeouts.
uploadAndProcessFlyer: vi.fn(() => Promise.resolve(new Response(JSON.stringify({ jobId: 'mock-job-id' })))),
isImageAFlyer: vi.fn(),
extractAddressFromImage: vi.fn(),
extractLogoFromImage: vi.fn(),
getQuickInsights: vi.fn(),
getDeepDiveAnalysis: vi.fn(),
searchWeb: vi.fn(),
planTripWithMaps: vi.fn(),
generateImageFromText: vi.fn(),
generateSpeechFromText: vi.fn(),
startVoiceSession: vi.fn(),
rescanImageArea: vi.fn(() => Promise.resolve(new Response(JSON.stringify({ text: 'mocked text' })))),
}));
/**
* Mocks the Express adapter for Bull Board.
* This is critical for any test that imports `admin.routes.ts`. It replaces the
* actual Bull Board UI adapter with a lightweight fake. This prevents the test
* suite from crashing when trying to initialize the real UI, which has complex
* dependencies not suitable for a test environment.
*/
vi.mock('@bull-board/express', () => ({
ExpressAdapter: class {
setBasePath() {}
getRouter() { return (req: Request, res: Response, next: NextFunction) => next(); }
},
}));
/**
* Mocks the logger.
*/
vi.mock('../../services/logger.client', () => ({
logger: {
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
debug: vi.fn(),
},
}));
/**
* Mocks the notification service.
*/
vi.mock('../../services/notificationService', () => ({
notifySuccess: vi.fn(),
notifyError: vi.fn(),
}));
/**
* Mocks react-hot-toast.
*/
vi.mock('react-hot-toast', () => ({
default: {
success: vi.fn(),
error: vi.fn(),
loading: vi.fn(() => 'id'),
dismiss: vi.fn(),
},
Toaster: () => null,
}));
// --- Database Service Mocks ---
vi.mock('../../services/db/user.db', async (importOriginal) => {
const actual = await importOriginal<typeof import('../../services/db/user.db')>();
return {
...actual,
findUserByEmail: vi.fn(),
createUser: vi.fn(),
findUserById: vi.fn(),
findUserWithPasswordHashById: vi.fn(),
findUserProfileById: vi.fn(),
updateUserProfile: vi.fn(),
updateUserPreferences: vi.fn(),
updateUserPassword: vi.fn(),
deleteUserById: vi.fn(),
saveRefreshToken: vi.fn(),
findUserByRefreshToken: vi.fn(),
createPasswordResetToken: vi.fn(),
getValidResetTokens: vi.fn(),
deleteResetToken: vi.fn(),
exportUserData: vi.fn(),
followUser: vi.fn(),
unfollowUser: vi.fn(),
getUserFeed: vi.fn(),
logSearchQuery: vi.fn(),
resetFailedLoginAttempts: vi.fn(),
getAddressById: vi.fn(),
upsertAddress: vi.fn(),
};
});
vi.mock('../../services/db/budget.db', async (importOriginal) => {
const actual = await importOriginal<typeof import('../../services/db/budget.db')>();
return {
...actual,
getBudgetsForUser: vi.fn().mockResolvedValue([]),
createBudget: vi.fn(),
updateBudget: vi.fn(),
deleteBudget: vi.fn(),
getSpendingByCategory: vi.fn().mockResolvedValue([]),
};
});
vi.mock('../../services/db/gamification.db', async (importOriginal) => {
const actual = await importOriginal<typeof import('../../services/db/gamification.db')>();
return {
...actual,
getAllAchievements: vi.fn().mockResolvedValue([]),
getUserAchievements: vi.fn().mockResolvedValue([]),
awardAchievement: vi.fn(),
getLeaderboard: vi.fn().mockResolvedValue([]),
};
});
vi.mock('../../services/db/notification.db', async (importOriginal) => {
const actual = await importOriginal<typeof import('../../services/db/notification.db')>();
return {
...actual,
createNotification: vi.fn(),
createBulkNotifications: vi.fn(),
getNotificationsForUser: vi.fn().mockResolvedValue([]),
markAllNotificationsAsRead: vi.fn(),
markNotificationAsRead: vi.fn(),
};
});
// --- Server-Side Service Mocks ---
vi.mock('../../services/aiService.server', async (importOriginal) => {
const actual = await importOriginal<typeof import('../../services/aiService.server')>();
return {
...actual,
// The singleton instance is named `aiService`. We mock the methods on it.
aiService: {
...actual.aiService, // Spread original methods in case new ones are added
extractItemsFromReceiptImage: vi.fn().mockResolvedValue([
{ raw_item_description: 'Mock Receipt Item', price_paid_cents: 100 },
]),
extractCoreDataFromFlyerImage: vi.fn().mockResolvedValue({
store_name: 'Mock Store',
valid_from: '2023-01-01',
valid_to: '2023-01-07',
store_address: '123 Mock St',
items: [
{ item: 'Mock Apple', price_display: '$1.00', price_in_cents: 100, quantity: '1 lb', category_name: 'Produce', master_item_id: undefined },
],
}),
extractTextFromImageArea: vi.fn().mockImplementation((path, mime, crop, type) => {
if (type === 'address') return Promise.resolve({ text: '123 AI Street, Server City' });
return Promise.resolve({ text: 'Mocked Extracted Text' });
}),
planTripWithMaps: vi.fn().mockResolvedValue({
text: 'Mocked trip plan.',
sources: [{ uri: 'http://maps.google.com/mock', title: 'Mock Map' }],
}),
},
};
});