All checks were successful
Deploy to Test Environment / deploy-to-test (push) Successful in 25m5s
384 lines
11 KiB
TypeScript
384 lines
11 KiB
TypeScript
// src/controllers/price.controller.test.ts
|
|
// ============================================================================
|
|
// PRICE CONTROLLER UNIT TESTS
|
|
// ============================================================================
|
|
// Unit tests for the PriceController class. These tests verify controller
|
|
// logic in isolation by mocking the price repository.
|
|
// ============================================================================
|
|
|
|
import { describe, it, expect, vi, beforeEach, afterEach, type Mocked } from 'vitest';
|
|
import type { Request as ExpressRequest } from 'express';
|
|
import { createMockLogger } from '../tests/utils/testHelpers';
|
|
|
|
// ============================================================================
|
|
// 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;
|
|
},
|
|
Post: () => () => {},
|
|
Route: () => () => {},
|
|
Tags: () => () => {},
|
|
Security: () => () => {},
|
|
Body: () => () => {},
|
|
Request: () => () => {},
|
|
SuccessResponse: () => () => {},
|
|
Response: () => () => {},
|
|
}));
|
|
|
|
// Mock price repository
|
|
vi.mock('../services/db/price.db', () => ({
|
|
priceRepo: {
|
|
getPriceHistory: vi.fn(),
|
|
},
|
|
}));
|
|
|
|
// Import mocked modules after mock definitions
|
|
import { priceRepo } from '../services/db/price.db';
|
|
import { PriceController } from './price.controller';
|
|
|
|
// Cast mocked modules for type-safe access
|
|
const mockedPriceRepo = priceRepo as Mocked<typeof priceRepo>;
|
|
|
|
// ============================================================================
|
|
// HELPER FUNCTIONS
|
|
// ============================================================================
|
|
|
|
/**
|
|
* Creates a mock Express request object with authenticated user.
|
|
*/
|
|
function createMockRequest(overrides: Partial<ExpressRequest> = {}): ExpressRequest {
|
|
return {
|
|
body: {},
|
|
params: {},
|
|
query: {},
|
|
user: createMockUserProfile(),
|
|
log: createMockLogger(),
|
|
...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 price history data point.
|
|
* Matches the PriceHistoryData interface from types.ts
|
|
*/
|
|
function createMockPriceHistoryData(overrides: Record<string, unknown> = {}) {
|
|
return {
|
|
master_item_id: 1,
|
|
price_in_cents: 350,
|
|
date: '2024-01-15',
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
// ============================================================================
|
|
// TEST SUITE
|
|
// ============================================================================
|
|
|
|
describe('PriceController', () => {
|
|
let controller: PriceController;
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
controller = new PriceController();
|
|
});
|
|
|
|
afterEach(() => {
|
|
vi.useRealTimers();
|
|
});
|
|
|
|
// ==========================================================================
|
|
// GET PRICE HISTORY
|
|
// ==========================================================================
|
|
|
|
describe('getPriceHistory()', () => {
|
|
it('should return price history for specified items', async () => {
|
|
// Arrange
|
|
const mockPriceHistory = [
|
|
createMockPriceHistoryData(),
|
|
createMockPriceHistoryData({ date: '2024-01-08', price_in_cents: 399 }),
|
|
createMockPriceHistoryData({ master_item_id: 2, price_in_cents: 450 }),
|
|
];
|
|
const request = createMockRequest();
|
|
|
|
mockedPriceRepo.getPriceHistory.mockResolvedValue(mockPriceHistory);
|
|
|
|
// Act
|
|
const result = await controller.getPriceHistory(request, {
|
|
masterItemIds: [1, 2],
|
|
});
|
|
|
|
// Assert
|
|
expect(result.success).toBe(true);
|
|
if (result.success) {
|
|
expect(result.data).toHaveLength(3);
|
|
expect(result.data[0].price_in_cents).toBe(350);
|
|
}
|
|
expect(mockedPriceRepo.getPriceHistory).toHaveBeenCalledWith(
|
|
[1, 2],
|
|
expect.anything(),
|
|
1000, // default limit
|
|
0, // default offset
|
|
);
|
|
});
|
|
|
|
it('should use default limit and offset when not provided', async () => {
|
|
// Arrange
|
|
const mockPriceHistory = [createMockPriceHistoryData()];
|
|
const request = createMockRequest();
|
|
|
|
mockedPriceRepo.getPriceHistory.mockResolvedValue(mockPriceHistory);
|
|
|
|
// Act
|
|
await controller.getPriceHistory(request, {
|
|
masterItemIds: [1],
|
|
});
|
|
|
|
// Assert
|
|
expect(mockedPriceRepo.getPriceHistory).toHaveBeenCalledWith([1], expect.anything(), 1000, 0);
|
|
});
|
|
|
|
it('should use custom limit and offset', async () => {
|
|
// Arrange
|
|
const mockPriceHistory = [createMockPriceHistoryData()];
|
|
const request = createMockRequest();
|
|
|
|
mockedPriceRepo.getPriceHistory.mockResolvedValue(mockPriceHistory);
|
|
|
|
// Act
|
|
await controller.getPriceHistory(request, {
|
|
masterItemIds: [1],
|
|
limit: 500,
|
|
offset: 100,
|
|
});
|
|
|
|
// Assert
|
|
expect(mockedPriceRepo.getPriceHistory).toHaveBeenCalledWith(
|
|
[1],
|
|
expect.anything(),
|
|
500,
|
|
100,
|
|
);
|
|
});
|
|
|
|
it('should return error when masterItemIds is empty', async () => {
|
|
// Arrange
|
|
const request = createMockRequest();
|
|
|
|
// Act
|
|
const result = await controller.getPriceHistory(request, {
|
|
masterItemIds: [],
|
|
});
|
|
|
|
// Assert
|
|
expect(result.success).toBe(false);
|
|
});
|
|
|
|
it('should return error when masterItemIds is not an array', async () => {
|
|
// Arrange
|
|
const request = createMockRequest();
|
|
|
|
// Act
|
|
const result = await controller.getPriceHistory(request, {
|
|
masterItemIds: null as unknown as number[],
|
|
});
|
|
|
|
// Assert
|
|
expect(result.success).toBe(false);
|
|
});
|
|
|
|
it('should normalize limit to at least 1', async () => {
|
|
// Arrange
|
|
const mockPriceHistory = [createMockPriceHistoryData()];
|
|
const request = createMockRequest();
|
|
|
|
mockedPriceRepo.getPriceHistory.mockResolvedValue(mockPriceHistory);
|
|
|
|
// Act
|
|
await controller.getPriceHistory(request, {
|
|
masterItemIds: [1],
|
|
limit: 0,
|
|
});
|
|
|
|
// Assert
|
|
expect(mockedPriceRepo.getPriceHistory).toHaveBeenCalledWith(
|
|
[1],
|
|
expect.anything(),
|
|
1, // floored to 1
|
|
0,
|
|
);
|
|
});
|
|
|
|
it('should normalize offset to at least 0', async () => {
|
|
// Arrange
|
|
const mockPriceHistory = [createMockPriceHistoryData()];
|
|
const request = createMockRequest();
|
|
|
|
mockedPriceRepo.getPriceHistory.mockResolvedValue(mockPriceHistory);
|
|
|
|
// Act
|
|
await controller.getPriceHistory(request, {
|
|
masterItemIds: [1],
|
|
offset: -10,
|
|
});
|
|
|
|
// Assert
|
|
expect(mockedPriceRepo.getPriceHistory).toHaveBeenCalledWith(
|
|
[1],
|
|
expect.anything(),
|
|
1000,
|
|
0, // floored to 0
|
|
);
|
|
});
|
|
|
|
it('should log request details', async () => {
|
|
// Arrange
|
|
const mockPriceHistory = [createMockPriceHistoryData()];
|
|
const mockLog = createMockLogger();
|
|
const request = createMockRequest({ log: mockLog });
|
|
|
|
mockedPriceRepo.getPriceHistory.mockResolvedValue(mockPriceHistory);
|
|
|
|
// Act
|
|
await controller.getPriceHistory(request, {
|
|
masterItemIds: [1, 2, 3],
|
|
limit: 100,
|
|
offset: 50,
|
|
});
|
|
|
|
// Assert
|
|
expect(mockLog.info).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
itemCount: 3,
|
|
limit: 100,
|
|
offset: 50,
|
|
}),
|
|
'[API /price-history] Received request for historical price data.',
|
|
);
|
|
});
|
|
|
|
it('should return empty array when no price history exists', async () => {
|
|
// Arrange
|
|
const request = createMockRequest();
|
|
|
|
mockedPriceRepo.getPriceHistory.mockResolvedValue([]);
|
|
|
|
// Act
|
|
const result = await controller.getPriceHistory(request, {
|
|
masterItemIds: [1],
|
|
});
|
|
|
|
// Assert
|
|
expect(result.success).toBe(true);
|
|
if (result.success) {
|
|
expect(result.data).toHaveLength(0);
|
|
}
|
|
});
|
|
|
|
it('should handle single item request', async () => {
|
|
// Arrange
|
|
const mockPriceHistory = [
|
|
createMockPriceHistoryData(),
|
|
createMockPriceHistoryData({ date: '2024-01-08' }),
|
|
];
|
|
const request = createMockRequest();
|
|
|
|
mockedPriceRepo.getPriceHistory.mockResolvedValue(mockPriceHistory);
|
|
|
|
// Act
|
|
const result = await controller.getPriceHistory(request, {
|
|
masterItemIds: [1],
|
|
});
|
|
|
|
// Assert
|
|
expect(result.success).toBe(true);
|
|
if (result.success) {
|
|
expect(result.data).toHaveLength(2);
|
|
}
|
|
expect(mockedPriceRepo.getPriceHistory).toHaveBeenCalledWith([1], expect.anything(), 1000, 0);
|
|
});
|
|
|
|
it('should handle multiple items request', async () => {
|
|
// Arrange
|
|
const mockPriceHistory = [
|
|
createMockPriceHistoryData({ master_item_id: 1 }),
|
|
createMockPriceHistoryData({ master_item_id: 2 }),
|
|
createMockPriceHistoryData({ master_item_id: 3 }),
|
|
];
|
|
const request = createMockRequest();
|
|
|
|
mockedPriceRepo.getPriceHistory.mockResolvedValue(mockPriceHistory);
|
|
|
|
// Act
|
|
const result = await controller.getPriceHistory(request, {
|
|
masterItemIds: [1, 2, 3, 4, 5],
|
|
});
|
|
|
|
// Assert
|
|
expect(result.success).toBe(true);
|
|
expect(mockedPriceRepo.getPriceHistory).toHaveBeenCalledWith(
|
|
[1, 2, 3, 4, 5],
|
|
expect.anything(),
|
|
1000,
|
|
0,
|
|
);
|
|
});
|
|
});
|
|
|
|
// ==========================================================================
|
|
// BASE CONTROLLER INTEGRATION
|
|
// ==========================================================================
|
|
|
|
describe('BaseController integration', () => {
|
|
it('should use success helper for consistent response format', async () => {
|
|
// Arrange
|
|
const mockPriceHistory = [createMockPriceHistoryData()];
|
|
const request = createMockRequest();
|
|
|
|
mockedPriceRepo.getPriceHistory.mockResolvedValue(mockPriceHistory);
|
|
|
|
// Act
|
|
const result = await controller.getPriceHistory(request, {
|
|
masterItemIds: [1],
|
|
});
|
|
|
|
// Assert
|
|
expect(result).toHaveProperty('success', true);
|
|
expect(result).toHaveProperty('data');
|
|
});
|
|
|
|
it('should use error helper for validation errors', async () => {
|
|
// Arrange
|
|
const request = createMockRequest();
|
|
|
|
// Act
|
|
const result = await controller.getPriceHistory(request, {
|
|
masterItemIds: [],
|
|
});
|
|
|
|
// Assert
|
|
expect(result).toHaveProperty('success', false);
|
|
});
|
|
});
|
|
});
|