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,539 @@
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('Performance Tests - Large Datasets', () => {
let testDbPath;
beforeAll(async () => {
// Set up test database with performance optimizations
testDbPath = path.join(__dirname, '..', 'test_performance.db');
if (fs.existsSync(testDbPath)) {
fs.unlinkSync(testDbPath);
}
database.dbPath = testDbPath;
await database.initialize();
// Apply additional performance settings for testing
const db = database.getDatabase();
db.pragma('cache_size = 2000'); // Increase cache size for tests
db.pragma('temp_store = memory');
db.pragma('mmap_size = 536870912'); // 512MB
});
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('Large Dataset Import Performance', () => {
test('should import 1000+ products within acceptable time', async () => {
const productCount = 1000;
const testData = [['Product Code', 'Description', 'Quantity', 'Category', 'Unit of Measure']];
// Generate large dataset
console.log(`Generating ${productCount} test products...`);
for (let i = 1; i <= productCount; i++) {
testData.push([
`PERF${i.toString().padStart(6, '0')}`,
`Performance Test Product ${i} - ${Math.random().toString(36).substring(7)}`,
Math.floor(Math.random() * 1000) + 1,
['Electronics', 'Books', 'Clothing', 'Home', 'Sports'][i % 5],
['pcs', 'kg', 'lbs', 'units', 'boxes'][i % 5]
]);
}
const worksheet = XLSX.utils.aoa_to_sheet(testData);
const workbook = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(workbook, worksheet, 'LargeDataset');
const buffer = XLSX.write(workbook, { type: 'buffer', bookType: 'xlsx' });
console.log(`Starting import of ${productCount} products...`);
const startTime = Date.now();
const response = await request(app)
.post('/api/products/import/excel')
.attach('file', buffer, 'large-dataset.xlsx')
.timeout(30000) // 30 second timeout
.expect(200);
const duration = Date.now() - startTime;
console.log(`Import completed in ${duration}ms (${(duration/1000).toFixed(2)}s)`);
console.log(`Average: ${(duration/productCount).toFixed(2)}ms per product`);
expect(response.body.success).toBe(true);
expect(response.body.data.importResults.imported).toBe(productCount);
expect(duration).toBeLessThan(15000); // Should complete within 15 seconds
expect(duration / productCount).toBeLessThan(15); // Less than 15ms per product
});
test('should handle 5000+ products with memory efficiency', async () => {
const productCount = 5000;
console.log(`Testing memory efficiency with ${productCount} products...`);
const initialMemory = process.memoryUsage();
console.log('Initial memory usage:', {
rss: `${(initialMemory.rss / 1024 / 1024).toFixed(2)}MB`,
heapUsed: `${(initialMemory.heapUsed / 1024 / 1024).toFixed(2)}MB`
});
// Generate large dataset in chunks to test streaming
const chunkSize = 1000;
let totalImported = 0;
for (let chunk = 0; chunk < Math.ceil(productCount / chunkSize); chunk++) {
const chunkStart = chunk * chunkSize + 1;
const chunkEnd = Math.min((chunk + 1) * chunkSize, productCount);
const chunkData = [['Product Code', 'Description', 'Quantity', 'Category']];
for (let i = chunkStart; i <= chunkEnd; i++) {
chunkData.push([
`CHUNK${chunk}_${i.toString().padStart(6, '0')}`,
`Chunk ${chunk} Product ${i}`,
Math.floor(Math.random() * 100) + 1,
`Category${i % 10}`
]);
}
const worksheet = XLSX.utils.aoa_to_sheet(chunkData);
const workbook = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(workbook, worksheet, `Chunk${chunk}`);
const buffer = XLSX.write(workbook, { type: 'buffer', bookType: 'xlsx' });
const chunkStartTime = Date.now();
const response = await request(app)
.post('/api/products/import/excel')
.attach('file', buffer, `chunk-${chunk}.xlsx`)
.timeout(30000)
.expect(200);
const chunkDuration = Date.now() - chunkStartTime;
totalImported += response.body.data.importResults.imported;
console.log(`Chunk ${chunk + 1}/${Math.ceil(productCount / chunkSize)} completed in ${chunkDuration}ms`);
// Check memory usage after each chunk
const currentMemory = process.memoryUsage();
const memoryIncrease = (currentMemory.heapUsed - initialMemory.heapUsed) / 1024 / 1024;
console.log(`Memory increase: ${memoryIncrease.toFixed(2)}MB`);
// Memory should not increase excessively (less than 100MB per 1000 products)
expect(memoryIncrease).toBeLessThan(100 * (chunk + 1));
}
expect(totalImported).toBe(productCount);
console.log(`Total imported: ${totalImported} products`);
// Verify final memory usage is reasonable
const finalMemory = process.memoryUsage();
const totalMemoryIncrease = (finalMemory.heapUsed - initialMemory.heapUsed) / 1024 / 1024;
console.log(`Total memory increase: ${totalMemoryIncrease.toFixed(2)}MB`);
expect(totalMemoryIncrease).toBeLessThan(500); // Less than 500MB total increase
});
});
describe('Database Query Performance', () => {
beforeEach(async () => {
// Create a large dataset for query testing
const db = database.getDatabase();
const insertProduct = db.prepare(`
INSERT INTO products (product_code, description, category, unit_of_measure)
VALUES (?, ?, ?, ?)
`);
const insertInventory = db.prepare(`
INSERT INTO inventory (product_id, current_level, minimum_level, maximum_level)
VALUES (?, ?, ?, ?)
`);
console.log('Setting up large dataset for query performance tests...');
const transaction = db.transaction(() => {
for (let i = 1; i <= 2000; i++) {
const result = insertProduct.run(
`QUERY${i.toString().padStart(6, '0')}`,
`Query Test Product ${i}`,
`Category${i % 20}`,
'pcs'
);
insertInventory.run(
result.lastInsertRowid,
Math.floor(Math.random() * 1000) + 1,
10,
500
);
}
});
transaction();
console.log('Large dataset setup completed');
});
test('should perform fast product lookups with indexes', async () => {
const iterations = 100;
const startTime = Date.now();
for (let i = 0; i < iterations; i++) {
const productCode = `QUERY${Math.floor(Math.random() * 2000 + 1).toString().padStart(6, '0')}`;
const response = await request(app)
.get(`/api/products/barcode/${productCode}`)
.expect(200);
expect(response.body.success).toBe(true);
}
const duration = Date.now() - startTime;
const avgTime = duration / iterations;
console.log(`${iterations} product lookups completed in ${duration}ms`);
console.log(`Average lookup time: ${avgTime.toFixed(2)}ms`);
expect(avgTime).toBeLessThan(50); // Less than 50ms per lookup
});
test('should perform fast inventory queries with pagination', async () => {
const pageSize = 50;
const totalPages = 10;
const startTime = Date.now();
for (let page = 0; page < totalPages; page++) {
const response = await request(app)
.get(`/api/inventory?limit=${pageSize}&offset=${page * pageSize}`)
.expect(200);
expect(response.body.success).toBe(true);
expect(response.body.data.length).toBeLessThanOrEqual(pageSize);
}
const duration = Date.now() - startTime;
const avgTime = duration / totalPages;
console.log(`${totalPages} paginated queries completed in ${duration}ms`);
console.log(`Average query time: ${avgTime.toFixed(2)}ms`);
expect(avgTime).toBeLessThan(100); // Less than 100ms per paginated query
});
test('should perform fast filtered searches', async () => {
const categories = ['Category0', 'Category5', 'Category10', 'Category15'];
const startTime = Date.now();
for (const category of categories) {
const response = await request(app)
.get(`/api/inventory?category=${category}`)
.expect(200);
expect(response.body.success).toBe(true);
expect(response.body.data.length).toBeGreaterThan(0);
// Verify all results match the filter
response.body.data.forEach(item => {
expect(item.category).toBe(category);
});
}
const duration = Date.now() - startTime;
const avgTime = duration / categories.length;
console.log(`${categories.length} filtered searches completed in ${duration}ms`);
console.log(`Average search time: ${avgTime.toFixed(2)}ms`);
expect(avgTime).toBeLessThan(200); // Less than 200ms per filtered search
});
test('should handle complex aggregation queries efficiently', async () => {
const startTime = Date.now();
// Test low stock query
const lowStockResponse = await request(app)
.get('/api/inventory/low-stock')
.expect(200);
// Test inventory summary with grouping
const summaryResponse = await request(app)
.get('/api/inventory?groupBy=category')
.expect(200);
const duration = Date.now() - startTime;
console.log(`Complex aggregation queries completed in ${duration}ms`);
expect(duration).toBeLessThan(1000); // Less than 1 second for complex queries
expect(lowStockResponse.body.success).toBe(true);
expect(summaryResponse.body.success).toBe(true);
});
});
describe('Concurrent Operations Performance', () => {
beforeEach(async () => {
// Create products for concurrent testing
const db = database.getDatabase();
const insertProduct = db.prepare(`
INSERT INTO products (product_code, description, category)
VALUES (?, ?, ?)
`);
const insertInventory = db.prepare(`
INSERT INTO inventory (product_id, current_level, minimum_level, maximum_level)
VALUES (?, ?, ?, ?)
`);
const transaction = db.transaction(() => {
for (let i = 1; i <= 100; i++) {
const result = insertProduct.run(
`CONCURRENT${i.toString().padStart(3, '0')}`,
`Concurrent Test Product ${i}`,
'Test'
);
insertInventory.run(result.lastInsertRowid, 100, 10, 200);
}
});
transaction();
});
test('should handle concurrent inventory updates efficiently', async () => {
const concurrentUsers = 20;
const updatesPerUser = 5;
console.log(`Testing ${concurrentUsers} concurrent users with ${updatesPerUser} updates each...`);
const startTime = Date.now();
const promises = [];
for (let user = 0; user < concurrentUsers; user++) {
for (let update = 0; update < updatesPerUser; update++) {
const productId = Math.floor(Math.random() * 100) + 1;
const newLevel = Math.floor(Math.random() * 200) + 1;
const promise = request(app)
.put(`/api/inventory/product/${productId}/level`)
.send({
newLevel: newLevel,
changeReason: `Concurrent update by user ${user}`,
updatedBy: `user-${user}`
});
promises.push(promise);
}
}
const results = await Promise.allSettled(promises);
const duration = Date.now() - startTime;
const successful = results.filter(r => r.status === 'fulfilled' && r.value.status === 200).length;
const conflicts = results.filter(r => r.status === 'fulfilled' && r.value.status === 409).length;
const errors = results.filter(r => r.status === 'rejected' || (r.status === 'fulfilled' && r.value.status >= 500)).length;
console.log(`Concurrent updates completed in ${duration}ms`);
console.log(`Successful: ${successful}, Conflicts: ${conflicts}, Errors: ${errors}`);
console.log(`Average time per update: ${(duration / promises.length).toFixed(2)}ms`);
expect(successful + conflicts).toBe(promises.length); // All should either succeed or conflict
expect(errors).toBe(0); // No server errors
expect(duration).toBeLessThan(10000); // Complete within 10 seconds
expect(successful).toBeGreaterThan(promises.length * 0.7); // At least 70% success rate
});
test('should handle concurrent read operations efficiently', async () => {
const concurrentReads = 50;
console.log(`Testing ${concurrentReads} concurrent read operations...`);
const startTime = Date.now();
const promises = [];
for (let i = 0; i < concurrentReads; i++) {
const operations = [
request(app).get('/api/products'),
request(app).get('/api/inventory'),
request(app).get('/api/inventory/low-stock'),
request(app).get(`/api/products/${Math.floor(Math.random() * 100) + 1}`),
request(app).get(`/api/inventory/product/${Math.floor(Math.random() * 100) + 1}`)
];
promises.push(...operations);
}
const results = await Promise.allSettled(promises);
const duration = Date.now() - startTime;
const successful = results.filter(r => r.status === 'fulfilled' && r.value.status === 200).length;
const errors = results.filter(r => r.status === 'rejected' || (r.status === 'fulfilled' && r.value.status >= 400)).length;
console.log(`${promises.length} concurrent reads completed in ${duration}ms`);
console.log(`Successful: ${successful}, Errors: ${errors}`);
console.log(`Average time per read: ${(duration / promises.length).toFixed(2)}ms`);
expect(successful).toBe(promises.length); // All reads should succeed
expect(errors).toBe(0);
expect(duration).toBeLessThan(5000); // Complete within 5 seconds
expect(duration / promises.length).toBeLessThan(100); // Less than 100ms per read
});
});
describe('Export Performance', () => {
beforeEach(async () => {
// Create large dataset for export testing
const db = database.getDatabase();
const insertProduct = db.prepare(`
INSERT INTO products (product_code, description, category, unit_of_measure)
VALUES (?, ?, ?, ?)
`);
const insertInventory = db.prepare(`
INSERT INTO inventory (product_id, current_level, minimum_level, maximum_level, last_updated, updated_by)
VALUES (?, ?, ?, ?, datetime('now'), ?)
`);
console.log('Setting up large dataset for export performance tests...');
const transaction = db.transaction(() => {
for (let i = 1; i <= 1500; i++) {
const result = insertProduct.run(
`EXPORT${i.toString().padStart(6, '0')}`,
`Export Test Product ${i} - ${Math.random().toString(36).substring(7)}`,
`Category${i % 15}`,
['pcs', 'kg', 'lbs', 'units'][i % 4]
);
insertInventory.run(
result.lastInsertRowid,
Math.floor(Math.random() * 500) + 1,
Math.floor(Math.random() * 20) + 5,
Math.floor(Math.random() * 200) + 300,
`export-user-${i % 10}`
);
}
});
transaction();
console.log('Export dataset setup completed');
});
test('should export large datasets efficiently', async () => {
console.log('Testing large dataset export performance...');
const startTime = Date.now();
const response = await request(app)
.get('/api/codes/export/excel')
.timeout(15000) // 15 second timeout
.expect(200);
const duration = Date.now() - startTime;
console.log(`Export of 1500 products completed in ${duration}ms (${(duration/1000).toFixed(2)}s)`);
expect(response.headers['content-type']).toContain('application/vnd.openxmlformats');
expect(duration).toBeLessThan(10000); // Should complete within 10 seconds
// Verify export content
const workbook = XLSX.read(response.body, { type: 'buffer' });
const sheet = workbook.Sheets[workbook.SheetNames[0]];
const data = XLSX.utils.sheet_to_json(sheet);
expect(data.length).toBe(1500);
expect(data[0]).toHaveProperty('Product Code');
expect(data[0]).toHaveProperty('Description');
expect(data[0]).toHaveProperty('Current Level');
});
test('should handle filtered exports efficiently', async () => {
const filters = [
{ category: 'Category0' },
{ category: 'Category5' },
{ lowStock: 'true' },
{ category: 'Category10', minLevel: 50 }
];
for (const filter of filters) {
const queryString = new URLSearchParams(filter).toString();
const startTime = Date.now();
const response = await request(app)
.get(`/api/codes/export/excel?${queryString}`)
.timeout(10000)
.expect(200);
const duration = Date.now() - startTime;
console.log(`Filtered export (${queryString}) completed in ${duration}ms`);
expect(duration).toBeLessThan(5000); // Filtered exports should be faster
expect(response.headers['content-type']).toContain('application/vnd.openxmlformats');
}
});
});
describe('Memory and Resource Management', () => {
test('should maintain stable memory usage during sustained operations', async () => {
const initialMemory = process.memoryUsage();
console.log('Initial memory:', {
rss: `${(initialMemory.rss / 1024 / 1024).toFixed(2)}MB`,
heapUsed: `${(initialMemory.heapUsed / 1024 / 1024).toFixed(2)}MB`
});
// Perform sustained operations
for (let cycle = 0; cycle < 10; cycle++) {
// Create some products
const products = [];
for (let i = 0; i < 50; i++) {
const response = await request(app)
.post('/api/products')
.send({
name: `MEMORY${cycle}_${i}`,
description: `Memory Test Product ${cycle}-${i}`,
category: 'MemoryTest'
})
.expect(201);
products.push(response.body.data.id);
}
// Update inventory levels
for (const productId of products) {
await request(app)
.post(`/api/inventory/product/${productId}`)
.send({
initialLevel: Math.floor(Math.random() * 100) + 1,
minimumLevel: 5,
maximumLevel: 150,
updatedBy: 'memory-test'
})
.expect(201);
}
// Clean up products to test garbage collection
for (const productId of products) {
await request(app)
.delete(`/api/products/${productId}`)
.expect(200);
}
// Check memory usage
const currentMemory = process.memoryUsage();
const memoryIncrease = (currentMemory.heapUsed - initialMemory.heapUsed) / 1024 / 1024;
console.log(`Cycle ${cycle + 1}/10 - Memory increase: ${memoryIncrease.toFixed(2)}MB`);
// Memory should not continuously increase
expect(memoryIncrease).toBeLessThan(50 * (cycle + 1)); // Allow some growth but not excessive
}
// Force garbage collection if available
if (global.gc) {
global.gc();
}
const finalMemory = process.memoryUsage();
const totalIncrease = (finalMemory.heapUsed - initialMemory.heapUsed) / 1024 / 1024;
console.log('Final memory increase:', `${totalIncrease.toFixed(2)}MB`);
expect(totalIncrease).toBeLessThan(100); // Total increase should be reasonable
});
});
});