test(fleet): journal prerequisite writes and stop on unknown results
This commit is contained in:
@@ -23,6 +23,63 @@ const id = value => value == null ? '' : String(value)
|
||||
const success = response => response?.http === 200 && id(response?.body?.code) === '200'
|
||||
const key = () => `fleet-${randomUUID()}`
|
||||
|
||||
/** Install before login/prerequisites. A caught unknown write must not permit another request. */
|
||||
export function guardFleetWrites(context, checkpoint) {
|
||||
let stopped = null, sequence = 0
|
||||
for (const name of ['request', 'requestWithBody', 'requestBinary']) {
|
||||
const original = context[name]
|
||||
if (typeof original !== 'function') continue
|
||||
context[name] = async function (method, path, body, token, headers) {
|
||||
if (stopped) throw stopped
|
||||
if (['GET', 'HEAD', 'OPTIONS'].includes(String(method).toUpperCase()))
|
||||
return original.apply(this, arguments)
|
||||
const entry = { sequence: ++sequence, method, path, status: 'SENDING', startedAt: new Date().toISOString(),
|
||||
headers, body }
|
||||
// Multipart delegates to requestWithBody, so it is journaled exactly once, after construction.
|
||||
if (body instanceof FormData) {
|
||||
entry.body = { multipart: [] }
|
||||
for (const [field, value] of body.entries()) entry.body.multipart.push(typeof value === 'string'
|
||||
? { field, value } : { field, name: value.name, type: value.type,
|
||||
base64: Buffer.from(await value.arrayBuffer()).toString('base64') })
|
||||
}
|
||||
try { await checkpoint(entry) } catch (error) { stopped = error; throw error }
|
||||
try {
|
||||
const response = await original.apply(this, arguments)
|
||||
const http = response?.http, payload = response?.body, code = id(payload?.code)
|
||||
const definite = Number.isInteger(http) && http >= 200 && http < 500
|
||||
&& payload && typeof payload === 'object' && !Array.isArray(payload) && response.requestId && response.traceId
|
||||
&& (http === 200 && code === '200' || /^[A-Z]\d{4}$/.test(code) && (http === 200 || http >= 400))
|
||||
Object.assign(entry, { status: definite ? 'RESPONDED' : 'UNKNOWN_RESULT', http, code,
|
||||
requestId: response?.requestId, traceId: response?.traceId, finishedAt: new Date().toISOString() })
|
||||
if (!definite) stopped = Error(`Unknown write outcome: ${method} ${path}; preserve original request, do not resend`)
|
||||
await checkpoint(entry)
|
||||
if (stopped) throw stopped
|
||||
return response
|
||||
} catch (error) {
|
||||
stopped ||= error
|
||||
if (entry.status === 'SENDING') {
|
||||
entry.status = 'UNKNOWN_RESULT'; entry.finishedAt = new Date().toISOString()
|
||||
await checkpoint(entry)
|
||||
}
|
||||
throw stopped
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function redactFleetWrite(value, secrets = []) {
|
||||
const sensitive = name => name !== 'draftToken' && /password|authorization|cookie|privateKey|token$|^(?:loginId|username)$/i.test(name)
|
||||
if (Array.isArray(value)) return value.map(item => redactFleetWrite(item, secrets))
|
||||
if (value && typeof value === 'object' && typeof value.field === 'string' && sensitive(value.field))
|
||||
return { field: value.field, value: '[REDACTED]' }
|
||||
if (value && typeof value === 'object') return Object.fromEntries(Object.entries(value).map(([name, item]) => [name,
|
||||
sensitive(name)
|
||||
? '[REDACTED]' : redactFleetWrite(item, secrets)]))
|
||||
if (typeof value === 'string') return secrets.filter(secret => typeof secret === 'string' && secret.length)
|
||||
.reduce((text, secret) => text.split(secret).join('[REDACTED]'), value)
|
||||
return value
|
||||
}
|
||||
|
||||
export function parseFleetArguments(args) {
|
||||
const config = { execute: false, environment: 'TEST', runId: 'plan', scenario: 'handover' }
|
||||
let action = null
|
||||
@@ -498,6 +555,14 @@ export async function runFleetAcceptance(config, { env = process.env, readers, c
|
||||
pdaUsername: env.ADMIN_PDA_USERNAME, pdaPassword: env.ADMIN_PDA_PASSWORD, merchantUsername: '', merchantPassword: '',
|
||||
markerNamespace: createHash('sha256').update(config.runId).digest('hex').slice(0, 12) })
|
||||
context = createContext ? createContext(options) : new AcceptanceRun(options)
|
||||
let writeCheckpoint = 0
|
||||
guardFleetWrites(context, entry => {
|
||||
const payload = JSON.stringify(redactFleetWrite({ marker: context.marker, runId: config.runId, request: entry },
|
||||
[...secrets, context.token, context.pdaToken, context.refreshToken]), null, 2) + '\n'
|
||||
const descriptor = openSync(join(output, 'evidence', `write-${String(++writeCheckpoint).padStart(6, '0')}.json`), 'wx')
|
||||
try { writeFileSync(descriptor, payload); fsyncSync(descriptor) }
|
||||
finally { closeSync(descriptor) }
|
||||
})
|
||||
context.context.fleetRunId = config.runId
|
||||
context.context.fleetScenario = config.scenario || 'handover'
|
||||
context.context.saveFleetHandoverFixture = fixture => save('handover-fixture.json', fixture)
|
||||
|
||||
Reference in New Issue
Block a user