minor test fixes
All checks were successful
Deploy to Test Environment / deploy-to-test (push) Successful in 1h56m3s
All checks were successful
Deploy to Test Environment / deploy-to-test (push) Successful in 1h56m3s
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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'),
|
||||
);
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user