Files
inventory-barcode-system/__tests__/inventory.export.routes.test.js

561 lines
18 KiB
JavaScript

const request = require('supertest');
const express = require('express');
const inventoryRoutes = require('../routes/inventory');
const ExcelExportService = require('../services/ExcelExportService');
const path = require('path');
const fs = require('fs');
// Mock dependencies
jest.mock('../services/ExcelExportService');
jest.mock('../models/Inventory');
jest.mock('../models/Product');
describe('Inventory Export Routes', () => {
let app;
let mockExportService;
beforeEach(() => {
app = express();
app.use(express.json());
app.use('/api/inventory', inventoryRoutes);
// Mock ExcelExportService
mockExportService = {
exportInventoryToExcel: jest.fn(),
getExportHistory: jest.fn(),
cleanupOldExports: jest.fn()
};
ExcelExportService.mockImplementation(() => mockExportService);
// Clear all mocks
jest.clearAllMocks();
});
describe('GET /api/inventory/export', () => {
it('should export inventory data successfully', async () => {
const mockExportResult = {
success: true,
filePath: '/test/path/export.xlsx',
filename: 'inventory_export_2024-01-15.xlsx',
sessionId: 123,
recordCount: 100,
exportDate: '2024-01-15T10:30:00Z'
};
mockExportService.exportInventoryToExcel.mockResolvedValue(mockExportResult);
// Mock res.sendFile
const response = await request(app)
.get('/api/inventory/export')
.query({
format: 'xlsx',
includeHistory: 'true',
category: 'Electronics'
});
expect(mockExportService.exportInventoryToExcel).toHaveBeenCalledWith({
format: 'xlsx',
includeHistory: true,
includeAuditInfo: true,
filename: undefined,
filters: {
category: 'Electronics'
}
});
// Note: Since we can't easily mock res.sendFile in this test environment,
// we'll check that the service was called correctly
expect(response.status).toBe(200);
});
it('should apply multiple filters correctly', async () => {
const mockExportResult = {
success: true,
filePath: '/test/path/export.xlsx',
filename: 'inventory_export_2024-01-15.xlsx',
sessionId: 123,
recordCount: 50
};
mockExportService.exportInventoryToExcel.mockResolvedValue(mockExportResult);
await request(app)
.get('/api/inventory/export')
.query({
category: 'Electronics',
stockStatus: 'low',
updatedSince: '2024-01-01T00:00:00Z',
productCodes: 'TEST001,TEST002,TEST003'
});
expect(mockExportService.exportInventoryToExcel).toHaveBeenCalledWith({
format: 'xlsx',
includeHistory: false,
includeAuditInfo: true,
filename: undefined,
filters: {
category: 'Electronics',
stockStatus: 'low',
updatedSince: '2024-01-01T00:00:00Z',
productCodes: ['TEST001', 'TEST002', 'TEST003']
}
});
});
it('should validate format parameter', async () => {
const response = await request(app)
.get('/api/inventory/export')
.query({ format: 'invalid' });
expect(response.status).toBe(400);
expect(response.body.success).toBe(false);
expect(response.body.error).toBe('Invalid format');
expect(response.body.message).toContain('Format must be one of');
});
it('should handle export service errors', async () => {
mockExportService.exportInventoryToExcel.mockResolvedValue({
success: false,
error: 'Database connection failed'
});
const response = await request(app)
.get('/api/inventory/export');
expect(response.status).toBe(500);
expect(response.body.success).toBe(false);
expect(response.body.error).toBe('Export failed');
expect(response.body.message).toBe('Database connection failed');
});
it('should handle service exceptions', async () => {
mockExportService.exportInventoryToExcel.mockRejectedValue(
new Error('Unexpected error')
);
const response = await request(app)
.get('/api/inventory/export');
expect(response.status).toBe(500);
expect(response.body.success).toBe(false);
expect(response.body.error).toBe('Export failed');
expect(response.body.message).toBe('Unexpected error');
});
});
describe('POST /api/inventory/export/with-original', () => {
it('should export with original file structure', async () => {
const mockExportResult = {
success: true,
filePath: '/test/path/export.xlsx',
filename: 'updated_inventory.xlsx',
sessionId: 124,
recordCount: 75,
metadata: {
preservedFormatting: true,
updatedRows: 50,
addedRows: 25
}
};
mockExportService.exportInventoryToExcel.mockResolvedValue(mockExportResult);
// Create a mock Excel file buffer
const mockFileBuffer = Buffer.from('mock excel data');
const response = await request(app)
.post('/api/inventory/export/with-original')
.attach('originalFile', mockFileBuffer, 'original.xlsx')
.field('format', 'xlsx')
.field('includeTimestamp', 'true')
.field('includeNewProducts', 'true')
.field('category', 'Tools');
expect(mockExportService.exportInventoryToExcel).toHaveBeenCalledWith({
format: 'xlsx',
includeHistory: false,
includeTimestamp: true,
includeNewProducts: true,
preserveFormatting: true,
filename: undefined,
originalFileBuffer: expect.any(Buffer),
sheetName: undefined,
filters: {
category: 'Tools'
}
});
expect(response.status).toBe(200);
});
it('should require original file', async () => {
const response = await request(app)
.post('/api/inventory/export/with-original')
.field('format', 'xlsx');
expect(response.status).toBe(400);
expect(response.body.success).toBe(false);
expect(response.body.error).toBe('Missing original file');
});
it('should handle product codes as array', async () => {
const mockExportResult = {
success: true,
filePath: '/test/path/export.xlsx',
filename: 'updated_inventory.xlsx',
sessionId: 125,
recordCount: 10
};
mockExportService.exportInventoryToExcel.mockResolvedValue(mockExportResult);
const mockFileBuffer = Buffer.from('mock excel data');
await request(app)
.post('/api/inventory/export/with-original')
.attach('originalFile', mockFileBuffer, 'original.xlsx')
.field('productCodes', ['TEST001', 'TEST002']);
expect(mockExportService.exportInventoryToExcel).toHaveBeenCalledWith(
expect.objectContaining({
filters: {
productCodes: ['TEST001', 'TEST002']
}
})
);
});
it('should handle export service errors', async () => {
mockExportService.exportInventoryToExcel.mockResolvedValue({
success: false,
error: 'Failed to parse original file'
});
const mockFileBuffer = Buffer.from('mock excel data');
const response = await request(app)
.post('/api/inventory/export/with-original')
.attach('originalFile', mockFileBuffer, 'original.xlsx');
expect(response.status).toBe(500);
expect(response.body.success).toBe(false);
expect(response.body.error).toBe('Export failed');
expect(response.body.message).toBe('Failed to parse original file');
});
});
describe('GET /api/inventory/export/history', () => {
it('should retrieve export history successfully', async () => {
const mockHistory = [
{
id: 1,
filename: 'export1.xlsx',
total_records: 100,
export_date: '2024-01-15T10:30:00Z',
filters: '{"category":"Electronics"}',
include_history: 0
},
{
id: 2,
filename: 'export2.xlsx',
total_records: 75,
export_date: '2024-01-14T15:45:00Z',
filters: '{}',
include_history: 1
}
];
mockExportService.getExportHistory.mockResolvedValue(mockHistory);
const response = await request(app)
.get('/api/inventory/export/history')
.query({ limit: 10, offset: 0 });
expect(response.status).toBe(200);
expect(response.body.success).toBe(true);
expect(response.body.data).toEqual(mockHistory);
expect(response.body.count).toBe(2);
expect(response.body.pagination).toEqual({
limit: 10,
offset: 0,
hasMore: false
});
expect(mockExportService.getExportHistory).toHaveBeenCalledWith({
limit: 10,
offset: 0
});
});
it('should use default pagination parameters', async () => {
mockExportService.getExportHistory.mockResolvedValue([]);
const response = await request(app)
.get('/api/inventory/export/history');
expect(response.status).toBe(200);
expect(mockExportService.getExportHistory).toHaveBeenCalledWith({
limit: 50,
offset: 0
});
});
it('should validate limit parameter', async () => {
const response = await request(app)
.get('/api/inventory/export/history')
.query({ limit: 2000 });
expect(response.status).toBe(400);
expect(response.body.success).toBe(false);
expect(response.body.error).toBe('Invalid limit');
expect(response.body.message).toBe('Limit must be between 1 and 1000');
});
it('should validate offset parameter', async () => {
const response = await request(app)
.get('/api/inventory/export/history')
.query({ offset: -1 });
expect(response.status).toBe(400);
expect(response.body.success).toBe(false);
expect(response.body.error).toBe('Invalid offset');
expect(response.body.message).toBe('Offset must be non-negative');
});
it('should handle service errors', async () => {
mockExportService.getExportHistory.mockRejectedValue(
new Error('Database error')
);
const response = await request(app)
.get('/api/inventory/export/history');
expect(response.status).toBe(500);
expect(response.body.success).toBe(false);
expect(response.body.error).toBe('Failed to retrieve export history');
expect(response.body.message).toBe('Database error');
});
it('should indicate hasMore when limit is reached', async () => {
const mockHistory = new Array(50).fill(null).map((_, i) => ({
id: i + 1,
filename: `export${i + 1}.xlsx`,
total_records: 100,
export_date: '2024-01-15T10:30:00Z'
}));
mockExportService.getExportHistory.mockResolvedValue(mockHistory);
const response = await request(app)
.get('/api/inventory/export/history')
.query({ limit: 50 });
expect(response.body.pagination.hasMore).toBe(true);
});
});
describe('DELETE /api/inventory/export/cleanup', () => {
it('should cleanup old export files successfully', async () => {
const mockCleanupResult = {
success: true,
deletedCount: 5,
message: 'Cleaned up 5 old export files'
};
mockExportService.cleanupOldExports.mockResolvedValue(mockCleanupResult);
const response = await request(app)
.delete('/api/inventory/export/cleanup')
.query({ maxAgeHours: 48 });
expect(response.status).toBe(200);
expect(response.body.success).toBe(true);
expect(response.body.data).toEqual({
deletedCount: 5,
maxAgeHours: 48
});
expect(response.body.message).toBe('Cleaned up 5 old export files');
expect(mockExportService.cleanupOldExports).toHaveBeenCalledWith(48);
});
it('should use default maxAgeHours', async () => {
const mockCleanupResult = {
success: true,
deletedCount: 2,
message: 'Cleaned up 2 old export files'
};
mockExportService.cleanupOldExports.mockResolvedValue(mockCleanupResult);
const response = await request(app)
.delete('/api/inventory/export/cleanup');
expect(response.status).toBe(200);
expect(mockExportService.cleanupOldExports).toHaveBeenCalledWith(24);
});
it('should validate maxAgeHours parameter', async () => {
const response = await request(app)
.delete('/api/inventory/export/cleanup')
.query({ maxAgeHours: 200 });
expect(response.status).toBe(400);
expect(response.body.success).toBe(false);
expect(response.body.error).toBe('Invalid maxAgeHours');
expect(response.body.message).toBe('maxAgeHours must be between 1 and 168 (1 week)');
});
it('should handle cleanup service errors', async () => {
mockExportService.cleanupOldExports.mockResolvedValue({
success: false,
error: 'Permission denied'
});
const response = await request(app)
.delete('/api/inventory/export/cleanup');
expect(response.status).toBe(500);
expect(response.body.success).toBe(false);
expect(response.body.error).toBe('Cleanup failed');
expect(response.body.message).toBe('Permission denied');
});
it('should handle service exceptions', async () => {
mockExportService.cleanupOldExports.mockRejectedValue(
new Error('File system error')
);
const response = await request(app)
.delete('/api/inventory/export/cleanup');
expect(response.status).toBe(500);
expect(response.body.success).toBe(false);
expect(response.body.error).toBe('Cleanup failed');
expect(response.body.message).toBe('File system error');
});
});
describe('File upload validation', () => {
it('should reject non-Excel files', async () => {
const mockFileBuffer = Buffer.from('not an excel file');
const response = await request(app)
.post('/api/inventory/export/with-original')
.attach('originalFile', mockFileBuffer, 'document.pdf');
expect(response.status).toBe(400);
expect(response.text).toContain('Only Excel files');
});
it('should accept .xlsx files', async () => {
const mockExportResult = {
success: true,
filePath: '/test/path/export.xlsx',
filename: 'updated_inventory.xlsx',
sessionId: 126,
recordCount: 10
};
mockExportService.exportInventoryToExcel.mockResolvedValue(mockExportResult);
const mockFileBuffer = Buffer.from('mock excel data');
const response = await request(app)
.post('/api/inventory/export/with-original')
.attach('originalFile', mockFileBuffer, 'original.xlsx');
expect(response.status).toBe(200);
});
it('should accept .xls files', async () => {
const mockExportResult = {
success: true,
filePath: '/test/path/export.xlsx',
filename: 'updated_inventory.xlsx',
sessionId: 127,
recordCount: 10
};
mockExportService.exportInventoryToExcel.mockResolvedValue(mockExportResult);
const mockFileBuffer = Buffer.from('mock excel data');
const response = await request(app)
.post('/api/inventory/export/with-original')
.attach('originalFile', mockFileBuffer, 'original.xls');
expect(response.status).toBe(200);
});
it('should enforce file size limit', async () => {
// This test would require creating a file larger than 10MB
// For now, we'll just verify the multer configuration is set correctly
const multerConfig = inventoryRoutes.stack
.find(layer => layer.route?.path === '/export/with-original')
?.route?.stack?.[0]?.handle?.options;
// Note: This is a simplified test since we can't easily test file size limits
// in this test environment without creating large files
expect(true).toBe(true); // Placeholder assertion
});
});
describe('Response headers', () => {
it('should set correct headers for Excel export', async () => {
const mockExportResult = {
success: true,
filePath: '/test/path/export.xlsx',
filename: 'inventory_export_2024-01-15.xlsx',
sessionId: 128,
recordCount: 100
};
mockExportService.exportInventoryToExcel.mockResolvedValue(mockExportResult);
// Mock res.sendFile to capture headers
const originalSendFile = express.response.sendFile;
let capturedHeaders = {};
express.response.sendFile = function(filePath, callback) {
capturedHeaders = { ...this.getHeaders() };
if (callback) callback();
return this;
};
try {
await request(app)
.get('/api/inventory/export')
.query({ format: 'xlsx' });
// Verify headers would be set (note: supertest doesn't capture custom headers easily)
expect(mockExportService.exportInventoryToExcel).toHaveBeenCalled();
} finally {
express.response.sendFile = originalSendFile;
}
});
it('should set correct headers for CSV export', async () => {
const mockExportResult = {
success: true,
filePath: '/test/path/export.csv',
filename: 'inventory_export_2024-01-15.csv',
sessionId: 129,
recordCount: 100
};
mockExportService.exportInventoryToExcel.mockResolvedValue(mockExportResult);
await request(app)
.get('/api/inventory/export')
.query({ format: 'csv' });
expect(mockExportService.exportInventoryToExcel).toHaveBeenCalledWith(
expect.objectContaining({
format: 'csv'
})
);
});
});
});