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

This commit is contained in:
2025-12-15 01:07:08 -08:00
parent 648f1c0239
commit 0f62a6330e
4 changed files with 46 additions and 44 deletions

View File

@@ -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');
});
});

View File

@@ -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';