Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b61a00003a | ||
| 52dba6f890 | |||
| 4242678aab | |||
|
|
b2e086d5ba | ||
| 07a9787570 | |||
|
|
4bf5dc3d58 | ||
| be3d269928 |
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "flyer-crawler",
|
||||
"version": "0.2.32",
|
||||
"version": "0.2.35",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "flyer-crawler",
|
||||
"version": "0.2.32",
|
||||
"version": "0.2.35",
|
||||
"dependencies": {
|
||||
"@bull-board/api": "^6.14.2",
|
||||
"@bull-board/express": "^6.14.2",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "flyer-crawler",
|
||||
"private": true,
|
||||
"version": "0.2.32",
|
||||
"version": "0.2.35",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "concurrently \"npm:start:dev\" \"vite\"",
|
||||
|
||||
@@ -113,13 +113,14 @@ describe('errorHandler Middleware', () => {
|
||||
expect(response.body.message).toBe('A generic server error occurred.');
|
||||
expect(response.body.stack).toBeDefined();
|
||||
expect(response.body.errorId).toEqual(expect.any(String));
|
||||
console.log('[DEBUG] errorHandler.test.ts: Received 500 error response with ID:', response.body.errorId);
|
||||
expect(mockLogger.error).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
err: expect.any(Error),
|
||||
errorId: expect.any(String),
|
||||
req: expect.objectContaining({ method: 'GET', url: '/generic-error' }),
|
||||
}),
|
||||
expect.stringMatching(/Unhandled API Error \(ID: \w+\)/),
|
||||
expect.stringMatching(/Unhandled API Error \(ID: [\w-]+\)/),
|
||||
);
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
||||
expect.stringMatching(/--- \[TEST\] UNHANDLED ERROR \(ID: \w+\) ---/),
|
||||
@@ -226,7 +227,7 @@ describe('errorHandler Middleware', () => {
|
||||
errorId: expect.any(String),
|
||||
req: expect.objectContaining({ method: 'GET', url: '/db-error-500' }),
|
||||
}),
|
||||
expect.stringMatching(/Unhandled API Error \(ID: \w+\)/),
|
||||
expect.stringMatching(/Unhandled API Error \(ID: [\w-]+\)/),
|
||||
);
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
||||
expect.stringMatching(/--- \[TEST\] UNHANDLED ERROR \(ID: \w+\) ---/),
|
||||
|
||||
@@ -164,11 +164,12 @@ describe('Health Routes (/api/health)', () => {
|
||||
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+\)/),
|
||||
expect.stringMatching(/Unhandled API Error \(ID: [\w-]+\)/),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -186,7 +187,7 @@ describe('Health Routes (/api/health)', () => {
|
||||
expect.objectContaining({
|
||||
err: expect.objectContaining({ message: 'DB connection failed' }),
|
||||
}),
|
||||
expect.stringMatching(/Unhandled API Error \(ID: \w+\)/),
|
||||
expect.stringMatching(/Unhandled API Error \(ID: [\w-]+\)/),
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -220,7 +221,7 @@ describe('Health Routes (/api/health)', () => {
|
||||
expect.objectContaining({
|
||||
err: expect.any(Error),
|
||||
}),
|
||||
expect.stringMatching(/Unhandled API Error \(ID: \w+\)/),
|
||||
expect.stringMatching(/Unhandled API Error \(ID: [\w-]+\)/),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -239,7 +240,7 @@ describe('Health Routes (/api/health)', () => {
|
||||
expect.objectContaining({
|
||||
err: expect.any(Error),
|
||||
}),
|
||||
expect.stringMatching(/Unhandled API Error \(ID: \w+\)/),
|
||||
expect.stringMatching(/Unhandled API Error \(ID: [\w-]+\)/),
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -300,7 +301,7 @@ describe('Health Routes (/api/health)', () => {
|
||||
expect.objectContaining({
|
||||
err: expect.any(Error),
|
||||
}),
|
||||
expect.stringMatching(/Unhandled API Error \(ID: \w+\)/),
|
||||
expect.stringMatching(/Unhandled API Error \(ID: [\w-]+\)/),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -321,7 +322,7 @@ describe('Health Routes (/api/health)', () => {
|
||||
expect.objectContaining({
|
||||
err: expect.objectContaining({ message: 'Pool is not initialized' }),
|
||||
}),
|
||||
expect.stringMatching(/Unhandled API Error \(ID: \w+\)/),
|
||||
expect.stringMatching(/Unhandled API Error \(ID: [\w-]+\)/),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -336,11 +337,12 @@ describe('Health Routes (/api/health)', () => {
|
||||
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+\)/),
|
||||
expect.stringMatching(/Unhandled API Error \(ID: [\w-]+\)/),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -357,7 +359,7 @@ describe('Health Routes (/api/health)', () => {
|
||||
expect.objectContaining({
|
||||
err: expect.any(Error),
|
||||
}),
|
||||
expect.stringMatching(/Unhandled API Error \(ID: \w+\)/),
|
||||
expect.stringMatching(/Unhandled API Error \(ID: [\w-]+\)/),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
// src/routes/system.routes.test.ts
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import supertest from 'supertest';
|
||||
import { exec, type ExecException, type ExecOptions } from 'child_process';
|
||||
import { geocodingService } from '../services/geocodingService.server';
|
||||
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: {
|
||||
@@ -24,46 +29,24 @@ vi.mock('../services/logger.server', () => ({
|
||||
}));
|
||||
|
||||
// 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(() => {
|
||||
// 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>;
|
||||
},
|
||||
);
|
||||
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');
|
||||
@@ -77,28 +60,10 @@ describe('System Routes (/api/system)', () => {
|
||||
});
|
||||
|
||||
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>;
|
||||
},
|
||||
);
|
||||
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');
|
||||
|
||||
@@ -109,33 +74,10 @@ describe('System Routes (/api/system)', () => {
|
||||
|
||||
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>;
|
||||
},
|
||||
);
|
||||
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');
|
||||
@@ -150,55 +92,17 @@ describe('System Routes (/api/system)', () => {
|
||||
|
||||
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 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.message).toBe(`PM2 command produced an error: ${stderrOutput}`);
|
||||
expect(response.body.message).toBe(serviceError.message);
|
||||
});
|
||||
|
||||
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>;
|
||||
},
|
||||
);
|
||||
const serviceError = new Error('System error');
|
||||
vi.mocked(systemService.getPm2Status).mockRejectedValue(serviceError);
|
||||
|
||||
// Act
|
||||
const response = await supertest(app).get('/api/system/pm2-status');
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// src/services/analyticsService.server.test.ts
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import { AnalyticsService } from './analyticsService.server';
|
||||
import { logger } from './logger.server';
|
||||
@@ -127,19 +128,17 @@ describe('AnalyticsService', () => {
|
||||
throw new Error('Processing failed');
|
||||
}); // "Successfully generated..."
|
||||
|
||||
// Wrap the async operation that is expected to reject in a function.
|
||||
// This prevents an "unhandled rejection" error by ensuring the `expect.rejects`
|
||||
// is actively waiting for the promise to reject when the timers are advanced.
|
||||
const testFunction = async () => {
|
||||
const promise = service.processWeeklyReportJob(job);
|
||||
// Advance timers to trigger the part of the code that throws.
|
||||
await vi.advanceTimersByTimeAsync(30000);
|
||||
// Await the promise to allow the rejection to be caught by `expect.rejects`.
|
||||
await promise;
|
||||
};
|
||||
// Get the promise from the service method.
|
||||
const promise = service.processWeeklyReportJob(job);
|
||||
|
||||
// Now, assert that the entire operation rejects as expected.
|
||||
await expect(testFunction()).rejects.toThrow('Processing failed');
|
||||
// Capture the expectation promise BEFORE triggering the rejection.
|
||||
const expectation = expect(promise).rejects.toThrow('Processing failed');
|
||||
|
||||
// Advance timers to trigger the part of the code that throws.
|
||||
await vi.advanceTimersByTimeAsync(30000);
|
||||
|
||||
// Await the expectation to ensure assertions ran.
|
||||
await expectation;
|
||||
|
||||
// Verify the side effect (error logging) after the rejection is confirmed.
|
||||
expect(mockLoggerInstance.error).toHaveBeenCalledWith(
|
||||
|
||||
@@ -148,11 +148,11 @@ describe('AuthService', () => {
|
||||
expect(result).toEqual({
|
||||
newUserProfile: mockUserProfile,
|
||||
accessToken: 'access-token',
|
||||
refreshToken: 'mocked-random-string',
|
||||
refreshToken: 'mocked_random_id',
|
||||
});
|
||||
expect(userRepo.saveRefreshToken).toHaveBeenCalledWith(
|
||||
'user-123',
|
||||
'mocked-random-string',
|
||||
'mocked_random_id',
|
||||
reqLog,
|
||||
);
|
||||
});
|
||||
@@ -178,7 +178,7 @@ describe('AuthService', () => {
|
||||
);
|
||||
expect(result).toEqual({
|
||||
accessToken: 'access-token',
|
||||
refreshToken: 'mocked-random-string',
|
||||
refreshToken: 'mocked_random_id',
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -218,10 +218,10 @@ describe('AuthService', () => {
|
||||
);
|
||||
expect(sendPasswordResetEmail).toHaveBeenCalledWith(
|
||||
'test@example.com',
|
||||
expect.stringContaining('/reset-password/mocked-random-string'),
|
||||
expect.stringContaining('/reset-password/mocked_random_id'),
|
||||
reqLog,
|
||||
);
|
||||
expect(result).toBe('mocked-random-string');
|
||||
expect(result).toBe('mocked_random_id');
|
||||
});
|
||||
|
||||
it('should log warning and return undefined for non-existent user', async () => {
|
||||
|
||||
@@ -190,7 +190,10 @@ describe('Worker Service Lifecycle', () => {
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
processExitSpy.mockRestore();
|
||||
if (processExitSpy && typeof processExitSpy.mockRestore === 'function') {
|
||||
console.log('[DEBUG] queueService.server.test.ts: Restoring process.exit spy');
|
||||
processExitSpy.mockRestore();
|
||||
}
|
||||
});
|
||||
|
||||
it('should close all workers, queues, the redis connection, and exit the process', async () => {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// src/services/systemService.test.ts
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { exec, type ExecException } from 'child_process';
|
||||
import { describe, it, expect, vi, beforeEach, Mock } from 'vitest';
|
||||
import { logger } from './logger.server';
|
||||
import type { ExecException } from 'child_process';
|
||||
|
||||
// Mock logger
|
||||
vi.mock('./logger.server', () => ({
|
||||
@@ -12,12 +12,19 @@ vi.mock('./logger.server', () => ({
|
||||
},
|
||||
}));
|
||||
|
||||
// Import service AFTER mocks to ensure top-level promisify uses the mock
|
||||
import { systemService } from './systemService';
|
||||
// Import the class, not the singleton instance, to apply Dependency Injection
|
||||
import { SystemService } from './systemService';
|
||||
|
||||
describe('SystemService', () => {
|
||||
let systemService: SystemService;
|
||||
let mockExecAsync: Mock;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
// Create a mock function for our dependency
|
||||
mockExecAsync = vi.fn();
|
||||
// Instantiate the service with the mock dependency
|
||||
systemService = new SystemService(mockExecAsync);
|
||||
});
|
||||
|
||||
describe('getPm2Status', () => {
|
||||
@@ -29,10 +36,7 @@ describe('SystemService', () => {
|
||||
│ 0 │ flyer-crawler-api │ online │
|
||||
└────┴──────────────────────┴──────────┘
|
||||
`;
|
||||
vi.mocked(exec).mockImplementation((cmd, callback: any) => {
|
||||
callback(null, stdout, '');
|
||||
return {} as any;
|
||||
});
|
||||
mockExecAsync.mockResolvedValue({ stdout, stderr: '' });
|
||||
|
||||
const result = await systemService.getPm2Status();
|
||||
|
||||
@@ -50,10 +54,7 @@ describe('SystemService', () => {
|
||||
│ 0 │ flyer-crawler-api │ stopped │
|
||||
└────┴──────────────────────┴──────────┘
|
||||
`;
|
||||
vi.mocked(exec).mockImplementation((cmd, callback: any) => {
|
||||
callback(null, stdout, '');
|
||||
return {} as any;
|
||||
});
|
||||
mockExecAsync.mockResolvedValue({ stdout, stderr: '' });
|
||||
|
||||
const result = await systemService.getPm2Status();
|
||||
|
||||
@@ -64,12 +65,11 @@ describe('SystemService', () => {
|
||||
});
|
||||
|
||||
it('should throw error if stderr has content', async () => {
|
||||
vi.mocked(exec).mockImplementation((cmd, callback: any) => {
|
||||
callback(null, 'some stdout', 'some stderr warning');
|
||||
return {} as any;
|
||||
});
|
||||
mockExecAsync.mockResolvedValue({ stdout: 'some stdout', stderr: 'some stderr warning' });
|
||||
|
||||
await expect(systemService.getPm2Status()).rejects.toThrow('PM2 command produced an error: some stderr warning');
|
||||
await expect(systemService.getPm2Status()).rejects.toThrow(
|
||||
'PM2 command produced an error: some stderr warning',
|
||||
);
|
||||
});
|
||||
|
||||
it('should return success: false when process does not exist', async () => {
|
||||
@@ -77,10 +77,7 @@ describe('SystemService', () => {
|
||||
error.code = 1;
|
||||
error.stderr = "[PM2][ERROR] Process or Namespace flyer-crawler-api doesn't exist";
|
||||
|
||||
vi.mocked(exec).mockImplementation((cmd, callback: any) => {
|
||||
callback(error, '', error.stderr);
|
||||
return {} as any;
|
||||
});
|
||||
mockExecAsync.mockRejectedValue(error);
|
||||
|
||||
const result = await systemService.getPm2Status();
|
||||
|
||||
@@ -89,7 +86,7 @@ describe('SystemService', () => {
|
||||
message: 'Application process is not running under PM2.',
|
||||
});
|
||||
expect(logger.warn).toHaveBeenCalledWith(
|
||||
expect.stringContaining('PM2 process "flyer-crawler-api" not found')
|
||||
expect.stringContaining('PM2 process "flyer-crawler-api" not found'),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,16 +1,24 @@
|
||||
// src/services/systemService.ts
|
||||
import { exec } from 'child_process';
|
||||
import { exec as nodeExec, type ExecException } from 'child_process';
|
||||
import { promisify } from 'util';
|
||||
import { logger } from './logger.server';
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
logger.debug({ typeOfExec: typeof exec, isExecFunction: typeof exec === 'function' }, 'SystemService: Initializing execAsync');
|
||||
logger.debug({ typeOfPromisify: typeof promisify, isPromisifyFunction: typeof promisify === 'function' }, 'SystemService: Initializing execAsync');
|
||||
// Define a type for the exec function for better type safety and testability.
|
||||
// It matches the signature of a promisified child_process.exec.
|
||||
export type ExecAsync = (
|
||||
command: string,
|
||||
) => Promise<{ stdout: string; stderr: string }>;
|
||||
|
||||
export class SystemService {
|
||||
private execAsync: ExecAsync;
|
||||
|
||||
constructor(execAsync: ExecAsync) {
|
||||
this.execAsync = execAsync;
|
||||
}
|
||||
|
||||
class SystemService {
|
||||
async getPm2Status(): Promise<{ success: boolean; message: string }> {
|
||||
try {
|
||||
const { stdout, stderr } = await execAsync('pm2 describe flyer-crawler-api');
|
||||
const { stdout, stderr } = await this.execAsync('pm2 describe flyer-crawler-api');
|
||||
|
||||
// If the command runs but produces output on stderr, treat it as an error.
|
||||
// This handles cases where pm2 might issue warnings but still exit 0.
|
||||
@@ -23,7 +31,7 @@ class SystemService {
|
||||
? 'Application is online and running under PM2.'
|
||||
: 'Application process exists but is not online.';
|
||||
return { success: isOnline, message };
|
||||
} catch (error: any) {
|
||||
} catch (error: ExecException | any) {
|
||||
// If the command fails (non-zero exit code), check if it's because the process doesn't exist.
|
||||
// This is a normal "not found" case, not a system error.
|
||||
// The error message can be in stdout or stderr depending on the pm2 version.
|
||||
@@ -42,4 +50,6 @@ class SystemService {
|
||||
}
|
||||
}
|
||||
|
||||
export const systemService = new SystemService();
|
||||
// Instantiate the service with the real dependency for the application
|
||||
const realExecAsync = promisify(nodeExec);
|
||||
export const systemService = new SystemService(realExecAsync);
|
||||
@@ -122,7 +122,7 @@ afterEach(cleanup);
|
||||
// when it's promisified. The standard util.promisify doesn't work on a simple vi.fn() mock.
|
||||
vi.mock('util', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('util')>();
|
||||
return {
|
||||
const mocked = {
|
||||
...actual,
|
||||
promisify: (fn: Function) => {
|
||||
return (...args: any[]) => {
|
||||
@@ -140,16 +140,9 @@ vi.mock('util', async (importOriginal) => {
|
||||
};
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
// Mock 'child_process' using the robust `importOriginal` pattern.
|
||||
// This preserves the module's structure and prevents "No default export" errors.
|
||||
vi.mock('child_process', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('child_process')>();
|
||||
return {
|
||||
...actual,
|
||||
// Provide a mock function for `exec` that can be implemented per-test.
|
||||
exec: vi.fn(),
|
||||
...mocked,
|
||||
default: mocked,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -172,8 +165,13 @@ vi.mock('bcrypt');
|
||||
vi.mock('crypto', () => ({
|
||||
default: {
|
||||
randomBytes: vi.fn().mockReturnValue({
|
||||
toString: vi.fn().mockReturnValue('mocked-random-string'),
|
||||
toString: vi.fn().mockImplementation((encoding) => {
|
||||
const id = 'mocked_random_id';
|
||||
console.log(`[DEBUG] tests-setup-unit.ts: crypto.randomBytes mock returning "${id}" for encoding "${encoding}"`);
|
||||
return id;
|
||||
}),
|
||||
}),
|
||||
randomUUID: vi.fn().mockReturnValue('mocked_random_id'),
|
||||
},
|
||||
}));
|
||||
|
||||
|
||||
Reference in New Issue
Block a user