73 lines
6.4 KiB
TypeScript
73 lines
6.4 KiB
TypeScript
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|||
|
|
import { ApiResponseError } from '@/shared/api/http'
|
||
|
|
import * as api from '../api/ownedBusiness'
|
||
|
|
import { ownedBusinessFixture, ownedBusinessCommandFixture } from './OwnedBusinessFixtures'
|
||
|
|
import { acceptOwnedBusinessCommand, newOwnedBusinessPending, persistOwnedBusinessPending, restoreOwnedBusinessPending, recoverOwnedBusinessCommand, ownedBusinessPendingKey } from './OwnedBusinessCommandRecovery'
|
||
|
|
import type { OwnedBusinessPhoto, OwnedBusinessSave } from '../ownedBusinessTypes'
|
||
|
|
|
||
|
|
beforeEach(() => sessionStorage.clear())
|
||
|
|
const no = 'DS-OWN-1'
|
||
|
|
function operation() : OwnedBusinessSave { return { recordVersion: '0', idempotencyKey: 'original-key', operations: { ...ownedBusinessFixture().row.operations, money: { currency: 'USD', amount: '9999999999999999.99' } }, reason: null } }
|
||
|
|
function client() {
|
||
|
|
const current = ownedBusinessFixture(); current.row.recordVersion = '1'
|
||
|
|
const photo: OwnedBusinessPhoto = { slot:'FUEL_START',category:'FUEL',fileId:'9007199254740993',fileName:'fuel.png',contentType:'image/png',sizeBytes:'5',uploadedAt:'2026-10-20T10:00:00Z',savedAt:null }
|
||
|
|
return { ...api, getOwnedBusiness: vi.fn(async () => current), getOwnedBusinessCommand: vi.fn(async () => ownedBusinessCommandFixture()), saveOwnedBusinessOperations: vi.fn(async () => current), bindOwnedBusinessPhoto: vi.fn(async () => current), uploadOwnedBusinessPhoto: vi.fn(async () => photo) }
|
||
|
|
}
|
||
|
|
describe('owned business exact command recovery', () => {
|
||
|
|
it('persists the complete original payload, actor and zero auxiliary version', () => {
|
||
|
|
const pending = newOwnedBusinessPending('actor', no, 'SAVE_OPERATIONS', operation())
|
||
|
|
persistOwnedBusinessPending('actor', no, 'operations', pending)
|
||
|
|
expect(restoreOwnedBusinessPending('actor', no, 'operations')).toEqual(pending)
|
||
|
|
expect(restoreOwnedBusinessPending('another', no, 'operations')).toBeNull()
|
||
|
|
const raw = JSON.parse(sessionStorage.getItem(ownedBusinessPendingKey('actor',no,'operations'))!)
|
||
|
|
raw.actor = 'another'; sessionStorage.setItem(ownedBusinessPendingKey('actor',no,'operations'),JSON.stringify(raw))
|
||
|
|
expect(() => restoreOwnedBusinessPending('actor',no,'operations')).toThrow('损坏')
|
||
|
|
})
|
||
|
|
it('looks up an unknown write first and retries only the unchanged original body on explicit not-found', async () => {
|
||
|
|
const pending = newOwnedBusinessPending('actor', no, 'SAVE_OPERATIONS', operation()); pending.attempted = true
|
||
|
|
const service = client(), persist = vi.fn()
|
||
|
|
service.getOwnedBusinessCommand.mockRejectedValue(new ApiResponseError('not found','A0404',404))
|
||
|
|
await recoverOwnedBusinessCommand(pending,persist,() => {},service)
|
||
|
|
expect(service.getOwnedBusinessCommand).toHaveBeenCalledWith(no,'original-key')
|
||
|
|
expect(service.saveOwnedBusinessOperations).toHaveBeenCalledExactlyOnceWith(no,operation())
|
||
|
|
expect(pending.body).toEqual(operation()); expect(pending.body?.recordVersion).toBe('0')
|
||
|
|
})
|
||
|
|
it('retains a proven success through failed fresh reads without writing it a second time', async () => {
|
||
|
|
const pending = newOwnedBusinessPending('actor', no, 'SAVE_OPERATIONS', operation()), service = client()
|
||
|
|
service.getOwnedBusiness.mockRejectedValueOnce(new Error('read offline'))
|
||
|
|
await expect(recoverOwnedBusinessCommand(pending,() => {},() => {},service)).rejects.toThrow('read offline')
|
||
|
|
expect(pending.confirmed?.recordVersion).toBe('1')
|
||
|
|
await recoverOwnedBusinessCommand(pending,() => {},() => {},service)
|
||
|
|
expect(service.saveOwnedBusinessOperations).toHaveBeenCalledTimes(1); expect(service.getOwnedBusinessCommand).not.toHaveBeenCalled()
|
||
|
|
})
|
||
|
|
it('never retransmits a file for an unknown upload, including command not-found', async () => {
|
||
|
|
const pending = newOwnedBusinessPending('actor',no,'UPLOAD_PHOTO',null,'FUEL_START'); pending.attempted = true
|
||
|
|
const service = client(); service.getOwnedBusinessCommand.mockRejectedValue(new ApiResponseError('not found','A0404',404))
|
||
|
|
await expect(recoverOwnedBusinessCommand(pending,() => {},() => {},service)).rejects.toThrow('禁止重复上传')
|
||
|
|
expect(service.uploadOwnedBusinessPhoto).not.toHaveBeenCalled(); expect(service.bindOwnedBusinessPhoto).not.toHaveBeenCalled(); expect(pending.key).toBeTruthy()
|
||
|
|
})
|
||
|
|
it('keeps the prepared file and exact binding key through unknown binding and later replacement', async () => {
|
||
|
|
const photo: OwnedBusinessPhoto = { slot:'FUEL_START',category:'FUEL',fileId:'9007199254740993',fileName:'fuel.png',contentType:'image/png',sizeBytes:'5',uploadedAt:'2026-10-20T10:00:00Z',savedAt:'2026-10-20T10:01:00Z' }
|
||
|
|
const pending = newOwnedBusinessPending('actor',no,'BIND_PHOTO',{recordVersion:'0',idempotencyKey:'bind-key',fileId:photo.fileId},photo.slot)
|
||
|
|
pending.attempted = true
|
||
|
|
const service = client(); const bound = ownedBusinessFixture(); bound.row.recordVersion='1'; bound.photos=[photo]
|
||
|
|
const original = ownedBusinessCommandFixture({action:'BIND_PHOTO',idempotencyKey:'bind-key',result:bound})
|
||
|
|
service.getOwnedBusinessCommand.mockResolvedValue(original)
|
||
|
|
const current = ownedBusinessFixture();current.row.recordVersion='2';current.photos=[{...photo,fileId:'9007199254740994'}]
|
||
|
|
service.getOwnedBusiness.mockResolvedValue(current)
|
||
|
|
const result = await recoverOwnedBusinessCommand(pending,() => {},() => {},service)
|
||
|
|
expect(result.detail?.photos[0]?.fileId).toBe('9007199254740994')
|
||
|
|
expect(pending.body).toEqual({recordVersion:'0',idempotencyKey:'bind-key',fileId:photo.fileId})
|
||
|
|
expect(service.uploadOwnedBusinessPhoto).not.toHaveBeenCalled();expect(service.bindOwnedBusinessPhoto).not.toHaveBeenCalled()
|
||
|
|
})
|
||
|
|
it('refuses wrong command identities, stale reads, definite rejection retries and scope changes', async () => {
|
||
|
|
const pending = newOwnedBusinessPending('actor',no,'SAVE_OPERATIONS',operation()), service=client()
|
||
|
|
expect(() => acceptOwnedBusinessCommand(pending,ownedBusinessCommandFixture({dispatchNo:'another'}))).toThrow('不一致')
|
||
|
|
pending.command=ownedBusinessCommandFixture({recordVersion:'9007199254740993'})
|
||
|
|
await expect(recoverOwnedBusinessCommand(pending,() => {},() => {},service)).rejects.toThrow('早于')
|
||
|
|
pending.rejected=true;await expect(recoverOwnedBusinessCommand(pending,() => {},() => {},service)).rejects.toThrow('明确拒绝')
|
||
|
|
pending.rejected=false;await expect(recoverOwnedBusinessCommand(pending,() => {},() => {throw new Error('scope changed')},service)).rejects.toThrow('scope changed')
|
||
|
|
expect(service.saveOwnedBusinessOperations).not.toHaveBeenCalled()
|
||
|
|
})
|
||
|
|
})
|