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>
This commit is contained in:
@@ -18,130 +18,132 @@ vi.mock('../../../../src/main/services/logger', () => ({
|
|||||||
getRequestId: () => undefined
|
getRequestId: () => undefined
|
||||||
}))
|
}))
|
||||||
|
|
||||||
// ─── DAO mock state ───
|
// ─── Hoisted mock functions shared between vi.mock() factories and tests ───
|
||||||
let returnEmptyMaterials = false
|
const {
|
||||||
let materialRecords: any[] = [
|
mockQueryAll,
|
||||||
{ MaterialName: 'MatA', MaterialCode: 'M1', Model: 'Mod', Specification: 'Spec' }
|
mockQueryBySource,
|
||||||
]
|
mockGetMaterialsByManager,
|
||||||
let filteredMaterialRecords: any[] = [
|
mockGetAllRecords,
|
||||||
{ MaterialName: 'FilteredMat', MaterialCode: 'MF1', Model: 'FMod', Specification: 'FSpec' }
|
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()
|
||||||
|
}))
|
||||||
|
|
||||||
vi.mock('../../../../src/main/services/database/discrete-material-plan-dao', () => {
|
// ─── DiscreteMaterialPlanDAO mock ───
|
||||||
return {
|
vi.mock('../../../../src/main/services/database/discrete-material-plan-dao', () => ({
|
||||||
DiscreteMaterialPlanDAO: class {
|
DiscreteMaterialPlanDAO: class {
|
||||||
async queryAllDistinctByMaterialCode() {
|
queryAllDistinctByMaterialCode = mockQueryAll
|
||||||
if (returnEmptyMaterials) return []
|
queryBySourceNumbersDistinct = mockQueryBySource
|
||||||
return materialRecords
|
|
||||||
}
|
}
|
||||||
async queryBySourceNumbersDistinct(_nums: string[]) {
|
}))
|
||||||
if (returnEmptyMaterials) return []
|
|
||||||
return filteredMaterialRecords
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// ─── MaterialsToBeDeletedDAO mock state ───
|
// ─── MaterialsToBeDeletedDAO mock ───
|
||||||
let materialsByManagerResult: any[] = []
|
vi.mock('../../../../src/main/services/database/materials-to-be-deleted-dao', () => ({
|
||||||
let allMaterialsResult: any[] = []
|
|
||||||
let allMaterialCodesResult: Set<string> = new Set()
|
|
||||||
|
|
||||||
vi.mock('../../../../src/main/services/database/materials-to-be-deleted-dao', () => {
|
|
||||||
return {
|
|
||||||
MaterialsToBeDeletedDAO: class {
|
MaterialsToBeDeletedDAO: class {
|
||||||
async getMaterialsByManager(_managerName: string) {
|
getMaterialsByManager = mockGetMaterialsByManager
|
||||||
return materialsByManagerResult
|
getAllRecords = mockGetAllRecords
|
||||||
|
getAllMaterialCodes = mockGetAllMaterialCodes
|
||||||
}
|
}
|
||||||
async getAllRecords() {
|
}))
|
||||||
return allMaterialsResult
|
|
||||||
}
|
|
||||||
async getAllMaterialCodes() {
|
|
||||||
return allMaterialCodesResult
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// ─── Production input service mock ───
|
// ─── Production input service mock ───
|
||||||
let sourceNumbersFromInputs: string[] = ['SC001', 'SC002']
|
|
||||||
|
|
||||||
vi.mock('../../../../src/main/services/validation/production-input-service', () => ({
|
vi.mock('../../../../src/main/services/validation/production-input-service', () => ({
|
||||||
getSourceNumbersFromInputs: vi.fn().mockImplementation(async () => sourceNumbersFromInputs),
|
getSourceNumbersFromInputs: mockGetSourceNumbers,
|
||||||
readProductionIds: vi.fn().mockReturnValue(['PROD001', 'PROD002'])
|
readProductionIds: mockReadProductionIds
|
||||||
}))
|
}))
|
||||||
|
|
||||||
// ─── Shared production IDs store mock ───
|
// ─── Shared production IDs store mock ───
|
||||||
let sharedIds: string[] = []
|
|
||||||
|
|
||||||
vi.mock('../../../../src/main/services/validation/shared-production-ids-store', () => ({
|
vi.mock('../../../../src/main/services/validation/shared-production-ids-store', () => ({
|
||||||
sharedProductionIdsStore: {
|
sharedProductionIdsStore: {
|
||||||
get: (_senderId: number) => sharedIds
|
get: mockSharedIdsGet
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
|
|
||||||
// ─── Validation database mock with robust SQL matching ───
|
// ─── Validation database mock ───
|
||||||
function matchQuery(sql: string): string {
|
|
||||||
// Extract table name from FROM clause to avoid substring ambiguity
|
|
||||||
const fromMatch = sql.match(/FROM\s+(\S+)/i)
|
|
||||||
const table = fromMatch?.[1] ?? ''
|
|
||||||
|
|
||||||
if (/MaterialsTypeToBeDeleted/i.test(table)) return 'typeKeywords'
|
|
||||||
if (/MaterialsToBeDeleted/i.test(table)) return 'markedCodes'
|
|
||||||
if (/DiscreteMaterialPlanData/i.test(table)) return 'materialDetail'
|
|
||||||
return 'unknown'
|
|
||||||
}
|
|
||||||
|
|
||||||
vi.mock('../../../../src/main/services/validation/validation-database', () => ({
|
vi.mock('../../../../src/main/services/validation/validation-database', () => ({
|
||||||
createValidationDatabaseService: vi.fn().mockImplementation(async () => ({
|
createValidationDatabaseService: mockCreateDbService,
|
||||||
type: 'mysql' as const,
|
|
||||||
query: vi.fn().mockImplementation((sql: string) => {
|
|
||||||
const kind = matchQuery(sql)
|
|
||||||
switch (kind) {
|
|
||||||
case 'typeKeywords':
|
|
||||||
return Promise.resolve({
|
|
||||||
rows: [{ MaterialName: 'MatA', ManagerName: 'Mgr' }],
|
|
||||||
rowCount: 1
|
|
||||||
})
|
|
||||||
case 'markedCodes':
|
|
||||||
return Promise.resolve({
|
|
||||||
rows: [{ MaterialCode: 'M1', ManagerName: 'Mgr' }],
|
|
||||||
rowCount: 1
|
|
||||||
})
|
|
||||||
case 'materialDetail':
|
|
||||||
return Promise.resolve({
|
|
||||||
rows: [{ MaterialName: 'MatA', Specification: 'Spec', Model: 'Mod' }],
|
|
||||||
rowCount: 1
|
|
||||||
})
|
|
||||||
default:
|
|
||||||
return Promise.resolve({ rows: [], rowCount: 0 })
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
disconnect: vi.fn().mockResolvedValue(undefined),
|
|
||||||
connect: vi.fn().mockResolvedValue(undefined)
|
|
||||||
})),
|
|
||||||
getValidationTableName: vi.fn().mockImplementation((name: string) => name)
|
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', () => {
|
describe('ValidationApplicationService', () => {
|
||||||
let service: ValidationApplicationService
|
let service: ValidationApplicationService
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
returnEmptyMaterials = false
|
vi.clearAllMocks()
|
||||||
materialRecords = [
|
|
||||||
|
// DiscreteMaterialPlanDAO defaults
|
||||||
|
mockQueryAll.mockResolvedValue([
|
||||||
{ MaterialName: 'MatA', MaterialCode: 'M1', Model: 'Mod', Specification: 'Spec' }
|
{ MaterialName: 'MatA', MaterialCode: 'M1', Model: 'Mod', Specification: 'Spec' }
|
||||||
]
|
])
|
||||||
filteredMaterialRecords = [
|
mockQueryBySource.mockResolvedValue([
|
||||||
{ MaterialName: 'FilteredMat', MaterialCode: 'MF1', Model: 'FMod', Specification: 'FSpec' }
|
{ MaterialName: 'FilteredMat', MaterialCode: 'MF1', Model: 'FMod', Specification: 'FSpec' }
|
||||||
]
|
])
|
||||||
materialsByManagerResult = [{ materialCode: 'M1', managerName: 'Mgr' }]
|
|
||||||
allMaterialsResult = [
|
// MaterialsToBeDeletedDAO defaults
|
||||||
|
mockGetMaterialsByManager.mockResolvedValue([{ materialCode: 'M1', managerName: 'Mgr' }])
|
||||||
|
mockGetAllRecords.mockResolvedValue([
|
||||||
{ materialCode: 'M1', managerName: 'Mgr' },
|
{ materialCode: 'M1', managerName: 'Mgr' },
|
||||||
{ materialCode: 'M2', managerName: 'Other' }
|
{ materialCode: 'M2', managerName: 'Other' }
|
||||||
]
|
])
|
||||||
allMaterialCodesResult = new Set(['M1'])
|
mockGetAllMaterialCodes.mockResolvedValue(new Set(['M1']))
|
||||||
sourceNumbersFromInputs = ['SC001', 'SC002']
|
|
||||||
sharedIds = []
|
// 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()
|
service = new ValidationApplicationService()
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -166,7 +168,7 @@ describe('ValidationApplicationService', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should return failure when material records are empty', async () => {
|
it('should return failure when material records are empty', async () => {
|
||||||
returnEmptyMaterials = true
|
mockQueryAll.mockResolvedValueOnce([])
|
||||||
const req: ValidationRequest = { mode: 'database_full' }
|
const req: ValidationRequest = { mode: 'database_full' }
|
||||||
const userInfo = { id: 1, username: 'admin', userType: 'Admin' } as any
|
const userInfo = { id: 1, username: 'admin', userType: 'Admin' } as any
|
||||||
const res = await service.validate(req, userInfo, 1)
|
const res = await service.validate(req, userInfo, 1)
|
||||||
@@ -175,10 +177,10 @@ describe('ValidationApplicationService', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should correctly compute stats for matched and marked records', async () => {
|
it('should correctly compute stats for matched and marked records', async () => {
|
||||||
materialRecords = [
|
mockQueryAll.mockResolvedValueOnce([
|
||||||
{ MaterialName: 'MatA', MaterialCode: 'M1', Model: 'Mod', Specification: 'Spec' },
|
{ MaterialName: 'MatA', MaterialCode: 'M1', Model: 'Mod', Specification: 'Spec' },
|
||||||
{ MaterialName: 'Other', MaterialCode: 'M2', Model: 'Mod', Specification: 'Spec' }
|
{ MaterialName: 'Other', MaterialCode: 'M2', Model: 'Mod', Specification: 'Spec' }
|
||||||
]
|
])
|
||||||
const req: ValidationRequest = { mode: 'database_full' }
|
const req: ValidationRequest = { mode: 'database_full' }
|
||||||
const userInfo = { id: 1, username: 'admin', userType: 'Admin' } as any
|
const userInfo = { id: 1, username: 'admin', userType: 'Admin' } as any
|
||||||
const res = await service.validate(req, userInfo, 1)
|
const res = await service.validate(req, userInfo, 1)
|
||||||
@@ -189,15 +191,15 @@ describe('ValidationApplicationService', () => {
|
|||||||
const m1Result = res.results!.find((r) => r.materialCode === 'M1')
|
const m1Result = res.results!.find((r) => r.materialCode === 'M1')
|
||||||
expect(m1Result?.isMarkedForDeletion).toBe(true)
|
expect(m1Result?.isMarkedForDeletion).toBe(true)
|
||||||
expect(m1Result?.managerName).toBe('Mgr')
|
expect(m1Result?.managerName).toBe('Mgr')
|
||||||
// M2 is NOT in markedCodes but 'Other' doesn't match any type keyword
|
// M2 is NOT in markedCodes and 'Other' doesn't match any type keyword
|
||||||
const m2Result = res.results!.find((r) => r.materialCode === 'M2')
|
const m2Result = res.results!.find((r) => r.materialCode === 'M2')
|
||||||
expect(m2Result?.isMarkedForDeletion).toBe(false)
|
expect(m2Result?.isMarkedForDeletion).toBe(false)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should match type keywords when material name contains keyword', async () => {
|
it('should match type keywords when material name contains keyword', async () => {
|
||||||
materialRecords = [
|
mockQueryAll.mockResolvedValueOnce([
|
||||||
{ MaterialName: 'MatA-Extra', MaterialCode: 'MX1', Model: 'Mod', Specification: 'Spec' }
|
{ MaterialName: 'MatA-Extra', MaterialCode: 'MX1', Model: 'Mod', Specification: 'Spec' }
|
||||||
]
|
])
|
||||||
const req: ValidationRequest = { mode: 'database_full' }
|
const req: ValidationRequest = { mode: 'database_full' }
|
||||||
const userInfo = { id: 1, username: 'admin', userType: 'Admin' } as any
|
const userInfo = { id: 1, username: 'admin', userType: 'Admin' } as any
|
||||||
const res = await service.validate(req, userInfo, 1)
|
const res = await service.validate(req, userInfo, 1)
|
||||||
@@ -212,7 +214,7 @@ describe('ValidationApplicationService', () => {
|
|||||||
// ─── validate – database_filtered with shared production IDs ───
|
// ─── validate – database_filtered with shared production IDs ───
|
||||||
describe('validate – database_filtered with shared IDs', () => {
|
describe('validate – database_filtered with shared IDs', () => {
|
||||||
it('should return success when shared IDs resolve to orders', async () => {
|
it('should return success when shared IDs resolve to orders', async () => {
|
||||||
sharedIds = ['PROD001']
|
mockSharedIdsGet.mockReturnValue(['PROD001'])
|
||||||
const req: ValidationRequest = { mode: 'database_filtered', useSharedProductionIds: true }
|
const req: ValidationRequest = { mode: 'database_filtered', useSharedProductionIds: true }
|
||||||
const userInfo = { id: 1, username: 'admin', userType: 'Admin' } as any
|
const userInfo = { id: 1, username: 'admin', userType: 'Admin' } as any
|
||||||
|
|
||||||
@@ -224,7 +226,7 @@ describe('ValidationApplicationService', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should return failure when shared IDs are empty', async () => {
|
it('should return failure when shared IDs are empty', async () => {
|
||||||
sharedIds = []
|
mockSharedIdsGet.mockReturnValue([])
|
||||||
const req: ValidationRequest = { mode: 'database_filtered', useSharedProductionIds: true }
|
const req: ValidationRequest = { mode: 'database_filtered', useSharedProductionIds: true }
|
||||||
const userInfo = { id: 1, username: 'admin', userType: 'Admin' } as any
|
const userInfo = { id: 1, username: 'admin', userType: 'Admin' } as any
|
||||||
|
|
||||||
@@ -235,8 +237,8 @@ describe('ValidationApplicationService', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should return failure when shared IDs yield no source numbers', async () => {
|
it('should return failure when shared IDs yield no source numbers', async () => {
|
||||||
sharedIds = ['PROD001']
|
mockSharedIdsGet.mockReturnValue(['PROD001'])
|
||||||
sourceNumbersFromInputs = []
|
mockGetSourceNumbers.mockResolvedValueOnce([])
|
||||||
const req: ValidationRequest = { mode: 'database_filtered', useSharedProductionIds: true }
|
const req: ValidationRequest = { mode: 'database_filtered', useSharedProductionIds: true }
|
||||||
const userInfo = { id: 1, username: 'admin', userType: 'Admin' } as any
|
const userInfo = { id: 1, username: 'admin', userType: 'Admin' } as any
|
||||||
|
|
||||||
@@ -263,7 +265,7 @@ describe('ValidationApplicationService', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should return failure when file IDs yield no source numbers', async () => {
|
it('should return failure when file IDs yield no source numbers', async () => {
|
||||||
sourceNumbersFromInputs = []
|
mockGetSourceNumbers.mockResolvedValueOnce([])
|
||||||
const req: ValidationRequest = {
|
const req: ValidationRequest = {
|
||||||
mode: 'database_filtered',
|
mode: 'database_filtered',
|
||||||
productionIdFile: '/tmp/ids.txt'
|
productionIdFile: '/tmp/ids.txt'
|
||||||
@@ -290,7 +292,7 @@ describe('ValidationApplicationService', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should return empty array when manager has no materials', async () => {
|
it('should return empty array when manager has no materials', async () => {
|
||||||
materialsByManagerResult = []
|
mockGetMaterialsByManager.mockResolvedValueOnce([])
|
||||||
|
|
||||||
const result = await service.getMaterialsByManager('Nobody')
|
const result = await service.getMaterialsByManager('Nobody')
|
||||||
|
|
||||||
@@ -312,7 +314,7 @@ describe('ValidationApplicationService', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should return empty array when no materials exist', async () => {
|
it('should return empty array when no materials exist', async () => {
|
||||||
allMaterialsResult = []
|
mockGetAllRecords.mockResolvedValueOnce([])
|
||||||
|
|
||||||
const result = await service.getAllMaterials()
|
const result = await service.getAllMaterials()
|
||||||
|
|
||||||
@@ -323,7 +325,7 @@ describe('ValidationApplicationService', () => {
|
|||||||
// ─── getCleanerData ───
|
// ─── getCleanerData ───
|
||||||
describe('getCleanerData', () => {
|
describe('getCleanerData', () => {
|
||||||
it('should return order numbers and material codes for admin user', async () => {
|
it('should return order numbers and material codes for admin user', async () => {
|
||||||
sharedIds = ['PROD001']
|
mockSharedIdsGet.mockReturnValue(['PROD001'])
|
||||||
const userInfo = { id: 1, username: 'admin', userType: 'Admin' } as any
|
const userInfo = { id: 1, username: 'admin', userType: 'Admin' } as any
|
||||||
|
|
||||||
const result = await service.getCleanerData(userInfo, 1)
|
const result = await service.getCleanerData(userInfo, 1)
|
||||||
@@ -335,7 +337,7 @@ describe('ValidationApplicationService', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should return empty order numbers when no shared IDs', async () => {
|
it('should return empty order numbers when no shared IDs', async () => {
|
||||||
sharedIds = []
|
mockSharedIdsGet.mockReturnValue([])
|
||||||
const userInfo = { id: 1, username: 'admin', userType: 'Admin' } as any
|
const userInfo = { id: 1, username: 'admin', userType: 'Admin' } as any
|
||||||
|
|
||||||
const result = await service.getCleanerData(userInfo, 1)
|
const result = await service.getCleanerData(userInfo, 1)
|
||||||
@@ -345,9 +347,7 @@ describe('ValidationApplicationService', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should handle errors gracefully', async () => {
|
it('should handle errors gracefully', async () => {
|
||||||
const { createValidationDatabaseService } =
|
mockCreateDbService.mockRejectedValueOnce(new Error('DB down'))
|
||||||
await import('../../../../src/main/services/validation/validation-database')
|
|
||||||
vi.mocked(createValidationDatabaseService).mockRejectedValueOnce(new Error('DB down'))
|
|
||||||
|
|
||||||
const userInfo = { id: 1, username: 'admin', userType: 'Admin' } as any
|
const userInfo = { id: 1, username: 'admin', userType: 'Admin' } as any
|
||||||
const result = await service.getCleanerData(userInfo, 1)
|
const result = await service.getCleanerData(userInfo, 1)
|
||||||
|
|||||||
Reference in New Issue
Block a user