From 811655c8d2c7aab7f97fb8f3cd66812044d62d64 Mon Sep 17 00:00:00 2001 From: robbond Date: Wed, 25 Mar 2026 13:37:49 +0000 Subject: [PATCH 01/14] TASK22269: add signed request client and migrate portal deleteWatchedCases pilot --- actions/clients/index.js | 1 + actions/clients/signedRequestClient.js | 38 ++++++++++++++++ actions/services/portalDirectService.js | 14 ++---- memory-bank/change-log.md | 43 +++++++++++++++++++ .../phase22/portal-service-behaviour.test.cjs | 21 +++++++++ tests/serviceHarness.cjs | 13 ++++++ 6 files changed, 120 insertions(+), 10 deletions(-) create mode 100644 actions/clients/signedRequestClient.js diff --git a/actions/clients/index.js b/actions/clients/index.js index 3c9b8ee9..82af288e 100644 --- a/actions/clients/index.js +++ b/actions/clients/index.js @@ -2,3 +2,4 @@ export * from "./relayClient"; export * from "./endpointClient"; export * from "./fileClient"; export * from "./fileRouteBuilder"; +export * from "./signedRequestClient"; diff --git a/actions/clients/signedRequestClient.js b/actions/clients/signedRequestClient.js new file mode 100644 index 00000000..55aa6191 --- /dev/null +++ b/actions/clients/signedRequestClient.js @@ -0,0 +1,38 @@ +import { requestJson } from "./endpointClient"; +import { buildHashedQueryUrl } from "./relayClient"; + +const signedRequestJson = async ({ method, queryUrl, data, config = {} }) => { + const signedUrl = await buildHashedQueryUrl(queryUrl); + + return requestJson({ + method, + url: signedUrl, + data, + ...config + }); +}; + +export const getSignedJson = (queryUrl, config = {}) => { + return signedRequestJson({ + method: "get", + queryUrl, + config + }); +}; + +export const postSignedJson = (queryUrl, data, config = {}) => { + return signedRequestJson({ + method: "post", + queryUrl, + data, + config + }); +}; + +export const deleteSignedJson = (queryUrl, config = {}) => { + return signedRequestJson({ + method: "delete", + queryUrl, + config + }); +}; diff --git a/actions/services/portalDirectService.js b/actions/services/portalDirectService.js index 54b1eb92..d462f57f 100644 --- a/actions/services/portalDirectService.js +++ b/actions/services/portalDirectService.js @@ -2,6 +2,7 @@ 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 { buildFileQuery, withBaseUrl, @@ -189,16 +190,9 @@ export const deleteWatchedCases = async (watchedCaseID) => { watchedCaseID }); - return buildHashedQueryUrl(queryUrl) - .then((signedUrl) => - requestJson({ - method: "delete", - url: signedUrl - }) - ) - .catch((error) => { - consoleLogger(error); - }); + return deleteSignedJson(queryUrl).catch((error) => { + consoleLogger(error); + }); }; export const sendCaseCompleteMessage = async ( diff --git a/memory-bank/change-log.md b/memory-bank/change-log.md index 269a4093..3caa5361 100644 --- a/memory-bank/change-log.md +++ b/memory-bank/change-log.md @@ -2725,3 +2725,46 @@ Validation: Follow-ups: - Sequence A step3 targeted gaps are now covered; further test expansion should be treated as new scope (e.g., deeper end-to-end journey assertions). + +--- + +### CL-076: TASK22269 Slice B1.1 — signed-request helper set + portal pilot signed-flow migration + +date: 2026-03-25 +author: Cline +scope: `actions/clients/{signedRequestClient,index}.js`, `actions/services/portalDirectService.js`, `tests/{serviceHarness,phase22/portal-service-behaviour}.cjs` +type: change +rationale: Execute Sequence B Workstream B1 pilot by introducing shared signed request helpers (GET/POST/DELETE) and migrating one bounded portal signed flow without broader module rollout. +impact: Reduces duplication and drift risk in hash-signing + method execution paths while preserving existing signed-flow behavior contracts. +status: completed + +Summary: + +- Added new shared signed-request client helper module: + - `actions/clients/signedRequestClient.js` + - exports: + - `getSignedJson(queryUrl, config?)` + - `postSignedJson(queryUrl, data, config?)` + - `deleteSignedJson(queryUrl, config?)` + - all helpers use existing `buildHashedQueryUrl(...)` + `requestJson(...)` composition to preserve signing semantics +- Exported new helper module via `actions/clients/index.js`. +- Migrated exactly one pilot signed flow in portal service: + - `deleteWatchedCases` in `actions/services/portalDirectService.js` + - from inline `buildHashedQueryUrl(...).then(requestJson(...))` to `deleteSignedJson(queryUrl)` + - preserved existing catch/log behavior (`consoleLogger` + `undefined` return on catch) +- Added test harness compatibility for VM import-stripping suites: + - `tests/serviceHarness.cjs` now injects default `deleteSignedJson` mock behavior. +- Expanded portal behavioral tests with explicit negative-path assertion: + - `tests/phase22/portal-service-behaviour.test.cjs` + - verifies `deleteWatchedCases` logs and safely returns `undefined` when signed delete fails. + +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 + - portal-service suite now 5/5 including signed-delete failure path +- `node tests/phase7/service-behaviour.test.cjs` -> pass (13/13) + +Follow-ups: + +- Continue Sequence B B1 in future bounded slices by migrating additional signed flows one module/function cluster at a time (outside this slice). diff --git a/tests/phase22/portal-service-behaviour.test.cjs b/tests/phase22/portal-service-behaviour.test.cjs index f1f33b4b..cc386e80 100644 --- a/tests/phase22/portal-service-behaviour.test.cjs +++ b/tests/phase22/portal-service-behaviour.test.cjs @@ -134,6 +134,27 @@ test("portal/sendRepCompleteMessage rejects when hash signing fails before reque assert.strictEqual(logger.calls.length, 0); }); +test("portal/deleteWatchedCases logs and returns undefined when signed delete fails", async () => { + const axios = createAxiosMock(); + const logger = createLoggerMock(); + const error = createAxiosError(401, "Unauthorized"); + + const portal = loadServiceModule("portalDirectService.js", { + axios, + BASE_URL: "", + consoleLogger: logger.consoleLogger, + deleteSignedJson: async () => { + throw error; + } + }); + + const result = await portal.deleteWatchedCases("watch-2"); + + assert.strictEqual(result, undefined); + assert.strictEqual(logger.calls.length, 1); + assert.strictEqual(logger.calls[0], error); +}); + const run = async () => { let passed = 0; diff --git a/tests/serviceHarness.cjs b/tests/serviceHarness.cjs index 4cf494ff..c73a4961 100644 --- a/tests/serviceHarness.cjs +++ b/tests/serviceHarness.cjs @@ -126,6 +126,18 @@ const loadServiceModule = (fileName, injected = {}) => { }); }; + const defaultDeleteSignedJson = async (queryUrl, config = {}) => { + const hashedUrl = await ( + injected.buildHashedQueryUrl || defaultBuildHashedQueryUrl + )(queryUrl); + + return (injected.requestJson || defaultRequestJson)({ + method: "delete", + url: hashedUrl, + ...config + }); + }; + const defaultBuildFileQuery = (pathValue, params = {}, options = {}) => { const { encode = false } = options; const entries = Object.entries(params).filter(([, value]) => { @@ -172,6 +184,7 @@ const loadServiceModule = (fileName, injected = {}) => { getSignedFileJson: injected.getSignedFileJson || defaultGetSignedFileJson, downloadFileBlob: injected.downloadFileBlob || defaultDownloadFileBlob, + deleteSignedJson: injected.deleteSignedJson || defaultDeleteSignedJson, buildHashedQueryUrl: injected.buildHashedQueryUrl || defaultBuildHashedQueryUrl, buildFileQuery: injected.buildFileQuery || defaultBuildFileQuery, From 7470975d0ece31be952517678e4d3e8d93820816 Mon Sep 17 00:00:00 2001 From: robbond Date: Wed, 25 Mar 2026 13:43:08 +0000 Subject: [PATCH 02/14] TASK22269: migrate portal deleteMyRepresentations to signed delete helper --- actions/services/portalDirectService.js | 29 +++++++++------------- memory-bank/change-log.md | 33 +++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 18 deletions(-) diff --git a/actions/services/portalDirectService.js b/actions/services/portalDirectService.js index d462f57f..cb063a2d 100644 --- a/actions/services/portalDirectService.js +++ b/actions/services/portalDirectService.js @@ -147,24 +147,17 @@ export const deleteMyRepresentations = (myRepresentationsID) => { myRepresentationsID }); - return buildHashedQueryUrl(queryUrl) - .then((signedUrl) => - requestJson({ - method: "delete", - url: signedUrl, - headers: { - "OData-MaxVersion": "4.0", - "OData-Version": "4.0", - "Accept": "application/json;odata.metadata=none", - "Prefer": - 'odata.include-annotations="*",return=representation', - "Content-Type": "application/json" - } - }) - ) - .catch((error) => { - consoleLogger(error); - }); + return deleteSignedJson(queryUrl, { + headers: { + "OData-MaxVersion": "4.0", + "OData-Version": "4.0", + "Accept": "application/json;odata.metadata=none", + "Prefer": 'odata.include-annotations="*",return=representation', + "Content-Type": "application/json" + } + }).catch((error) => { + consoleLogger(error); + }); }; export const deleteAwaitingSubmissions = (incidentID) => { diff --git a/memory-bank/change-log.md b/memory-bank/change-log.md index 3caa5361..f41fef9f 100644 --- a/memory-bank/change-log.md +++ b/memory-bank/change-log.md @@ -2768,3 +2768,36 @@ Validation: Follow-ups: - Continue Sequence B B1 in future bounded slices by migrating additional signed flows one module/function cluster at a time (outside this slice). + +--- + +### CL-077: TASK22269 Slice B1.2 — portal signed-delete bundle (headered delete migration) + +date: 2026-03-25 +author: Cline +scope: `actions/services/portalDirectService.js` +type: change +rationale: Continue signed-request consolidation using bounded grouping by migrating the remaining portal signed delete flow (`deleteMyRepresentations`) onto shared signed helper while preserving required OData headers. +impact: Further reduces duplicated sign+delete boilerplate in portal service and centralizes signed DELETE execution semantics. +status: completed + +Summary: + +- Migrated `deleteMyRepresentations` from inline `buildHashedQueryUrl(...).then(requestJson(...))` to shared `deleteSignedJson(queryUrl, { headers })`. +- Preserved behavior-critical headers exactly: + - `OData-MaxVersion` + - `OData-Version` + - `Accept` + - `Prefer` + - `Content-Type` +- Preserved existing catch/log behavior (`consoleLogger` with safe undefined return on failure). + +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: + +- Next bounded signed GET candidate in portal service is `sendRepCompleteMessage` (single signed URL + GET request path). From 85cc7d165e4f880c52cacf57cd41424fd4a3310c Mon Sep 17 00:00:00 2001 From: robbond Date: Wed, 25 Mar 2026 13:47:46 +0000 Subject: [PATCH 03/14] TASK22269: delegate fileClient signed get/post to signedRequestClient --- actions/clients/fileClient.js | 18 ++------- memory-bank/change-log.md | 31 +++++++++++++++ tests/phase22/file-client-behaviour.test.cjs | 41 +++++++------------- 3 files changed, 49 insertions(+), 41 deletions(-) diff --git a/actions/clients/fileClient.js b/actions/clients/fileClient.js index e639bb30..c58c9748 100644 --- a/actions/clients/fileClient.js +++ b/actions/clients/fileClient.js @@ -1,17 +1,12 @@ import { getJson, requestJson } from "./endpointClient"; -import { buildHashedQueryUrl } from "./relayClient"; +import { getSignedJson, postSignedJson } from "./signedRequestClient"; export const getFileJson = (url) => { return getJson(url); }; export const getSignedFileJson = async (queryUrl) => { - const signedUrl = await buildHashedQueryUrl(queryUrl); - - return requestJson({ - method: "get", - url: signedUrl - }); + return getSignedJson(queryUrl); }; export const downloadFileBlob = (url) => { @@ -23,12 +18,5 @@ export const downloadFileBlob = (url) => { }; export const postSignedFileJson = async (queryUrl, data, config = {}) => { - const signedUrl = await buildHashedQueryUrl(queryUrl); - - return requestJson({ - method: "post", - url: signedUrl, - data, - ...config - }); + return postSignedJson(queryUrl, data, config); }; diff --git a/memory-bank/change-log.md b/memory-bank/change-log.md index f41fef9f..489c8b30 100644 --- a/memory-bank/change-log.md +++ b/memory-bank/change-log.md @@ -2801,3 +2801,34 @@ Validation: Follow-ups: - Next bounded signed GET candidate in portal service is `sendRepCompleteMessage` (single signed URL + GET request path). + +--- + +### CL-078: TASK22269 Slice B1.3 — fileClient signed helper delegation bundle + +date: 2026-03-25 +author: Cline +scope: `actions/clients/fileClient.js`, `tests/phase22/file-client-behaviour.test.cjs` +type: change +rationale: Continue grouped signed-request consolidation by reducing duplicate signing logic in `fileClient` and delegating signed GET/POST operations to shared `signedRequestClient` helpers. +impact: Centralizes signed method execution behavior in one helper layer and lowers drift risk across file-service call paths. +status: completed + +Summary: + +- Updated `actions/clients/fileClient.js`: + - replaced direct `buildHashedQueryUrl + requestJson` logic in: + - `getSignedFileJson` -> now delegates to `getSignedJson` + - `postSignedFileJson` -> now delegates to `postSignedJson` + - retained `downloadFileBlob` and `getFileJson` behavior unchanged. +- Updated `tests/phase22/file-client-behaviour.test.cjs` to assert delegation contracts for `getSignedJson` and `postSignedJson` rather than direct signing internals. + +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: + +- 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. diff --git a/tests/phase22/file-client-behaviour.test.cjs b/tests/phase22/file-client-behaviour.test.cjs index 767aad88..e648c954 100644 --- a/tests/phase22/file-client-behaviour.test.cjs +++ b/tests/phase22/file-client-behaviour.test.cjs @@ -24,8 +24,11 @@ const loadFileClientModule = (injected = {}) => { requestJson: async () => { throw new Error("requestJson not injected"); }, - buildHashedQueryUrl: async () => { - throw new Error("buildHashedQueryUrl not injected"); + getSignedJson: async () => { + throw new Error("getSignedJson not injected"); + }, + postSignedJson: async () => { + throw new Error("postSignedJson not injected"); }, ...injected }; @@ -56,15 +59,10 @@ test("clients/fileClient getFileJson delegates to getJson", async () => { test("clients/fileClient getSignedFileJson signs url and requests json", async () => { const signedCalls = []; - const requestCalls = []; const mod = loadFileClientModule({ - buildHashedQueryUrl: async (queryUrl) => { + getSignedJson: async (queryUrl) => { signedCalls.push(queryUrl); - return queryUrl + "&hash=signed"; - }, - requestJson: async (config) => { - requestCalls.push(config); return { deleted: true }; } }); @@ -77,11 +75,9 @@ test("clients/fileClient getSignedFileJson signs url and requests json", async ( deleted: true }); assert.strictEqual(signedCalls.length, 1); - assert.strictEqual(requestCalls.length, 1); - assert.strictEqual(requestCalls[0].method, "get"); assert.strictEqual( - requestCalls[0].url, - "/api/file/deleteblobcase?container=a&casefolderID=b&hash=signed" + signedCalls[0], + "/api/file/deleteblobcase?container=a&casefolderID=b" ); }); @@ -106,15 +102,12 @@ test("clients/fileClient downloadFileBlob requests blob response", async () => { test("clients/fileClient postSignedFileJson signs url and posts payload", async () => { const signedCalls = []; - const requestCalls = []; + const postedCalls = []; const mod = loadFileClientModule({ - buildHashedQueryUrl: async (queryUrl) => { + postSignedJson: async (queryUrl, data, config) => { signedCalls.push(queryUrl); - return queryUrl + "&hash=signed-post"; - }, - requestJson: async (config) => { - requestCalls.push(config); + postedCalls.push({ data, config }); return { uploaded: true }; } }); @@ -132,18 +125,14 @@ test("clients/fileClient postSignedFileJson signs url and posts payload", async uploaded: true }); assert.strictEqual(signedCalls.length, 1); - assert.strictEqual(requestCalls.length, 1); - assert.strictEqual(requestCalls[0].method, "post"); - assert.strictEqual( - requestCalls[0].url, - "/api/file/uploadsinglefile&hash=signed-post" - ); + assert.strictEqual(postedCalls.length, 1); + assert.strictEqual(signedCalls[0], "/api/file/uploadsinglefile"); assert.deepStrictEqual( - JSON.parse(JSON.stringify(requestCalls[0].data)), + JSON.parse(JSON.stringify(postedCalls[0].data)), payload ); assert.strictEqual( - requestCalls[0].headers["content-type"], + postedCalls[0].config.headers["content-type"], "multipart/form-data" ); }); From 4e964ad1ad7afb6e4f927c5f437bb2097cac7ec2 Mon Sep 17 00:00:00 2001 From: robbond Date: Wed, 25 Mar 2026 14:06:18 +0000 Subject: [PATCH 04/14] 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, From 1b7e6009aa23e4045a59586a2d8de8e047bcbb7c Mon Sep 17 00:00:00 2001 From: robbond Date: Wed, 25 Mar 2026 14:13:53 +0000 Subject: [PATCH 05/14] TASK22269: normalize document hash-suffix route composition --- actions/clients/fileRouteBuilder.js | 4 +++ actions/services/documentDirectService.js | 13 +++---- memory-bank/change-log.md | 34 +++++++++++++++++++ tests/phase22/client-utils-behaviour.test.cjs | 8 ++++- tests/serviceHarness.cjs | 3 ++ 5 files changed, 55 insertions(+), 7 deletions(-) diff --git a/actions/clients/fileRouteBuilder.js b/actions/clients/fileRouteBuilder.js index 944c366f..0d366469 100644 --- a/actions/clients/fileRouteBuilder.js +++ b/actions/clients/fileRouteBuilder.js @@ -31,3 +31,7 @@ export const withBaseUrl = (baseUrl, route) => { export const appendQuerySuffix = (route, suffix = "") => { return `${route}${suffix}`; }; + +export const appendHashSuffix = (route, hashBuilder) => { + return appendQuerySuffix(route, hashBuilder(route)); +}; diff --git a/actions/services/documentDirectService.js b/actions/services/documentDirectService.js index b3d0d3c6..8075e3bf 100644 --- a/actions/services/documentDirectService.js +++ b/actions/services/documentDirectService.js @@ -4,7 +4,8 @@ import { hashAPIPath } from "../core/hash"; import { buildFileQuery, withBaseUrl, - appendQuerySuffix + appendQuerySuffix, + appendHashSuffix } from "../clients/fileRouteBuilder"; import { getFileJson, @@ -19,7 +20,7 @@ export const getAwaitingSubmissionFromBlob = (containerName) => { }); return getFileJson( - withBaseUrl(BASE_URL, appendQuerySuffix(route, hashAPIPath(route))) + withBaseUrl(BASE_URL, appendHashSuffix(route, hashAPIPath)) ).catch((error) => { consoleLogger(error); }); @@ -31,7 +32,7 @@ export const getRepsFromBlob = (containerName) => { }); return getFileJson( - withBaseUrl(BASE_URL, appendQuerySuffix(route, hashAPIPath(route))) + withBaseUrl(BASE_URL, appendHashSuffix(route, hashAPIPath)) ).catch((error) => { consoleLogger(error); }); @@ -227,7 +228,7 @@ export const getFilesFromBlob = (containerName, casefolderID) => { }); return getFileJson( - withBaseUrl(BASE_URL, appendQuerySuffix(route, hashAPIPath(route))) + withBaseUrl(BASE_URL, appendHashSuffix(route, hashAPIPath)) ).catch((error) => { consoleLogger(error); }); @@ -328,7 +329,7 @@ export const getProgressFromBlob = async (containerName, casereference) => { ); return getFileJson( - withBaseUrl(BASE_URL, appendQuerySuffix(route, hashAPIPath(route))) + withBaseUrl(BASE_URL, appendHashSuffix(route, hashAPIPath)) ).catch((error) => { consoleLogger(error); }); @@ -338,7 +339,7 @@ export const createContainerProxy = (containerName) => { var route = buildFileQuery("/api/file/setupcontainer", { ident: containerName }); - var queryUrl = appendQuerySuffix(route, hashAPIPath(route)); + var queryUrl = appendHashSuffix(route, hashAPIPath); return getFileJson(withBaseUrl(BASE_URL, queryUrl)).catch((error) => { consoleLogger(error); diff --git a/memory-bank/change-log.md b/memory-bank/change-log.md index 18114986..a8714674 100644 --- a/memory-bank/change-log.md +++ b/memory-bank/change-log.md @@ -2871,3 +2871,37 @@ Validation: 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. + +--- + +### CL-080: TASK22269 Slice B1.5 — document hash-suffix route normalization helper + +date: 2026-03-25 +author: Cline +scope: `actions/clients/fileRouteBuilder.js`, `actions/services/documentDirectService.js`, `tests/{serviceHarness,phase22/client-utils-behaviour}.cjs` +type: change +rationale: Continue grouped follow-on candidates by normalizing repeated deterministic hash-suffix route assembly in document service behind one shared route-builder helper. +impact: Reduces repeated `appendQuerySuffix(route, hashAPIPath(route))` composition drift risk while preserving route/query/hash behavior. +status: completed + +Summary: + +- Added `appendHashSuffix(route, hashBuilder)` to `fileRouteBuilder`. +- Migrated document service deterministic hash-suffix paths to new helper: + - `getAwaitingSubmissionFromBlob` + - `getRepsFromBlob` + - `getFilesFromBlob` + - `getProgressFromBlob` + - `createContainerProxy` +- Updated shared VM harness defaults (`tests/serviceHarness.cjs`) to inject `appendHashSuffix`. +- Expanded phase22 utility test to cover new helper behavior (`tests/phase22/client-utils-behaviour.test.cjs`). + +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 non-service candidate for this stream is `actions/azurestorage.js` direct `hashAPIPath` metadata assembly (separate bounded slice if desired). diff --git a/tests/phase22/client-utils-behaviour.test.cjs b/tests/phase22/client-utils-behaviour.test.cjs index 76bad6fe..310c58fc 100644 --- a/tests/phase22/client-utils-behaviour.test.cjs +++ b/tests/phase22/client-utils-behaviour.test.cjs @@ -70,7 +70,7 @@ const loadFileRouteBuilderModule = (injected = {}) => { source = source.replace(/import[\s\S]*?from\s+"[^"]+";\n?/g, ""); source = source.replace(/export const\s+/g, "const "); source += - "\nmodule.exports = { buildFileQuery, withBaseUrl, appendQuerySuffix };\n"; + "\nmodule.exports = { buildFileQuery, withBaseUrl, appendQuerySuffix, appendHashSuffix };\n"; const context = { module: { exports: {} }, @@ -211,6 +211,12 @@ test("clients/fileRouteBuilder builds query with optional encoding and suffix he mod.appendQuerySuffix(unencoded, "&hash=123"), "/api/file/getbloblist?container=abc&casefolderID=x/y&hash=123" ); + assert.strictEqual( + mod.appendHashSuffix(unencoded, (route) => + route.includes("getbloblist") ? "&hash=abc" : "" + ), + "/api/file/getbloblist?container=abc&casefolderID=x/y&hash=abc" + ); }); const run = async () => { diff --git a/tests/serviceHarness.cjs b/tests/serviceHarness.cjs index 9ab82344..aea3882b 100644 --- a/tests/serviceHarness.cjs +++ b/tests/serviceHarness.cjs @@ -200,6 +200,8 @@ const loadServiceModule = (fileName, injected = {}) => { const defaultWithBaseUrl = (baseUrl, route) => `${baseUrl}${route}`; const defaultAppendQuerySuffix = (route, suffix = "") => `${route}${suffix}`; + const defaultAppendHashSuffix = (route, hashBuilder) => + defaultAppendQuerySuffix(route, hashBuilder(route)); const context = { module: { exports: {} }, @@ -230,6 +232,7 @@ const loadServiceModule = (fileName, injected = {}) => { withBaseUrl: injected.withBaseUrl || defaultWithBaseUrl, appendQuerySuffix: injected.appendQuerySuffix || defaultAppendQuerySuffix, + appendHashSuffix: injected.appendHashSuffix || defaultAppendHashSuffix, ...injected }; From fcce6a397820b397df33a8af8fe3ec53309b8713 Mon Sep 17 00:00:00 2001 From: robbond Date: Wed, 25 Mar 2026 14:21:09 +0000 Subject: [PATCH 06/14] TASK22269: normalize azurestorage hash query path builders --- actions/azurestorage.js | 150 ++++++++++++++++++++++++-------------- memory-bank/change-log.md | 37 ++++++++++ 2 files changed, 134 insertions(+), 53 deletions(-) diff --git a/actions/azurestorage.js b/actions/azurestorage.js index f75d6000..b2b89887 100644 --- a/actions/azurestorage.js +++ b/actions/azurestorage.js @@ -31,6 +31,59 @@ const QUEUE_PATH = process.env.AZURE_PEDW_QUEUE_ENDPOINT; const accountName = process.env.AZURE_STORAGE_ACCOUNT_NAME; +const buildDownloadBlobQueryPath = ({ + containerName, + casefolderID, + blobname, + encodeCasefolderID = true, + encodeBlobname = true +}) => { + const casefolderPart = encodeCasefolderID + ? encodeURIComponent(casefolderID) + : casefolderID; + const blobPart = encodeBlobname ? encodeURIComponent(blobname) : blobname; + + return ( + "/api/file/downloadblob?container=" + + containerName + + "&casefolderID=" + + casefolderPart + + "&blobname=" + + blobPart + ); +}; + +const buildDeleteBlobQueryPath = ({ + containerName, + casefolderID, + blobname, + encodeCasefolderID = true, + encodeBlobname = true +}) => { + const casefolderPart = encodeCasefolderID + ? encodeURIComponent(casefolderID) + : casefolderID; + const blobPart = encodeBlobname ? encodeURIComponent(blobname) : blobname; + + return ( + "/api/file/deleteblob?container=" + + containerName + + "&casefolderID=" + + casefolderPart + + "&blobname=" + + blobPart + ); +}; + +const buildGetBlobListQueryPath = ({ containerName, casefolderID }) => { + return ( + "/api/file/getbloblist?container=" + + containerName + + "&casefolderID=" + + casefolderID + ); +}; + export const createContainerSas = async (containerName) => { // Get environment variables @@ -214,12 +267,11 @@ export const getBlobs = async (containerName, casefolderID) => { "&blobname=" + encodeURIComponent(blob.name), "hashedfilepath": hashAPIPath( - "/api/file/downloadblob?container=" + - containerName + - "&casefolderID=" + - encodeURIComponent(casefolderID) + - "&blobname=" + - encodeURIComponent(blob.name.split("/")[2]) + buildDownloadBlobQueryPath({ + containerName, + casefolderID, + blobname: blob.name.split("/")[2] + }) ), "deletepath": "/api/file/deleteblob?container=" + @@ -229,18 +281,17 @@ export const getBlobs = async (containerName, casefolderID) => { "&blobname=" + encodeURIComponent(blob.name.split("/")[2]), "hasheddeletepath": hashAPIPath( - "/api/file/deleteblob?container=" + - containerName + - "&casefolderID=" + - encodeURIComponent(casefolderID) + - "&blobname=" + - encodeURIComponent(blob.name.split("/")[2]) + buildDeleteBlobQueryPath({ + containerName, + casefolderID, + blobname: blob.name.split("/")[2] + }) ), "hashgetblobs": hashAPIPath( - "/api/file/getbloblist?container=" + - containerName + - "&casefolderID=" + + buildGetBlobListQueryPath({ + containerName, casefolderID + }) ) }); } @@ -1246,26 +1297,24 @@ export const getProgressBlobs = async (containerName, caseReference) => { "contentType": blob.contentType, "lastModified": blob.properties.lastModified, "hashedfilepath": hashAPIPath( - "/api/file/downloadblob?container=" + - containerName + - "&casefolderID=" + - encodeURIComponent(caseReference) + - "&blobname=" + - encodeURIComponent(blob.name.split("/")[1]) + buildDownloadBlobQueryPath({ + containerName, + casefolderID: caseReference, + blobname: blob.name.split("/")[1] + }) ), "hasheddeletepath": hashAPIPath( - "/api/file/deleteblob?container=" + - containerName + - "&casefolderID=" + - encodeURIComponent(caseReference) + - "&blobname=" + - encodeURIComponent(blob.name.split("/")[1]) + buildDeleteBlobQueryPath({ + containerName, + casefolderID: caseReference, + blobname: blob.name.split("/")[1] + }) ), "hashgetblobs": hashAPIPath( - "/api/file/getbloblist?container=" + - containerName + - "&casefolderID=" + - caseReference + buildGetBlobListQueryPath({ + containerName, + casefolderID: caseReference + }) ) }); } @@ -1455,14 +1504,12 @@ export const getRepsFilesBlobs = async ( "&blobname=" + encodeURIComponent(blob.name.split("/")[3]), "hashedfilepath": hashAPIPath( - "/api/file/downloadblob?container=" + - containerName + - "&casefolderID=" + - encodeURIComponent( - blob.name.split("/")[0] + "/" + blob.name.split("/")[1] - ) + - "&blobname=" + - encodeURIComponent(blob.name.split("/")[3]) + buildDownloadBlobQueryPath({ + containerName, + casefolderID: + blob.name.split("/")[0] + "/" + blob.name.split("/")[1], + blobname: blob.name.split("/")[3] + }) ), "deletepath": "/api/file/deleteblob?container=" + @@ -1474,22 +1521,19 @@ export const getRepsFilesBlobs = async ( "&blobname=" + blob.name.split("/")[3], "hasheddeletepath": hashAPIPath( - "/api/file/deleteblob?container=" + - containerName + - "&casefolderID=" + - encodeURIComponent( - blob.name.split("/")[0] + "/" + blob.name.split("/")[1] - ) + - "&blobname=" + - encodeURIComponent(blob.name.split("/")[3]) + buildDeleteBlobQueryPath({ + containerName, + casefolderID: + blob.name.split("/")[0] + "/" + blob.name.split("/")[1], + blobname: blob.name.split("/")[3] + }) ), "hashgetblobs": hashAPIPath( - "/api/file/getbloblist?container=" + - containerName + - "&casefolderID=" + - blob.name.split("/")[0] + - "/" + - blob.name.split("/")[1] + buildGetBlobListQueryPath({ + containerName, + casefolderID: + blob.name.split("/")[0] + "/" + blob.name.split("/")[1] + }) ) }); } diff --git a/memory-bank/change-log.md b/memory-bank/change-log.md index a8714674..7f54c220 100644 --- a/memory-bank/change-log.md +++ b/memory-bank/change-log.md @@ -2905,3 +2905,40 @@ Validation: Follow-ups: - Remaining non-service candidate for this stream is `actions/azurestorage.js` direct `hashAPIPath` metadata assembly (separate bounded slice if desired). + +--- + +### CL-081: TASK22269 Slice B1.6 — azurestorage hash-query metadata builder normalization + +date: 2026-03-25 +author: Cline +scope: `actions/azurestorage.js` +type: change +rationale: Continue requested follow-on slice by reducing repeated hash-query path string composition in azure storage metadata builders behind local helper functions. +impact: Lowers duplication/drift risk in hashed metadata path generation while preserving existing route and encoding behavior. +status: completed + +Summary: + +- Added internal helper builders in `actions/azurestorage.js`: + - `buildDownloadBlobQueryPath(...)` + - `buildDeleteBlobQueryPath(...)` + - `buildGetBlobListQueryPath(...)` +- Replaced repeated inline hash path literals with helper usage in targeted metadata object builders: + - `getBlobs` + - `getProgressBlobs` + - `getRepsFilesBlobs` +- Preserved existing behavior semantics for hash path construction: + - encoded `casefolderID`/`blobname` where previously encoded + - unchanged `containerName` and `casefolderID` value sourcing + - unchanged returned object field names and shape + +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 potential cleanups in `actions/azurestorage.js` are broader non-slice refactors (legacy logging verbosity, large function decomposition) and should be handled separately to keep risk bounded. From 8744d3dafc3d2d33ae360fd1ecbdaf4885ad88e5 Mon Sep 17 00:00:00 2001 From: robbond Date: Wed, 25 Mar 2026 14:27:52 +0000 Subject: [PATCH 07/14] TASK22269: consolidate azurestorage hash metadata mapping --- actions/azurestorage.js | 108 +++++++++++++++----------------------- memory-bank/change-log.md | 33 ++++++++++++ 2 files changed, 76 insertions(+), 65 deletions(-) diff --git a/actions/azurestorage.js b/actions/azurestorage.js index b2b89887..48b8e66d 100644 --- a/actions/azurestorage.js +++ b/actions/azurestorage.js @@ -84,6 +84,31 @@ const buildGetBlobListQueryPath = ({ containerName, casefolderID }) => { ); }; +const buildHashMetadataPaths = ({ containerName, casefolderID, blobname }) => { + return { + "hashedfilepath": hashAPIPath( + buildDownloadBlobQueryPath({ + containerName, + casefolderID, + blobname + }) + ), + "hasheddeletepath": hashAPIPath( + buildDeleteBlobQueryPath({ + containerName, + casefolderID, + blobname + }) + ), + "hashgetblobs": hashAPIPath( + buildGetBlobListQueryPath({ + containerName, + casefolderID + }) + ) + }; +}; + export const createContainerSas = async (containerName) => { // Get environment variables @@ -266,33 +291,18 @@ export const getBlobs = async (containerName, casefolderID) => { encodeURIComponent(casefolderID) + "&blobname=" + encodeURIComponent(blob.name), - "hashedfilepath": hashAPIPath( - buildDownloadBlobQueryPath({ - containerName, - casefolderID, - blobname: blob.name.split("/")[2] - }) - ), + ...buildHashMetadataPaths({ + containerName, + casefolderID, + blobname: blob.name.split("/")[2] + }), "deletepath": "/api/file/deleteblob?container=" + containerName + "&casefolderID=" + encodeURIComponent(casefolderID) + "&blobname=" + - encodeURIComponent(blob.name.split("/")[2]), - "hasheddeletepath": hashAPIPath( - buildDeleteBlobQueryPath({ - containerName, - casefolderID, - blobname: blob.name.split("/")[2] - }) - ), - "hashgetblobs": hashAPIPath( - buildGetBlobListQueryPath({ - containerName, - casefolderID - }) - ) + encodeURIComponent(blob.name.split("/")[2]) }); } //console.log("blobObj:", blobObj); @@ -1296,26 +1306,11 @@ export const getProgressBlobs = async (containerName, caseReference) => { "contentLength": blob.properties.contentLength, "contentType": blob.contentType, "lastModified": blob.properties.lastModified, - "hashedfilepath": hashAPIPath( - buildDownloadBlobQueryPath({ - containerName, - casefolderID: caseReference, - blobname: blob.name.split("/")[1] - }) - ), - "hasheddeletepath": hashAPIPath( - buildDeleteBlobQueryPath({ - containerName, - casefolderID: caseReference, - blobname: blob.name.split("/")[1] - }) - ), - "hashgetblobs": hashAPIPath( - buildGetBlobListQueryPath({ - containerName, - casefolderID: caseReference - }) - ) + ...buildHashMetadataPaths({ + containerName, + casefolderID: caseReference, + blobname: blob.name.split("/")[1] + }) }); } @@ -1503,14 +1498,12 @@ export const getRepsFilesBlobs = async ( ) + "&blobname=" + encodeURIComponent(blob.name.split("/")[3]), - "hashedfilepath": hashAPIPath( - buildDownloadBlobQueryPath({ - containerName, - casefolderID: - blob.name.split("/")[0] + "/" + blob.name.split("/")[1], - blobname: blob.name.split("/")[3] - }) - ), + ...buildHashMetadataPaths({ + containerName, + casefolderID: + blob.name.split("/")[0] + "/" + blob.name.split("/")[1], + blobname: blob.name.split("/")[3] + }), "deletepath": "/api/file/deleteblob?container=" + containerName + @@ -1519,22 +1512,7 @@ export const getRepsFilesBlobs = async ( "/" + blob.name.split("/")[1] + "&blobname=" + - blob.name.split("/")[3], - "hasheddeletepath": hashAPIPath( - buildDeleteBlobQueryPath({ - containerName, - casefolderID: - blob.name.split("/")[0] + "/" + blob.name.split("/")[1], - blobname: blob.name.split("/")[3] - }) - ), - "hashgetblobs": hashAPIPath( - buildGetBlobListQueryPath({ - containerName, - casefolderID: - blob.name.split("/")[0] + "/" + blob.name.split("/")[1] - }) - ) + blob.name.split("/")[3] }); } console.log("blobObj:", blobObj); diff --git a/memory-bank/change-log.md b/memory-bank/change-log.md index 7f54c220..f9e737d4 100644 --- a/memory-bank/change-log.md +++ b/memory-bank/change-log.md @@ -2942,3 +2942,36 @@ Validation: Follow-ups: - Remaining potential cleanups in `actions/azurestorage.js` are broader non-slice refactors (legacy logging verbosity, large function decomposition) and should be handled separately to keep risk bounded. + +--- + +### CL-082: TASK22269 Slice B1.7 — azurestorage hash metadata helper consolidation + +date: 2026-03-25 +author: Cline +scope: `actions/azurestorage.js` +type: change +rationale: Continue bounded normalization by consolidating repeated hash metadata object field population into a single local helper. +impact: Reduces duplicated metadata field assembly and drift risk while preserving existing output shape and hash behavior. +status: completed + +Summary: + +- Added `buildHashMetadataPaths({ containerName, casefolderID, blobname })` helper. +- Replaced repeated per-object hash metadata assignment in: + - `getBlobs` + - `getProgressBlobs` + - `getRepsFilesBlobs` +- Preserved existing metadata contracts: + - keys unchanged: `hashedfilepath`, `hasheddeletepath`, `hashgetblobs` + - same encoded query path inputs and route semantics. + +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: + +- Any further `azurestorage.js` cleanup should remain bounded (e.g., logging-only normalization) and separated from behavior-affecting refactors. From 58e1eef1f5db217a88ea92b954ca37f888eee362 Mon Sep 17 00:00:00 2001 From: robbond Date: Thu, 26 Mar 2026 11:11:24 +0000 Subject: [PATCH 08/14] TASK22269: add phase22 azurestorage helper contract tests --- memory-bank/change-log.md | 32 +++++ .../azurestorage-helper-behaviour.test.cjs | 125 ++++++++++++++++++ tests/phase22/index.test.cjs | 2 + 3 files changed, 159 insertions(+) create mode 100644 tests/phase22/azurestorage-helper-behaviour.test.cjs diff --git a/memory-bank/change-log.md b/memory-bank/change-log.md index f9e737d4..15b9892a 100644 --- a/memory-bank/change-log.md +++ b/memory-bank/change-log.md @@ -2975,3 +2975,35 @@ Validation: Follow-ups: - Any further `azurestorage.js` cleanup should remain bounded (e.g., logging-only normalization) and separated from behavior-affecting refactors. + +--- + +### CL-083: TASK22269 Slice B1.8 — phase22 azurestorage helper contract coverage + +date: 2026-03-26 +author: Cline +scope: `tests/phase22/{azurestorage-helper-behaviour,index}.test.cjs` +type: change +rationale: Execute the selected bounded test-only follow-up by adding focused regression coverage for recently added azurestorage helper contracts. +impact: Improves confidence in query-path and hash-metadata helper output stability without changing runtime behavior. +status: completed + +Summary: + +- Added new phase22 suite: `tests/phase22/azurestorage-helper-behaviour.test.cjs`. +- Test suite isolates helper block from `actions/azurestorage.js` and verifies: + - `buildDownloadBlobQueryPath` default encoding output + - `buildDeleteBlobQueryPath` non-encoded option behavior + - `buildGetBlobListQueryPath` query output contract + - `buildHashMetadataPaths` key/value shape (`hashedfilepath`, `hasheddeletepath`, `hashgetblobs`) +- Wired suite into aggregate runner `tests/phase22/index.test.cjs`. + +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 (includes new azurestorage-helper 4/4) +- `node tests/phase7/service-behaviour.test.cjs` -> pass (13/13) + +Follow-ups: + +- Optional next bounded slice: add an explicit assertion for encoded `casefolderID` variants containing reserved query characters (`?`, `&`) if those inputs are expected in future flows. diff --git a/tests/phase22/azurestorage-helper-behaviour.test.cjs b/tests/phase22/azurestorage-helper-behaviour.test.cjs new file mode 100644 index 00000000..e9f68049 --- /dev/null +++ b/tests/phase22/azurestorage-helper-behaviour.test.cjs @@ -0,0 +1,125 @@ +const fs = require("fs"); +const path = require("path"); +const vm = require("vm"); +const assert = require("assert"); + +const rootDir = path.resolve(__dirname, "..", ".."); + +const loadAzureStorageHelperModule = (injected = {}) => { + const filePath = path.join(rootDir, "actions", "azurestorage.js"); + const source = fs.readFileSync(filePath, "utf8"); + + const start = source.indexOf("const buildDownloadBlobQueryPath ="); + const end = source.indexOf("export const createContainerSas ="); + + if (start < 0 || end < 0 || end <= start) { + throw new Error("Unable to isolate azurestorage helper function block"); + } + + let helperSource = source.slice(start, end); + helperSource += + "\nmodule.exports = { buildDownloadBlobQueryPath, buildDeleteBlobQueryPath, buildGetBlobListQueryPath, buildHashMetadataPaths };\n"; + + const context = { + module: { exports: {} }, + exports: {}, + require, + encodeURIComponent, + hashAPIPath: (route) => `HASH(${route})`, + ...injected + }; + + vm.runInNewContext(helperSource, context, { filename: filePath }); + return context.module.exports; +}; + +const tests = []; +const test = (name, fn) => tests.push({ name, fn }); + +test("azurestorage helper builds download path with default encoding", () => { + const mod = loadAzureStorageHelperModule(); + + const pathResult = mod.buildDownloadBlobQueryPath({ + containerName: "alpha", + casefolderID: "A/B", + blobname: "my doc.pdf" + }); + + assert.strictEqual( + pathResult, + "/api/file/downloadblob?container=alpha&casefolderID=A%2FB&blobname=my%20doc.pdf" + ); +}); + +test("azurestorage helper preserves raw values when encoding disabled", () => { + const mod = loadAzureStorageHelperModule(); + + const deletePath = mod.buildDeleteBlobQueryPath({ + containerName: "alpha", + casefolderID: "A/B", + blobname: "my doc.pdf", + encodeCasefolderID: false, + encodeBlobname: false + }); + + assert.strictEqual( + deletePath, + "/api/file/deleteblob?container=alpha&casefolderID=A/B&blobname=my doc.pdf" + ); +}); + +test("azurestorage helper builds getbloblist path without field mutation", () => { + const mod = loadAzureStorageHelperModule(); + + const listPath = mod.buildGetBlobListQueryPath({ + containerName: "alpha", + casefolderID: "A/B" + }); + + assert.strictEqual( + listPath, + "/api/file/getbloblist?container=alpha&casefolderID=A/B" + ); +}); + +test("azurestorage helper builds hash metadata map with stable keys", () => { + const mod = loadAzureStorageHelperModule({ + hashAPIPath: (route) => `signed:${route}` + }); + + const metadata = mod.buildHashMetadataPaths({ + containerName: "alpha", + casefolderID: "A/B", + blobname: "my doc.pdf" + }); + + assert.deepStrictEqual(JSON.parse(JSON.stringify(metadata)), { + hashedfilepath: + "signed:/api/file/downloadblob?container=alpha&casefolderID=A%2FB&blobname=my%20doc.pdf", + hasheddeletepath: + "signed:/api/file/deleteblob?container=alpha&casefolderID=A%2FB&blobname=my%20doc.pdf", + hashgetblobs: + "signed:/api/file/getbloblist?container=alpha&casefolderID=A/B" + }); +}); + +const run = async () => { + let passed = 0; + for (const currentTest of tests) { + await currentTest.fn(); + passed += 1; + } + + console.log( + `Phase 22 azurestorage-helper 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 f5f3f3eb..02d4341c 100644 --- a/tests/phase22/index.test.cjs +++ b/tests/phase22/index.test.cjs @@ -5,6 +5,7 @@ const runCaseServiceTests = require("./case-service-behaviour.test.cjs"); const runPortalServiceTests = require("./portal-service-behaviour.test.cjs"); const runAuthRedirectSafetyTests = require("./auth-redirect-safety.test.cjs"); const runI18nRouteParityTests = require("./i18n-route-parity.test.cjs"); +const runAzurestorageHelperTests = require("./azurestorage-helper-behaviour.test.cjs"); const run = async () => { await runCoreTokenTests(); @@ -14,6 +15,7 @@ const run = async () => { await runPortalServiceTests(); await runAuthRedirectSafetyTests(); await runI18nRouteParityTests(); + await runAzurestorageHelperTests(); console.log("Phase 22 combined suite passed."); }; From 97912706706449efa16e4422bb840d856a4f4264 Mon Sep 17 00:00:00 2001 From: robbond Date: Thu, 26 Mar 2026 11:13:48 +0000 Subject: [PATCH 09/14] TASK22269: tidy azurestorage blob path split locals --- actions/azurestorage.js | 47 +++++++++++++++++++-------------------- memory-bank/change-log.md | 31 ++++++++++++++++++++++++++ 2 files changed, 54 insertions(+), 24 deletions(-) diff --git a/actions/azurestorage.js b/actions/azurestorage.js index 48b8e66d..3cc4f2d2 100644 --- a/actions/azurestorage.js +++ b/actions/azurestorage.js @@ -267,16 +267,16 @@ export const getBlobs = async (containerName, casefolderID) => { for await (const blob of containerClient.listBlobsFlat({ prefix: casefolderID + "/files/" })) { + const blobPathParts = blob.name.split("/"); + const fileName = blobPathParts[2]; let blobDocumentType = blob.name .split("/")[2] .slice(0, blob.name.split("/")[2].indexOf("_")); blobObj.push({ - "name": blob.name.split("/")[2], + "name": fileName, "path": blob.name, - "documentType": getDocumentTypeFromFilename( - blob.name.split("/")[2] - ), + "documentType": getDocumentTypeFromFilename(fileName), "versionId": blob.versionId, "caseObj": casefolderID + "/" + casefolderID + "_case.json", "isCurrentVersion": blob.isCurrentVersion, @@ -294,7 +294,7 @@ export const getBlobs = async (containerName, casefolderID) => { ...buildHashMetadataPaths({ containerName, casefolderID, - blobname: blob.name.split("/")[2] + blobname: fileName }), "deletepath": "/api/file/deleteblob?container=" + @@ -302,7 +302,7 @@ export const getBlobs = async (containerName, casefolderID) => { "&casefolderID=" + encodeURIComponent(casefolderID) + "&blobname=" + - encodeURIComponent(blob.name.split("/")[2]) + encodeURIComponent(fileName) }); } //console.log("blobObj:", blobObj); @@ -1296,9 +1296,12 @@ export const getProgressBlobs = async (containerName, caseReference) => { for await (const blob of containerClient.listBlobsFlat({ prefix: caseReference + "/" + caseReference + "_appeal.json" })) { - console.log("getProgressBlobs in here", blob.name.split("/")); + const blobPathParts = blob.name.split("/"); + const appealBlobName = blobPathParts[1]; + + console.log("getProgressBlobs in here", blobPathParts); blobObj.push({ - "name": blob.name.split("/")[1], + "name": appealBlobName, "path": blob.name, "versionId": blob.versionId, "caseObj": caseReference + "/" + caseReference + "_case.json", @@ -1309,7 +1312,7 @@ export const getProgressBlobs = async (containerName, caseReference) => { ...buildHashMetadataPaths({ containerName, casefolderID: caseReference, - blobname: blob.name.split("/")[1] + blobname: appealBlobName }) }); } @@ -1473,6 +1476,9 @@ export const getRepsFilesBlobs = async ( for await (const blob of containerClient.listBlobsFlat({ prefix: casefolderID + "/" + filenamePrefix + "/files/" })) { + const blobPathParts = blob.name.split("/"); + const casefolderPath = blobPathParts[0] + "/" + blobPathParts[1]; + const repFileName = blobPathParts[3]; let blobDocumentType = blob.name .split("/")[2] .slice(0, blob.name.split("/")[2].indexOf("_")); @@ -1480,11 +1486,9 @@ export const getRepsFilesBlobs = async ( console.log(blob.name); blobObj.push({ - "name": blob.name.split("/")[3], + "name": repFileName, "path": blob.name, - "documentType": getDocumentTypeFromFilename( - blob.name.split("/")[3] - ), + "documentType": getDocumentTypeFromFilename(repFileName), "versionId": blob.versionId, "isCurrentVersion": blob.isCurrentVersion, "contentLength": blob.properties.contentLength, @@ -1493,26 +1497,21 @@ export const getRepsFilesBlobs = async ( "/api/file/downloadblob?container=" + containerName + "&casefolderID=" + - encodeURIComponent( - blob.name.split("/")[0] + "/" + blob.name.split("/")[1] - ) + + encodeURIComponent(casefolderPath) + "&blobname=" + - encodeURIComponent(blob.name.split("/")[3]), + encodeURIComponent(repFileName), ...buildHashMetadataPaths({ containerName, - casefolderID: - blob.name.split("/")[0] + "/" + blob.name.split("/")[1], - blobname: blob.name.split("/")[3] + casefolderID: casefolderPath, + blobname: repFileName }), "deletepath": "/api/file/deleteblob?container=" + containerName + "&casefolderID=" + - blob.name.split("/")[0] + - "/" + - blob.name.split("/")[1] + + casefolderPath + "&blobname=" + - blob.name.split("/")[3] + repFileName }); } console.log("blobObj:", blobObj); diff --git a/memory-bank/change-log.md b/memory-bank/change-log.md index 15b9892a..d804bdab 100644 --- a/memory-bank/change-log.md +++ b/memory-bank/change-log.md @@ -3007,3 +3007,34 @@ Validation: Follow-ups: - Optional next bounded slice: add an explicit assertion for encoded `casefolderID` variants containing reserved query characters (`?`, `&`) if those inputs are expected in future flows. + +--- + +### CL-084: TASK22269 Slice B1.9 — azurestorage local split-value tidy in touched helper consumers + +date: 2026-03-26 +author: Cline +scope: `actions/azurestorage.js` +type: change +rationale: Execute the selected next bounded readability-only slice by reducing repeated `blob.name.split("/")` access in the recently touched helper-consumer functions. +impact: Non-behavioral maintainability improvement in azurestorage helper-consumer paths; no API/route contract changes. +status: completed + +Summary: + +- In targeted functions (`getBlobs`, `getProgressBlobs`, `getRepsFilesBlobs`), introduced local path-part variables to avoid repeated inline splitting: + - `blobPathParts` + - `fileName` / `appealBlobName` / `repFileName` + - `casefolderPath` +- Replaced repeated field reads and helper arguments with these locals in object construction and hash metadata composition. +- Preserved existing query composition and output shape/keys (including hashed path metadata fields). + +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: + +- Optional next bounded slice: logging-only normalization in these same azurestorage functions (no behavior change), done separately from structural refactors. From 427e1c46739b489133069f49dd1a7a2ceefa8e27 Mon Sep 17 00:00:00 2001 From: robbond Date: Thu, 26 Mar 2026 11:15:33 +0000 Subject: [PATCH 10/14] TASK22269: normalize touched azurestorage logging calls --- actions/azurestorage.js | 6 +++--- memory-bank/change-log.md | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 3 deletions(-) diff --git a/actions/azurestorage.js b/actions/azurestorage.js index 3cc4f2d2..773ad7cb 100644 --- a/actions/azurestorage.js +++ b/actions/azurestorage.js @@ -1299,7 +1299,7 @@ export const getProgressBlobs = async (containerName, caseReference) => { const blobPathParts = blob.name.split("/"); const appealBlobName = blobPathParts[1]; - console.log("getProgressBlobs in here", blobPathParts); + consoleLogger("getProgressBlobs in here", blobPathParts); blobObj.push({ "name": appealBlobName, "path": blob.name, @@ -1483,7 +1483,7 @@ export const getRepsFilesBlobs = async ( .split("/")[2] .slice(0, blob.name.split("/")[2].indexOf("_")); - console.log(blob.name); + consoleLogger(blob.name); blobObj.push({ "name": repFileName, @@ -1514,7 +1514,7 @@ export const getRepsFilesBlobs = async ( repFileName }); } - console.log("blobObj:", blobObj); + consoleLogger("blobObj:", blobObj); return blobObj; }; diff --git a/memory-bank/change-log.md b/memory-bank/change-log.md index d804bdab..5b3884bb 100644 --- a/memory-bank/change-log.md +++ b/memory-bank/change-log.md @@ -3038,3 +3038,36 @@ Validation: Follow-ups: - Optional next bounded slice: logging-only normalization in these same azurestorage functions (no behavior change), done separately from structural refactors. + +--- + +### CL-085: TASK22269 Slice B1.10 — azurestorage touched-function logging normalization + +date: 2026-03-26 +author: Cline +scope: `actions/azurestorage.js` +type: change +rationale: Execute the next bounded, logging-only slice by normalizing selected touched-function logs to `consoleLogger` for consistency with current helper/error logging style. +impact: Observability consistency improvement only; no API/route behavior or payload contract changes. +status: completed + +Summary: + +- In previously touched helper-consumer functions only: + - `getProgressBlobs` + - `getRepsFilesBlobs` +- Replaced selected direct `console.log(...)` calls with `consoleLogger(...)`: + - progress blob path-parts trace + - per-blob name trace in reps file listing + - final `blobObj` trace in reps file listing +- Scope intentionally excludes broader file-wide logging normalization to keep risk bounded. + +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: + +- Optional next bounded slice: prune currently-unused local `blobDocumentType` variables in the same touched functions (readability-only, no behavior change). From 361328c14a319337a6d6bf5e08fea5d022ddf4b4 Mon Sep 17 00:00:00 2001 From: robbond Date: Thu, 26 Mar 2026 11:17:09 +0000 Subject: [PATCH 11/14] TASK22269: prune touched azurestorage unused locals --- actions/azurestorage.js | 6 ------ memory-bank/change-log.md | 29 +++++++++++++++++++++++++++++ 2 files changed, 29 insertions(+), 6 deletions(-) diff --git a/actions/azurestorage.js b/actions/azurestorage.js index 773ad7cb..eb6b748d 100644 --- a/actions/azurestorage.js +++ b/actions/azurestorage.js @@ -269,9 +269,6 @@ export const getBlobs = async (containerName, casefolderID) => { })) { const blobPathParts = blob.name.split("/"); const fileName = blobPathParts[2]; - let blobDocumentType = blob.name - .split("/")[2] - .slice(0, blob.name.split("/")[2].indexOf("_")); blobObj.push({ "name": fileName, @@ -1479,9 +1476,6 @@ export const getRepsFilesBlobs = async ( const blobPathParts = blob.name.split("/"); const casefolderPath = blobPathParts[0] + "/" + blobPathParts[1]; const repFileName = blobPathParts[3]; - let blobDocumentType = blob.name - .split("/")[2] - .slice(0, blob.name.split("/")[2].indexOf("_")); consoleLogger(blob.name); diff --git a/memory-bank/change-log.md b/memory-bank/change-log.md index 5b3884bb..789d170d 100644 --- a/memory-bank/change-log.md +++ b/memory-bank/change-log.md @@ -3071,3 +3071,32 @@ Validation: Follow-ups: - Optional next bounded slice: prune currently-unused local `blobDocumentType` variables in the same touched functions (readability-only, no behavior change). + +--- + +### CL-086: TASK22269 Slice B1.11 — azurestorage touched-function unused-local prune + +date: 2026-03-26 +author: Cline +scope: `actions/azurestorage.js` +type: change +rationale: Execute the next bounded readability-only slice by removing now-unused local variables left in recently touched helper-consumer functions. +impact: Maintainability/readability improvement only; no API/route behavior changes. +status: completed + +Summary: + +- Removed unused local `blobDocumentType` declarations from: + - `getBlobs` + - `getRepsFilesBlobs` +- No object shape, query generation, hash metadata logic, or routing behavior changed. + +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: + +- Optional next bounded slice: align remaining low-risk direct `console.log` calls in these functions to `consoleLogger` only where already touched and safe. From 5c62f3088e9802049a96b2c3e2dc70815ca6398f Mon Sep 17 00:00:00 2001 From: robbond Date: Thu, 26 Mar 2026 11:18:50 +0000 Subject: [PATCH 12/14] TASK22269: reuse azurestorage path builders in touched blob object fields --- actions/azurestorage.js | 50 +++++++++++++++++---------------------- memory-bank/change-log.md | 32 +++++++++++++++++++++++++ 2 files changed, 54 insertions(+), 28 deletions(-) diff --git a/actions/azurestorage.js b/actions/azurestorage.js index eb6b748d..ac3c5519 100644 --- a/actions/azurestorage.js +++ b/actions/azurestorage.js @@ -281,25 +281,21 @@ export const getBlobs = async (containerName, casefolderID) => { "size": blob.properties.contentLength, "contentType": blob.contentType, "lastModified": blob.properties.lastModified, - "filepath": - "/api/file/downloadblob?container=" + - containerName + - "&casefolderID=" + - encodeURIComponent(casefolderID) + - "&blobname=" + - encodeURIComponent(blob.name), + "filepath": buildDownloadBlobQueryPath({ + containerName, + casefolderID, + blobname: blob.name + }), ...buildHashMetadataPaths({ containerName, casefolderID, blobname: fileName }), - "deletepath": - "/api/file/deleteblob?container=" + - containerName + - "&casefolderID=" + - encodeURIComponent(casefolderID) + - "&blobname=" + - encodeURIComponent(fileName) + "deletepath": buildDeleteBlobQueryPath({ + containerName, + casefolderID, + blobname: fileName + }) }); } //console.log("blobObj:", blobObj); @@ -1487,25 +1483,23 @@ export const getRepsFilesBlobs = async ( "isCurrentVersion": blob.isCurrentVersion, "contentLength": blob.properties.contentLength, "filenameprefix": filenamePrefix, - "filepath": - "/api/file/downloadblob?container=" + - containerName + - "&casefolderID=" + - encodeURIComponent(casefolderPath) + - "&blobname=" + - encodeURIComponent(repFileName), + "filepath": buildDownloadBlobQueryPath({ + containerName, + casefolderID: casefolderPath, + blobname: repFileName + }), ...buildHashMetadataPaths({ containerName, casefolderID: casefolderPath, blobname: repFileName }), - "deletepath": - "/api/file/deleteblob?container=" + - containerName + - "&casefolderID=" + - casefolderPath + - "&blobname=" + - repFileName + "deletepath": buildDeleteBlobQueryPath({ + containerName, + casefolderID: casefolderPath, + blobname: repFileName, + encodeCasefolderID: false, + encodeBlobname: false + }) }); } consoleLogger("blobObj:", blobObj); diff --git a/memory-bank/change-log.md b/memory-bank/change-log.md index 789d170d..1646a9be 100644 --- a/memory-bank/change-log.md +++ b/memory-bank/change-log.md @@ -3100,3 +3100,35 @@ Validation: Follow-ups: - Optional next bounded slice: align remaining low-risk direct `console.log` calls in these functions to `consoleLogger` only where already touched and safe. + +--- + +### CL-087: TASK22269 Slice B1.12 — azurestorage touched-function path assembly helper reuse + +date: 2026-03-26 +author: Cline +scope: `actions/azurestorage.js` +type: change +rationale: Execute the next bounded maintainability slice by reusing existing local query-path helpers for touched `filepath`/`deletepath` assembly, reducing repeated literal concatenation. +impact: Readability/consistency improvement only; preserves query parameter values and route behavior. +status: completed + +Summary: + +- In touched functions: + - `getBlobs` + - `getRepsFilesBlobs` +- Replaced inline `filepath` string concatenation with `buildDownloadBlobQueryPath(...)`. +- Replaced inline `deletepath` string concatenation with `buildDeleteBlobQueryPath(...)`. +- Preserved previous encoding behavior where required by passing explicit options: + - kept non-encoded `casefolderID`/`blobname` behavior in `getRepsFilesBlobs.deletepath` via helper options. + +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: + +- Optional next bounded slice: targeted helper-consumer tidy in the same functions for any remaining repeated query-path literals outside touched object fields. From 48878e9ebd53cf260e6fe00e56205e7926ecf085 Mon Sep 17 00:00:00 2001 From: robbond Date: Thu, 26 Mar 2026 11:20:37 +0000 Subject: [PATCH 13/14] TASK22269: extract touched azurestorage case object path helper --- actions/azurestorage.js | 8 ++++++-- memory-bank/change-log.md | 30 ++++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 2 deletions(-) diff --git a/actions/azurestorage.js b/actions/azurestorage.js index ac3c5519..9bb8394b 100644 --- a/actions/azurestorage.js +++ b/actions/azurestorage.js @@ -84,6 +84,10 @@ const buildGetBlobListQueryPath = ({ containerName, casefolderID }) => { ); }; +const buildCaseObjectPath = (casefolderID) => { + return casefolderID + "/" + casefolderID + "_case.json"; +}; + const buildHashMetadataPaths = ({ containerName, casefolderID, blobname }) => { return { "hashedfilepath": hashAPIPath( @@ -275,7 +279,7 @@ export const getBlobs = async (containerName, casefolderID) => { "path": blob.name, "documentType": getDocumentTypeFromFilename(fileName), "versionId": blob.versionId, - "caseObj": casefolderID + "/" + casefolderID + "_case.json", + "caseObj": buildCaseObjectPath(casefolderID), "isCurrentVersion": blob.isCurrentVersion, "contentLength": blob.properties.contentLength, "size": blob.properties.contentLength, @@ -1297,7 +1301,7 @@ export const getProgressBlobs = async (containerName, caseReference) => { "name": appealBlobName, "path": blob.name, "versionId": blob.versionId, - "caseObj": caseReference + "/" + caseReference + "_case.json", + "caseObj": buildCaseObjectPath(caseReference), "isCurrentVersion": blob.isCurrentVersion, "contentLength": blob.properties.contentLength, "contentType": blob.contentType, diff --git a/memory-bank/change-log.md b/memory-bank/change-log.md index 1646a9be..fbd49187 100644 --- a/memory-bank/change-log.md +++ b/memory-bank/change-log.md @@ -3132,3 +3132,33 @@ Validation: Follow-ups: - Optional next bounded slice: targeted helper-consumer tidy in the same functions for any remaining repeated query-path literals outside touched object fields. + +--- + +### CL-088: TASK22269 Slice B1.13 — azurestorage touched-function caseObj helper reuse + +date: 2026-03-26 +author: Cline +scope: `actions/azurestorage.js` +type: change +rationale: Execute the next bounded readability slice by centralizing repeated case-object path composition in touched helper-consumer functions. +impact: Maintainability/readability improvement only; no route/query behavior changes. +status: completed + +Summary: + +- Added local helper `buildCaseObjectPath(casefolderID)`. +- Replaced repeated `caseObj` string assembly in touched functions: + - `getBlobs` + - `getProgressBlobs` +- Preserved existing `caseObj` output format (`/_case.json`). + +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: + +- Optional next bounded slice: continue tiny helper reuse in touched functions only if any duplicated path literals remain and can be reduced without behavior change. From 830e36d0c40947b6b1c87fbfd5485975225e296a Mon Sep 17 00:00:00 2001 From: robbond Date: Thu, 26 Mar 2026 11:36:47 +0000 Subject: [PATCH 14/14] TASK22269: reuse contentLength locals in touched azurestorage blob loops --- actions/azurestorage.js | 11 +++++++---- memory-bank/change-log.md | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 4 deletions(-) diff --git a/actions/azurestorage.js b/actions/azurestorage.js index 9bb8394b..02383819 100644 --- a/actions/azurestorage.js +++ b/actions/azurestorage.js @@ -273,6 +273,7 @@ export const getBlobs = async (containerName, casefolderID) => { })) { const blobPathParts = blob.name.split("/"); const fileName = blobPathParts[2]; + const contentLength = blob.properties.contentLength; blobObj.push({ "name": fileName, @@ -281,8 +282,8 @@ export const getBlobs = async (containerName, casefolderID) => { "versionId": blob.versionId, "caseObj": buildCaseObjectPath(casefolderID), "isCurrentVersion": blob.isCurrentVersion, - "contentLength": blob.properties.contentLength, - "size": blob.properties.contentLength, + "contentLength": contentLength, + "size": contentLength, "contentType": blob.contentType, "lastModified": blob.properties.lastModified, "filepath": buildDownloadBlobQueryPath({ @@ -1295,6 +1296,7 @@ export const getProgressBlobs = async (containerName, caseReference) => { })) { const blobPathParts = blob.name.split("/"); const appealBlobName = blobPathParts[1]; + const contentLength = blob.properties.contentLength; consoleLogger("getProgressBlobs in here", blobPathParts); blobObj.push({ @@ -1303,7 +1305,7 @@ export const getProgressBlobs = async (containerName, caseReference) => { "versionId": blob.versionId, "caseObj": buildCaseObjectPath(caseReference), "isCurrentVersion": blob.isCurrentVersion, - "contentLength": blob.properties.contentLength, + "contentLength": contentLength, "contentType": blob.contentType, "lastModified": blob.properties.lastModified, ...buildHashMetadataPaths({ @@ -1476,6 +1478,7 @@ export const getRepsFilesBlobs = async ( const blobPathParts = blob.name.split("/"); const casefolderPath = blobPathParts[0] + "/" + blobPathParts[1]; const repFileName = blobPathParts[3]; + const contentLength = blob.properties.contentLength; consoleLogger(blob.name); @@ -1485,7 +1488,7 @@ export const getRepsFilesBlobs = async ( "documentType": getDocumentTypeFromFilename(repFileName), "versionId": blob.versionId, "isCurrentVersion": blob.isCurrentVersion, - "contentLength": blob.properties.contentLength, + "contentLength": contentLength, "filenameprefix": filenamePrefix, "filepath": buildDownloadBlobQueryPath({ containerName, diff --git a/memory-bank/change-log.md b/memory-bank/change-log.md index fbd49187..c6e4d9b9 100644 --- a/memory-bank/change-log.md +++ b/memory-bank/change-log.md @@ -3162,3 +3162,35 @@ Validation: Follow-ups: - Optional next bounded slice: continue tiny helper reuse in touched functions only if any duplicated path literals remain and can be reduced without behavior change. + +--- + +### CL-089: TASK22269 Slice B1.14 — azurestorage touched-function contentLength local reuse + +date: 2026-03-26 +author: Cline +scope: `actions/azurestorage.js` +type: change +rationale: Execute the next tiny bounded readability slice by reusing local `contentLength` values in touched helper-consumer functions to reduce repeated property access and keep object assembly consistent. +impact: Maintainability/readability improvement only; no route/query/output behavior changes. +status: completed + +Summary: + +- In touched functions: + - `getBlobs` + - `getProgressBlobs` + - `getRepsFilesBlobs` +- Added local `contentLength` variable (`blob.properties.contentLength`) per loop iteration. +- Replaced repeated inline `blob.properties.contentLength` assignments in object assembly with the local variable. +- Preserved field contracts (`contentLength`, `size`) and values. + +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: + +- Optional next bounded slice: stop or switch scope; touched-function micro-tidies in this area are now largely exhausted.