minor test fixes
All checks were successful
Deploy to Test Environment / deploy-to-test (push) Successful in 1h56m3s

This commit is contained in:
2025-12-22 10:21:50 -08:00
parent a10f84aa48
commit 22513a967b
3 changed files with 48 additions and 51 deletions

View File

@@ -57,33 +57,18 @@ describe('useProfileAddress Hook', () => {
mockGeocode = vi.fn(); mockGeocode = vi.fn();
mockFetchAddress = vi.fn(); mockFetchAddress = vi.fn();
// FIXED: Use function name checking for stability instead of call count. // Robust mock implementation based on argument types.
// This prevents mocks from swapping if render order changes. // This handles the two useApi calls (Geocode: string input, Fetch: number input)
mockedUseApi.mockImplementation((fn: any) => { // without relying on unstable function names or render order.
const name = fn?.name; mockedUseApi.mockImplementation(() => {
if (name === 'geocodeWrapper') {
return {
execute: mockGeocode,
loading: false,
error: null,
data: null,
reset: vi.fn(),
isRefetching: false,
};
}
if (name === 'fetchAddressWrapper') {
return {
execute: mockFetchAddress,
loading: false,
error: null,
data: null,
reset: vi.fn(),
isRefetching: false,
};
}
// Default fallback
return { return {
execute: vi.fn(), execute: vi.fn(async (arg: any) => {
if (typeof arg === 'string') {
return mockGeocode(arg);
} else if (typeof arg === 'number') {
return mockFetchAddress(arg);
}
}),
loading: false, loading: false,
error: null, error: null,
data: null, data: null,

View File

@@ -74,6 +74,10 @@ vi.mock('./passport.routes', () => ({
describe('AI Routes (/api/ai)', () => { describe('AI Routes (/api/ai)', () => {
beforeEach(() => { beforeEach(() => {
vi.clearAllMocks(); vi.clearAllMocks();
// Reset logger implementation to no-op to prevent "Logging failed" leaks from previous tests
vi.mocked(mockLogger.info).mockImplementation(() => {});
vi.mocked(mockLogger.error).mockImplementation(() => {});
vi.mocked(mockLogger.warn).mockImplementation(() => {});
}); });
const app = createTestApp({ router: aiRouter, basePath: '/api/ai' }); const app = createTestApp({ router: aiRouter, basePath: '/api/ai' });
@@ -163,7 +167,7 @@ describe('AI Routes (/api/ai)', () => {
it('should return 500 if enqueuing the job fails', async () => { it('should return 500 if enqueuing the job fails', async () => {
vi.mocked(mockedDb.flyerRepo.findFlyerByChecksum).mockResolvedValue(undefined); vi.mocked(mockedDb.flyerRepo.findFlyerByChecksum).mockResolvedValue(undefined);
vi.mocked(flyerQueue.add).mockRejectedValue(new Error('Redis connection failed')); vi.mocked(flyerQueue.add).mockRejectedValueOnce(new Error('Redis connection failed'));
const response = await supertest(app) const response = await supertest(app)
.post('/api/ai/upload-and-process') .post('/api/ai/upload-and-process')
@@ -379,7 +383,9 @@ describe('AI Routes (/api/ai)', () => {
it('should handle a generic error during flyer creation', async () => { it('should handle a generic error during flyer creation', async () => {
vi.mocked(mockedDb.flyerRepo.findFlyerByChecksum).mockResolvedValue(undefined); vi.mocked(mockedDb.flyerRepo.findFlyerByChecksum).mockResolvedValue(undefined);
vi.mocked(mockedDb.createFlyerAndItems).mockRejectedValue(new Error('DB transaction failed')); vi.mocked(mockedDb.createFlyerAndItems).mockRejectedValueOnce(
new Error('DB transaction failed'),
);
const response = await supertest(app) const response = await supertest(app)
.post('/api/ai/flyers/process') .post('/api/ai/flyers/process')
@@ -447,7 +453,7 @@ describe('AI Routes (/api/ai)', () => {
it('should return 500 on a generic error', async () => { it('should return 500 on a generic error', async () => {
// To trigger the catch block, we can cause the middleware to fail. // To trigger the catch block, we can cause the middleware to fail.
// Mock logger.info to throw, which is inside the try block. // Mock logger.info to throw, which is inside the try block.
vi.mocked(mockLogger.info).mockImplementation(() => { vi.mocked(mockLogger.info).mockImplementationOnce(() => {
throw new Error('Logging failed'); throw new Error('Logging failed');
}); });
// Attach a valid file to get past the `if (!req.file)` check. // Attach a valid file to get past the `if (!req.file)` check.
@@ -504,7 +510,7 @@ describe('AI Routes (/api/ai)', () => {
it('should return 500 on a generic error', async () => { it('should return 500 on a generic error', async () => {
// An empty buffer can sometimes cause underlying libraries to throw an error // An empty buffer can sometimes cause underlying libraries to throw an error
// To reliably trigger the catch block, mock the logger to throw. // To reliably trigger the catch block, mock the logger to throw.
vi.mocked(mockLogger.info).mockImplementation(() => { vi.mocked(mockLogger.info).mockImplementationOnce(() => {
throw new Error('Logging failed'); throw new Error('Logging failed');
}); });
const response = await supertest(app) const response = await supertest(app)
@@ -532,7 +538,7 @@ describe('AI Routes (/api/ai)', () => {
it('should return 500 on a generic error', async () => { it('should return 500 on a generic error', async () => {
// An empty buffer can sometimes cause underlying libraries to throw an error // An empty buffer can sometimes cause underlying libraries to throw an error
// To reliably trigger the catch block, mock the logger to throw. // To reliably trigger the catch block, mock the logger to throw.
vi.mocked(mockLogger.info).mockImplementation(() => { vi.mocked(mockLogger.info).mockImplementationOnce(() => {
throw new Error('Logging failed'); throw new Error('Logging failed');
}); });
const response = await supertest(app) const response = await supertest(app)
@@ -559,7 +565,7 @@ describe('AI Routes (/api/ai)', () => {
it('should call the AI service and return the result on success (authenticated)', async () => { it('should call the AI service and return the result on success (authenticated)', async () => {
const mockResult = { text: 'Rescanned Text' }; const mockResult = { text: 'Rescanned Text' };
vi.mocked(aiService.aiService.extractTextFromImageArea).mockResolvedValue(mockResult); vi.mocked(aiService.aiService.extractTextFromImageArea).mockResolvedValueOnce(mockResult);
const response = await supertest(app) const response = await supertest(app)
.post('/api/ai/rescan-area') .post('/api/ai/rescan-area')
@@ -573,7 +579,7 @@ describe('AI Routes (/api/ai)', () => {
}); });
it('should return 500 if the AI service throws an error (authenticated)', async () => { it('should return 500 if the AI service throws an error (authenticated)', async () => {
vi.mocked(aiService.aiService.extractTextFromImageArea).mockRejectedValue( vi.mocked(aiService.aiService.extractTextFromImageArea).mockRejectedValueOnce(
new Error('AI API is down'), new Error('AI API is down'),
); );
@@ -613,7 +619,7 @@ describe('AI Routes (/api/ai)', () => {
it('POST /quick-insights should return 500 on a generic error', async () => { it('POST /quick-insights should return 500 on a generic error', async () => {
// To hit the catch block, we can simulate an error by making the logger throw. // To hit the catch block, we can simulate an error by making the logger throw.
vi.mocked(mockLogger.info).mockImplementation(() => { vi.mocked(mockLogger.info).mockImplementationOnce(() => {
throw new Error('Logging failed'); throw new Error('Logging failed');
}); });
const response = await supertest(app) const response = await supertest(app)
@@ -663,7 +669,7 @@ describe('AI Routes (/api/ai)', () => {
it('POST /plan-trip should return result on success', async () => { it('POST /plan-trip should return result on success', async () => {
const mockResult = { text: 'Trip plan', sources: [] }; const mockResult = { text: 'Trip plan', sources: [] };
vi.mocked(aiService.aiService.planTripWithMaps).mockResolvedValue(mockResult); vi.mocked(aiService.aiService.planTripWithMaps).mockResolvedValueOnce(mockResult);
const response = await supertest(app) const response = await supertest(app)
.post('/api/ai/plan-trip') .post('/api/ai/plan-trip')
@@ -678,7 +684,7 @@ describe('AI Routes (/api/ai)', () => {
}); });
it('POST /plan-trip should return 500 if the AI service fails', async () => { it('POST /plan-trip should return 500 if the AI service fails', async () => {
vi.mocked(aiService.aiService.planTripWithMaps).mockRejectedValue( vi.mocked(aiService.aiService.planTripWithMaps).mockRejectedValueOnce(
new Error('Maps API key invalid'), new Error('Maps API key invalid'),
); );

View File

@@ -200,18 +200,21 @@ router.post(
return res.status(400).json({ message: 'A flyer file (PDF or image) is required.' }); return res.status(400).json({ message: 'A flyer file (PDF or image) is required.' });
} }
logger.debug(
{ filename: req.file.originalname, size: req.file.size, checksum: req.body.checksum },
'Handling /upload-and-process',
);
const { checksum } = req.body; const { checksum } = req.body;
// Check for duplicate flyer using checksum before even creating a job // Check for duplicate flyer using checksum before even creating a job
const existingFlyer = await db.flyerRepo.findFlyerByChecksum(checksum, req.log); const existingFlyer = await db.flyerRepo.findFlyerByChecksum(checksum, req.log);
if (existingFlyer) { if (existingFlyer) {
logger.warn(`Duplicate flyer upload attempt blocked for checksum: ${checksum}`); logger.warn(`Duplicate flyer upload attempt blocked for checksum: ${checksum}`);
// Use 409 Conflict for duplicates // Use 409 Conflict for duplicates
return res return res.status(409).json({
.status(409) message: 'This flyer has already been processed.',
.json({ flyerId: existingFlyer.flyer_id,
message: 'This flyer has already been processed.', });
flyerId: existingFlyer.flyer_id,
});
} }
const userProfile = req.user as UserProfile | undefined; const userProfile = req.user as UserProfile | undefined;
@@ -302,7 +305,7 @@ router.post(
// Diagnostic & tolerant parsing for flyers/process // Diagnostic & tolerant parsing for flyers/process
logger.debug( logger.debug(
{ keys: Object.keys(req.body || {}) }, { keys: Object.keys(req.body || {}) },
'[API /ai/flyers/process] req.body keys:', '[API /ai/flyers/process] Processing legacy upload',
); );
logger.debug({ filePresent: !!req.file }, '[API /ai/flyers/process] file present:'); logger.debug({ filePresent: !!req.file }, '[API /ai/flyers/process] file present:');
@@ -582,12 +585,10 @@ router.post(
try { try {
const { items } = req.body; const { items } = req.body;
logger.info(`Server-side price comparison requested for ${items.length} items.`); logger.info(`Server-side price comparison requested for ${items.length} items.`);
res res.status(200).json({
.status(200) text: 'This is a server-generated price comparison. Milk is cheaper at SuperMart.',
.json({ sources: [],
text: 'This is a server-generated price comparison. Milk is cheaper at SuperMart.', }); // Stubbed response
sources: [],
}); // Stubbed response
} catch (error) { } catch (error) {
next(error); next(error);
} }
@@ -601,11 +602,11 @@ router.post(
async (req, res, next: NextFunction) => { async (req, res, next: NextFunction) => {
try { try {
const { items, store, userLocation } = req.body; const { items, store, userLocation } = req.body;
logger.info(`Server-side trip planning requested for user.`); logger.debug({ itemCount: items.length, storeName: store.name }, 'Trip planning requested.');
const result = await aiService.aiService.planTripWithMaps(items, store, userLocation); const result = await aiService.aiService.planTripWithMaps(items, store, userLocation);
res.status(200).json(result); res.status(200).json(result);
} catch (error) { } catch (error) {
logger.error({ error }, 'Error in /api/ai/plan-trip endpoint:'); logger.error({ error: errMsg(error) }, 'Error in /api/ai/plan-trip endpoint:');
next(error); next(error);
} }
}, },
@@ -657,6 +658,11 @@ router.post(
const { extractionType } = req.body; const { extractionType } = req.body;
const { path, mimetype } = req.file; const { path, mimetype } = req.file;
logger.debug(
{ extractionType, cropArea, filename: req.file.originalname },
'Rescan area requested',
);
const result = await aiService.aiService.extractTextFromImageArea( const result = await aiService.aiService.extractTextFromImageArea(
path, path,
mimetype, mimetype,