Files
inventory-barcode-system/__tests__/products.routes.test.js

589 lines
18 KiB
JavaScript

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