GuDingZiChan/backend/src/controllers/__tests__/disposalController.state-machine.test.ts

289 lines
7.8 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 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
}
})