Files
flyer-crawler.projectium.com/src/routes/system.routes.test.ts

269 lines
9.5 KiB
TypeScript

// src/routes/system.routes.test.ts
import { describe, it, expect, vi, beforeEach } from 'vitest';
import supertest from 'supertest';
import systemRouter from './system.routes'; // This was a duplicate, fixed.
import { exec, type ExecException, type ExecOptions } from 'child_process';
import { geocodingService } from '../services/geocodingService.server';
import { createTestApp } from '../tests/utils/createTestApp';
// FIX: Use the simple factory pattern for child_process to avoid default export issues
vi.mock('child_process', () => {
const mockExec = vi.fn((command, callback) => {
if (typeof callback === 'function') {
callback(null, 'PM2 OK', '');
}
return { unref: () => {} };
});
return {
default: { exec: mockExec },
exec: mockExec,
};
});
// 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(),
},
}));
describe('System Routes (/api/system)', () => {
const app = createTestApp({ router: systemRouter, basePath: '/api/system' });
beforeEach(() => {
// We cast here to get type-safe access to mock functions like .mockImplementation
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.
const pm2OnlineOutput = `
┌─ PM2 info ────────────────┐
│ status │ online │
└───────────┴───────────┘
`;
type ExecCallback = (error: ExecException | null, stdout: string, stderr: string) => void;
// A robust mock for `exec` that handles its multiple overloads.
// This avoids the complex and error-prone `...args` signature.
vi.mocked(exec).mockImplementation(
(
command: string,
options?: ExecOptions | ExecCallback | null,
callback?: ExecCallback | null,
) => {
// The actual callback can be the second or third argument.
const actualCallback = (
typeof options === 'function' ? options : callback
) as ExecCallback;
if (actualCallback) {
actualCallback(null, pm2OnlineOutput, '');
}
// Return a minimal object that satisfies the ChildProcess type for .unref()
return { unref: () => {} } as ReturnType<typeof exec>;
},
);
// Act
const response = await supertest(app).get('/api/system/pm2-status');
// Assert
expect(response.status).toBe(200);
expect(response.body).toEqual({
success: true,
message: 'Application is online and running under PM2.',
});
});
it('should return success: false when pm2 process is stopped or errored', async () => {
const pm2StoppedOutput = `│ status │ stopped │`;
vi.mocked(exec).mockImplementation(
(
command: string,
options?:
| ExecOptions
| ((error: ExecException | null, stdout: string, stderr: string) => void)
| null,
callback?: ((error: ExecException | null, stdout: string, stderr: string) => void) | null,
) => {
const actualCallback = (typeof options === 'function' ? options : callback) as (
error: ExecException | null,
stdout: string,
stderr: string,
) => void;
if (actualCallback) {
actualCallback(null, pm2StoppedOutput, '');
}
return { unref: () => {} } as ReturnType<typeof exec>;
},
);
const response = await supertest(app).get('/api/system/pm2-status');
// Assert
expect(response.status).toBe(200);
expect(response.body.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.
const processNotFoundOutput =
"[PM2][ERROR] Process or Namespace flyer-crawler-api doesn't exist";
const processNotFoundError = new Error(
'Command failed: pm2 describe flyer-crawler-api',
) as ExecException;
processNotFoundError.code = 1;
vi.mocked(exec).mockImplementation(
(
command: string,
options?:
| ExecOptions
| ((error: ExecException | null, stdout: string, stderr: string) => void)
| null,
callback?: ((error: ExecException | null, stdout: string, stderr: string) => void) | null,
) => {
const actualCallback = (typeof options === 'function' ? options : callback) as (
error: ExecException | null,
stdout: string,
stderr: string,
) => void;
if (actualCallback) {
actualCallback(processNotFoundError, processNotFoundOutput, '');
}
return { unref: () => {} } as ReturnType<typeof exec>;
},
);
// Act
const response = await supertest(app).get('/api/system/pm2-status');
// Assert
expect(response.status).toBe(200);
expect(response.body).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 stderrOutput = 'A non-fatal warning occurred.';
vi.mocked(exec).mockImplementation(
(
command: string,
options?:
| ExecOptions
| ((error: ExecException | null, stdout: string, stderr: string) => void)
| null,
callback?: ((error: ExecException | null, stdout: string, stderr: string) => void) | null,
) => {
const actualCallback = (typeof options === 'function' ? options : callback) as (
error: ExecException | null,
stdout: string,
stderr: string,
) => void;
if (actualCallback) {
actualCallback(null, 'Some stdout', stderrOutput);
}
return { unref: () => {} } as ReturnType<typeof exec>;
},
);
const response = await supertest(app).get('/api/system/pm2-status');
expect(response.status).toBe(500);
expect(response.body.message).toBe(`PM2 command produced an error: ${stderrOutput}`);
});
it('should return 500 on a generic exec error', async () => {
vi.mocked(exec).mockImplementation(
(
command: string,
options?:
| ExecOptions
| ((error: ExecException | null, stdout: string, stderr: string) => void)
| null,
callback?: ((error: ExecException | null, stdout: string, stderr: string) => void) | null,
) => {
const actualCallback = (typeof options === 'function' ? options : callback) as (
error: ExecException | null,
stdout: string,
stderr: string,
) => void;
if (actualCallback) {
actualCallback(new Error('System error') as ExecException, '', 'stderr output');
}
return { unref: () => {} } as ReturnType<typeof exec>;
},
);
// Act
const response = await supertest(app).get('/api/system/pm2-status');
// Assert
expect(response.status).toBe(500);
expect(response.body.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).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.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.errors[0].message).toMatch(/An address string is required|Required/i);
});
});
});