// 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; }, ); // 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; }, ); 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; }, ); // 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; }, ); 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; }, ); // 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); }); }); });