Initial commit: Inventory Barcode System
This commit is contained in:
589
__tests__/products.routes.test.js
Normal file
589
__tests__/products.routes.test.js
Normal file
@ -0,0 +1,589 @@
|
||||
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');
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user