64 lines
5.1 KiB
TypeScript
64 lines
5.1 KiB
TypeScript
import { beforeEach, expect, it, vi } from 'vitest'
|
|||
|
|
import { ApiResponseError } from '@/shared/api/http'
|
||
|
|
import { useDispatchCommandRecovery } from './dispatchCommandRecovery'
|
||
|
|
import { command } from './dispatchTestFixtures'
|
||
|
|
beforeEach(() => sessionStorage.clear())
|
||
|
|
it('retains the exact original version and body when POST outcome is unknown and a newer detail was read', async () => {
|
||
|
|
const owner = crypto.randomUUID(), recovery = useDispatchCommandRecovery(() => owner)
|
||
|
|
recovery.begin('PC/1', 'DEPART', { recordVersion: 0, idempotencyKey: 'key', remark: 'original' })
|
||
|
|
const send = vi.fn().mockRejectedValueOnce(new Error('network unknown')).mockResolvedValueOnce(command('DEPART'))
|
||
|
|
const read = vi.fn().mockResolvedValue({ dispatchNo: 'PC/1', recordVersion: 50 }), find = vi.fn().mockRejectedValue(new ApiResponseError('not found', 'HTTP_404'))
|
||
|
|
await expect(recovery.execute(send, read, find)).rejects.toThrow('network unknown')
|
||
|
|
await recovery.execute(send, read, find)
|
||
|
|
expect(send.mock.calls[1]![0].body).toEqual({ recordVersion: 0, idempotencyKey: 'key', remark: 'original' })
|
||
|
|
expect(recovery.pending.value).toBeNull()
|
||
|
|
})
|
||
|
|
it('keeps a proven result across GET failure and does not send the command a second time', async () => {
|
||
|
|
const owner = crypto.randomUUID(), recovery = useDispatchCommandRecovery(() => owner)
|
||
|
|
recovery.begin('PC/1', 'LOADING_COMPLETE', { recordVersion: 0, idempotencyKey: 'key' })
|
||
|
|
const send = vi.fn().mockResolvedValue(command('LOADING_COMPLETE')), read = vi.fn().mockRejectedValueOnce(new Error('read failed')).mockResolvedValueOnce({ dispatchNo: 'PC/1', recordVersion: 1 }), find = vi.fn()
|
||
|
|
await expect(recovery.execute(send, read, find)).rejects.toThrow('read failed')
|
||
|
|
expect(recovery.pending.value?.result?.stage).toBe('AFTER_LOADING')
|
||
|
|
await recovery.execute(send, read, find)
|
||
|
|
expect(send).toHaveBeenCalledTimes(1); expect(find).not.toHaveBeenCalled()
|
||
|
|
})
|
||
|
|
it('shares one pending command between empty-create and workflow dialogs and refuses a new command', () => {
|
||
|
|
const owner = crypto.randomUUID(), first = useDispatchCommandRecovery(() => owner), second = useDispatchCommandRecovery(() => owner)
|
||
|
|
first.begin('PC/1', 'EDIT', { recordVersion: 0, idempotencyKey: 'key' })
|
||
|
|
expect(second.pending.value?.dispatchNo).toBe('PC/1')
|
||
|
|
expect(() => second.begin('', 'CREATE', { idempotencyKey: 'new' })).toThrow('原请求')
|
||
|
|
})
|
||
|
|
it('does not discard an earlier unknown write on a later business rejection', async () => {
|
||
|
|
const recovery = useDispatchCommandRecovery(() => crypto.randomUUID())
|
||
|
|
expect(() => recovery.begin('PC/1', 'EDIT', { recordVersion: 0, idempotencyKey: 'key' })).toThrow('账号')
|
||
|
|
const owner = crypto.randomUUID(), stable = useDispatchCommandRecovery(() => owner)
|
||
|
|
stable.begin('PC/1', 'EDIT', { recordVersion: 0, idempotencyKey: 'key' })
|
||
|
|
const send = vi.fn().mockRejectedValueOnce(new Error('unknown')).mockRejectedValueOnce(new ApiResponseError('version conflict', 'A0409'))
|
||
|
|
const find = vi.fn().mockRejectedValue(new ApiResponseError('not found', 'HTTP_404'))
|
||
|
|
await expect(stable.execute(send, vi.fn(), find)).rejects.toThrow('unknown')
|
||
|
|
await expect(stable.execute(send, vi.fn(), find)).rejects.toThrow('version conflict')
|
||
|
|
stable.discardRejected(); expect(stable.pending.value).not.toBeNull()
|
||
|
|
})
|
||
|
|
it('rejects a mismatched business number/action/key instead of treating it as successful', async () => {
|
||
|
|
const owner = crypto.randomUUID(), recovery = useDispatchCommandRecovery(() => owner)
|
||
|
|
recovery.begin('PC/1', 'DEPART', { recordVersion: 0, idempotencyKey: 'key' })
|
||
|
|
const read = vi.fn()
|
||
|
|
await expect(recovery.execute(vi.fn().mockResolvedValue(command('DEPART', 'different')), read, vi.fn())).rejects.toThrow('不一致')
|
||
|
|
expect(read).not.toHaveBeenCalled(); expect(recovery.pending.value).not.toBeNull()
|
||
|
|
})
|
||
|
|
it('does not release a proven result for an older detail version, including versions beyond safe integer precision', async () => {
|
||
|
|
const owner = crypto.randomUUID(), recovery = useDispatchCommandRecovery(() => owner)
|
||
|
|
recovery.begin('PC/1', 'DEPART', { recordVersion: '9007199254740992', idempotencyKey: 'key' })
|
||
|
|
const send = vi.fn().mockResolvedValue({ ...command('DEPART'), recordVersion: '9007199254740993' }), find = vi.fn()
|
||
|
|
await expect(recovery.execute(send, vi.fn().mockResolvedValue({ dispatchNo: 'PC/1', recordVersion: '9007199254740992' }), find)).rejects.toThrow('详情版本早于')
|
||
|
|
expect(recovery.pending.value?.result).not.toBeNull()
|
||
|
|
await recovery.execute(send, vi.fn().mockResolvedValue({ dispatchNo: 'PC/1', recordVersion: '9007199254740994' }), find)
|
||
|
|
expect(send).toHaveBeenCalledTimes(1); expect(recovery.pending.value).toBeNull()
|
||
|
|
})
|
||
|
|
it('treats an internal server error as unknown instead of authorizing a new command', async () => {
|
||
|
|
const owner = crypto.randomUUID(), recovery = useDispatchCommandRecovery(() => owner)
|
||
|
|
recovery.begin('PC/1', 'EDIT', { recordVersion: 0, idempotencyKey: 'key' })
|
||
|
|
await expect(recovery.execute(vi.fn().mockRejectedValue(new ApiResponseError('server failure', 'A0500', 500)), vi.fn(), vi.fn())).rejects.toThrow('server failure')
|
||
|
|
recovery.discardRejected(); expect(recovery.pending.value?.rejected).toBe(false)
|
||
|
|
})
|