From 93eb19d028c32d78f869c388b8f8fa54a351d00c Mon Sep 17 00:00:00 2001 From: robbond Date: Wed, 25 Mar 2026 10:49:37 +0000 Subject: [PATCH] test(phase22): add client utility behaviour coverage --- memory-bank/change-log.md | 36 ++++ tests/phase22/client-utils-behaviour.test.cjs | 173 ++++++++++++++++++ tests/phase22/index.test.cjs | 2 + 3 files changed, 211 insertions(+) create mode 100644 tests/phase22/client-utils-behaviour.test.cjs diff --git a/memory-bank/change-log.md b/memory-bank/change-log.md index 352edf27..776b655b 100644 --- a/memory-bank/change-log.md +++ b/memory-bank/change-log.md @@ -2230,3 +2230,39 @@ Validation: Follow-ups: - Optional next bounded slice: add additional phase22 suites (e.g., shared-client utility behaviour tests) under this aggregate runner as migration coverage expands. + +--- + +### CL-064: TASK22260 next slice — phase22 client utility behaviour coverage expansion + +date: 2026-03-25 +author: Cline +scope: `tests/phase22/{client-utils-behaviour,index}.test.cjs` +type: change +rationale: Execute the next bounded phase22 slice by adding focused behavioural coverage for shared client utilities to reduce regression risk as direct-service/client-wrapper migration continues. +impact: Improves confidence in shared client helper contracts (`endpointClient`, `relayClient`) and keeps phase22 aggregate suite aligned with new coverage. +status: completed + +Summary: + +- Added new suite: `tests/phase22/client-utils-behaviour.test.cjs`. +- Added assertions for shared client utility behavior: + - `endpointClient.getJson` returns `axios.get(...).data` + - `endpointClient.requestJson` returns `axios(config).data` + - `relayClient.buildHashedQueryUrl` appends browser hash-service response + - server fallback path uses `hashAPIPath` when `HASHKEY` is present + - browser/no-HASHKEY failure path rethrows hash-service error +- Updated `tests/phase22/index.test.cjs` to include the new client-utils suite in the phase aggregate runner. + +Validation: + +- `node tests/phase22/client-utils-behaviour.test.cjs` -> pass (5/5) +- `node tests/phase22/index.test.cjs` -> pass + - core-token: 2/2 + - client-utils: 5/5 + - phase22 combined: pass +- `npm run lint` -> warnings only (pre-existing `react-hooks/exhaustive-deps`; no new lint errors) + +Follow-ups: + +- Optional next bounded slice: add focused phase22 behavioural coverage for any future shared client wrappers introduced beyond `endpointClient`/`relayClient`. diff --git a/tests/phase22/client-utils-behaviour.test.cjs b/tests/phase22/client-utils-behaviour.test.cjs new file mode 100644 index 00000000..b20a82df --- /dev/null +++ b/tests/phase22/client-utils-behaviour.test.cjs @@ -0,0 +1,173 @@ +const fs = require("fs"); +const path = require("path"); +const vm = require("vm"); +const assert = require("assert"); + +const rootDir = path.resolve(__dirname, "..", ".."); + +const loadEndpointClientModule = (injected = {}) => { + const filePath = path.join( + rootDir, + "actions", + "clients", + "endpointClient.js" + ); + let source = fs.readFileSync(filePath, "utf8"); + + source = source.replace(/import[\s\S]*?from\s+"[^"]+";\n?/g, ""); + source = source.replace(/export const\s+/g, "const "); + source += "\nmodule.exports = { getJson, requestJson };\n"; + + const context = { + module: { exports: {} }, + exports: {}, + require, + axios: () => { + throw new Error("axios not injected"); + }, + ...injected + }; + + vm.runInNewContext(source, context, { filename: filePath }); + return context.module.exports; +}; + +const loadRelayClientModule = (injected = {}) => { + const filePath = path.join(rootDir, "actions", "clients", "relayClient.js"); + let source = fs.readFileSync(filePath, "utf8"); + + source = source.replace(/import[\s\S]*?from\s+"[^"]+";\n?/g, ""); + source = source.replace(/export const\s+/g, "const "); + source += "\nmodule.exports = { buildHashedQueryUrl };\n"; + + const context = { + module: { exports: {} }, + exports: {}, + require, + axios: { + get: async () => { + throw new Error("axios.get not injected"); + } + }, + hashAPIPath: () => "&hash=fallback", + process: { env: {} }, + ...injected + }; + + vm.runInNewContext(source, context, { filename: filePath }); + return context.module.exports; +}; + +const tests = []; +const test = (name, fn) => tests.push({ name, fn }); + +test("clients/endpointClient getJson returns response.data from axios.get", async () => { + const calls = []; + const axios = { + get: async (url, config) => { + calls.push({ url, config }); + return { data: { ok: true } }; + } + }; + + const mod = loadEndpointClientModule({ axios }); + const result = await mod.getJson("/x", { headers: { a: 1 } }); + + assert.deepStrictEqual(JSON.parse(JSON.stringify(result)), { ok: true }); + assert.strictEqual(calls.length, 1); + assert.strictEqual(calls[0].url, "/x"); + assert.strictEqual(calls[0].config.headers.a, 1); +}); + +test("clients/endpointClient requestJson returns response.data from axios(config)", async () => { + const calls = []; + const axios = async (config) => { + calls.push(config); + return { data: { saved: true } }; + }; + + const mod = loadEndpointClientModule({ axios }); + const result = await mod.requestJson({ method: "post", url: "/y" }); + + assert.deepStrictEqual(JSON.parse(JSON.stringify(result)), { saved: true }); + assert.strictEqual(calls.length, 1); + assert.strictEqual(calls[0].method, "post"); +}); + +test("clients/relayClient buildHashedQueryUrl appends browser hash response", async () => { + const calls = []; + + const mod = loadRelayClientModule({ + axios: { + get: async (url) => { + calls.push(url); + return { data: { hash: "&hash=abc" } }; + } + }, + process: { env: {} } + }); + + const result = await mod.buildHashedQueryUrl("/api/file/deleteblob?x=1"); + + assert.strictEqual(result, "/api/file/deleteblob?x=1&hash=abc"); + assert.strictEqual(calls.length, 1); + assert.strictEqual( + calls[0], + "/api/endpoint/gethash_api?path=%2Fapi%2Ffile%2Fdeleteblob%3Fx%3D1" + ); +}); + +test("clients/relayClient falls back to hashAPIPath on server when HASHKEY present", async () => { + const mod = loadRelayClientModule({ + axios: { + get: async () => { + throw new Error("network unavailable"); + } + }, + hashAPIPath: () => "&hash=server-fallback", + process: { env: { HASHKEY: "secret" } } + }); + + const result = await mod.buildHashedQueryUrl("/api/file/deleteblob?x=1"); + assert.strictEqual(result, "/api/file/deleteblob?x=1&hash=server-fallback"); +}); + +test("clients/relayClient rethrows when browser hash call fails and no HASHKEY", async () => { + const expected = new Error("hash service down"); + + const mod = loadRelayClientModule({ + axios: { + get: async () => { + throw expected; + } + }, + process: { env: {} } + }); + + await assert.rejects( + () => mod.buildHashedQueryUrl("/api/file/deleteblob?x=1"), + (error) => error === expected + ); +}); + +const run = async () => { + let passed = 0; + + for (const currentTest of tests) { + await currentTest.fn(); + passed += 1; + } + + console.log( + `Phase 22 client-utils tests passed (${passed}/${tests.length}).` + ); +}; + +module.exports = run; + +if (require.main === module) { + run().catch((error) => { + console.error(error); + process.exit(1); + }); +} diff --git a/tests/phase22/index.test.cjs b/tests/phase22/index.test.cjs index b4f274b6..e856ac47 100644 --- a/tests/phase22/index.test.cjs +++ b/tests/phase22/index.test.cjs @@ -1,7 +1,9 @@ const runCoreTokenTests = require("./core-token-behaviour.test.cjs"); +const runClientUtilsTests = require("./client-utils-behaviour.test.cjs"); const run = async () => { await runCoreTokenTests(); + await runClientUtilsTests(); console.log("Phase 22 combined suite passed."); };