Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e2049c6b9f | ||
| a3839c2f0d |
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "flyer-crawler",
|
||||
"version": "0.9.46",
|
||||
"version": "0.9.47",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "flyer-crawler",
|
||||
"version": "0.9.46",
|
||||
"version": "0.9.47",
|
||||
"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.46",
|
||||
"version": "0.9.47",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "concurrently \"npm:start:dev\" \"vite\"",
|
||||
|
||||
@@ -360,6 +360,58 @@ describe('Flyer DB Service', () => {
|
||||
'Database error in insertFlyerItems',
|
||||
);
|
||||
});
|
||||
|
||||
it('should sanitize empty or whitespace-only price_display to null', async () => {
|
||||
const itemsData: FlyerItemInsert[] = [
|
||||
{
|
||||
item: 'Free Item',
|
||||
price_display: '', // Empty string
|
||||
price_in_cents: 0,
|
||||
quantity: '1',
|
||||
category_name: 'Promo',
|
||||
view_count: 0,
|
||||
click_count: 0,
|
||||
},
|
||||
{
|
||||
item: 'Whitespace Item',
|
||||
price_display: ' ', // Whitespace only
|
||||
price_in_cents: null,
|
||||
quantity: '1',
|
||||
category_name: 'Promo',
|
||||
view_count: 0,
|
||||
click_count: 0,
|
||||
},
|
||||
];
|
||||
const mockItems = itemsData.map((item, i) =>
|
||||
createMockFlyerItem({ ...item, flyer_item_id: i + 1, flyer_id: 1 }),
|
||||
);
|
||||
mockPoolInstance.query.mockResolvedValue({ rows: mockItems });
|
||||
|
||||
await flyerRepo.insertFlyerItems(1, itemsData, mockLogger);
|
||||
|
||||
expect(mockPoolInstance.query).toHaveBeenCalledTimes(1);
|
||||
|
||||
// Check that the values array passed to the query has null for price_display
|
||||
const queryValues = mockPoolInstance.query.mock.calls[0][1];
|
||||
expect(queryValues).toEqual([
|
||||
1, // flyerId for item 1
|
||||
'Free Item',
|
||||
null, // Sanitized price_display for item 1
|
||||
0,
|
||||
'1',
|
||||
'Promo',
|
||||
0,
|
||||
0,
|
||||
1, // flyerId for item 2
|
||||
'Whitespace Item',
|
||||
null, // Sanitized price_display for item 2
|
||||
null,
|
||||
'1',
|
||||
'Promo',
|
||||
0,
|
||||
0,
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('createFlyerAndItems', () => {
|
||||
@@ -433,6 +485,34 @@ describe('Flyer DB Service', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('should create a flyer with no items if items array is empty', async () => {
|
||||
const flyerData: FlyerInsert = {
|
||||
file_name: 'empty.jpg',
|
||||
store_name: 'Empty Store',
|
||||
} as FlyerInsert;
|
||||
const itemsData: FlyerItemInsert[] = [];
|
||||
const mockFlyer = createMockFlyer({ ...flyerData, flyer_id: 100, store_id: 2 });
|
||||
|
||||
const mockClient = { query: vi.fn() };
|
||||
mockClient.query
|
||||
.mockResolvedValueOnce({ rows: [], rowCount: 0 }) // findOrCreateStore (insert)
|
||||
.mockResolvedValueOnce({ rows: [{ store_id: 2 }] }) // findOrCreateStore (select)
|
||||
.mockResolvedValueOnce({ rows: [mockFlyer] }); // insertFlyer
|
||||
|
||||
const result = await createFlyerAndItems(
|
||||
flyerData,
|
||||
itemsData,
|
||||
mockLogger,
|
||||
mockClient as unknown as PoolClient,
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
flyer: mockFlyer,
|
||||
items: [],
|
||||
});
|
||||
expect(mockClient.query).toHaveBeenCalledTimes(3);
|
||||
});
|
||||
|
||||
it('should propagate an error if any step fails', async () => {
|
||||
const flyerData: FlyerInsert = {
|
||||
file_name: 'fail.jpg',
|
||||
|
||||
@@ -141,9 +141,12 @@ export class FlyerRepository {
|
||||
`($${paramIndex++}, $${paramIndex++}, $${paramIndex++}, $${paramIndex++}, $${paramIndex++}, $${paramIndex++}, $${paramIndex++}, $${paramIndex++})`,
|
||||
);
|
||||
|
||||
// FIX: Sanitize price_display. Convert empty string to null.
|
||||
// Sanitize price_display. The database requires a non-empty string.
|
||||
// We provide a default value if the input is null, undefined, or an empty string.
|
||||
const priceDisplay =
|
||||
item.price_display && item.price_display.trim() !== '' ? item.price_display : null;
|
||||
item.price_display && item.price_display.trim() !== ''
|
||||
? item.price_display
|
||||
: 'N/A';
|
||||
|
||||
values.push(
|
||||
flyerId,
|
||||
|
||||
@@ -113,6 +113,8 @@ describe('Flyer Processing Background Job Integration Test', () => {
|
||||
const runBackgroundProcessingTest = async (user?: UserProfile, token?: string) => {
|
||||
console.log(`[TEST START] runBackgroundProcessingTest. User: ${user?.user.email ?? 'ANONYMOUS'}`);
|
||||
// Arrange: Load a mock flyer PDF.
|
||||
console.log('[TEST] about to read test-flyer-image.jpg')
|
||||
|
||||
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.
|
||||
@@ -121,11 +123,13 @@ 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);
|
||||
|
||||
// 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)
|
||||
// The icon name is derived from the original filename.
|
||||
const iconFileName = `icon-${path.parse(uniqueFileName).name}.webp`;
|
||||
createdFilePaths.push(path.join(uploadDir, 'icons', iconFileName));
|
||||
@@ -299,6 +303,10 @@ 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)
|
||||
|
||||
|
||||
// The `tags` object will be empty if no EXIF data is found.
|
||||
expect(exifResult.tags).toEqual({});
|
||||
expect(exifResult.tags.Software).toBeUndefined();
|
||||
@@ -380,6 +388,9 @@ 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)
|
||||
|
||||
|
||||
const savedImageMetadata = await sharp(savedImagePath).metadata();
|
||||
|
||||
// The test should fail here initially because PNGs are not processed.
|
||||
@@ -387,6 +398,7 @@ describe('Flyer Processing Background Job Integration Test', () => {
|
||||
expect(savedImageMetadata.exif).toBeUndefined();
|
||||
},
|
||||
240000,
|
||||
|
||||
);
|
||||
|
||||
it(
|
||||
@@ -544,6 +556,7 @@ it(
|
||||
await expect(fs.access(tempFilePath), 'Expected temporary file to exist after job failure, but it was deleted.');
|
||||
},
|
||||
240000,
|
||||
|
||||
);
|
||||
|
||||
});
|
||||
|
||||
@@ -216,7 +216,7 @@ describe('Gamification Flow Integration Test', () => {
|
||||
checksum: checksum,
|
||||
extractedData: {
|
||||
store_name: storeName,
|
||||
items: [{ item: 'Legacy Milk', price_in_cents: 250 }],
|
||||
items: [{ item: 'Legacy Milk', price_in_cents: 250, price_display: '$2.50' }],
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -227,24 +227,26 @@ describe('Public API Routes Integration Tests', () => {
|
||||
|
||||
describe('Rate Limiting on Public Routes', () => {
|
||||
it('should block requests to /api/personalization/master-items after exceeding the limit', async () => {
|
||||
const limit = 5; // Assume configured limit is 5 for testing
|
||||
// The limit might be higher than 5. We loop enough times to ensure we hit the rate limit.
|
||||
const maxRequests = 30;
|
||||
let blockedResponse: any;
|
||||
|
||||
// Send requests up to the limit
|
||||
for (let i = 0; i < limit; i++) {
|
||||
await request
|
||||
for (let i = 0; i < maxRequests; i++) {
|
||||
const response = await request
|
||||
.get('/api/personalization/master-items')
|
||||
.set('X-Test-Rate-Limit-Enable', 'true') // Enable rate limiter middleware
|
||||
.expect(200);
|
||||
.set('X-Test-Rate-Limit-Enable', 'true'); // Enable rate limiter middleware
|
||||
|
||||
if (response.status === 429) {
|
||||
blockedResponse = response;
|
||||
break;
|
||||
}
|
||||
expect(response.status).toBe(200);
|
||||
}
|
||||
|
||||
// Exceed the limit
|
||||
const response = await request
|
||||
.get('/api/personalization/master-items')
|
||||
.set('X-Test-Rate-Limit-Enable', 'true') // Enable rate limiter middleware
|
||||
.expect(429);
|
||||
|
||||
expect(response.headers).toHaveProperty('x-ratelimit-limit');
|
||||
expect(response.headers).toHaveProperty('x-ratelimit-remaining');
|
||||
expect(blockedResponse).toBeDefined();
|
||||
expect(blockedResponse.status).toBe(429);
|
||||
expect(blockedResponse.headers).toHaveProperty('x-ratelimit-limit');
|
||||
expect(blockedResponse.headers).toHaveProperty('x-ratelimit-remaining');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user