many fixes resultnig from latest refactoring
Some checks failed
Deploy to Web Server flyer-crawler.projectium.com / deploy (push) Failing after 5m46s
Some checks failed
Deploy to Web Server flyer-crawler.projectium.com / deploy (push) Failing after 5m46s
This commit is contained in:
@@ -20,7 +20,6 @@ import systemRouter from './src/routes/system.routes';
|
||||
import healthRouter from './src/routes/health.routes';
|
||||
import { errorHandler } from './src/middleware/errorHandler';
|
||||
import { backgroundJobService, startBackgroundJobs } from './src/services/backgroundJobService.ts';
|
||||
import * as db from './src/services/db/index.db';
|
||||
import { analyticsQueue, weeklyAnalyticsQueue, gracefulShutdown } from './src/services/queueService.server';
|
||||
|
||||
// --- START DEBUG LOGGING ---
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
// src/App.tsx
|
||||
import React, { useState, useCallback, useEffect } from 'react';
|
||||
import { Routes, Route, useParams, useLocation, Outlet } from 'react-router-dom';
|
||||
import { Routes, Route, useParams, useLocation } from 'react-router-dom';
|
||||
import { Toaster } from 'react-hot-toast';
|
||||
import * as pdfjsLib from 'pdfjs-dist';
|
||||
import { Header } from './components/Header';
|
||||
import { logger } from './services/logger.client';
|
||||
import type { Flyer, Profile, User, UserProfile } from './types';
|
||||
import * as apiClient from './services/apiClient';
|
||||
import type { Flyer, Profile, User } from './types';
|
||||
import { ProfileManager } from './pages/admin/components/ProfileManager';
|
||||
import { VoiceAssistant } from './features/voice-assistant/VoiceAssistant';
|
||||
import { AdminPage } from './pages/admin/AdminPage';
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import * as apiClient from '../services/apiClient';
|
||||
import { LeaderboardUser } from '../types';
|
||||
import { logger } from '../services/logger';
|
||||
import { logger } from '../services/logger.client';
|
||||
import { Award, Crown, ShieldAlert } from 'lucide-react';
|
||||
|
||||
export const Leaderboard: React.FC = () => {
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
import { Pool } from 'pg';
|
||||
import bcrypt from 'bcrypt';
|
||||
import { logger } from '../services/logger';
|
||||
import { logger } from '../services/logger.server';
|
||||
import { CATEGORIES } from '../types';
|
||||
|
||||
const pool = new Pool({
|
||||
|
||||
@@ -8,7 +8,7 @@ import { TrashIcon } from '../../components/icons/TrashIcon';
|
||||
import { SpeakerWaveIcon } from '../../components/icons/SpeakerWaveIcon';
|
||||
import { generateSpeechFromText } from '../../services/aiApiClient';
|
||||
import { decode, decodeAudioData } from '../../utils/audioUtils';
|
||||
import { logger } from '../../services/logger';
|
||||
import { logger } from '../../services/logger.client';
|
||||
|
||||
interface ShoppingListComponentProps {
|
||||
user: User | null;
|
||||
|
||||
@@ -9,7 +9,7 @@ import { CATEGORIES } from '../../types';
|
||||
import { TrashIcon } from '../../components/icons/TrashIcon';
|
||||
import { UserIcon } from '../../components/icons/UserIcon';
|
||||
import { PlusCircleIcon } from '../../components/icons/PlusCircleIcon';
|
||||
import { logger } from '../../services/logger';
|
||||
import { logger } from '../../services/logger.client';
|
||||
interface WatchedItemsListProps {
|
||||
items: MasterGroceryItem[];
|
||||
onAddItem: (itemName: string, category: string) => Promise<void>;
|
||||
|
||||
@@ -5,7 +5,7 @@ import { MicrophoneIcon } from '../../components/icons/MicrophoneIcon';
|
||||
// FIX: Corrected the import path. Types should be imported from the top-level '@google/genai' package.
|
||||
import type { LiveServerMessage, Blob } from '@google/genai';
|
||||
import { encode } from '../../utils/audioUtils';
|
||||
import { logger } from '../../services/logger';
|
||||
import { logger } from '../../services/logger.client';
|
||||
import { XMarkIcon } from '../../components/icons/XMarkIcon';
|
||||
|
||||
interface VoiceAssistantProps {
|
||||
|
||||
@@ -75,7 +75,7 @@ describe('useActiveDeals Hook', () => {
|
||||
mockedApiClient.countFlyerItemsForFlyers.mockResolvedValue(new Response(JSON.stringify({ count: 0 })));
|
||||
mockedApiClient.fetchFlyerItemsForFlyers.mockResolvedValue(new Response(JSON.stringify([])));
|
||||
|
||||
const { result } = renderHook(() => useActiveDeals(mockFlyers, mockWatchedItems));
|
||||
renderHook(() => useActiveDeals(mockFlyers, mockWatchedItems));
|
||||
|
||||
await waitFor(() => {
|
||||
// Only the valid flyer (id: 1) should be used in the API calls
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { WatchedItemDeal } from '../types';
|
||||
import { apiFetch } from '../services/apiClient';
|
||||
import { logger } from '../services/logger';
|
||||
import { logger } from '../services/logger.client';
|
||||
import { AlertCircle, Tag, Store, Calendar } from 'lucide-react';
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import * as apiClient from '../services/apiClient';
|
||||
import { UserProfile, Achievement, UserAchievement } from '../types';
|
||||
import { logger } from '../services/logger';
|
||||
import { logger } from '../services/logger.client';
|
||||
import { notifySuccess, notifyError } from '../services/notificationService';
|
||||
import { AchievementsList } from '../components/AchievementsList';
|
||||
|
||||
|
||||
@@ -59,7 +59,7 @@ vi.mock('@bull-board/express', () => ({
|
||||
}));
|
||||
|
||||
// Import the mocked modules to control them
|
||||
import { BackgroundJobService, backgroundJobService } from '../services/backgroundJobService';
|
||||
import { backgroundJobService } from '../services/backgroundJobService';
|
||||
import { flyerQueue, analyticsQueue, cleanupQueue } from '../services/queueService.server';
|
||||
|
||||
// Mock the logger
|
||||
|
||||
@@ -88,7 +88,7 @@ const createApp = (user?: UserProfile) => {
|
||||
});
|
||||
}
|
||||
app.use('/api/admin', adminRouter);
|
||||
app.use((err: Error, req: Request, res: Response, next: NextFunction) => {
|
||||
app.use((err: Error, req: Request, res: Response ) => {
|
||||
res.status(500).json({ message: err.message || 'Internal Server Error' });
|
||||
});
|
||||
return app;
|
||||
|
||||
@@ -66,7 +66,7 @@ const createApp = (user?: UserProfile) => {
|
||||
});
|
||||
}
|
||||
app.use('/api/admin', adminRouter);
|
||||
app.use((err: Error, req: Request, res: Response, next: NextFunction) => {
|
||||
app.use((err: Error, req: Request, res: Response) => {
|
||||
res.status(500).json({ message: err.message || 'Internal Server Error' });
|
||||
});
|
||||
return app;
|
||||
|
||||
@@ -4,7 +4,7 @@ import supertest from 'supertest';
|
||||
import express, { Request, Response, NextFunction } from 'express';
|
||||
import adminRouter from './admin.routes';
|
||||
import { createMockUserProfile } from '../tests/utils/mockFactories';
|
||||
import { User, UserProfile, SuggestedCorrection, Brand, Recipe, RecipeComment, UnmatchedFlyerItem, ActivityLogItem, AdminUserView } from '../types';
|
||||
import { User, UserProfile } from '../types';
|
||||
|
||||
// Mock the Service Layer directly.
|
||||
// The admin.routes.ts file imports from '.../db/index.db'. We need to mock that module.
|
||||
@@ -90,7 +90,7 @@ const createApp = (user?: UserProfile) => {
|
||||
});
|
||||
}
|
||||
app.use('/api/admin', adminRouter);
|
||||
app.use((err: Error, req: Request, res: Response, next: NextFunction) => {
|
||||
app.use((err: Error, req: Request, res: Response) => {
|
||||
res.status(500).json({ message: err.message || 'Internal Server Error' });
|
||||
});
|
||||
return app;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// src/routes/ai.test.ts
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import supertest from 'supertest';
|
||||
import express, { type Request, type Response, type NextFunction, Router } from 'express';
|
||||
import express, { type Request, type Response, type NextFunction } from 'express';
|
||||
import path from 'node:path';
|
||||
import aiRouter from './ai.routes';
|
||||
import { createMockUserProfile, createMockFlyer } from '../tests/utils/mockFactories';
|
||||
@@ -61,7 +61,7 @@ app.use(express.json({ strict: false }));
|
||||
app.use('/api/ai', aiRouter);
|
||||
|
||||
// Add a generic error handler to catch errors passed via next()
|
||||
app.use((err: Error, req: Request, res: Response, next: NextFunction) => {
|
||||
app.use((err: Error, req: Request, res: Response) => {
|
||||
res.status(500).json({ message: err.message || 'Internal Server Error' });
|
||||
});
|
||||
|
||||
|
||||
@@ -276,7 +276,7 @@ router.post('/flyers/process', optionalAuth, uploadToDisk.single('flyerImage'),
|
||||
// 3. Create flyer and its items in a transaction
|
||||
const { flyer: newFlyer, items: newItems } = await createFlyerAndItems(flyerData, itemsArray);
|
||||
|
||||
logger.info(`Successfully processed and saved new flyer: ${newFlyer.file_name} (ID: ${newFlyer.flyer_id})`);
|
||||
logger.info(`Successfully processed and saved new flyer: ${newFlyer.file_name} (ID: ${newFlyer.flyer_id}) with ${newItems.length} items.`);
|
||||
|
||||
// Log this significant event
|
||||
await db.adminRepo.logActivity({
|
||||
|
||||
@@ -54,7 +54,7 @@ type PassportCallback = (error: Error | null, user: Express.User | false, info?:
|
||||
// Mock Passport middleware
|
||||
vi.mock('./passport.routes', () => ({
|
||||
default: {
|
||||
authenticate: (strategy: string, options: Record<string, unknown>, callback: PassportCallback) => (req: Request, res: any, next: any) => {
|
||||
authenticate: (strategy: string, options: Record<string, unknown>, callback: PassportCallback) => (req: Request, res: any) => {
|
||||
// Logic to simulate passport authentication outcome based on test input
|
||||
if (req.body.password === 'wrong_password') {
|
||||
// Simulate incorrect credentials
|
||||
@@ -84,7 +84,7 @@ app.use(cookieParser()); // Add cookie-parser middleware to populate req.cookies
|
||||
app.use('/api/auth', authRouter);
|
||||
|
||||
// Add error handler to catch and log 500s during tests
|
||||
app.use((err: any, req: Request, res: any, next: any) => {
|
||||
app.use((err: any, req: Request, res: any) => {
|
||||
console.error('[TEST APP ERROR]', err);
|
||||
res.status(500).json({ message: err.message });
|
||||
});
|
||||
|
||||
@@ -76,7 +76,7 @@ router.post('/register', async (req, res, next) => {
|
||||
newUser = await repoWithTransaction.createUser(email, hashedPassword, { full_name, avatar_url });
|
||||
|
||||
await client.query('COMMIT');
|
||||
} catch (error: any) {
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof UniqueConstraintError) {
|
||||
// If the email is a duplicate, return a 409 Conflict status.
|
||||
return res.status(409).json({ message: error.message });
|
||||
|
||||
@@ -45,7 +45,6 @@ fs.mkdir(avatarUploadDir, { recursive: true }).catch(err => {
|
||||
const avatarStorage = multer.diskStorage({
|
||||
destination: (req, file, cb) => cb(null, avatarUploadDir),
|
||||
filename: (req, file, cb) => {
|
||||
const user = req.user as User;
|
||||
const uniqueSuffix = `${(req.user as UserProfile).user.user_id}-${Date.now()}${path.extname(file.originalname)}`;
|
||||
cb(null, uniqueSuffix);
|
||||
},
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// src/services/aiService.server.test.ts
|
||||
import { describe, it, expect, vi, beforeEach, afterAll } from 'vitest';
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import type { MasterGroceryItem } from '../types';
|
||||
// Import the class, not the singleton instance, so we can instantiate it with mocks.
|
||||
import { AIService } from './aiService.server';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// src/services/apiClient.test.ts
|
||||
import { describe, it, expect, vi, beforeAll, afterAll, afterEach, beforeEach } from 'vitest';
|
||||
import { describe, it, expect, vi, afterAll, afterEach, beforeEach } from 'vitest';
|
||||
import { setupServer } from 'msw/node';
|
||||
import { http, HttpResponse } from 'msw';
|
||||
|
||||
@@ -43,27 +43,31 @@ describe('API Client', () => {
|
||||
// These variables will be used to capture details from the fetch call.
|
||||
let capturedUrl: URL | null = null;
|
||||
let capturedHeaders: Headers | null = null;
|
||||
let capturedBody: any = null;
|
||||
let capturedBody: string | FormData | Record<string, unknown> | null = null;
|
||||
|
||||
beforeEach(() => {
|
||||
// This mock now captures the arguments passed to fetch before returning a successful response.
|
||||
global.fetch = vi.fn((url, options) => {
|
||||
// Define a correctly typed mock function for fetch.
|
||||
const mockFetch = (url: RequestInfo | URL, options?: RequestInit): Promise<Response> => {
|
||||
capturedUrl = new URL(url as string, 'http://localhost');
|
||||
capturedHeaders = options?.headers as Headers;
|
||||
|
||||
// FIX: Parse body if it is a string
|
||||
if (typeof options?.body === 'string') {
|
||||
try {
|
||||
capturedBody = JSON.parse(options.body);
|
||||
} catch {
|
||||
capturedBody = options.body;
|
||||
}
|
||||
} else if (options?.body instanceof FormData) {
|
||||
capturedBody = options.body;
|
||||
} else {
|
||||
capturedBody = options?.body || null;
|
||||
capturedBody = null;
|
||||
}
|
||||
|
||||
return Promise.resolve(new Response(JSON.stringify({ data: 'mock-success' }), { status: 200, headers: new Headers() }));
|
||||
}) as any;
|
||||
return Promise.resolve(new Response(JSON.stringify({ data: 'mock-success' }), { status: 200, headers: new Headers() as Headers }));
|
||||
};
|
||||
|
||||
// Assign the mock function to global.fetch and cast it to a Vitest mock.
|
||||
global.fetch = vi.fn(mockFetch);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@@ -184,10 +188,8 @@ describe('API Client', () => {
|
||||
|
||||
describe('Budget API Functions', () => {
|
||||
it('getBudgets should call the correct endpoint', async () => {
|
||||
let wasCalled = false;
|
||||
server.use(
|
||||
http.get('http://localhost/api/budgets', () => {
|
||||
wasCalled = true;
|
||||
return HttpResponse.json([]);
|
||||
})
|
||||
);
|
||||
@@ -350,10 +352,8 @@ describe('API Client', () => {
|
||||
|
||||
it('forkRecipe should send a POST request to the correct URL', async () => {
|
||||
const recipeId = 99;
|
||||
let wasCalled = false;
|
||||
server.use(
|
||||
http.post(`http://localhost/api/recipes/${recipeId}/fork`, () => {
|
||||
wasCalled = true;
|
||||
return HttpResponse.json({ success: true });
|
||||
})
|
||||
);
|
||||
@@ -459,10 +459,8 @@ describe('API Client', () => {
|
||||
});
|
||||
|
||||
it('checkDbSchema should call the correct health check endpoint', async () => {
|
||||
let wasCalled = false;
|
||||
server.use(
|
||||
http.get('http://localhost/api/health/db-schema', () => {
|
||||
wasCalled = true;
|
||||
return HttpResponse.json({ success: true });
|
||||
})
|
||||
);
|
||||
@@ -471,10 +469,8 @@ describe('API Client', () => {
|
||||
});
|
||||
|
||||
it('checkStorage should call the correct health check endpoint', async () => {
|
||||
let wasCalled = false;
|
||||
server.use(
|
||||
http.get('http://localhost/api/health/storage', () => {
|
||||
wasCalled = true;
|
||||
return HttpResponse.json({ success: true });
|
||||
})
|
||||
);
|
||||
@@ -483,10 +479,8 @@ describe('API Client', () => {
|
||||
});
|
||||
|
||||
it('checkDbPoolHealth should call the correct health check endpoint', async () => {
|
||||
let wasCalled = false;
|
||||
server.use(
|
||||
http.get('http://localhost/api/health/db-pool', () => {
|
||||
wasCalled = true;
|
||||
return HttpResponse.json({ success: true });
|
||||
})
|
||||
);
|
||||
@@ -495,10 +489,8 @@ describe('API Client', () => {
|
||||
});
|
||||
|
||||
it('checkRedisHealth should call the correct health check endpoint', async () => {
|
||||
let wasCalled = false;
|
||||
server.use(
|
||||
http.get('http://localhost/api/health/redis', () => {
|
||||
wasCalled = true;
|
||||
return HttpResponse.json({ success: true });
|
||||
})
|
||||
);
|
||||
@@ -507,10 +499,8 @@ describe('API Client', () => {
|
||||
});
|
||||
|
||||
it('checkPm2Status should call the correct system endpoint', async () => {
|
||||
let wasCalled = false;
|
||||
server.use(
|
||||
http.get('http://localhost/api/system/pm2-status', () => {
|
||||
wasCalled = true;
|
||||
return HttpResponse.json({ success: true });
|
||||
})
|
||||
);
|
||||
@@ -519,10 +509,8 @@ describe('API Client', () => {
|
||||
});
|
||||
|
||||
it('fetchFlyers should call the correct public endpoint', async () => {
|
||||
let wasCalled = false;
|
||||
server.use(
|
||||
http.get('http://localhost/api/flyers', () => {
|
||||
wasCalled = true;
|
||||
return HttpResponse.json([]);
|
||||
})
|
||||
);
|
||||
@@ -531,10 +519,8 @@ describe('API Client', () => {
|
||||
});
|
||||
|
||||
it('fetchMasterItems should call the correct public endpoint', async () => {
|
||||
let wasCalled = false;
|
||||
server.use(
|
||||
http.get('http://localhost/api/master-items', () => {
|
||||
wasCalled = true;
|
||||
return HttpResponse.json([]);
|
||||
})
|
||||
);
|
||||
@@ -543,10 +529,8 @@ describe('API Client', () => {
|
||||
});
|
||||
|
||||
it('fetchCategories should call the correct public endpoint', async () => {
|
||||
let wasCalled = false;
|
||||
server.use(
|
||||
http.get('http://localhost/api/categories', () => {
|
||||
wasCalled = true;
|
||||
return HttpResponse.json([]);
|
||||
})
|
||||
);
|
||||
@@ -556,10 +540,8 @@ describe('API Client', () => {
|
||||
|
||||
it('fetchFlyerItems should call the correct public endpoint for a specific flyer', async () => {
|
||||
const flyerId = 123;
|
||||
let wasCalled = false;
|
||||
server.use(
|
||||
http.get(`http://localhost/api/flyers/${flyerId}/items`, () => {
|
||||
wasCalled = true;
|
||||
return HttpResponse.json([]);
|
||||
})
|
||||
);
|
||||
|
||||
@@ -9,15 +9,16 @@ vi.mock('node-cron', () => ({ default: { schedule: mockCronSchedule } }));
|
||||
|
||||
import { BackgroundJobService, startBackgroundJobs } from './backgroundJobService';
|
||||
import type { Queue } from 'bullmq';
|
||||
import { WatchedItemDeal } from '../types';
|
||||
import { AdminUserView } from './db/admin.db';
|
||||
|
||||
describe('Background Job Service', () => {
|
||||
// Create mock dependencies that will be injected into the service
|
||||
const mockDbService = {
|
||||
createBulkNotifications: vi.fn(),
|
||||
const mockPersonalizationRepo = {
|
||||
getBestSalePricesForAllUsers: vi.fn(),
|
||||
};
|
||||
const mockNotificationRepo = {
|
||||
createBulkNotifications: vi.fn(),
|
||||
};
|
||||
|
||||
const mockEmailQueue = {
|
||||
add: vi.fn(),
|
||||
};
|
||||
@@ -47,27 +48,27 @@ describe('Background Job Service', () => {
|
||||
];
|
||||
|
||||
// Instantiate the service with mock dependencies for each test run
|
||||
const service = new BackgroundJobService(mockDbService as any, mockEmailQueue as unknown as Queue<any>, mockLogger);
|
||||
const service = new BackgroundJobService(mockPersonalizationRepo as any, mockNotificationRepo as any, mockEmailQueue as unknown as Queue<any>, mockLogger);
|
||||
|
||||
it('should do nothing if no deals are found for any user', async () => {
|
||||
mockDbService.getBestSalePricesForAllUsers.mockResolvedValue([]);
|
||||
mockPersonalizationRepo.getBestSalePricesForAllUsers.mockResolvedValue([]);
|
||||
|
||||
await service.runDailyDealCheck();
|
||||
|
||||
expect(mockLogger.info).toHaveBeenCalledWith('[BackgroundJob] Starting daily deal check for all users...');
|
||||
expect(mockLogger.info).toHaveBeenCalledWith('[BackgroundJob] No deals found for any watched items. Skipping.');
|
||||
expect(mockDbService.getBestSalePricesForAllUsers).toHaveBeenCalledTimes(1);
|
||||
expect(mockPersonalizationRepo.getBestSalePricesForAllUsers).toHaveBeenCalledTimes(1);
|
||||
expect(mockEmailQueue.add).not.toHaveBeenCalled();
|
||||
expect(mockDbService.createBulkNotifications).not.toHaveBeenCalled();
|
||||
expect(mockNotificationRepo.createBulkNotifications).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should create notifications and enqueue emails when deals are found', async () => {
|
||||
mockDbService.getBestSalePricesForAllUsers.mockResolvedValue(mockDealsForAllUsers);
|
||||
mockPersonalizationRepo.getBestSalePricesForAllUsers.mockResolvedValue(mockDealsForAllUsers);
|
||||
|
||||
await service.runDailyDealCheck();
|
||||
|
||||
// Check that it fetched all deals once
|
||||
expect(mockDbService.getBestSalePricesForAllUsers).toHaveBeenCalledTimes(1);
|
||||
expect(mockPersonalizationRepo.getBestSalePricesForAllUsers).toHaveBeenCalledTimes(1);
|
||||
|
||||
// Check that email jobs were enqueued for both users
|
||||
expect(mockEmailQueue.add).toHaveBeenCalledTimes(2);
|
||||
@@ -80,8 +81,8 @@ describe('Background Job Service', () => {
|
||||
expect(secondEmailJob[1].html).toContain('Bread');
|
||||
|
||||
// Check that in-app notifications were created for both users
|
||||
expect(mockDbService.createBulkNotifications).toHaveBeenCalledTimes(1);
|
||||
const notificationPayload = mockDbService.createBulkNotifications.mock.calls[0][0];
|
||||
expect(mockNotificationRepo.createBulkNotifications).toHaveBeenCalledTimes(1);
|
||||
const notificationPayload = mockNotificationRepo.createBulkNotifications.mock.calls[0][0];
|
||||
expect(notificationPayload).toHaveLength(2);
|
||||
// Use expect.arrayContaining to be order-agnostic.
|
||||
expect(notificationPayload).toEqual(expect.arrayContaining([
|
||||
@@ -99,7 +100,7 @@ describe('Background Job Service', () => {
|
||||
});
|
||||
|
||||
it('should handle and log errors for individual users without stopping the process', async () => {
|
||||
mockDbService.getBestSalePricesForAllUsers.mockResolvedValue(mockDealsForAllUsers);
|
||||
mockPersonalizationRepo.getBestSalePricesForAllUsers.mockResolvedValue(mockDealsForAllUsers);
|
||||
// Simulate the email queue failing for the first user but succeeding for the second
|
||||
mockEmailQueue.add
|
||||
.mockRejectedValueOnce(new Error('Email queue is down'))
|
||||
@@ -116,13 +117,13 @@ describe('Background Job Service', () => {
|
||||
// Check that it still processed user 2 successfully
|
||||
// The email queue add should be attempted for both users.
|
||||
expect(mockEmailQueue.add).toHaveBeenCalledTimes(2);
|
||||
expect(mockDbService.createBulkNotifications).toHaveBeenCalledTimes(1);
|
||||
expect(mockDbService.createBulkNotifications.mock.calls[0][0]).toHaveLength(1); // Only one notification created
|
||||
expect(mockDbService.createBulkNotifications.mock.calls[0][0][0].user_id).toBe('user-2');
|
||||
expect(mockNotificationRepo.createBulkNotifications).toHaveBeenCalledTimes(1);
|
||||
expect(mockNotificationRepo.createBulkNotifications.mock.calls[0][0]).toHaveLength(1); // Only one notification created
|
||||
expect(mockNotificationRepo.createBulkNotifications.mock.calls[0][0][0].user_id).toBe('user-2');
|
||||
});
|
||||
|
||||
it('should log a critical error if getBestSalePricesForAllUsers fails', async () => {
|
||||
mockDbService.getBestSalePricesForAllUsers.mockRejectedValue(new Error('Critical DB Failure'));
|
||||
mockPersonalizationRepo.getBestSalePricesForAllUsers.mockRejectedValue(new Error('Critical DB Failure'));
|
||||
await expect(service.runDailyDealCheck()).rejects.toThrow('Critical DB Failure');
|
||||
expect(mockLogger.error).toHaveBeenCalledWith(
|
||||
'[BackgroundJob] A critical error occurred during the daily deal check:',
|
||||
@@ -139,23 +140,28 @@ describe('Background Job Service', () => {
|
||||
const mockAnalyticsQueue = {
|
||||
add: vi.fn(),
|
||||
} as unknown as Queue;
|
||||
const mockWeeklyAnalyticsQueue = {
|
||||
add: vi.fn(),
|
||||
} as unknown as Queue;
|
||||
|
||||
beforeEach(() => {
|
||||
mockCronSchedule.mockClear();
|
||||
vi.mocked(mockBackgroundJobService.runDailyDealCheck).mockClear();
|
||||
vi.mocked(mockAnalyticsQueue.add).mockClear();
|
||||
vi.mocked(mockWeeklyAnalyticsQueue.add).mockClear();
|
||||
});
|
||||
|
||||
it('should schedule two cron jobs with the correct schedules', () => {
|
||||
startBackgroundJobs(mockBackgroundJobService, mockAnalyticsQueue);
|
||||
it('should schedule three cron jobs with the correct schedules', () => {
|
||||
startBackgroundJobs(mockBackgroundJobService, mockAnalyticsQueue, mockWeeklyAnalyticsQueue);
|
||||
|
||||
expect(mockCronSchedule).toHaveBeenCalledTimes(2);
|
||||
expect(mockCronSchedule).toHaveBeenCalledTimes(3);
|
||||
expect(mockCronSchedule).toHaveBeenCalledWith('0 2 * * *', expect.any(Function));
|
||||
expect(mockCronSchedule).toHaveBeenCalledWith('0 3 * * *', expect.any(Function));
|
||||
expect(mockCronSchedule).toHaveBeenCalledWith('0 4 * * 0', expect.any(Function));
|
||||
});
|
||||
|
||||
it('should call runDailyDealCheck when the first cron job function is executed', async () => {
|
||||
startBackgroundJobs(mockBackgroundJobService, mockAnalyticsQueue);
|
||||
startBackgroundJobs(mockBackgroundJobService, mockAnalyticsQueue, mockWeeklyAnalyticsQueue);
|
||||
|
||||
// Get the callback function for the first cron job
|
||||
const dailyDealCheckCallback = mockCronSchedule.mock.calls[0][1];
|
||||
@@ -167,7 +173,7 @@ describe('Background Job Service', () => {
|
||||
it('should log an error and release the lock if runDailyDealCheck fails', async () => {
|
||||
const jobError = new Error('Cron job failed');
|
||||
vi.mocked(mockBackgroundJobService.runDailyDealCheck).mockRejectedValue(jobError);
|
||||
startBackgroundJobs(mockBackgroundJobService, mockAnalyticsQueue);
|
||||
startBackgroundJobs(mockBackgroundJobService, mockAnalyticsQueue, mockWeeklyAnalyticsQueue);
|
||||
|
||||
const dailyDealCheckCallback = mockCronSchedule.mock.calls[0][1];
|
||||
await dailyDealCheckCallback();
|
||||
@@ -189,7 +195,7 @@ describe('Background Job Service', () => {
|
||||
// Make the first call hang indefinitely
|
||||
vi.mocked(mockBackgroundJobService.runDailyDealCheck).mockReturnValue(new Promise(() => {}));
|
||||
|
||||
startBackgroundJobs(mockBackgroundJobService, mockAnalyticsQueue);
|
||||
startBackgroundJobs(mockBackgroundJobService, mockAnalyticsQueue, mockWeeklyAnalyticsQueue);
|
||||
const dailyDealCheckCallback = mockCronSchedule.mock.calls[0][1];
|
||||
|
||||
// Trigger the job once, it will hang
|
||||
@@ -204,7 +210,7 @@ describe('Background Job Service', () => {
|
||||
});
|
||||
|
||||
it('should enqueue an analytics job when the second cron job function is executed', async () => {
|
||||
startBackgroundJobs(mockBackgroundJobService, mockAnalyticsQueue);
|
||||
startBackgroundJobs(mockBackgroundJobService, mockAnalyticsQueue, mockWeeklyAnalyticsQueue);
|
||||
|
||||
const analyticsJobCallback = mockCronSchedule.mock.calls[1][1];
|
||||
await analyticsJobCallback();
|
||||
@@ -215,7 +221,7 @@ describe('Background Job Service', () => {
|
||||
it('should log an error if enqueuing the analytics job fails', async () => {
|
||||
const queueError = new Error('Redis is down');
|
||||
vi.mocked(mockAnalyticsQueue.add).mockRejectedValue(queueError);
|
||||
startBackgroundJobs(mockBackgroundJobService, mockAnalyticsQueue);
|
||||
startBackgroundJobs(mockBackgroundJobService, mockAnalyticsQueue, mockWeeklyAnalyticsQueue);
|
||||
|
||||
const analyticsJobCallback = mockCronSchedule.mock.calls[1][1];
|
||||
await analyticsJobCallback();
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// src/services/geocodingService.server.test.ts
|
||||
import { describe, it, expect, vi, beforeEach, afterEach, type Mocked } from 'vitest';
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
|
||||
// --- Hoisted Mocks ---
|
||||
const mocks = vi.hoisted(() => ({
|
||||
|
||||
Reference in New Issue
Block a user