TASK22269: group migrate remaining account/portal signed GET flows

This commit is contained in:
2026-03-25 14:06:18 +00:00
parent 85cc7d165e
commit 4e964ad1ad
7 changed files with 149 additions and 16 deletions
+9 -2
View File
@@ -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
});
};
+5 -7
View File
@@ -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) => {
+6 -4
View File
@@ -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",
+39
View File
@@ -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.
@@ -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();
+10 -3
View File
@@ -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: "",
+39
View File
@@ -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,