Compare commits

...

2 Commits

Author SHA1 Message Date
Gitea Actions
99efca0371 ci: Bump version to 0.4.4 [skip ci] 2025-12-30 15:11:01 +05:00
1448950b81 fix unit tests
Some checks failed
Deploy to Test Environment / deploy-to-test (push) Failing after 42s
2025-12-30 02:10:29 -08:00
6 changed files with 49 additions and 66 deletions

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "flyer-crawler",
"version": "0.4.3",
"version": "0.4.4",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "flyer-crawler",
"version": "0.4.3",
"version": "0.4.4",
"dependencies": {
"@bull-board/api": "^6.14.2",
"@bull-board/express": "^6.14.2",

View File

@@ -1,7 +1,7 @@
{
"name": "flyer-crawler",
"private": true,
"version": "0.4.3",
"version": "0.4.4",
"type": "module",
"scripts": {
"dev": "concurrently \"npm:start:dev\" \"vite\"",

View File

@@ -4,6 +4,7 @@ import multer from 'multer';
import type { Request, Response, NextFunction } from 'express';
import { createUploadMiddleware, handleMulterError } from './multer.middleware';
import { createMockUserProfile } from '../tests/utils/mockFactories';
import { ValidationError } from '../services/db/errors.db';
// 1. Hoist the mocks so they can be referenced inside vi.mock factories.
const mocks = vi.hoisted(() => ({
@@ -217,9 +218,11 @@ describe('createUploadMiddleware', () => {
const cb = vi.fn();
const mockTextFile = { mimetype: 'text/plain' } as Express.Multer.File;
multerOptions!.fileFilter!({} as Request, mockTextFile, cb);
multerOptions!.fileFilter!({} as Request, { ...mockTextFile, fieldname: 'test' }, cb);
expect(cb).toHaveBeenCalledWith(new Error('Only image files are allowed!'));
const error = (cb as Mock).mock.calls[0][0];
expect(error).toBeInstanceOf(ValidationError);
expect(error.validationErrors[0].message).toBe('Only image files are allowed!');
});
});
});
@@ -248,13 +251,13 @@ describe('handleMulterError Middleware', () => {
expect(mockNext).not.toHaveBeenCalled();
});
it('should handle the custom image file filter error', () => {
// This test covers lines 59-61
const err = new Error('Only image files are allowed!');
it('should pass on a ValidationError to the next handler', () => {
const err = new ValidationError([], 'Only image files are allowed!');
handleMulterError(err, mockRequest as Request, mockResponse as Response, mockNext);
expect(mockResponse.status).toHaveBeenCalledWith(400);
expect(mockResponse.json).toHaveBeenCalledWith({ message: 'Only image files are allowed!' });
expect(mockNext).not.toHaveBeenCalled();
// It should now pass the error to the global error handler
expect(mockNext).toHaveBeenCalledWith(err);
expect(mockResponse.status).not.toHaveBeenCalled();
expect(mockResponse.json).not.toHaveBeenCalled();
});
it('should pass on non-multer errors to the next error handler', () => {

View File

@@ -5,6 +5,7 @@ import fs from 'node:fs/promises';
import { Request, Response, NextFunction } from 'express';
import { UserProfile } from '../types';
import { sanitizeFilename } from '../utils/stringUtils';
import { ValidationError } from '../services/db/errors.db';
import { logger } from '../services/logger.server';
export const flyerStoragePath =
@@ -69,8 +70,9 @@ const imageFileFilter = (req: Request, file: Express.Multer.File, cb: multer.Fil
cb(null, true);
} else {
// Reject the file with a specific error that can be caught by a middleware.
const err = new Error('Only image files are allowed!');
cb(err);
const validationIssue = { path: ['file', file.fieldname], message: 'Only image files are allowed!' };
const err = new ValidationError([validationIssue]);
cb(err as Error); // Cast to Error to satisfy multer's type, though ValidationError extends Error.
}
};
@@ -114,9 +116,6 @@ export const handleMulterError = (
if (err instanceof multer.MulterError) {
// A Multer error occurred when uploading (e.g., file too large).
return res.status(400).json({ message: `File upload error: ${err.message}` });
} else if (err && err.message === 'Only image files are allowed!') {
// A custom error from our fileFilter.
return res.status(400).json({ message: err.message });
}
// If it's not a multer error, pass it on.
next(err);

View File

@@ -1029,8 +1029,8 @@ describe('AI Service (Server)', () => {
// Verify that the error was caught and logged using errMsg logic
expect(mockLoggerInstance.warn).toHaveBeenCalledWith(
expect.objectContaining({ error: expect.any(String) }), // errMsg converts Error to string message
expect.stringContaining('Failed to parse parsed.data'),
expect.objectContaining({ error: expect.any(String) }),
'[AIService] Failed to parse nested "data" property string.',
);
});

View File

@@ -787,56 +787,37 @@ async enqueueFlyerProcessing(
logger: Logger,
): { parsed: FlyerProcessPayload; extractedData: Partial<ExtractedCoreData> | null | undefined } {
let parsed: FlyerProcessPayload = {};
let extractedData: Partial<ExtractedCoreData> | null | undefined = {};
try {
if (body && (body.data || body.extractedData)) {
const raw = body.data ?? body.extractedData;
try {
parsed = typeof raw === 'string' ? JSON.parse(raw) : raw;
} catch (err) {
logger.warn(
{ error: errMsg(err) },
'[AIService] Failed to JSON.parse raw extractedData; falling back to direct assign',
);
parsed = (
typeof raw === 'string' ? JSON.parse(String(raw).slice(0, 2000)) : raw
) as FlyerProcessPayload;
}
extractedData = 'extractedData' in parsed ? parsed.extractedData : (parsed as Partial<ExtractedCoreData>);
} else {
try {
parsed = typeof body === 'string' ? JSON.parse(body) : body;
} catch (err) {
logger.warn(
{ error: errMsg(err) },
'[AIService] Failed to JSON.parse req.body; using empty object',
);
parsed = (body as FlyerProcessPayload) || {};
}
if (parsed.data) {
try {
const inner = typeof parsed.data === 'string' ? JSON.parse(parsed.data) : parsed.data;
extractedData = inner.extractedData ?? inner;
} catch (err) {
logger.warn({ error: errMsg(err) }, '[AIService] Failed to parse parsed.data; falling back');
extractedData = parsed.data as unknown as Partial<ExtractedCoreData>;
}
} else if (parsed.extractedData) {
extractedData = parsed.extractedData;
} else {
if ('items' in parsed || 'store_name' in parsed || 'valid_from' in parsed) {
extractedData = parsed as Partial<ExtractedCoreData>;
} else {
extractedData = {};
}
}
}
} catch (err) {
logger.error({ error: err }, '[AIService] Unexpected error while parsing legacy request body');
parsed = {};
extractedData = {};
parsed = typeof body === 'string' ? JSON.parse(body) : body || {};
} catch (e) {
logger.warn({ error: errMsg(e) }, '[AIService] Failed to parse top-level request body string.');
return { parsed: {}, extractedData: {} };
}
return { parsed, extractedData };
// If the real payload is nested inside a 'data' property (which could be a string),
// we parse it out but keep the original `parsed` object for top-level properties like checksum.
let potentialPayload: FlyerProcessPayload = parsed;
if (parsed.data) {
if (typeof parsed.data === 'string') {
try {
potentialPayload = JSON.parse(parsed.data);
} catch (e) {
logger.warn({ error: errMsg(e) }, '[AIService] Failed to parse nested "data" property string.');
}
} else if (typeof parsed.data === 'object') {
potentialPayload = parsed.data;
}
}
// The extracted data is either in an `extractedData` key or is the payload itself.
const extractedData = potentialPayload.extractedData ?? potentialPayload;
// Merge for checksum lookup: properties in the outer `parsed` object (like a top-level checksum)
// take precedence over any same-named properties inside `potentialPayload`.
const finalParsed = { ...potentialPayload, ...parsed };
return { parsed: finalParsed, extractedData };
}
async processLegacyFlyerUpload(