Implement MaterialsToDeleteDAO class with secure parameterized queries following the pattern established in Task 11 (commit 5aafb23). Features: - getMaterialsToDeleteByManagers(): Query materials_to_delete table with optional filtering by manager_names using parameterized queries - getAllMaterialsToDelete(): Wrapper to retrieve all materials without filtering Security: - Uses parameterized queries (@param0, @param1, etc.) to prevent SQL injection - Parameters passed separately from query string via executeSQLServerQuery() - Input validation for empty arrays - Comprehensive test coverage including SQL injection attempt scenarios Testing: - 11 comprehensive unit tests covering all methods and edge cases - Tests verify parameterized query pattern prevents SQL injection - All tests passing (12/12 including existing tests) Files: - src/main/dao/materials-to-delete.dao.ts: DAO implementation - tests/unit/dao/materials-to-delete.dao.test.ts: Unit tests Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
233 lines
7.5 KiB
TypeScript
233 lines
7.5 KiB
TypeScript
import { MaterialsToDeleteDAO } from '../../../src/main/dao/materials-to-delete.dao';
|
|
import { DatabaseService } from '../../../src/main/services/database.service';
|
|
|
|
// Mock LoggerService to avoid Electron app dependency
|
|
jest.mock('../../../src/main/services/logger.service', () => ({
|
|
LoggerService: {
|
|
info: jest.fn(),
|
|
warn: jest.fn(),
|
|
error: jest.fn(),
|
|
debug: jest.fn(),
|
|
},
|
|
}));
|
|
|
|
describe('MaterialsToDeleteDAO', () => {
|
|
let dao: MaterialsToDeleteDAO;
|
|
let mockDbService: jest.Mocked<DatabaseService>;
|
|
|
|
beforeEach(() => {
|
|
// Clear all mocks before each test
|
|
jest.clearAllMocks();
|
|
|
|
// Create a mock DatabaseService
|
|
mockDbService = {
|
|
executeSQLServerQuery: jest.fn(),
|
|
} as any;
|
|
|
|
dao = new MaterialsToDeleteDAO(mockDbService);
|
|
});
|
|
|
|
describe('getMaterialsToDeleteByManagers', () => {
|
|
it('should return all materials when managerNames is null', async () => {
|
|
const mockResult = {
|
|
rows: [
|
|
{ material_code: 'MAT001' },
|
|
{ material_code: 'MAT002' },
|
|
{ material_code: 'MAT003' },
|
|
],
|
|
rowCount: 3,
|
|
};
|
|
|
|
mockDbService.executeSQLServerQuery.mockResolvedValue(mockResult);
|
|
|
|
const result = await dao.getMaterialsToDeleteByManagers(null);
|
|
|
|
expect(result).toEqual(['MAT001', 'MAT002', 'MAT003']);
|
|
expect(mockDbService.executeSQLServerQuery).toHaveBeenCalledTimes(1);
|
|
|
|
// Verify the query does not contain WHERE clause
|
|
const queryCall = mockDbService.executeSQLServerQuery.mock.calls[0];
|
|
const query = queryCall[0];
|
|
expect(query).not.toContain('WHERE');
|
|
expect(query).toContain('SELECT DISTINCT material_code');
|
|
expect(query).toContain('FROM materials_to_delete');
|
|
});
|
|
|
|
it('should return materials filtered by manager names using parameterized query', async () => {
|
|
const managerNames = ['Manager1', 'Manager2'];
|
|
const mockResult = {
|
|
rows: [
|
|
{ material_code: 'MAT001' },
|
|
{ material_code: 'MAT002' },
|
|
],
|
|
rowCount: 2,
|
|
};
|
|
|
|
mockDbService.executeSQLServerQuery.mockResolvedValue(mockResult);
|
|
|
|
const result = await dao.getMaterialsToDeleteByManagers(managerNames);
|
|
|
|
expect(result).toEqual(['MAT001', 'MAT002']);
|
|
expect(mockDbService.executeSQLServerQuery).toHaveBeenCalledTimes(1);
|
|
|
|
// Verify parameterized query is used (SQL injection protection)
|
|
const queryCall = mockDbService.executeSQLServerQuery.mock.calls[0];
|
|
const query = queryCall[0];
|
|
const params = queryCall[1];
|
|
|
|
expect(query).toContain('WHERE manager_name IN');
|
|
expect(query).toContain('@param0');
|
|
expect(query).toContain('@param1');
|
|
|
|
// Verify parameters are passed separately (not concatenated in query)
|
|
expect(params).toEqual(managerNames);
|
|
|
|
// Ensure no string concatenation of values in query
|
|
expect(query).not.toContain("'Manager1'");
|
|
expect(query).not.toContain("'Manager2'");
|
|
});
|
|
|
|
it('should return empty array when managerNames is empty', async () => {
|
|
const result = await dao.getMaterialsToDeleteByManagers([]);
|
|
|
|
expect(result).toEqual([]);
|
|
expect(mockDbService.executeSQLServerQuery).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should handle single manager name', async () => {
|
|
const managerNames = ['Manager1'];
|
|
const mockResult = {
|
|
rows: [{ material_code: 'MAT001' }],
|
|
rowCount: 1,
|
|
};
|
|
|
|
mockDbService.executeSQLServerQuery.mockResolvedValue(mockResult);
|
|
|
|
const result = await dao.getMaterialsToDeleteByManagers(managerNames);
|
|
|
|
expect(result).toEqual(['MAT001']);
|
|
expect(mockDbService.executeSQLServerQuery).toHaveBeenCalledTimes(1);
|
|
|
|
const queryCall = mockDbService.executeSQLServerQuery.mock.calls[0];
|
|
expect(queryCall[0]).toContain('@param0');
|
|
expect(queryCall[1]).toEqual(['Manager1']);
|
|
});
|
|
|
|
it('should return empty array when no materials found', async () => {
|
|
const mockResult = {
|
|
rows: [],
|
|
rowCount: 0,
|
|
};
|
|
|
|
mockDbService.executeSQLServerQuery.mockResolvedValue(mockResult);
|
|
|
|
const result = await dao.getMaterialsToDeleteByManagers(['Manager1']);
|
|
|
|
expect(result).toEqual([]);
|
|
});
|
|
|
|
it('should handle SQL injection attempts via parameterized query', async () => {
|
|
const maliciousInput = [
|
|
"Manager1'; DROP TABLE materials_to_delete; --",
|
|
"Manager2' OR '1'='1",
|
|
];
|
|
|
|
const mockResult = {
|
|
rows: [],
|
|
rowCount: 0,
|
|
};
|
|
|
|
mockDbService.executeSQLServerQuery.mockResolvedValue(mockResult);
|
|
|
|
await dao.getMaterialsToDeleteByManagers(maliciousInput);
|
|
|
|
// Verify parameters are passed as values, not concatenated
|
|
const queryCall = mockDbService.executeSQLServerQuery.mock.calls[0];
|
|
const query = queryCall[0];
|
|
const params = queryCall[1];
|
|
|
|
// The malicious strings should be in params, not in query
|
|
expect(params).toEqual(maliciousInput);
|
|
|
|
// Query should only contain placeholders, not actual values
|
|
expect(query).not.toContain('DROP TABLE');
|
|
expect(query).not.toContain('OR 1=1');
|
|
expect(query).toMatch(/@param\d+/);
|
|
});
|
|
|
|
it('should throw error when database query fails', async () => {
|
|
const dbError = new Error('Database connection failed');
|
|
mockDbService.executeSQLServerQuery.mockRejectedValue(dbError);
|
|
|
|
await expect(
|
|
dao.getMaterialsToDeleteByManagers(['Manager1'])
|
|
).rejects.toThrow('Database connection failed');
|
|
});
|
|
|
|
it('should handle large number of manager names', async () => {
|
|
const managerNames = Array.from({ length: 100 }, (_, i) => `Manager${i}`);
|
|
const mockResult = {
|
|
rows: [{ material_code: 'MAT001' }],
|
|
rowCount: 1,
|
|
};
|
|
|
|
mockDbService.executeSQLServerQuery.mockResolvedValue(mockResult);
|
|
|
|
const result = await dao.getMaterialsToDeleteByManagers(managerNames);
|
|
|
|
expect(result).toEqual(['MAT001']);
|
|
|
|
// Verify all parameters are passed
|
|
const queryCall = mockDbService.executeSQLServerQuery.mock.calls[0];
|
|
expect(queryCall[1]).toEqual(managerNames);
|
|
expect(queryCall[1]?.length).toBe(100);
|
|
});
|
|
});
|
|
|
|
describe('getAllMaterialsToDelete', () => {
|
|
it('should return all materials by calling getMaterialsToDeleteByManagers with null', async () => {
|
|
const mockResult = {
|
|
rows: [
|
|
{ material_code: 'MAT001' },
|
|
{ material_code: 'MAT002' },
|
|
{ material_code: 'MAT003' },
|
|
],
|
|
rowCount: 3,
|
|
};
|
|
|
|
mockDbService.executeSQLServerQuery.mockResolvedValue(mockResult);
|
|
|
|
const result = await dao.getAllMaterialsToDelete();
|
|
|
|
expect(result).toEqual(['MAT001', 'MAT002', 'MAT003']);
|
|
expect(mockDbService.executeSQLServerQuery).toHaveBeenCalledTimes(1);
|
|
|
|
// Verify query without filter
|
|
const queryCall = mockDbService.executeSQLServerQuery.mock.calls[0];
|
|
expect(queryCall[0]).not.toContain('WHERE');
|
|
});
|
|
|
|
it('should propagate errors from getMaterialsToDeleteByManagers', async () => {
|
|
const dbError = new Error('Database error');
|
|
mockDbService.executeSQLServerQuery.mockRejectedValue(dbError);
|
|
|
|
await expect(dao.getAllMaterialsToDelete()).rejects.toThrow(
|
|
'Database error'
|
|
);
|
|
});
|
|
|
|
it('should return empty array when no materials exist', async () => {
|
|
const mockResult = {
|
|
rows: [],
|
|
rowCount: 0,
|
|
};
|
|
|
|
mockDbService.executeSQLServerQuery.mockResolvedValue(mockResult);
|
|
|
|
const result = await dao.getAllMaterialsToDelete();
|
|
|
|
expect(result).toEqual([]);
|
|
});
|
|
});
|
|
});
|