Files
flyer-crawler.projectium.com/src/routes/health.routes.test.ts
Torben Sorensen 07a9787570
All checks were successful
Deploy to Test Environment / deploy-to-test (push) Successful in 12m5s
fix unit tests
2025-12-29 19:44:25 -08:00

367 lines
14 KiB
TypeScript

// src/routes/health.routes.test.ts
import { describe, it, expect, vi, beforeEach, afterEach, type Mocked } from 'vitest';
import supertest from 'supertest';
import { connection as redisConnection } from '../services/queueService.server';
import fs from 'node:fs/promises';
import { createTestApp } from '../tests/utils/createTestApp';
import { mockLogger } from '../tests/utils/mockLogger';
// 1. Mock the dependencies of the health router.
vi.mock('../services/db/connection.db', () => ({
checkTablesExist: vi.fn(),
getPoolStatus: vi.fn(),
}));
vi.mock('node:fs/promises', () => ({
default: {
access: vi.fn(),
constants: { W_OK: 1 },
},
}));
// In this case, it's the redisConnection from the queueService.
vi.mock('../services/queueService.server', () => ({
// We need to mock the `connection` export which is an object with a `ping` method.
connection: {
ping: vi.fn(),
},
}));
// Import the router and mocked modules AFTER all mocks are defined.
import healthRouter from './health.routes';
import * as dbConnection from '../services/db/connection.db';
// Mock the logger to keep test output clean.
vi.mock('../services/logger.server', async () => ({
// Use async import to avoid hoisting issues with mockLogger
logger: (await import('../tests/utils/mockLogger')).mockLogger,
}));
// Cast the mocked import to a Mocked type for type-safe access to mock functions.
const mockedRedisConnection = redisConnection as Mocked<typeof redisConnection>;
const mockedDbConnection = dbConnection as Mocked<typeof dbConnection>;
const mockedFs = fs as Mocked<typeof fs>;
const { logger } = await import('../services/logger.server');
// 2. Create a minimal Express app to host the router for testing.
const app = createTestApp({ router: healthRouter, basePath: '/api/health' });
describe('Health Routes (/api/health)', () => {
beforeEach(() => {
// Clear mock history before each test to ensure isolation.
vi.clearAllMocks();
});
afterEach(() => {
vi.useRealTimers(); // Restore real timers after each test
});
describe('GET /ping', () => {
it('should return 200 OK with "pong"', async () => {
// Act
const response = await supertest(app).get('/api/health/ping');
// Assert
expect(response.status).toBe(200);
expect(response.text).toBe('pong');
});
});
describe('GET /redis', () => {
it('should return 200 OK if Redis ping is successful', async () => {
// Arrange: Simulate a successful ping by having the mock resolve to 'PONG'.
mockedRedisConnection.ping.mockResolvedValue('PONG');
// Act: Make a request to the endpoint.
const response = await supertest(app).get('/api/health/redis');
// Assert: Check for the correct status and response body.
expect(response.status).toBe(200);
expect(response.body).toEqual({
success: true,
message: 'Redis connection is healthy.',
});
});
it('should return 500 if Redis ping fails', async () => {
// Arrange: Simulate a failure by having the mock reject with an error.
const redisError = new Error('Connection timed out');
mockedRedisConnection.ping.mockRejectedValue(redisError);
// Act
const response = await supertest(app).get('/api/health/redis');
// Assert
expect(response.status).toBe(500);
expect(response.body.message).toBe('Connection timed out');
});
it('should return 500 if Redis ping returns an unexpected response', async () => {
// Arrange: Simulate a successful ping but with an unexpected reply.
mockedRedisConnection.ping.mockResolvedValue('OK'); // Not 'PONG'
// Act
const response = await supertest(app).get('/api/health/redis');
// Assert
expect(response.status).toBe(500);
expect(response.body.message).toContain('Unexpected Redis ping response: OK');
});
});
describe('GET /time', () => {
it('should return the current server time, year, and week number', async () => {
// Arrange: Set a specific date to test against using fake timers.
const fakeDate = new Date('2024-03-15T10:30:00.000Z'); // This is a Friday, in the 11th week of 2024
vi.useFakeTimers();
vi.setSystemTime(fakeDate);
// Act
const response = await supertest(app).get('/api/health/time');
// Assert
expect(response.status).toBe(200);
expect(response.body.currentTime).toBe('2024-03-15T10:30:00.000Z');
expect(response.body.year).toBe(2024);
expect(response.body.week).toBe(11);
});
});
describe('GET /db-schema', () => {
it('should return 200 OK if all tables exist', async () => {
// Arrange: Mock the service function to return an empty array (no missing tables)
mockedDbConnection.checkTablesExist.mockResolvedValue([]);
// Act
const response = await supertest(app).get('/api/health/db-schema');
// Assert
expect(response.status).toBe(200);
expect(response.body.success).toBe(true);
expect(response.body.message).toBe('All required database tables exist.');
});
it('should return 500 if tables are missing', async () => {
// Arrange: Mock the service function to return missing table names
mockedDbConnection.checkTablesExist.mockResolvedValue(['missing_table_1', 'missing_table_2']);
const response = await supertest(app).get('/api/health/db-schema');
expect(response.status).toBe(500);
expect(response.body.message).toContain('Missing tables: missing_table_1, missing_table_2');
// The error is passed to next(), so the global error handler would log it, not the route handler itself.
});
it('should return 500 if the database check fails', async () => {
// Arrange: Mock the service function to reject with a generic error
const dbError = new Error('DB connection failed');
mockedDbConnection.checkTablesExist.mockRejectedValue(dbError);
const response = await supertest(app).get('/api/health/db-schema');
expect(response.status).toBe(500);
expect(response.body.message).toBe('DB connection failed'); // This is the message from the original error
expect(response.body.stack).toBeDefined();
expect(response.body.errorId).toEqual(expect.any(String));
console.log('[DEBUG] health.routes.test.ts: Verifying logger.error for DB schema check failure');
expect(mockLogger.error).toHaveBeenCalledWith(
expect.objectContaining({
err: expect.any(Error),
}),
expect.stringMatching(/Unhandled API Error \(ID: [\w-]+\)/),
);
});
it('should return 500 if the database check fails with a non-Error object', async () => {
// Arrange: Mock the service function to reject with a non-error object
const dbError = { message: 'DB connection failed' };
mockedDbConnection.checkTablesExist.mockRejectedValue(dbError);
const response = await supertest(app).get('/api/health/db-schema');
expect(response.status).toBe(500);
expect(response.body.message).toBe('DB connection failed'); // This is the message from the original error
expect(response.body.errorId).toEqual(expect.any(String));
expect(mockLogger.error).toHaveBeenCalledWith(
expect.objectContaining({
err: expect.objectContaining({ message: 'DB connection failed' }),
}),
expect.stringMatching(/Unhandled API Error \(ID: [\w-]+\)/),
);
});
});
describe('GET /storage', () => {
it('should return 200 OK if storage is accessible and writable', async () => {
// Arrange: Mock fs.access to resolve, indicating success.
mockedFs.access.mockResolvedValue(undefined);
// Act
const response = await supertest(app).get('/api/health/storage');
// Assert
expect(response.status).toBe(200);
expect(response.body.success).toBe(true);
expect(response.body.message).toContain('is accessible and writable');
});
it('should return 500 if storage is not accessible or writable', async () => {
// Arrange: Mock fs.access to reject, indicating failure.
const accessError = new Error('EACCES: permission denied');
mockedFs.access.mockRejectedValue(accessError);
// Act
const response = await supertest(app).get('/api/health/storage');
// Assert
expect(response.status).toBe(500);
expect(response.body.message).toContain('Storage check failed.');
expect(mockLogger.error).toHaveBeenCalledWith(
expect.objectContaining({
err: expect.any(Error),
}),
expect.stringMatching(/Unhandled API Error \(ID: [\w-]+\)/),
);
});
it('should return 500 if storage check fails with a non-Error object', async () => {
// Arrange: Mock fs.access to reject with a non-error object.
const accessError = { message: 'EACCES: permission denied' };
mockedFs.access.mockRejectedValue(accessError);
// Act
const response = await supertest(app).get('/api/health/storage');
// Assert
expect(response.status).toBe(500);
expect(response.body.message).toContain('Storage check failed.');
expect(mockLogger.error).toHaveBeenCalledWith(
expect.objectContaining({
err: expect.any(Error),
}),
expect.stringMatching(/Unhandled API Error \(ID: [\w-]+\)/),
);
});
});
describe('GET /db-pool', () => {
it('should return 200 OK for a healthy pool status', async () => {
// Arrange: Mock a healthy pool status (waitingCount < 5)
mockedDbConnection.getPoolStatus.mockReturnValue({
totalCount: 10,
idleCount: 8,
waitingCount: 1,
});
// Act
const response = await supertest(app).get('/api/health/db-pool');
// Assert
expect(response.status).toBe(200);
expect(response.body.success).toBe(true);
expect(response.body.message).toContain('Pool Status: 10 total, 8 idle, 1 waiting.');
});
it('should return 500 for an unhealthy pool status', async () => {
// Arrange: Mock an unhealthy pool status (waitingCount >= 5)
mockedDbConnection.getPoolStatus.mockReturnValue({
totalCount: 20,
idleCount: 5,
waitingCount: 15,
});
// Act
const response = await supertest(app).get('/api/health/db-pool');
// Assert
expect(response.status).toBe(500);
expect(response.body.success).toBe(false);
expect(response.body.message).toContain('Pool may be under stress.');
expect(response.body.message).toContain('Pool Status: 20 total, 5 idle, 15 waiting.');
expect(logger.warn).toHaveBeenCalledWith(
'Database pool health check shows high waiting count: 15',
);
});
});
it('should return 500 if getPoolStatus throws an error', async () => {
// Arrange: Mock getPoolStatus to throw an error
const poolError = new Error('Pool is not initialized');
mockedDbConnection.getPoolStatus.mockImplementation(() => {
throw poolError;
});
const response = await supertest(app).get('/api/health/db-pool');
expect(response.status).toBe(500);
expect(response.body.message).toBe('Pool is not initialized'); // This is the message from the original error
expect(response.body.errorId).toEqual(expect.any(String));
expect(mockLogger.error).toHaveBeenCalledWith(
expect.objectContaining({
err: expect.any(Error),
}),
expect.stringMatching(/Unhandled API Error \(ID: [\w-]+\)/),
);
});
it('should return 500 if getPoolStatus throws a non-Error object', async () => {
// Arrange: Mock getPoolStatus to throw a non-error object
const poolError = { message: 'Pool is not initialized' };
mockedDbConnection.getPoolStatus.mockImplementation(() => {
throw poolError;
});
const response = await supertest(app).get('/api/health/db-pool');
expect(response.status).toBe(500);
expect(response.body.message).toBe('Pool is not initialized'); // This is the message from the original error
expect(response.body.stack).toBeDefined();
expect(response.body.errorId).toEqual(expect.any(String));
expect(mockLogger.error).toHaveBeenCalledWith(
expect.objectContaining({
err: expect.objectContaining({ message: 'Pool is not initialized' }),
}),
expect.stringMatching(/Unhandled API Error \(ID: [\w-]+\)/),
);
});
describe('GET /redis', () => {
it('should return 500 if Redis ping fails', async () => {
const redisError = new Error('Connection timed out');
mockedRedisConnection.ping.mockRejectedValue(redisError);
const response = await supertest(app).get('/api/health/redis');
expect(response.status).toBe(500);
expect(response.body.message).toBe('Connection timed out');
expect(response.body.stack).toBeDefined();
expect(response.body.errorId).toEqual(expect.any(String));
console.log('[DEBUG] health.routes.test.ts: Checking if logger.error was called with the correct pattern');
expect(mockLogger.error).toHaveBeenCalledWith(
expect.objectContaining({
err: expect.any(Error),
}),
expect.stringMatching(/Unhandled API Error \(ID: [\w-]+\)/),
);
});
it('should return 500 if Redis ping returns an unexpected response', async () => {
mockedRedisConnection.ping.mockResolvedValue('OK'); // Not 'PONG'
const response = await supertest(app).get('/api/health/redis');
expect(response.status).toBe(500);
expect(response.body.message).toContain('Unexpected Redis ping response: OK');
expect(response.body.stack).toBeDefined();
expect(response.body.errorId).toEqual(expect.any(String));
expect(mockLogger.error).toHaveBeenCalledWith(
expect.objectContaining({
err: expect.any(Error),
}),
expect.stringMatching(/Unhandled API Error \(ID: [\w-]+\)/),
);
});
});
});