Files
flyer-crawler.projectium.com/src/routes/upc.routes.test.ts
Torben Sorensen 2a5cc5bb51
Some checks failed
Deploy to Test Environment / deploy-to-test (push) Failing after 1m17s
unit test repairs
2026-01-12 08:10:37 -08:00

530 lines
16 KiB
TypeScript

// src/routes/upc.routes.test.ts
import { describe, it, expect, vi, beforeEach } from 'vitest';
import supertest from 'supertest';
import type { Request, Response, NextFunction } from 'express';
import { createMockUserProfile } from '../tests/utils/mockFactories';
import { createTestApp } from '../tests/utils/createTestApp';
import { NotFoundError } from '../services/db/errors.db';
import type { UpcScanSource } from '../types/upc';
// Mock the upcService module
vi.mock('../services/upcService.server', () => ({
scanUpc: vi.fn(),
lookupUpc: vi.fn(),
getScanHistory: vi.fn(),
getScanById: vi.fn(),
getScanStats: vi.fn(),
linkUpcToProduct: vi.fn(),
}));
// Mock the logger to keep test output clean
vi.mock('../services/logger.server', async () => ({
logger: (await import('../tests/utils/mockLogger')).mockLogger,
}));
// Import the router and mocked service AFTER all mocks are defined.
import upcRouter from './upc.routes';
import * as upcService from '../services/upcService.server';
const mockUser = createMockUserProfile({
user: { user_id: 'user-123', email: 'test@test.com' },
});
const _mockAdminUser = createMockUserProfile({
user: { user_id: 'admin-123', email: 'admin@test.com' },
role: 'admin',
});
// Standardized mock for passport
// Note: createTestApp sets req.user before the router runs, so we preserve it here
vi.mock('../config/passport', () => ({
default: {
authenticate: vi.fn(() => (req: Request, res: Response, next: NextFunction) => {
// Preserve the user set by createTestApp if already present
if (!req.user) {
req.user = mockUser;
}
next();
}),
initialize: () => (req: Request, res: Response, next: NextFunction) => next(),
},
isAdmin: (req: Request, res: Response, next: NextFunction) => {
const user = req.user as typeof _mockAdminUser;
if (user?.role === 'admin') {
next();
} else {
res.status(403).json({ success: false, error: { message: 'Forbidden' } });
}
},
}));
// Define a reusable matcher for the logger object.
const expectLogger = expect.objectContaining({
info: expect.any(Function),
error: expect.any(Function),
});
describe('UPC Routes (/api/upc)', () => {
const mockUserProfile = createMockUserProfile({
user: { user_id: 'user-123', email: 'test@test.com' },
});
const mockAdminProfile = createMockUserProfile({
user: { user_id: 'admin-123', email: 'admin@test.com' },
role: 'admin',
});
beforeEach(() => {
vi.clearAllMocks();
// Provide default mock implementations
vi.mocked(upcService.getScanHistory).mockResolvedValue({ scans: [], total: 0 });
vi.mocked(upcService.getScanStats).mockResolvedValue({
total_scans: 0,
successful_lookups: 0,
unique_products: 0,
scans_today: 0,
scans_this_week: 0,
});
});
const app = createTestApp({
router: upcRouter,
basePath: '/api/upc',
authenticatedUser: mockUserProfile,
});
const adminApp = createTestApp({
router: upcRouter,
basePath: '/api/upc',
authenticatedUser: mockAdminProfile,
});
describe('POST /scan', () => {
it('should scan a manually entered UPC code successfully', async () => {
const mockScanResult = {
scan_id: 1,
upc_code: '012345678905',
product: {
product_id: 1,
name: 'Test Product',
brand: 'Test Brand',
category: 'Snacks',
description: null,
size: '500g',
upc_code: '012345678905',
image_url: null,
master_item_id: null,
},
external_lookup: null,
confidence: null,
lookup_successful: true,
is_new_product: false,
scanned_at: new Date().toISOString(),
};
vi.mocked(upcService.scanUpc).mockResolvedValue(mockScanResult);
const response = await supertest(app).post('/api/upc/scan').send({
upc_code: '012345678905',
scan_source: 'manual_entry',
});
expect(response.status).toBe(200);
expect(response.body.data.scan_id).toBe(1);
expect(response.body.data.upc_code).toBe('012345678905');
expect(response.body.data.lookup_successful).toBe(true);
expect(upcService.scanUpc).toHaveBeenCalledWith(
mockUserProfile.user.user_id,
{ upc_code: '012345678905', scan_source: 'manual_entry' },
expectLogger,
);
});
it('should scan from base64 image', async () => {
const mockScanResult = {
scan_id: 2,
upc_code: '987654321098',
product: null,
external_lookup: {
name: 'External Product',
brand: 'External Brand',
category: null,
description: null,
image_url: null,
source: 'openfoodfacts' as const,
},
confidence: 0.95,
lookup_successful: true,
is_new_product: true,
scanned_at: new Date().toISOString(),
};
vi.mocked(upcService.scanUpc).mockResolvedValue(mockScanResult);
const response = await supertest(app).post('/api/upc/scan').send({
image_base64: 'SGVsbG8gV29ybGQ=',
scan_source: 'image_upload',
});
expect(response.status).toBe(200);
expect(response.body.data.confidence).toBe(0.95);
expect(response.body.data.is_new_product).toBe(true);
});
it('should return 400 when neither upc_code nor image_base64 is provided', async () => {
const response = await supertest(app).post('/api/upc/scan').send({
scan_source: 'manual_entry',
});
expect(response.status).toBe(400);
expect(response.body.error.details).toBeDefined();
});
it('should return 400 for invalid scan_source', async () => {
const response = await supertest(app).post('/api/upc/scan').send({
upc_code: '012345678905',
scan_source: 'invalid_source',
});
expect(response.status).toBe(400);
});
it('should return 500 if the scan service fails', async () => {
vi.mocked(upcService.scanUpc).mockRejectedValue(new Error('Scan service error'));
const response = await supertest(app).post('/api/upc/scan').send({
upc_code: '012345678905',
scan_source: 'manual_entry',
});
expect(response.status).toBe(500);
expect(response.body.error.message).toBe('Scan service error');
});
});
describe('GET /lookup', () => {
it('should look up a UPC code successfully', async () => {
const mockLookupResult = {
upc_code: '012345678905',
product: {
product_id: 1,
name: 'Test Product',
brand: 'Test Brand',
category: 'Snacks',
description: null,
size: '500g',
upc_code: '012345678905',
image_url: null,
master_item_id: null,
},
external_lookup: null,
found: true,
from_cache: false,
};
vi.mocked(upcService.lookupUpc).mockResolvedValue(mockLookupResult);
const response = await supertest(app).get('/api/upc/lookup?upc_code=012345678905');
expect(response.status).toBe(200);
expect(response.body.data.upc_code).toBe('012345678905');
expect(response.body.data.found).toBe(true);
});
it('should support include_external and force_refresh parameters', async () => {
const mockLookupResult = {
upc_code: '012345678905',
product: null,
external_lookup: {
name: 'External Product',
brand: 'External Brand',
category: null,
description: null,
image_url: null,
source: 'openfoodfacts' as const,
},
found: true,
from_cache: false,
};
vi.mocked(upcService.lookupUpc).mockResolvedValue(mockLookupResult);
const response = await supertest(app).get(
'/api/upc/lookup?upc_code=012345678905&include_external=true&force_refresh=true',
);
expect(response.status).toBe(200);
expect(upcService.lookupUpc).toHaveBeenCalledWith(
expect.objectContaining({
upc_code: '012345678905',
force_refresh: true,
}),
expectLogger,
);
});
it('should return 400 for invalid UPC code format', async () => {
const response = await supertest(app).get('/api/upc/lookup?upc_code=123');
expect(response.status).toBe(400);
expect(response.body.error.details[0].message).toMatch(/8-14 digits/);
});
it('should return 400 when upc_code is missing', async () => {
const response = await supertest(app).get('/api/upc/lookup');
expect(response.status).toBe(400);
});
it('should return 500 if the lookup service fails', async () => {
vi.mocked(upcService.lookupUpc).mockRejectedValue(new Error('Lookup error'));
const response = await supertest(app).get('/api/upc/lookup?upc_code=012345678905');
expect(response.status).toBe(500);
});
});
describe('GET /history', () => {
it('should return scan history with pagination', async () => {
const mockHistory = {
scans: [
{
scan_id: 1,
user_id: 'user-123',
upc_code: '012345678905',
product_id: 1,
scan_source: 'manual_entry' as UpcScanSource,
scan_confidence: null,
raw_image_path: null,
lookup_successful: true,
created_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
},
],
total: 1,
};
vi.mocked(upcService.getScanHistory).mockResolvedValue(mockHistory);
const response = await supertest(app).get('/api/upc/history?limit=10&offset=0');
expect(response.status).toBe(200);
expect(response.body.data.scans).toHaveLength(1);
expect(response.body.data.total).toBe(1);
expect(upcService.getScanHistory).toHaveBeenCalledWith(
expect.objectContaining({
user_id: mockUserProfile.user.user_id,
limit: 10,
offset: 0,
}),
expectLogger,
);
});
it('should support filtering by lookup_successful', async () => {
vi.mocked(upcService.getScanHistory).mockResolvedValue({ scans: [], total: 0 });
const response = await supertest(app).get('/api/upc/history?lookup_successful=true');
expect(response.status).toBe(200);
expect(upcService.getScanHistory).toHaveBeenCalledWith(
expect.objectContaining({
lookup_successful: true,
}),
expectLogger,
);
});
it('should support filtering by scan_source', async () => {
vi.mocked(upcService.getScanHistory).mockResolvedValue({ scans: [], total: 0 });
const response = await supertest(app).get('/api/upc/history?scan_source=image_upload');
expect(response.status).toBe(200);
expect(upcService.getScanHistory).toHaveBeenCalledWith(
expect.objectContaining({
scan_source: 'image_upload',
}),
expectLogger,
);
});
it('should support filtering by date range', async () => {
vi.mocked(upcService.getScanHistory).mockResolvedValue({ scans: [], total: 0 });
const response = await supertest(app).get(
'/api/upc/history?from_date=2024-01-01&to_date=2024-01-31',
);
expect(response.status).toBe(200);
expect(upcService.getScanHistory).toHaveBeenCalledWith(
expect.objectContaining({
from_date: '2024-01-01',
to_date: '2024-01-31',
}),
expectLogger,
);
});
it('should return 400 for invalid date format', async () => {
const response = await supertest(app).get('/api/upc/history?from_date=01-01-2024');
expect(response.status).toBe(400);
});
it('should return 500 if the history service fails', async () => {
vi.mocked(upcService.getScanHistory).mockRejectedValue(new Error('History error'));
const response = await supertest(app).get('/api/upc/history');
expect(response.status).toBe(500);
});
});
describe('GET /history/:scanId', () => {
it('should return a specific scan by ID', async () => {
const mockScan = {
scan_id: 1,
user_id: 'user-123',
upc_code: '012345678905',
product_id: 1,
scan_source: 'manual_entry' as UpcScanSource,
scan_confidence: null,
raw_image_path: null,
lookup_successful: true,
created_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
};
vi.mocked(upcService.getScanById).mockResolvedValue(mockScan);
const response = await supertest(app).get('/api/upc/history/1');
expect(response.status).toBe(200);
expect(response.body.data.scan_id).toBe(1);
expect(upcService.getScanById).toHaveBeenCalledWith(
1,
mockUserProfile.user.user_id,
expectLogger,
);
});
it('should return 404 when scan not found', async () => {
vi.mocked(upcService.getScanById).mockRejectedValue(new NotFoundError('Scan not found'));
const response = await supertest(app).get('/api/upc/history/999');
expect(response.status).toBe(404);
expect(response.body.error.message).toBe('Scan not found');
});
it('should return 400 for invalid scan ID', async () => {
const response = await supertest(app).get('/api/upc/history/abc');
expect(response.status).toBe(400);
expect(response.body.error.details[0].message).toMatch(/Invalid ID|number/i);
});
});
describe('GET /stats', () => {
it('should return scan statistics', async () => {
const mockStats = {
total_scans: 100,
successful_lookups: 80,
unique_products: 50,
scans_today: 5,
scans_this_week: 25,
};
vi.mocked(upcService.getScanStats).mockResolvedValue(mockStats);
const response = await supertest(app).get('/api/upc/stats');
expect(response.status).toBe(200);
expect(response.body.data.total_scans).toBe(100);
expect(response.body.data.successful_lookups).toBe(80);
expect(upcService.getScanStats).toHaveBeenCalledWith(
mockUserProfile.user.user_id,
expectLogger,
);
});
it('should return 500 if the stats service fails', async () => {
vi.mocked(upcService.getScanStats).mockRejectedValue(new Error('Stats error'));
const response = await supertest(app).get('/api/upc/stats');
expect(response.status).toBe(500);
});
});
describe('POST /link', () => {
it('should link UPC to product (admin only)', async () => {
vi.mocked(upcService.linkUpcToProduct).mockResolvedValue(undefined);
const response = await supertest(adminApp).post('/api/upc/link').send({
upc_code: '012345678905',
product_id: 1,
});
expect(response.status).toBe(204);
expect(upcService.linkUpcToProduct).toHaveBeenCalledWith(1, '012345678905', expectLogger);
});
it('should return 403 for non-admin users', async () => {
const response = await supertest(app).post('/api/upc/link').send({
upc_code: '012345678905',
product_id: 1,
});
expect(response.status).toBe(403);
expect(upcService.linkUpcToProduct).not.toHaveBeenCalled();
});
it('should return 400 for invalid UPC code format', async () => {
const response = await supertest(adminApp).post('/api/upc/link').send({
upc_code: '123',
product_id: 1,
});
expect(response.status).toBe(400);
expect(response.body.error.details[0].message).toMatch(/8-14 digits/);
});
it('should return 400 for invalid product_id', async () => {
const response = await supertest(adminApp).post('/api/upc/link').send({
upc_code: '012345678905',
product_id: -1,
});
expect(response.status).toBe(400);
});
it('should return 404 when product not found', async () => {
vi.mocked(upcService.linkUpcToProduct).mockRejectedValue(
new NotFoundError('Product not found'),
);
const response = await supertest(adminApp).post('/api/upc/link').send({
upc_code: '012345678905',
product_id: 999,
});
expect(response.status).toBe(404);
expect(response.body.error.message).toBe('Product not found');
});
it('should return 500 if the link service fails', async () => {
vi.mocked(upcService.linkUpcToProduct).mockRejectedValue(new Error('Link error'));
const response = await supertest(adminApp).post('/api/upc/link').send({
upc_code: '012345678905',
product_id: 1,
});
expect(response.status).toBe(500);
});
});
});