Some checks failed
Deploy to Test Environment / deploy-to-test (push) Failing after 59s
740 lines
26 KiB
TypeScript
740 lines
26 KiB
TypeScript
// src/services/backgroundJobService.test.ts
|
|
import { describe, it, expect, vi, beforeEach, afterEach, Mocked } from 'vitest';
|
|
import type { Logger } from 'pino';
|
|
|
|
// Use vi.hoisted to ensure the mock variable is available when vi.mock is executed.
|
|
const { mockCronSchedule } = vi.hoisted(() => {
|
|
return { mockCronSchedule: vi.fn() };
|
|
});
|
|
vi.mock('node-cron', () => ({ default: { schedule: mockCronSchedule } }));
|
|
|
|
// Mock the logger.server module globally so cron.schedule callbacks use this mock.
|
|
vi.mock('../services/logger.server', () => ({
|
|
logger: {
|
|
info: vi.fn(),
|
|
error: vi.fn(),
|
|
warn: vi.fn(),
|
|
debug: vi.fn(),
|
|
fatal: vi.fn(),
|
|
trace: vi.fn(),
|
|
silent: vi.fn(),
|
|
},
|
|
}));
|
|
|
|
// Mock the date utility to control the output for the weekly analytics job
|
|
vi.mock('../utils/dateUtils', () => ({
|
|
getSimpleWeekAndYear: vi.fn(() => ({ year: 2024, week: 42 })),
|
|
getCurrentDateISOString: vi.fn(() => '2024-10-18'),
|
|
}));
|
|
|
|
vi.mock('../services/queueService.server', () => ({
|
|
analyticsQueue: {
|
|
add: vi.fn(),
|
|
},
|
|
weeklyAnalyticsQueue: {
|
|
add: vi.fn(),
|
|
},
|
|
emailQueue: {
|
|
add: vi.fn(),
|
|
},
|
|
}));
|
|
|
|
import { BackgroundJobService, startBackgroundJobs } from './backgroundJobService';
|
|
import type { Queue } from 'bullmq';
|
|
import type { PersonalizationRepository } from './db/personalization.db';
|
|
import type { NotificationRepository } from './db/notification.db';
|
|
import { createMockWatchedItemDeal } from '../tests/utils/mockFactories';
|
|
import { logger as globalMockLogger } from '../services/logger.server'; // Import the mocked logger
|
|
import { analyticsQueue, weeklyAnalyticsQueue } from '../services/queueService.server';
|
|
|
|
describe('Background Job Service', () => {
|
|
// Create mock dependencies that will be injected into the service
|
|
const mockPersonalizationRepo = {
|
|
getBestSalePricesForAllUsers: vi.fn(),
|
|
};
|
|
const mockNotificationRepo = {
|
|
createBulkNotifications: vi.fn(),
|
|
};
|
|
const mockEmailQueue = {
|
|
add: vi.fn(),
|
|
};
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
});
|
|
|
|
afterEach(() => {
|
|
vi.useRealTimers();
|
|
});
|
|
|
|
describe('runDailyDealCheck', () => {
|
|
// Mock data representing the result of the new single database query
|
|
const mockDealsForAllUsers = [
|
|
// Deals for user-1
|
|
{
|
|
...createMockWatchedItemDeal({
|
|
master_item_id: 1,
|
|
item_name: 'Apples',
|
|
best_price_in_cents: 199,
|
|
store_name: 'Green Grocer',
|
|
flyer_id: 101,
|
|
valid_to: '2024-10-20',
|
|
}),
|
|
user_id: 'user-1',
|
|
email: 'user1@test.com',
|
|
full_name: 'User One',
|
|
},
|
|
// Deals for user-2
|
|
{
|
|
...createMockWatchedItemDeal({
|
|
master_item_id: 2,
|
|
item_name: 'Milk',
|
|
best_price_in_cents: 450,
|
|
store_name: 'Dairy Farm',
|
|
flyer_id: 102,
|
|
valid_to: '2024-10-21',
|
|
}),
|
|
user_id: 'user-2',
|
|
email: 'user2@test.com',
|
|
full_name: 'User Two',
|
|
},
|
|
{
|
|
...createMockWatchedItemDeal({
|
|
master_item_id: 3,
|
|
item_name: 'Bread',
|
|
best_price_in_cents: 250,
|
|
store_name: 'Bakery',
|
|
flyer_id: 103,
|
|
valid_to: '2024-10-22',
|
|
}),
|
|
user_id: 'user-2',
|
|
email: 'user2@test.com',
|
|
full_name: 'User Two',
|
|
},
|
|
];
|
|
|
|
// Helper to create a type-safe mock logger
|
|
const createMockLogger = (): Logger =>
|
|
({
|
|
info: vi.fn(),
|
|
error: vi.fn(),
|
|
warn: vi.fn(),
|
|
debug: vi.fn(),
|
|
child: vi.fn(() => createMockLogger()),
|
|
}) as unknown as Logger;
|
|
|
|
// Instantiate the service with mock dependencies for each test run
|
|
const mockServiceLogger = createMockLogger();
|
|
const service = new BackgroundJobService(
|
|
mockPersonalizationRepo as unknown as PersonalizationRepository,
|
|
mockNotificationRepo as unknown as NotificationRepository,
|
|
mockEmailQueue as unknown as Queue,
|
|
mockServiceLogger,
|
|
);
|
|
|
|
describe('Manual Triggers', () => {
|
|
it('triggerAnalyticsReport should add a daily report job to the queue', async () => {
|
|
// The mock should return the jobId passed to it to simulate bullmq's behavior
|
|
vi.mocked(analyticsQueue.add).mockImplementation(async (name, data, opts) => ({ id: opts?.jobId }) as any);
|
|
const jobId = await service.triggerAnalyticsReport();
|
|
|
|
expect(jobId).toContain('manual-report-');
|
|
expect(analyticsQueue.add).toHaveBeenCalledWith(
|
|
'generate-daily-report',
|
|
{ reportDate: '2024-10-18' },
|
|
{ jobId: expect.stringContaining('manual-report-') },
|
|
);
|
|
});
|
|
|
|
it('triggerWeeklyAnalyticsReport should add a weekly report job to the queue', async () => {
|
|
// The mock should return the jobId passed to it
|
|
vi.mocked(weeklyAnalyticsQueue.add).mockImplementation(async (name, data, opts) => ({ id: opts?.jobId }) as any);
|
|
const jobId = await service.triggerWeeklyAnalyticsReport();
|
|
|
|
expect(jobId).toContain('manual-weekly-report-');
|
|
expect(weeklyAnalyticsQueue.add).toHaveBeenCalledWith(
|
|
'generate-weekly-report',
|
|
{
|
|
reportYear: 2024, // From mocked dateUtils
|
|
reportWeek: 42, // From mocked dateUtils
|
|
},
|
|
{ jobId: expect.stringContaining('manual-weekly-report-') },
|
|
);
|
|
});
|
|
});
|
|
|
|
it('should do nothing if no deals are found for any user', async () => {
|
|
mockPersonalizationRepo.getBestSalePricesForAllUsers.mockResolvedValue([]);
|
|
await service.runDailyDealCheck();
|
|
expect(mockServiceLogger.info).toHaveBeenCalledWith(
|
|
'[BackgroundJob] Starting daily deal check for all users...',
|
|
);
|
|
expect(mockServiceLogger.info).toHaveBeenCalledWith(
|
|
'[BackgroundJob] No deals found for any watched items. Skipping.',
|
|
);
|
|
expect(mockPersonalizationRepo.getBestSalePricesForAllUsers).toHaveBeenCalledTimes(1);
|
|
expect(mockEmailQueue.add).not.toHaveBeenCalled();
|
|
expect(mockNotificationRepo.createBulkNotifications).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should create notifications and enqueue emails when deals are found', async () => {
|
|
mockPersonalizationRepo.getBestSalePricesForAllUsers.mockResolvedValue(mockDealsForAllUsers);
|
|
|
|
await service.runDailyDealCheck();
|
|
|
|
// Check that it fetched all deals once
|
|
expect(mockPersonalizationRepo.getBestSalePricesForAllUsers).toHaveBeenCalledTimes(1);
|
|
|
|
// Check that email jobs were enqueued for both users
|
|
expect(mockEmailQueue.add).toHaveBeenCalledTimes(2);
|
|
const firstEmailJob = mockEmailQueue.add.mock.calls[0];
|
|
expect(firstEmailJob[1].to).toBe('user1@test.com');
|
|
const secondEmailJob = mockEmailQueue.add.mock.calls[1];
|
|
expect(secondEmailJob[1].to).toBe('user2@test.com');
|
|
// Check the content for the user with multiple deals
|
|
expect(secondEmailJob[1].html).toContain('Milk');
|
|
expect(secondEmailJob[1].html).toContain('Bread');
|
|
|
|
// Check that in-app notifications were created for both users
|
|
expect(mockNotificationRepo.createBulkNotifications).toHaveBeenCalledTimes(1);
|
|
const notificationPayload = mockNotificationRepo.createBulkNotifications.mock.calls[0][0];
|
|
|
|
// Sort by user_id to ensure a consistent order for a direct `toEqual` comparison.
|
|
// This provides a clearer diff on failure than `expect.arrayContaining`.
|
|
const sortedPayload = [...notificationPayload].sort((a, b) =>
|
|
a.user_id.localeCompare(b.user_id),
|
|
);
|
|
|
|
expect(sortedPayload).toEqual([
|
|
{
|
|
user_id: 'user-1',
|
|
content: 'You have 1 new deal(s) on your watched items!',
|
|
link_url: '/dashboard/deals',
|
|
updated_at: expect.any(String),
|
|
},
|
|
{
|
|
user_id: 'user-2',
|
|
content: 'You have 2 new deal(s) on your watched items!',
|
|
link_url: '/dashboard/deals',
|
|
updated_at: expect.any(String),
|
|
},
|
|
]);
|
|
});
|
|
|
|
it('should handle and log errors for individual users without stopping the process', async () => {
|
|
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'))
|
|
.mockResolvedValueOnce({ id: 'job-2' });
|
|
|
|
await service.runDailyDealCheck();
|
|
|
|
// Check that it logged the error for user 1
|
|
expect(mockServiceLogger.error).toHaveBeenCalledWith(
|
|
{ err: expect.any(Error) },
|
|
expect.stringContaining('Failed to process deals for user user-1'),
|
|
);
|
|
|
|
// Check that it still processed user 2 successfully
|
|
// The email queue add should be attempted for both users.
|
|
expect(mockEmailQueue.add).toHaveBeenCalledTimes(2);
|
|
expect(mockNotificationRepo.createBulkNotifications).toHaveBeenCalledTimes(1);
|
|
const notificationPayload = mockNotificationRepo.createBulkNotifications.mock.calls[0][0];
|
|
expect(notificationPayload).toHaveLength(1); // Only one notification created
|
|
expect(notificationPayload[0]).toEqual({
|
|
user_id: 'user-2',
|
|
content: 'You have 2 new deal(s) on your watched items!',
|
|
link_url: '/dashboard/deals',
|
|
updated_at: expect.any(String),
|
|
});
|
|
});
|
|
|
|
it('should log a critical error if getBestSalePricesForAllUsers fails', async () => {
|
|
mockPersonalizationRepo.getBestSalePricesForAllUsers.mockRejectedValue(
|
|
new Error('Critical DB Failure'),
|
|
);
|
|
await expect(service.runDailyDealCheck()).rejects.toThrow('Critical DB Failure'); // This was a duplicate, fixed.
|
|
expect(mockServiceLogger.error).toHaveBeenCalledWith(
|
|
{ err: expect.any(Error) },
|
|
'[BackgroundJob] A critical error occurred during the daily deal check',
|
|
);
|
|
});
|
|
|
|
it('should log a critical error if createBulkNotifications fails', async () => {
|
|
// Arrange
|
|
mockPersonalizationRepo.getBestSalePricesForAllUsers.mockResolvedValue(mockDealsForAllUsers);
|
|
const dbError = new Error('Bulk insert failed');
|
|
mockNotificationRepo.createBulkNotifications.mockRejectedValue(dbError);
|
|
|
|
// Act & Assert
|
|
await expect(service.runDailyDealCheck()).rejects.toThrow(dbError);
|
|
expect(mockServiceLogger.error).toHaveBeenCalledWith(
|
|
{ err: dbError },
|
|
'[BackgroundJob] A critical error occurred during the daily deal check',
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('startBackgroundJobs', () => {
|
|
const mockBackgroundJobService = {
|
|
runDailyDealCheck: vi.fn(),
|
|
} as unknown as Mocked<BackgroundJobService>;
|
|
|
|
const mockAnalyticsQueue = {
|
|
add: vi.fn(),
|
|
} as unknown as Mocked<Queue>;
|
|
const mockWeeklyAnalyticsQueue = {
|
|
add: vi.fn(),
|
|
} as unknown as Mocked<Queue>;
|
|
const mockTokenCleanupQueue = {
|
|
add: vi.fn(),
|
|
} as unknown as Mocked<Queue>;
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks(); // Clear global mock logger calls too
|
|
mockCronSchedule.mockClear();
|
|
vi.mocked(mockBackgroundJobService.runDailyDealCheck).mockClear();
|
|
vi.mocked(mockAnalyticsQueue.add).mockClear();
|
|
vi.mocked(mockTokenCleanupQueue.add).mockClear();
|
|
vi.mocked(mockWeeklyAnalyticsQueue.add).mockClear();
|
|
});
|
|
|
|
it('should schedule four cron jobs with the correct schedules', () => {
|
|
startBackgroundJobs(
|
|
mockBackgroundJobService,
|
|
mockAnalyticsQueue,
|
|
mockWeeklyAnalyticsQueue,
|
|
mockTokenCleanupQueue,
|
|
globalMockLogger,
|
|
);
|
|
|
|
expect(mockCronSchedule).toHaveBeenCalledTimes(4);
|
|
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));
|
|
expect(mockCronSchedule).toHaveBeenCalledWith('0 5 * * *', expect.any(Function));
|
|
});
|
|
|
|
it('should call runDailyDealCheck when the first cron job function is executed', async () => {
|
|
startBackgroundJobs(
|
|
mockBackgroundJobService,
|
|
mockAnalyticsQueue,
|
|
mockWeeklyAnalyticsQueue,
|
|
mockTokenCleanupQueue,
|
|
globalMockLogger,
|
|
);
|
|
|
|
// Get the callback function for the first cron job
|
|
const dailyDealCheckCallback = mockCronSchedule.mock.calls[0][1];
|
|
await dailyDealCheckCallback();
|
|
|
|
expect(mockBackgroundJobService.runDailyDealCheck).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
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,
|
|
mockWeeklyAnalyticsQueue,
|
|
mockTokenCleanupQueue,
|
|
globalMockLogger,
|
|
);
|
|
|
|
const dailyDealCheckCallback = mockCronSchedule.mock.calls[0][1];
|
|
await dailyDealCheckCallback();
|
|
|
|
expect(mockBackgroundJobService.runDailyDealCheck).toHaveBeenCalledTimes(1);
|
|
expect(globalMockLogger.error).toHaveBeenCalledWith(
|
|
{ err: jobError },
|
|
'[BackgroundJob] Cron job for daily deal check failed unexpectedly.',
|
|
);
|
|
// It should run again, proving the lock was released in the finally block
|
|
await dailyDealCheckCallback();
|
|
expect(mockBackgroundJobService.runDailyDealCheck).toHaveBeenCalledTimes(2);
|
|
});
|
|
|
|
it('should handle errors in the daily deal check cron wrapper', async () => {
|
|
// Arrange: Mock the service to throw a non-Error object
|
|
const jobError = 'a string error';
|
|
vi.mocked(mockBackgroundJobService.runDailyDealCheck).mockRejectedValue(jobError);
|
|
startBackgroundJobs(
|
|
mockBackgroundJobService,
|
|
mockAnalyticsQueue,
|
|
mockWeeklyAnalyticsQueue,
|
|
mockTokenCleanupQueue,
|
|
globalMockLogger,
|
|
);
|
|
|
|
// Act
|
|
const dailyDealCheckCallback = mockCronSchedule.mock.calls[0][1];
|
|
await dailyDealCheckCallback();
|
|
|
|
// Assert: Verify it hits the internal catch block
|
|
expect(globalMockLogger.error).toHaveBeenCalledWith(
|
|
{ err: jobError },
|
|
'[BackgroundJob] Cron job for daily deal check failed unexpectedly.',
|
|
);
|
|
});
|
|
|
|
it('should prevent runDailyDealCheck from running if it is already in progress', async () => {
|
|
// Use fake timers to control promise resolution
|
|
vi.useFakeTimers();
|
|
|
|
// Create a controllable promise
|
|
let resolveRun!: () => void;
|
|
const runPromise = new Promise<void>((resolve) => {
|
|
resolveRun = resolve;
|
|
});
|
|
|
|
// Make the first call hang indefinitely
|
|
vi.mocked(mockBackgroundJobService.runDailyDealCheck).mockReturnValue(runPromise);
|
|
|
|
startBackgroundJobs(
|
|
mockBackgroundJobService,
|
|
mockAnalyticsQueue,
|
|
mockWeeklyAnalyticsQueue,
|
|
mockTokenCleanupQueue,
|
|
globalMockLogger,
|
|
);
|
|
const dailyDealCheckCallback = mockCronSchedule.mock.calls[0][1];
|
|
|
|
// Trigger the job once, it will hang
|
|
const firstCall = dailyDealCheckCallback();
|
|
// Trigger it a second time immediately
|
|
const secondCall = dailyDealCheckCallback();
|
|
|
|
// Resolve the first call so the test can finish
|
|
resolveRun();
|
|
|
|
await Promise.all([firstCall, secondCall]);
|
|
|
|
// The service method should only have been called once
|
|
expect(mockBackgroundJobService.runDailyDealCheck).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it('should handle unhandled rejections in the daily deal check cron wrapper', async () => {
|
|
// Use fake timers to control promise resolution
|
|
vi.useFakeTimers();
|
|
|
|
// Create a controllable promise
|
|
let resolveRun!: () => void;
|
|
const runPromise = new Promise<void>((resolve) => {
|
|
resolveRun = resolve;
|
|
});
|
|
|
|
// Make the first call hang indefinitely to keep the lock active
|
|
vi.mocked(mockBackgroundJobService.runDailyDealCheck).mockReturnValue(runPromise);
|
|
|
|
// Make logger.warn throw an error. This is outside the main try/catch in the cron job.
|
|
const warnError = new Error('Logger warn failed');
|
|
vi.mocked(globalMockLogger.warn).mockImplementationOnce(() => {
|
|
throw warnError;
|
|
});
|
|
|
|
startBackgroundJobs(
|
|
mockBackgroundJobService,
|
|
mockAnalyticsQueue,
|
|
mockWeeklyAnalyticsQueue,
|
|
mockTokenCleanupQueue,
|
|
globalMockLogger,
|
|
);
|
|
const dailyDealCheckCallback = mockCronSchedule.mock.calls[0][1];
|
|
|
|
// Trigger the job once, it will hang and set the lock. Then trigger it a second time
|
|
// to enter the `if (isDailyDealCheckRunning)` block and call the throwing logger.warn.
|
|
const firstCall = dailyDealCheckCallback();
|
|
const secondCall = dailyDealCheckCallback();
|
|
|
|
// Resolve the first call so the test can finish
|
|
resolveRun();
|
|
|
|
await Promise.allSettled([firstCall, secondCall]);
|
|
|
|
// The outer catch block should have been called with the error from logger.warn
|
|
expect(globalMockLogger.error).toHaveBeenCalledWith(
|
|
{ err: warnError },
|
|
'[BackgroundJob] Unhandled rejection in daily deal check cron wrapper.',
|
|
);
|
|
});
|
|
|
|
it('should enqueue an analytics job when the second cron job function is executed', async () => {
|
|
startBackgroundJobs(
|
|
mockBackgroundJobService,
|
|
mockAnalyticsQueue,
|
|
mockWeeklyAnalyticsQueue,
|
|
mockTokenCleanupQueue,
|
|
globalMockLogger,
|
|
);
|
|
|
|
const analyticsJobCallback = mockCronSchedule.mock.calls[1][1];
|
|
await analyticsJobCallback();
|
|
|
|
expect(mockAnalyticsQueue.add).toHaveBeenCalledWith(
|
|
'generate-daily-report',
|
|
expect.any(Object),
|
|
expect.any(Object),
|
|
);
|
|
});
|
|
|
|
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,
|
|
mockWeeklyAnalyticsQueue,
|
|
mockTokenCleanupQueue,
|
|
globalMockLogger,
|
|
);
|
|
|
|
const analyticsJobCallback = mockCronSchedule.mock.calls[1][1];
|
|
await analyticsJobCallback();
|
|
|
|
expect(mockAnalyticsQueue.add).toHaveBeenCalledTimes(1);
|
|
expect(globalMockLogger.error).toHaveBeenCalledWith(
|
|
{ err: queueError },
|
|
'[BackgroundJob] Failed to enqueue daily analytics job.',
|
|
);
|
|
});
|
|
|
|
it('should handle errors in the analytics report cron wrapper', async () => {
|
|
// Arrange: Mock the queue to throw
|
|
const queueError = 'a string error';
|
|
vi.mocked(mockAnalyticsQueue.add).mockRejectedValue(queueError);
|
|
startBackgroundJobs(
|
|
mockBackgroundJobService,
|
|
mockAnalyticsQueue,
|
|
mockWeeklyAnalyticsQueue,
|
|
mockTokenCleanupQueue,
|
|
globalMockLogger,
|
|
);
|
|
|
|
// Act
|
|
const analyticsJobCallback = mockCronSchedule.mock.calls[1][1];
|
|
await analyticsJobCallback();
|
|
|
|
// Assert
|
|
expect(globalMockLogger.error).toHaveBeenCalledWith(
|
|
{ err: queueError },
|
|
'[BackgroundJob] Failed to enqueue daily analytics job.',
|
|
);
|
|
});
|
|
|
|
it('should handle unhandled rejections in the analytics report cron wrapper', async () => {
|
|
const infoError = new Error('Logger info failed');
|
|
startBackgroundJobs(
|
|
mockBackgroundJobService,
|
|
mockAnalyticsQueue,
|
|
mockWeeklyAnalyticsQueue,
|
|
mockTokenCleanupQueue,
|
|
globalMockLogger,
|
|
);
|
|
|
|
// Make logger.info throw, which is outside the try/catch in the cron job.
|
|
const infoSpy = vi.spyOn(globalMockLogger, 'info').mockImplementation(() => {
|
|
throw infoError;
|
|
});
|
|
|
|
const analyticsJobCallback = mockCronSchedule.mock.calls[1][1];
|
|
await analyticsJobCallback();
|
|
|
|
expect(globalMockLogger.error).toHaveBeenCalledWith(
|
|
{ err: infoError }, // The implementation uses `err` key here
|
|
'[BackgroundJob] Unhandled rejection in analytics report cron wrapper.',
|
|
);
|
|
infoSpy.mockRestore();
|
|
});
|
|
|
|
it('should enqueue a weekly analytics job when the third cron job function is executed', async () => {
|
|
startBackgroundJobs(
|
|
mockBackgroundJobService,
|
|
mockAnalyticsQueue,
|
|
mockWeeklyAnalyticsQueue,
|
|
mockTokenCleanupQueue,
|
|
globalMockLogger,
|
|
);
|
|
|
|
// The weekly job is the third one scheduled
|
|
const weeklyAnalyticsJobCallback = mockCronSchedule.mock.calls[2][1];
|
|
await weeklyAnalyticsJobCallback();
|
|
|
|
expect(mockWeeklyAnalyticsQueue.add).toHaveBeenCalledWith(
|
|
'generate-weekly-report',
|
|
{ reportYear: 2024, reportWeek: 42 }, // Values from the mocked dateUtils
|
|
{ jobId: 'weekly-report-2024-42' },
|
|
);
|
|
});
|
|
|
|
it('should log an error if enqueuing the weekly analytics job fails', async () => {
|
|
const queueError = new Error('Redis is down for weekly job');
|
|
vi.mocked(mockWeeklyAnalyticsQueue.add).mockRejectedValue(queueError);
|
|
startBackgroundJobs(
|
|
mockBackgroundJobService,
|
|
mockAnalyticsQueue,
|
|
mockWeeklyAnalyticsQueue,
|
|
mockTokenCleanupQueue,
|
|
globalMockLogger,
|
|
);
|
|
|
|
const weeklyAnalyticsJobCallback = mockCronSchedule.mock.calls[2][1];
|
|
await weeklyAnalyticsJobCallback();
|
|
|
|
expect(globalMockLogger.error).toHaveBeenCalledWith(
|
|
{ err: queueError },
|
|
'[BackgroundJob] Failed to enqueue weekly analytics job.',
|
|
);
|
|
});
|
|
|
|
it('should handle errors in the weekly analytics report cron wrapper', async () => {
|
|
const queueError = 'a string error';
|
|
vi.mocked(mockWeeklyAnalyticsQueue.add).mockRejectedValue(queueError);
|
|
startBackgroundJobs(
|
|
mockBackgroundJobService,
|
|
mockAnalyticsQueue,
|
|
mockWeeklyAnalyticsQueue,
|
|
mockTokenCleanupQueue,
|
|
globalMockLogger,
|
|
);
|
|
|
|
// Act
|
|
const weeklyAnalyticsJobCallback = mockCronSchedule.mock.calls[2][1];
|
|
await weeklyAnalyticsJobCallback();
|
|
|
|
// Assert
|
|
expect(globalMockLogger.error).toHaveBeenCalledWith(
|
|
{ err: queueError },
|
|
'[BackgroundJob] Failed to enqueue weekly analytics job.',
|
|
);
|
|
});
|
|
|
|
it('should handle unhandled rejections in the weekly analytics report cron wrapper', async () => {
|
|
const infoError = new Error('Logger info failed');
|
|
startBackgroundJobs(
|
|
mockBackgroundJobService,
|
|
mockAnalyticsQueue,
|
|
mockWeeklyAnalyticsQueue,
|
|
mockTokenCleanupQueue,
|
|
globalMockLogger,
|
|
);
|
|
|
|
const infoSpy = vi.spyOn(globalMockLogger, 'info').mockImplementation(() => {
|
|
throw infoError;
|
|
});
|
|
|
|
const weeklyAnalyticsJobCallback = mockCronSchedule.mock.calls[2][1];
|
|
await weeklyAnalyticsJobCallback();
|
|
|
|
expect(globalMockLogger.error).toHaveBeenCalledWith(
|
|
{ err: infoError },
|
|
'[BackgroundJob] Unhandled rejection in weekly analytics report cron wrapper.',
|
|
);
|
|
infoSpy.mockRestore();
|
|
});
|
|
|
|
it('should enqueue a token cleanup job when the fourth cron job function is executed', async () => {
|
|
startBackgroundJobs(
|
|
mockBackgroundJobService,
|
|
mockAnalyticsQueue,
|
|
mockWeeklyAnalyticsQueue,
|
|
mockTokenCleanupQueue,
|
|
globalMockLogger,
|
|
);
|
|
|
|
const tokenCleanupCallback = mockCronSchedule.mock.calls[3][1];
|
|
await tokenCleanupCallback();
|
|
|
|
expect(mockTokenCleanupQueue.add).toHaveBeenCalledWith(
|
|
'cleanup-tokens',
|
|
expect.any(Object),
|
|
expect.any(Object),
|
|
);
|
|
});
|
|
|
|
it('should log an error if enqueuing the token cleanup job fails', async () => {
|
|
const queueError = new Error('Redis is down for token cleanup');
|
|
vi.mocked(mockTokenCleanupQueue.add).mockRejectedValue(queueError);
|
|
startBackgroundJobs(
|
|
mockBackgroundJobService,
|
|
mockAnalyticsQueue,
|
|
mockWeeklyAnalyticsQueue,
|
|
mockTokenCleanupQueue,
|
|
globalMockLogger,
|
|
);
|
|
const tokenCleanupCallback = mockCronSchedule.mock.calls[3][1];
|
|
await tokenCleanupCallback();
|
|
expect(globalMockLogger.error).toHaveBeenCalledWith(
|
|
{ err: queueError },
|
|
'[BackgroundJob] Failed to enqueue token cleanup job.',
|
|
);
|
|
});
|
|
|
|
it('should handle errors in the token cleanup cron wrapper', async () => {
|
|
const queueError = 'a string error';
|
|
vi.mocked(mockTokenCleanupQueue.add).mockRejectedValue(queueError);
|
|
startBackgroundJobs(
|
|
mockBackgroundJobService,
|
|
mockAnalyticsQueue,
|
|
mockWeeklyAnalyticsQueue,
|
|
mockTokenCleanupQueue,
|
|
globalMockLogger,
|
|
);
|
|
|
|
// Act
|
|
const tokenCleanupCallback = mockCronSchedule.mock.calls[3][1];
|
|
await tokenCleanupCallback();
|
|
|
|
// Assert
|
|
expect(globalMockLogger.error).toHaveBeenCalledWith(
|
|
{ err: queueError },
|
|
'[BackgroundJob] Failed to enqueue token cleanup job.',
|
|
);
|
|
});
|
|
|
|
it('should handle unhandled rejections in the token cleanup cron wrapper', async () => {
|
|
const infoError = new Error('Logger info failed');
|
|
startBackgroundJobs(
|
|
mockBackgroundJobService,
|
|
mockAnalyticsQueue,
|
|
mockWeeklyAnalyticsQueue,
|
|
mockTokenCleanupQueue,
|
|
globalMockLogger,
|
|
);
|
|
|
|
const infoSpy = vi.spyOn(globalMockLogger, 'info').mockImplementation(() => {
|
|
throw infoError;
|
|
});
|
|
|
|
const tokenCleanupCallback = mockCronSchedule.mock.calls[3][1];
|
|
await tokenCleanupCallback();
|
|
|
|
expect(globalMockLogger.error).toHaveBeenCalledWith(
|
|
{ err: infoError },
|
|
'[BackgroundJob] Unhandled rejection in token cleanup cron wrapper.',
|
|
);
|
|
infoSpy.mockRestore();
|
|
});
|
|
|
|
it('should log a critical error if scheduling fails', () => {
|
|
mockCronSchedule.mockImplementation(() => {
|
|
throw new Error('Scheduling failed');
|
|
});
|
|
|
|
startBackgroundJobs(
|
|
mockBackgroundJobService,
|
|
mockAnalyticsQueue,
|
|
mockWeeklyAnalyticsQueue,
|
|
mockTokenCleanupQueue,
|
|
globalMockLogger,
|
|
);
|
|
|
|
expect(globalMockLogger.error).toHaveBeenCalledWith(
|
|
{ err: expect.any(Error) },
|
|
'[BackgroundJob] Failed to schedule a cron job. This is a critical setup error.',
|
|
);
|
|
});
|
|
});
|
|
});
|