const request = require('supertest'); const fs = require('fs'); const path = require('path'); const app = require('../server'); const Product = require('../models/Product'); const database = require('../models/database'); // Mock the database to avoid actual database operations during tests jest.mock('../models/database'); describe('Product 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); }); describe('GET /api/products', () => { it('should return all products successfully', async () => { const mockProducts = [ { id: 1, name: 'Product 1', description: 'Test product 1', quantity: 10 }, { id: 2, name: 'Product 2', description: 'Test product 2', quantity: 5 } ]; const mockStmt = { all: jest.fn().mockReturnValue(mockProducts) }; mockDb.prepare.mockReturnValue(mockStmt); const response = await request(app) .get('/api/products') .expect(200); expect(response.body.success).toBe(true); expect(response.body.data).toHaveLength(2); expect(response.body.count).toBe(2); }); it('should filter products by category', async () => { const mockProducts = [ { id: 1, name: 'Product 1', category: 'Electronics', quantity: 10 } ]; const mockStmt = { all: jest.fn().mockReturnValue(mockProducts) }; mockDb.prepare.mockReturnValue(mockStmt); const response = await request(app) .get('/api/products?category=Electronics') .expect(200); expect(response.body.success).toBe(true); expect(mockStmt.all).toHaveBeenCalledWith('Electronics'); }); it('should handle database errors gracefully', async () => { mockDb.prepare.mockImplementation(() => { throw new Error('Database connection failed'); }); const response = await request(app) .get('/api/products') .expect(500); expect(response.body.success).toBe(false); expect(response.body.error).toBe('Failed to retrieve products'); }); }); describe('GET /api/products/:id', () => { it('should return a specific product by ID', async () => { const mockProduct = { id: 1, name: 'Test Product', description: 'Test description', quantity: 10 }; const mockStmt = { get: jest.fn().mockReturnValue(mockProduct) }; mockDb.prepare.mockReturnValue(mockStmt); const response = await request(app) .get('/api/products/1') .expect(200); expect(response.body.success).toBe(true); expect(response.body.data.id).toBe(1); expect(response.body.data.name).toBe('Test Product'); }); it('should return 404 for non-existent product', async () => { const mockStmt = { get: jest.fn().mockReturnValue(null) }; mockDb.prepare.mockReturnValue(mockStmt); const response = await request(app) .get('/api/products/999') .expect(404); expect(response.body.success).toBe(false); expect(response.body.error).toBe('Product not found'); }); it('should return 400 for invalid product ID', async () => { const response = await request(app) .get('/api/products/invalid') .expect(400); expect(response.body.success).toBe(false); expect(response.body.error).toBe('Invalid product ID'); }); }); describe('GET /api/products/barcode/:barcode', () => { it('should return product by barcode', async () => { const mockProduct = { id: 1, name: 'Test Product', barcode: '123456789', quantity: 10 }; const mockStmt = { get: jest.fn().mockReturnValue(mockProduct) }; mockDb.prepare.mockReturnValue(mockStmt); const response = await request(app) .get('/api/products/barcode/123456789') .expect(200); expect(response.body.success).toBe(true); expect(response.body.data.barcode).toBe('123456789'); }); it('should return 404 for non-existent barcode', async () => { const mockStmt = { get: jest.fn().mockReturnValue(null) }; mockDb.prepare.mockReturnValue(mockStmt); const response = await request(app) .get('/api/products/barcode/nonexistent') .expect(404); expect(response.body.success).toBe(false); expect(response.body.error).toBe('Product not found'); }); it('should return 400 for empty barcode', async () => { const response = await request(app) .get('/api/products/barcode/%20') // URL encoded space .expect(400); expect(response.body.success).toBe(false); expect(response.body.error).toBe('Invalid barcode'); }); }); describe('POST /api/products', () => { it('should create a new product successfully', async () => { const newProduct = { name: 'New Product', description: 'New product description', category: 'Electronics', quantity: 15, unit: 'pieces' }; const mockStmt = { run: jest.fn().mockReturnValue({ lastInsertRowid: 1 }) }; mockDb.prepare.mockReturnValue(mockStmt); const response = await request(app) .post('/api/products') .send(newProduct) .expect(201); expect(response.body.success).toBe(true); expect(response.body.data.name).toBe('New Product'); expect(response.body.message).toBe('Product created successfully'); }); it('should return 400 for invalid product data', async () => { const invalidProduct = { // Missing required name field description: 'Product without name', quantity: -5 // Invalid negative quantity }; const response = await request(app) .post('/api/products') .send(invalidProduct) .expect(400); expect(response.body.success).toBe(false); expect(response.body.error).toBe('Validation failed'); expect(response.body.details).toContain('Product name is required'); }); it('should return 409 for duplicate barcode', async () => { const duplicateProduct = { name: 'Duplicate Product', barcode: '123456789', quantity: 10 }; mockDb.prepare.mockImplementation(() => { const error = new Error('A product with this barcode already exists'); error.code = 'SQLITE_CONSTRAINT_UNIQUE'; throw error; }); const response = await request(app) .post('/api/products') .send(duplicateProduct) .expect(409); expect(response.body.success).toBe(false); expect(response.body.error).toBe('Conflict'); }); }); describe('PUT /api/products/:id', () => { it('should update an existing product successfully', async () => { const existingProduct = { id: 1, name: 'Old Product', description: 'Old description', quantity: 5 }; const updatedData = { name: 'Updated Product', description: 'Updated description', quantity: 10 }; // Mock finding existing product const mockGetStmt = { get: jest.fn().mockReturnValue(existingProduct) }; // Mock update statement const mockUpdateStmt = { run: jest.fn().mockReturnValue({ changes: 1 }) }; mockDb.prepare .mockReturnValueOnce(mockGetStmt) // For findById .mockReturnValueOnce(mockUpdateStmt); // For save/update const response = await request(app) .put('/api/products/1') .send(updatedData) .expect(200); expect(response.body.success).toBe(true); expect(response.body.data.name).toBe('Updated Product'); expect(response.body.message).toBe('Product updated successfully'); }); it('should return 404 for non-existent product', async () => { const mockStmt = { get: jest.fn().mockReturnValue(null) }; mockDb.prepare.mockReturnValue(mockStmt); const response = await request(app) .put('/api/products/999') .send({ name: 'Updated Product' }) .expect(404); expect(response.body.success).toBe(false); expect(response.body.error).toBe('Product not found'); }); it('should return 400 for invalid product ID', async () => { const response = await request(app) .put('/api/products/invalid') .send({ name: 'Updated Product' }) .expect(400); expect(response.body.success).toBe(false); expect(response.body.error).toBe('Invalid product ID'); }); }); describe('DELETE /api/products/:id', () => { it('should delete a product successfully', async () => { const mockStmt = { run: jest.fn().mockReturnValue({ changes: 1 }) }; mockDb.prepare.mockReturnValue(mockStmt); const response = await request(app) .delete('/api/products/1') .expect(200); expect(response.body.success).toBe(true); expect(response.body.message).toBe('Product deleted successfully'); }); it('should return 404 for non-existent product', async () => { const mockStmt = { run: jest.fn().mockReturnValue({ changes: 0 }) }; mockDb.prepare.mockReturnValue(mockStmt); const response = await request(app) .delete('/api/products/999') .expect(404); expect(response.body.success).toBe(false); expect(response.body.error).toBe('Product not found'); }); it('should return 400 for invalid product ID', async () => { const response = await request(app) .delete('/api/products/invalid') .expect(400); expect(response.body.success).toBe(false); expect(response.body.error).toBe('Invalid product ID'); }); }); describe('POST /api/products/import/excel', () => { let mockExcelBuffer; beforeEach(() => { // Create a mock Excel file buffer mockExcelBuffer = Buffer.from('mock excel data'); }); it('should import Excel file successfully', async () => { // Mock successful import results const mockResults = { success: true, parseResults: { success: true, data: { products: [ { productCode: 'P001', description: 'Product 1', quantity: 10 } ], totalRows: 1 } }, validationResults: { isValid: true, statistics: { validProducts: 1, invalidProducts: 0 } }, importResults: { success: true, imported: 1, failed: 0 } }; // Mock ExcelImportService const ExcelImportService = require('../services/ExcelImportService'); jest.spyOn(ExcelImportService.prototype, 'processImport') .mockResolvedValue(mockResults); const response = await request(app) .post('/api/products/import/excel') .attach('file', mockExcelBuffer, 'test.xlsx') .expect(200); expect(response.body.success).toBe(true); expect(response.body.message).toBe('Excel file processed successfully'); }); it('should return 400 when no file is uploaded', async () => { const response = await request(app) .post('/api/products/import/excel') .expect(400); expect(response.body.success).toBe(false); expect(response.body.error).toBe('No file uploaded'); }); it('should return 422 for validation errors', async () => { const mockResults = { success: true, parseResults: { success: true }, validationResults: { isValid: false, statistics: { validProducts: 0, invalidProducts: 1 } } }; const ExcelImportService = require('../services/ExcelImportService'); jest.spyOn(ExcelImportService.prototype, 'processImport') .mockResolvedValue(mockResults); const response = await request(app) .post('/api/products/import/excel') .attach('file', mockExcelBuffer, 'test.xlsx') .expect(422); expect(response.body.success).toBe(true); expect(response.body.data.validationResults.isValid).toBe(false); }); }); describe('POST /api/products/import/excel/preview', () => { let mockExcelBuffer; beforeEach(() => { mockExcelBuffer = Buffer.from('mock excel data'); }); it('should preview Excel file without importing', async () => { const mockResults = { success: true, parseResults: { success: true, data: { products: [ { productCode: 'P001', description: 'Product 1', quantity: 10 } ], totalRows: 1 } }, validationResults: { isValid: true, statistics: { validProducts: 1, invalidProducts: 0 }, validatedProducts: [ { productCode: 'P001', description: 'Product 1', quantity: 10 } ] } }; const ExcelImportService = require('../services/ExcelImportService'); jest.spyOn(ExcelImportService.prototype, 'processImport') .mockResolvedValue(mockResults); const response = await request(app) .post('/api/products/import/excel/preview') .attach('file', mockExcelBuffer, 'test.xlsx') .expect(200); expect(response.body.success).toBe(true); expect(response.body.data.preview).toBeDefined(); expect(response.body.data.preview.sampleProducts).toHaveLength(1); }); it('should return 400 when no file is uploaded', async () => { const response = await request(app) .post('/api/products/import/excel/preview') .expect(400); expect(response.body.success).toBe(false); expect(response.body.error).toBe('No file uploaded'); }); }); describe('POST /api/products/bulk', () => { it('should create multiple products successfully', async () => { const bulkProducts = [ { name: 'Product 1', quantity: 10 }, { name: 'Product 2', quantity: 5 } ]; const mockStmt = { run: jest.fn() .mockReturnValueOnce({ lastInsertRowid: 1 }) .mockReturnValueOnce({ lastInsertRowid: 2 }) }; mockDb.prepare.mockReturnValue(mockStmt); const response = await request(app) .post('/api/products/bulk') .send({ products: bulkProducts }) .expect(201); expect(response.body.success).toBe(true); expect(response.body.created).toBe(2); expect(response.body.failed).toBe(0); expect(response.body.createdProducts).toHaveLength(2); }); it('should handle partial failures in bulk creation', async () => { const bulkProducts = [ { name: 'Valid Product', quantity: 10 }, { name: '', quantity: -5 } // Invalid product ]; const mockStmt = { run: jest.fn().mockReturnValue({ lastInsertRowid: 1 }) }; mockDb.prepare.mockReturnValue(mockStmt); const response = await request(app) .post('/api/products/bulk') .send({ products: bulkProducts }) .expect(207); // Partial success expect(response.body.success).toBe(true); expect(response.body.created).toBe(1); expect(response.body.failed).toBe(1); expect(response.body.errors).toHaveLength(1); }); it('should return 400 for invalid input', async () => { const response = await request(app) .post('/api/products/bulk') .send({ products: [] }) .expect(400); expect(response.body.success).toBe(false); expect(response.body.error).toBe('Invalid input'); }); it('should return 400 when all products fail', async () => { const bulkProducts = [ { name: '', quantity: -5 }, // Invalid { name: '', quantity: -10 } // Invalid ]; const response = await request(app) .post('/api/products/bulk') .send({ products: bulkProducts }) .expect(400); expect(response.body.success).toBe(false); expect(response.body.created).toBe(0); expect(response.body.failed).toBe(2); }); }); describe('Error Handling', () => { it('should handle invalid file types', async () => { // Create a buffer that mimics a text file const textBuffer = Buffer.from('This is not an Excel file'); const response = await request(app) .post('/api/products/import/excel') .attach('file', textBuffer, 'test.txt') .expect(400); expect(response.body.success).toBe(false); expect(response.body.error).toBe('Invalid file type'); }); it('should handle Excel import service errors', async () => { const ExcelImportService = require('../services/ExcelImportService'); jest.spyOn(ExcelImportService.prototype, 'processImport') .mockRejectedValue(new Error('Excel processing failed')); const response = await request(app) .post('/api/products/import/excel') .attach('file', Buffer.from('mock excel'), 'test.xlsx') .expect(500); expect(response.body.success).toBe(false); expect(response.body.error).toBe('Import failed'); }); it('should handle database connection errors', async () => { mockDb.prepare.mockImplementation(() => { throw new Error('Database connection failed'); }); const response = await request(app) .get('/api/products') .expect(500); expect(response.body.success).toBe(false); expect(response.body.error).toBe('Failed to retrieve products'); }); }); });