Some checks failed
Deploy to Test Environment / deploy-to-test (push) Has been cancelled
- 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.
415 lines
14 KiB
TypeScript
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' }],
|
|
}),
|
|
},
|
|
};
|
|
}); |