Files
flyer-crawler.projectium.com/src/controllers/price.controller.test.ts
Torben Sorensen 174b637a0a
All checks were successful
Deploy to Test Environment / deploy-to-test (push) Successful in 25m5s
even more typescript fixes
2026-02-17 17:20:54 -08:00

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