Compare commits

...

13 Commits

Author SHA1 Message Date
Gitea Actions
ac0115af2b ci: Bump version to 0.7.19 [skip ci] 2026-01-02 00:55:57 +05:00
f24b15f19b integration test fixes
All checks were successful
Deploy to Test Environment / deploy-to-test (push) Successful in 14m22s
2026-01-01 11:55:26 -08:00
Gitea Actions
e64426bd84 ci: Bump version to 0.7.18 [skip ci] 2026-01-02 00:35:49 +05:00
0ec4cd68d2 integration test fixes
All checks were successful
Deploy to Test Environment / deploy-to-test (push) Successful in 17m25s
2026-01-01 11:35:23 -08:00
Gitea Actions
840516d2a3 ci: Bump version to 0.7.17 [skip ci] 2026-01-02 00:29:45 +05:00
59355c3eef integration test fixes
Some checks failed
Deploy to Test Environment / deploy-to-test (push) Failing after 39s
2026-01-01 11:29:10 -08:00
d024935fe9 integration test fixes 2026-01-01 11:18:27 -08:00
Gitea Actions
5a5470634e ci: Bump version to 0.7.16 [skip ci] 2026-01-01 23:07:19 +05:00
392231ad63 more db
All checks were successful
Deploy to Test Environment / deploy-to-test (push) Successful in 19m34s
2026-01-01 10:06:49 -08:00
Gitea Actions
4b1c896621 ci: Bump version to 0.7.15 [skip ci] 2026-01-01 22:33:18 +05:00
720920a51c more db
All checks were successful
Deploy to Test Environment / deploy-to-test (push) Successful in 20m35s
2026-01-01 09:31:49 -08:00
Gitea Actions
460adb9506 ci: Bump version to 0.7.14 [skip ci] 2026-01-01 16:08:43 +05:00
7aa1f756a9 more db
All checks were successful
Deploy to Test Environment / deploy-to-test (push) Successful in 10m26s
2026-01-01 03:08:02 -08:00
16 changed files with 187 additions and 61 deletions

4
package-lock.json generated
View File

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

View File

@@ -1248,7 +1248,8 @@ INSERT INTO public.achievements (name, description, icon, points_value) VALUES
('List Sharer', 'Share a shopping list with another user for the first time.', 'list', 20),
('First Favorite', 'Mark a recipe as one of your favorites.', 'heart', 5),
('First Fork', 'Make a personal copy of a public recipe.', 'git-fork', 10),
('First Budget Created', 'Create your first budget to track spending.', 'piggy-bank', 15)
('First Budget Created', 'Create your first budget to track spending.', 'piggy-bank', 15),
('First-Upload', 'Upload your first flyer.', 'upload-cloud', 25)
ON CONFLICT (name) DO NOTHING;
-- ============================================================================
@@ -2557,16 +2558,21 @@ DROP FUNCTION IF EXISTS public.log_new_flyer();
CREATE OR REPLACE FUNCTION public.log_new_flyer()
RETURNS TRIGGER AS $$
BEGIN
INSERT INTO public.activity_log (action, display_text, icon, details)
-- If the flyer was uploaded by a registered user, award the 'First-Upload' achievement.
-- The award_achievement function handles checking if the user already has it.
IF NEW.uploaded_by IS NOT NULL THEN
PERFORM public.award_achievement(NEW.uploaded_by, 'First-Upload');
END IF;
INSERT INTO public.activity_log (user_id, action, display_text, icon, details)
VALUES (
NEW.uploaded_by, -- Log the user who uploaded it
'flyer_uploaded',
'A new flyer for ' || (SELECT name FROM public.stores WHERE store_id = NEW.store_id) || ' has been uploaded.',
'file-text',
jsonb_build_object(
'flyer_id', NEW.flyer_id,
'store_name', (SELECT name FROM public.stores WHERE store_id = NEW.store_id),
'valid_from', to_char(NEW.valid_from, 'YYYY-MM-DD'),
'valid_to', to_char(NEW.valid_to, 'YYYY-MM-DD')
'store_name', (SELECT name FROM public.stores WHERE store_id = NEW.store_id)
)
);
RETURN NEW;

