152 lines
6.1 KiB
TypeScript
152 lines
6.1 KiB
TypeScript
// src/tests/integration/flyer-processing.integration.test.ts
|
|
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
|
import supertest from 'supertest';
|
|
import app from '../../../server';
|
|
import fs from 'node:fs/promises';
|
|
import path from 'path';
|
|
import * as db from '../../services/db/index.db';
|
|
import { getPool } from '../../services/db/connection.db';
|
|
import { generateFileChecksum } from '../../utils/checksum';
|
|
import { logger } from '../../services/logger.server';
|
|
import type { UserProfile } from '../../types';
|
|
import { createAndLoginUser } from '../utils/testHelpers';
|
|
|
|
/**
|
|
* @vitest-environment node
|
|
*/
|
|
|
|
const request = supertest(app);
|
|
|
|
describe('Flyer Processing Background Job Integration Test', () => {
|
|
const createdUserIds: string[] = [];
|
|
const createdFlyerIds: number[] = [];
|
|
|
|
beforeAll(async () => {
|
|
// This setup is now simpler as the worker handles fetching master items.
|
|
});
|
|
|
|
afterAll(async () => {
|
|
// Clean up all entities created during the tests using their collected IDs.
|
|
// This is safer than using LIKE queries.
|
|
if (createdFlyerIds.length > 0) {
|
|
await getPool().query('DELETE FROM public.flyers WHERE flyer_id = ANY($1::bigint[])', [
|
|
createdFlyerIds,
|
|
]);
|
|
}
|
|
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);
|
|
}
|
|
});
|
|
|
|
/**
|
|
* This is the new end-to-end test for the background job processing flow.
|
|
* It uploads a file, polls for completion, and verifies the result in the database.
|
|
*/
|
|
const runBackgroundProcessingTest = async (user?: UserProfile, token?: string) => {
|
|
// Arrange: Load a mock flyer PDF.
|
|
const imagePath = path.resolve(__dirname, '../assets/test-flyer-image.jpg');
|
|
const imageBuffer = await fs.readFile(imagePath);
|
|
// Create a unique buffer and filename for each test run to ensure a unique checksum.
|
|
// This prevents a 409 Conflict error when the second test runs.
|
|
const uniqueContent = Buffer.concat([imageBuffer, Buffer.from(Date.now().toString())]);
|
|
const uniqueFileName = `test-flyer-image-${Date.now()}.jpg`;
|
|
const mockImageFile = new File([uniqueContent], uniqueFileName, { type: 'image/jpeg' });
|
|
const checksum = await generateFileChecksum(mockImageFile);
|
|
|
|
// Act 1: Upload the file to start the background job.
|
|
const uploadReq = request
|
|
.post('/api/ai/upload-and-process')
|
|
.field('checksum', checksum)
|
|
.attach('flyerFile', uniqueContent, uniqueFileName);
|
|
if (token) {
|
|
uploadReq.set('Authorization', `Bearer ${token}`);
|
|
}
|
|
const uploadResponse = await uploadReq;
|
|
const { jobId } = uploadResponse.body;
|
|
|
|
// Assert 1: Check that a job ID was returned.
|
|
expect(jobId).toBeTypeOf('string');
|
|
|
|
// Act 2: Poll for the job status until it completes.
|
|
let jobStatus;
|
|
const maxRetries = 30; // Poll for up to 90 seconds (30 * 3s)
|
|
for (let i = 0; i < maxRetries; i++) {
|
|
await new Promise((resolve) => setTimeout(resolve, 3000)); // Wait 3 seconds between polls
|
|
const statusReq = request.get(`/api/ai/jobs/${jobId}/status`);
|
|
if (token) {
|
|
statusReq.set('Authorization', `Bearer ${token}`);
|
|
}
|
|
const statusResponse = await statusReq;
|
|
jobStatus = statusResponse.body;
|
|
if (jobStatus.state === 'completed' || jobStatus.state === 'failed') {
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Assert 2: Check that the job completed successfully.
|
|
expect(jobStatus?.state).toBe('completed');
|
|
const flyerId = jobStatus?.returnValue?.flyerId;
|
|
expect(flyerId).toBeTypeOf('number');
|
|
createdFlyerIds.push(flyerId); // Track for cleanup
|
|
|
|
// Assert 3: Verify the flyer and its items were actually saved in the database.
|
|
const savedFlyer = await db.flyerRepo.findFlyerByChecksum(checksum, logger);
|
|
expect(savedFlyer).toBeDefined();
|
|
expect(savedFlyer?.flyer_id).toBe(flyerId);
|
|
|
|
const items = await db.flyerRepo.getFlyerItems(flyerId, logger);
|
|
// The stubbed AI response returns items, so we expect them to be here.
|
|
expect(items.length).toBeGreaterThan(0);
|
|
expect(items[0].item).toBeTypeOf('string');
|
|
|
|
// Assert 4: Verify user association is correct.
|
|
if (token) {
|
|
expect(savedFlyer?.uploaded_by).toBe(user?.user.user_id);
|
|
} else {
|
|
expect(savedFlyer?.uploaded_by).toBe(null);
|
|
}
|
|
};
|
|
|
|
it('should successfully process a flyer for an AUTHENTICATED user via the background queue', async ({
|
|
onTestFinished,
|
|
}) => {
|
|
// Arrange: Create a new user specifically for this test.
|
|
const email = `auth-flyer-user-${Date.now()}@example.com`;
|
|
const { user: authUser, token } = await createAndLoginUser({
|
|
email,
|
|
fullName: 'Flyer Uploader',
|
|
});
|
|
createdUserIds.push(authUser.user.user_id); // Track for cleanup
|
|
|
|
// Use a cleanup function to delete the user even if the test fails.
|
|
onTestFinished(async () => {
|
|
await getPool().query('DELETE FROM public.users WHERE user_id = $1', [authUser.user.user_id]);
|
|
});
|
|
|
|
// Act & Assert
|
|
await runBackgroundProcessingTest(authUser, token);
|
|
}, 120000); // Increase timeout to 120 seconds for this long-running test
|
|
|
|
it('should successfully process a flyer for an ANONYMOUS user via the background queue', async () => {
|
|
// Act & Assert: Call the test helper without a user or token.
|
|
await runBackgroundProcessingTest();
|
|
}, 120000); // Increase timeout to 120 seconds for this long-running test
|
|
});
|