Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7a52bf499e | ||
| 2489ec8d2d |
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "flyer-crawler",
|
||||
"version": "0.2.30",
|
||||
"version": "0.2.31",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "flyer-crawler",
|
||||
"version": "0.2.30",
|
||||
"version": "0.2.31",
|
||||
"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.30",
|
||||
"version": "0.2.31",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "concurrently \"npm:start:dev\" \"vite\"",
|
||||
|
||||
@@ -30,17 +30,15 @@ vi.mock('util', async (importOriginal) => {
|
||||
};
|
||||
});
|
||||
|
||||
// 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: () => {} };
|
||||
});
|
||||
|
||||
// The `importOriginal` pattern is the robust way to mock built-in Node modules.
|
||||
// It preserves the module's original structure, preventing "No default export" errors
|
||||
// that can occur with simple factory mocks when using ESM-based test runners like Vitest.
|
||||
vi.mock('child_process', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('child_process')>();
|
||||
return {
|
||||
exec: mockExec,
|
||||
...actual,
|
||||
// We provide a basic mock function that will be implemented in each test.
|
||||
exec: vi.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { UniqueConstraintError } from './db/errors.db';
|
||||
import type { UserProfile } from '../types';
|
||||
|
||||
describe('AuthService', () => {
|
||||
@@ -10,6 +9,7 @@ describe('AuthService', () => {
|
||||
let adminRepo: typeof import('./db/index.db').adminRepo;
|
||||
let logger: typeof import('./logger.server').logger;
|
||||
let sendPasswordResetEmail: typeof import('./emailService.server').sendPasswordResetEmail;
|
||||
let UniqueConstraintError: typeof import('./db/errors.db').UniqueConstraintError;
|
||||
|
||||
const reqLog = {}; // Mock request logger object
|
||||
const mockUser = {
|
||||
@@ -32,7 +32,10 @@ describe('AuthService', () => {
|
||||
|
||||
// Mock all dependencies before dynamically importing the service
|
||||
vi.mock('bcrypt');
|
||||
vi.mock('jsonwebtoken');
|
||||
vi.mock('jsonwebtoken', () => ({
|
||||
sign: vi.fn(),
|
||||
verify: vi.fn(),
|
||||
}));
|
||||
vi.mock('crypto', () => ({
|
||||
default: {
|
||||
randomBytes: vi.fn().mockReturnValue({
|
||||
@@ -75,6 +78,7 @@ describe('AuthService', () => {
|
||||
adminRepo = dbModule.adminRepo;
|
||||
logger = (await import('./logger.server')).logger;
|
||||
sendPasswordResetEmail = (await import('./emailService.server')).sendPasswordResetEmail;
|
||||
UniqueConstraintError = (await import('./db/errors.db')).UniqueConstraintError;
|
||||
});
|
||||
|
||||
describe('registerUser', () => {
|
||||
|
||||
@@ -409,24 +409,18 @@ describe('FlyerProcessingService', () => {
|
||||
mockFileHandler.prepareImageInputs.mockRejectedValue(fileTypeError);
|
||||
const { logger } = await import('./logger.server');
|
||||
|
||||
const reportErrorSpy = vi.spyOn(service as any, '_reportErrorAndThrow');
|
||||
|
||||
await expect(service.processJob(job)).rejects.toThrow(UnsupportedFileTypeError);
|
||||
expect(job.updateProgress).toHaveBeenCalledWith({
|
||||
errorCode: 'UNSUPPORTED_FILE_TYPE',
|
||||
message: 'Unsupported file type: .txt. Supported types are PDF, JPG, PNG, WEBP, HEIC, HEIF, GIF, TIFF, SVG, BMP.',
|
||||
stages: [
|
||||
{ name: 'Preparing Inputs', status: 'failed', critical: true, detail: 'Unsupported file type: .txt. Supported types are PDF, JPG, PNG, WEBP, HEIC, HEIF, GIF, TIFF, SVG, BMP.' },
|
||||
{ name: 'Extracting Data with AI', status: 'skipped', critical: true, detail: 'Communicating with AI model...' },
|
||||
{ name: 'Transforming AI Data', status: 'skipped', critical: true },
|
||||
{ name: 'Saving to Database', status: 'skipped', critical: true },
|
||||
],
|
||||
});
|
||||
|
||||
expect(reportErrorSpy).toHaveBeenCalledWith(fileTypeError, job, expect.any(Object), expect.any(Array));
|
||||
expect(mockCleanupQueue.add).not.toHaveBeenCalled();
|
||||
expect(logger.warn).toHaveBeenCalledWith(
|
||||
'Job failed. Temporary files will NOT be cleaned up to allow for manual inspection.',
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw an error and not enqueue cleanup if icon generation fails', async () => {
|
||||
it('should delegate to _reportErrorAndThrow if icon generation fails', async () => {
|
||||
const job = createMockJob({});
|
||||
const { logger } = await import('./logger.server');
|
||||
const iconError = new Error('Icon generation failed.');
|
||||
@@ -435,18 +429,11 @@ describe('FlyerProcessingService', () => {
|
||||
// bubbling up from the icon generation step.
|
||||
vi.spyOn(FlyerDataTransformer.prototype, 'transform').mockRejectedValue(iconError);
|
||||
|
||||
const reportErrorSpy = vi.spyOn(service as any, '_reportErrorAndThrow');
|
||||
|
||||
await expect(service.processJob(job)).rejects.toThrow('Icon generation failed.');
|
||||
|
||||
expect(job.updateProgress).toHaveBeenCalledWith({
|
||||
errorCode: 'UNKNOWN_ERROR',
|
||||
message: 'Icon generation failed.',
|
||||
stages: [
|
||||
{ name: 'Preparing Inputs', status: 'completed', critical: true, detail: '1 page(s) ready for AI.' },
|
||||
{ name: 'Extracting Data with AI', status: 'completed', critical: true, detail: 'Communicating with AI model...' },
|
||||
{ name: 'Transforming AI Data', status: 'failed', critical: true, detail: 'Icon generation failed.' },
|
||||
{ name: 'Saving to Database', status: 'skipped', critical: true },
|
||||
],
|
||||
}); // This was a duplicate, fixed.
|
||||
expect(reportErrorSpy).toHaveBeenCalledWith(iconError, job, expect.any(Object), expect.any(Array));
|
||||
expect(mockCleanupQueue.add).not.toHaveBeenCalled();
|
||||
expect(logger.warn).toHaveBeenCalledWith(
|
||||
'Job failed. Temporary files will NOT be cleaned up to allow for manual inspection.',
|
||||
@@ -454,8 +441,58 @@ describe('FlyerProcessingService', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('_reportErrorAndThrow (private method)', () => {
|
||||
it('should update progress and throw UnrecoverableError for quota messages', async () => {
|
||||
describe('_reportErrorAndThrow (Error Reporting Logic)', () => {
|
||||
it('should update progress with a generic error and re-throw', async () => {
|
||||
const { logger } = await import('./logger.server');
|
||||
const job = createMockJob({});
|
||||
const genericError = new Error('A standard failure');
|
||||
const initialStages = [
|
||||
{ name: 'Stage 1', status: 'completed', critical: true, detail: 'Done' },
|
||||
{ name: 'Stage 2', status: 'in-progress', critical: true, detail: 'Working...' },
|
||||
{ name: 'Stage 3', status: 'pending', critical: true, detail: 'Waiting...' },
|
||||
];
|
||||
const privateMethod = (service as any)._reportErrorAndThrow;
|
||||
|
||||
await expect(privateMethod(genericError, job, logger, initialStages)).rejects.toThrow(genericError);
|
||||
|
||||
expect(job.updateProgress).toHaveBeenCalledWith({
|
||||
errorCode: 'UNKNOWN_ERROR',
|
||||
message: 'A standard failure',
|
||||
stages: [
|
||||
{ name: 'Stage 1', status: 'completed', critical: true, detail: 'Done' },
|
||||
{ name: 'Stage 2', status: 'failed', critical: true, detail: 'A standard failure' },
|
||||
{ name: 'Stage 3', status: 'skipped', critical: true },
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('should use toErrorPayload for FlyerProcessingError instances', async () => {
|
||||
const { logger } = await import('./logger.server');
|
||||
const job = createMockJob({});
|
||||
const validationError = new AiDataValidationError(
|
||||
'Validation failed',
|
||||
{ foo: 'bar' },
|
||||
{ raw: 'data' },
|
||||
);
|
||||
const initialStages = [
|
||||
{ name: 'Extracting Data with AI', status: 'in-progress', critical: true, detail: '...' },
|
||||
];
|
||||
const privateMethod = (service as any)._reportErrorAndThrow;
|
||||
|
||||
await expect(privateMethod(validationError, job, logger, initialStages)).rejects.toThrow(validationError);
|
||||
|
||||
expect(job.updateProgress).toHaveBeenCalledWith({
|
||||
errorCode: 'AI_VALIDATION_FAILED',
|
||||
message: "The AI couldn't read the flyer's format. Please try a clearer image or a different flyer.",
|
||||
validationErrors: { foo: 'bar' },
|
||||
rawData: { raw: 'data' },
|
||||
stages: [
|
||||
{ name: 'Extracting Data with AI', status: 'failed', critical: true, detail: "The AI couldn't read the flyer's format. Please try a clearer image or a different flyer." },
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw UnrecoverableError for quota messages', async () => {
|
||||
const { logger } = await import('./logger.server');
|
||||
const job = createMockJob({});
|
||||
const quotaError = new Error('RESOURCE_EXHAUSTED');
|
||||
@@ -472,53 +509,35 @@ describe('FlyerProcessingService', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should use toErrorPayload for FlyerProcessingError instances', async () => {
|
||||
const { logger } = await import('./logger.server');
|
||||
const job = createMockJob({});
|
||||
const validationError = new AiDataValidationError(
|
||||
'Validation failed',
|
||||
{ foo: 'bar' },
|
||||
{ raw: 'data' },
|
||||
);
|
||||
const privateMethod = (service as any)._reportErrorAndThrow;
|
||||
|
||||
await expect(privateMethod(validationError, job, logger, [])).rejects.toThrow(
|
||||
validationError,
|
||||
);
|
||||
|
||||
// The payload should now come from the error's `toErrorPayload` method
|
||||
expect(job.updateProgress).toHaveBeenCalledWith({
|
||||
errorCode: 'AI_VALIDATION_FAILED',
|
||||
message:
|
||||
"The AI couldn't read the flyer's format. Please try a clearer image or a different flyer.",
|
||||
validationErrors: { foo: 'bar' },
|
||||
rawData: { raw: 'data' },
|
||||
stages: [],
|
||||
});
|
||||
});
|
||||
|
||||
it('should update progress and re-throw standard errors', async () => {
|
||||
const { logger } = await import('./logger.server');
|
||||
const job = createMockJob({});
|
||||
const genericError = new Error('A standard failure');
|
||||
const privateMethod = (service as any)._reportErrorAndThrow;
|
||||
|
||||
await expect(privateMethod(genericError, job, logger, [])).rejects.toThrow(genericError);
|
||||
|
||||
expect(job.updateProgress).toHaveBeenCalledWith({
|
||||
errorCode: 'UNKNOWN_ERROR',
|
||||
message: 'A standard failure', // This was a duplicate, fixed.
|
||||
stages: [],
|
||||
});
|
||||
});
|
||||
|
||||
it('should wrap and throw non-Error objects', async () => {
|
||||
const { logger } = await import('./logger.server');
|
||||
const job = createMockJob({});
|
||||
const nonError = 'just a string error';
|
||||
const privateMethod = (service as any)._reportErrorAndThrow;
|
||||
|
||||
await expect(privateMethod(nonError, job, logger, [])).rejects.toThrow('just a string error');
|
||||
await expect(privateMethod(nonError, job, logger, [])).rejects.toThrow(
|
||||
'just a string error',
|
||||
);
|
||||
});
|
||||
|
||||
it('should correctly identify the failed stage based on error code', async () => {
|
||||
const { logger } = await import('./logger.server');
|
||||
const job = createMockJob({});
|
||||
const pdfError = new PdfConversionError('PDF failed');
|
||||
const initialStages = [
|
||||
{ name: 'Preparing Inputs', status: 'in-progress', critical: true, detail: '...' },
|
||||
{ name: 'Extracting Data with AI', status: 'pending', critical: true, detail: '...' },
|
||||
];
|
||||
const privateMethod = (service as any)._reportErrorAndThrow;
|
||||
|
||||
await expect(privateMethod(pdfError, job, logger, initialStages)).rejects.toThrow(pdfError);
|
||||
|
||||
expect(job.updateProgress).toHaveBeenCalledWith(expect.objectContaining({
|
||||
stages: [
|
||||
{ name: 'Preparing Inputs', status: 'failed', critical: true, detail: expect.any(String) },
|
||||
{ name: 'Extracting Data with AI', status: 'skipped', critical: true },
|
||||
],
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -36,10 +36,14 @@ vi.mock('util', async (importOriginal) => {
|
||||
});
|
||||
|
||||
// Mock child_process
|
||||
vi.mock('child_process', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('child_process')>();
|
||||
// Node.js built-in modules like 'child_process' are CommonJS modules.
|
||||
// When mocked in an ESM context (like Vitest), they might sometimes
|
||||
// be interpreted as having a default export if not explicitly handled.
|
||||
// By providing `__esModule: true` and explicitly defining `exec`,
|
||||
// we ensure Vitest correctly resolves the named import.
|
||||
vi.mock('child_process', () => {
|
||||
return {
|
||||
...actual,
|
||||
__esModule: true, // Explicitly mark as an ES module
|
||||
exec: vi.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
@@ -4,6 +4,8 @@ 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');
|
||||
|
||||
class SystemService {
|
||||
async getPm2Status(): Promise<{ success: boolean; message: string }> {
|
||||
|
||||
Reference in New Issue
Block a user