Files
BIPMaterialManager/tests/unit/services/erp/extractor.test.ts
Misaka d0f8ad0fef test(erp): add unit tests for ERP services and test coverage docs
Add unit tests for core ERP service modules including ErpBrowserManager,
cleaner, erp-auth, extractor-core, extractor, and order-resolver. Also
includes test coverage improvement plan and quality review report.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-05 17:23:00 +08:00

351 lines
12 KiB
TypeScript

import { describe, it, expect, beforeEach, vi } from 'vitest'
import { ExtractorService } from '../../../../src/main/services/erp/extractor'
import { ErpAuthService } from '../../../../src/main/services/erp/erp-auth'
import fs from 'fs/promises'
import type { ExtractorInput, ImportResult } from '../../../../src/main/types/extractor.types'
import type { ExcelParser } from '../../../../src/main/services/excel/excel-parser'
import type { DataImportService } from '../../../../src/main/services/database/data-importer'
// Mock external dependencies
vi.mock('fs/promises', () => ({
default: {
mkdir: vi.fn().mockResolvedValue(undefined),
unlink: vi.fn().mockResolvedValue(undefined)
},
mkdir: vi.fn().mockResolvedValue(undefined),
unlink: vi.fn().mockResolvedValue(undefined)
}))
vi.mock('../../../../src/main/services/logger', () => {
const mockLogger = {
info: vi.fn(),
debug: vi.fn(),
warn: vi.fn(),
error: vi.fn()
}
return {
createLogger: vi.fn(() => mockLogger),
withRequestContext: vi.fn(async (fn) => fn()),
getRequestId: vi.fn(() => 'test-request-id')
}
})
vi.mock('../../../../src/main/services/logger/performance-monitor', () => ({
trackDuration: vi.fn(async (fn) => ({ result: await fn() }))
}))
// Mock ExcelParser - reset in beforeEach
let mockExcelParserInstance: any
vi.mock('../../../../src/main/services/excel/excel-parser', () => ({
ExcelParser: function ExcelParser() {
return mockExcelParserInstance
}
}))
// Mock DataImportService - reset in beforeEach
let mockDataImportInstance: any
vi.mock('../../../../src/main/services/database/data-importer', () => ({
DataImportService: function DataImportService() {
return mockDataImportInstance
}
}))
// Mock ExtractorCore - reset in beforeEach
let mockExtractorCoreInstance: any
vi.mock('../../../../src/main/services/erp/extractor-core', () => ({
ExtractorCore: function ExtractorCore() {
return mockExtractorCoreInstance
}
}))
describe('ExtractorService', () => {
let mockAuthService: ErpAuthService
let mockSession: any
beforeEach(() => {
vi.clearAllMocks()
mockSession = { cookie: 'test-cookie' }
mockAuthService = {
getSession: vi.fn(() => mockSession)
} as unknown as ErpAuthService
// Initialize mock instances
mockExcelParserInstance = {
parse: vi.fn().mockResolvedValue(undefined),
_lastOrders: [] as Array<{ orderInfo: any; materials: any[] }>,
get lastOrders() {
return this._lastOrders
},
set lastOrders(val) {
this._lastOrders = val
}
}
// Explicitly reset lastOrders
mockExcelParserInstance.lastOrders = []
mockDataImportInstance = {
importFromExcel: vi.fn().mockResolvedValue({
success: true,
recordsRead: 0,
recordsDeleted: 0,
recordsImported: 0,
uniqueSourceNumbers: 0,
errors: []
} as ImportResult)
}
const mockDownloadAllBatches = vi.fn().mockResolvedValue({
downloadedFiles: [],
errors: []
})
mockExtractorCoreInstance = {
downloadAllBatches: mockDownloadAllBatches
}
})
// TODO: Complex extract() flow tests need integration test setup
describe('Constructor', () => {
it('should create instance with default download dir', () => {
const service = new ExtractorService(mockAuthService)
expect(service).toBeInstanceOf(ExtractorService)
})
it('should create instance with custom download dir', () => {
const service = new ExtractorService(mockAuthService, './custom-downloads')
expect(service).toBeInstanceOf(ExtractorService)
})
it('should ensure download directory exists', async () => {
new ExtractorService(mockAuthService, './test-downloads')
expect(fs.mkdir).toHaveBeenCalledWith('./test-downloads', { recursive: true })
})
})
describe('extract() - Basic Behavior', () => {
it('should return result object', async () => {
const service = new ExtractorService(mockAuthService, './test-downloads')
const input: ExtractorInput = { orderNumbers: [] }
const result = await service.extract(input)
expect(result).toBeDefined()
expect(typeof result).toBe('object')
})
it('should handle empty order numbers', async () => {
const service = new ExtractorService(mockAuthService, './test-downloads')
await expect(service.extract({ orderNumbers: [] })).resolves.toBeDefined()
})
it('should capture errors from ExtractorCore', async () => {
mockExtractorCoreInstance.downloadAllBatches.mockResolvedValue({
downloadedFiles: [],
errors: ['Download failed']
})
const service = new ExtractorService(mockAuthService, './test-downloads')
const result = await service.extract({ orderNumbers: ['ORD001'] })
expect(result.downloadedFiles).toEqual([])
expect(result.errors).toContain('Download failed')
})
it('should handle extraction errors gracefully', async () => {
mockExtractorCoreInstance.downloadAllBatches.mockRejectedValue(new Error('Network error'))
const service = new ExtractorService(mockAuthService, './test-downloads')
const result = await service.extract({ orderNumbers: ['ORD001'] })
expect(Array.isArray(result.errors)).toBe(true)
})
})
describe('mergeFiles()', () => {
it('should return null when no files to merge', async () => {
const service = new ExtractorService(mockAuthService)
// @ts-ignore - accessing private method for testing
const result = await service.mergeFiles([], ['ORD001'])
expect(result.mergedFile).toBeNull()
expect(result.recordCount).toBe(0)
expect(result.orderRecordCounts).toEqual([])
})
it('should handle single file', async () => {
mockExcelParserInstance.lastOrders = [
{
orderInfo: { productionOrder: 'ORD001' },
materials: [{ materialCode: 'MAT001', quantity: 10 }]
}
]
const service = new ExtractorService(mockAuthService, './test-downloads')
// @ts-ignore - accessing private method for testing
const result = await service.mergeFiles(['./file1.xlsx'], ['ORD001'])
expect(result.recordCount).toBe(1)
expect(result.orderRecordCounts).toEqual([{ orderNumber: 'ORD001', recordCount: 1 }])
})
it('should handle multiple files', async () => {
// Mock parse to return different data for each file
let callCount = 0
mockExcelParserInstance.parse = vi.fn().mockImplementation(() => {
callCount++
if (callCount === 1) {
mockExcelParserInstance._lastOrders = [
{
orderInfo: { productionOrder: 'ORD001' },
materials: [{ materialCode: 'MAT001', quantity: 5 }]
}
]
} else {
mockExcelParserInstance._lastOrders = [
{
orderInfo: { productionOrder: 'ORD002' },
materials: [
{ materialCode: 'MAT002', quantity: 10 },
{ materialCode: 'MAT003', quantity: 15 }
]
}
]
}
return Promise.resolve()
})
const service = new ExtractorService(mockAuthService, './test-downloads')
// @ts-ignore - accessing private method for testing
const result = await service.mergeFiles(
['./file1.xlsx', './file2.xlsx'],
['ORD001', 'ORD002']
)
expect(result.recordCount).toBe(3)
expect(result.orderRecordCounts).toHaveLength(2)
expect(result.orderRecordCounts[0]).toEqual({ orderNumber: 'ORD001', recordCount: 1 })
expect(result.orderRecordCounts[1]).toEqual({ orderNumber: 'ORD002', recordCount: 2 })
})
})
describe('cleanupTempFiles()', () => {
it('should delete all temporary files', async () => {
const service = new ExtractorService(mockAuthService)
const files = ['./temp1.xlsx', './temp2.xlsx', './temp3.xlsx']
// @ts-ignore - accessing private method for testing
await service.cleanupTempFiles(files, ['ORD001'])
expect(fs.unlink).toHaveBeenCalledTimes(3)
expect(fs.unlink).toHaveBeenCalledWith('./temp1.xlsx')
expect(fs.unlink).toHaveBeenCalledWith('./temp2.xlsx')
expect(fs.unlink).toHaveBeenCalledWith('./temp3.xlsx')
})
it('should handle deletion errors gracefully', async () => {
vi.mocked(fs.unlink).mockRejectedValue(new Error('File not found'))
const service = new ExtractorService(mockAuthService)
const files = ['./temp1.xlsx', './temp2.xlsx']
// @ts-ignore - accessing private method for testing
await expect(service.cleanupTempFiles(files, ['ORD001'])).resolves.not.toThrow()
expect(fs.unlink).toHaveBeenCalledTimes(2)
})
})
describe('importToDatabaseWithLogging()', () => {
it('should return success result', async () => {
mockDataImportInstance.importFromExcel.mockResolvedValue({
success: true,
recordsRead: 100,
recordsDeleted: 50,
recordsImported: 50,
uniqueSourceNumbers: 5,
errors: []
})
const service = new ExtractorService(mockAuthService)
const onLog = vi.fn()
// @ts-ignore - accessing private method for testing
const result = await service.importToDatabaseWithLogging('./merged.xlsx', onLog)
expect(result.success).toBe(true)
expect(result.recordsRead).toBe(100)
expect(result.recordsImported).toBe(50)
expect(onLog).toHaveBeenCalledWith('success', expect.stringContaining('导入完成'))
})
it('should handle import failure', async () => {
mockDataImportInstance.importFromExcel.mockRejectedValue(
new Error('Database connection failed')
)
const service = new ExtractorService(mockAuthService)
const onLog = vi.fn()
// @ts-ignore - accessing private method for testing
const result = await service.importToDatabaseWithLogging('./merged.xlsx', onLog)
expect(result.success).toBe(false)
expect(result.errors.some((e) => e.includes('Database connection failed'))).toBe(true)
expect(onLog).toHaveBeenCalledWith('error', expect.stringContaining('导入失败'))
})
it('should handle import with errors in result', async () => {
mockDataImportInstance.importFromExcel.mockResolvedValue({
success: false,
recordsRead: 50,
recordsDeleted: 0,
recordsImported: 0,
uniqueSourceNumbers: 0,
errors: ['Validation failed', 'Duplicate records']
})
const service = new ExtractorService(mockAuthService)
const onLog = vi.fn()
// @ts-ignore - accessing private method for testing
const result = await service.importToDatabaseWithLogging('./merged.xlsx', onLog)
expect(result.success).toBe(false)
expect(result.errors).toEqual(['Validation failed', 'Duplicate records'])
expect(onLog).toHaveBeenCalledTimes(3)
})
it('should wrap import in trackDuration', async () => {
mockDataImportInstance.importFromExcel.mockResolvedValue({
success: true,
recordsRead: 10,
recordsDeleted: 0,
recordsImported: 10,
uniqueSourceNumbers: 1,
errors: []
})
const { trackDuration } =
await import('../../../../src/main/services/logger/performance-monitor')
const service = new ExtractorService(mockAuthService)
const onLog = vi.fn()
// @ts-ignore - accessing private method for testing
await service.importToDatabaseWithLogging('./merged.xlsx', onLog)
expect(trackDuration).toHaveBeenCalledWith(
expect.any(Function),
expect.objectContaining({ operationName: 'Database Import' })
)
})
})
})