Some checks failed
Deploy to Test Environment / deploy-to-test (push) Failing after 1m17s
530 lines
16 KiB
TypeScript
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);
|
|
});
|
|
});
|
|
});
|