259 lines
9.0 KiB
TypeScript
259 lines
9.0 KiB
TypeScript
// src/routes/admin.monitoring.routes.test.ts
|
|
import { describe, it, expect, vi, beforeEach, type Mocked } from 'vitest';
|
|
import supertest from 'supertest';
|
|
import type { Request, Response, NextFunction } from 'express';
|
|
import { createMockUserProfile, createMockActivityLogItem } from '../tests/utils/mockFactories';
|
|
import type { UserProfile } from '../types';
|
|
import { createTestApp } from '../tests/utils/createTestApp';
|
|
|
|
const { mockLogger } = vi.hoisted(() => ({
|
|
mockLogger: {
|
|
info: vi.fn(),
|
|
warn: vi.fn(),
|
|
error: vi.fn(),
|
|
debug: vi.fn(),
|
|
child: vi.fn().mockReturnThis(),
|
|
},
|
|
}));
|
|
|
|
vi.mock('../lib/queue', () => ({
|
|
serverAdapter: {
|
|
getRouter: () => (req: Request, res: Response, next: NextFunction) => next(), // Return a dummy express handler
|
|
},
|
|
// Mock other exports if needed
|
|
emailQueue: {},
|
|
cleanupQueue: {},
|
|
}));
|
|
|
|
vi.mock('../services/db/index.db', () => ({
|
|
adminRepo: {
|
|
getActivityLog: vi.fn(),
|
|
},
|
|
flyerRepo: {},
|
|
recipeRepo: {},
|
|
userRepo: {},
|
|
personalizationRepo: {},
|
|
notificationRepo: {},
|
|
}));
|
|
|
|
// Mock the queue service for queue status checks
|
|
vi.mock('../services/queueService.server', () => ({
|
|
flyerQueue: { name: 'flyer-processing', getJobCounts: vi.fn() },
|
|
emailQueue: { name: 'email-sending', getJobCounts: vi.fn() },
|
|
analyticsQueue: { name: 'analytics-reporting', getJobCounts: vi.fn() },
|
|
cleanupQueue: { name: 'file-cleanup', getJobCounts: vi.fn() },
|
|
weeklyAnalyticsQueue: { name: 'weekly-analytics-reporting', getJobCounts: vi.fn() },
|
|
}));
|
|
|
|
// Mock the worker service for worker status checks
|
|
vi.mock('../services/workers.server', () => ({
|
|
flyerWorker: { name: 'flyer-processing', isRunning: vi.fn() },
|
|
emailWorker: { name: 'email-sending', isRunning: vi.fn() },
|
|
analyticsWorker: { name: 'analytics-reporting', isRunning: vi.fn() },
|
|
cleanupWorker: { name: 'file-cleanup', isRunning: vi.fn() },
|
|
weeklyAnalyticsWorker: { name: 'weekly-analytics-reporting', isRunning: vi.fn() },
|
|
}));
|
|
|
|
// Mock other dependencies that are part of the adminRouter setup but not directly tested here
|
|
vi.mock('../services/db/flyer.db');
|
|
vi.mock('../services/db/recipe.db');
|
|
vi.mock('../services/db/user.db');
|
|
vi.mock('node:fs/promises');
|
|
vi.mock('../services/backgroundJobService');
|
|
vi.mock('../services/geocodingService.server');
|
|
vi.mock('@bull-board/api');
|
|
vi.mock('@bull-board/api/bullMQAdapter');
|
|
|
|
// Fix: Mock ExpressAdapter as a class to allow `new ExpressAdapter()` to work.
|
|
vi.mock('@bull-board/express', () => ({
|
|
ExpressAdapter: class {
|
|
setBasePath = vi.fn();
|
|
getRouter = vi
|
|
.fn()
|
|
.mockReturnValue((req: Request, res: Response, next: NextFunction) => next());
|
|
},
|
|
}));
|
|
|
|
// Import the router AFTER all mocks are defined.
|
|
import adminRouter from './admin.routes';
|
|
|
|
// Import the mocked modules to control them
|
|
import * as queueService from '../services/queueService.server';
|
|
import * as workerService from '../services/workers.server';
|
|
import { adminRepo } from '../services/db/index.db';
|
|
const mockedQueueService = queueService as Mocked<typeof queueService>;
|
|
const mockedWorkerService = workerService as Mocked<typeof workerService>;
|
|
|
|
// Mock the logger
|
|
vi.mock('../services/logger.server', () => ({
|
|
logger: mockLogger,
|
|
}));
|
|
|
|
// Mock the passport middleware
|
|
vi.mock('./passport.routes', () => ({
|
|
default: {
|
|
authenticate: vi.fn(() => (req: Request, res: Response, next: NextFunction) => {
|
|
if (!req.user) return res.status(401).json({ message: 'Unauthorized' });
|
|
next();
|
|
}),
|
|
},
|
|
isAdmin: (req: Request, res: Response, next: NextFunction) => {
|
|
const user = req.user as UserProfile | undefined;
|
|
if (user && user.role === 'admin') next();
|
|
else res.status(403).json({ message: 'Forbidden: Administrator access required.' });
|
|
},
|
|
}));
|
|
|
|
describe('Admin Monitoring Routes (/api/admin)', () => {
|
|
const adminUser = createMockUserProfile({
|
|
role: 'admin',
|
|
user: { user_id: 'admin-user-id', email: 'admin@test.com' },
|
|
});
|
|
// Create a single app instance with an admin user for all tests in this suite.
|
|
const app = createTestApp({
|
|
router: adminRouter,
|
|
basePath: '/api/admin',
|
|
authenticatedUser: adminUser,
|
|
});
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
});
|
|
|
|
describe('GET /activity-log', () => {
|
|
it('should return a list of activity logs with default pagination', async () => {
|
|
const mockLogs = [createMockActivityLogItem({ action: 'flyer_processed' })];
|
|
vi.mocked(adminRepo.getActivityLog).mockResolvedValue(mockLogs);
|
|
|
|
const response = await supertest(app).get('/api/admin/activity-log');
|
|
|
|
expect(response.status).toBe(200);
|
|
expect(response.body).toEqual(mockLogs);
|
|
expect(adminRepo.getActivityLog).toHaveBeenCalledWith(50, 0, expect.anything());
|
|
});
|
|
|
|
it('should use limit and offset query parameters when provided', async () => {
|
|
vi.mocked(adminRepo.getActivityLog).mockResolvedValue([]);
|
|
|
|
await supertest(app).get('/api/admin/activity-log?limit=10&offset=20');
|
|
|
|
expect(adminRepo.getActivityLog).toHaveBeenCalledWith(10, 20, expect.anything());
|
|
});
|
|
|
|
it('should return 400 for invalid limit and offset query parameters', async () => {
|
|
const response = await supertest(app).get('/api/admin/activity-log?limit=abc&offset=-1');
|
|
expect(response.status).toBe(400);
|
|
expect(response.body.errors).toBeDefined();
|
|
expect(response.body.errors.length).toBe(2); // Both limit and offset are invalid
|
|
});
|
|
});
|
|
|
|
describe('GET /workers/status', () => {
|
|
it('should return the status of all registered workers', async () => {
|
|
// Arrange: Set the mock status for each worker
|
|
vi.mocked(mockedWorkerService.flyerWorker.isRunning).mockReturnValue(true);
|
|
vi.mocked(mockedWorkerService.emailWorker.isRunning).mockReturnValue(true);
|
|
vi.mocked(mockedWorkerService.analyticsWorker.isRunning).mockReturnValue(false); // Simulate one worker being stopped
|
|
vi.mocked(mockedWorkerService.cleanupWorker.isRunning).mockReturnValue(true);
|
|
vi.mocked(mockedWorkerService.weeklyAnalyticsWorker.isRunning).mockReturnValue(true);
|
|
|
|
// Act
|
|
const response = await supertest(app).get('/api/admin/workers/status');
|
|
|
|
// Assert
|
|
expect(response.status).toBe(200);
|
|
expect(response.body).toEqual([
|
|
{ name: 'flyer-processing', isRunning: true },
|
|
{ name: 'email-sending', isRunning: true },
|
|
{ name: 'analytics-reporting', isRunning: false },
|
|
{ name: 'file-cleanup', isRunning: true },
|
|
{ name: 'weekly-analytics-reporting', isRunning: true },
|
|
]);
|
|
});
|
|
});
|
|
|
|
describe('GET /queues/status', () => {
|
|
it('should return job counts for all registered queues', async () => {
|
|
// Arrange: Set the mock job counts for each queue
|
|
vi.mocked(mockedQueueService.flyerQueue.getJobCounts).mockResolvedValue({
|
|
waiting: 5,
|
|
active: 1,
|
|
completed: 100,
|
|
failed: 2,
|
|
delayed: 0,
|
|
paused: 0,
|
|
});
|
|
vi.mocked(mockedQueueService.emailQueue.getJobCounts).mockResolvedValue({
|
|
waiting: 0,
|
|
active: 0,
|
|
completed: 50,
|
|
failed: 0,
|
|
delayed: 0,
|
|
paused: 0,
|
|
});
|
|
vi.mocked(mockedQueueService.analyticsQueue.getJobCounts).mockResolvedValue({
|
|
waiting: 0,
|
|
active: 1,
|
|
completed: 10,
|
|
failed: 1,
|
|
delayed: 0,
|
|
paused: 0,
|
|
});
|
|
vi.mocked(mockedQueueService.cleanupQueue.getJobCounts).mockResolvedValue({
|
|
waiting: 2,
|
|
active: 0,
|
|
completed: 25,
|
|
failed: 0,
|
|
delayed: 0,
|
|
paused: 0,
|
|
});
|
|
vi.mocked(mockedQueueService.weeklyAnalyticsQueue.getJobCounts).mockResolvedValue({
|
|
waiting: 1,
|
|
active: 0,
|
|
completed: 5,
|
|
failed: 0,
|
|
delayed: 0,
|
|
paused: 0,
|
|
});
|
|
|
|
// Act
|
|
const response = await supertest(app).get('/api/admin/queues/status');
|
|
|
|
// Assert
|
|
expect(response.status).toBe(200);
|
|
expect(response.body).toEqual([
|
|
{
|
|
name: 'flyer-processing',
|
|
counts: { waiting: 5, active: 1, completed: 100, failed: 2, delayed: 0, paused: 0 },
|
|
},
|
|
{
|
|
name: 'email-sending',
|
|
counts: { waiting: 0, active: 0, completed: 50, failed: 0, delayed: 0, paused: 0 },
|
|
},
|
|
{
|
|
name: 'analytics-reporting',
|
|
counts: { waiting: 0, active: 1, completed: 10, failed: 1, delayed: 0, paused: 0 },
|
|
},
|
|
{
|
|
name: 'file-cleanup',
|
|
counts: { waiting: 2, active: 0, completed: 25, failed: 0, delayed: 0, paused: 0 },
|
|
},
|
|
{
|
|
name: 'weekly-analytics-reporting',
|
|
counts: { waiting: 1, active: 0, completed: 5, failed: 0, delayed: 0, paused: 0 },
|
|
},
|
|
]);
|
|
});
|
|
|
|
it('should return 500 if fetching queue counts fails', async () => {
|
|
vi.mocked(mockedQueueService.flyerQueue.getJobCounts).mockRejectedValue(
|
|
new Error('Redis is down'),
|
|
);
|
|
|
|
const response = await supertest(app).get('/api/admin/queues/status');
|
|
expect(response.status).toBe(500);
|
|
expect(response.body.message).toBe('Redis is down');
|
|
});
|
|
});
|
|
});
|