From 4e964ad1ad7afb6e4f927c5f437bb2097cac7ec2 Mon Sep 17 00:00:00 2001 From: robbond Date: Wed, 25 Mar 2026 14:06:18 +0000 Subject: [PATCH] TASK22269: group migrate remaining account/portal signed GET flows --- actions/clients/signedRequestClient.js | 11 ++++- actions/services/accountDirectService.js | 12 +++--- actions/services/portalDirectService.js | 10 +++-- memory-bank/change-log.md | 39 ++++++++++++++++++ .../phase22/portal-service-behaviour.test.cjs | 41 +++++++++++++++++++ tests/phase7/service-behaviour.test.cjs | 13 ++++-- tests/serviceHarness.cjs | 39 ++++++++++++++++++ 7 files changed, 149 insertions(+), 16 deletions(-) diff --git a/actions/clients/signedRequestClient.js b/actions/clients/signedRequestClient.js index 55aa6191..744c2da1 100644 --- a/actions/clients/signedRequestClient.js +++ b/actions/clients/signedRequestClient.js @@ -1,14 +1,21 @@ import { requestJson } from "./endpointClient"; import { buildHashedQueryUrl } from "./relayClient"; +export const buildSignedUrl = async (queryUrl, config = {}) => { + const { baseUrl = "" } = config; + + return `${baseUrl}${await buildHashedQueryUrl(queryUrl)}`; +}; + const signedRequestJson = async ({ method, queryUrl, data, config = {} }) => { - const signedUrl = await buildHashedQueryUrl(queryUrl); + const { baseUrl, ...requestConfig } = config; + const signedUrl = await buildSignedUrl(queryUrl, { baseUrl }); return requestJson({ method, url: signedUrl, data, - ...config + ...requestConfig }); }; diff --git a/actions/services/accountDirectService.js b/actions/services/accountDirectService.js index 4284b1f9..20a41ec0 100644 --- a/actions/services/accountDirectService.js +++ b/actions/services/accountDirectService.js @@ -1,7 +1,7 @@ import { BASE_URL } from "../core/env"; import { consoleLogger } from "../core/logger"; -import { buildHashedQueryUrl } from "../clients/relayClient"; import { getJson, requestJson } from "../clients/endpointClient"; +import { getSignedJson } from "../clients/signedRequestClient"; export const getPersonalAccount = (contactid) => { return getJson( @@ -78,13 +78,11 @@ export const getPortalLogin = async (emailAddress) => { var queryUrl = "/api/endpoint/getportallogin_api?emailAddress=" + emailAddress; - return getJson(BASE_URL + (await buildHashedQueryUrl(queryUrl))).catch( - (error) => { - consoleLogger(error); + return getSignedJson(queryUrl, { baseUrl: BASE_URL }).catch((error) => { + consoleLogger(error); - return JSON.stringify(error); - } - ); + return JSON.stringify(error); + }); }; export const getPortalLoginProxy = async (emailAddress) => { diff --git a/actions/services/portalDirectService.js b/actions/services/portalDirectService.js index cb063a2d..8b6a9654 100644 --- a/actions/services/portalDirectService.js +++ b/actions/services/portalDirectService.js @@ -1,8 +1,10 @@ import { BASE_URL } from "../core/env"; import { consoleLogger } from "../core/logger"; -import { buildHashedQueryUrl } from "../clients/relayClient"; import { getJson, requestJson } from "../clients/endpointClient"; -import { deleteSignedJson } from "../clients/signedRequestClient"; +import { + deleteSignedJson, + buildSignedUrl +} from "../clients/signedRequestClient"; import { buildFileQuery, withBaseUrl, @@ -207,7 +209,7 @@ export const sendCaseCompleteMessage = async ( inv }); - var signedQueryUrl = await buildHashedQueryUrl(hashQueryPath); + var signedQueryUrl = await buildSignedUrl(hashQueryPath); queryUrl = appendQuerySuffix( queryUrl, @@ -260,7 +262,7 @@ export const sendRepCompleteMessage = async ( } ); - var queryUrl = await buildHashedQueryUrl(hashQueryPath); + var queryUrl = await buildSignedUrl(hashQueryPath); var config = { method: "get", diff --git a/memory-bank/change-log.md b/memory-bank/change-log.md index 489c8b30..18114986 100644 --- a/memory-bank/change-log.md +++ b/memory-bank/change-log.md @@ -2832,3 +2832,42 @@ Validation: Follow-ups: - Candidate map now indicates remaining explicit signed request composition is primarily in account/portal signed GET edge paths (`getPortalLogin`, `sendRepCompleteMessage`, and signed suffix append flow in `sendCaseCompleteMessage`) for future bounded slices. + +--- + +### CL-079: TASK22269 Slice B1.4 — signed GET consolidation bundle (portal + account) + +date: 2026-03-25 +author: Cline +scope: `actions/clients/signedRequestClient.js`, `actions/services/{portalDirectService,accountDirectService}.js`, `tests/{serviceHarness,phase22/portal-service-behaviour,phase7/service-behaviour}.cjs` +type: change +rationale: Continue grouped signed-request migration by consolidating remaining direct signed-GET composition paths onto shared signed helper primitives while preserving route behavior contracts. +impact: Reduces residual signing duplication and standardizes signed URL creation across portal/account service read/message flows. +status: completed + +Summary: + +- Enhanced `signedRequestClient`: + - added `buildSignedUrl(queryUrl, { baseUrl? })` helper for signed URL generation reuse + - updated internal signed request execution to use `buildSignedUrl` +- Migrated account signed GET candidate: + - `accountDirectService.getPortalLogin` now uses `getSignedJson(queryUrl, { baseUrl: BASE_URL })` + - preserved existing error semantics (`consoleLogger` + `JSON.stringify(error)`) +- Migrated portal signed GET candidates: + - `portalDirectService.sendRepCompleteMessage` now uses `buildSignedUrl(hashQueryPath)` + - `portalDirectService.sendCaseCompleteMessage` now uses `buildSignedUrl(hashQueryPath)` + existing signed suffix append behavior + - preserved existing request method/URL shape and catch-path behavior +- Updated test harness and suites: + - `tests/serviceHarness.cjs` now provides defaults for `buildSignedUrl`, `getSignedJson`, `postSignedJson` + - `tests/phase22/portal-service-behaviour.test.cjs` includes assertion for `sendRepCompleteMessage` signed-helper delegation + - `tests/phase7/service-behaviour.test.cjs` account portal-login expectations aligned to request-config path used by shared signed helper + +Validation: + +- `npm run lint` -> pass with warnings only (pre-existing `react-hooks/exhaustive-deps`; no new lint errors) +- `node tests/phase22/index.test.cjs` -> pass +- `node tests/phase7/service-behaviour.test.cjs` -> pass (13/13) + +Follow-ups: + +- Remaining special-case signed pattern is now primarily the signed-suffix append composition in `sendCaseCompleteMessage` (already using shared `buildSignedUrl`), with broader module migrations to be planned in future bounded slices. diff --git a/tests/phase22/portal-service-behaviour.test.cjs b/tests/phase22/portal-service-behaviour.test.cjs index cc386e80..26ac9b13 100644 --- a/tests/phase22/portal-service-behaviour.test.cjs +++ b/tests/phase22/portal-service-behaviour.test.cjs @@ -134,6 +134,47 @@ test("portal/sendRepCompleteMessage rejects when hash signing fails before reque assert.strictEqual(logger.calls.length, 0); }); +test("portal/sendRepCompleteMessage requests signed URL from shared signed helper", async () => { + const axios = createAxiosMock(); + const logger = createLoggerMock(); + + const signedCalls = []; + const requestCalls = []; + + const portal = loadServiceModule("portalDirectService.js", { + axios, + BASE_URL: "", + consoleLogger: logger.consoleLogger, + buildSignedUrl: async (queryUrl) => { + signedCalls.push(queryUrl); + return `${queryUrl}&hash=signed-portal`; + }, + requestJson: async (config) => { + requestCalls.push(config); + return { ok: true }; + } + }); + + const result = await portal.sendRepCompleteMessage( + "container-x", + "CASE-99", + "rep-a" + ); + + assert.deepStrictEqual(normalize(result), { ok: true }); + assert.strictEqual(signedCalls.length, 1); + assert.strictEqual( + signedCalls[0], + "/api/file/createrepcompletemessage_api?container=container-x&tempcaseref=CASE-99&repid=rep-a" + ); + assert.strictEqual(requestCalls.length, 1); + assert.strictEqual(requestCalls[0].method, "get"); + assert.strictEqual( + requestCalls[0].url, + "/api/file/createrepcompletemessage_api?container=container-x&tempcaseref=CASE-99&repid=rep-a&hash=signed-portal" + ); +}); + test("portal/deleteWatchedCases logs and returns undefined when signed delete fails", async () => { const axios = createAxiosMock(); const logger = createLoggerMock(); diff --git a/tests/phase7/service-behaviour.test.cjs b/tests/phase7/service-behaviour.test.cjs index fb008ae4..dbe91bb3 100644 --- a/tests/phase7/service-behaviour.test.cjs +++ b/tests/phase7/service-behaviour.test.cjs @@ -256,9 +256,13 @@ test("account/getPortalLogin appends hash and returns res.data", async () => { return { data: { hash: "&hash=login123" } }; } - return { data: { value: [{ id: "user-1" }] } }; + throw new Error("Unexpected get url: " + url); }; + axios.requestHandler = async () => ({ + data: { value: [{ id: "user-1" }] } + }); + const account = loadServiceModule("accountDirectService.js", { axios, BASE_URL: "http://example.local", @@ -274,8 +278,9 @@ test("account/getPortalLogin appends hash and returns res.data", async () => { signCalls[0], "/api/endpoint/gethash_api?path=%2Fapi%2Fendpoint%2Fgetportallogin_api%3FemailAddress%3Dperson%40example.com" ); + assert.strictEqual(axios.calls[1].config.method, "get"); assert.strictEqual( - axios.calls[1].url, + axios.calls[1].config.url, "http://example.local/api/endpoint/getportallogin_api?emailAddress=person@example.com&hash=login123" ); }); @@ -290,9 +295,11 @@ test("account/getPortalLogin returns JSON stringified error on failure", async ( return { data: { hash: "&hash=err" } }; } - return Promise.reject(error); + throw new Error("Unexpected get url: " + url); }; + axios.requestHandler = async () => Promise.reject(error); + const account = loadServiceModule("accountDirectService.js", { axios, BASE_URL: "", diff --git a/tests/serviceHarness.cjs b/tests/serviceHarness.cjs index c73a4961..9ab82344 100644 --- a/tests/serviceHarness.cjs +++ b/tests/serviceHarness.cjs @@ -107,6 +107,15 @@ const loadServiceModule = (fileName, injected = {}) => { return queryUrl + hashResponse.data.hash; }; + const defaultBuildSignedUrl = async (queryUrl, config = {}) => { + const { baseUrl = "" } = config; + const hashedUrl = await ( + injected.buildHashedQueryUrl || defaultBuildHashedQueryUrl + )(queryUrl); + + return `${baseUrl}${hashedUrl}`; + }; + const defaultGetSignedFileJson = async (queryUrl) => { const hashedUrl = await ( injected.buildHashedQueryUrl || defaultBuildHashedQueryUrl @@ -138,6 +147,33 @@ const loadServiceModule = (fileName, injected = {}) => { }); }; + const defaultGetSignedJson = async (queryUrl, config = {}) => { + const { baseUrl, ...requestConfig } = config; + const signedUrl = await ( + injected.buildSignedUrl || defaultBuildSignedUrl + )(queryUrl, { baseUrl }); + + return (injected.requestJson || defaultRequestJson)({ + method: "get", + url: signedUrl, + ...requestConfig + }); + }; + + const defaultPostSignedJson = async (queryUrl, data, config = {}) => { + const { baseUrl, ...requestConfig } = config; + const signedUrl = await ( + injected.buildSignedUrl || defaultBuildSignedUrl + )(queryUrl, { baseUrl }); + + return (injected.requestJson || defaultRequestJson)({ + method: "post", + url: signedUrl, + data, + ...requestConfig + }); + }; + const defaultBuildFileQuery = (pathValue, params = {}, options = {}) => { const { encode = false } = options; const entries = Object.entries(params).filter(([, value]) => { @@ -184,7 +220,10 @@ const loadServiceModule = (fileName, injected = {}) => { getSignedFileJson: injected.getSignedFileJson || defaultGetSignedFileJson, downloadFileBlob: injected.downloadFileBlob || defaultDownloadFileBlob, + getSignedJson: injected.getSignedJson || defaultGetSignedJson, + postSignedJson: injected.postSignedJson || defaultPostSignedJson, deleteSignedJson: injected.deleteSignedJson || defaultDeleteSignedJson, + buildSignedUrl: injected.buildSignedUrl || defaultBuildSignedUrl, buildHashedQueryUrl: injected.buildHashedQueryUrl || defaultBuildHashedQueryUrl, buildFileQuery: injected.buildFileQuery || defaultBuildFileQuery,