Initial commit: Inventory Barcode System

This commit is contained in:
2025-07-22 20:24:51 -04:00
commit 511b01748d
63 changed files with 26932 additions and 0 deletions

View File

@ -0,0 +1,503 @@
const request = require('supertest');
const app = require('../server');
const database = require('../models/database');
const fs = require('fs');
const path = require('path');
describe('Concurrency and Locking Tests', () => {
let testDbPath;
beforeAll(async () => {
// Set up test database
testDbPath = path.join(__dirname, '..', 'test_concurrency.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('Optimistic Locking for Inventory Updates', () => {
let productId;
beforeEach(async () => {
// Create a test product with inventory
const createResponse = await request(app)
.post('/api/products')
.send({
product_code: 'LOCK_TEST_001',
description: 'Locking Test Product',
category: 'Test'
})
.expect(201);
productId = createResponse.body.data.id;
await request(app)
.post(`/api/inventory/product/${productId}`)
.send({
initialLevel: 100,
minimumLevel: 10,
maximumLevel: 200,
updatedBy: 'test-setup'
})
.expect(201);
});
test('should handle concurrent updates with optimistic locking', async () => {
const concurrentUpdates = 10;
const promises = [];
// Create multiple concurrent update requests
for (let i = 0; i < concurrentUpdates; i++) {
const promise = request(app)
.put(`/api/inventory/product/${productId}/level`)
.send({
newLevel: 100 + i,
changeReason: `Concurrent update ${i}`,
updatedBy: `user-${i}`
});
promises.push(promise);
}
const results = await Promise.allSettled(promises);
// Analyze results
const successful = results.filter(r => r.status === 'fulfilled' && r.value.status === 200);
const conflicts = results.filter(r => r.status === 'fulfilled' && r.value.status === 409);
const errors = results.filter(r => r.status === 'rejected' || (r.status === 'fulfilled' && r.value.status >= 500));
console.log(`Concurrent updates: ${concurrentUpdates}`);
console.log(`Successful: ${successful.length}, Conflicts: ${conflicts.length}, Errors: ${errors.length}`);
// Expectations
expect(successful.length).toBeGreaterThan(0); // At least one should succeed
expect(successful.length + conflicts.length).toBe(concurrentUpdates); // All should either succeed or conflict
expect(errors.length).toBe(0); // No server errors
// 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).toBeLessThan(100 + concurrentUpdates);
// Verify history records match successful updates
const historyResponse = await request(app)
.get(`/api/inventory/product/${productId}/history`)
.expect(200);
// Should have initial record + successful updates
expect(historyResponse.body.data.length).toBe(successful.length + 1);
});
test('should maintain data consistency under high concurrency', async () => {
const highConcurrency = 50;
const batchSize = 10;
// Run multiple batches of concurrent updates
for (let batch = 0; batch < Math.ceil(highConcurrency / batchSize); batch++) {
const batchPromises = [];
for (let i = 0; i < batchSize && (batch * batchSize + i) < highConcurrency; i++) {
const updateIndex = batch * batchSize + i;
const promise = request(app)
.put(`/api/inventory/product/${productId}/level`)
.send({
newLevel: 50 + updateIndex,
changeReason: `Batch ${batch} update ${i}`,
updatedBy: `batch-user-${updateIndex}`
});
batchPromises.push(promise);
}
const batchResults = await Promise.allSettled(batchPromises);
// Small delay between batches to allow processing
await new Promise(resolve => setTimeout(resolve, 100));
console.log(`Batch ${batch + 1} completed`);
}
// Verify final consistency
const finalResponse = await request(app)
.get(`/api/inventory/product/${productId}`)
.expect(200);
const historyResponse = await request(app)
.get(`/api/inventory/product/${productId}/history?limit=100`)
.expect(200);
// Verify no data corruption
expect(finalResponse.body.data.current_level).toBeGreaterThanOrEqual(50);
expect(finalResponse.body.data.current_level).toBeLessThan(50 + highConcurrency);
// Verify history integrity
const historyLevels = historyResponse.body.data.map(h => h.new_level);
const uniqueLevels = [...new Set(historyLevels)];
expect(uniqueLevels.length).toBe(historyLevels.length); // No duplicate levels
});
});
describe('Database Transaction Integrity', () => {
test('should maintain transaction integrity during bulk operations', async () => {
// Create multiple products for bulk testing
const productCount = 20;
const products = [];
for (let i = 1; i <= productCount; i++) {
const response = await request(app)
.post('/api/products')
.send({
product_code: `BULK_TX_${i.toString().padStart(3, '0')}`,
description: `Bulk Transaction Test Product ${i}`,
category: 'BulkTest'
})
.expect(201);
products.push(response.body.data);
}
// Create inventory for all products
const inventoryPromises = products.map(product =>
request(app)
.post(`/api/inventory/product/${product.id}`)
.send({
initialLevel: 100,
minimumLevel: 10,
maximumLevel: 200,
updatedBy: 'bulk-setup'
})
);
await Promise.all(inventoryPromises);
// Perform bulk updates with some that should fail
const bulkUpdates = products.map((product, index) => ({
productId: product.id,
newLevel: index % 5 === 0 ? -10 : 50 + index, // Every 5th update is invalid (negative)
changeReason: `Bulk update ${index}`,
updatedBy: 'bulk-user'
}));
const bulkResponse = await request(app)
.post('/api/inventory/bulk-update')
.send({ updates: bulkUpdates });
// Should handle partial failures gracefully
if (bulkResponse.status === 200) {
// If bulk update succeeded, verify only valid updates were applied
expect(bulkResponse.body.count).toBeLessThan(productCount);
} else {
// If bulk update failed, verify no partial updates were applied
expect(bulkResponse.status).toBe(400);
}
// Verify database consistency
const inventoryResponse = await request(app)
.get('/api/inventory')
.expect(200);
inventoryResponse.body.data.forEach(item => {
expect(item.current_level).toBeGreaterThanOrEqual(0); // No negative levels
});
});
test('should handle database deadlocks gracefully', async () => {
// Create products for deadlock testing
const product1Response = await request(app)
.post('/api/products')
.send({
name: 'DEADLOCK_001',
description: 'Deadlock Test Product 1',
category: 'DeadlockTest'
})
.expect(201);
const product2Response = await request(app)
.post('/api/products')
.send({
name: 'DEADLOCK_002',
description: 'Deadlock Test Product 2',
category: 'DeadlockTest'
})
.expect(201);
const product1Id = product1Response.body.data.id;
const product2Id = product2Response.body.data.id;
// Create inventory for both products
await request(app)
.post(`/api/inventory/product/${product1Id}`)
.send({ initialLevel: 100, minimumLevel: 10, maximumLevel: 200, updatedBy: 'deadlock-setup' })
.expect(201);
await request(app)
.post(`/api/inventory/product/${product2Id}`)
.send({ initialLevel: 100, minimumLevel: 10, maximumLevel: 200, updatedBy: 'deadlock-setup' })
.expect(201);
// Create potential deadlock scenario with cross-updates
const deadlockPromises = [];
for (let i = 0; i < 20; i++) {
// Alternate between updating product1 and product2
const productId = i % 2 === 0 ? product1Id : product2Id;
const otherProductId = i % 2 === 0 ? product2Id : product1Id;
// Create updates that might cause deadlocks
deadlockPromises.push(
request(app)
.put(`/api/inventory/product/${productId}/level`)
.send({
newLevel: 80 + i,
changeReason: `Deadlock test ${i}`,
updatedBy: `deadlock-user-${i}`
})
);
}
const results = await Promise.allSettled(deadlockPromises);
// Analyze results - should handle deadlocks without hanging
const successful = results.filter(r => r.status === 'fulfilled' && r.value.status === 200);
const failed = results.filter(r => r.status === 'rejected' || (r.status === 'fulfilled' && r.value.status >= 400));
console.log(`Deadlock test: ${successful.length} successful, ${failed.length} failed`);
// Should complete without hanging (test timeout would catch hanging)
expect(successful.length + failed.length).toBe(deadlockPromises.length);
expect(successful.length).toBeGreaterThan(0); // At least some should succeed
// Verify final state is consistent
const final1Response = await request(app)
.get(`/api/inventory/product/${product1Id}`)
.expect(200);
const final2Response = await request(app)
.get(`/api/inventory/product/${product2Id}`)
.expect(200);
expect(final1Response.body.data.current_level).toBeGreaterThanOrEqual(80);
expect(final2Response.body.data.current_level).toBeGreaterThanOrEqual(80);
});
});
describe('Connection Pool and Resource Management', () => {
test('should handle multiple simultaneous connections efficiently', async () => {
// Create test data
const products = [];
for (let i = 1; i <= 10; i++) {
const response = await request(app)
.post('/api/products')
.send({
name: `CONN_TEST_${i.toString().padStart(3, '0')}`,
description: `Connection Test Product ${i}`,
category: 'ConnectionTest'
})
.expect(201);
products.push(response.body.data);
}
// Simulate many simultaneous read operations
const readOperations = [];
const operationCount = 100;
for (let i = 0; i < operationCount; i++) {
const operations = [
request(app).get('/api/products'),
request(app).get('/api/inventory'),
request(app).get(`/api/products/${products[i % products.length].id}`),
request(app).get('/api/inventory/low-stock')
];
readOperations.push(...operations);
}
const startTime = Date.now();
const results = await Promise.allSettled(readOperations);
const duration = Date.now() - startTime;
const successful = results.filter(r => r.status === 'fulfilled' && r.value.status === 200);
const failed = results.filter(r => r.status === 'rejected' || (r.status === 'fulfilled' && r.value.status >= 400));
console.log(`${readOperations.length} simultaneous operations completed in ${duration}ms`);
console.log(`Successful: ${successful.length}, Failed: ${failed.length}`);
console.log(`Average time per operation: ${(duration / readOperations.length).toFixed(2)}ms`);
expect(successful.length).toBe(readOperations.length); // All reads should succeed
expect(failed.length).toBe(0);
expect(duration).toBeLessThan(10000); // Should complete within 10 seconds
});
test('should handle mixed read/write operations under load', async () => {
// Create test products
const products = [];
for (let i = 1; i <= 5; i++) {
const response = await request(app)
.post('/api/products')
.send({
name: `MIXED_TEST_${i.toString().padStart(3, '0')}`,
description: `Mixed Operations Test Product ${i}`,
category: 'MixedTest'
})
.expect(201);
products.push(response.body.data);
// Create inventory
await request(app)
.post(`/api/inventory/product/${response.body.data.id}`)
.send({
initialLevel: 100,
minimumLevel: 10,
maximumLevel: 200,
updatedBy: 'mixed-setup'
})
.expect(201);
}
// Mix of read and write operations
const mixedOperations = [];
const totalOperations = 50;
for (let i = 0; i < totalOperations; i++) {
const productId = products[i % products.length].id;
if (i % 3 === 0) {
// Write operation (update inventory)
mixedOperations.push(
request(app)
.put(`/api/inventory/product/${productId}/level`)
.send({
newLevel: 80 + (i % 20),
changeReason: `Mixed test update ${i}`,
updatedBy: `mixed-user-${i}`
})
);
} else {
// Read operations
const readOps = [
request(app).get(`/api/products/${productId}`),
request(app).get(`/api/inventory/product/${productId}`),
request(app).get('/api/inventory?limit=10')
];
mixedOperations.push(readOps[i % readOps.length]);
}
}
const startTime = Date.now();
const results = await Promise.allSettled(mixedOperations);
const duration = Date.now() - startTime;
const successful = results.filter(r => r.status === 'fulfilled' && r.value.status === 200);
const conflicts = results.filter(r => r.status === 'fulfilled' && r.value.status === 409);
const errors = results.filter(r => r.status === 'rejected' || (r.status === 'fulfilled' && r.value.status >= 500));
console.log(`${mixedOperations.length} mixed operations completed in ${duration}ms`);
console.log(`Successful: ${successful.length}, Conflicts: ${conflicts.length}, Errors: ${errors.length}`);
expect(successful.length + conflicts.length).toBe(mixedOperations.length);
expect(errors.length).toBe(0); // No server errors
expect(duration).toBeLessThan(15000); // Should complete within 15 seconds
});
});
describe('Data Consistency Under Stress', () => {
test('should maintain referential integrity under concurrent operations', async () => {
const productCount = 10;
const operationsPerProduct = 10;
// Create products concurrently
const productPromises = [];
for (let i = 1; i <= productCount; i++) {
productPromises.push(
request(app)
.post('/api/products')
.send({
name: `INTEGRITY_${i.toString().padStart(3, '0')}`,
description: `Integrity Test Product ${i}`,
category: 'IntegrityTest'
})
);
}
const productResults = await Promise.all(productPromises);
const products = productResults.map(r => r.body.data);
// Create inventory and perform operations concurrently
const allOperations = [];
products.forEach((product, productIndex) => {
// Create inventory
allOperations.push(
request(app)
.post(`/api/inventory/product/${product.id}`)
.send({
initialLevel: 100,
minimumLevel: 10,
maximumLevel: 200,
updatedBy: 'integrity-setup'
})
);
// Add multiple operations per product
for (let i = 0; i < operationsPerProduct; i++) {
allOperations.push(
request(app)
.put(`/api/inventory/product/${product.id}/level`)
.send({
newLevel: 50 + i + productIndex,
changeReason: `Integrity test ${productIndex}-${i}`,
updatedBy: `integrity-user-${productIndex}-${i}`
})
);
}
});
// Execute all operations
const results = await Promise.allSettled(allOperations);
// Verify referential integrity
const inventoryResponse = await request(app)
.get('/api/inventory')
.expect(200);
expect(inventoryResponse.body.data).toHaveLength(productCount);
// Verify each inventory record has a corresponding product
for (const inventoryItem of inventoryResponse.body.data) {
const productExists = products.some(p => p.id === inventoryItem.product_id);
expect(productExists).toBe(true);
}
// Verify history records maintain referential integrity
for (const product of products) {
const historyResponse = await request(app)
.get(`/api/inventory/product/${product.id}/history`)
.expect(200);
// Should have at least the initial inventory creation
expect(historyResponse.body.data.length).toBeGreaterThan(0);
// All history records should reference the correct product
historyResponse.body.data.forEach(historyItem => {
expect(historyItem.product_id).toBe(product.id);
});
}
});
});
});