|
| 1 | +'use strict'; |
| 2 | +const assert = require('node:assert/strict'); |
| 3 | +const http = require('node:http'); |
| 4 | +const { test } = require('node:test'); |
| 5 | +const { BaseRegistryClient, BRegPreparedProposal } = process.env.BREG_CLIENT_PACKAGE |
| 6 | + ? require(process.env.BREG_CLIENT_PACKAGE).breg : require('..'); |
| 7 | + |
| 8 | +const id = '00000000-0000-4000-8000-000000000001'; |
| 9 | +const revision = `sha256:${'a'.repeat(64)}`; |
| 10 | +const metadata = {id: 'proposal-json', version: '1', revision, metadataVersion: '2', recordTypes: [], operations: [], |
| 11 | + proposals: {method: 'GET', path: '/v1/proposals', accessProfiles: ['author'], |
| 12 | + actions: [{actionId: 'correct', accessProfile: 'author', operations: ['get', 'list'], readableInputs: [], readableContextFields: []}], |
| 13 | + get: {method: 'GET', path: '/v1/proposals/{proposal_id}', accessProfiles: ['author']}}}; |
| 14 | +const proposal = {id, actionId: 'correct', actionRevision: revision, submissionVersion: 1, revision: 1, state: 'draft', links: { |
| 15 | + self: {method: 'GET', path: `/v1/proposals/${id}`, accessProfile: 'author', operation: 'get'}, |
| 16 | + edit: {method: 'POST', path: `/v1/proposals/${id}/edit`, accessProfile: 'author', operation: 'edit', ifMatch: '"draft-condition"'}, |
| 17 | +}}; |
| 18 | +const inputJson = '{"nullable":null,"nested":{"wide":9223372036854775807,"decimal":"0.7500","values":[9223372036854775807,null,{"amount":"0.7500"}]}}'; |
| 19 | + |
| 20 | +test('exact Proposal JSON methods preserve nested values and recovery authority through native HTTP', async () => { |
| 21 | + const requests = []; |
| 22 | + let current = JSON.stringify(proposal).replace('"links":', `"input":${inputJson},"links":`); |
| 23 | + const server = http.createServer(async (request, response) => { |
| 24 | + let body = ''; for await (const chunk of request) body += chunk; |
| 25 | + requests.push({url: request.url, body, headers: request.headers}); |
| 26 | + response.writeHead(200, {'content-type': 'application/json', 'cache-control': 'no-store', vary: 'authorization, accept', |
| 27 | + traceparent: '00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01'}); |
| 28 | + if (request.url.startsWith('/tenant/v1/registry')) return response.end(JSON.stringify(metadata)); |
| 29 | + if (request.url.startsWith('/tenant/v1/proposals?')) return response.end(`{"items":[${current}],"nextCursor":"next-page"}`); |
| 30 | + if (request.url.includes('/edit?')) { |
| 31 | + current = current.replace('"revision":1', '"revision":2').replace(inputJson, body.slice('{"input":'.length, -1)); |
| 32 | + } |
| 33 | + response.end(current); |
| 34 | + }); |
| 35 | + await new Promise(resolve => server.listen(0, '127.0.0.1', resolve)); |
| 36 | + try { |
| 37 | + const client = new BaseRegistryClient({baseUrl: `http://127.0.0.1:${server.address().port}/tenant/`}); |
| 38 | + assert.equal(typeof client.prepareProposalJson, 'function'); |
| 39 | + assert.equal(typeof client.listProposalsJson, 'function'); |
| 40 | + const contract = await client.registryContract('author'); |
| 41 | + const collection = contract.selectProposals('author'); |
| 42 | + const page = await client.listProposalsJson(collection, '{"action":"correct","limit":1}'); |
| 43 | + assert.equal(page.nextCursor, 'next-page'); |
| 44 | + assert.match(page.items[0].inputJson, /9223372036854775807/); |
| 45 | + assert.match(page.items[0].inputJson, /"decimal":"0.7500"/); |
| 46 | + const url = new URL(requests.at(-1).url, 'http://fixture.invalid'); |
| 47 | + assert.equal(url.pathname, '/tenant/v1/proposals'); |
| 48 | + assert.equal(url.searchParams.get('accessProfile'), 'author'); |
| 49 | + assert.equal(url.searchParams.get('action'), 'correct'); |
| 50 | + assert.equal(url.searchParams.get('limit'), '1'); |
| 51 | + const draft = page.items[0]; |
| 52 | + const before = requests.length; |
| 53 | + for (const body of ['{"input":{"wide":1,"wide":2}}', '{"input":{"wide":1e9999}}', '{"input":[]}', '[]']) { |
| 54 | + assert.throws(() => client.prepareProposalJson(draft, 'edit', body, 'invalid'), error => error.kind === 'invalid-request' && !error.outcomeUnknown); |
| 55 | + } |
| 56 | + assert.throws(() => client.prepareProposalJson(draft, 'apply', '{}', 'illegal-transition'), error => error.kind === 'invalid-request'); |
| 57 | + assert.throws(() => client.prepareProposal(draft, 'edit', {input: {wide: Number.MAX_SAFE_INTEGER + 1}}, 'unsafe'), error => error.kind === 'invalid-request'); |
| 58 | + for (const query of ['{"limit":1,"limit":2}', '{"limit":0}', '{"unknown":true}', '{"limit":"one"}']) { |
| 59 | + await assert.rejects(client.listProposalsJson(collection, query), error => error.kind === 'invalid-request', query); |
| 60 | + } |
| 61 | + assert.equal(requests.length, before, 'invalid JSON, query, structured unsafe numbers and transitions fail before I/O'); |
| 62 | + assert.throws(() => client.prepareProposalJson(draft, 'edit', `{"input":${inputJson}}`, 'inexact-binary64'), error => error.kind === 'invalid-request'); |
| 63 | + const exactEditInputJson = inputJson.replaceAll('9223372036854775807', '9007199254740992'); |
| 64 | + const attempt = client.prepareProposalJson(draft, 'edit', `{"input":${exactEditInputJson}}`, 'exact-edit-key'); |
| 65 | + const bytes = attempt.toBytes(); |
| 66 | + const restored = BRegPreparedProposal.fromBytes(bytes); |
| 67 | + await assert.rejects(client.executeProposal(restored), error => error.kind === 'invalid-request'); |
| 68 | + assert.equal(requests.length, before, 'persisted evidence requires fresh matching Proposal authority'); |
| 69 | + const recovered = client.recoverProposal(await client.readProposal(draft), restored); |
| 70 | + assert.deepEqual(recovered.toBytes(), bytes); |
| 71 | + const edited = await client.executeProposal(recovered); |
| 72 | + assert.match(edited.inputJson, /9007199254740992/); |
| 73 | + const sent = requests.at(-1); |
| 74 | + assert.equal(sent.url, `/tenant/v1/proposals/${id}/edit?accessProfile=author`); |
| 75 | + assert.equal(sent.headers['idempotency-key'], 'exact-edit-key'); |
| 76 | + assert.equal(sent.headers['if-match'], '"draft-condition"'); |
| 77 | + assert.match(sent.body, /"wide":9007199254740992/); |
| 78 | + assert.match(sent.body, /"decimal":"0.7500"/); |
| 79 | + assert.match(sent.body, /"values":\[9007199254740992,null,\{"amount":"0.7500"\}\]/); |
| 80 | + assert.equal(JSON.parse(sent.body).input.nullable, null); |
| 81 | + assert.ok(!Object.hasOwn(JSON.parse(sent.body).input, 'absent')); |
| 82 | + } finally { await new Promise(resolve => server.close(resolve)); } |
| 83 | +}); |
0 commit comments