289 lines
7.8 KiB
TypeScript
289 lines
7.8 KiB
TypeScript
import test from 'node:test'
|
||
import assert from 'node:assert/strict'
|
||
|
||
import prisma from '../../utils/prisma'
|
||
import { approveDisposal, createDisposal } from '../disposalController'
|
||
|
||
const now = new Date('2026-07-14T03:00:00.000Z')
|
||
const rootEquipment = {
|
||
id: 10,
|
||
equipment_code: 'JS-PC-0010',
|
||
equipment_name: '整机',
|
||
parent_id: null,
|
||
department_id: 3,
|
||
storage_location_id: 1,
|
||
status: '可用',
|
||
mount_status: 'none',
|
||
total_quantity: 1,
|
||
remaining_quantity: 1,
|
||
unit_price: 1000,
|
||
updated_at: now
|
||
}
|
||
const childEquipment = {
|
||
id: 11,
|
||
equipment_code: 'JS-RAM-0011',
|
||
equipment_name: '内存',
|
||
parent_id: 10,
|
||
department_id: 3,
|
||
storage_location_id: 1,
|
||
status: '可用',
|
||
mount_status: 'mounted',
|
||
total_quantity: 1,
|
||
remaining_quantity: 1,
|
||
unit_price: 200,
|
||
updated_at: now
|
||
}
|
||
|
||
const createResponse = (role = 'it') => {
|
||
const state = { statusCode: 200, body: null as any }
|
||
const res = {
|
||
locals: { user: { id: 6, role, name: '测试用户', username: 'tester' } },
|
||
status(code: number) {
|
||
state.statusCode = code
|
||
return this
|
||
},
|
||
json(payload: any) {
|
||
state.body = payload
|
||
return this
|
||
}
|
||
}
|
||
return { res, state }
|
||
}
|
||
|
||
test('主资产仍挂载未勾选配件时申请报废返回 409 且不写状态', async () => {
|
||
const originalTransaction = prisma.$transaction
|
||
let stateWriteCount = 0
|
||
let disposalCreateCount = 0
|
||
|
||
;(prisma.$transaction as any) = async (callback: any) => callback({
|
||
$queryRaw: async (query: any) => {
|
||
if (query.sql.includes('WHERE equipment.id = ?')) return [rootEquipment]
|
||
if (query.sql.includes('WHERE equipment.parent_id IN')) {
|
||
return query.values.includes(rootEquipment.id) ? [childEquipment] : []
|
||
}
|
||
return []
|
||
},
|
||
equipment: {
|
||
findUnique: async () => rootEquipment,
|
||
findMany: async (args: any) => {
|
||
const parentIds = args.where.parent_id.in as number[]
|
||
return parentIds.includes(10) ? [childEquipment] : []
|
||
},
|
||
updateMany: async () => {
|
||
stateWriteCount += 1
|
||
return { count: 1 }
|
||
}
|
||
},
|
||
disposalRecord: {
|
||
create: async () => {
|
||
disposalCreateCount += 1
|
||
return { id: 1 }
|
||
}
|
||
}
|
||
})
|
||
|
||
try {
|
||
const { res, state } = createResponse()
|
||
await createDisposal(
|
||
{
|
||
params: {},
|
||
body: {
|
||
equipment_id: 10,
|
||
item_ids: [10],
|
||
disposal_reason: '设备老化',
|
||
disposal_method: '报废',
|
||
estimated_value: 0
|
||
}
|
||
} as any,
|
||
res as any
|
||
)
|
||
|
||
assert.equal(state.statusCode, 409)
|
||
assert.match(state.body.message, /请先卸载未勾选配件/)
|
||
assert.equal(disposalCreateCount, 0)
|
||
assert.equal(stateWriteCount, 0)
|
||
} finally {
|
||
;(prisma.$transaction as any) = originalTransaction
|
||
}
|
||
})
|
||
|
||
test('初读资产树后并发挂载新配件时报废申请中止且不写数据', async () => {
|
||
const originalTransaction = prisma.$transaction
|
||
let mountedAfterInitialRead = false
|
||
let stateWriteCount = 0
|
||
let disposalCreateCount = 0
|
||
const lockQueries: any[] = []
|
||
|
||
;(prisma.$transaction as any) = async (callback: any) => callback({
|
||
$queryRaw: async (query: any) => {
|
||
if (query.sql.startsWith('SELECT id FROM equipment')) {
|
||
lockQueries.push(query)
|
||
mountedAfterInitialRead = true
|
||
return []
|
||
}
|
||
if (query.sql.includes('WHERE equipment.id = ?')) return [rootEquipment]
|
||
if (query.sql.includes('WHERE equipment.parent_id IN')) {
|
||
return mountedAfterInitialRead && query.values.includes(rootEquipment.id)
|
||
? [childEquipment]
|
||
: []
|
||
}
|
||
return []
|
||
},
|
||
equipment: {
|
||
findUnique: async () => rootEquipment,
|
||
// 模拟 RR 旧快照:锁后再做 ORM 普通读仍会漏掉并发新配件。
|
||
findMany: async () => [],
|
||
updateMany: async () => {
|
||
stateWriteCount += 1
|
||
return { count: 1 }
|
||
}
|
||
},
|
||
disposalRecord: {
|
||
create: async () => {
|
||
disposalCreateCount += 1
|
||
return { id: 1 }
|
||
}
|
||
}
|
||
})
|
||
|
||
try {
|
||
const { res, state } = createResponse()
|
||
await createDisposal(
|
||
{
|
||
params: {},
|
||
body: {
|
||
equipment_id: rootEquipment.id,
|
||
item_ids: [rootEquipment.id],
|
||
disposal_reason: '设备老化',
|
||
disposal_method: '报废',
|
||
estimated_value: 0
|
||
}
|
||
} as any,
|
||
res as any
|
||
)
|
||
|
||
assert.equal(state.statusCode, 409)
|
||
assert.match(state.body.message, /资产树状态或挂载关系已变化/)
|
||
assert.match(lockQueries[0].sql, /IN \(\?\) ORDER BY id FOR UPDATE/)
|
||
assert.deepEqual(lockQueries[0].values, [10])
|
||
assert.equal(disposalCreateCount, 0)
|
||
assert.equal(stateWriteCount, 0)
|
||
} finally {
|
||
;(prisma.$transaction as any) = originalTransaction
|
||
}
|
||
})
|
||
|
||
test('并发审批 CAS 失败时返回 409,不追加动作也不改资产', async () => {
|
||
const originalTransaction = prisma.$transaction
|
||
let actionCount = 0
|
||
let equipmentWriteCount = 0
|
||
const disposal = {
|
||
id: 91,
|
||
equipment_id: 10,
|
||
equipment_code: rootEquipment.equipment_code,
|
||
applied_by: 4,
|
||
status: 'pending',
|
||
items: [{
|
||
equipment_id: 10,
|
||
equipment_code: rootEquipment.equipment_code,
|
||
parent_equipment_id: null,
|
||
pre_status: '可用',
|
||
equipment: { ...rootEquipment, status: '待报废' }
|
||
}]
|
||
}
|
||
|
||
;(prisma.$transaction as any) = async (callback: any) => callback({
|
||
disposalRecord: {
|
||
findUnique: async () => disposal,
|
||
updateMany: async () => ({ count: 0 })
|
||
},
|
||
disposalAction: {
|
||
create: async () => {
|
||
actionCount += 1
|
||
}
|
||
},
|
||
equipment: {
|
||
updateMany: async () => {
|
||
equipmentWriteCount += 1
|
||
return { count: 1 }
|
||
}
|
||
}
|
||
})
|
||
|
||
try {
|
||
const { res, state } = createResponse('administrative')
|
||
await approveDisposal(
|
||
{ params: { id: '91' }, body: { approved: true } } as any,
|
||
res as any
|
||
)
|
||
|
||
assert.equal(state.statusCode, 409)
|
||
assert.equal(actionCount, 0)
|
||
assert.equal(equipmentWriteCount, 0)
|
||
} finally {
|
||
;(prisma.$transaction as any) = originalTransaction
|
||
}
|
||
})
|
||
|
||
test('驳回报废时按每个 item.pre_status 分别恢复', async () => {
|
||
const originalTransaction = prisma.$transaction
|
||
const originalLogCreate = prisma.operationLog.create
|
||
const restoredStatuses: string[] = []
|
||
const disposal = {
|
||
id: 92,
|
||
equipment_id: 10,
|
||
equipment_code: rootEquipment.equipment_code,
|
||
applied_by: 4,
|
||
status: 'pending',
|
||
items: [
|
||
{
|
||
equipment_id: 10,
|
||
equipment_code: rootEquipment.equipment_code,
|
||
parent_equipment_id: null,
|
||
pre_status: '可用',
|
||
equipment: { ...rootEquipment, status: '待报废' }
|
||
},
|
||
{
|
||
equipment_id: 11,
|
||
equipment_code: childEquipment.equipment_code,
|
||
parent_equipment_id: 10,
|
||
pre_status: '已损坏',
|
||
equipment: { ...childEquipment, status: '待报废' }
|
||
}
|
||
]
|
||
}
|
||
|
||
;(prisma.$transaction as any) = async (callback: any) => callback({
|
||
disposalRecord: {
|
||
findUnique: async () => disposal,
|
||
updateMany: async () => ({ count: 1 })
|
||
},
|
||
equipment: {
|
||
updateMany: async (args: any) => {
|
||
restoredStatuses.push(args.data.status)
|
||
return { count: 1 }
|
||
}
|
||
},
|
||
disposalAction: { create: async () => ({ id: 1 }) },
|
||
user: { findMany: async () => [] }
|
||
})
|
||
;(prisma.operationLog.create as any) = async () => ({ id: 1 })
|
||
|
||
try {
|
||
const { res, state } = createResponse('administrative')
|
||
await approveDisposal(
|
||
{
|
||
params: { id: '92' },
|
||
body: { approved: false, reject_reason: '尚可继续使用' }
|
||
} as any,
|
||
res as any
|
||
)
|
||
|
||
assert.equal(state.statusCode, 200)
|
||
assert.deepEqual(restoredStatuses, ['可用', '已损坏'])
|
||
} finally {
|
||
;(prisma.$transaction as any) = originalTransaction
|
||
;(prisma.operationLog.create as any) = originalLogCreate
|
||
}
|
||
})
|