Files
BIPMaterialManager/tests/unit/services/validation/validation-application-service.test.ts
Misaka 188117e5ce refactor(test): replace module-level mutable state with vi.fn() mocks
Replace fragile module-level let variables and SQL string parsing
with vi.hoisted() mock functions that are reset and configured
per-test in beforeEach via mockResolvedValue/mockResolvedValueOnce.

- Remove 8 module-level mutable state variables
- Remove matchQuery() SQL parser
- Use vi.hoisted() for shared mock functions across vi.mock() factories
- Each test explicitly controls mock return values with mockResolvedValueOnce
- Fix getCleanerData error test to use direct mock instead of dynamic import

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-06 09:21:04 +08:00

360 lines
13 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { describe, it, expect, beforeEach, vi } from 'vitest'
import { ValidationApplicationService } from '../../../../src/main/services/validation/validation-application-service'
import type { ValidationRequest } from '../../../../src/main/types/validation.types'
// ─── Mock logger to prevent real winston initialization and console noise ───
vi.mock('../../../../src/main/services/logger', () => ({
createLogger: () => ({
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
debug: vi.fn()
}),
withRequestContext: (_fn: () => Promise<any>) => _fn(),
trackDuration: async <T>(fn: () => Promise<T>) => {
const result = await fn()
return { result, durationMs: 0, isSlow: false }
},
getRequestId: () => undefined
}))
// ─── Hoisted mock functions shared between vi.mock() factories and tests ───
const {
mockQueryAll,
mockQueryBySource,
mockGetMaterialsByManager,
mockGetAllRecords,
mockGetAllMaterialCodes,
mockGetSourceNumbers,
mockReadProductionIds,
mockSharedIdsGet,
mockDbQuery,
mockDbDisconnect,
mockCreateDbService
} = vi.hoisted(() => ({
mockQueryAll: vi.fn(),
mockQueryBySource: vi.fn(),
mockGetMaterialsByManager: vi.fn(),
mockGetAllRecords: vi.fn(),
mockGetAllMaterialCodes: vi.fn(),
mockGetSourceNumbers: vi.fn(),
mockReadProductionIds: vi.fn(),
mockSharedIdsGet: vi.fn(),
mockDbQuery: vi.fn(),
mockDbDisconnect: vi.fn(),
mockCreateDbService: vi.fn()
}))
// ─── DiscreteMaterialPlanDAO mock ───
vi.mock('../../../../src/main/services/database/discrete-material-plan-dao', () => ({
DiscreteMaterialPlanDAO: class {
queryAllDistinctByMaterialCode = mockQueryAll
queryBySourceNumbersDistinct = mockQueryBySource
}
}))
// ─── MaterialsToBeDeletedDAO mock ───
vi.mock('../../../../src/main/services/database/materials-to-be-deleted-dao', () => ({
MaterialsToBeDeletedDAO: class {
getMaterialsByManager = mockGetMaterialsByManager
getAllRecords = mockGetAllRecords
getAllMaterialCodes = mockGetAllMaterialCodes
}
}))
// ─── Production input service mock ───
vi.mock('../../../../src/main/services/validation/production-input-service', () => ({
getSourceNumbersFromInputs: mockGetSourceNumbers,
readProductionIds: mockReadProductionIds
}))
// ─── Shared production IDs store mock ───
vi.mock('../../../../src/main/services/validation/shared-production-ids-store', () => ({
sharedProductionIdsStore: {
get: mockSharedIdsGet
}
}))
// ─── Validation database mock ───
vi.mock('../../../../src/main/services/validation/validation-database', () => ({
createValidationDatabaseService: mockCreateDbService,
getValidationTableName: vi.fn().mockImplementation((name: string) => name)
}))
function createDbService() {
return {
type: 'mysql' as const,
query: mockDbQuery,
connect: vi.fn().mockResolvedValue(undefined),
disconnect: mockDbDisconnect
}
}
describe('ValidationApplicationService', () => {
let service: ValidationApplicationService
beforeEach(() => {
vi.clearAllMocks()
// DiscreteMaterialPlanDAO defaults
mockQueryAll.mockResolvedValue([
{ MaterialName: 'MatA', MaterialCode: 'M1', Model: 'Mod', Specification: 'Spec' }
])
mockQueryBySource.mockResolvedValue([
{ MaterialName: 'FilteredMat', MaterialCode: 'MF1', Model: 'FMod', Specification: 'FSpec' }
])
// MaterialsToBeDeletedDAO defaults
mockGetMaterialsByManager.mockResolvedValue([{ materialCode: 'M1', managerName: 'Mgr' }])
mockGetAllRecords.mockResolvedValue([
{ materialCode: 'M1', managerName: 'Mgr' },
{ materialCode: 'M2', managerName: 'Other' }
])
mockGetAllMaterialCodes.mockResolvedValue(new Set(['M1']))
// Production input defaults
mockGetSourceNumbers.mockResolvedValue(['SC001', 'SC002'])
mockReadProductionIds.mockReturnValue(['PROD001', 'PROD002'])
// Shared IDs default: empty
mockSharedIdsGet.mockReturnValue([])
// DB service creation
mockCreateDbService.mockImplementation(async () => createDbService())
mockDbDisconnect.mockResolvedValue(undefined)
// DB query dispatches by table name extracted from SQL
mockDbQuery.mockImplementation((sql: string) => {
const table = sql.match(/FROM\s+(\S+)/i)?.[1] ?? ''
if (/MaterialsTypeToBeDeleted/i.test(table))
return Promise.resolve({
rows: [{ MaterialName: 'MatA', ManagerName: 'Mgr' }],
rowCount: 1
})
if (/MaterialsToBeDeleted/i.test(table))
return Promise.resolve({
rows: [{ MaterialCode: 'M1', ManagerName: 'Mgr' }],
rowCount: 1
})
if (/DiscreteMaterialPlanData/i.test(table))
return Promise.resolve({
rows: [{ MaterialName: 'MatA', Specification: 'Spec', Model: 'Mod' }],
rowCount: 1
})
return Promise.resolve({ rows: [], rowCount: 0 })
})
service = new ValidationApplicationService()
})
// ─── validate database_full mode ───
describe('validate database_full mode', () => {
it('should return success with results for admin user', async () => {
const req: ValidationRequest = { mode: 'database_full' }
const userInfo = { id: 1, username: 'admin', userType: 'Admin' } as any
const res = await service.validate(req, userInfo, 1)
expect(res.success).toBe(true)
expect(res.results).toBeDefined()
expect(res.stats).toBeDefined()
expect(res.stats!.totalRecords).toBe(1)
})
it('should return success for regular user', async () => {
const req: ValidationRequest = { mode: 'database_full' }
const userInfo = { id: 2, username: 'guest', userType: 'User' } as any
const res = await service.validate(req, userInfo, 2)
expect(res.success).toBe(true)
expect(res.results).toBeDefined()
})
it('should return failure when material records are empty', async () => {
mockQueryAll.mockResolvedValueOnce([])
const req: ValidationRequest = { mode: 'database_full' }
const userInfo = { id: 1, username: 'admin', userType: 'Admin' } as any
const res = await service.validate(req, userInfo, 1)
expect(res.success).toBe(false)
expect(res.error).toContain('未找到物料记录')
})
it('should correctly compute stats for matched and marked records', async () => {
mockQueryAll.mockResolvedValueOnce([
{ MaterialName: 'MatA', MaterialCode: 'M1', Model: 'Mod', Specification: 'Spec' },
{ MaterialName: 'Other', MaterialCode: 'M2', Model: 'Mod', Specification: 'Spec' }
])
const req: ValidationRequest = { mode: 'database_full' }
const userInfo = { id: 1, username: 'admin', userType: 'Admin' } as any
const res = await service.validate(req, userInfo, 1)
expect(res.success).toBe(true)
expect(res.stats!.totalRecords).toBe(2)
// M1 is in markedCodes → isMarkedForDeletion=true, managerName='Mgr'
const m1Result = res.results!.find((r) => r.materialCode === 'M1')
expect(m1Result?.isMarkedForDeletion).toBe(true)
expect(m1Result?.managerName).toBe('Mgr')
// M2 is NOT in markedCodes and 'Other' doesn't match any type keyword
const m2Result = res.results!.find((r) => r.materialCode === 'M2')
expect(m2Result?.isMarkedForDeletion).toBe(false)
})
it('should match type keywords when material name contains keyword', async () => {
mockQueryAll.mockResolvedValueOnce([
{ MaterialName: 'MatA-Extra', MaterialCode: 'MX1', Model: 'Mod', Specification: 'Spec' }
])
const req: ValidationRequest = { mode: 'database_full' }
const userInfo = { id: 1, username: 'admin', userType: 'Admin' } as any
const res = await service.validate(req, userInfo, 1)
expect(res.success).toBe(true)
// 'MatA-Extra' contains 'MatA' which is a type keyword → matched
expect(res.results![0].matchedTypeKeyword).toBe('MatA')
expect(res.results![0].managerName).toBe('Mgr')
})
})
// ─── validate database_filtered with shared production IDs ───
describe('validate database_filtered with shared IDs', () => {
it('should return success when shared IDs resolve to orders', async () => {
mockSharedIdsGet.mockReturnValue(['PROD001'])
const req: ValidationRequest = { mode: 'database_filtered', useSharedProductionIds: true }
const userInfo = { id: 1, username: 'admin', userType: 'Admin' } as any
const res = await service.validate(req, userInfo, 1)
expect(res.success).toBe(true)
expect(res.results).toBeDefined()
expect(res.results!.length).toBeGreaterThan(0)
})
it('should return failure when shared IDs are empty', async () => {
mockSharedIdsGet.mockReturnValue([])
const req: ValidationRequest = { mode: 'database_filtered', useSharedProductionIds: true }
const userInfo = { id: 1, username: 'admin', userType: 'Admin' } as any
const res = await service.validate(req, userInfo, 1)
expect(res.success).toBe(false)
expect(res.error).toContain('共享')
})
it('should return failure when shared IDs yield no source numbers', async () => {
mockSharedIdsGet.mockReturnValue(['PROD001'])
mockGetSourceNumbers.mockResolvedValueOnce([])
const req: ValidationRequest = { mode: 'database_filtered', useSharedProductionIds: true }
const userInfo = { id: 1, username: 'admin', userType: 'Admin' } as any
const res = await service.validate(req, userInfo, 1)
expect(res.success).toBe(false)
expect(res.error).toContain('共享')
})
})
// ─── validate database_filtered with production ID file ───
describe('validate database_filtered with file', () => {
it('should return success when file IDs resolve to orders', async () => {
const req: ValidationRequest = {
mode: 'database_filtered',
productionIdFile: '/tmp/ids.txt'
}
const userInfo = { id: 1, username: 'admin', userType: 'Admin' } as any
const res = await service.validate(req, userInfo, 1)
expect(res.success).toBe(true)
expect(res.results).toBeDefined()
})
it('should return failure when file IDs yield no source numbers', async () => {
mockGetSourceNumbers.mockResolvedValueOnce([])
const req: ValidationRequest = {
mode: 'database_filtered',
productionIdFile: '/tmp/ids.txt'
}
const userInfo = { id: 1, username: 'admin', userType: 'Admin' } as any
const res = await service.validate(req, userInfo, 1)
expect(res.success).toBe(false)
expect(res.error).toContain('文件')
})
})
// ─── getMaterialsByManager ───
describe('getMaterialsByManager', () => {
it('should return enriched materials for a manager', async () => {
const result = await service.getMaterialsByManager('Mgr')
expect(result).toBeDefined()
expect(result.length).toBe(1)
expect(result[0].materialCode).toBe('M1')
expect(result[0].materialName).toBe('MatA')
expect(result[0].isMarked).toBe(true) // M1 is in allMaterialCodesResult
})
it('should return empty array when manager has no materials', async () => {
mockGetMaterialsByManager.mockResolvedValueOnce([])
const result = await service.getMaterialsByManager('Nobody')
expect(result).toEqual([])
})
})
// ─── getAllMaterials ───
describe('getAllMaterials', () => {
it('should return all enriched materials', async () => {
const result = await service.getAllMaterials()
expect(result).toBeDefined()
expect(result.length).toBe(2)
expect(result[0].materialCode).toBe('M1')
expect(result[0].isMarked).toBe(true)
expect(result[1].materialCode).toBe('M2')
expect(result[1].isMarked).toBe(false) // M2 not in allMaterialCodesResult
})
it('should return empty array when no materials exist', async () => {
mockGetAllRecords.mockResolvedValueOnce([])
const result = await service.getAllMaterials()
expect(result).toEqual([])
})
})
// ─── getCleanerData ───
describe('getCleanerData', () => {
it('should return order numbers and material codes for admin user', async () => {
mockSharedIdsGet.mockReturnValue(['PROD001'])
const userInfo = { id: 1, username: 'admin', userType: 'Admin' } as any
const result = await service.getCleanerData(userInfo, 1)
expect(result.success).toBe(true)
expect(result.orderNumbers).toBeDefined()
expect(result.orderNumbers!.length).toBeGreaterThan(0)
expect(result.materialCodes).toBeDefined()
})
it('should return empty order numbers when no shared IDs', async () => {
mockSharedIdsGet.mockReturnValue([])
const userInfo = { id: 1, username: 'admin', userType: 'Admin' } as any
const result = await service.getCleanerData(userInfo, 1)
expect(result.success).toBe(true)
expect(result.orderNumbers).toEqual([])
})
it('should handle errors gracefully', async () => {
mockCreateDbService.mockRejectedValueOnce(new Error('DB down'))
const userInfo = { id: 1, username: 'admin', userType: 'Admin' } as any
const result = await service.getCleanerData(userInfo, 1)
expect(result.success).toBe(false)
expect(result.error).toContain('DB down')
})
})
})