83 lines
2.7 KiB
TypeScript
83 lines
2.7 KiB
TypeScript
/** @jest-environment node */
|
||
|
||
import {
|
||
acquireTaskNoAssignmentLock,
|
||
assignTaskNoOnComplete,
|
||
} from '@/lib/product-package'
|
||
import { prisma } from '@/lib/prisma'
|
||
|
||
jest.mock('@/lib/prisma', () => ({
|
||
prisma: {
|
||
$transaction: jest.fn(),
|
||
material: {
|
||
findFirst: jest.fn(),
|
||
findMany: jest.fn(),
|
||
},
|
||
},
|
||
}))
|
||
|
||
describe('任务号完成写入并发锁', () => {
|
||
beforeEach(() => {
|
||
jest.clearAllMocks()
|
||
;(prisma.material.findFirst as jest.Mock).mockResolvedValue(null)
|
||
;(prisma.material.findMany as jest.Mock).mockResolvedValue([])
|
||
})
|
||
|
||
test('advisory lock 只返回 Prisma 可反序列化的整数列', async () => {
|
||
const tx = { $queryRaw: jest.fn().mockResolvedValue([{ locked: 1 }]) } as any
|
||
|
||
await acquireTaskNoAssignmentLock(tx, 'TASK202604230001')
|
||
|
||
const [template, lockName] = tx.$queryRaw.mock.calls[0]
|
||
const sql = Array.from(template as TemplateStringsArray).join('?')
|
||
expect(sql).toContain('SELECT 1 AS locked')
|
||
expect(sql).toContain('pg_advisory_xact_lock(hashtext(?)')
|
||
expect(lockName).toBe('task-no:TASK202604230001')
|
||
})
|
||
|
||
test('两个素材并发完成同一任务号时只能一个写入成功', async () => {
|
||
let assignedMaterialId: string | null = null
|
||
let transactionTail = Promise.resolve()
|
||
const tx = {
|
||
$queryRaw: jest.fn().mockResolvedValue([{ locked: 1 }]),
|
||
material: {
|
||
findFirst: jest.fn(async () => assignedMaterialId
|
||
? {
|
||
id: assignedMaterialId,
|
||
productModel: 'SPU-EXISTING',
|
||
materialType: '任务-详情',
|
||
}
|
||
: null),
|
||
update: jest.fn(async ({ where }: { where: { id: string } }) => {
|
||
assignedMaterialId = where.id
|
||
return { id: where.id }
|
||
}),
|
||
},
|
||
}
|
||
;(prisma.$transaction as jest.Mock).mockImplementation(async (callback) => {
|
||
const previous = transactionTail
|
||
let release!: () => void
|
||
transactionTail = new Promise<void>((resolve) => { release = resolve })
|
||
await previous
|
||
try {
|
||
return await callback(tx)
|
||
} finally {
|
||
release()
|
||
}
|
||
})
|
||
|
||
const results = await Promise.allSettled([
|
||
assignTaskNoOnComplete('material-a', 'TASK202604230001'),
|
||
assignTaskNoOnComplete('material-b', 'TASK202604230001'),
|
||
])
|
||
|
||
expect(results.filter((result) => result.status === 'fulfilled')).toHaveLength(1)
|
||
const rejected = results.find((result): result is PromiseRejectedResult => result.status === 'rejected')
|
||
expect(rejected?.reason).toEqual(new Error(
|
||
'任务号 TASK202604230001 已存在,对应素材:SPU-EXISTING(任务-详情)',
|
||
))
|
||
expect(tx.material.update).toHaveBeenCalledTimes(1)
|
||
expect(tx.$queryRaw).toHaveBeenCalledTimes(2)
|
||
})
|
||
})
|