Some checks failed
Deploy to Test Environment / deploy-to-test (push) Failing after 3m58s
516 lines
14 KiB
TypeScript
516 lines
14 KiB
TypeScript
// 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<typeof upcService>;
|
|
|
|
// ============================================================================
|
|
// HELPER FUNCTIONS
|
|
// ============================================================================
|
|
|
|
/**
|
|
* Creates a mock Express request object with authenticated user.
|
|
*/
|
|
function createMockRequest(overrides: Partial<ExpressRequest> = {}): 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();
|
|
});
|
|
});
|
|
});
|