Files
flyer-crawler.projectium.com/src/tests/setup/tests-setup-unit.ts
Torben Sorensen 327d3d4fbc
Some checks failed
Deploy to Test Environment / deploy-to-test (push) Failing after 1m7s
latest batch of fixes after frontend testing - almost done?
2026-01-18 18:25:31 -08:00

630 lines
20 KiB
TypeScript

// src/tests/setup/tests-setup-unit.ts
import { vi, afterEach, beforeEach } from 'vitest';
import { cleanup } from '@testing-library/react';
import type { Request, Response, NextFunction } from 'express';
import '@testing-library/jest-dom/vitest';
import { resetMockIds } from '../utils/mockFactories';
// Reset mock ID counter before each test to ensure test isolation.
beforeEach(() => {
resetMockIds();
});
// 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.
// Guard against node environment where window doesn't exist (integration tests).
if (typeof window !== 'undefined') {
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.
// --- Centralized Core Node/NPM Module Mocks ---
// Mock 'util' to correctly handle the (err, stdout, stderr) signature of child_process.exec
// when it's promisified. The standard util.promisify doesn't work on a simple vi.fn() mock.
vi.mock('util', async (importOriginal) => {
const actual = await importOriginal<typeof import('util')>();
const mocked = {
...actual,
promisify: (fn: Function) => {
return (...args: any[]) => {
return new Promise((resolve, reject) => {
fn(...args, (err: Error | null, stdout: string, stderr: string) => {
if (err) {
// Attach stdout/stderr to the error object to mimic child_process.exec behavior
Object.assign(err, { stdout, stderr });
reject(err);
} else {
resolve({ stdout, stderr });
}
});
});
};
},
};
return {
...mocked,
default: mocked,
};
});
// Mock 'jsonwebtoken'. The `default` key is crucial because the code under test
// uses `import jwt from 'jsonwebtoken'`, which imports the default export.
vi.mock('jsonwebtoken', () => ({
default: {
sign: vi.fn(),
verify: vi.fn(),
},
// Also mock named exports for completeness.
sign: vi.fn(),
verify: vi.fn(),
}));
// Mock 'bcrypt'. The service uses `import * as bcrypt from 'bcrypt'`.
vi.mock('bcrypt');
// Mock 'crypto'. The service uses `import crypto from 'crypto'`.
vi.mock('crypto', () => ({
default: {
randomBytes: vi.fn().mockReturnValue({
toString: vi.fn().mockImplementation((encoding) => {
const id = 'mocked_random_id';
console.log(
`[DEBUG] tests-setup-unit.ts: crypto.randomBytes mock returning "${id}" for encoding "${encoding}"`,
);
return id;
}),
}),
randomUUID: vi.fn().mockReturnValue('mocked_random_id'),
},
}));
// --- 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 barcode service module.
* This prevents the dynamic import of zxing-wasm/reader from failing in unit tests.
* The zxing-wasm package uses WebAssembly which isn't available in the jsdom test environment.
*/
vi.mock('../../services/barcodeService.server', () => ({
detectBarcode: vi.fn().mockResolvedValue({
detected: false,
upc_code: null,
confidence: null,
format: null,
error: null,
}),
processBarcodeDetectionJob: vi.fn().mockResolvedValue(undefined),
isValidUpcFormat: vi.fn().mockReturnValue(false),
calculateUpcCheckDigit: vi.fn().mockReturnValue(null),
validateUpcCheckDigit: vi.fn().mockReturnValue(false),
detectMultipleBarcodes: vi.fn().mockResolvedValue([]),
enhanceImageForDetection: vi.fn().mockImplementation((path: string) => Promise.resolve(path)),
}));
/**
* Mocks the client-side config module.
* This prevents errors when sentry.client.ts tries to access config.sentry.dsn.
*/
vi.mock('../../config', () => ({
default: {
app: {
version: '1.0.0-test',
commitMessage: 'test commit',
commitUrl: 'https://example.com',
},
google: {
mapsEmbedApiKey: '',
},
sentry: {
dsn: '',
environment: 'test',
debug: false,
enabled: false,
},
},
}));
// 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() {}
setQueues() {} // Required by createBullBoard
setViewsPath() {} // Required by createBullBoard
setStaticPath() {} // Required by createBullBoard
setEntryRoute() {} // Required by createBullBoard
setErrorHandler() {} // Required by createBullBoard
setApiRoutes() {} // Required by createBullBoard
getRouter() {
return (req: Request, res: Response, next: NextFunction) => next();
}
},
}));
/**
* Mocks the @bull-board/api module.
* createBullBoard normally calls methods on the serverAdapter, but in tests
* we want to skip all of that initialization.
*/
vi.mock('@bull-board/api', () => ({
createBullBoard: vi.fn(() => ({
addQueue: vi.fn(),
removeQueue: vi.fn(),
setQueues: vi.fn(),
})),
BullMQAdapter: class {
constructor() {}
},
}));
/**
* Mocks the Sentry client.
* This prevents errors when tests import modules that depend on sentry.client.ts.
*/
vi.mock('../../services/sentry.client', () => ({
isSentryConfigured: false,
initSentry: vi.fn(),
captureException: vi.fn(),
captureMessage: vi.fn(),
setUser: vi.fn(),
addBreadcrumb: vi.fn(),
// Re-export a mock Sentry object for ErrorBoundary and other advanced usage
Sentry: {
init: vi.fn(),
captureException: vi.fn(),
captureMessage: vi.fn(),
setUser: vi.fn(),
setContext: vi.fn(),
addBreadcrumb: vi.fn(),
withScope: vi.fn(),
// Mock the ErrorBoundary component for React
ErrorBoundary: ({ children }: { children: React.ReactNode }) => children,
},
}));
/**
* Mocks the client-side logger.
*/
vi.mock('../../services/logger.client', () => ({
logger: {
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
debug: vi.fn(),
},
}));
/**
* Mocks the server-side logger.
* This mock provides both `logger` and `createScopedLogger` exports.
* Uses vi.hoisted to ensure the mock values are available during module import.
* IMPORTANT: Uses import() syntax to ensure correct path resolution for all importers.
*/
const { mockServerLogger, mockCreateScopedLogger } = vi.hoisted(() => {
const mockLogger = {
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
debug: vi.fn(),
trace: vi.fn(),
fatal: vi.fn(),
child: vi.fn().mockReturnThis(),
level: 'debug',
};
return {
mockServerLogger: mockLogger,
mockCreateScopedLogger: vi.fn(() => mockLogger),
};
});
vi.mock('../../services/logger.server', () => ({
logger: mockServerLogger,
createScopedLogger: mockCreateScopedLogger,
}));
/**
* 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 ---
// Mock for db/index.db which exports repository instances used by many routes
vi.mock('../../services/db/index.db', () => ({
userRepo: {
findUserProfileById: vi.fn(),
updateUserProfile: vi.fn(),
updateUserPreferences: vi.fn(),
},
personalizationRepo: {
getWatchedItems: vi.fn(),
removeWatchedItem: vi.fn(),
addWatchedItem: vi.fn(),
getUserDietaryRestrictions: vi.fn(),
setUserDietaryRestrictions: vi.fn(),
getUserAppliances: vi.fn(),
setUserAppliances: vi.fn(),
},
shoppingRepo: {
getShoppingLists: vi.fn(),
createShoppingList: vi.fn(),
deleteShoppingList: vi.fn(),
addShoppingListItem: vi.fn(),
updateShoppingListItem: vi.fn(),
removeShoppingListItem: vi.fn(),
getShoppingListById: vi.fn(),
},
recipeRepo: {
createRecipe: vi.fn(),
deleteRecipe: vi.fn(),
updateRecipe: vi.fn(),
},
addressRepo: {
getAddressById: vi.fn(),
upsertAddress: vi.fn(),
},
notificationRepo: {
getNotificationsForUser: vi.fn(),
markAllNotificationsAsRead: vi.fn(),
markNotificationAsRead: vi.fn(),
},
}));
// Mock userService used by routes
vi.mock('../../services/userService', () => ({
userService: {
updateUserAvatar: vi.fn(),
updateUserPassword: vi.fn(),
deleteUserAccount: vi.fn(),
getUserAddress: vi.fn(),
upsertUserAddress: vi.fn(),
processTokenCleanupJob: vi.fn(),
deleteUserAsAdmin: vi.fn(),
},
}));
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 ---
/**
* Mocks the AI service.
* IMPORTANT: This mock does NOT use `importOriginal` because aiService.server has
* complex dependencies (logger.server, etc.) that cause circular mock resolution issues.
* Instead, we provide a complete mock of the aiService singleton.
*/
vi.mock('../../services/aiService.server', () => ({
aiService: {
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' }],
}),
extractAndValidateData: vi.fn().mockResolvedValue({
store_name: 'Mock Store',
valid_from: '2023-01-01',
valid_to: '2023-01-07',
store_address: '123 Mock St',
items: [],
}),
isImageAFlyer: vi.fn().mockResolvedValue(true),
},
// Export the AIService class as a mock constructor for tests that need it
AIService: vi.fn().mockImplementation(() => ({
extractItemsFromReceiptImage: vi.fn(),
extractCoreDataFromFlyerImage: vi.fn(),
extractTextFromImageArea: vi.fn(),
planTripWithMaps: vi.fn(),
extractAndValidateData: vi.fn(),
isImageAFlyer: vi.fn(),
})),
}));