View File

@@ -111,7 +111,7 @@ async function main() {
const flyerQuery = `
INSERT INTO public.flyers (file_name, image_url, checksum, store_id, valid_from, valid_to)
VALUES ('safeway-flyer.jpg', '/sample-assets/safeway-flyer.jpg', 'a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0', ${storeMap.get('Safeway')}, $1, $2)
VALUES ('safeway-flyer.jpg', 'https://example.com/flyer-images/safeway-flyer.jpg', 'a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0', ${storeMap.get('Safeway')}, $1, $2)
RETURNING flyer_id;
`;
const flyerRes = await client.query<{ flyer_id: number }>(flyerQuery, [

View File

@@ -482,6 +482,12 @@ describe('User Routes (/api/users)', () => {
expect(response.status).toBe(201);
expect(response.body).toEqual(mockAddedItem);
expect(db.shoppingRepo.addShoppingListItem).toHaveBeenCalledWith(
listId,
mockUserProfile.user.user_id,
itemData,
expectLogger,
);
});
it('should return 400 on foreign key error when adding an item', async () => {
@@ -519,6 +525,12 @@ describe('User Routes (/api/users)', () => {
expect(response.status).toBe(200);
expect(response.body).toEqual(mockUpdatedItem);
expect(db.shoppingRepo.updateShoppingListItem).toHaveBeenCalledWith(
itemId,
mockUserProfile.user.user_id,
updates,
expectLogger,
);
});
it('should return 404 if item to update is not found', async () => {
@@ -554,6 +566,11 @@ describe('User Routes (/api/users)', () => {
vi.mocked(db.shoppingRepo.removeShoppingListItem).mockResolvedValue(undefined);
const response = await supertest(app).delete('/api/users/shopping-lists/items/101');
expect(response.status).toBe(204);
expect(db.shoppingRepo.removeShoppingListItem).toHaveBeenCalledWith(
101,
mockUserProfile.user.user_id,
expectLogger,
);
});
it('should return 404 if item to delete is not found', async () => {

View File

@@ -478,10 +478,16 @@ router.post(
validateRequest(addShoppingListItemSchema),
async (req, res, next: NextFunction) => {
logger.debug(`[ROUTE] POST /api/users/shopping-lists/:listId/items - ENTER`);
const userProfile = req.user as UserProfile;
// Apply ADR-003 pattern for type safety
const { params, body } = req as unknown as AddShoppingListItemRequest;
try {
const newItem = await db.shoppingRepo.addShoppingListItem(params.listId, body, req.log);
const newItem = await db.shoppingRepo.addShoppingListItem(
params.listId,
userProfile.user.user_id,
body,
req.log,
);
res.status(201).json(newItem);
} catch (error) {
if (error instanceof ForeignKeyConstraintError) {
@@ -512,11 +518,13 @@ router.put(
validateRequest(updateShoppingListItemSchema),
async (req, res, next: NextFunction) => {
logger.debug(`[ROUTE] PUT /api/users/shopping-lists/items/:itemId - ENTER`);
const userProfile = req.user as UserProfile;
// Apply ADR-003 pattern for type safety
const { params, body } = req as unknown as UpdateShoppingListItemRequest;
try {
const updatedItem = await db.shoppingRepo.updateShoppingListItem(
params.itemId,
userProfile.user.user_id,
body,
req.log,
);
@@ -541,10 +549,11 @@ router.delete(
validateRequest(shoppingListItemIdSchema),
async (req, res, next: NextFunction) => {
logger.debug(`[ROUTE] DELETE /api/users/shopping-lists/items/:itemId - ENTER`);
const userProfile = req.user as UserProfile;
// Apply ADR-003 pattern for type safety
const { params } = req as unknown as DeleteShoppingListItemRequest;
try {
await db.shoppingRepo.removeShoppingListItem(params.itemId, req.log);
await db.shoppingRepo.removeShoppingListItem(params.itemId, userProfile.user.user_id, req.log);
res.status(204).send();
} catch (error: unknown) {
logger.error(

View File

@@ -249,6 +249,17 @@ describe('Budget DB Service', () => {
expect(result).toEqual(mockUpdatedBudget);
});
it('should prevent a user from updating a budget they do not own', async () => {
// Arrange: Mock the query to return 0 rows affected
mockDb.query.mockResolvedValue({ rows: [], rowCount: 0 });
// Act & Assert: Attempt to update with a different user ID should throw an error.
await expect(
budgetRepo.updateBudget(1, 'another-user', { name: 'Updated Groceries' }, mockLogger),
).rejects.toThrow('Budget not found or user does not have permission to update.');
});
it('should throw an error if no rows are updated', async () => {
// Arrange: Mock the query to return 0 rows affected
mockDb.query.mockResolvedValue({ rows: [], rowCount: 0 });

View File

@@ -97,7 +97,7 @@ export class FlyerRepository {
flyerData.store_address, // $8
flyerData.status, // $9
flyerData.item_count, // $10
flyerData.uploaded_by, // $11
flyerData.uploaded_by ?? null, // $11
];
const result = await this.db.query<Flyer>(query, values);

View File

@@ -264,6 +264,16 @@ describe('Notification DB Service', () => {
});
});
describe('markNotificationAsRead - Ownership Check', () => {
it('should not mark a notification as read if the user does not own it', async () => {
mockPoolInstance.query.mockResolvedValue({ rowCount: 0 });
await expect(notificationRepo.markNotificationAsRead(1, 'wrong-user', mockLogger)).rejects.toThrow(
'Notification not found or user does not have permission.',
);
});
});
describe('markAllNotificationsAsRead', () => {
it('should execute an UPDATE query to mark all notifications as read for a user', async () => {
mockPoolInstance.query.mockResolvedValue({ rowCount: 3 });

View File

@@ -268,6 +268,17 @@ describe('Recipe DB Service', () => {
);
});
});
describe('deleteRecipe - Ownership Check', () => {
it('should not delete recipe if the user does not own it and is not an admin', async () => {
mockQuery.mockResolvedValue({ rowCount: 0 });
await expect(recipeRepo.deleteRecipe(1, 'wrong-user', false, mockLogger)).rejects.toThrow(
'Recipe not found or user does not have permission to delete.',
);
});
});
describe('updateRecipe', () => {
it('should execute an UPDATE query with the correct fields', async () => {

View File

@@ -190,13 +190,14 @@ describe('Shopping DB Service', () => {
const result = await shoppingRepo.addShoppingListItem(
1,
'user-1',
{ customItemName: 'Custom Item' },
mockLogger,
);
expect(mockPoolInstance.query).toHaveBeenCalledWith(
expect.stringContaining('INSERT INTO public.shopping_list_items'),
[1, null, 'Custom Item'],
[1, null, 'Custom Item', 'user-1'],
);
expect(result).toEqual(mockItem);
});
@@ -205,11 +206,11 @@ describe('Shopping DB Service', () => {
const mockItem = createMockShoppingListItem({ master_item_id: 123 });
mockPoolInstance.query.mockResolvedValue({ rows: [mockItem] });
const result = await shoppingRepo.addShoppingListItem(1, { masterItemId: 123 }, mockLogger);
const result = await shoppingRepo.addShoppingListItem(1, 'user-1', { masterItemId: 123 }, mockLogger);
expect(mockPoolInstance.query).toHaveBeenCalledWith(
expect.stringContaining('INSERT INTO public.shopping_list_items'),
[1, 123, null],
[1, 123, null, 'user-1'],
);
expect(result).toEqual(mockItem);
});
@@ -223,19 +224,20 @@ describe('Shopping DB Service', () => {
const result = await shoppingRepo.addShoppingListItem(
1,
'user-1',
{ masterItemId: 123, customItemName: 'Organic Apples' },
mockLogger,
);
expect(mockPoolInstance.query).toHaveBeenCalledWith(
expect.stringContaining('INSERT INTO public.shopping_list_items'),
[1, 123, 'Organic Apples'],
[1, 123, 'Organic Apples', 'user-1'],
);
expect(result).toEqual(mockItem);
});
it('should throw an error if both masterItemId and customItemName are missing', async () => {
await expect(shoppingRepo.addShoppingListItem(1, {}, mockLogger)).rejects.toThrow(
await expect(shoppingRepo.addShoppingListItem(1, 'user-1', {}, mockLogger)).rejects.toThrow(
'Either masterItemId or customItemName must be provided.',
);
});
@@ -244,19 +246,19 @@ describe('Shopping DB Service', () => {
const dbError = new Error('violates foreign key constraint');
(dbError as Error & { code: string }).code = '23503';
mockPoolInstance.query.mockRejectedValue(dbError);
await expect(
shoppingRepo.addShoppingListItem(999, { masterItemId: 999 }, mockLogger),
).rejects.toThrow('Referenced list or item does not exist.');
await expect(shoppingRepo.addShoppingListItem(999, 'user-1', { masterItemId: 999 }, mockLogger)).rejects.toThrow(
'Referenced list or item does not exist.',
);
});
it('should throw a generic error if the database query fails', async () => {
const dbError = new Error('DB Connection Error');
mockPoolInstance.query.mockRejectedValue(dbError);
await expect(
shoppingRepo.addShoppingListItem(1, { customItemName: 'Test' }, mockLogger),
shoppingRepo.addShoppingListItem(1, 'user-1', { customItemName: 'Test' }, mockLogger),
).rejects.toThrow('Failed to add item to shopping list.');
expect(mockLogger.error).toHaveBeenCalledWith(
{ err: dbError, listId: 1, item: { customItemName: 'Test' } },
{ err: dbError, listId: 1, userId: 'user-1', item: { customItemName: 'Test' } },
'Database error in addShoppingListItem',
);
});
@@ -269,13 +271,14 @@ describe('Shopping DB Service', () => {
const result = await shoppingRepo.updateShoppingListItem(
1,
'user-1',
{ is_purchased: true },
mockLogger,
);
expect(mockPoolInstance.query).toHaveBeenCalledWith(
'UPDATE public.shopping_list_items SET is_purchased = $1 WHERE shopping_list_item_id = $2 RETURNING *',
[true, 1],
expect.stringContaining('UPDATE public.shopping_list_items sli'),
[true, 1, 'user-1'],
);
expect(result).toEqual(mockItem);
});
@@ -285,11 +288,11 @@ describe('Shopping DB Service', () => {
const mockItem = createMockShoppingListItem({ shopping_list_item_id: 1, ...updates });
mockPoolInstance.query.mockResolvedValue({ rows: [mockItem], rowCount: 1 });
const result = await shoppingRepo.updateShoppingListItem(1, updates, mockLogger);
const result = await shoppingRepo.updateShoppingListItem(1, 'user-1', updates, mockLogger);
expect(mockPoolInstance.query).toHaveBeenCalledWith(
'UPDATE public.shopping_list_items SET quantity = $1, is_purchased = $2, notes = $3 WHERE shopping_list_item_id = $4 RETURNING *',
[updates.quantity, updates.is_purchased, updates.notes, 1],
expect.stringContaining('UPDATE public.shopping_list_items sli'),
[updates.quantity, updates.is_purchased, updates.notes, 1, 'user-1'],
);
expect(result).toEqual(mockItem);
});
@@ -297,13 +300,13 @@ describe('Shopping DB Service', () => {
it('should throw an error if the item to update is not found', async () => {
mockPoolInstance.query.mockResolvedValue({ rowCount: 0, rows: [], command: 'UPDATE' });
await expect(
shoppingRepo.updateShoppingListItem(999, { quantity: 5 }, mockLogger),
shoppingRepo.updateShoppingListItem(999, 'user-1', { quantity: 5 }, mockLogger),
).rejects.toThrow('Shopping list item not found.');
});
it('should throw an error if no valid fields are provided to update', async () => {
// The function should throw before even querying the database.
await expect(shoppingRepo.updateShoppingListItem(1, {}, mockLogger)).rejects.toThrow(
await expect(shoppingRepo.updateShoppingListItem(1, 'user-1', {}, mockLogger)).rejects.toThrow(
'No valid fields to update.',
);
});
@@ -312,44 +315,65 @@ describe('Shopping DB Service', () => {
const dbError = new Error('DB Connection Error');
mockPoolInstance.query.mockRejectedValue(dbError);
await expect(
shoppingRepo.updateShoppingListItem(1, { is_purchased: true }, mockLogger),
shoppingRepo.updateShoppingListItem(1, 'user-1', { is_purchased: true }, mockLogger),
).rejects.toThrow('Failed to update shopping list item.');
expect(mockLogger.error).toHaveBeenCalledWith(
{ err: dbError, itemId: 1, updates: { is_purchased: true } },
{ err: dbError, itemId: 1, userId: 'user-1', updates: { is_purchased: true } },
'Database error in updateShoppingListItem',
);
});
});
describe('updateShoppingListItem - Ownership Check', () => {
it('should not update an item if the user does not own the shopping list', async () => {
mockPoolInstance.query.mockResolvedValue({ rowCount: 0 });
await expect(
shoppingRepo.updateShoppingListItem(1, 'wrong-user', { is_purchased: true }, mockLogger),
).rejects.toThrow('Shopping list item not found.');
});
});
describe('removeShoppingListItem', () => {
it('should delete an item if rowCount is 1', async () => {
mockPoolInstance.query.mockResolvedValue({ rowCount: 1, rows: [], command: 'DELETE' });
await expect(shoppingRepo.removeShoppingListItem(1, mockLogger)).resolves.toBeUndefined();
await expect(shoppingRepo.removeShoppingListItem(1, 'user-1', mockLogger)).resolves.toBeUndefined();
expect(mockPoolInstance.query).toHaveBeenCalledWith(
'DELETE FROM public.shopping_list_items WHERE shopping_list_item_id = $1',
[1],
expect.stringContaining('DELETE FROM public.shopping_list_items sli'),
[1, 'user-1'],
);
});
it('should throw an error if no rows are deleted (item not found)', async () => {
mockPoolInstance.query.mockResolvedValue({ rowCount: 0, rows: [], command: 'DELETE' });
await expect(shoppingRepo.removeShoppingListItem(999, mockLogger)).rejects.toThrow(
'Shopping list item not found.',
await expect(shoppingRepo.removeShoppingListItem(999, 'user-1', mockLogger)).rejects.toThrow(
'Shopping list item not found or user does not have permission.',
);
});
it('should throw a generic error if the database query fails', async () => {
const dbError = new Error('DB Connection Error');
mockPoolInstance.query.mockRejectedValue(dbError);
await expect(shoppingRepo.removeShoppingListItem(1, mockLogger)).rejects.toThrow(
await expect(shoppingRepo.removeShoppingListItem(1, 'user-1', mockLogger)).rejects.toThrow(
'Failed to remove item from shopping list.',
);
expect(mockLogger.error).toHaveBeenCalledWith(
{ err: dbError, itemId: 1 },
{ err: dbError, itemId: 1, userId: 'user-1' },
'Database error in removeShoppingListItem',
);
});
});
describe('removeShoppingListItem - Ownership Check', () => {
it('should not remove an item if the user does not own the shopping list', async () => {
mockPoolInstance.query.mockResolvedValue({ rowCount: 0 });
await expect(shoppingRepo.removeShoppingListItem(1, 'wrong-user', mockLogger)).rejects.toThrow(
'Shopping list item not found or user does not have permission.',
);
});
});
describe('completeShoppingList', () => {
it('should call the complete_shopping_list database function', async () => {

View File

@@ -156,6 +156,7 @@ export class ShoppingRepository {
*/
async addShoppingListItem(
listId: number,
userId: string,
item: { masterItemId?: number; customItemName?: string },
logger: Logger,
): Promise<ShoppingListItem> {
@@ -165,13 +166,29 @@ export class ShoppingRepository {
}
try {
const res = await this.db.query<ShoppingListItem>(
'INSERT INTO public.shopping_list_items (shopping_list_id, master_item_id, custom_item_name) VALUES ($1, $2, $3) RETURNING *',
[listId, item.masterItemId ?? null, item.customItemName ?? null],
);
const query = `
INSERT INTO public.shopping_list_items (shopping_list_id, master_item_id, custom_item_name)
SELECT $1, $2, $3
WHERE EXISTS (
SELECT 1 FROM public.shopping_lists WHERE shopping_list_id = $1 AND user_id = $4
)
RETURNING *;
`;
const res = await this.db.query<ShoppingListItem>(query, [
listId,
item.masterItemId ?? null,
item.customItemName ?? null,
userId,
]);
if (res.rowCount === 0) {
throw new NotFoundError('Shopping list not found or user does not have permission.');
}
return res.rows[0];
} catch (error) {
handleDbError(error, logger, 'Database error in addShoppingListItem', { listId, item }, {
if (error instanceof NotFoundError) throw error;
handleDbError(error, logger, 'Database error in addShoppingListItem', { listId, userId, item }, {
fkMessage: 'Referenced list or item does not exist.',
checkMessage: 'Shopping list item must have a master item or a custom name.',
defaultMessage: 'Failed to add item to shopping list.',
@@ -183,19 +200,23 @@ export class ShoppingRepository {
* Removes an item from a shopping list.
* @param itemId The ID of the shopping list item to remove.
*/
async removeShoppingListItem(itemId: number, logger: Logger): Promise<void> {
async removeShoppingListItem(itemId: number, userId: string, logger: Logger): Promise<void> {
try {
const res = await this.db.query(
'DELETE FROM public.shopping_list_items WHERE shopping_list_item_id = $1',
[itemId],
);
// The patch requested this specific error handling.
const query = `
DELETE FROM public.shopping_list_items sli
WHERE sli.shopping_list_item_id = $1
AND EXISTS (
SELECT 1 FROM public.shopping_lists sl
WHERE sl.shopping_list_id = sli.shopping_list_id AND sl.user_id = $2
);
`;
const res = await this.db.query(query, [itemId, userId]);
if (res.rowCount === 0) {
throw new NotFoundError('Shopping list item not found.');
throw new NotFoundError('Shopping list item not found or user does not have permission.');
}
} catch (error) {
if (error instanceof NotFoundError) throw error;
handleDbError(error, logger, 'Database error in removeShoppingListItem', { itemId }, {
handleDbError(error, logger, 'Database error in removeShoppingListItem', { itemId, userId }, {
defaultMessage: 'Failed to remove item from shopping list.',
});
}
@@ -312,6 +333,7 @@ export class ShoppingRepository {
*/
async updateShoppingListItem(
itemId: number,
userId: string,
updates: Partial<ShoppingListItem>,
logger: Logger,
): Promise<ShoppingListItem> {
@@ -341,10 +363,19 @@ export class ShoppingRepository {
}
values.push(itemId);
const query = `UPDATE public.shopping_list_items SET ${setClauses.join(', ')} WHERE shopping_list_item_id = $${valueIndex} RETURNING *`;
values.push(userId);
const query = `
UPDATE public.shopping_list_items sli
SET ${setClauses.join(', ')}
FROM public.shopping_lists sl
WHERE sli.shopping_list_item_id = $${valueIndex}
AND sli.shopping_list_id = sl.shopping_list_id
AND sl.user_id = $${valueIndex + 1}
RETURNING sli.*;
`;
const res = await this.db.query<ShoppingListItem>(query, values);
// The patch requested this specific error handling.
if (res.rowCount === 0) {
throw new NotFoundError('Shopping list item not found.');
}
@@ -357,7 +388,7 @@ export class ShoppingRepository {
) {
throw error;
}
handleDbError(error, logger, 'Database error in updateShoppingListItem', { itemId, updates }, {
handleDbError(error, logger, 'Database error in updateShoppingListItem', { itemId, userId, updates }, {
defaultMessage: 'Failed to update shopping list item.',
});
}

View File

@@ -165,7 +165,9 @@ describe('Admin API Routes Integration Tests', () => {
const flyerRes = await getPool().query(
`INSERT INTO public.flyers (store_id, file_name, image_url, item_count, checksum)
VALUES ($1, 'admin-test.jpg', 'http://test.com/img.jpg', 1, $2) RETURNING flyer_id`,
[testStoreId, `checksum-${Date.now()}-${Math.random()}`],
// The checksum must be a unique 64-character string to satisfy the DB constraint.
// We generate a dynamic string and pad it to 64 characters.
[testStoreId, `checksum-${Date.now()}-${Math.random()}`.padEnd(64, '0')],
);
const flyerId = flyerRes.rows[0].flyer_id;

View File

@@ -80,7 +80,7 @@ describe('Flyer Processing Background Job Integration Test', () => {
// Act 2: Poll for the job status until it completes.
let jobStatus;
const maxRetries = 30; // Poll for up to 90 seconds (30 * 3s)
const maxRetries = 60; // Poll for up to 180 seconds (60 * 3s)
for (let i = 0; i < maxRetries; i++) {
console.log(`Polling attempt ${i + 1}...`);
await new Promise((resolve) => setTimeout(resolve, 3000)); // Wait 3 seconds between polls
@@ -149,12 +149,12 @@ describe('Flyer Processing Background Job Integration Test', () => {
// Act & Assert
await runBackgroundProcessingTest(authUser, token);
}, 120000); // Increase timeout to 120 seconds for this long-running test
}, 240000); // Increase timeout to 240 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
}, 240000); // Increase timeout to 240 seconds for this long-running test
it(
'should strip EXIF data from uploaded JPEG images during processing',
@@ -238,7 +238,7 @@ describe('Flyer Processing Background Job Integration Test', () => {
expect(exifResult.tags).toEqual({});
expect(exifResult.tags.Software).toBeUndefined();
},
120000,
240000,
);
it(
@@ -322,6 +322,6 @@ describe('Flyer Processing Background Job Integration Test', () => {
// The `exif` property should be undefined after the fix.
expect(savedImageMetadata.exif).toBeUndefined();
},
120000,
240000,
);
});

View File

@@ -26,7 +26,7 @@ describe('Public Flyer API Routes Integration Tests', () => {
const flyerRes = await getPool().query(
`INSERT INTO public.flyers (store_id, file_name, image_url, item_count, checksum)
VALUES ($1, 'integration-test.jpg', 'http://test.com/img.jpg', 1, $2) RETURNING flyer_id`,
[storeId, `checksum-${Date.now()}`],
[storeId, `checksum-${Date.now()}`.padEnd(64, '0')],
);
createdFlyerId = flyerRes.rows[0].flyer_id;

View File

@@ -81,12 +81,17 @@ describe('Gamification Flow Integration Test', () => {
break;
}
}
if (!jobStatus) {
console.error('[DEBUG] Gamification test job timed out: No job status received.');
throw new Error('Gamification test job timed out: No job status received.');
}
// --- Assert 1: Verify the job completed successfully ---
if (jobStatus?.state === 'failed') {
console.error('[DEBUG] Gamification test job failed:', jobStatus.failedReason);
}
expect(jobStatus?.state).toBe('completed');
const flyerId = jobStatus?.returnValue?.flyerId;
expect(flyerId).toBeTypeOf('number');
createdFlyerIds.push(flyerId); // Track for cleanup