Files
baiyue-firstleg/scripts/admin-test-env-modules/phase3-evidence-transport-contract.test.mjs
T

87 lines
4.9 KiB
JavaScript

import test from 'node:test'
import assert from 'node:assert/strict'
import { createHash } from 'node:crypto'
import { uploadEvidence } from './phase3-loading.mjs'
const context = () => ({ marker: 'CURRENT-SEAL', options: { gateway: 'https://offline.invalid' },
pdaToken: 'secret-test-token', token: 'different-admin-token', createdResources: {} })
const invoke = ctx => uploadEvidence(ctx, '2105979722985902081', '193000000000200001', 'SEAL', 0, 'SEAL', { sealNo: 'SEAL-UNIQUE' })
async function withTransport(handler, check) {
const originalFetch = globalThis.fetch, originalTimeout = globalThis.setTimeout
const waits = []
globalThis.fetch = handler
globalThis.setTimeout = (callback, delay) => { waits.push(delay); queueMicrotask(callback); return 0 }
try { await check(waits) } finally { globalThis.fetch = originalFetch; globalThis.setTimeout = originalTimeout }
}
function lost(code = 'ECONNRESET') {
return new TypeError('fetch failed https://private-host.invalid secret-test-token', { cause: { code, address: 'private-host.invalid' } })
}
test('committed upload with lost response replays frozen content/key and creates exactly one modeled evidence row', async () => {
const ctx = context(), records = new Map(), calls = []
let mutations = 0
await withTransport(async (url, options) => {
const file = options.body.get('file')
const hash = createHash('sha256').update(`${url}|${file.name}|${file.type}|${file.size}|`)
.update(Buffer.from(await file.arrayBuffer())).digest('hex')
const key = options.headers['Idempotency-Key']
calls.push({ url, key, hash, auth: options.headers.Authorization, body: options.body })
if (records.has(key)) assert.equal(records.get(key).hash, hash, 'same-key different payload must be rejected')
else { records.set(key, { hash, evidenceId: 'persisted-evidence' }); mutations++ }
if (calls.length === 1) {
ctx.marker = 'CHANGED'; ctx.pdaToken = 'new-token'; ctx.options.gateway = 'https://changed.invalid'
throw lost()
}
return new Response(JSON.stringify({ code: '200', data: { evidenceId: records.get(key).evidenceId } }), { status: 200 })
}, async waits => {
const result = await invoke(ctx)
assert.equal(result.body.data.evidenceId, 'persisted-evidence')
assert.equal(mutations, 1); assert.equal(records.size, 1); assert.equal(calls.length, 2)
assert.deepEqual(calls[0], calls[1]); assert.deepEqual(waits, [250])
assert.deepEqual(result.transportAttempts.map(item => item.outcome), ['NO_HTTP_RESPONSE', 'HTTP_RESPONSE'])
assert.equal(result.transportAttempts[0].causeCode, 'ECONNRESET')
const evidence = JSON.stringify(ctx.createdResources)
for (const secret of ['secret-test-token', 'new-token', 'private-host', 'offline.invalid', 'changed.invalid']) assert.equal(evidence.includes(secret), false)
})
})
test('persistent no-response failures stop at three attempts with bounded backoff and sanitized cause', async () => {
let calls = 0
await withTransport(async () => { calls++; throw lost('UND_ERR_CONNECT_TIMEOUT') }, async waits => {
const result = await invoke(context())
assert.equal(calls, 3); assert.equal(result.http, 0); assert.equal(result.body, null)
assert.deepEqual(waits, [250, 500]); assert.equal(result.transportAttempts.length, 3)
assert.match(result.raw, /UND_ERR_CONNECT_TIMEOUT/)
assert.doesNotMatch(JSON.stringify(result), /private-host|secret-test-token|address/)
})
})
for (const [http, code] of [[200, 'A0409'], [200, 'A0422'], [503, 'S0503'], [403, 'A0403']]) {
test(`HTTP ${http} business ${code} is returned without retry`, async () => {
let calls = 0
await withTransport(async () => { calls++; return new Response(JSON.stringify({ code, message: 'rejected', requestId: 'request' }), { status: http }) }, async waits => {
const result = await invoke(context())
assert.equal(calls, 1); assert.deepEqual(waits, []); assert.equal(result.http, http)
assert.equal(result.body.code, code); assert.equal(result.requestId, 'request')
})
})
}
test('received HTTP response with unreadable body is not reclassified as no-response retry', async () => {
let calls = 0
await withTransport(async () => { calls++; return { status: 502, headers: new Headers({ 'x-request-id': 'edge-request' }),
text: async () => { throw lost() } } }, async waits => {
const result = await invoke(context())
assert.equal(calls, 1); assert.deepEqual(waits, []); assert.equal(result.http, 502)
assert.equal(result.requestId, 'edge-request'); assert.equal(result.transportAttempts[0].outcome, 'RESPONSE_READ_FAILED')
})
})
test('unrecognized cause code containing sensitive material is omitted', async () => {
await withTransport(async () => { throw lost('secret-test-token https://private-host.invalid') }, async () => {
const result = await invoke(context())
assert.ok(result.transportAttempts.every(item => item.causeCode === null))
assert.doesNotMatch(JSON.stringify(result), /secret-test-token|private-host/)
})
})