Refactor: Update test files to improve mock structure and organization
All checks were successful
Deploy to Test Environment / deploy-to-test (push) Successful in 6m57s
All checks were successful
Deploy to Test Environment / deploy-to-test (push) Successful in 6m57s
This commit is contained in:
@@ -1,13 +1,11 @@
|
||||
// src/routes/ai.test.ts
|
||||
// src/routes/ai.routes.test.ts
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import supertest from 'supertest';
|
||||
import { type Request, type Response, type NextFunction } from 'express';
|
||||
import path from 'node:path';
|
||||
import type { Job } from 'bullmq';
|
||||
import aiRouter from './ai.routes';
|
||||
import { createMockUserProfile, createMockFlyer } from '../tests/utils/mockFactories';
|
||||
import * as flyerDb from '../services/db/flyer.db';
|
||||
import * as db from '../services/db/index.db';
|
||||
import { createMockUserProfile, createMockFlyer } from '../tests/utils/mockFactories';
|
||||
import * as aiService from '../services/aiService.server';
|
||||
import { mockLogger } from '../tests/utils/mockLogger';
|
||||
import { createTestApp } from '../tests/utils/createTestApp';
|
||||
@@ -19,17 +17,23 @@ vi.mock('../services/aiService.server', () => ({
|
||||
},
|
||||
}));
|
||||
|
||||
// Mock the specific DB modules used by the AI router.
|
||||
// We mock the standalone `createFlyerAndItems` function from flyer.db
|
||||
vi.mock('../services/db/flyer.db', () => ({
|
||||
createFlyerAndItems: vi.fn(),
|
||||
}));
|
||||
// We mock the repository instances from the main db index
|
||||
vi.mock('../services/db/index.db', () => ({
|
||||
flyerRepo: { findFlyerByChecksum: vi.fn() },
|
||||
adminRepo: { logActivity: vi.fn() },
|
||||
const { mockedDb } = vi.hoisted(() => ({
|
||||
mockedDb: {
|
||||
flyerRepo: {
|
||||
findFlyerByChecksum: vi.fn(),
|
||||
},
|
||||
adminRepo: {
|
||||
logActivity: vi.fn(),
|
||||
},
|
||||
// This function is a standalone export, not part of a repo
|
||||
createFlyerAndItems: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('../services/db/flyer.db', () => ({ createFlyerAndItems: mockedDb.createFlyerAndItems }));
|
||||
|
||||
vi.mock('../services/db/index.db', () => ({ flyerRepo: mockedDb.flyerRepo, adminRepo: mockedDb.adminRepo }));
|
||||
|
||||
// Mock the queue service
|
||||
vi.mock('../services/queueService.server', () => ({
|
||||
flyerQueue: {
|
||||
@@ -66,7 +70,7 @@ describe('AI Routes (/api/ai)', () => {
|
||||
const imagePath = path.resolve(__dirname, '../tests/assets/test-flyer-image.jpg');
|
||||
|
||||
it('should enqueue a job and return 202 on success', async () => {
|
||||
vi.mocked(db.flyerRepo.findFlyerByChecksum).mockResolvedValue(undefined);
|
||||
vi.mocked(mockedDb.flyerRepo.findFlyerByChecksum).mockResolvedValue(undefined);
|
||||
vi.mocked(flyerQueue.add).mockResolvedValue({ id: 'job-123' } as unknown as Job);
|
||||
|
||||
const response = await supertest(app)
|
||||
@@ -99,7 +103,7 @@ describe('AI Routes (/api/ai)', () => {
|
||||
});
|
||||
|
||||
it('should return 409 if flyer checksum already exists', async () => {
|
||||
vi.mocked(db.flyerRepo.findFlyerByChecksum).mockResolvedValue(createMockFlyer({ flyer_id: 99 }));
|
||||
vi.mocked(mockedDb.flyerRepo.findFlyerByChecksum).mockResolvedValue(createMockFlyer({ flyer_id: 99 }));
|
||||
|
||||
const response = await supertest(app)
|
||||
.post('/api/ai/upload-and-process')
|
||||
@@ -111,7 +115,7 @@ describe('AI Routes (/api/ai)', () => {
|
||||
});
|
||||
|
||||
it('should return 500 if enqueuing the job fails', async () => {
|
||||
vi.mocked(db.flyerRepo.findFlyerByChecksum).mockResolvedValue(undefined);
|
||||
vi.mocked(mockedDb.flyerRepo.findFlyerByChecksum).mockResolvedValue(undefined);
|
||||
vi.mocked(flyerQueue.add).mockRejectedValue(new Error('Redis connection failed'));
|
||||
|
||||
const response = await supertest(app)
|
||||
@@ -129,7 +133,7 @@ describe('AI Routes (/api/ai)', () => {
|
||||
const mockUser = createMockUserProfile({ user_id: 'auth-user-1' });
|
||||
const authenticatedApp = createTestApp({ router: aiRouter, basePath: '/api/ai', authenticatedUser: mockUser });
|
||||
|
||||
vi.mocked(db.flyerRepo.findFlyerByChecksum).mockResolvedValue(undefined);
|
||||
vi.mocked(mockedDb.flyerRepo.findFlyerByChecksum).mockResolvedValue(undefined);
|
||||
vi.mocked(flyerQueue.add).mockResolvedValue({ id: 'job-456' } as unknown as Job);
|
||||
|
||||
// Act
|
||||
@@ -209,9 +213,9 @@ describe('AI Routes (/api/ai)', () => {
|
||||
flyer_id: 1,
|
||||
file_name: mockDataPayload.originalFileName,
|
||||
});
|
||||
vi.mocked(db.flyerRepo.findFlyerByChecksum).mockResolvedValue(undefined); // No duplicate
|
||||
vi.mocked(flyerDb.createFlyerAndItems).mockResolvedValue({ flyer: mockFlyer, items: [] });
|
||||
vi.mocked(db.adminRepo.logActivity).mockResolvedValue();
|
||||
vi.mocked(mockedDb.flyerRepo.findFlyerByChecksum).mockResolvedValue(undefined); // No duplicate
|
||||
vi.mocked(mockedDb.createFlyerAndItems).mockResolvedValue({ flyer: mockFlyer, items: [] });
|
||||
vi.mocked(mockedDb.adminRepo.logActivity).mockResolvedValue();
|
||||
|
||||
// Act
|
||||
const response = await supertest(app)
|
||||
@@ -222,7 +226,7 @@ describe('AI Routes (/api/ai)', () => {
|
||||
// Assert
|
||||
expect(response.status).toBe(201);
|
||||
expect(response.body.message).toBe('Flyer processed and saved successfully.');
|
||||
expect(flyerDb.createFlyerAndItems).toHaveBeenCalledTimes(1);
|
||||
expect(mockedDb.createFlyerAndItems).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should return 400 if no flyer image is provided', async () => {
|
||||
@@ -235,7 +239,7 @@ describe('AI Routes (/api/ai)', () => {
|
||||
it('should return 409 Conflict if flyer checksum already exists', async () => {
|
||||
// Arrange
|
||||
const mockExistingFlyer = createMockFlyer({ flyer_id: 99 });
|
||||
vi.mocked(db.flyerRepo.findFlyerByChecksum).mockResolvedValue(mockExistingFlyer); // Duplicate found
|
||||
vi.mocked(mockedDb.flyerRepo.findFlyerByChecksum).mockResolvedValue(mockExistingFlyer); // Duplicate found
|
||||
|
||||
// Act
|
||||
const response = await supertest(app)
|
||||
@@ -246,7 +250,7 @@ describe('AI Routes (/api/ai)', () => {
|
||||
// Assert
|
||||
expect(response.status).toBe(409);
|
||||
expect(response.body.message).toBe('This flyer has already been processed.');
|
||||
expect(flyerDb.createFlyerAndItems).not.toHaveBeenCalled();
|
||||
expect(mockedDb.createFlyerAndItems).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should accept payload when extractedData.items is missing and save with empty items', async () => {
|
||||
@@ -257,12 +261,12 @@ describe('AI Routes (/api/ai)', () => {
|
||||
extractedData: { store_name: 'Partial Store' } // no items key
|
||||
};
|
||||
|
||||
vi.mocked(db.flyerRepo.findFlyerByChecksum).mockResolvedValue(undefined);
|
||||
vi.mocked(mockedDb.flyerRepo.findFlyerByChecksum).mockResolvedValue(undefined);
|
||||
const mockFlyer = createMockFlyer({
|
||||
flyer_id: 2,
|
||||
file_name: partialPayload.originalFileName,
|
||||
});
|
||||
vi.mocked(flyerDb.createFlyerAndItems).mockResolvedValue({ flyer: mockFlyer, items: [] });
|
||||
vi.mocked(mockedDb.createFlyerAndItems).mockResolvedValue({ flyer: mockFlyer, items: [] });
|
||||
|
||||
const response = await supertest(app)
|
||||
.post('/api/ai/flyers/process')
|
||||
@@ -270,9 +274,9 @@ describe('AI Routes (/api/ai)', () => {
|
||||
.attach('flyerImage', imagePath);
|
||||
|
||||
expect(response.status).toBe(201);
|
||||
expect(flyerDb.createFlyerAndItems).toHaveBeenCalledTimes(1);
|
||||
expect(mockedDb.createFlyerAndItems).toHaveBeenCalledTimes(1);
|
||||
// verify the items array passed to DB was an empty array
|
||||
const callArgs = vi.mocked(flyerDb.createFlyerAndItems).mock.calls[0]?.[1];
|
||||
const callArgs = vi.mocked(mockedDb.createFlyerAndItems).mock.calls[0]?.[1];
|
||||
expect(callArgs).toBeDefined();
|
||||
expect(Array.isArray(callArgs)).toBe(true);
|
||||
// use non-null assertion for the runtime-checked variable so TypeScript is satisfied
|
||||
@@ -286,12 +290,12 @@ describe('AI Routes (/api/ai)', () => {
|
||||
extractedData: { items: [] } // store_name missing
|
||||
};
|
||||
|
||||
vi.mocked(db.flyerRepo.findFlyerByChecksum).mockResolvedValue(undefined);
|
||||
vi.mocked(mockedDb.flyerRepo.findFlyerByChecksum).mockResolvedValue(undefined);
|
||||
const mockFlyer = createMockFlyer({
|
||||
flyer_id: 3,
|
||||
file_name: payloadNoStore.originalFileName,
|
||||
});
|
||||
vi.mocked(flyerDb.createFlyerAndItems).mockResolvedValue({ flyer: mockFlyer, items: [] });
|
||||
vi.mocked(mockedDb.createFlyerAndItems).mockResolvedValue({ flyer: mockFlyer, items: [] });
|
||||
|
||||
const response = await supertest(app)
|
||||
.post('/api/ai/flyers/process')
|
||||
@@ -299,15 +303,15 @@ describe('AI Routes (/api/ai)', () => {
|
||||
.attach('flyerImage', imagePath);
|
||||
|
||||
expect(response.status).toBe(201);
|
||||
expect(flyerDb.createFlyerAndItems).toHaveBeenCalledTimes(1);
|
||||
expect(mockedDb.createFlyerAndItems).toHaveBeenCalledTimes(1);
|
||||
// verify the flyerData.store_name passed to DB was the fallback string
|
||||
const flyerDataArg = vi.mocked(flyerDb.createFlyerAndItems).mock.calls[0][0];
|
||||
const flyerDataArg = vi.mocked(mockedDb.createFlyerAndItems).mock.calls[0][0];
|
||||
expect(flyerDataArg.store_name).toContain('Unknown Store');
|
||||
});
|
||||
|
||||
it('should handle a generic error during flyer creation', async () => {
|
||||
vi.mocked(db.flyerRepo.findFlyerByChecksum).mockResolvedValue(undefined);
|
||||
vi.mocked(flyerDb.createFlyerAndItems).mockRejectedValue(new Error('DB transaction failed'));
|
||||
vi.mocked(mockedDb.flyerRepo.findFlyerByChecksum).mockResolvedValue(undefined);
|
||||
vi.mocked(mockedDb.createFlyerAndItems).mockRejectedValue(new Error('DB transaction failed'));
|
||||
|
||||
const response = await supertest(app)
|
||||
.post('/api/ai/flyers/process')
|
||||
@@ -328,8 +332,8 @@ describe('AI Routes (/api/ai)', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
const mockFlyer = createMockFlyer({ flyer_id: 1 });
|
||||
vi.mocked(db.flyerRepo.findFlyerByChecksum).mockResolvedValue(undefined);
|
||||
vi.mocked(flyerDb.createFlyerAndItems).mockResolvedValue({ flyer: mockFlyer, items: [] });
|
||||
vi.mocked(mockedDb.flyerRepo.findFlyerByChecksum).mockResolvedValue(undefined);
|
||||
vi.mocked(mockedDb.createFlyerAndItems).mockResolvedValue({ flyer: mockFlyer, items: [] });
|
||||
});
|
||||
|
||||
it('should handle payload where "data" field is an object, not stringified JSON', async () => {
|
||||
@@ -339,7 +343,7 @@ describe('AI Routes (/api/ai)', () => {
|
||||
.attach('flyerImage', imagePath);
|
||||
|
||||
expect(response.status).toBe(201);
|
||||
expect(flyerDb.createFlyerAndItems).toHaveBeenCalledTimes(1);
|
||||
expect(mockedDb.createFlyerAndItems).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should handle payload where extractedData is at the root of the body', async () => {
|
||||
@@ -353,8 +357,8 @@ describe('AI Routes (/api/ai)', () => {
|
||||
.attach('flyerImage', imagePath);
|
||||
|
||||
expect(response.status).toBe(201);
|
||||
expect(flyerDb.createFlyerAndItems).toHaveBeenCalledTimes(1);
|
||||
const flyerDataArg = vi.mocked(flyerDb.createFlyerAndItems).mock.calls[0][0];
|
||||
expect(mockedDb.createFlyerAndItems).toHaveBeenCalledTimes(1);
|
||||
const flyerDataArg = vi.mocked(mockedDb.createFlyerAndItems).mock.calls[0][0];
|
||||
expect(flyerDataArg.store_name).toBe('Root Store');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// src/routes/ai.ts
|
||||
// src/routes/ai.routes.ts
|
||||
import { Router, Request, Response, NextFunction } from 'express';
|
||||
import multer from 'multer';
|
||||
import path from 'path';
|
||||
|
||||
Reference in New Issue
Block a user