// src/controllers/upc.controller.test.ts // ============================================================================ // UPC CONTROLLER UNIT TESTS // ============================================================================ // Unit tests for the UpcController class. These tests verify controller // logic in isolation by mocking the UPC service. // ============================================================================ import { describe, it, expect, vi, beforeEach, afterEach, type Mocked } from 'vitest'; import type { Request as ExpressRequest } from 'express'; // ============================================================================ // MOCK SETUP // ============================================================================ // Mock tsoa decorators and Controller class vi.mock('tsoa', () => ({ Controller: class Controller { protected setStatus(status: number): void { this._status = status; } private _status = 200; }, Get: () => () => {}, Post: () => () => {}, Route: () => () => {}, Tags: () => () => {}, Security: () => () => {}, Path: () => () => {}, Query: () => () => {}, Body: () => () => {}, Request: () => () => {}, SuccessResponse: () => () => {}, Response: () => () => {}, })); // Mock UPC service vi.mock('../services/upcService.server', () => ({ scanUpc: vi.fn(), lookupUpc: vi.fn(), getScanHistory: vi.fn(), getScanById: vi.fn(), getScanStats: vi.fn(), linkUpcToProduct: vi.fn(), })); // Import mocked modules after mock definitions import * as upcService from '../services/upcService.server'; import { UpcController } from './upc.controller'; // Cast mocked modules for type-safe access const mockedUpcService = upcService as Mocked; // ============================================================================ // HELPER FUNCTIONS // ============================================================================ /** * Creates a mock Express request object with authenticated user. */ function createMockRequest(overrides: Partial = {}): ExpressRequest { return { body: {}, params: {}, query: {}, user: createMockUserProfile(), log: { debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn(), }, ...overrides, } as unknown as ExpressRequest; } /** * Creates a mock user profile for testing. */ function createMockUserProfile() { return { full_name: 'Test User', role: 'user' as const, user: { user_id: 'test-user-id', email: 'test@example.com', }, }; } /** * Creates a mock admin user profile. */ function createMockAdminProfile() { return { full_name: 'Admin User', role: 'admin' as const, user: { user_id: 'admin-user-id', email: 'admin@example.com', }, }; } /** * Creates a mock scan result. */ function createMockScanResult() { return { scan_id: 1, upc_code: '012345678901', product: { product_id: 100, name: 'Test Product', brand: 'Test Brand', category: 'Grocery', description: 'A test product', size: '500g', upc_code: '012345678901', image_url: null, master_item_id: 50, }, external_lookup: null, confidence: 0.95, lookup_successful: true, is_new_product: false, scanned_at: '2024-01-01T00:00:00.000Z', }; } // ============================================================================ // TEST SUITE // ============================================================================ describe('UpcController', () => { let controller: UpcController; beforeEach(() => { vi.clearAllMocks(); controller = new UpcController(); }); afterEach(() => { vi.useRealTimers(); }); // ========================================================================== // SCAN ENDPOINTS // ========================================================================== describe('scanUpc()', () => { it('should scan a UPC code successfully', async () => { // Arrange const mockResult = createMockScanResult(); const request = createMockRequest(); mockedUpcService.scanUpc.mockResolvedValue(mockResult); // Act const result = await controller.scanUpc( { upc_code: '012345678901', scan_source: 'manual_entry', }, request, ); // Assert expect(result.success).toBe(true); if (result.success) { expect(result.data.upc_code).toBe('012345678901'); expect(result.data.lookup_successful).toBe(true); } }); it('should reject when neither upc_code nor image provided', async () => { // Arrange const request = createMockRequest(); // Act & Assert await expect(controller.scanUpc({ scan_source: 'manual_entry' }, request)).rejects.toThrow( 'Either upc_code or image_base64 must be provided.', ); }); it('should support image-based scanning', async () => { // Arrange const mockResult = createMockScanResult(); const request = createMockRequest(); mockedUpcService.scanUpc.mockResolvedValue(mockResult); // Act const result = await controller.scanUpc( { image_base64: 'base64encodedimage', scan_source: 'image_upload', }, request, ); // Assert expect(result.success).toBe(true); expect(mockedUpcService.scanUpc).toHaveBeenCalledWith( 'test-user-id', expect.objectContaining({ image_base64: 'base64encodedimage', scan_source: 'image_upload', }), expect.anything(), ); }); it('should log scan requests', async () => { // Arrange const mockResult = createMockScanResult(); const mockLog = { debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn(), }; const request = createMockRequest({ log: mockLog }); mockedUpcService.scanUpc.mockResolvedValue(mockResult); // Act await controller.scanUpc({ upc_code: '012345678901', scan_source: 'manual_entry' }, request); // Assert expect(mockLog.info).toHaveBeenCalledWith( expect.objectContaining({ userId: 'test-user-id', scanSource: 'manual_entry', }), 'UPC scan request received', ); }); }); // ========================================================================== // LOOKUP ENDPOINTS // ========================================================================== describe('lookupUpc()', () => { it('should lookup a UPC code', async () => { // Arrange const mockResult = { upc_code: '012345678901', product: { product_id: 1, name: 'Test' }, external_lookup: null, found: true, from_cache: false, }; const request = createMockRequest(); mockedUpcService.lookupUpc.mockResolvedValue(mockResult); // Act const result = await controller.lookupUpc(request, '012345678901'); // Assert expect(result.success).toBe(true); if (result.success) { expect(result.data.upc_code).toBe('012345678901'); expect(result.data.found).toBe(true); } }); it('should support force refresh option', async () => { // Arrange const mockResult = { upc_code: '012345678901', product: null, external_lookup: null, found: false, from_cache: false, }; const request = createMockRequest(); mockedUpcService.lookupUpc.mockResolvedValue(mockResult); // Act await controller.lookupUpc(request, '012345678901', true, true); // Assert expect(mockedUpcService.lookupUpc).toHaveBeenCalledWith( { upc_code: '012345678901', force_refresh: true }, expect.anything(), ); }); }); // ========================================================================== // HISTORY ENDPOINTS // ========================================================================== describe('getScanHistory()', () => { it('should return scan history with default pagination', async () => { // Arrange const mockResult = { scans: [{ scan_id: 1, upc_code: '012345678901' }], total: 1, }; const request = createMockRequest(); mockedUpcService.getScanHistory.mockResolvedValue(mockResult); // Act const result = await controller.getScanHistory(request); // Assert expect(result.success).toBe(true); if (result.success) { expect(result.data.scans).toHaveLength(1); expect(result.data.total).toBe(1); } expect(mockedUpcService.getScanHistory).toHaveBeenCalledWith( expect.objectContaining({ user_id: 'test-user-id', limit: 50, offset: 0, }), expect.anything(), ); }); it('should cap limit at 100', async () => { // Arrange const mockResult = { scans: [], total: 0 }; const request = createMockRequest(); mockedUpcService.getScanHistory.mockResolvedValue(mockResult); // Act await controller.getScanHistory(request, 200); // Assert expect(mockedUpcService.getScanHistory).toHaveBeenCalledWith( expect.objectContaining({ limit: 100 }), expect.anything(), ); }); it('should support filtering by scan source', async () => { // Arrange const mockResult = { scans: [], total: 0 }; const request = createMockRequest(); mockedUpcService.getScanHistory.mockResolvedValue(mockResult); // Act await controller.getScanHistory(request, 50, 0, undefined, 'camera_scan'); // Assert expect(mockedUpcService.getScanHistory).toHaveBeenCalledWith( expect.objectContaining({ scan_source: 'camera_scan' }), expect.anything(), ); }); }); describe('getScanById()', () => { it('should return a specific scan record', async () => { // Arrange const mockScan = { scan_id: 1, user_id: 'test-user-id', upc_code: '012345678901', product_id: 100, scan_source: 'manual_entry', created_at: '2024-01-01T00:00:00.000Z', }; const request = createMockRequest(); mockedUpcService.getScanById.mockResolvedValue(mockScan); // Act const result = await controller.getScanById(1, request); // Assert expect(result.success).toBe(true); if (result.success) { expect(result.data.scan_id).toBe(1); } expect(mockedUpcService.getScanById).toHaveBeenCalledWith( 1, 'test-user-id', expect.anything(), ); }); }); // ========================================================================== // STATISTICS ENDPOINTS // ========================================================================== describe('getScanStats()', () => { it('should return scan statistics', async () => { // Arrange const mockStats = { total_scans: 100, successful_lookups: 85, unique_products: 50, scans_today: 5, scans_this_week: 20, }; const request = createMockRequest(); mockedUpcService.getScanStats.mockResolvedValue(mockStats); // Act const result = await controller.getScanStats(request); // Assert expect(result.success).toBe(true); if (result.success) { expect(result.data.total_scans).toBe(100); expect(result.data.successful_lookups).toBe(85); } }); }); // ========================================================================== // ADMIN ENDPOINTS // ========================================================================== describe('linkUpcToProduct()', () => { it('should link UPC to product (admin)', async () => { // Arrange const request = createMockRequest({ user: createMockAdminProfile() }); mockedUpcService.linkUpcToProduct.mockResolvedValue(undefined); // Act const result = await controller.linkUpcToProduct( { upc_code: '012345678901', product_id: 100 }, request, ); // Assert expect(result).toBeUndefined(); expect(mockedUpcService.linkUpcToProduct).toHaveBeenCalledWith( 100, '012345678901', expect.anything(), ); }); it('should log link operations', async () => { // Arrange const mockLog = { debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn(), }; const request = createMockRequest({ user: createMockAdminProfile(), log: mockLog, }); mockedUpcService.linkUpcToProduct.mockResolvedValue(undefined); // Act await controller.linkUpcToProduct({ upc_code: '012345678901', product_id: 100 }, request); // Assert expect(mockLog.info).toHaveBeenCalledWith( expect.objectContaining({ productId: 100, upcCode: '012345678901', }), 'UPC link request received', ); }); }); // ========================================================================== // BASE CONTROLLER INTEGRATION // ========================================================================== describe('BaseController integration', () => { it('should use success helper for consistent response format', async () => { // Arrange const mockStats = { total_scans: 0 }; const request = createMockRequest(); mockedUpcService.getScanStats.mockResolvedValue(mockStats); // Act const result = await controller.getScanStats(request); // Assert expect(result).toHaveProperty('success', true); expect(result).toHaveProperty('data'); }); it('should use noContent helper for 204 responses', async () => { // Arrange const request = createMockRequest({ user: createMockAdminProfile() }); mockedUpcService.linkUpcToProduct.mockResolvedValue(undefined); // Act const result = await controller.linkUpcToProduct( { upc_code: '012345678901', product_id: 1 }, request, ); // Assert expect(result).toBeUndefined(); }); }); });