test: isolate fee void ACK loss and preserve wallet delivery
This commit is contained in:
@@ -19,7 +19,7 @@ const event = (id = targetId, planId = '2100000000000000001') => ({ eventId: id,
|
||||
payload: { loadingPlanId: planId, syntheticPrivateField: secret } })
|
||||
const close = server => new Promise(resolve => server.close(resolve))
|
||||
|
||||
async function fixture(selector = ['--loading-plan-id', '2100000000000000001'], eventType = 'LoadingCompleted') {
|
||||
async function fixture(selector = ['--loading-plan-id', '2100000000000000001'], eventType = 'LoadingCompleted', endpoint = '/internal/events') {
|
||||
const calls = []
|
||||
const applied = new Set()
|
||||
let invalidAck = false
|
||||
@@ -28,7 +28,7 @@ async function fixture(selector = ['--loading-plan-id', '2100000000000000001'],
|
||||
for await (const chunk of request) chunks.push(chunk)
|
||||
const raw = Buffer.concat(chunks).toString()
|
||||
const envelope = JSON.parse(raw)
|
||||
calls.push({ raw, authorization: request.headers.authorization, eventId: envelope.eventId })
|
||||
calls.push({ raw, authorization: request.headers.authorization, eventId: envelope.eventId, path: request.url })
|
||||
const outcome = applied.has(envelope.eventId) ? 'DUPLICATE' : 'APPLIED'
|
||||
applied.add(envelope.eventId)
|
||||
const body = JSON.stringify({ code: '200', data: { eventId: invalidAck ? otherId : envelope.eventId,
|
||||
@@ -40,7 +40,7 @@ async function fixture(selector = ['--loading-plan-id', '2100000000000000001'],
|
||||
const directory = await mkdtemp(join(tmpdir(), 'event-fault-offline-'))
|
||||
const log = join(directory, 'evidence.jsonl')
|
||||
const child = spawn(process.env.PYTHON || 'python', [script, '--upstream',
|
||||
`http://127.0.0.1:${consumer.address().port}/internal/events`, '--event-type', eventType,
|
||||
`http://127.0.0.1:${consumer.address().port}${endpoint}`, '--event-type', eventType,
|
||||
...selector, '--log', log], { env: { ...process.env, EVENT_FAULT_CONTROL_TOKEN: controlToken },
|
||||
stdio: ['ignore', 'pipe', 'pipe'] })
|
||||
let output = ''; let errors = ''
|
||||
@@ -58,12 +58,12 @@ async function fixture(selector = ['--loading-plan-id', '2100000000000000001'],
|
||||
child.once('error', error => { clearTimeout(timer); reject(error) })
|
||||
child.once('exit', code => { if (!output) { clearTimeout(timer); reject(new Error(`Proxy exit ${code}`)) } })
|
||||
})
|
||||
const dataUrl = `http://127.0.0.1:${ready.dataPort}/internal/events`
|
||||
const dataUrl = `http://127.0.0.1:${ready.dataPort}${endpoint}`
|
||||
const controlUrl = `http://127.0.0.1:${ready.controlPort}`
|
||||
return { calls, applied, log, invalidAck: value => { invalidAck = value },
|
||||
send: async envelope => {
|
||||
send: async (envelope, path = endpoint) => {
|
||||
const sent = JSON.stringify(envelope)
|
||||
const response = await fetch(dataUrl, { method: 'POST', body: sent,
|
||||
const response = await fetch(new URL(path, dataUrl), { method: 'POST', body: sent,
|
||||
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${secret}` } })
|
||||
return { status: response.status, raw: await response.text(), headers: response.headers, sent }
|
||||
},
|
||||
@@ -85,6 +85,26 @@ async function fixture(selector = ['--loading-plan-id', '2100000000000000001'],
|
||||
}
|
||||
}
|
||||
|
||||
test('fee fault preserves wallet endpoint bytes and auth without selecting its body', async t => {
|
||||
const proxy = await fixture(['--fee-source-id', '2100000000000000001', '--source-version', '1'],
|
||||
'BusinessFeeSourceChanged', '/internal/finance/v1/business-fee-source-events')
|
||||
t.after(() => proxy.stop())
|
||||
await proxy.control('/mode', { mode: 'FAULT' })
|
||||
const envelope = { ...event(), eventType: 'BusinessFeeSourceChanged',
|
||||
payload: { sourceFeeId: '2100000000000000001', sourceVersion: 1, action: 'VOIDED' } }
|
||||
const walletPath = '/internal/finance/v1/wallet-command-events'
|
||||
const wallet = await proxy.send(envelope, walletPath)
|
||||
assert.equal(wallet.status, 200)
|
||||
assert.equal(proxy.calls[0].path, walletPath)
|
||||
assert.equal(proxy.calls[0].raw, wallet.sent)
|
||||
assert.equal(proxy.calls[0].authorization, `Bearer ${secret}`)
|
||||
assert.equal((await (await proxy.control('/status')).json()).attempt, 0)
|
||||
assert.equal((await proxy.send(envelope, '/internal/finance/v1/unknown')).status, 404)
|
||||
assert.equal(proxy.calls.length, 1)
|
||||
assert.equal((await proxy.send(envelope)).status, 503)
|
||||
assert.equal((await (await proxy.control('/status')).json()).attempt, 1)
|
||||
})
|
||||
|
||||
test('ACK-loss proxy only targets one event and forwards committed upstream results before dropping ACK', async t => {
|
||||
const proxy = await fixture()
|
||||
t.after(() => proxy.stop())
|
||||
@@ -260,3 +280,33 @@ test('delivery selection rejects malformed IDs or a second selector before openi
|
||||
assert.ok(!result.stdout.includes('"ready"'))
|
||||
}
|
||||
})
|
||||
|
||||
test('fee void selector leaves publication, other sources and versions unchanged while recovering exact VOIDED ACK', async t => {
|
||||
const sourceFeeId='2100000000000000011'
|
||||
const proxy=await fixture(['--fee-source-id',sourceFeeId,'--source-version','2'],'BusinessFeeSourceChanged')
|
||||
t.after(()=>proxy.stop())
|
||||
await proxy.control('/mode',{mode:'FAULT'})
|
||||
const voided={...event(),eventType:'BusinessFeeSourceChanged',payload:{sourceFeeId,sourceVersion:2,action:'VOIDED',totalAmount:30}}
|
||||
for(const payload of [{...voided.payload,action:'PUBLISHED'},{...voided.payload,sourceVersion:1},{...voided.payload,sourceFeeId:'2100000000000000012'},
|
||||
{...voided.payload,action:'VOID'}]) assert.equal((await proxy.send({...voided,eventId:otherId,payload})).status,200)
|
||||
assert.equal((await proxy.send(voided)).status,503)
|
||||
assert.equal((await proxy.send(voided)).status,503)
|
||||
assert.equal((await proxy.send({...voided,eventId:samePlanOtherId})).status,200)
|
||||
const before=proxy.calls.length
|
||||
assert.equal((await proxy.send({...voided,payload:{...voided.payload,sourceVersion:3}})).status,409)
|
||||
assert.equal(proxy.calls.length,before)
|
||||
await proxy.control('/mode',{mode:'PASS'})
|
||||
assert.equal(JSON.parse((await proxy.send(voided)).raw).data.outcome,'DUPLICATE')
|
||||
const state=await (await proxy.control('/status')).json()
|
||||
assert.deepEqual(state.records.filter(r=>r.errorCode!=='TARGET_EVENT_HASH_CONFLICT').map(r=>r.outcome),['APPLIED','DUPLICATE','DUPLICATE'])
|
||||
})
|
||||
|
||||
test('fee void selector requires a bounded version paired with only its own source selector',()=>{
|
||||
for(const args of [['--fee-source-id','11'],['--event-id',targetId,'--source-version','1'],['--fee-source-id','0','--source-version','1'],
|
||||
['--fee-source-id','11','--source-version','0'],['--fee-source-id','11','--source-version','2147483648'],
|
||||
['--fee-source-id','../11','--source-version','1'],['--fee-source-id','11','--source-version','1','--delivery-id','88']]) {
|
||||
const result=spawnSync(process.env.PYTHON||'python',[script,'--upstream','http://127.0.0.1:1/internal/events','--event-type','BusinessFeeSourceChanged',...args],
|
||||
{encoding:'utf8',timeout:5000,env:{...process.env,EVENT_FAULT_CONTROL_TOKEN:controlToken}})
|
||||
assert.equal(result.status,2);assert.ok(!result.stdout.includes('"ready"'))
|
||||
}
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user