Compare commits

...

4 Commits

Author SHA1 Message Date
Gitea Actions
c579543b8a ci: Bump version to 0.9.6 [skip ci] 2026-01-03 09:31:41 +05:00
0d84137786 test fixes
All checks were successful
Deploy to Test Environment / deploy-to-test (push) Successful in 23m17s
2026-01-02 20:31:08 -08:00
Gitea Actions
20ee30c4b4 ci: Bump version to 0.9.5 [skip ci] 2026-01-03 08:52:26 +05:00
93612137e3 test fixes
All checks were successful
Deploy to Test Environment / deploy-to-test (push) Successful in 25m23s
2026-01-02 19:51:10 -08:00
9 changed files with 40 additions and 19 deletions

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "flyer-crawler",
"version": "0.9.4",
"version": "0.9.6",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "flyer-crawler",
"version": "0.9.4",
"version": "0.9.6",
"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.9.4",
"version": "0.9.6",
"type": "module",
"scripts": {
"dev": "concurrently \"npm:start:dev\" \"vite\"",

View File

@@ -279,8 +279,8 @@ describe('AI Service (Server)', () => {
});
// Check second call
expect(mockGenerateContent).toHaveBeenNthCalledWith(2, { // The second model in the list is 'gemini-2.5-flash'
model: 'gemini-2.5-flash',
expect(mockGenerateContent).toHaveBeenNthCalledWith(2, { // The second model in the list is 'gemini-2.5-pro'
model: 'gemini-2.5-pro',
...request,
});

View File

@@ -21,6 +21,11 @@ describe('FlyerDataTransformer', () => {
beforeEach(() => {
vi.clearAllMocks();
transformer = new FlyerDataTransformer();
// Stub environment variables to ensure consistency and predictability.
// Prioritize FRONTEND_URL to match the updated service logic.
vi.stubEnv('FRONTEND_URL', 'http://localhost:3000');
vi.stubEnv('BASE_URL', ''); // Ensure this is not used to confirm priority logic
vi.stubEnv('PORT', ''); // Ensure this is not used
// Provide a default mock implementation for generateFlyerIcon
vi.mocked(generateFlyerIcon).mockResolvedValue('icon-flyer-page-1.webp');
@@ -70,6 +75,9 @@ describe('FlyerDataTransformer', () => {
mockLogger,
);
// Dynamically construct the expected base URL, mirroring the logic in the transformer.
const expectedBaseUrl = `http://localhost:3000`;
// Assert
// 0. Check logging
expect(mockLogger.info).toHaveBeenCalledWith(
@@ -83,8 +91,8 @@ describe('FlyerDataTransformer', () => {
// 1. Check flyer data
expect(flyerData).toEqual({
file_name: originalFileName,
image_url: `http://localhost:3000/flyer-images/flyer-page-1.jpg`,
icon_url: `http://localhost:3000/flyer-images/icons/icon-flyer-page-1.webp`,
image_url: `${expectedBaseUrl}/flyer-images/flyer-page-1.jpg`,
icon_url: `${expectedBaseUrl}/flyer-images/icons/icon-flyer-page-1.webp`,
checksum,
store_name: 'Test Store',
valid_from: '2024-01-01',
@@ -151,6 +159,9 @@ describe('FlyerDataTransformer', () => {
mockLogger,
);
// Dynamically construct the expected base URL, mirroring the logic in the transformer.
const expectedBaseUrl = `http://localhost:3000`;
// Assert
// 0. Check logging
expect(mockLogger.info).toHaveBeenCalledWith(
@@ -167,8 +178,8 @@ describe('FlyerDataTransformer', () => {
expect(itemsForDb).toHaveLength(0);
expect(flyerData).toEqual({
file_name: originalFileName,
image_url: `http://localhost:3000/flyer-images/another.png`,
icon_url: `http://localhost:3000/flyer-images/icons/icon-another.webp`,
image_url: `${expectedBaseUrl}/flyer-images/another.png`,
icon_url: `${expectedBaseUrl}/flyer-images/icons/icon-another.webp`,
checksum,
store_name: 'Unknown Store (auto)', // Should use fallback
valid_from: null,

View File

@@ -76,19 +76,25 @@ export class FlyerDataTransformer {
}
// Construct proper URLs including protocol and host to satisfy DB constraints
const baseUrl = process.env.BASE_URL || `http://localhost:${process.env.PORT || 3000}`;
const rawBaseUrl = process.env.FRONTEND_URL || process.env.BASE_URL || `http://localhost:${process.env.PORT || 3000}`;
// Normalize base URL by removing any trailing slash to prevent double slashes in the final URL,
// and replace the strict `new URL()` constructor to prevent exceptions in test environments.
const baseUrl = rawBaseUrl.endsWith('/') ? rawBaseUrl.slice(0, -1) : rawBaseUrl;
const flyerData: FlyerInsert = {
file_name: originalFileName,
image_url: new URL(`/flyer-images/${path.basename(firstImage)}`, baseUrl).href,
icon_url: new URL(`/flyer-images/icons/${iconFileName}`, baseUrl).href,
image_url: `${baseUrl}/flyer-images/${path.basename(firstImage)}`,
icon_url: `${baseUrl}/flyer-images/icons/${iconFileName}`,
checksum,
store_name: storeName,
valid_from: extractedData.valid_from,
valid_to: extractedData.valid_to,
store_address: extractedData.store_address, // The number of items is now calculated directly from the transformed data.
item_count: itemsForDb.length,
uploaded_by: userId,
// Defensively handle the userId. An empty string ('') is not a valid UUID,
// but `null` is. This ensures that any falsy value for userId (undefined, null, '')
// is converted to `null` for the database, preventing a 22P02 error.
uploaded_by: userId || null,
status: needsReview ? 'needs_review' : 'processed',
};

View File

@@ -174,6 +174,10 @@ describe('Authentication E2E Flow', () => {
expect(registerResponse.status).toBe(201);
createdUserIds.push(registerData.userprofile.user.user_id);
// Add a small delay to mitigate potential DB replication lag or race conditions
// where the user might not be found immediately after creation.
await new Promise((resolve) => setTimeout(resolve, 2000));
// Act 1: Request a password reset.
// The test environment returns the token directly in the response for E2E testing.
const forgotResponse = await apiClient.requestPasswordReset(email);

View File

@@ -89,7 +89,7 @@ describe('E2E Flyer Upload and Processing Workflow', () => {
// 5. Poll for job completion
let jobStatus;
const maxRetries = 30; // Poll for up to 90 seconds
const maxRetries = 60; // Poll for up to 180 seconds
for (let i = 0; i < maxRetries; i++) {
await new Promise((resolve) => setTimeout(resolve, 3000)); // Wait 3s
@@ -106,5 +106,5 @@ describe('E2E Flyer Upload and Processing Workflow', () => {
expect(jobStatus.state).toBe('completed');
flyerId = jobStatus.returnValue?.flyerId;
expect(flyerId).toBeTypeOf('number');
}, 120000); // Extended timeout for AI processing
}, 240000); // Extended timeout for AI processing
});

View File

@@ -223,7 +223,7 @@ describe('Flyer Processing Background Job Integration Test', () => {
// Poll for job completion
let jobStatus;
const maxRetries = 30; // Poll for up to 90 seconds
const maxRetries = 60; // Poll for up to 180 seconds
for (let i = 0; i < maxRetries; i++) {
await new Promise((resolve) => setTimeout(resolve, 3000));
const statusResponse = await request
@@ -309,7 +309,7 @@ describe('Flyer Processing Background Job Integration Test', () => {
// Poll for job completion
let jobStatus;
const maxRetries = 30;
const maxRetries = 60; // Poll for up to 180 seconds
for (let i = 0; i < maxRetries; i++) {
await new Promise((resolve) => setTimeout(resolve, 3000));
const statusResponse = await request

View File

@@ -101,7 +101,7 @@ describe('Gamification Flow Integration Test', () => {
// --- Act 2: Poll for job completion ---
let jobStatus;
const maxRetries = 30; // Poll for up to 90 seconds
const maxRetries = 60; // Poll for up to 180 seconds
for (let i = 0; i < maxRetries; i++) {
await new Promise((resolve) => setTimeout(resolve, 3000));
const statusResponse = await request
@@ -162,6 +162,6 @@ describe('Gamification Flow Integration Test', () => {
firstUploadAchievement!.points_value,
);
},
120000, // Increase timeout to 120 seconds for this long-running test
240000, // Increase timeout to 240s to match other long-running processing tests
);
});