test fixes to align with latest tests
All checks were successful
Deploy to Test Environment / deploy-to-test (push) Successful in 16m51s

This commit is contained in:
2026-01-18 14:27:20 -08:00
parent 67cfe39249
commit 941626004e
3 changed files with 253 additions and 49 deletions

View File

@@ -668,12 +668,17 @@ describe('Admin DB Service', () => {
const mockUsers: AdminUserView[] = [ const mockUsers: AdminUserView[] = [
createMockAdminUserView({ user_id: '1', email: 'test@test.com' }), createMockAdminUserView({ user_id: '1', email: 'test@test.com' }),
]; ];
mockDb.query.mockResolvedValue({ rows: mockUsers }); // Mock count query
mockDb.query.mockResolvedValueOnce({ rows: [{ count: '1' }] });
// Mock users query
mockDb.query.mockResolvedValueOnce({ rows: mockUsers });
const result = await adminRepo.getAllUsers(mockLogger); const result = await adminRepo.getAllUsers(mockLogger);
expect(mockDb.query).toHaveBeenCalledWith( expect(mockDb.query).toHaveBeenCalledWith(
expect.stringContaining('FROM public.users u JOIN public.profiles p'), expect.stringContaining('FROM public.users u JOIN public.profiles p'),
undefined,
); );
expect(result).toEqual(mockUsers); expect(result).toEqual({ users: mockUsers, total: 1 });
}); });
it('should throw an error if the database query fails', async () => { it('should throw an error if the database query fails', async () => {

View File

@@ -5,7 +5,10 @@ import type { Pool, PoolClient } from 'pg';
import { withTransaction } from './connection.db'; import { withTransaction } from './connection.db';
import { PersonalizationRepository } from './personalization.db'; import { PersonalizationRepository } from './personalization.db';
import type { MasterGroceryItem, UserAppliance, DietaryRestriction, Appliance } from '../../types'; import type { MasterGroceryItem, UserAppliance, DietaryRestriction, Appliance } from '../../types';
import { createMockMasterGroceryItem, createMockUserAppliance } from '../../tests/utils/mockFactories'; import {
createMockMasterGroceryItem,
createMockUserAppliance,
} from '../../tests/utils/mockFactories';
// Un-mock the module we are testing to ensure we use the real implementation. // Un-mock the module we are testing to ensure we use the real implementation.
vi.unmock('./personalization.db'); vi.unmock('./personalization.db');
@@ -50,7 +53,10 @@ describe('Personalization DB Service', () => {
const mockItems: MasterGroceryItem[] = [ const mockItems: MasterGroceryItem[] = [
createMockMasterGroceryItem({ master_grocery_item_id: 1, name: 'Apples' }), createMockMasterGroceryItem({ master_grocery_item_id: 1, name: 'Apples' }),
]; ];
mockQuery.mockResolvedValue({ rows: mockItems }); // Mock count query
mockQuery.mockResolvedValueOnce({ rows: [{ count: '1' }] });
// Mock items query
mockQuery.mockResolvedValueOnce({ rows: mockItems });
const result = await personalizationRepo.getAllMasterItems(mockLogger); const result = await personalizationRepo.getAllMasterItems(mockLogger);
@@ -64,14 +70,17 @@ describe('Personalization DB Service', () => {
// The query string in the implementation has a lot of whitespace from the template literal. // The query string in the implementation has a lot of whitespace from the template literal.
// This updated expectation matches the new query exactly. // This updated expectation matches the new query exactly.
expect(mockQuery).toHaveBeenCalledWith(expectedQuery); expect(mockQuery).toHaveBeenCalledWith(expectedQuery, undefined);
expect(result).toEqual(mockItems); expect(result).toEqual({ items: mockItems, total: 1 });
}); });
it('should return an empty array if no master items exist', async () => { it('should return an empty array if no master items exist', async () => {
mockQuery.mockResolvedValue({ rows: [] }); // Mock count query
mockQuery.mockResolvedValueOnce({ rows: [{ count: '0' }] });
// Mock items query
mockQuery.mockResolvedValueOnce({ rows: [] });
const result = await personalizationRepo.getAllMasterItems(mockLogger); const result = await personalizationRepo.getAllMasterItems(mockLogger);
expect(result).toEqual([]); expect(result).toEqual({ items: [], total: 0 });
}); });
it('should throw an error if the database query fails', async () => { it('should throw an error if the database query fails', async () => {

View File

@@ -37,7 +37,7 @@ describe('FlyerAiProcessor', () => {
extractCoreDataFromFlyerImage: vi.fn(), extractCoreDataFromFlyerImage: vi.fn(),
} as unknown as AIService; } as unknown as AIService;
mockPersonalizationRepo = { mockPersonalizationRepo = {
getAllMasterItems: vi.fn().mockResolvedValue([]), getAllMasterItems: vi.fn().mockResolvedValue({ items: [], total: 0 }),
} as unknown as PersonalizationRepository; } as unknown as PersonalizationRepository;
service = new FlyerAiProcessor(mockAiService, mockPersonalizationRepo); service = new FlyerAiProcessor(mockAiService, mockPersonalizationRepo);
@@ -86,9 +86,9 @@ describe('FlyerAiProcessor', () => {
const imagePaths = [{ path: 'page1.jpg', mimetype: 'image/jpeg' }]; const imagePaths = [{ path: 'page1.jpg', mimetype: 'image/jpeg' }];
// Act & Assert // Act & Assert
await expect( await expect(service.extractAndValidateData(imagePaths, jobData, logger)).rejects.toThrow(
service.extractAndValidateData(imagePaths, jobData, logger), dbError,
).rejects.toThrow(dbError); );
// Verify that the process stops before calling the AI service // Verify that the process stops before calling the AI service
expect(mockAiService.extractCoreDataFromFlyerImage).not.toHaveBeenCalled(); expect(mockAiService.extractCoreDataFromFlyerImage).not.toHaveBeenCalled();
@@ -103,8 +103,20 @@ describe('FlyerAiProcessor', () => {
valid_to: '2024-01-07', valid_to: '2024-01-07',
store_address: '123 Good St', store_address: '123 Good St',
items: [ items: [
{ item: 'Priced Item 1', price_in_cents: 199, price_display: '$1.99', quantity: '1', category_name: 'A' }, {
{ item: 'Priced Item 2', price_in_cents: 299, price_display: '$2.99', quantity: '1', category_name: 'B' }, item: 'Priced Item 1',
price_in_cents: 199,
price_display: '$1.99',
quantity: '1',
category_name: 'A',
},
{
item: 'Priced Item 2',
price_in_cents: 299,
price_display: '$2.99',
quantity: '1',
category_name: 'B',
},
], ],
}; };
vi.mocked(mockAiService.extractCoreDataFromFlyerImage).mockResolvedValue(mockAiResponse); vi.mocked(mockAiService.extractCoreDataFromFlyerImage).mockResolvedValue(mockAiResponse);
@@ -128,7 +140,9 @@ describe('FlyerAiProcessor', () => {
valid_to: null, valid_to: null,
store_address: null, store_address: null,
}; };
vi.mocked(mockAiService.extractCoreDataFromFlyerImage).mockResolvedValue(invalidResponse as any); vi.mocked(mockAiService.extractCoreDataFromFlyerImage).mockResolvedValue(
invalidResponse as any,
);
const imagePaths = [{ path: 'page1.jpg', mimetype: 'image/jpeg' }]; const imagePaths = [{ path: 'page1.jpg', mimetype: 'image/jpeg' }];
await expect(service.extractAndValidateData(imagePaths, jobData, logger)).rejects.toThrow( await expect(service.extractAndValidateData(imagePaths, jobData, logger)).rejects.toThrow(
@@ -140,7 +154,15 @@ describe('FlyerAiProcessor', () => {
const jobData = createMockJobData({}); const jobData = createMockJobData({});
const mockAiResponse = { const mockAiResponse = {
store_name: null, // Missing store name store_name: null, // Missing store name
items: [{ item: 'Test Item', price_display: '$1.99', price_in_cents: 199, quantity: 'each', category_name: 'Grocery' }], items: [
{
item: 'Test Item',
price_display: '$1.99',
price_in_cents: 199,
quantity: 'each',
category_name: 'Grocery',
},
],
valid_from: '2024-01-01', valid_from: '2024-01-01',
valid_to: '2024-01-07', valid_to: '2024-01-07',
store_address: null, store_address: null,
@@ -187,9 +209,27 @@ describe('FlyerAiProcessor', () => {
valid_to: '2024-01-07', valid_to: '2024-01-07',
store_address: '123 Test St', store_address: '123 Test St',
items: [ items: [
{ item: 'Priced Item', price_in_cents: 199, price_display: '$1.99', quantity: '1', category_name: 'A' }, {
{ item: 'Unpriced Item 1', price_in_cents: null, price_display: 'See store', quantity: '1', category_name: 'B' }, item: 'Priced Item',
{ item: 'Unpriced Item 2', price_in_cents: null, price_display: 'FREE', quantity: '1', category_name: 'C' }, price_in_cents: 199,
price_display: '$1.99',
quantity: '1',
category_name: 'A',
},
{
item: 'Unpriced Item 1',
price_in_cents: null,
price_display: 'See store',
quantity: '1',
category_name: 'B',
},
{
item: 'Unpriced Item 2',
price_in_cents: null,
price_display: 'FREE',
quantity: '1',
category_name: 'C',
},
], // 1/3 = 33% have price, which is < 50% ], // 1/3 = 33% have price, which is < 50%
}; };
vi.mocked(mockAiService.extractCoreDataFromFlyerImage).mockResolvedValue(mockAiResponse); vi.mocked(mockAiService.extractCoreDataFromFlyerImage).mockResolvedValue(mockAiResponse);
@@ -200,7 +240,9 @@ describe('FlyerAiProcessor', () => {
expect(result.needsReview).toBe(true); expect(result.needsReview).toBe(true);
expect(logger.warn).toHaveBeenCalledWith( expect(logger.warn).toHaveBeenCalledWith(
expect.objectContaining({ qualityIssues: ['Low price quality (33% of items have a price)'] }), expect.objectContaining({
qualityIssues: ['Low price quality (33% of items have a price)'],
}),
expect.stringContaining('AI response has quality issues.'), expect.stringContaining('AI response has quality issues.'),
); );
}); });
@@ -216,10 +258,34 @@ describe('FlyerAiProcessor', () => {
valid_to: '2024-01-07', valid_to: '2024-01-07',
store_address: '123 Test St', store_address: '123 Test St',
items: [ items: [
{ item: 'Priced Item 1', price_in_cents: 199, price_display: '$1.99', quantity: '1', category_name: 'A' }, {
{ item: 'Priced Item 2', price_in_cents: 299, price_display: '$2.99', quantity: '1', category_name: 'B' }, item: 'Priced Item 1',
{ item: 'Priced Item 3', price_in_cents: 399, price_display: '$3.99', quantity: '1', category_name: 'C' }, price_in_cents: 199,
{ item: 'Unpriced Item 1', price_in_cents: null, price_display: 'See store', quantity: '1', category_name: 'D' }, price_display: '$1.99',
quantity: '1',
category_name: 'A',
},
{
item: 'Priced Item 2',
price_in_cents: 299,
price_display: '$2.99',
quantity: '1',
category_name: 'B',
},
{
item: 'Priced Item 3',
price_in_cents: 399,
price_display: '$3.99',
quantity: '1',
category_name: 'C',
},
{
item: 'Unpriced Item 1',
price_in_cents: null,
price_display: 'See store',
quantity: '1',
category_name: 'D',
},
], // 3/4 = 75% have price. This is > 50% (default) but < 80% (custom). ], // 3/4 = 75% have price. This is > 50% (default) but < 80% (custom).
}; };
vi.mocked(mockAiService.extractCoreDataFromFlyerImage).mockResolvedValue(mockAiResponse); vi.mocked(mockAiService.extractCoreDataFromFlyerImage).mockResolvedValue(mockAiResponse);
@@ -233,7 +299,9 @@ describe('FlyerAiProcessor', () => {
// Because 75% < 80%, it should be flagged for review. // Because 75% < 80%, it should be flagged for review.
expect(result.needsReview).toBe(true); expect(result.needsReview).toBe(true);
expect(logger.warn).toHaveBeenCalledWith( expect(logger.warn).toHaveBeenCalledWith(
expect.objectContaining({ qualityIssues: ['Low price quality (75% of items have a price)'] }), expect.objectContaining({
qualityIssues: ['Low price quality (75% of items have a price)'],
}),
expect.stringContaining('AI response has quality issues.'), expect.stringContaining('AI response has quality issues.'),
); );
}); });
@@ -243,9 +311,17 @@ describe('FlyerAiProcessor', () => {
const mockAiResponse = { const mockAiResponse = {
store_name: 'Test Store', store_name: 'Test Store',
valid_from: null, // Missing date valid_from: null, // Missing date
valid_to: null, // Missing date valid_to: null, // Missing date
store_address: '123 Test St', store_address: '123 Test St',
items: [{ item: 'Test Item', price_in_cents: 199, price_display: '$1.99', quantity: '1', category_name: 'A' }], items: [
{
item: 'Test Item',
price_in_cents: 199,
price_display: '$1.99',
quantity: '1',
category_name: 'A',
},
],
}; };
vi.mocked(mockAiService.extractCoreDataFromFlyerImage).mockResolvedValue(mockAiResponse); vi.mocked(mockAiService.extractCoreDataFromFlyerImage).mockResolvedValue(mockAiResponse);
const { logger } = await import('./logger.server'); const { logger } = await import('./logger.server');
@@ -264,7 +340,7 @@ describe('FlyerAiProcessor', () => {
const jobData = createMockJobData({}); const jobData = createMockJobData({});
const mockAiResponse = { const mockAiResponse = {
store_name: null, // Issue 1 store_name: null, // Issue 1
items: [], // Issue 2 items: [], // Issue 2
valid_from: null, // Issue 3 valid_from: null, // Issue 3
valid_to: null, valid_to: null,
store_address: null, store_address: null,
@@ -277,7 +353,14 @@ describe('FlyerAiProcessor', () => {
expect(result.needsReview).toBe(true); expect(result.needsReview).toBe(true);
expect(logger.warn).toHaveBeenCalledWith( expect(logger.warn).toHaveBeenCalledWith(
{ rawData: mockAiResponse, qualityIssues: ['Missing store name', 'No items were extracted', 'Missing both valid_from and valid_to dates'] }, {
rawData: mockAiResponse,
qualityIssues: [
'Missing store name',
'No items were extracted',
'Missing both valid_from and valid_to dates',
],
},
'AI response has quality issues. Flagging for review. Issues: Missing store name, No items were extracted, Missing both valid_from and valid_to dates', 'AI response has quality issues. Flagging for review. Issues: Missing store name, No items were extracted, Missing both valid_from and valid_to dates',
); );
}); });
@@ -291,7 +374,15 @@ describe('FlyerAiProcessor', () => {
valid_from: '2024-01-01', valid_from: '2024-01-01',
valid_to: '2024-01-07', valid_to: '2024-01-07',
store_address: '123 Test St', store_address: '123 Test St',
items: [{ item: 'Test Item', price_in_cents: 199, price_display: '$1.99', quantity: '1', category_name: 'A' }], items: [
{
item: 'Test Item',
price_in_cents: 199,
price_display: '$1.99',
quantity: '1',
category_name: 'A',
},
],
}; };
vi.mocked(mockAiService.extractCoreDataFromFlyerImage).mockResolvedValue(mockAiResponse); vi.mocked(mockAiService.extractCoreDataFromFlyerImage).mockResolvedValue(mockAiResponse);
@@ -300,7 +391,11 @@ describe('FlyerAiProcessor', () => {
// Assert // Assert
expect(mockAiService.extractCoreDataFromFlyerImage).toHaveBeenCalledWith( expect(mockAiService.extractCoreDataFromFlyerImage).toHaveBeenCalledWith(
imagePaths, [], undefined, '456 Fallback Ave', logger imagePaths,
[],
undefined,
'456 Fallback Ave',
logger,
); );
}); });
@@ -323,8 +418,22 @@ describe('FlyerAiProcessor', () => {
valid_to: '2025-01-07', valid_to: '2025-01-07',
store_address: '123 Batch St', store_address: '123 Batch St',
items: [ items: [
{ item: 'Item A', price_display: '$1', price_in_cents: 100, quantity: '1', category_name: 'Cat A', master_item_id: 1 }, {
{ item: 'Item B', price_display: '$2', price_in_cents: 200, quantity: '1', category_name: 'Cat B', master_item_id: 2 }, item: 'Item A',
price_display: '$1',
price_in_cents: 100,
quantity: '1',
category_name: 'Cat A',
master_item_id: 1,
},
{
item: 'Item B',
price_display: '$2',
price_in_cents: 200,
quantity: '1',
category_name: 'Cat B',
master_item_id: 2,
},
], ],
}; };
@@ -334,7 +443,14 @@ describe('FlyerAiProcessor', () => {
valid_to: null, valid_to: null,
store_address: null, store_address: null,
items: [ items: [
{ item: 'Item C', price_display: '$3', price_in_cents: 300, quantity: '1', category_name: 'Cat C', master_item_id: 3 }, {
item: 'Item C',
price_display: '$3',
price_in_cents: 300,
quantity: '1',
category_name: 'Cat C',
master_item_id: 3,
},
], ],
}; };
@@ -351,8 +467,22 @@ describe('FlyerAiProcessor', () => {
expect(mockAiService.extractCoreDataFromFlyerImage).toHaveBeenCalledTimes(2); expect(mockAiService.extractCoreDataFromFlyerImage).toHaveBeenCalledTimes(2);
// 2. Check the arguments for each call // 2. Check the arguments for each call
expect(mockAiService.extractCoreDataFromFlyerImage).toHaveBeenNthCalledWith(1, imagePaths.slice(0, 4), [], undefined, undefined, logger); expect(mockAiService.extractCoreDataFromFlyerImage).toHaveBeenNthCalledWith(
expect(mockAiService.extractCoreDataFromFlyerImage).toHaveBeenNthCalledWith(2, imagePaths.slice(4, 5), [], undefined, undefined, logger); 1,
imagePaths.slice(0, 4),
[],
undefined,
undefined,
logger,
);
expect(mockAiService.extractCoreDataFromFlyerImage).toHaveBeenNthCalledWith(
2,
imagePaths.slice(4, 5),
[],
undefined,
undefined,
logger,
);
// 3. Check the merged data // 3. Check the merged data
expect(result.data.store_name).toBe('Batch 1 Store'); // Metadata from the first batch expect(result.data.store_name).toBe('Batch 1 Store'); // Metadata from the first batch
@@ -362,11 +492,13 @@ describe('FlyerAiProcessor', () => {
// 4. Check that items from both batches are merged // 4. Check that items from both batches are merged
expect(result.data.items).toHaveLength(3); expect(result.data.items).toHaveLength(3);
expect(result.data.items).toEqual(expect.arrayContaining([ expect(result.data.items).toEqual(
expect.objectContaining({ item: 'Item A' }), expect.arrayContaining([
expect.objectContaining({ item: 'Item B' }), expect.objectContaining({ item: 'Item A' }),
expect.objectContaining({ item: 'Item C' }), expect.objectContaining({ item: 'Item B' }),
])); expect.objectContaining({ item: 'Item C' }),
]),
);
// 5. Check that the job is not flagged for review // 5. Check that the job is not flagged for review
expect(result.needsReview).toBe(false); expect(result.needsReview).toBe(false);
@@ -376,7 +508,11 @@ describe('FlyerAiProcessor', () => {
// Arrange // Arrange
const jobData = createMockJobData({}); const jobData = createMockJobData({});
const imagePaths = [ const imagePaths = [
{ path: 'page1.jpg', mimetype: 'image/jpeg' }, { path: 'page2.jpg', mimetype: 'image/jpeg' }, { path: 'page3.jpg', mimetype: 'image/jpeg' }, { path: 'page4.jpg', mimetype: 'image/jpeg' }, { path: 'page5.jpg', mimetype: 'image/jpeg' }, { path: 'page1.jpg', mimetype: 'image/jpeg' },
{ path: 'page2.jpg', mimetype: 'image/jpeg' },
{ path: 'page3.jpg', mimetype: 'image/jpeg' },
{ path: 'page4.jpg', mimetype: 'image/jpeg' },
{ path: 'page5.jpg', mimetype: 'image/jpeg' },
]; ];
const mockAiResponseBatch1 = { const mockAiResponseBatch1 = {
@@ -385,7 +521,14 @@ describe('FlyerAiProcessor', () => {
valid_to: '2025-01-07', valid_to: '2025-01-07',
store_address: '123 Good St', store_address: '123 Good St',
items: [ items: [
{ item: 'Item A', price_display: '$1', price_in_cents: 100, quantity: '1', category_name: 'Cat A', master_item_id: 1 }, {
item: 'Item A',
price_display: '$1',
price_in_cents: 100,
quantity: '1',
category_name: 'Cat A',
master_item_id: 1,
},
], ],
}; };
@@ -416,11 +559,45 @@ describe('FlyerAiProcessor', () => {
// Arrange // Arrange
const jobData = createMockJobData({}); const jobData = createMockJobData({});
const imagePaths = [ const imagePaths = [
{ path: 'page1.jpg', mimetype: 'image/jpeg' }, { path: 'page2.jpg', mimetype: 'image/jpeg' }, { path: 'page3.jpg', mimetype: 'image/jpeg' }, { path: 'page4.jpg', mimetype: 'image/jpeg' }, { path: 'page5.jpg', mimetype: 'image/jpeg' }, { path: 'page1.jpg', mimetype: 'image/jpeg' },
{ path: 'page2.jpg', mimetype: 'image/jpeg' },
{ path: 'page3.jpg', mimetype: 'image/jpeg' },
{ path: 'page4.jpg', mimetype: 'image/jpeg' },
{ path: 'page5.jpg', mimetype: 'image/jpeg' },
]; ];
const mockAiResponseBatch1 = { store_name: null, valid_from: '2025-01-01', valid_to: '2025-01-07', store_address: null, items: [{ item: 'Item A', price_display: '$1', price_in_cents: 100, quantity: '1', category_name: 'Cat A', master_item_id: 1 }] }; const mockAiResponseBatch1 = {
const mockAiResponseBatch2 = { store_name: 'Batch 2 Store', valid_from: '2025-01-02', valid_to: null, store_address: '456 Subsequent St', items: [{ item: 'Item C', price_display: '$3', price_in_cents: 300, quantity: '1', category_name: 'Cat C', master_item_id: 3 }] }; store_name: null,
valid_from: '2025-01-01',
valid_to: '2025-01-07',
store_address: null,
items: [
{
item: 'Item A',
price_display: '$1',
price_in_cents: 100,
quantity: '1',
category_name: 'Cat A',
master_item_id: 1,
},
],
};
const mockAiResponseBatch2 = {
store_name: 'Batch 2 Store',
valid_from: '2025-01-02',
valid_to: null,
store_address: '456 Subsequent St',
items: [
{
item: 'Item C',
price_display: '$3',
price_in_cents: 300,
quantity: '1',
category_name: 'Cat C',
master_item_id: 3,
},
],
};
vi.mocked(mockAiService.extractCoreDataFromFlyerImage) vi.mocked(mockAiService.extractCoreDataFromFlyerImage)
.mockResolvedValueOnce(mockAiResponseBatch1) .mockResolvedValueOnce(mockAiResponseBatch1)
@@ -453,7 +630,14 @@ describe('FlyerAiProcessor', () => {
valid_to: '2025-02-07', valid_to: '2025-02-07',
store_address: '789 Single St', store_address: '789 Single St',
items: [ items: [
{ item: 'Item X', price_display: '$10', price_in_cents: 1000, quantity: '1', category_name: 'Cat X', master_item_id: 10 }, {
item: 'Item X',
price_display: '$10',
price_in_cents: 1000,
quantity: '1',
category_name: 'Cat X',
master_item_id: 10,
},
], ],
}; };
@@ -468,9 +652,15 @@ describe('FlyerAiProcessor', () => {
expect(mockAiService.extractCoreDataFromFlyerImage).toHaveBeenCalledTimes(1); expect(mockAiService.extractCoreDataFromFlyerImage).toHaveBeenCalledTimes(1);
// 2. Check the arguments for the single call. // 2. Check the arguments for the single call.
expect(mockAiService.extractCoreDataFromFlyerImage).toHaveBeenCalledWith(imagePaths, [], undefined, undefined, logger); expect(mockAiService.extractCoreDataFromFlyerImage).toHaveBeenCalledWith(
imagePaths,
[],
undefined,
undefined,
logger,
);
// 3. Check that the final data matches the single batch's data. // 3. Check that the final data matches the single batch's data.
expect(result.data).toEqual(mockAiResponse); expect(result.data).toEqual(mockAiResponse);
}); });
}); });