const request = require('supertest'); const app = require('../server'); const Inventory = require('../models/Inventory'); const Product = require('../models/Product'); const database = require('../models/database'); // Mock the database and models jest.mock('../models/database'); jest.mock('../models/Inventory'); jest.mock('../models/Product'); describe('Inventory API Endpoints', () => { let mockDb; beforeEach(() => { // Reset all mocks jest.clearAllMocks(); // Create mock database instance mockDb = { prepare: jest.fn(), transaction: jest.fn() }; database.getDatabase.mockReturnValue(mockDb); database.getInstance = jest.fn().mockReturnValue(mockDb); database.executeTransaction = jest.fn(); }); describe('GET /api/inventory', () => { it('should return inventory summary for all products', async () => { const mockInventorySummary = [ { id: 1, product_code: 'P001', description: 'Product 1', current_level: 10, minimum_level: 5, stock_status: 'normal' }, { id: 2, product_code: 'P002', description: 'Product 2', current_level: 2, minimum_level: 5, stock_status: 'low' } ]; Inventory.getInventorySummary.mockResolvedValue(mockInventorySummary); const response = await request(app) .get('/api/inventory') .expect(200); expect(response.body.success).toBe(true); expect(response.body.data).toHaveLength(2); expect(response.body.count).toBe(2); expect(Inventory.getInventorySummary).toHaveBeenCalledWith({}); }); it('should filter inventory by category', async () => { const mockInventorySummary = [ { id: 1, product_code: 'P001', description: 'Product 1', category: 'Electronics', current_level: 10 } ]; Inventory.getInventorySummary.mockResolvedValue(mockInventorySummary); const response = await request(app) .get('/api/inventory?category=Electronics') .expect(200); expect(response.body.success).toBe(true); expect(Inventory.getInventorySummary).toHaveBeenCalledWith({ category: 'Electronics' }); }); it('should filter for low stock items', async () => { const mockLowStockSummary = [ { id: 2, product_code: 'P002', description: 'Product 2', current_level: 2, minimum_level: 5, stock_status: 'low' } ]; Inventory.getInventorySummary.mockResolvedValue(mockLowStockSummary); const response = await request(app) .get('/api/inventory?lowStock=true') .expect(200); expect(response.body.success).toBe(true); expect(Inventory.getInventorySummary).toHaveBeenCalledWith({ lowStock: true }); }); it('should handle database errors gracefully', async () => { Inventory.getInventorySummary.mockRejectedValue(new Error('Database connection failed')); const response = await request(app) .get('/api/inventory') .expect(500); expect(response.body.success).toBe(false); expect(response.body.error).toBe('Failed to retrieve inventory summary'); }); }); describe('GET /api/inventory/low-stock', () => { it('should return low stock items', async () => { const mockLowStockItems = [ { id: 1, product_code: 'P001', description: 'Low Stock Product', current_level: 2, minimum_level: 10 } ]; Inventory.getLowStockItems.mockResolvedValue(mockLowStockItems); const response = await request(app) .get('/api/inventory/low-stock') .expect(200); expect(response.body.success).toBe(true); expect(response.body.data).toHaveLength(1); expect(response.body.count).toBe(1); }); it('should handle errors when retrieving low stock items', async () => { Inventory.getLowStockItems.mockRejectedValue(new Error('Database error')); const response = await request(app) .get('/api/inventory/low-stock') .expect(500); expect(response.body.success).toBe(false); expect(response.body.error).toBe('Failed to retrieve low stock items'); }); }); describe('GET /api/inventory/product/:productId', () => { it('should return inventory details for a specific product', async () => { const mockInventory = { id: 1, product_id: 1, current_level: 15, minimum_level: 5, maximum_level: 50, toJSON: jest.fn().mockReturnValue({ id: 1, product_id: 1, current_level: 15, minimum_level: 5, maximum_level: 50 }) }; Inventory.getByProductId.mockResolvedValue(mockInventory); const response = await request(app) .get('/api/inventory/product/1') .expect(200); expect(response.body.success).toBe(true); expect(response.body.data.product_id).toBe(1); expect(response.body.data.current_level).toBe(15); }); it('should return 404 for non-existent inventory', async () => { Inventory.getByProductId.mockResolvedValue(null); const response = await request(app) .get('/api/inventory/product/999') .expect(404); expect(response.body.success).toBe(false); expect(response.body.error).toBe('Inventory not found'); }); it('should return 400 for invalid product ID', async () => { const response = await request(app) .get('/api/inventory/product/invalid') .expect(400); expect(response.body.success).toBe(false); expect(response.body.error).toBe('Invalid product ID'); }); }); describe('GET /api/inventory/product/:productId/level', () => { it('should return current inventory level', async () => { Inventory.getCurrentLevel.mockResolvedValue(25); const response = await request(app) .get('/api/inventory/product/1/level') .expect(200); expect(response.body.success).toBe(true); expect(response.body.data.product_id).toBe(1); expect(response.body.data.current_level).toBe(25); }); it('should return 400 for invalid product ID', async () => { const response = await request(app) .get('/api/inventory/product/invalid/level') .expect(400); expect(response.body.success).toBe(false); expect(response.body.error).toBe('Invalid product ID'); }); }); describe('GET /api/inventory/product/:productId/history', () => { it('should return inventory history with default pagination', async () => { const mockHistory = [ { id: 1, product_id: 1, old_level: 10, new_level: 15, change_reason: 'Stock received', updated_at: '2023-01-01T10:00:00Z' } ]; Inventory.getInventoryHistory.mockResolvedValue(mockHistory); const response = await request(app) .get('/api/inventory/product/1/history') .expect(200); expect(response.body.success).toBe(true); expect(response.body.data).toHaveLength(1); expect(response.body.pagination.limit).toBe(50); expect(response.body.pagination.offset).toBe(0); expect(Inventory.getInventoryHistory).toHaveBeenCalledWith(1, { limit: 50, offset: 0, startDate: undefined, endDate: undefined }); }); it('should return inventory history with custom pagination', async () => { const mockHistory = []; Inventory.getInventoryHistory.mockResolvedValue(mockHistory); const response = await request(app) .get('/api/inventory/product/1/history?limit=10&offset=20') .expect(200); expect(response.body.success).toBe(true); expect(response.body.pagination.limit).toBe(10); expect(response.body.pagination.offset).toBe(20); expect(Inventory.getInventoryHistory).toHaveBeenCalledWith(1, { limit: 10, offset: 20, startDate: undefined, endDate: undefined }); }); it('should return 400 for invalid limit', async () => { const response = await request(app) .get('/api/inventory/product/1/history?limit=2000') .expect(400); expect(response.body.success).toBe(false); expect(response.body.error).toBe('Invalid limit'); }); it('should return 400 for negative offset', async () => { const response = await request(app) .get('/api/inventory/product/1/history?offset=-1') .expect(400); expect(response.body.success).toBe(false); expect(response.body.error).toBe('Invalid offset'); }); }); describe('PUT /api/inventory/product/:productId/level', () => { it('should update inventory level successfully', async () => { const mockProduct = { id: 1, name: 'Test Product' }; const mockUpdatedInventory = { id: 1, product_id: 1, current_level: 20, toJSON: jest.fn().mockReturnValue({ id: 1, product_id: 1, current_level: 20 }) }; Product.findById.mockResolvedValue(mockProduct); Inventory.updateInventoryLevel.mockResolvedValue(mockUpdatedInventory); const response = await request(app) .put('/api/inventory/product/1/level') .send({ newLevel: 20, changeReason: 'Stock adjustment', updatedBy: 'test-user' }) .expect(200); expect(response.body.success).toBe(true); expect(response.body.data.current_level).toBe(20); expect(response.body.message).toBe('Inventory level updated successfully'); expect(Inventory.updateInventoryLevel).toHaveBeenCalledWith( 1, 20, 'Stock adjustment', 'test-user' ); }); it('should return 400 for invalid new level', async () => { const response = await request(app) .put('/api/inventory/product/1/level') .send({ newLevel: 'invalid' }) .expect(400); expect(response.body.success).toBe(false); expect(response.body.error).toBe('Invalid new level'); }); it('should return 400 for negative new level', async () => { const response = await request(app) .put('/api/inventory/product/1/level') .send({ newLevel: -5 }) .expect(400); expect(response.body.success).toBe(false); expect(response.body.error).toBe('Invalid new level'); }); it('should return 404 for non-existent product', async () => { Product.findById.mockResolvedValue(null); const response = await request(app) .put('/api/inventory/product/999/level') .send({ newLevel: 10 }) .expect(404); expect(response.body.success).toBe(false); expect(response.body.error).toBe('Product not found'); }); it('should return 409 for concurrent update conflict', async () => { const mockProduct = { id: 1, name: 'Test Product' }; Product.findById.mockResolvedValue(mockProduct); Inventory.updateInventoryLevel.mockRejectedValue( new Error('Concurrent update detected. Please refresh and try again.') ); const response = await request(app) .put('/api/inventory/product/1/level') .send({ newLevel: 10 }) .expect(409); expect(response.body.success).toBe(false); expect(response.body.error).toBe('Concurrent update conflict'); }); }); describe('PUT /api/inventory/product/:productId', () => { it('should update inventory settings successfully', async () => { const mockProduct = { id: 1, name: 'Test Product' }; const mockInventory = { id: 1, product_id: 1, current_level: 15, minimum_level: 5, maximum_level: 50, version: 1, validate: jest.fn().mockReturnValue({ isValid: true, errors: [] }), save: jest.fn().mockResolvedValue(true), toJSON: jest.fn().mockReturnValue({ id: 1, product_id: 1, current_level: 15, minimum_level: 10, maximum_level: 100 }) }; Product.findById.mockResolvedValue(mockProduct); Inventory.getByProductId.mockResolvedValue(mockInventory); const response = await request(app) .put('/api/inventory/product/1') .send({ minimum_level: 10, maximum_level: 100, updatedBy: 'test-user' }) .expect(200); expect(response.body.success).toBe(true); expect(response.body.message).toBe('Inventory settings updated successfully'); expect(mockInventory.minimum_level).toBe(10); expect(mockInventory.maximum_level).toBe(100); expect(mockInventory.save).toHaveBeenCalled(); }); it('should return 404 for non-existent inventory', async () => { const mockProduct = { id: 1, name: 'Test Product' }; Product.findById.mockResolvedValue(mockProduct); Inventory.getByProductId.mockResolvedValue(null); const response = await request(app) .put('/api/inventory/product/1') .send({ minimum_level: 10 }) .expect(404); expect(response.body.success).toBe(false); expect(response.body.error).toBe('Inventory not found'); }); it('should return 400 for invalid minimum level', async () => { const mockProduct = { id: 1, name: 'Test Product' }; const mockInventory = { id: 1, product_id: 1 }; Product.findById.mockResolvedValue(mockProduct); Inventory.getByProductId.mockResolvedValue(mockInventory); const response = await request(app) .put('/api/inventory/product/1') .send({ minimum_level: -5 }) .expect(400); expect(response.body.success).toBe(false); expect(response.body.error).toBe('Invalid minimum level'); }); }); describe('POST /api/inventory/product/:productId', () => { it('should create inventory record successfully', async () => { const mockProduct = { id: 1, name: 'Test Product' }; const mockInventory = { id: 1, product_id: 1, current_level: 10, toJSON: jest.fn().mockReturnValue({ id: 1, product_id: 1, current_level: 10 }) }; Product.findById.mockResolvedValue(mockProduct); Inventory.getByProductId.mockResolvedValue(null); // No existing inventory Inventory.createForProduct.mockResolvedValue(mockInventory); const response = await request(app) .post('/api/inventory/product/1') .send({ initialLevel: 10, minimumLevel: 5, maximumLevel: 50, updatedBy: 'test-user' }) .expect(201); expect(response.body.success).toBe(true); expect(response.body.message).toBe('Inventory record created successfully'); expect(Inventory.createForProduct).toHaveBeenCalledWith(1, 10, 5, 50, 'test-user'); }); it('should return 409 if inventory already exists', async () => { const mockProduct = { id: 1, name: 'Test Product' }; const mockExistingInventory = { id: 1, product_id: 1 }; Product.findById.mockResolvedValue(mockProduct); Inventory.getByProductId.mockResolvedValue(mockExistingInventory); const response = await request(app) .post('/api/inventory/product/1') .send({ initialLevel: 10 }) .expect(409); expect(response.body.success).toBe(false); expect(response.body.error).toBe('Inventory already exists'); }); it('should return 400 for invalid initial level', async () => { const mockProduct = { id: 1, name: 'Test Product' }; Product.findById.mockResolvedValue(mockProduct); Inventory.getByProductId.mockResolvedValue(null); const response = await request(app) .post('/api/inventory/product/1') .send({ initialLevel: -5 }) .expect(400); expect(response.body.success).toBe(false); expect(response.body.error).toBe('Invalid initial level'); }); }); describe('POST /api/inventory/bulk-update', () => { it('should bulk update inventory levels successfully', async () => { const updates = [ { productId: 1, newLevel: 20, changeReason: 'Restock', updatedBy: 'user1' }, { productId: 2, newLevel: 15, changeReason: 'Adjustment', updatedBy: 'user1' } ]; const mockUpdatedInventories = [ { id: 1, product_id: 1, current_level: 20, toJSON: jest.fn().mockReturnValue({ id: 1, product_id: 1, current_level: 20 }) }, { id: 2, product_id: 2, current_level: 15, toJSON: jest.fn().mockReturnValue({ id: 2, product_id: 2, current_level: 15 }) } ]; Inventory.bulkUpdateInventory.mockResolvedValue(mockUpdatedInventories); const response = await request(app) .post('/api/inventory/bulk-update') .send({ updates }) .expect(200); expect(response.body.success).toBe(true); expect(response.body.count).toBe(2); expect(response.body.message).toBe('Successfully updated 2 inventory records'); expect(Inventory.bulkUpdateInventory).toHaveBeenCalledWith(updates); }); it('should return 400 for empty updates array', async () => { const response = await request(app) .post('/api/inventory/bulk-update') .send({ updates: [] }) .expect(400); expect(response.body.success).toBe(false); expect(response.body.error).toBe('Invalid input'); }); it('should return 400 for invalid update data', async () => { const updates = [ { productId: 'invalid', newLevel: 20 } ]; const response = await request(app) .post('/api/inventory/bulk-update') .send({ updates }) .expect(400); expect(response.body.success).toBe(false); expect(response.body.error).toBe('Invalid update data'); }); it('should return 409 for concurrent update conflict', async () => { const updates = [{ productId: 1, newLevel: 20 }]; Inventory.bulkUpdateInventory.mockRejectedValue( new Error('Concurrent update detected for product 1. Please refresh and try again.') ); const response = await request(app) .post('/api/inventory/bulk-update') .send({ updates }) .expect(409); expect(response.body.success).toBe(false); expect(response.body.error).toBe('Concurrent update conflict'); }); }); describe('POST /api/inventory/adjust/:productId', () => { it('should adjust inventory level positively', async () => { const mockProduct = { id: 1, name: 'Test Product' }; const mockUpdatedInventory = { id: 1, product_id: 1, current_level: 25, toJSON: jest.fn().mockReturnValue({ id: 1, product_id: 1, current_level: 25 }) }; Product.findById.mockResolvedValue(mockProduct); Inventory.getCurrentLevel.mockResolvedValue(20); Inventory.updateInventoryLevel.mockResolvedValue(mockUpdatedInventory); const response = await request(app) .post('/api/inventory/adjust/1') .send({ adjustment: 5, changeReason: 'Stock received', updatedBy: 'test-user' }) .expect(200); expect(response.body.success).toBe(true); expect(response.body.data.current_level).toBe(25); expect(response.body.data.adjustment).toBe(5); expect(response.body.data.previous_level).toBe(20); expect(Inventory.updateInventoryLevel).toHaveBeenCalledWith( 1, 25, 'Stock received', 'test-user' ); }); it('should adjust inventory level negatively', async () => { const mockProduct = { id: 1, name: 'Test Product' }; const mockUpdatedInventory = { id: 1, product_id: 1, current_level: 15, toJSON: jest.fn().mockReturnValue({ id: 1, product_id: 1, current_level: 15 }) }; Product.findById.mockResolvedValue(mockProduct); Inventory.getCurrentLevel.mockResolvedValue(20); Inventory.updateInventoryLevel.mockResolvedValue(mockUpdatedInventory); const response = await request(app) .post('/api/inventory/adjust/1') .send({ adjustment: -5 }) .expect(200); expect(response.body.success).toBe(true); expect(response.body.data.current_level).toBe(15); expect(response.body.data.adjustment).toBe(-5); expect(Inventory.updateInventoryLevel).toHaveBeenCalledWith( 1, 15, 'Inventory adjustment: -5', 'api-user' ); }); it('should return 400 for adjustment that would cause negative inventory', async () => { const mockProduct = { id: 1, name: 'Test Product' }; Product.findById.mockResolvedValue(mockProduct); Inventory.getCurrentLevel.mockResolvedValue(5); const response = await request(app) .post('/api/inventory/adjust/1') .send({ adjustment: -10 }) .expect(400); expect(response.body.success).toBe(false); expect(response.body.error).toBe('Invalid adjustment'); expect(response.body.message).toContain('would result in negative inventory'); }); it('should return 400 for invalid adjustment type', async () => { const response = await request(app) .post('/api/inventory/adjust/1') .send({ adjustment: 'invalid' }) .expect(400); expect(response.body.success).toBe(false); expect(response.body.error).toBe('Invalid adjustment'); }); }); describe('Error Handling', () => { it('should handle database connection errors', async () => { Inventory.getInventorySummary.mockRejectedValue(new Error('Database connection failed')); const response = await request(app) .get('/api/inventory') .expect(500); expect(response.body.success).toBe(false); expect(response.body.error).toBe('Failed to retrieve inventory summary'); }); it('should handle inventory not found errors', async () => { Inventory.updateInventoryLevel.mockRejectedValue( new Error('Inventory record for product ID 999 not found') ); const mockProduct = { id: 999, name: 'Test Product' }; Product.findById.mockResolvedValue(mockProduct); const response = await request(app) .put('/api/inventory/product/999/level') .send({ newLevel: 10 }) .expect(404); expect(response.body.success).toBe(false); expect(response.body.error).toBe('Inventory not found'); }); }); });