394 lines
14 KiB
JavaScript
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
|
|
}
|
|
});
|
|
});
|
|
}); |