589 lines
18 KiB
JavaScript
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');
|
|
});
|
|
});
|
|
}); |