Compare commits
2 Commits
2ecd8db79a
...
9349391834
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9349391834 | ||
|
|
b4d270faf6 |
61
src/main/dao/materials-to-delete.dao.ts
Normal file
61
src/main/dao/materials-to-delete.dao.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import { DatabaseService } from '../services/database.service';
|
||||
import { LoggerService } from '../services/logger.service';
|
||||
|
||||
export class MaterialsToDeleteDAO {
|
||||
constructor(private dbService: DatabaseService) {}
|
||||
|
||||
/**
|
||||
* Query materials_to_delete table and return distinct material names
|
||||
* @param managerNames - Optional array of manager names to filter by. Pass null to get all materials.
|
||||
* @returns Promise<string[]> - Array of distinct material names
|
||||
*/
|
||||
async getMaterialsToDeleteByManagers(
|
||||
managerNames: string[] | null
|
||||
): Promise<string[]> {
|
||||
try {
|
||||
// Input validation
|
||||
if (managerNames && managerNames.length === 0) {
|
||||
LoggerService.warn('Empty manager names array provided, returning empty result');
|
||||
return [];
|
||||
}
|
||||
|
||||
LoggerService.info(
|
||||
`Querying materials_to_delete${managerNames ? ` for ${managerNames.length} managers` : ' (all managers)'}`
|
||||
);
|
||||
|
||||
// Build base query
|
||||
let query = `
|
||||
SELECT DISTINCT material_name
|
||||
FROM materials_to_delete
|
||||
`;
|
||||
|
||||
let params: string[] | undefined;
|
||||
|
||||
// Add WHERE clause with parameterized query if manager names provided
|
||||
// SECURITY: Use parameterized queries to prevent SQL injection
|
||||
if (managerNames && managerNames.length > 0) {
|
||||
const placeholders = managerNames.map((_, i) => `@param${i}`).join(',');
|
||||
query += ` WHERE manager_name IN (${placeholders})`;
|
||||
params = managerNames;
|
||||
}
|
||||
|
||||
// Execute query with parameters (if any)
|
||||
const result = await this.dbService.executeSQLServerQuery(query, params);
|
||||
const materials = result.rows.map((row: any) => row.material_name);
|
||||
|
||||
LoggerService.info(`Found ${materials.length} materials to delete`);
|
||||
return materials;
|
||||
} catch (error) {
|
||||
LoggerService.error('Failed to query materials_to_delete table', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all materials from materials_to_delete table without filtering
|
||||
* @returns Promise<string[]> - Array of all distinct material names
|
||||
*/
|
||||
async getAllMaterialsToDelete(): Promise<string[]> {
|
||||
return this.getMaterialsToDeleteByManagers(null);
|
||||
}
|
||||
}
|
||||
@@ -8,14 +8,21 @@ export class ProductionOrderDAO {
|
||||
try {
|
||||
LoggerService.info(`Querying production orders for ${productionIds.length} IDs`);
|
||||
|
||||
const idsString = productionIds.map((id) => `'${id}'`).join(',');
|
||||
// Validate input
|
||||
if (productionIds.length === 0) {
|
||||
LoggerService.warn('No production IDs provided, returning empty array');
|
||||
return [];
|
||||
}
|
||||
|
||||
// Use parameterized query to prevent SQL injection
|
||||
const placeholders = productionIds.map((_, i) => `@param${i}`).join(',');
|
||||
const query = `
|
||||
SELECT DISTINCT production_order_no
|
||||
FROM production_orders
|
||||
WHERE production_id IN (${idsString})
|
||||
WHERE production_id IN (${placeholders})
|
||||
`;
|
||||
|
||||
const result = await this.dbService.executeSQLServerQuery(query);
|
||||
const result = await this.dbService.executeSQLServerQuery(query, productionIds);
|
||||
const orderNumbers = result.rows.map((row: any) => row.production_order_no);
|
||||
|
||||
LoggerService.info(`Found ${orderNumbers.length} production orders`);
|
||||
@@ -29,12 +36,24 @@ export class ProductionOrderDAO {
|
||||
async readProductionIds(filePath: string): Promise<string[]> {
|
||||
try {
|
||||
const fs = await import('fs/promises');
|
||||
|
||||
// Validate file existence
|
||||
try {
|
||||
await fs.access(filePath, fs.constants.R_OK);
|
||||
} catch {
|
||||
throw new Error(`Production IDs file not found: ${filePath}`);
|
||||
}
|
||||
|
||||
const content = await fs.readFile(filePath, 'utf-8');
|
||||
const ids = content
|
||||
.split('\n')
|
||||
.map((line) => line.trim())
|
||||
.filter((line) => line.length > 0);
|
||||
|
||||
if (ids.length === 0) {
|
||||
LoggerService.warn('Production IDs file is empty');
|
||||
}
|
||||
|
||||
LoggerService.info(`Read ${ids.length} production IDs from file`);
|
||||
return ids;
|
||||
} catch (error) {
|
||||
|
||||
232
tests/unit/dao/materials-to-delete.dao.test.ts
Normal file
232
tests/unit/dao/materials-to-delete.dao.test.ts
Normal file
@@ -0,0 +1,232 @@
|
||||
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_name: 'MAT001' },
|
||||
{ material_name: 'MAT002' },
|
||||
{ material_name: '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_name');
|
||||
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_name: 'MAT001' },
|
||||
{ material_name: '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_name: '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_name: '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_name: 'MAT001' },
|
||||
{ material_name: 'MAT002' },
|
||||
{ material_name: '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([]);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user