Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ab63f83f50 | ||
| b546a55eaf | |||
|
|
dfa53a93dd | ||
| f30464cd0e |
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "flyer-crawler",
|
||||
"version": "0.9.56",
|
||||
"version": "0.9.58",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "flyer-crawler",
|
||||
"version": "0.9.56",
|
||||
"version": "0.9.58",
|
||||
"dependencies": {
|
||||
"@bull-board/api": "^6.14.2",
|
||||
"@bull-board/express": "^6.14.2",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "flyer-crawler",
|
||||
"private": true,
|
||||
"version": "0.9.56",
|
||||
"version": "0.9.58",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "concurrently \"npm:start:dev\" \"vite\"",
|
||||
|
||||
@@ -63,21 +63,36 @@ export class FlyerRepository {
|
||||
* @returns The newly created flyer record with its ID.
|
||||
*/
|
||||
async insertFlyer(flyerData: FlyerDbInsert, logger: Logger): Promise<Flyer> {
|
||||
console.error('[DEBUG] FlyerRepository.insertFlyer called with:', JSON.stringify(flyerData, null, 2));
|
||||
try {
|
||||
// Sanitize icon_url: Ensure empty strings become NULL to avoid regex constraint violations
|
||||
let iconUrl = flyerData.icon_url && flyerData.icon_url.trim() !== '' ? flyerData.icon_url : null;
|
||||
let imageUrl = flyerData.image_url;
|
||||
console.error('[DB DEBUG] FlyerRepository.insertFlyer called with:', JSON.stringify(flyerData, null, 2));
|
||||
// Sanitize icon_url: Ensure empty strings become NULL to avoid regex constraint violations
|
||||
let iconUrl = flyerData.icon_url && flyerData.icon_url.trim() !== '' ? flyerData.icon_url : null;
|
||||
let imageUrl = flyerData.image_url || 'placeholder.jpg';
|
||||
|
||||
try {
|
||||
// Fallback for tests/workers sending relative URLs to satisfy DB 'url_check' constraint
|
||||
const baseUrl = process.env.FRONTEND_URL || 'https://example.com';
|
||||
const rawBaseUrl = process.env.FRONTEND_URL || 'https://example.com';
|
||||
const baseUrl = rawBaseUrl.endsWith('/') ? rawBaseUrl.slice(0, -1) : rawBaseUrl;
|
||||
|
||||
// [DEBUG] Log URL transformation for debugging test failures
|
||||
if ((imageUrl && !imageUrl.startsWith('http')) || (iconUrl && !iconUrl.startsWith('http'))) {
|
||||
console.error('[DB DEBUG] Transforming relative URLs:', {
|
||||
baseUrl,
|
||||
originalImage: imageUrl,
|
||||
originalIcon: iconUrl,
|
||||
});
|
||||
}
|
||||
|
||||
if (imageUrl && !imageUrl.startsWith('http')) {
|
||||
imageUrl = `${baseUrl}${imageUrl.startsWith('/') ? '' : '/'}${imageUrl}`;
|
||||
const cleanPath = imageUrl.startsWith('/') ? imageUrl.substring(1) : imageUrl;
|
||||
imageUrl = `${baseUrl}/${cleanPath}`;
|
||||
}
|
||||
if (iconUrl && !iconUrl.startsWith('http')) {
|
||||
iconUrl = `${baseUrl}${iconUrl.startsWith('/') ? '' : '/'}${iconUrl}`;
|
||||
const cleanPath = iconUrl.startsWith('/') ? iconUrl.substring(1) : iconUrl;
|
||||
iconUrl = `${baseUrl}/${cleanPath}`;
|
||||
}
|
||||
|
||||
console.error('[DB DEBUG] Final URLs for insert:', { imageUrl, iconUrl });
|
||||
|
||||
const query = `
|
||||
INSERT INTO flyers (
|
||||
file_name, image_url, icon_url, checksum, store_id, valid_from, valid_to, store_address,
|
||||
@@ -108,6 +123,7 @@ export class FlyerRepository {
|
||||
const result = await this.db.query<Flyer>(query, values);
|
||||
return result.rows[0];
|
||||
} catch (error) {
|
||||
console.error('[DB DEBUG] insertFlyer caught error:', error);
|
||||
const errorMessage = error instanceof Error ? error.message : '';
|
||||
let checkMsg = 'A database check constraint failed.';
|
||||
|
||||
@@ -132,7 +148,7 @@ export class FlyerRepository {
|
||||
} else if (errorMessage.includes('flyers_status_check')) {
|
||||
checkMsg = 'Invalid status provided for flyer.';
|
||||
} else if (errorMessage.includes('url_check')) {
|
||||
checkMsg = 'Invalid URL format provided for image or icon.';
|
||||
checkMsg = `[URL_CHECK_FAIL] Invalid URL format. Image: '${imageUrl}', Icon: '${iconUrl}'`;
|
||||
}
|
||||
|
||||
handleDbError(error, logger, 'Database error in insertFlyer', { flyerData }, {
|
||||
|
||||
@@ -182,7 +182,8 @@ class UserService {
|
||||
try {
|
||||
await db.userRepo.deleteUserById(userToDeleteId, log);
|
||||
} catch (error) {
|
||||
if (error instanceof ValidationError) {
|
||||
// Rethrow known errors so they are handled correctly by the API layer (e.g. 404 for NotFound)
|
||||
if (error instanceof ValidationError || error instanceof NotFoundError) {
|
||||
throw error;
|
||||
}
|
||||
const errorMessage = error instanceof Error ? error.message : 'An unknown error occurred.';
|
||||
|
||||
@@ -26,6 +26,39 @@ vi.mock('../../utils/imageProcessor', async () => {
|
||||
};
|
||||
});
|
||||
|
||||
// FIX: Mock storageService to return valid URLs (for DB) and write files to disk (for test verification)
|
||||
vi.mock('../../services/storage/storageService', () => {
|
||||
const fs = require('node:fs/promises');
|
||||
const path = require('path');
|
||||
// Match the directory used in the test helpers
|
||||
const uploadDir = path.join(process.cwd(), 'flyer-images');
|
||||
|
||||
return {
|
||||
storageService: {
|
||||
upload: vi.fn().mockImplementation(async (fileData, fileName) => {
|
||||
const name = fileName || (fileData && fileData.name) || (typeof fileData === 'string' ? path.basename(fileData) : `upload-${Date.now()}.jpg`);
|
||||
|
||||
await fs.mkdir(uploadDir, { recursive: true });
|
||||
const destPath = path.join(uploadDir, name);
|
||||
|
||||
let content = Buffer.from('');
|
||||
if (Buffer.isBuffer(fileData)) {
|
||||
content = fileData as any;
|
||||
} else if (typeof fileData === 'string') {
|
||||
try { content = await fs.readFile(fileData); } catch (e) {}
|
||||
} else if (fileData && fileData.path) {
|
||||
try { content = await fs.readFile(fileData.path); } catch (e) {}
|
||||
}
|
||||
await fs.writeFile(destPath, content);
|
||||
|
||||
// Return a valid URL to satisfy the 'url_check' DB constraint
|
||||
return `https://example.com/uploads/${name}`;
|
||||
}),
|
||||
delete: vi.fn().mockResolvedValue(undefined),
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
// FIX: Import the singleton instance directly to spy on it
|
||||
import { aiService } from '../../services/aiService.server';
|
||||
|
||||
@@ -64,7 +97,7 @@ describe('Flyer Processing Background Job Integration Test', () => {
|
||||
// IMPORTANT: This must run BEFORE the app is imported so workers inherit the env var.
|
||||
vi.stubEnv('FRONTEND_URL', 'https://example.com');
|
||||
process.env.FRONTEND_URL = 'https://example.com';
|
||||
console.log('[TEST SETUP] FRONTEND_URL stubbed to:', process.env.FRONTEND_URL);
|
||||
console.error('[TEST SETUP] FRONTEND_URL stubbed to:', process.env.FRONTEND_URL);
|
||||
|
||||
// FIX: Spy on the actual singleton instance. This ensures that when the worker
|
||||
// imports 'aiService', it gets the instance we are controlling here.
|
||||
@@ -72,7 +105,7 @@ describe('Flyer Processing Background Job Integration Test', () => {
|
||||
|
||||
// NEW: Import workers to start them IN-PROCESS.
|
||||
// This ensures they run in the same memory space as our mocks.
|
||||
console.log('[TEST SETUP] Starting in-process workers...');
|
||||
console.error('[TEST SETUP] Starting in-process workers...');
|
||||
workersModule = await import('../../services/workers.server');
|
||||
|
||||
const appModule = await import('../../../server');
|
||||
@@ -83,7 +116,7 @@ describe('Flyer Processing Background Job Integration Test', () => {
|
||||
// FIX: Reset mocks before each test to ensure isolation.
|
||||
// This prevents "happy path" mocks from leaking into error handling tests and vice versa.
|
||||
beforeEach(async () => {
|
||||
console.log('[TEST SETUP] Resetting mocks before test execution');
|
||||
console.error('[TEST SETUP] Resetting mocks before test execution');
|
||||
// 1. Reset AI Service Mock to default success state
|
||||
mockExtractCoreData.mockReset();
|
||||
mockExtractCoreData.mockResolvedValue({
|
||||
@@ -129,7 +162,7 @@ describe('Flyer Processing Background Job Integration Test', () => {
|
||||
|
||||
// NEW: Clean up workers and Redis connection to prevent tests from hanging.
|
||||
if (workersModule) {
|
||||
console.log('[TEST TEARDOWN] Closing in-process workers...');
|
||||
console.error('[TEST TEARDOWN] Closing in-process workers...');
|
||||
await workersModule.closeWorkers();
|
||||
}
|
||||
|
||||
@@ -143,9 +176,9 @@ describe('Flyer Processing Background Job Integration Test', () => {
|
||||
* It uploads a file, polls for completion, and verifies the result in the database.
|
||||
*/
|
||||
const runBackgroundProcessingTest = async (user?: UserProfile, token?: string) => {
|
||||
console.log(`[TEST START] runBackgroundProcessingTest. User: ${user?.user.email ?? 'ANONYMOUS'}`);
|
||||
console.error(`[TEST START] runBackgroundProcessingTest. User: ${user?.user.email ?? 'ANONYMOUS'}`);
|
||||
// Arrange: Load a mock flyer PDF.
|
||||
console.log('[TEST] about to read test-flyer-image.jpg')
|
||||
console.error('[TEST] about to read test-flyer-image.jpg')
|
||||
|
||||
const imagePath = path.resolve(__dirname, '../assets/test-flyer-image.jpg');
|
||||
const imageBuffer = await fs.readFile(imagePath);
|
||||
@@ -155,20 +188,20 @@ describe('Flyer Processing Background Job Integration Test', () => {
|
||||
const uniqueFileName = `test-flyer-image-${Date.now()}.jpg`;
|
||||
const mockImageFile = new File([new Uint8Array(uniqueContent)], uniqueFileName, { type: 'image/jpeg' });
|
||||
const checksum = await generateFileChecksum(mockImageFile);
|
||||
console.log('[TEST] mockImageFile created with uniqueFileName: ', uniqueFileName)
|
||||
console.log('[TEST DATA] Generated checksum for test:', checksum);
|
||||
console.error('[TEST] mockImageFile created with uniqueFileName: ', uniqueFileName)
|
||||
console.error('[TEST DATA] Generated checksum for test:', checksum);
|
||||
|
||||
// Track created files for cleanup
|
||||
const uploadDir = path.resolve(__dirname, '../../../flyer-images');
|
||||
createdFilePaths.push(path.join(uploadDir, uniqueFileName));
|
||||
console.log('[TEST] createdFilesPaths after 1st push: ', createdFilePaths)
|
||||
console.error('[TEST] createdFilesPaths after 1st push: ', createdFilePaths)
|
||||
// The icon name is derived from the original filename.
|
||||
const iconFileName = `icon-${path.parse(uniqueFileName).name}.webp`;
|
||||
createdFilePaths.push(path.join(uploadDir, 'icons', iconFileName));
|
||||
|
||||
// Act 1: Upload the file to start the background job.
|
||||
const testBaseUrl = 'https://example.com';
|
||||
console.log('[TEST ACTION] Uploading file with baseUrl:', testBaseUrl);
|
||||
console.error('[TEST ACTION] Uploading file with baseUrl:', testBaseUrl);
|
||||
|
||||
const uploadReq = request
|
||||
.post('/api/ai/upload-and-process')
|
||||
@@ -181,8 +214,8 @@ describe('Flyer Processing Background Job Integration Test', () => {
|
||||
uploadReq.set('Authorization', `Bearer ${token}`);
|
||||
}
|
||||
const uploadResponse = await uploadReq;
|
||||
console.log('[TEST RESPONSE] Upload status:', uploadResponse.status);
|
||||
console.log('[TEST RESPONSE] Upload body:', JSON.stringify(uploadResponse.body));
|
||||
console.error('[TEST RESPONSE] Upload status:', uploadResponse.status);
|
||||
console.error('[TEST RESPONSE] Upload body:', JSON.stringify(uploadResponse.body));
|
||||
const { jobId } = uploadResponse.body;
|
||||
|
||||
// Assert 1: Check that a job ID was returned.
|
||||
@@ -196,7 +229,7 @@ describe('Flyer Processing Background Job Integration Test', () => {
|
||||
statusReq.set('Authorization', `Bearer ${token}`);
|
||||
}
|
||||
const statusResponse = await statusReq;
|
||||
console.log(`[TEST POLL] Job ${jobId} current state:`, statusResponse.body?.state);
|
||||
console.error(`[TEST POLL] Job ${jobId} current state:`, statusResponse.body?.state);
|
||||
return statusResponse.body;
|
||||
},
|
||||
(status) => status.state === 'completed' || status.state === 'failed',
|
||||
@@ -319,6 +352,8 @@ describe('Flyer Processing Background Job Integration Test', () => {
|
||||
// 3. Assert
|
||||
if (jobStatus?.state === 'failed') {
|
||||
console.error('[DEBUG] EXIF test job failed:', jobStatus.failedReason);
|
||||
console.error('[DEBUG] Job stack trace:', jobStatus.stacktrace);
|
||||
console.error('[DEBUG] Job return value:', JSON.stringify(jobStatus.returnValue, null, 2));
|
||||
}
|
||||
expect(jobStatus?.state).toBe('completed');
|
||||
const flyerId = jobStatus?.returnValue?.flyerId;
|
||||
@@ -336,8 +371,8 @@ describe('Flyer Processing Background Job Integration Test', () => {
|
||||
const parser = exifParser.create(savedImageBuffer);
|
||||
const exifResult = parser.parse();
|
||||
|
||||
console.log('[TEST] savedImagePath during EXIF data stripping: ', savedImagePath)
|
||||
console.log('[TEST] exifResult.tags: ', exifResult.tags)
|
||||
console.error('[TEST] savedImagePath during EXIF data stripping: ', savedImagePath)
|
||||
console.error('[TEST] exifResult.tags: ', exifResult.tags)
|
||||
|
||||
|
||||
// The `tags` object will be empty if no EXIF data is found.
|
||||
@@ -408,6 +443,8 @@ describe('Flyer Processing Background Job Integration Test', () => {
|
||||
// 3. Assert job completion
|
||||
if (jobStatus?.state === 'failed') {
|
||||
console.error('[DEBUG] PNG metadata test job failed:', jobStatus.failedReason);
|
||||
console.error('[DEBUG] Job stack trace:', jobStatus.stacktrace);
|
||||
console.error('[DEBUG] Job return value:', JSON.stringify(jobStatus.returnValue, null, 2));
|
||||
}
|
||||
expect(jobStatus?.state).toBe('completed');
|
||||
const flyerId = jobStatus?.returnValue?.flyerId;
|
||||
@@ -421,7 +458,7 @@ describe('Flyer Processing Background Job Integration Test', () => {
|
||||
const savedImagePath = path.join(uploadDir, path.basename(savedFlyer!.image_url));
|
||||
createdFilePaths.push(savedImagePath); // Add final path for cleanup
|
||||
|
||||
console.log('[TEST] savedImagePath during PNG metadata stripping: ', savedImagePath)
|
||||
console.error('[TEST] savedImagePath during PNG metadata stripping: ', savedImagePath)
|
||||
|
||||
|
||||
const savedImageMetadata = await sharp(savedImagePath).metadata();
|
||||
@@ -475,6 +512,10 @@ it(
|
||||
);
|
||||
|
||||
// Assert 1: Check that the job failed.
|
||||
if (jobStatus?.state === 'failed') {
|
||||
console.error('[TEST DEBUG] AI Failure Test - Job Failed Reason:', jobStatus.failedReason);
|
||||
console.error('[TEST DEBUG] AI Failure Test - Job Stack:', jobStatus.stacktrace);
|
||||
}
|
||||
expect(jobStatus?.state).toBe('failed');
|
||||
expect(jobStatus?.failedReason).toContain('AI model failed to extract data.');
|
||||
|
||||
|
||||
@@ -183,6 +183,8 @@ describe('Gamification Flow Integration Test', () => {
|
||||
// --- Assert 1: Verify the job completed successfully ---
|
||||
if (jobStatus?.state === 'failed') {
|
||||
console.error('[DEBUG] Gamification test job failed:', jobStatus.failedReason);
|
||||
console.error('[DEBUG] Job stack trace:', jobStatus.stacktrace);
|
||||
console.error('[DEBUG] Job return value:', JSON.stringify(jobStatus.returnValue, null, 2));
|
||||
}
|
||||
expect(jobStatus?.state).toBe('completed');
|
||||
|
||||
|
||||
Reference in New Issue
Block a user