Some checks failed
Deploy to Test Environment / deploy-to-test (push) Failing after 1m17s
241 lines
7.6 KiB
TypeScript
241 lines
7.6 KiB
TypeScript
// 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,
|
|
});
|
|
});
|
|
});
|
|
});
|