Files
inventory-barcode-system/__tests__/integration.test.js

394 lines
14 KiB
JavaScript

const request = require('supertest');
const app = require('../server');
const database = require('../models/database');
const fs = require('fs');
const path = require('path');
const XLSX = require('xlsx');
describe('Integration Tests - Complete Workflows', () => {
let testDbPath;
beforeAll(async () => {
// Set up test database
testDbPath = path.join(__dirname, '..', 'test_integration.db');
if (fs.existsSync(testDbPath)) {
fs.unlinkSync(testDbPath);
}
database.dbPath = testDbPath;
await database.initialize();
});
afterAll(async () => {
database.close();
if (fs.existsSync(testDbPath)) {
fs.unlinkSync(testDbPath);
}
});
beforeEach(async () => {
// Clean up database before each test
const db = database.getDatabase();
db.exec('DELETE FROM inventory_history');
db.exec('DELETE FROM inventory');
db.exec('DELETE FROM products');
db.exec('DELETE FROM import_sessions');
});
describe('End-to-End Workflow: Import → Generate → Scan → Export', () => {
test('should complete full workflow successfully', async () => {
// Step 1: Import Excel file with products
const testData = [
['Product Code', 'Description', 'Quantity', 'Category'],
['ABC123', 'Test Product 1', 100, 'Electronics'],
['DEF456', 'Test Product 2', 50, 'Books'],
['GHI789', 'Test Product 3', 75, 'Clothing']
];
const worksheet = XLSX.utils.aoa_to_sheet(testData);
const workbook = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(workbook, worksheet, 'Products');
const buffer = XLSX.write(workbook, { type: 'buffer', bookType: 'xlsx' });
// Import products
const importResponse = await request(app)
.post('/api/products/import/excel')
.attach('file', buffer, 'test-products.xlsx')
.expect(200);
expect(importResponse.body.success).toBe(true);
expect(importResponse.body.data.importResults.imported).toBe(3);
// Verify products were created
const productsResponse = await request(app)
.get('/api/products')
.expect(200);
expect(productsResponse.body.data).toHaveLength(3);
const productIds = productsResponse.body.data.map(p => p.id);
// Step 2: Generate barcodes for products (using layout generation as proxy)
const generateResponse = await request(app)
.post('/api/codes/layouts/preview')
.send({
productIds: productIds.slice(0, 3), // Preview only takes first 5
options: { format: 'code128', includeQR: true }
})
.expect(200);
expect(generateResponse.body.success).toBe(true);
expect(generateResponse.body.data.sampleProducts).toHaveLength(3);
// Step 3: Simulate scanning and inventory updates
for (let i = 0; i < productIds.length; i++) {
const productId = productIds[i];
const newLevel = 80 + (i * 10); // Different levels for each product
const scanResponse = await request(app)
.put(`/api/inventory/product/${productId}/level`)
.send({
newLevel: newLevel,
changeReason: 'Scanned inventory update',
updatedBy: 'scanner-user'
})
.expect(200);
expect(scanResponse.body.success).toBe(true);
expect(scanResponse.body.data.current_level).toBe(newLevel);
}
// Verify inventory history was recorded
const historyResponse = await request(app)
.get(`/api/inventory/product/${productIds[0]}/history`)
.expect(200);
expect(historyResponse.body.data).toHaveLength(2); // Initial + update
// Step 4: Export updated inventory data
const exportResponse = await request(app)
.get('/api/codes/export/excel')
.expect(200);
expect(exportResponse.headers['content-type']).toContain('application/vnd.openxmlformats');
expect(exportResponse.headers['content-disposition']).toContain('attachment');
// Verify export contains updated data
const exportedWorkbook = XLSX.read(exportResponse.body, { type: 'buffer' });
const exportedSheet = exportedWorkbook.Sheets[exportedWorkbook.SheetNames[0]];
const exportedData = XLSX.utils.sheet_to_json(exportedSheet);
expect(exportedData).toHaveLength(3);
expect(exportedData[0]['Current Level']).toBe(80);
expect(exportedData[1]['Current Level']).toBe(90);
expect(exportedData[2]['Current Level']).toBe(100);
});
test('should handle workflow with validation errors', async () => {
// Import data with some invalid entries
const testData = [
['Product Code', 'Description', 'Quantity', 'Category'],
['ABC123', 'Valid Product', 100, 'Electronics'],
['', 'Invalid Product - No Code', 50, 'Books'], // Missing product code
['DEF@456', 'Invalid Product - Bad Code', -10, 'Clothing'] // Invalid code and negative quantity
];
const worksheet = XLSX.utils.aoa_to_sheet(testData);
const workbook = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(workbook, worksheet, 'Products');
const buffer = XLSX.write(workbook, { type: 'buffer', bookType: 'xlsx' });
const importResponse = await request(app)
.post('/api/products/import/excel')
.attach('file', buffer, 'test-invalid.xlsx')
.expect(200);
expect(importResponse.body.success).toBe(true);
expect(importResponse.body.data.importResults.imported).toBe(1); // Only valid product
expect(importResponse.body.data.validationResults.statistics.invalidProducts).toBe(2); // Two invalid products
// Verify only valid product was imported
const productsResponse = await request(app)
.get('/api/products')
.expect(200);
expect(productsResponse.body.data).toHaveLength(1);
expect(productsResponse.body.data[0].product_code).toBe('ABC123');
});
test('should handle concurrent inventory updates', async () => {
// First, create a product
const createResponse = await request(app)
.post('/api/products')
.send({
name: 'CONCURRENT123',
description: 'Concurrent Test Product',
category: 'Test'
})
.expect(201);
const productId = createResponse.body.data.id;
// Create initial inventory
await request(app)
.post(`/api/inventory/product/${productId}`)
.send({
initialLevel: 100,
minimumLevel: 10,
maximumLevel: 200,
updatedBy: 'test-user'
})
.expect(201);
// Simulate concurrent updates
const updatePromises = [];
for (let i = 0; i < 5; i++) {
const promise = request(app)
.put(`/api/inventory/product/${productId}/level`)
.send({
newLevel: 100 + i,
changeReason: `Concurrent update ${i}`,
updatedBy: `user-${i}`
});
updatePromises.push(promise);
}
const results = await Promise.allSettled(updatePromises);
// At least one should succeed
const successfulUpdates = results.filter(r => r.status === 'fulfilled' && r.value.status === 200);
expect(successfulUpdates.length).toBeGreaterThan(0);
// Some might fail due to concurrent update detection
const conflictUpdates = results.filter(r => r.status === 'fulfilled' && r.value.status === 409);
// Total successful + conflicts should equal total attempts
expect(successfulUpdates.length + conflictUpdates.length).toBe(5);
// Verify final state is consistent
const finalResponse = await request(app)
.get(`/api/inventory/product/${productId}`)
.expect(200);
expect(finalResponse.body.data.current_level).toBeGreaterThanOrEqual(100);
expect(finalResponse.body.data.current_level).toBeLessThanOrEqual(104);
});
});
describe('Bulk Operations Workflow', () => {
test('should handle bulk import and bulk updates', async () => {
// Create large dataset for bulk operations
const testData = [['Product Code', 'Description', 'Quantity', 'Category']];
for (let i = 1; i <= 50; i++) {
testData.push([
`BULK${i.toString().padStart(3, '0')}`,
`Bulk Product ${i}`,
Math.floor(Math.random() * 100) + 10,
i % 2 === 0 ? 'Electronics' : 'Books'
]);
}
const worksheet = XLSX.utils.aoa_to_sheet(testData);
const workbook = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(workbook, worksheet, 'BulkProducts');
const buffer = XLSX.write(workbook, { type: 'buffer', bookType: 'xlsx' });
// Bulk import
const importStart = Date.now();
const importResponse = await request(app)
.post('/api/products/import/excel')
.attach('file', buffer, 'bulk-products.xlsx')
.expect(200);
const importDuration = Date.now() - importStart;
expect(importResponse.body.success).toBe(true);
expect(importResponse.body.data.importResults.imported).toBe(50);
expect(importDuration).toBeLessThan(5000); // Should complete within 5 seconds
// Get all product IDs
const productsResponse = await request(app)
.get('/api/products')
.expect(200);
const productIds = productsResponse.body.data.map(p => p.id);
// Bulk inventory updates
const bulkUpdates = productIds.map((id, index) => ({
productId: id,
newLevel: 50 + index,
changeReason: 'Bulk inventory update',
updatedBy: 'bulk-user'
}));
const bulkUpdateStart = Date.now();
const bulkUpdateResponse = await request(app)
.post('/api/inventory/bulk-update')
.send({ updates: bulkUpdates })
.expect(200);
const bulkUpdateDuration = Date.now() - bulkUpdateStart;
expect(bulkUpdateResponse.body.success).toBe(true);
expect(bulkUpdateResponse.body.count).toBe(50);
expect(bulkUpdateDuration).toBeLessThan(3000); // Should complete within 3 seconds
// Verify bulk export performance
const exportStart = Date.now();
const exportResponse = await request(app)
.get('/api/codes/export/excel')
.expect(200);
const exportDuration = Date.now() - exportStart;
expect(exportDuration).toBeLessThan(2000); // Should complete within 2 seconds
expect(exportResponse.headers['content-type']).toContain('application/vnd.openxmlformats');
});
});
describe('Error Recovery Workflow', () => {
test('should recover from database connection issues', async () => {
// Create a product first
const createResponse = await request(app)
.post('/api/products')
.send({
name: 'RECOVERY123',
description: 'Recovery Test Product',
category: 'Test'
})
.expect(201);
const productId = createResponse.body.data.id;
// Simulate database connection issue by closing and reopening
database.close();
// This should fail initially
const failedResponse = await request(app)
.get(`/api/products/${productId}`)
.expect(500);
expect(failedResponse.body.success).toBe(false);
// Reinitialize database
await database.initialize();
// This should work after recovery
const recoveredResponse = await request(app)
.get(`/api/products/${productId}`)
.expect(200);
expect(recoveredResponse.body.success).toBe(true);
expect(recoveredResponse.body.data.name).toBe('RECOVERY123');
});
});
describe('Data Consistency Workflow', () => {
test('should maintain data consistency across operations', async () => {
// Create products with inventory
const products = [];
for (let i = 1; i <= 10; i++) {
const createResponse = await request(app)
.post('/api/products')
.send({
name: `CONSISTENCY${i}`,
description: `Consistency Test Product ${i}`,
category: 'Test'
})
.expect(201);
products.push(createResponse.body.data);
// Create inventory for each product
await request(app)
.post(`/api/inventory/product/${createResponse.body.data.id}`)
.send({
initialLevel: 100,
minimumLevel: 10,
maximumLevel: 200,
updatedBy: 'consistency-test'
})
.expect(201);
}
// Perform various operations and verify consistency
const operations = [];
// Update inventory levels
for (let i = 0; i < products.length; i++) {
operations.push(
request(app)
.put(`/api/inventory/product/${products[i].id}/level`)
.send({
newLevel: 80 + i,
changeReason: 'Consistency test update',
updatedBy: 'consistency-user'
})
);
}
// Execute all operations
const results = await Promise.all(operations);
results.forEach(result => {
expect(result.status).toBe(200);
expect(result.body.success).toBe(true);
});
// Verify data consistency
const inventoryResponse = await request(app)
.get('/api/inventory')
.expect(200);
expect(inventoryResponse.body.data).toHaveLength(10);
// Check that all inventory levels are correct
inventoryResponse.body.data.forEach((item, index) => {
expect(item.current_level).toBe(80 + index);
});
// Verify history records exist for all updates
for (let i = 0; i < products.length; i++) {
const historyResponse = await request(app)
.get(`/api/inventory/product/${products[i].id}/history`)
.expect(200);
expect(historyResponse.body.data).toHaveLength(2); // Initial + update
}
});
});
});