diff --git a/server.ts b/server.ts index d42ecbc..9d42c1e 100644 --- a/server.ts +++ b/server.ts @@ -80,6 +80,21 @@ const getDurationInMilliseconds = (start: [number, number]): number => { return (diff[0] * NS_PER_SEC + diff[1]) / NS_TO_MS; }; +/** + * Defines the structure for the detailed log object created for each request. + * This ensures type safety and consistency in our structured logs. + */ +interface RequestLogDetails { + user_id?: string; + method: string; + originalUrl: string; + statusCode: number; + statusMessage: string; + duration: string; + // The 'req' property is added conditionally for client/server errors. + req?: { headers: express.Request['headers']; body: express.Request['body'] }; +} + const requestLogger = (req: Request, res: Response, next: NextFunction) => { const requestId = randomUUID(); const user = req.user as UserProfile | undefined; @@ -102,7 +117,7 @@ const requestLogger = (req: Request, res: Response, next: NextFunction) => { const finalUser = req.user as UserProfile | undefined; // The base log object includes details relevant for all status codes. - const logDetails: Record = { + const logDetails: RequestLogDetails = { user_id: finalUser?.user_id, method, originalUrl, diff --git a/src/App.tsx b/src/App.tsx index e3502ce..6904278 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -37,8 +37,6 @@ function App() { const location = useLocation(); const params = useParams<{ flyerId?: string }>(); - const [error, setError] = useState(null); - const [isDarkMode, setIsDarkMode] = useState(false); // Fetch items for the currently selected flyer @@ -110,8 +108,9 @@ function App() { // The useData hook will automatically refetch user data when `user` changes. // We can remove the explicit fetch here. } catch (e) { - const errorMessage = e instanceof Error ? e.message : String(e); - setError(errorMessage); + // The `login` function within the `useAuth` hook already handles its own errors + // and notifications, so we just need to log any unexpected failures here. + logger.error({ err: e }, 'An error occurred during the login success handling.'); } }; @@ -150,7 +149,6 @@ function App() { const handleFlyerSelect = useCallback(async (flyer: Flyer) => { setSelectedFlyer(flyer); - setError(null); }, []); useEffect(() => { diff --git a/src/features/charts/PriceChart.tsx b/src/features/charts/PriceChart.tsx index c4f5746..70fab2b 100644 --- a/src/features/charts/PriceChart.tsx +++ b/src/features/charts/PriceChart.tsx @@ -1,6 +1,6 @@ // src/features/charts/PriceChart.tsx import React from 'react'; -import type { DealItem, User } from '../../types'; +import type { User } from '../../types'; import { useActiveDeals } from '../../hooks/useActiveDeals'; import { TagIcon } from '../../components/icons/TagIcon'; import { LoadingSpinner } from '../../components/LoadingSpinner'; diff --git a/src/features/charts/PriceHistoryChart.tsx b/src/features/charts/PriceHistoryChart.tsx index 52b764b..907bc41 100644 --- a/src/features/charts/PriceHistoryChart.tsx +++ b/src/features/charts/PriceHistoryChart.tsx @@ -3,7 +3,6 @@ import React, { useState, useEffect, useMemo } from 'react'; import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from 'recharts'; import * as apiClient from '../../services/apiClient'; import { LoadingSpinner } from '../../components/LoadingSpinner'; // This path is correct -import type { MasterGroceryItem } from '../../types'; import { useUserData } from '../../hooks/useUserData'; interface HistoricalPriceDataPoint { diff --git a/src/features/flyer/AnalysisPanel.tsx b/src/features/flyer/AnalysisPanel.tsx index 8035533..99cfda6 100644 --- a/src/features/flyer/AnalysisPanel.tsx +++ b/src/features/flyer/AnalysisPanel.tsx @@ -1,6 +1,6 @@ // src/features/flyer/AnalysisPanel.tsx import React, { useState } from 'react'; -import { AnalysisType, Flyer, MasterGroceryItem } from '../../types'; +import { AnalysisType, Flyer } from '../../types'; import { LoadingSpinner } from '../../components/LoadingSpinner'; import { LightbulbIcon } from '../../components/icons/LightbulbIcon'; import { BrainIcon } from '../../components/icons/BrainIcon'; @@ -16,6 +16,15 @@ interface AnalysisPanelProps { selectedFlyer: Flyer | null; } +/** + * Defines the types of analysis that correspond to a visible tab in the panel. + * This is a subset of the main `AnalysisType` enum and is used to ensure + * that the `activeTab` state can only be one of these values, preventing + * type errors when indexing into results or loading state objects. + */ +type AnalysisTabType = Exclude; + + interface Source { uri: string; title: string; @@ -47,7 +56,7 @@ const TabButton: React.FC = ({ label, icon, isActive, onClick }) export const AnalysisPanel: React.FC = ({ selectedFlyer }) => { const { flyerItems, isLoading: isLoadingItems, error: itemsError } = useFlyerItems(selectedFlyer); const { watchedItems, isLoading: isLoadingWatchedItems } = useUserData(); - const [activeTab, setActiveTab] = useState(AnalysisType.QUICK_INSIGHTS); + const [activeTab, setActiveTab] = useState(AnalysisType.QUICK_INSIGHTS); const { results, diff --git a/src/features/flyer/ExtractedDataTable.tsx b/src/features/flyer/ExtractedDataTable.tsx index 0086656..998a1ea 100644 --- a/src/features/flyer/ExtractedDataTable.tsx +++ b/src/features/flyer/ExtractedDataTable.tsx @@ -1,6 +1,6 @@ // src/features/flyer/ExtractedDataTable.tsx import React, { useMemo, useState } from 'react'; -import type { FlyerItem, ShoppingListItem } from '../../types'; // Import ShoppingListItem +import type { FlyerItem, MasterGroceryItem, ShoppingList, ShoppingListItem, User } from '../../types'; // Import ShoppingListItem import { formatUnitPrice } from '../../utils/unitConverter'; import { PlusCircleIcon } from '../../components/icons/PlusCircleIcon'; import { useAuth } from '../../hooks/useAuth'; @@ -9,10 +9,17 @@ import { useMasterItems } from '../../hooks/useMasterItems'; import { useWatchedItems } from '../../hooks/useWatchedItems'; import { useShoppingLists } from '../../hooks/useShoppingLists'; -interface ExtractedDataTableProps { +export interface ExtractedDataTableProps { items: FlyerItem[]; totalActiveItems?: number; + watchedItems: MasterGroceryItem[]; + masterItems: MasterGroceryItem[]; unitSystem: 'metric' | 'imperial'; + user: User | null; + onAddItem: (itemName: string, category: string) => Promise; + shoppingLists: ShoppingList[]; + activeListId: number | null; + onAddItemToList: (masterItemId: number) => void; } export const ExtractedDataTable: React.FC = ({ items, totalActiveItems, unitSystem }) => { diff --git a/src/features/flyer/FlyerDisplay.tsx b/src/features/flyer/FlyerDisplay.tsx index 2053c2e..7c13796 100644 --- a/src/features/flyer/FlyerDisplay.tsx +++ b/src/features/flyer/FlyerDisplay.tsx @@ -16,7 +16,7 @@ const formatDateRange = (from: string | null | undefined, to: string | null | un return fromDate ? `Deals start ${fromDate}` : (toDate ? `Deals end ${toDate}` : null); }; -interface FlyerDisplayProps { +export interface FlyerDisplayProps { imageUrl: string | null; store?: Store; validFrom?: string | null; diff --git a/src/features/flyer/FlyerUploader.tsx b/src/features/flyer/FlyerUploader.tsx index 36d1e93..be589c1 100644 --- a/src/features/flyer/FlyerUploader.tsx +++ b/src/features/flyer/FlyerUploader.tsx @@ -1,8 +1,8 @@ // src/features/flyer/FlyerUploader.tsx -import React, { useState, useEffect, useRef, useCallback, Fragment } from 'react'; +import React, { useState, useEffect, useRef, useCallback } from 'react'; import { useNavigate, Link } from 'react-router-dom'; import { uploadAndProcessFlyer, getJobStatus } from '../../services/aiApiClient'; -import { generateFileChecksum } from '../../utils/checksum'; // Assuming you have this utility +import { generateFileChecksum } from '../../utils/checksum'; import { logger } from '../../services/logger.client'; import { ProcessingStatus } from './ProcessingStatus'; import type { ProcessingStage } from '../../types'; diff --git a/src/hooks/useInfiniteQuery.test.ts b/src/hooks/useInfiniteQuery.test.ts index 555f950..447281e 100644 --- a/src/hooks/useInfiniteQuery.test.ts +++ b/src/hooks/useInfiniteQuery.test.ts @@ -12,7 +12,7 @@ describe('useInfiniteQuery Hook', () => { }); // Helper to create a mock paginated response - const createMockResponse = (items: T[], nextCursor: any): Response => { + const createMockResponse = (items: T[], nextCursor: number | string | null): Response => { const paginatedResponse: PaginatedResponse = { items, nextCursor }; return new Response(JSON.stringify(paginatedResponse)); }; diff --git a/src/hooks/useShoppingLists.tsx b/src/hooks/useShoppingLists.tsx index 88a1fdb..5340bbb 100644 --- a/src/hooks/useShoppingLists.tsx +++ b/src/hooks/useShoppingLists.tsx @@ -5,7 +5,6 @@ import { useUserData } from './useUserData'; import { useApi } from './useApi'; import * as apiClient from '../services/apiClient'; import type { ShoppingList, ShoppingListItem } from '../types'; -import { logger } from '../services/logger.client'; /** * A custom hook to manage all state and logic related to shopping lists. diff --git a/src/hooks/useWatchedItems.tsx b/src/hooks/useWatchedItems.tsx index 63e69f8..fee418c 100644 --- a/src/hooks/useWatchedItems.tsx +++ b/src/hooks/useWatchedItems.tsx @@ -5,7 +5,6 @@ import { useApi } from './useApi'; import { useUserData } from './useUserData'; import * as apiClient from '../services/apiClient'; import type { MasterGroceryItem } from '../types'; -import { logger } from '../services/logger.client'; /** * A custom hook to manage all state and logic related to a user's watched items. diff --git a/src/pages/HomePage.test.tsx b/src/pages/HomePage.test.tsx index 5a73e27..40cd225 100644 --- a/src/pages/HomePage.test.tsx +++ b/src/pages/HomePage.test.tsx @@ -5,17 +5,13 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'; import { MemoryRouter, useOutletContext } from 'react-router-dom'; import { HomePage } from './HomePage'; import { createMockFlyer, createMockFlyerItem } from '../tests/utils/mockFactories'; -import type { Flyer, FlyerItem } from '../types'; -import { ExtractedDataTable } from '../features/flyer/ExtractedDataTable'; +import type { Flyer, FlyerItem, MasterGroceryItem, ShoppingList } from '../types'; +import type { FlyerDisplayProps } from '../features/flyer/FlyerDisplay'; // Keep this for FlyerDisplay mock +import type { ExtractedDataTableProps } from '../features/flyer/ExtractedDataTable'; // Import the props for ExtractedDataTable // Mock child components to isolate the HomePage logic vi.mock('../features/flyer/FlyerDisplay', () => ({ - FlyerDisplay: (props: any) =>
, -})); -vi.mock('../features/flyer/ExtractedDataTable', () => ({ - // Wrap the mock component in vi.fn() to allow spying on its calls. - // This gives us access to `mock.calls` in our tests. - ExtractedDataTable: vi.fn((props: { items: FlyerItem[] }) =>
{props.items.length} items
), + FlyerDisplay: (props: FlyerDisplayProps) =>
, })); vi.mock('../features/flyer/AnalysisPanel', () => ({ AnalysisPanel: () =>
, @@ -30,6 +26,13 @@ vi.mock('react-router-dom', async (importOriginal) => { }; }); +// Mock ExtractedDataTable separately to use the imported props interface +import { ExtractedDataTable } from '../features/flyer/ExtractedDataTable'; +vi.mock('../features/flyer/ExtractedDataTable', () => ({ + // Wrap the mock component in vi.fn() to allow spying on its calls. + ExtractedDataTable: vi.fn((props: ExtractedDataTableProps) =>
{props.items.length} items
), +})); + const mockedUseOutletContext = vi.mocked(useOutletContext); describe('HomePage Component', () => { diff --git a/src/pages/admin/components/AdminBrandManager.tsx b/src/pages/admin/components/AdminBrandManager.tsx index b56d4e0..9b74387 100644 --- a/src/pages/admin/components/AdminBrandManager.tsx +++ b/src/pages/admin/components/AdminBrandManager.tsx @@ -1,5 +1,5 @@ // src/pages/admin/components/AdminBrandManager.tsx -import React, { useState, useEffect, useCallback } from 'react'; +import React, { useState, useEffect } from 'react'; import toast from 'react-hot-toast'; import { fetchAllBrands, uploadBrandLogo } from '../../../services/apiClient'; import { Brand } from '../../../types'; @@ -8,8 +8,12 @@ import { useApiOnMount } from '../../../hooks/useApiOnMount'; export const AdminBrandManager: React.FC = () => { // Use the new hook to fetch brands when the component mounts. - // It handles loading and error states for us. - const { data: initialBrands, loading, error } = useApiOnMount(fetchAllBrands, []); + // It handles loading and error states for us. We create a wrapper function + // to ensure the AbortSignal from the hook is correctly passed to the API client. + // The underlying `fetchAllBrands` function takes no arguments, so we create a wrapper + // that accepts the signal from the hook but doesn't pass it down. + const fetchBrandsWrapper = () => fetchAllBrands(); + const { data: initialBrands, loading, error } = useApiOnMount(fetchBrandsWrapper, []); // We still need local state for brands so we can update it after a logo upload // without needing to re-fetch the entire list. diff --git a/src/routes/admin.routes.ts b/src/routes/admin.routes.ts index 4b8d17e..7e0e68b 100644 --- a/src/routes/admin.routes.ts +++ b/src/routes/admin.routes.ts @@ -4,21 +4,19 @@ import passport from './passport.routes'; import { isAdmin } from './passport.routes'; // Correctly imported import multer from 'multer';// --- Zod Schemas for Admin Routes (as per ADR-003) --- import { z } from 'zod'; -import crypto from 'crypto'; import * as db from '../services/db/index.db'; import { logger } from '../services/logger.server'; import { UserProfile } from '../types'; -import { clearGeocodeCache } from '../services/geocodingService.server'; +import { geocodingService } from '../services/geocodingService.server'; import { requireFileUpload } from '../middleware/fileUpload.middleware'; // This was a duplicate, fixed. -import { ForeignKeyConstraintError, NotFoundError, ValidationError } from '../services/db/errors.db'; +import { NotFoundError, ValidationError } from '../services/db/errors.db'; import { validateRequest } from '../middleware/validation.middleware'; // --- Bull Board (Job Queue UI) Imports --- import { createBullBoard } from '@bull-board/api'; import { BullMQAdapter } from '@bull-board/api/bullMQAdapter'; import { ExpressAdapter } from '@bull-board/express'; -import { render, screen } from '@testing-library/react'; import type { Queue } from 'bullmq'; import { backgroundJobService } from '../services/backgroundJobService'; @@ -108,7 +106,7 @@ router.use(passport.authenticate('jwt', { session: false }), isAdmin); router.get('/corrections', async (req, res, next: NextFunction) => { try { - const corrections = await db.adminRepo.getSuggestedCorrections(); + const corrections = await db.adminRepo.getSuggestedCorrections(req.log); res.json(corrections); } catch (error) { next(error); @@ -117,7 +115,7 @@ router.get('/corrections', async (req, res, next: NextFunction) => { router.get('/brands', async (req, res, next: NextFunction) => { try { - const brands = await db.flyerRepo.getAllBrands(); + const brands = await db.flyerRepo.getAllBrands(req.log); res.json(brands); } catch (error) { next(error); @@ -126,7 +124,7 @@ router.get('/brands', async (req, res, next: NextFunction) => { router.get('/stats', async (req, res, next: NextFunction) => { try { - const stats = await db.adminRepo.getApplicationStats(); + const stats = await db.adminRepo.getApplicationStats(req.log); res.json(stats); } catch (error) { next(error); @@ -135,7 +133,7 @@ router.get('/stats', async (req, res, next: NextFunction) => { router.get('/stats/daily', async (req, res, next: NextFunction) => { try { - const dailyStats = await db.adminRepo.getDailyStatsForLast30Days(); + const dailyStats = await db.adminRepo.getDailyStatsForLast30Days(req.log); res.json(dailyStats); } catch (error) { next(error); @@ -145,7 +143,7 @@ router.get('/stats/daily', async (req, res, next: NextFunction) => { router.post('/corrections/:id/approve', validateRequest(numericIdParamSchema('id')), async (req, res, next: NextFunction) => { try { const correctionId = req.params.id as unknown as number; - await db.adminRepo.approveCorrection(correctionId); + await db.adminRepo.approveCorrection(correctionId, req.log); res.status(200).json({ message: 'Correction approved successfully.' }); } catch (error) { next(error); @@ -155,7 +153,7 @@ router.post('/corrections/:id/approve', validateRequest(numericIdParamSchema('id router.post('/corrections/:id/reject', validateRequest(numericIdParamSchema('id')), async (req, res, next: NextFunction) => { try { const correctionId = req.params.id as unknown as number; - await db.adminRepo.rejectCorrection(correctionId); + await db.adminRepo.rejectCorrection(correctionId, req.log); res.status(200).json({ message: 'Correction rejected successfully.' }); } catch (error) { next(error); @@ -166,7 +164,7 @@ router.put('/corrections/:id', validateRequest(updateCorrectionSchema), async (r const correctionId = req.params.id as unknown as number; const { suggested_value } = req.body; try { - const updatedCorrection = await db.adminRepo.updateSuggestedCorrection(correctionId, suggested_value); + const updatedCorrection = await db.adminRepo.updateSuggestedCorrection(correctionId, suggested_value, req.log); res.status(200).json(updatedCorrection); } catch (error) { next(error); @@ -177,7 +175,7 @@ router.put('/recipes/:id/status', validateRequest(updateRecipeStatusSchema), asy const recipeId = req.params.id as unknown as number; const { status } = req.body; try { - const updatedRecipe = await db.adminRepo.updateRecipeStatus(recipeId, status); // This is still a standalone function in admin.db.ts + const updatedRecipe = await db.adminRepo.updateRecipeStatus(recipeId, status, req.log); // This is still a standalone function in admin.db.ts res.status(200).json(updatedRecipe); } catch (error) { next(error); // Pass all errors to the central error handler @@ -193,9 +191,9 @@ router.post('/brands/:id/logo', validateRequest(numericIdParamSchema('id')), upl throw new ValidationError([], 'Logo image file is missing.'); } const logoUrl = `/assets/${req.file.filename}`; - await db.adminRepo.updateBrandLogo(brandId, logoUrl); + await db.adminRepo.updateBrandLogo(brandId, logoUrl, req.log); - logger.info(`Brand logo updated for brand ID: ${brandId}`, { brandId, logoUrl }); + logger.info({ brandId, logoUrl }, `Brand logo updated for brand ID: ${brandId}`); res.status(200).json({ message: 'Brand logo updated successfully.', logoUrl }); } catch (error) { next(error); @@ -204,7 +202,7 @@ router.post('/brands/:id/logo', validateRequest(numericIdParamSchema('id')), upl router.get('/unmatched-items', async (req, res, next: NextFunction) => { try { - const items = await db.adminRepo.getUnmatchedFlyerItems(); + const items = await db.adminRepo.getUnmatchedFlyerItems(req.log); res.json(items); } catch (error) { next(error); @@ -220,7 +218,7 @@ router.delete('/recipes/:recipeId', validateRequest(numericIdParamSchema('recipe try { // The isAdmin flag bypasses the ownership check in the repository method. - await db.recipeRepo.deleteRecipe(recipeId, adminUser.user_id, true); + await db.recipeRepo.deleteRecipe(recipeId, adminUser.user_id, true, req.log); res.status(204).send(); } catch (error: unknown) { next(error); @@ -233,7 +231,7 @@ router.delete('/recipes/:recipeId', validateRequest(numericIdParamSchema('recipe router.delete('/flyers/:flyerId', validateRequest(numericIdParamSchema('flyerId')), async (req, res, next: NextFunction) => { const flyerId = req.params.flyerId as unknown as number; try { - await db.flyerRepo.deleteFlyer(flyerId); + await db.flyerRepo.deleteFlyer(flyerId, req.log); res.status(204).send(); } catch (error: unknown) { next(error); @@ -244,7 +242,7 @@ router.put('/comments/:id/status', validateRequest(updateCommentStatusSchema), a const commentId = req.params.id as unknown as number; const { status } = req.body; try { - const updatedComment = await db.adminRepo.updateRecipeCommentStatus(commentId, status); // This is still a standalone function in admin.db.ts + const updatedComment = await db.adminRepo.updateRecipeCommentStatus(commentId, status, req.log); // This is still a standalone function in admin.db.ts res.status(200).json(updatedComment); } catch (error: unknown) { next(error); @@ -253,7 +251,7 @@ router.put('/comments/:id/status', validateRequest(updateCommentStatusSchema), a router.get('/users', async (req, res, next: NextFunction) => { try { - const users = await db.adminRepo.getAllUsers(); + const users = await db.adminRepo.getAllUsers(req.log); res.json(users); } catch (error) { next(error); @@ -263,7 +261,7 @@ router.get('/users', async (req, res, next: NextFunction) => { router.get('/activity-log', validateRequest(activityLogSchema), async (req, res, next: NextFunction) => { const { limit, offset } = req.query as unknown as { limit: number; offset: number }; try { - const logs = await db.adminRepo.getActivityLog(limit, offset); + const logs = await db.adminRepo.getActivityLog(limit, offset, req.log); res.json(logs); } catch (error) { next(error); @@ -272,7 +270,7 @@ router.get('/activity-log', validateRequest(activityLogSchema), async (req, res, router.get('/users/:id', validateRequest(z.object({ params: z.object({ id: z.string().uuid() }) })), async (req, res, next: NextFunction) => { try { - const user = await db.userRepo.findUserProfileById(req.params.id); + const user = await db.userRepo.findUserProfileById(req.params.id, req.log); res.json(user); } catch (error) { next(error); @@ -283,10 +281,10 @@ router.put('/users/:id', validateRequest(updateUserRoleSchema), async (req, res, const { role } = req.body; try { - const updatedUser = await db.adminRepo.updateUserRole(req.params.id, role); + const updatedUser = await db.adminRepo.updateUserRole(req.params.id, role, req.log); res.json(updatedUser); } catch (error) { - logger.error(`Error updating user ${req.params.id}:`, { error }); + logger.error({ error }, `Error updating user ${req.params.id}:`); next(error); } }); @@ -297,7 +295,7 @@ router.delete('/users/:id', validateRequest(z.object({ params: z.object({ id: z. if (adminUser.user.user_id === req.params.id) { throw new ValidationError([], 'Admins cannot delete their own account.'); } - await db.userRepo.deleteUserById(req.params.id); + await db.userRepo.deleteUserById(req.params.id, req.log); res.status(204).send(); } catch (error) { next(error); @@ -318,7 +316,7 @@ router.post('/trigger/daily-deal-check', async (req, res, next: NextFunction) => backgroundJobService.runDailyDealCheck(); res.status(202).json({ message: 'Daily deal check job has been triggered successfully. It will run in the background.' }); } catch (error) { - logger.error('[Admin] Failed to trigger daily deal check job.', { error }); + logger.error({ error }, '[Admin] Failed to trigger daily deal check job.'); next(error); } }); @@ -340,7 +338,7 @@ router.post('/trigger/analytics-report', async (req, res, next: NextFunction) => res.status(202).json({ message: `Analytics report generation job has been enqueued successfully. Job ID: ${job.id}` }); } catch (error) { - logger.error('[Admin] Failed to enqueue analytics report job.', { error }); + logger.error({ error }, '[Admin] Failed to enqueue analytics report job.'); next(error); } }); @@ -390,10 +388,10 @@ router.post('/system/clear-geocode-cache', async (req, res, next: NextFunction) logger.info(`[Admin] Manual trigger for geocode cache clear received from user: ${adminUser.user_id}`); try { - const keysDeleted = await clearGeocodeCache(); + const keysDeleted = await geocodingService.clearGeocodeCache(req.log); res.status(200).json({ message: `Successfully cleared the geocode cache. ${keysDeleted} keys were removed.` }); } catch (error) { - logger.error('[Admin] Failed to clear geocode cache.', { error }); + logger.error({ error }, '[Admin] Failed to clear geocode cache.'); next(error); } }); diff --git a/src/services/db/user.db.test.ts b/src/services/db/user.db.test.ts index 62f3de3..0259668 100644 --- a/src/services/db/user.db.test.ts +++ b/src/services/db/user.db.test.ts @@ -8,7 +8,6 @@ // --- END FIX REGISTRY --- import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { PoolClient } from 'pg'; -import { withTransaction } from './connection.db'; // Mock the logger to prevent stderr noise during tests vi.mock('../logger.server', () => ({ @@ -24,11 +23,12 @@ import { logger as mockLogger } from '../logger.server'; // Un-mock the module we are testing to ensure we use the real implementation. vi.unmock('./user.db'); -// Mock the withTransaction helper since we are testing a function that uses it. +// Mock the withTransaction helper. This is the key fix. vi.mock('./connection.db', async (importOriginal) => { const actual = await importOriginal(); return { ...actual, withTransaction: vi.fn() }; }); +import { withTransaction } from './connection.db'; import { UserRepository, exportUserData } from './user.db'; import { mockPoolInstance } from '../../tests/setup/tests-setup-unit'; @@ -61,8 +61,7 @@ describe('User DB Service', () => { beforeEach(() => { vi.clearAllMocks(); userRepo = new UserRepository(mockPoolInstance as any); - // Reset the withTransaction mock before each test - const { withTransaction } = require('./connection.db'); // eslint-disable-line @typescript-eslint/no-var-requires + // Provide a default mock implementation for withTransaction for all tests. vi.mocked(withTransaction).mockImplementation(async (callback: (client: PoolClient) => Promise) => callback(mockPoolInstance as any)); }); diff --git a/src/services/queueService.server.test.ts b/src/services/queueService.server.test.ts index 647bd64..3762cff 100644 --- a/src/services/queueService.server.test.ts +++ b/src/services/queueService.server.test.ts @@ -104,6 +104,11 @@ describe('Queue Service Setup and Lifecycle', () => { cleanupWorker = queueService.cleanupWorker; }); + afterEach(() => { + // Clean up all event listeners on the mock connection to prevent open handles. + mocks.mockRedisConnection.removeAllListeners(); + }); + it('should log a success message when Redis connects', () => { // Act: Simulate the 'connect' event on the mock Redis connection mocks.mockRedisConnection.emit('connect');