diff --git a/src/routes/admin.test.ts b/src/routes/admin.test.ts index 33061383..57da799b 100644 --- a/src/routes/admin.test.ts +++ b/src/routes/admin.test.ts @@ -4,7 +4,7 @@ import supertest from 'supertest'; import express, { Request, Response, NextFunction } from 'express'; import path from 'node:path'; import fs from 'node:fs/promises'; -import adminRouter from './admin'; +import adminRouter from './admin'; // Correctly imported import * as db from '../services/db'; import { UserProfile } from '../types'; @@ -378,6 +378,18 @@ describe('Admin Routes (/api/admin)', () => { await fs.unlink(dummyFilePath); }); + // Add a cleanup hook to remove the file created on the server by multer + afterAll(async () => { + const uploadDir = path.resolve(__dirname, '../../../flyer-images'); + try { + const files = await fs.readdir(uploadDir); + const testFiles = files.filter(f => f.startsWith('logoImage-')); + for (const file of testFiles) { + await fs.unlink(path.join(uploadDir, file)); + } + } catch (error) { console.error('Error during admin test file cleanup:', error); } + }); + it('should return a 400 error if no logo image is provided', async () => { // Arrange const brandId = 56; diff --git a/src/routes/admin.ts b/src/routes/admin.ts index 1b32411d..3b095c02 100644 --- a/src/routes/admin.ts +++ b/src/routes/admin.ts @@ -18,7 +18,7 @@ import { flyerQueue, emailQueue, analyticsQueue } from '../services/queueService const router = Router(); // --- Multer Configuration for File Uploads --- -const storagePath = process.env.STORAGE_PATH || '/var/www/flyer-crawler.projectium.com/assets'; +const storagePath = process.env.STORAGE_PATH || '/var/www/flyer-crawler.projectium.com/flyer-images'; const storage = multer.diskStorage({ destination: function (req, file, cb) { cb(null, storagePath); diff --git a/src/tests/integration/ai.integration.test.ts b/src/tests/integration/ai.integration.test.ts index 2eaf5dbd..f2db5780 100644 --- a/src/tests/integration/ai.integration.test.ts +++ b/src/tests/integration/ai.integration.test.ts @@ -1,7 +1,9 @@ // src/tests/integration/ai.integration.test.ts -import { describe, it, expect, beforeAll } from 'vitest'; +import { describe, it, expect, beforeAll, afterAll } from 'vitest'; import * as apiClient from '../../services/apiClient'; import * as aiApiClient from '../../services/aiApiClient'; +import fs from 'fs/promises'; +import path from 'path'; /** * @vitest-environment node @@ -32,6 +34,21 @@ describe('AI API Routes Integration Tests', () => { authToken = loginToken; }); + afterAll(async () => { + // Clean up any files created in the flyer-images directory during these tests. + const uploadDir = path.resolve(__dirname, '../../../flyer-images'); + try { + const files = await fs.readdir(uploadDir); + // Target files created by the 'image' and 'images' multer instances. + const testFiles = files.filter(f => f.startsWith('image-') || f.startsWith('images-')); + for (const file of testFiles) { + await fs.unlink(path.join(uploadDir, file)); + } + } catch (error) { + console.error('Error during AI integration test file cleanup:', error); + } + }); + it('POST /api/ai/check-flyer should return a boolean', async () => { const mockImageFile = new File(['content'], 'test.jpg', { type: 'image/jpeg' }); const response = await aiApiClient.isImageAFlyer(mockImageFile, authToken); diff --git a/src/tests/integration/flyer-processing.integration.test.ts b/src/tests/integration/flyer-processing.integration.test.ts index 308eab41..c4a636e3 100644 --- a/src/tests/integration/flyer-processing.integration.test.ts +++ b/src/tests/integration/flyer-processing.integration.test.ts @@ -1,6 +1,6 @@ // src/tests/integration/flyer-processing.integration.test.ts import { describe, it, expect, beforeAll, afterAll } from 'vitest'; -import fs from 'fs'; +import fs from 'fs/promises'; import path from 'path'; import * as apiClient from '../../services/apiClient'; import * as aiApiClient from '../../services/aiApiClient'; @@ -46,6 +46,21 @@ describe('Flyer Processing Background Job Integration Test', () => { if (createdUserIds.length > 0) { await getPool().query('DELETE FROM public.users WHERE user_id = ANY($1::uuid[])', [createdUserIds]); } + + // Clean up any files created in the flyer-images directory during tests. + const uploadDir = path.resolve(__dirname, '../../../flyer-images'); + try { + const files = await fs.readdir(uploadDir); + // Use a more specific filter to only target files created by this test suite. + const testFiles = files.filter(f => f.includes('test-flyer-image')); + for (const file of testFiles) { + await fs.unlink(path.join(uploadDir, file)); + // Also try to remove from the icons subdirectory + await fs.unlink(path.join(uploadDir, 'icons', `icon-${file}`)).catch(() => {}); + } + } catch (error) { + console.error('Error during test file cleanup:', error); + } }); /** @@ -55,7 +70,7 @@ describe('Flyer Processing Background Job Integration Test', () => { const runBackgroundProcessingTest = async (user?: User, token?: string) => { // Arrange: Load a mock flyer PDF. const imagePath = path.resolve(__dirname, '../assets/test-flyer-image.jpg'); - const imageBuffer = fs.readFileSync(imagePath); + const imageBuffer = await fs.readFile(imagePath); const mockImageFile = new File([imageBuffer], 'test-flyer-image.jpg', { type: 'image/jpeg' }); const checksum = await generateFileChecksum(mockImageFile); diff --git a/src/utils/imageProcessor.ts b/src/utils/imageProcessor.ts index 20197174..cfb11e06 100644 --- a/src/utils/imageProcessor.ts +++ b/src/utils/imageProcessor.ts @@ -13,16 +13,24 @@ import { logger } from '../services/logger.server'; */ export async function generateFlyerIcon(sourceImagePath: string, iconsDirectory: string): Promise { try { - const originalFileName = path.basename(sourceImagePath); - const iconFileName = `icon-${originalFileName}`; + // 1. Create a new filename, standardizing the extension to .webp for consistency and performance. + // We parse the original filename to remove its extension before adding the new one. + const originalFileName = path.basename(sourceImagePath, path.extname(sourceImagePath)); + const iconFileName = `icon-${originalFileName}.webp`; const iconOutputPath = path.join(iconsDirectory, iconFileName); // Ensure the icons subdirectory exists. await fs.mkdir(iconsDirectory, { recursive: true }); - // Use sharp to resize the image to 64x64. + // 2. Use sharp to process the image. await sharp(sourceImagePath) - .resize(64, 64) + // Use `resize` with a `fit` strategy to prevent distortion. + // `sharp.fit.cover` will resize to fill 64x64 and crop any excess, + // ensuring the icon is always a non-distorted square. + .resize(64, 64, { fit: sharp.fit.cover }) + // 3. Convert the output to WebP format. + // The `quality` option is a good balance between size and clarity. + .webp({ quality: 80 }) .toFile(iconOutputPath); logger.info(`Generated 64x64 icon: ${iconFileName}`);