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); }); } }); }); });