Some checks failed
Deploy to Test Environment / deploy-to-test (push) Failing after 1m1s
185 lines
6.6 KiB
TypeScript
185 lines
6.6 KiB
TypeScript
// src/routes/system.routes.test.ts
|
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
import supertest from 'supertest';
|
|
import { createTestApp } from '../tests/utils/createTestApp';
|
|
|
|
// 1. Mock the Service Layer
|
|
// This decouples the route test from the service's implementation details.
|
|
vi.mock('../services/systemService', () => ({
|
|
systemService: {
|
|
getPm2Status: vi.fn(),
|
|
},
|
|
}));
|
|
// 2. Mock Geocoding
|
|
vi.mock('../services/geocodingService.server', () => ({
|
|
geocodingService: {
|
|
geocodeAddress: vi.fn(),
|
|
},
|
|
}));
|
|
|
|
// 3. Mock Logger
|
|
vi.mock('../services/logger.server', () => ({
|
|
logger: {
|
|
info: vi.fn(),
|
|
debug: vi.fn(),
|
|
error: vi.fn(),
|
|
warn: vi.fn(),
|
|
child: vi.fn().mockReturnThis(),
|
|
},
|
|
}));
|
|
|
|
// Import the router AFTER all mocks are defined to ensure systemService picks up the mocked util.promisify
|
|
import { systemService } from '../services/systemService';
|
|
import systemRouter from './system.routes';
|
|
import { geocodingService } from '../services/geocodingService.server';
|
|
|
|
describe('System Routes (/api/system)', () => {
|
|
const app = createTestApp({ router: systemRouter, basePath: '/api/system' });
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
});
|
|
|
|
describe('GET /pm2-status', () => {
|
|
it('should return success: true when pm2 process is online', async () => {
|
|
// Arrange: Simulate a successful `pm2 describe` output for an online process.
|
|
vi.mocked(systemService.getPm2Status).mockResolvedValue({
|
|
success: true,
|
|
message: 'Application is online and running under PM2.',
|
|
});
|
|
|
|
// Act
|
|
const response = await supertest(app).get('/api/system/pm2-status');
|
|
|
|
// Assert
|
|
expect(response.status).toBe(200);
|
|
expect(response.body.data).toEqual({
|
|
success: true,
|
|
message: 'Application is online and running under PM2.',
|
|
});
|
|
});
|
|
|
|
it('should return success: false when pm2 process is stopped or errored', async () => {
|
|
vi.mocked(systemService.getPm2Status).mockResolvedValue({
|
|
success: false,
|
|
message: 'Application process exists but is not online.',
|
|
});
|
|
|
|
const response = await supertest(app).get('/api/system/pm2-status');
|
|
|
|
// Assert
|
|
expect(response.status).toBe(200);
|
|
expect(response.body.data.success).toBe(false);
|
|
});
|
|
|
|
it('should return success: false when pm2 process does not exist', async () => {
|
|
// Arrange: Simulate `pm2 describe` failing because the process isn't found.
|
|
vi.mocked(systemService.getPm2Status).mockResolvedValue({
|
|
success: false,
|
|
message: 'Application process is not running under PM2.',
|
|
});
|
|
|
|
// Act
|
|
const response = await supertest(app).get('/api/system/pm2-status');
|
|
|
|
// Assert
|
|
expect(response.status).toBe(200);
|
|
expect(response.body.data).toEqual({
|
|
success: false,
|
|
message: 'Application process is not running under PM2.',
|
|
});
|
|
});
|
|
|
|
it('should return 500 if pm2 command produces stderr output', async () => {
|
|
// Arrange: Simulate a successful exit code but with content in stderr.
|
|
const serviceError = new Error(
|
|
'PM2 command produced an error: A non-fatal warning occurred.',
|
|
);
|
|
vi.mocked(systemService.getPm2Status).mockRejectedValue(serviceError);
|
|
|
|
const response = await supertest(app).get('/api/system/pm2-status');
|
|
expect(response.status).toBe(500);
|
|
expect(response.body.error.message).toBe(serviceError.message);
|
|
});
|
|
|
|
it('should return 500 on a generic exec error', async () => {
|
|
const serviceError = new Error('System error');
|
|
vi.mocked(systemService.getPm2Status).mockRejectedValue(serviceError);
|
|
|
|
// Act
|
|
const response = await supertest(app).get('/api/system/pm2-status');
|
|
|
|
// Assert
|
|
expect(response.status).toBe(500);
|
|
expect(response.body.error.message).toBe('System error');
|
|
});
|
|
});
|
|
|
|
describe('POST /geocode', () => {
|
|
it('should return geocoded coordinates for a valid address', async () => {
|
|
// Arrange
|
|
const mockCoordinates = { lat: 48.4284, lng: -123.3656 };
|
|
vi.mocked(geocodingService.geocodeAddress).mockResolvedValue(mockCoordinates);
|
|
|
|
// Act
|
|
const response = await supertest(app)
|
|
.post('/api/system/geocode')
|
|
.send({ address: 'Victoria, BC' });
|
|
|
|
// Assert
|
|
expect(response.status).toBe(200);
|
|
expect(response.body.data).toEqual(mockCoordinates);
|
|
});
|
|
|
|
it('should return 404 if the address cannot be geocoded', async () => {
|
|
vi.mocked(geocodingService.geocodeAddress).mockResolvedValue(null);
|
|
const response = await supertest(app)
|
|
.post('/api/system/geocode')
|
|
.send({ address: 'Invalid Address' });
|
|
expect(response.status).toBe(404);
|
|
expect(response.body.error.message).toBe('Could not geocode the provided address.');
|
|
});
|
|
|
|
it('should return 500 if the geocoding service throws an error', async () => {
|
|
const geocodeError = new Error('Geocoding service unavailable');
|
|
vi.mocked(geocodingService.geocodeAddress).mockRejectedValue(geocodeError);
|
|
const response = await supertest(app)
|
|
.post('/api/system/geocode')
|
|
.send({ address: 'Any Address' });
|
|
expect(response.status).toBe(500);
|
|
});
|
|
|
|
it('should return 400 if the address is missing from the body', async () => {
|
|
const response = await supertest(app)
|
|
.post('/api/system/geocode')
|
|
.send({ not_address: 'Victoria, BC' });
|
|
expect(response.status).toBe(400);
|
|
// Zod validation error message can vary slightly depending on configuration or version
|
|
expect(response.body.error.details[0].message).toMatch(
|
|
/An address string is required|Required/i,
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('Rate Limiting on /geocode', () => {
|
|
it('should block requests after exceeding the limit when the opt-in header is sent', async () => {
|
|
const limit = 100; // Matches geocodeLimiter config
|
|
const address = '123 Test St';
|
|
vi.mocked(geocodingService.geocodeAddress).mockResolvedValue({ lat: 0, lng: 0 });
|
|
|
|
// We only need to verify it blocks eventually.
|
|
// Instead of running 100 requests, we check for the headers which confirm the middleware is active.
|
|
const response = await supertest(app)
|
|
.post('/api/system/geocode')
|
|
.set('X-Test-Rate-Limit-Enable', 'true')
|
|
.send({ address });
|
|
|
|
expect(response.status).toBe(200);
|
|
expect(response.headers).toHaveProperty('ratelimit-limit');
|
|
expect(response.headers).toHaveProperty('ratelimit-remaining');
|
|
expect(parseInt(response.headers['ratelimit-limit'])).toBe(limit);
|
|
expect(parseInt(response.headers['ratelimit-remaining'])).toBeLessThan(limit);
|
|
});
|
|
});
|
|
});
|