// src/services/monitoringService.server.test.ts import { describe, it, expect, vi, beforeEach } from 'vitest'; import type { Job } from 'bullmq'; import { NotFoundError, ValidationError } from './db/errors.db'; import { logger } from './logger.server'; // --- Hoisted Mocks --- const mocks = vi.hoisted(() => { const createMockWorker = (name: string) => ({ name, isRunning: vi.fn().mockReturnValue(true), }); const createMockQueue = (name: string) => ({ name, getJobCounts: vi.fn().mockResolvedValue({ waiting: 0, active: 0, completed: 0, failed: 0, delayed: 0, paused: 0, }), getJob: vi.fn(), }); return { flyerWorker: createMockWorker('flyer-processing'), emailWorker: createMockWorker('email-sending'), analyticsWorker: createMockWorker('analytics-reporting'), cleanupWorker: createMockWorker('file-cleanup'), weeklyAnalyticsWorker: createMockWorker('weekly-analytics-reporting'), tokenCleanupWorker: createMockWorker('token-cleanup'), flyerQueue: createMockQueue('flyer-processing'), emailQueue: createMockQueue('email-sending'), analyticsQueue: createMockQueue('analytics-reporting'), cleanupQueue: createMockQueue('file-cleanup'), weeklyAnalyticsQueue: createMockQueue('weekly-analytics-reporting'), tokenCleanupQueue: createMockQueue('token-cleanup'), }; }); // --- Mock Modules --- vi.mock('./queues.server', () => ({ flyerQueue: mocks.flyerQueue, emailQueue: mocks.emailQueue, analyticsQueue: mocks.analyticsQueue, cleanupQueue: mocks.cleanupQueue, weeklyAnalyticsQueue: mocks.weeklyAnalyticsQueue, tokenCleanupQueue: mocks.tokenCleanupQueue, })); vi.mock('./workers.server', () => ({ flyerWorker: mocks.flyerWorker, emailWorker: mocks.emailWorker, analyticsWorker: mocks.analyticsWorker, cleanupWorker: mocks.cleanupWorker, weeklyAnalyticsWorker: mocks.weeklyAnalyticsWorker, tokenCleanupWorker: mocks.tokenCleanupWorker, flyerProcessingService: {}, })); vi.mock('./db/errors.db', () => ({ NotFoundError: class NotFoundError extends Error { constructor(message: string) { super(message); this.name = 'NotFoundError'; } }, ValidationError: class ValidationError extends Error { constructor(issues: [], message: string) { super(message); this.name = 'ValidationError'; } }, })); vi.mock('./logger.server', () => ({ logger: { info: vi.fn(), error: vi.fn(), warn: vi.fn(), debug: vi.fn(), }, })); // Import the service to be tested AFTER all mocks are set up. import { monitoringService } from './monitoringService.server'; describe('MonitoringService', () => { beforeEach(() => { vi.clearAllMocks(); }); describe('getWorkerStatuses', () => { it('should return the running status of all workers', async () => { // Arrange: one worker is not running mocks.emailWorker.isRunning.mockReturnValue(false); // Act const statuses = await monitoringService.getWorkerStatuses(); // Assert expect(statuses).toEqual([ { name: 'flyer-processing', isRunning: true }, { name: 'email-sending', isRunning: false }, { name: 'analytics-reporting', isRunning: true }, { name: 'file-cleanup', isRunning: true }, { name: 'weekly-analytics-reporting', isRunning: true }, { name: 'token-cleanup', isRunning: true }, ]); expect(mocks.flyerWorker.isRunning).toHaveBeenCalledTimes(1); expect(mocks.emailWorker.isRunning).toHaveBeenCalledTimes(1); }); }); describe('getQueueStatuses', () => { it('should return job counts for all queues', async () => { const defaultCounts = { waiting: 0, active: 0, completed: 0, failed: 0, delayed: 0, paused: 0, }; // Arrange - override specific queue counts mocks.flyerQueue.getJobCounts.mockResolvedValue({ ...defaultCounts, active: 1, failed: 2 }); mocks.emailQueue.getJobCounts.mockResolvedValue({ ...defaultCounts, completed: 10, waiting: 5, }); // Act const statuses = await monitoringService.getQueueStatuses(); // Assert expect(statuses).toEqual( expect.arrayContaining([ { name: 'flyer-processing', counts: { ...defaultCounts, active: 1, failed: 2 } }, { name: 'email-sending', counts: { ...defaultCounts, completed: 10, waiting: 5 } }, { name: 'analytics-reporting', counts: defaultCounts }, { name: 'file-cleanup', counts: defaultCounts }, { name: 'weekly-analytics-reporting', counts: defaultCounts }, { name: 'token-cleanup', counts: defaultCounts }, ]), ); expect(mocks.flyerQueue.getJobCounts).toHaveBeenCalledTimes(1); expect(mocks.emailQueue.getJobCounts).toHaveBeenCalledTimes(1); }); }); describe('retryFailedJob', () => { const userId = 'admin-user'; const jobId = 'failed-job-1'; it('should throw NotFoundError for an unknown queue name', async () => { await expect( monitoringService.retryFailedJob('unknown-queue', jobId, userId), ).rejects.toThrow(new NotFoundError(`Queue 'unknown-queue' not found.`)); }); it('should throw NotFoundError if the job does not exist in the queue', async () => { mocks.flyerQueue.getJob.mockResolvedValue(null); await expect( monitoringService.retryFailedJob('flyer-processing', jobId, userId), ).rejects.toThrow( new NotFoundError(`Job with ID '${jobId}' not found in queue 'flyer-processing'.`), ); }); it("should throw ValidationError if the job is not in a 'failed' state", async () => { const mockJob = { id: jobId, getState: vi.fn().mockResolvedValue('completed'), retry: vi.fn(), } as unknown as Job; mocks.flyerQueue.getJob.mockResolvedValue(mockJob); await expect( monitoringService.retryFailedJob('flyer-processing', jobId, userId), ).rejects.toThrow( new ValidationError([], `Job is not in a 'failed' state. Current state: completed.`), ); }); it("should call job.retry() and log if the job is in a 'failed' state", async () => { const mockJob = { id: jobId, getState: vi.fn().mockResolvedValue('failed'), retry: vi.fn().mockResolvedValue(undefined), } as unknown as Job; mocks.flyerQueue.getJob.mockResolvedValue(mockJob); await monitoringService.retryFailedJob('flyer-processing', jobId, userId); expect(mockJob.retry).toHaveBeenCalledTimes(1); expect(logger.info).toHaveBeenCalledWith( `[Admin] User ${userId} manually retried job ${jobId} in queue flyer-processing.`, ); }); }); describe('getFlyerJobStatus', () => { const jobId = 'flyer-job-123'; it('should throw NotFoundError if the job is not found', async () => { mocks.flyerQueue.getJob.mockResolvedValue(null); await expect(monitoringService.getFlyerJobStatus(jobId)).rejects.toThrow( new NotFoundError('Job not found.'), ); }); it('should return the job status object if the job is found', async () => { const mockJob = { id: jobId, getState: vi.fn().mockResolvedValue('completed'), progress: 100, returnvalue: { flyerId: 99 }, failedReason: null, } as unknown as Job; mocks.flyerQueue.getJob.mockResolvedValue(mockJob); const status = await monitoringService.getFlyerJobStatus(jobId); expect(status).toEqual({ id: jobId, state: 'completed', progress: 100, returnValue: { flyerId: 99 }, failedReason: null, }); }); }); });