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();
mockFetchAddress = vi.fn();
// FIXED: Use function name checking for stability instead of call count.
// This prevents mocks from swapping if render order changes.
mockedUseApi.mockImplementation((fn: any) => {
const name = fn?.name;
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
// Robust mock implementation based on argument types.
// This handles the two useApi calls (Geocode: string input, Fetch: number input)
// without relying on unstable function names or render order.
mockedUseApi.mockImplementation(() => {
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,
error: null,
data: null,

View File

@@ -74,6 +74,10 @@ vi.mock('./passport.routes', () => ({
describe('AI Routes (/api/ai)', () => {
beforeEach(() => {
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' });
@@ -163,7 +167,7 @@ describe('AI Routes (/api/ai)', () => {
it('should return 500 if enqueuing the job fails', async () => {
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)
.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 () => {
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)
.post('/api/ai/flyers/process')
@@ -447,7 +453,7 @@ describe('AI Routes (/api/ai)', () => {
it('should return 500 on a generic error', async () => {
// To trigger the catch block, we can cause the middleware to fail.
// 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');
});
// 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 () => {
// An empty buffer can sometimes cause underlying libraries to throw an error
// 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');
});
const response = await supertest(app)
@@ -532,7 +538,7 @@ describe('AI Routes (/api/ai)', () => {
it('should return 500 on a generic error', async () => {
// An empty buffer can sometimes cause underlying libraries to throw an error
// 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');
});
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 () => {
const mockResult = { text: 'Rescanned Text' };
vi.mocked(aiService.aiService.extractTextFromImageArea).mockResolvedValue(mockResult);
vi.mocked(aiService.aiService.extractTextFromImageArea).mockResolvedValueOnce(mockResult);
const response = await supertest(app)
.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 () => {
vi.mocked(aiService.aiService.extractTextFromImageArea).mockRejectedValue(
vi.mocked(aiService.aiService.extractTextFromImageArea).mockRejectedValueOnce(
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 () => {
// 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');
});
const response = await supertest(app)
@@ -663,7 +669,7 @@ describe('AI Routes (/api/ai)', () => {
it('POST /plan-trip should return result on success', async () => {
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)
.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 () => {
vi.mocked(aiService.aiService.planTripWithMaps).mockRejectedValue(
vi.mocked(aiService.aiService.planTripWithMaps).mockRejectedValueOnce(
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.' });
}
logger.debug(
{ filename: req.file.originalname, size: req.file.size, checksum: req.body.checksum },
'Handling /upload-and-process',
);
const { checksum } = req.body;
// Check for duplicate flyer using checksum before even creating a job
const existingFlyer = await db.flyerRepo.findFlyerByChecksum(checksum, req.log);
if (existingFlyer) {
logger.warn(`Duplicate flyer upload attempt blocked for checksum: ${checksum}`);
// Use 409 Conflict for duplicates
return res
.status(409)
.json({
message: 'This flyer has already been processed.',
flyerId: existingFlyer.flyer_id,
});
return res.status(409).json({
message: 'This flyer has already been processed.',
flyerId: existingFlyer.flyer_id,
});
}
const userProfile = req.user as UserProfile | undefined;
@@ -302,7 +305,7 @@ router.post(
// Diagnostic & tolerant parsing for flyers/process
logger.debug(
{ 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:');
@@ -582,12 +585,10 @@ router.post(
try {
const { items } = req.body;
logger.info(`Server-side price comparison requested for ${items.length} items.`);
res
.status(200)
.json({
text: 'This is a server-generated price comparison. Milk is cheaper at SuperMart.',
sources: [],
}); // Stubbed response
res.status(200).json({
text: 'This is a server-generated price comparison. Milk is cheaper at SuperMart.',
sources: [],
}); // Stubbed response
} catch (error) {
next(error);
}
@@ -601,11 +602,11 @@ router.post(
async (req, res, next: NextFunction) => {
try {
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);
res.status(200).json(result);
} 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);
}
},
@@ -657,6 +658,11 @@ router.post(
const { extractionType } = req.body;
const { path, mimetype } = req.file;
logger.debug(
{ extractionType, cropArea, filename: req.file.originalname },
'Rescan area requested',
);
const result = await aiService.aiService.extractTextFromImageArea(
path,
mimetype,