// src/controllers/stats.controller.test.ts // ============================================================================ // STATS CONTROLLER UNIT TESTS // ============================================================================ // Unit tests for the StatsController class. These tests verify controller // logic in isolation by mocking the admin repository. // ============================================================================ 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: () => () => {}, Route: () => () => {}, Tags: () => () => {}, Query: () => () => {}, Request: () => () => {}, SuccessResponse: () => () => {}, })); // Mock admin repository vi.mock('../services/db/index.db', () => ({ adminRepo: { getMostFrequentSaleItems: vi.fn(), }, })); // Import mocked modules after mock definitions import { adminRepo } from '../services/db/index.db'; import { StatsController } from './stats.controller'; // Cast mocked modules for type-safe access const mockedAdminRepo = adminRepo as Mocked; // ============================================================================ // HELPER FUNCTIONS // ============================================================================ /** * Creates a mock Express request object. */ function createMockRequest(overrides: Partial = {}): ExpressRequest { return { body: {}, params: {}, query: {}, log: { debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn(), }, ...overrides, } as unknown as ExpressRequest; } /** * Creates a mock most frequent sale item. */ function createMockSaleItem(overrides: Record = {}) { return { master_item_id: 1, item_name: 'Milk 2%', category_name: 'Dairy & Eggs', sale_count: 15, avg_discount_percent: 25.5, lowest_price_cents: 299, highest_price_cents: 450, ...overrides, }; } // ============================================================================ // TEST SUITE // ============================================================================ describe('StatsController', () => { let controller: StatsController; beforeEach(() => { vi.clearAllMocks(); controller = new StatsController(); }); afterEach(() => { vi.useRealTimers(); }); // ========================================================================== // MOST FREQUENT SALES // ========================================================================== describe('getMostFrequentSales()', () => { it('should return most frequent sale items with default parameters', async () => { // Arrange const mockItems = [ createMockSaleItem(), createMockSaleItem({ master_item_id: 2, item_name: 'Bread', sale_count: 12 }), ]; const request = createMockRequest(); mockedAdminRepo.getMostFrequentSaleItems.mockResolvedValue(mockItems); // Act const result = await controller.getMostFrequentSales(undefined, undefined, request); // Assert expect(result.success).toBe(true); if (result.success) { expect(result.data).toHaveLength(2); expect(result.data[0].item_name).toBe('Milk 2%'); expect(result.data[0].sale_count).toBe(15); } expect(mockedAdminRepo.getMostFrequentSaleItems).toHaveBeenCalledWith( 30, // default days 10, // default limit expect.anything(), ); }); it('should use custom days parameter', async () => { // Arrange const mockItems = [createMockSaleItem()]; const request = createMockRequest(); mockedAdminRepo.getMostFrequentSaleItems.mockResolvedValue(mockItems); // Act await controller.getMostFrequentSales(60, undefined, request); // Assert expect(mockedAdminRepo.getMostFrequentSaleItems).toHaveBeenCalledWith( 60, 10, expect.anything(), ); }); it('should use custom limit parameter', async () => { // Arrange const mockItems = [createMockSaleItem()]; const request = createMockRequest(); mockedAdminRepo.getMostFrequentSaleItems.mockResolvedValue(mockItems); // Act await controller.getMostFrequentSales(undefined, 25, request); // Assert expect(mockedAdminRepo.getMostFrequentSaleItems).toHaveBeenCalledWith( 30, 25, expect.anything(), ); }); it('should cap days at 365', async () => { // Arrange const mockItems = [createMockSaleItem()]; const request = createMockRequest(); mockedAdminRepo.getMostFrequentSaleItems.mockResolvedValue(mockItems); // Act await controller.getMostFrequentSales(500, undefined, request); // Assert expect(mockedAdminRepo.getMostFrequentSaleItems).toHaveBeenCalledWith( 365, 10, expect.anything(), ); }); it('should floor days at 1', async () => { // Arrange const mockItems = [createMockSaleItem()]; const request = createMockRequest(); mockedAdminRepo.getMostFrequentSaleItems.mockResolvedValue(mockItems); // Act await controller.getMostFrequentSales(0, undefined, request); // Assert expect(mockedAdminRepo.getMostFrequentSaleItems).toHaveBeenCalledWith( 1, 10, expect.anything(), ); }); it('should cap limit at 50', async () => { // Arrange const mockItems = [createMockSaleItem()]; const request = createMockRequest(); mockedAdminRepo.getMostFrequentSaleItems.mockResolvedValue(mockItems); // Act await controller.getMostFrequentSales(undefined, 100, request); // Assert expect(mockedAdminRepo.getMostFrequentSaleItems).toHaveBeenCalledWith( 30, 50, expect.anything(), ); }); it('should floor limit at 1', async () => { // Arrange const mockItems = [createMockSaleItem()]; const request = createMockRequest(); mockedAdminRepo.getMostFrequentSaleItems.mockResolvedValue(mockItems); // Act await controller.getMostFrequentSales(undefined, 0, request); // Assert expect(mockedAdminRepo.getMostFrequentSaleItems).toHaveBeenCalledWith( 30, 1, expect.anything(), ); }); it('should handle negative values', async () => { // Arrange const mockItems = [createMockSaleItem()]; const request = createMockRequest(); mockedAdminRepo.getMostFrequentSaleItems.mockResolvedValue(mockItems); // Act await controller.getMostFrequentSales(-10, -5, request); // Assert expect(mockedAdminRepo.getMostFrequentSaleItems).toHaveBeenCalledWith( 1, // floored to 1 1, // floored to 1 expect.anything(), ); }); it('should return empty array when no sale items exist', async () => { // Arrange const request = createMockRequest(); mockedAdminRepo.getMostFrequentSaleItems.mockResolvedValue([]); // Act const result = await controller.getMostFrequentSales(undefined, undefined, request); // Assert expect(result.success).toBe(true); if (result.success) { expect(result.data).toHaveLength(0); } }); it('should handle decimal values by flooring them', async () => { // Arrange const mockItems = [createMockSaleItem()]; const request = createMockRequest(); mockedAdminRepo.getMostFrequentSaleItems.mockResolvedValue(mockItems); // Act await controller.getMostFrequentSales(45.7, 15.3, request); // Assert expect(mockedAdminRepo.getMostFrequentSaleItems).toHaveBeenCalledWith( 45, // floored 15, // floored expect.anything(), ); }); }); // ========================================================================== // PUBLIC ACCESS (NO AUTH REQUIRED) // ========================================================================== describe('Public access', () => { it('should work without user authentication', async () => { // Arrange const mockItems = [createMockSaleItem()]; const request = createMockRequest({ user: undefined }); mockedAdminRepo.getMostFrequentSaleItems.mockResolvedValue(mockItems); // Act const result = await controller.getMostFrequentSales(undefined, undefined, request); // Assert expect(result.success).toBe(true); if (result.success) { expect(result.data).toHaveLength(1); } }); }); // ========================================================================== // BASE CONTROLLER INTEGRATION // ========================================================================== describe('BaseController integration', () => { it('should use success helper for consistent response format', async () => { // Arrange const mockItems = [createMockSaleItem()]; const request = createMockRequest(); mockedAdminRepo.getMostFrequentSaleItems.mockResolvedValue(mockItems); // Act const result = await controller.getMostFrequentSales(undefined, undefined, request); // Assert expect(result).toHaveProperty('success', true); expect(result).toHaveProperty('data'); }); }); });