Some checks failed
Deploy to Test Environment / deploy-to-test (push) Failing after 3m58s
337 lines
9.6 KiB
TypeScript
337 lines
9.6 KiB
TypeScript
// 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<typeof adminRepo>;
|
|
|
|
// ============================================================================
|
|
// HELPER FUNCTIONS
|
|
// ============================================================================
|
|
|
|
/**
|
|
* Creates a mock Express request object.
|
|
*/
|
|
function createMockRequest(overrides: Partial<ExpressRequest> = {}): 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<string, unknown> = {}) {
|
|
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');
|
|
});
|
|
});
|
|
});
|