diff --git a/packages/casting/test/net-access-fixture.js b/packages/casting/test/net-access-fixture.js index 0426ccb4bd9..2112fe1be10 100644 --- a/packages/casting/test/net-access-fixture.js +++ b/packages/casting/test/net-access-fixture.js @@ -1,5 +1,13 @@ const { stringify: jq } = JSON; +/** + * @file to regenerate + * 1. set RECORDING=true in test-interpose-net-access.js + * 2. run: yarn test test/test-test-interpose-net-access.js --update-snapshots + * 3. for each map in test-test-interpose-net-access.js.md, copy it and + * 4. replace all occurences of => with : and paste as args to Object.fromEntries() + * 5. change RECORDING back to false + */ export const web1 = new Map([ [ jq([ @@ -7,7 +15,7 @@ export const web1 = new Map([ { method: 'POST', body: jq({ - id: 1, + id: 1208387614, method: 'no-such-method', params: [], jsonrpc: '2.0', @@ -31,7 +39,7 @@ export const web1 = new Map([ method: 'POST', body: jq({ jsonrpc: '2.0', - id: 2, + id: 797030719, method: 'abci_query', params: { path: '/cosmos.bank.v1beta1.Query/Balance', @@ -70,7 +78,7 @@ export const web2 = new Map([ method: 'POST', body: jq({ jsonrpc: '2.0', - id: 1, + id: 1757612624, method: 'abci_query', params: { path: '/agoric.vstorage.Query/Children', @@ -103,18 +111,56 @@ export const web2 = new Map([ ]); /** - * capture JSON RPC IO traffic + * @param {string} str + * ack: https://stackoverflow.com/a/7616484 + */ +const hashCode = str => { + let hash = 0; + let i; + let chr; + if (str.length === 0) return hash; + for (i = 0; i < str.length; i += 1) { + chr = str.charCodeAt(i); + // eslint-disable-next-line no-bitwise + hash = (hash << 5) - hash + chr; + // eslint-disable-next-line no-bitwise + hash |= 0; // Convert to 32bit integer + } + return hash; +}; + +/** + * Normalize JSON RPC request ID * - * This was used to generate the fixtures above. + * tendermint-rpc generates ids using ambient access to Math.random() + * So we normalize them to a hash of the rest of the JSON. + * + * Earlier, we tried a sequence number, but it was non-deterministic + * with multiple interleaved requests. + * + * @param {string} argsKey + */ +const normalizeID = argsKey => { + // arbitrary string unlikely to occur in a request. from `pwgen 16 -1` + const placeholder = 'Ajaz1chei7ohnguv'; + + const noid = argsKey.replace(/\\"id\\":\d+/, `\\"id\\":${placeholder}`); + const id = Math.abs(hashCode(noid)); + return noid.replace(placeholder, `${id}`); +}; + +/** + * Wrap `fetch` to capture JSON RPC IO traffic. * * @param {typeof window.fetch} fetch + * returns wraped fetch along with a .web map for use with {@link replayIO} */ export const captureIO = fetch => { const web = new Map(); /** @type {typeof window.fetch} */ // @ts-expect-error mock const f = async (...args) => { - const key = JSON.stringify(args); + const key = normalizeID(JSON.stringify(args)); const resp = await fetch(...args); return { json: async () => { @@ -126,3 +172,25 @@ export const captureIO = fetch => { }; return { fetch: f, web }; }; + +/** + * Replay captured JSON RPC IO. + * + * @param {Map} web map from + * JSON-stringified fetch args to fetched JSON data. + */ +export const replayIO = web => { + /** @type {typeof window.fetch} */ + // @ts-expect-error mock + const f = async (...args) => { + const key = normalizeID(JSON.stringify(args)); + const data = web.get(key); + if (!data) { + throw Error(`no data for ${key}`); + } + return { + json: async () => data, + }; + }; + return f; +}; diff --git a/packages/casting/test/test-interpose-net-access.js b/packages/casting/test/test-interpose-net-access.js index e60f4ab1d88..bcf54166c78 100644 --- a/packages/casting/test/test-interpose-net-access.js +++ b/packages/casting/test/test-interpose-net-access.js @@ -10,32 +10,12 @@ import { Tendermint34Client } from '@cosmjs/tendermint-rpc'; import { QueryClientImpl } from '@agoric/cosmic-proto/vstorage/query.js'; import { makeHttpClient } from '../src/makeHttpClient.js'; -import { captureIO, web1, web2 } from './net-access-fixture.js'; +import { captureIO, replayIO, web1, web2 } from './net-access-fixture.js'; /** @type {import('ava').TestFn>>} */ const test = /** @type {any} */ (anyTest); -/** @param {Map} web */ -const replayIO = web => { - // tendermint-rpc generates ids using ambient access to Math.random() - // So we normalize them to sequence numbers. - let nextID = 0; - const normalizeID = data => - data.replace(/\\"id\\":\d+/, `\\"id\\":${nextID}`); - - /** @type {typeof window.fetch} */ - // @ts-expect-error mock - const f = async (...args) => { - nextID += 1; - const key = normalizeID(JSON.stringify(args)); - const data = web.get(key); - if (!data) throw Error(`no data for ${key}`); - return { - json: async () => data, - }; - }; - return f; -}; +const RECORDING = false; const makeTestContext = async () => { return { fetch: globalThis.fetch }; @@ -93,8 +73,12 @@ const scenario2 = { ], }; -test('vstorage query: Children', async t => { - const fetchMock = replayIO(web2); +test(`vstorage query: Children (RECORDING: ${RECORDING})`, async t => { + const { context: io } = t; + + const { fetch: fetchMock, web } = io.recording + ? captureIO(io.fetch) + : { fetch: replayIO(web2), web: new Map() }; const rpcClient = makeHttpClient(scenario2.endpoint, fetchMock); const tmClient = await Tendermint34Client.create(rpcClient); @@ -103,25 +87,11 @@ test('vstorage query: Children', async t => { const queryService = new QueryClientImpl(rpc); const children = await queryService.Children({ path: '' }); + if (io.recording) { + t.snapshot(web); + } t.deepEqual(children, { children: scenario2.children, pagination: undefined, }); }); - -// Fixtures for the tests above were captured via integration testing like... -test.skip('vstorage query: Data (capture IO)', async t => { - const { context: io } = t; - const { fetch: fetchMock, web } = captureIO(io.fetch); - const rpcClient = makeHttpClient(scenario2.endpoint, fetchMock); - - const tmClient = await Tendermint34Client.create(rpcClient); - const qClient = new QueryClient(tmClient); - const rpc = createProtobufRpcClient(qClient); - const queryService = new QueryClientImpl(rpc); - - const data = await queryService.Data({ path: '' }); - t.deepEqual(data, { value: '' }); - - t.snapshot(web); -});