661 lines
21 KiB
JavaScript
661 lines
21 KiB
JavaScript
const request = require('supertest');
|
|
const app = require('../server');
|
|
const CodeGenerationService = require('../services/CodeGenerationService');
|
|
const PrintableLayoutService = require('../services/PrintableLayoutService');
|
|
const Product = require('../models/Product');
|
|
const Inventory = require('../models/Inventory');
|
|
|
|
// Mock the services and models
|
|
jest.mock('../services/CodeGenerationService');
|
|
jest.mock('../services/PrintableLayoutService');
|
|
jest.mock('../models/Product');
|
|
jest.mock('../models/Inventory');
|
|
|
|
describe('Codes API Endpoints', () => {
|
|
beforeEach(() => {
|
|
// Reset all mocks
|
|
jest.clearAllMocks();
|
|
});
|
|
|
|
describe('GET /api/codes/formats', () => {
|
|
it('should return supported barcode formats', async () => {
|
|
const mockFormats = ['CODE128', 'CODE39', 'EAN13', 'EAN8', 'UPC'];
|
|
CodeGenerationService.prototype.getSupportedFormats = jest.fn().mockReturnValue(mockFormats);
|
|
|
|
const response = await request(app)
|
|
.get('/api/codes/formats')
|
|
.expect(200);
|
|
|
|
expect(response.body.success).toBe(true);
|
|
expect(response.body.data.barcodeFormats).toEqual(mockFormats);
|
|
expect(response.body.data.qrCodeSupported).toBe(true);
|
|
});
|
|
|
|
it('should handle service errors gracefully', async () => {
|
|
CodeGenerationService.prototype.getSupportedFormats = jest.fn().mockImplementation(() => {
|
|
throw new Error('Service error');
|
|
});
|
|
|
|
const response = await request(app)
|
|
.get('/api/codes/formats')
|
|
.expect(500);
|
|
|
|
expect(response.body.success).toBe(false);
|
|
expect(response.body.error).toBe('Failed to retrieve supported formats');
|
|
});
|
|
});
|
|
|
|
describe('POST /api/codes/barcode', () => {
|
|
it('should generate barcode successfully', async () => {
|
|
const mockResult = {
|
|
success: true,
|
|
data: 'data:image/png;base64,mockbarcodedata',
|
|
format: 'CODE128',
|
|
productCode: 'TEST123'
|
|
};
|
|
|
|
CodeGenerationService.prototype.generateBarcode = jest.fn().mockResolvedValue(mockResult);
|
|
|
|
const response = await request(app)
|
|
.post('/api/codes/barcode')
|
|
.send({
|
|
productCode: 'TEST123',
|
|
format: 'CODE128',
|
|
options: { width: 2 }
|
|
})
|
|
.expect(200);
|
|
|
|
expect(response.body.success).toBe(true);
|
|
expect(response.body.data).toEqual(mockResult);
|
|
expect(response.body.message).toBe('Barcode generated successfully');
|
|
});
|
|
|
|
it('should return 400 for missing product code', async () => {
|
|
const response = await request(app)
|
|
.post('/api/codes/barcode')
|
|
.send({ format: 'CODE128' })
|
|
.expect(400);
|
|
|
|
expect(response.body.success).toBe(false);
|
|
expect(response.body.error).toBe('Missing product code');
|
|
});
|
|
|
|
it('should return 400 for barcode generation failure', async () => {
|
|
const mockResult = {
|
|
success: false,
|
|
error: 'Invalid product code format'
|
|
};
|
|
|
|
CodeGenerationService.prototype.generateBarcode = jest.fn().mockResolvedValue(mockResult);
|
|
|
|
const response = await request(app)
|
|
.post('/api/codes/barcode')
|
|
.send({ productCode: 'INVALID' })
|
|
.expect(400);
|
|
|
|
expect(response.body.success).toBe(false);
|
|
expect(response.body.error).toBe('Barcode generation failed');
|
|
});
|
|
});
|
|
|
|
describe('POST /api/codes/qrcode', () => {
|
|
it('should generate QR code successfully', async () => {
|
|
const mockResult = {
|
|
success: true,
|
|
data: 'data:image/png;base64,mockqrcodedata',
|
|
productCode: 'TEST123',
|
|
embeddedData: { code: 'TEST123', desc: 'Test Product' }
|
|
};
|
|
|
|
CodeGenerationService.prototype.generateQRCode = jest.fn().mockResolvedValue(mockResult);
|
|
|
|
const response = await request(app)
|
|
.post('/api/codes/qrcode')
|
|
.send({
|
|
productData: {
|
|
product_code: 'TEST123',
|
|
description: 'Test Product'
|
|
}
|
|
})
|
|
.expect(200);
|
|
|
|
expect(response.body.success).toBe(true);
|
|
expect(response.body.data).toEqual(mockResult);
|
|
expect(response.body.message).toBe('QR code generated successfully');
|
|
});
|
|
|
|
it('should return 400 for invalid product data', async () => {
|
|
const response = await request(app)
|
|
.post('/api/codes/qrcode')
|
|
.send({ productData: {} })
|
|
.expect(400);
|
|
|
|
expect(response.body.success).toBe(false);
|
|
expect(response.body.error).toBe('Invalid product data');
|
|
});
|
|
|
|
it('should return 400 for QR code generation failure', async () => {
|
|
const mockResult = {
|
|
success: false,
|
|
error: 'Invalid product data format'
|
|
};
|
|
|
|
CodeGenerationService.prototype.generateQRCode = jest.fn().mockResolvedValue(mockResult);
|
|
|
|
const response = await request(app)
|
|
.post('/api/codes/qrcode')
|
|
.send({
|
|
productData: { product_code: 'TEST123' }
|
|
})
|
|
.expect(400);
|
|
|
|
expect(response.body.success).toBe(false);
|
|
expect(response.body.error).toBe('QR code generation failed');
|
|
});
|
|
});
|
|
|
|
describe('POST /api/codes/both', () => {
|
|
it('should generate both barcode and QR code successfully', async () => {
|
|
const mockResult = {
|
|
productCode: 'TEST123',
|
|
barcode: { success: true, data: 'barcode-data' },
|
|
qrCode: { success: true, data: 'qrcode-data' },
|
|
timestamp: '2023-01-01T00:00:00.000Z'
|
|
};
|
|
|
|
CodeGenerationService.prototype.generateBothCodes = jest.fn().mockResolvedValue(mockResult);
|
|
|
|
const response = await request(app)
|
|
.post('/api/codes/both')
|
|
.send({
|
|
productData: {
|
|
product_code: 'TEST123',
|
|
description: 'Test Product'
|
|
}
|
|
})
|
|
.expect(200);
|
|
|
|
expect(response.body.success).toBe(true);
|
|
expect(response.body.data).toEqual(mockResult);
|
|
expect(response.body.message).toBe('Codes generated successfully');
|
|
});
|
|
|
|
it('should return 400 for invalid product data', async () => {
|
|
const response = await request(app)
|
|
.post('/api/codes/both')
|
|
.send({ productData: {} })
|
|
.expect(400);
|
|
|
|
expect(response.body.success).toBe(false);
|
|
expect(response.body.error).toBe('Invalid product data');
|
|
});
|
|
});
|
|
|
|
describe('POST /api/codes/product/:productId', () => {
|
|
it('should generate codes for specific product successfully', async () => {
|
|
const mockProduct = {
|
|
id: 1,
|
|
name: 'TEST123',
|
|
description: 'Test Product',
|
|
category: 'Electronics',
|
|
unit: 'pieces',
|
|
toJSON: jest.fn().mockReturnValue({
|
|
id: 1,
|
|
name: 'TEST123',
|
|
description: 'Test Product',
|
|
category: 'Electronics',
|
|
unit: 'pieces'
|
|
})
|
|
};
|
|
|
|
const mockResult = {
|
|
productCode: 'TEST123',
|
|
barcode: { success: true, data: 'barcode-data' },
|
|
qrCode: { success: true, data: 'qrcode-data' }
|
|
};
|
|
|
|
Product.findById.mockResolvedValue(mockProduct);
|
|
CodeGenerationService.prototype.generateBothCodes = jest.fn().mockResolvedValue(mockResult);
|
|
|
|
const response = await request(app)
|
|
.post('/api/codes/product/1')
|
|
.send({ codeType: 'both' })
|
|
.expect(200);
|
|
|
|
expect(response.body.success).toBe(true);
|
|
expect(response.body.data.product.id).toBe(1);
|
|
expect(response.body.data.codes).toEqual(mockResult);
|
|
});
|
|
|
|
it('should return 404 for non-existent product', async () => {
|
|
Product.findById.mockResolvedValue(null);
|
|
|
|
const response = await request(app)
|
|
.post('/api/codes/product/999')
|
|
.send({ codeType: 'barcode' })
|
|
.expect(404);
|
|
|
|
expect(response.body.success).toBe(false);
|
|
expect(response.body.error).toBe('Product not found');
|
|
});
|
|
|
|
it('should return 400 for invalid product ID', async () => {
|
|
const response = await request(app)
|
|
.post('/api/codes/product/invalid')
|
|
.send({ codeType: 'barcode' })
|
|
.expect(400);
|
|
|
|
expect(response.body.success).toBe(false);
|
|
expect(response.body.error).toBe('Invalid product ID');
|
|
});
|
|
|
|
it('should generate only barcode when requested', async () => {
|
|
const mockProduct = {
|
|
id: 1,
|
|
name: 'TEST123',
|
|
toJSON: jest.fn().mockReturnValue({ id: 1, name: 'TEST123' })
|
|
};
|
|
|
|
const mockResult = {
|
|
success: true,
|
|
data: 'barcode-data'
|
|
};
|
|
|
|
Product.findById.mockResolvedValue(mockProduct);
|
|
CodeGenerationService.prototype.generateBarcode = jest.fn().mockResolvedValue(mockResult);
|
|
|
|
const response = await request(app)
|
|
.post('/api/codes/product/1')
|
|
.send({ codeType: 'barcode' })
|
|
.expect(200);
|
|
|
|
expect(response.body.success).toBe(true);
|
|
expect(CodeGenerationService.prototype.generateBarcode).toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe('GET /api/codes/layouts/sizes', () => {
|
|
it('should return available label sizes', async () => {
|
|
const mockLabelSizes = {
|
|
'avery-5160': { width: 66.7, height: 25.4, columns: 3, rows: 10 },
|
|
'avery-5161': { width: 101.6, height: 25.4, columns: 2, rows: 10 }
|
|
};
|
|
|
|
PrintableLayoutService.prototype.getAvailableLabelSizes = jest.fn().mockReturnValue(mockLabelSizes);
|
|
|
|
const response = await request(app)
|
|
.get('/api/codes/layouts/sizes')
|
|
.expect(200);
|
|
|
|
expect(response.body.success).toBe(true);
|
|
expect(response.body.data).toEqual(mockLabelSizes);
|
|
});
|
|
});
|
|
|
|
describe('POST /api/codes/layouts/preview', () => {
|
|
it('should generate layout preview successfully', async () => {
|
|
const mockProducts = [
|
|
{ id: 1, name: 'P001', description: 'Product 1' },
|
|
{ id: 2, name: 'P002', description: 'Product 2' }
|
|
];
|
|
|
|
const mockPreview = {
|
|
success: true,
|
|
preview: {
|
|
labelSize: 'avery-5160',
|
|
labelsPerPage: 30,
|
|
totalPages: 1,
|
|
includeBarcode: true,
|
|
includeQRCode: false
|
|
}
|
|
};
|
|
|
|
Product.findById
|
|
.mockResolvedValueOnce(mockProducts[0])
|
|
.mockResolvedValueOnce(mockProducts[1]);
|
|
|
|
PrintableLayoutService.prototype.generateLayoutPreview = jest.fn().mockResolvedValue(mockPreview);
|
|
|
|
const response = await request(app)
|
|
.post('/api/codes/layouts/preview')
|
|
.send({
|
|
productIds: [1, 2],
|
|
options: { labelSize: 'avery-5160' }
|
|
})
|
|
.expect(200);
|
|
|
|
expect(response.body.success).toBe(true);
|
|
expect(response.body.data.labelSize).toBe('avery-5160');
|
|
expect(response.body.data.totalRequestedProducts).toBe(2);
|
|
});
|
|
|
|
it('should return 400 for empty product IDs', async () => {
|
|
const response = await request(app)
|
|
.post('/api/codes/layouts/preview')
|
|
.send({ productIds: [] })
|
|
.expect(400);
|
|
|
|
expect(response.body.success).toBe(false);
|
|
expect(response.body.error).toBe('Invalid product IDs');
|
|
});
|
|
|
|
it('should return 404 when no valid products found', async () => {
|
|
Product.findById.mockResolvedValue(null);
|
|
|
|
const response = await request(app)
|
|
.post('/api/codes/layouts/preview')
|
|
.send({ productIds: [999] })
|
|
.expect(404);
|
|
|
|
expect(response.body.success).toBe(false);
|
|
expect(response.body.error).toBe('No valid products found');
|
|
});
|
|
});
|
|
|
|
describe('POST /api/codes/layouts/generate', () => {
|
|
it('should generate printable PDF layout successfully', async () => {
|
|
const mockProduct = {
|
|
id: 1,
|
|
name: 'P001',
|
|
description: 'Product 1',
|
|
category: 'Electronics',
|
|
unit: 'pieces'
|
|
};
|
|
|
|
const mockPdfBuffer = Buffer.from('mock-pdf-data');
|
|
const mockResult = {
|
|
success: true,
|
|
data: mockPdfBuffer,
|
|
metadata: {
|
|
totalProducts: 1,
|
|
totalPages: 1,
|
|
labelSize: 'avery-5160'
|
|
}
|
|
};
|
|
|
|
Product.findById.mockResolvedValue(mockProduct);
|
|
PrintableLayoutService.prototype.generatePrintableLayout = jest.fn().mockResolvedValue(mockResult);
|
|
|
|
const response = await request(app)
|
|
.post('/api/codes/layouts/generate')
|
|
.send({
|
|
productIds: [1],
|
|
options: { labelSize: 'avery-5160' }
|
|
})
|
|
.expect(200);
|
|
|
|
expect(response.headers['content-type']).toBe('application/pdf');
|
|
expect(response.headers['content-disposition']).toContain('attachment');
|
|
expect(Buffer.isBuffer(response.body)).toBe(true);
|
|
});
|
|
|
|
it('should return 400 for too many products', async () => {
|
|
const productIds = Array.from({ length: 1001 }, (_, i) => i + 1);
|
|
|
|
const response = await request(app)
|
|
.post('/api/codes/layouts/generate')
|
|
.send({ productIds })
|
|
.expect(400);
|
|
|
|
expect(response.body.success).toBe(false);
|
|
expect(response.body.error).toBe('Too many products');
|
|
});
|
|
|
|
it('should return 400 for layout generation failure', async () => {
|
|
const mockProduct = { id: 1, name: 'P001' };
|
|
const mockResult = {
|
|
success: false,
|
|
error: 'Layout generation failed'
|
|
};
|
|
|
|
Product.findById.mockResolvedValue(mockProduct);
|
|
PrintableLayoutService.prototype.generatePrintableLayout = jest.fn().mockResolvedValue(mockResult);
|
|
|
|
const response = await request(app)
|
|
.post('/api/codes/layouts/generate')
|
|
.send({ productIds: [1] })
|
|
.expect(400);
|
|
|
|
expect(response.body.success).toBe(false);
|
|
expect(response.body.error).toBe('Layout generation failed');
|
|
});
|
|
});
|
|
|
|
describe('GET /api/codes/export/excel', () => {
|
|
it('should export inventory data to Excel successfully', async () => {
|
|
const mockInventoryData = [
|
|
{
|
|
product_code: 'P001',
|
|
description: 'Product 1',
|
|
category: 'Electronics',
|
|
current_level: 10,
|
|
minimum_level: 5,
|
|
stock_status: 'normal',
|
|
last_updated: '2023-01-01T00:00:00Z',
|
|
updated_by: 'user1'
|
|
}
|
|
];
|
|
|
|
Inventory.getInventorySummary.mockResolvedValue(mockInventoryData);
|
|
|
|
const response = await request(app)
|
|
.get('/api/codes/export/excel')
|
|
.expect(200);
|
|
|
|
expect(response.headers['content-type']).toContain('spreadsheetml.sheet');
|
|
expect(response.headers['content-disposition']).toContain('attachment');
|
|
expect(response.headers['content-disposition']).toContain('inventory-export-');
|
|
});
|
|
|
|
it('should filter inventory data by category', async () => {
|
|
const mockInventoryData = [
|
|
{
|
|
product_code: 'P001',
|
|
description: 'Product 1',
|
|
category: 'Electronics',
|
|
current_level: 10
|
|
}
|
|
];
|
|
|
|
Inventory.getInventorySummary.mockResolvedValue(mockInventoryData);
|
|
|
|
const response = await request(app)
|
|
.get('/api/codes/export/excel?category=Electronics')
|
|
.expect(200);
|
|
|
|
expect(Inventory.getInventorySummary).toHaveBeenCalledWith({ category: 'Electronics' });
|
|
});
|
|
|
|
it('should return 404 when no data to export', async () => {
|
|
Inventory.getInventorySummary.mockResolvedValue([]);
|
|
|
|
const response = await request(app)
|
|
.get('/api/codes/export/excel')
|
|
.expect(404);
|
|
|
|
expect(response.body.success).toBe(false);
|
|
expect(response.body.error).toBe('No data to export');
|
|
});
|
|
});
|
|
|
|
describe('POST /api/codes/export/excel/custom', () => {
|
|
it('should export custom inventory data successfully', async () => {
|
|
const mockProduct = {
|
|
id: 1,
|
|
name: 'P001',
|
|
description: 'Product 1',
|
|
category: 'Electronics'
|
|
};
|
|
|
|
const mockInventory = {
|
|
current_level: 10,
|
|
minimum_level: 5,
|
|
last_updated: '2023-01-01T00:00:00Z',
|
|
updated_by: 'user1'
|
|
};
|
|
|
|
Product.findById.mockResolvedValue(mockProduct);
|
|
Inventory.getByProductId.mockResolvedValue(mockInventory);
|
|
|
|
const response = await request(app)
|
|
.post('/api/codes/export/excel/custom')
|
|
.send({
|
|
productIds: [1],
|
|
columns: ['product_code', 'description', 'current_level'],
|
|
filename: 'custom-export.xlsx'
|
|
})
|
|
.expect(200);
|
|
|
|
expect(response.headers['content-type']).toContain('spreadsheetml.sheet');
|
|
expect(response.headers['content-disposition']).toContain('custom-export.xlsx');
|
|
});
|
|
|
|
it('should include history when requested', async () => {
|
|
const mockProduct = { id: 1, name: 'P001' };
|
|
const mockInventory = { current_level: 10 };
|
|
const mockHistory = [
|
|
{
|
|
product_code: 'P001',
|
|
old_level: 5,
|
|
new_level: 10,
|
|
change_reason: 'Restock',
|
|
updated_by: 'user1',
|
|
updated_at: '2023-01-01T00:00:00Z'
|
|
}
|
|
];
|
|
|
|
Product.findById.mockResolvedValue(mockProduct);
|
|
Inventory.getByProductId.mockResolvedValue(mockInventory);
|
|
Inventory.getInventoryHistory.mockResolvedValue(mockHistory);
|
|
|
|
const response = await request(app)
|
|
.post('/api/codes/export/excel/custom')
|
|
.send({
|
|
productIds: [1],
|
|
columns: ['product_code'],
|
|
includeHistory: true
|
|
})
|
|
.expect(200);
|
|
|
|
expect(Inventory.getInventoryHistory).toHaveBeenCalled();
|
|
});
|
|
|
|
it('should return 400 for empty product IDs', async () => {
|
|
const response = await request(app)
|
|
.post('/api/codes/export/excel/custom')
|
|
.send({ productIds: [] })
|
|
.expect(400);
|
|
|
|
expect(response.body.success).toBe(false);
|
|
expect(response.body.error).toBe('Invalid product IDs');
|
|
});
|
|
|
|
it('should return 404 when no valid products found', async () => {
|
|
Product.findById.mockResolvedValue(null);
|
|
|
|
const response = await request(app)
|
|
.post('/api/codes/export/excel/custom')
|
|
.send({ productIds: [999] })
|
|
.expect(404);
|
|
|
|
expect(response.body.success).toBe(false);
|
|
expect(response.body.error).toBe('No data to export');
|
|
});
|
|
});
|
|
|
|
describe('POST /api/codes/qr/parse', () => {
|
|
it('should parse QR code data successfully', async () => {
|
|
const mockResult = {
|
|
success: true,
|
|
productCode: 'TEST123',
|
|
description: 'Test Product',
|
|
category: 'Electronics',
|
|
timestamp: '2023-01-01T00:00:00Z'
|
|
};
|
|
|
|
CodeGenerationService.prototype.parseQRCodeData = jest.fn().mockReturnValue(mockResult);
|
|
|
|
const response = await request(app)
|
|
.post('/api/codes/qr/parse')
|
|
.send({
|
|
qrData: '{"code":"TEST123","desc":"Test Product","cat":"Electronics","ts":"2023-01-01T00:00:00Z"}'
|
|
})
|
|
.expect(200);
|
|
|
|
expect(response.body.success).toBe(true);
|
|
expect(response.body.data).toEqual(mockResult);
|
|
expect(response.body.message).toBe('QR code parsed successfully');
|
|
});
|
|
|
|
it('should return 400 for missing QR data', async () => {
|
|
const response = await request(app)
|
|
.post('/api/codes/qr/parse')
|
|
.send({})
|
|
.expect(400);
|
|
|
|
expect(response.body.success).toBe(false);
|
|
expect(response.body.error).toBe('Missing QR data');
|
|
});
|
|
|
|
it('should return 400 for QR parsing failure', async () => {
|
|
const mockResult = {
|
|
success: false,
|
|
error: 'Invalid QR code data format'
|
|
};
|
|
|
|
CodeGenerationService.prototype.parseQRCodeData = jest.fn().mockReturnValue(mockResult);
|
|
|
|
const response = await request(app)
|
|
.post('/api/codes/qr/parse')
|
|
.send({ qrData: 'invalid-data' })
|
|
.expect(400);
|
|
|
|
expect(response.body.success).toBe(false);
|
|
expect(response.body.error).toBe('QR code parsing failed');
|
|
});
|
|
});
|
|
|
|
describe('Error Handling', () => {
|
|
it('should handle service initialization errors', async () => {
|
|
// Mock constructor to throw error
|
|
const originalCodeGenService = CodeGenerationService;
|
|
CodeGenerationService.mockImplementation(() => {
|
|
throw new Error('Service initialization failed');
|
|
});
|
|
|
|
const response = await request(app)
|
|
.post('/api/codes/barcode')
|
|
.send({ productCode: 'TEST123' })
|
|
.expect(500);
|
|
|
|
expect(response.body.success).toBe(false);
|
|
expect(response.body.error).toBe('Failed to generate barcode');
|
|
|
|
// Restore original
|
|
CodeGenerationService.mockImplementation(originalCodeGenService);
|
|
});
|
|
|
|
it('should handle database connection errors', async () => {
|
|
Product.findById.mockRejectedValue(new Error('Database connection failed'));
|
|
|
|
const response = await request(app)
|
|
.post('/api/codes/product/1')
|
|
.send({ codeType: 'barcode' })
|
|
.expect(500);
|
|
|
|
expect(response.body.success).toBe(false);
|
|
expect(response.body.error).toBe('Failed to generate codes for product');
|
|
});
|
|
|
|
it('should handle Excel export errors', async () => {
|
|
Inventory.getInventorySummary.mockRejectedValue(new Error('Database error'));
|
|
|
|
const response = await request(app)
|
|
.get('/api/codes/export/excel')
|
|
.expect(500);
|
|
|
|
expect(response.body.success).toBe(false);
|
|
expect(response.body.error).toBe('Failed to export Excel file');
|
|
});
|
|
});
|
|
}); |