diff --git a/actions/azurestorage.js b/actions/azurestorage.js index f75d6000..02383819 100644 --- a/actions/azurestorage.js +++ b/actions/azurestorage.js @@ -31,6 +31,88 @@ 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 + ); +}; + +const buildCaseObjectPath = (casefolderID) => { + return casefolderID + "/" + casefolderID + "_case.json"; +}; + +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 @@ -189,59 +271,36 @@ export const getBlobs = async (containerName, casefolderID) => { for await (const blob of containerClient.listBlobsFlat({ prefix: casefolderID + "/files/" })) { - let blobDocumentType = blob.name - .split("/")[2] - .slice(0, blob.name.split("/")[2].indexOf("_")); + const blobPathParts = blob.name.split("/"); + const fileName = blobPathParts[2]; + const contentLength = blob.properties.contentLength; 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", + "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": - "/api/file/downloadblob?container=" + - containerName + - "&casefolderID=" + - encodeURIComponent(casefolderID) + - "&blobname=" + - encodeURIComponent(blob.name), - "hashedfilepath": hashAPIPath( - "/api/file/downloadblob?container=" + - containerName + - "&casefolderID=" + - encodeURIComponent(casefolderID) + - "&blobname=" + - encodeURIComponent(blob.name.split("/")[2]) - ), - "deletepath": - "/api/file/deleteblob?container=" + - containerName + - "&casefolderID=" + - encodeURIComponent(casefolderID) + - "&blobname=" + - encodeURIComponent(blob.name.split("/")[2]), - "hasheddeletepath": hashAPIPath( - "/api/file/deleteblob?container=" + - containerName + - "&casefolderID=" + - encodeURIComponent(casefolderID) + - "&blobname=" + - encodeURIComponent(blob.name.split("/")[2]) - ), - "hashgetblobs": hashAPIPath( - "/api/file/getbloblist?container=" + - containerName + - "&casefolderID=" + - casefolderID - ) + "filepath": buildDownloadBlobQueryPath({ + containerName, + casefolderID, + blobname: blob.name + }), + ...buildHashMetadataPaths({ + containerName, + casefolderID, + blobname: fileName + }), + "deletepath": buildDeleteBlobQueryPath({ + containerName, + casefolderID, + blobname: fileName + }) }); } //console.log("blobObj:", blobObj); @@ -1235,38 +1294,25 @@ 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]; + const contentLength = blob.properties.contentLength; + + consoleLogger("getProgressBlobs in here", blobPathParts); blobObj.push({ - "name": blob.name.split("/")[1], + "name": appealBlobName, "path": blob.name, "versionId": blob.versionId, - "caseObj": caseReference + "/" + caseReference + "_case.json", + "caseObj": buildCaseObjectPath(caseReference), "isCurrentVersion": blob.isCurrentVersion, - "contentLength": blob.properties.contentLength, + "contentLength": contentLength, "contentType": blob.contentType, "lastModified": blob.properties.lastModified, - "hashedfilepath": hashAPIPath( - "/api/file/downloadblob?container=" + - containerName + - "&casefolderID=" + - encodeURIComponent(caseReference) + - "&blobname=" + - encodeURIComponent(blob.name.split("/")[1]) - ), - "hasheddeletepath": hashAPIPath( - "/api/file/deleteblob?container=" + - containerName + - "&casefolderID=" + - encodeURIComponent(caseReference) + - "&blobname=" + - encodeURIComponent(blob.name.split("/")[1]) - ), - "hashgetblobs": hashAPIPath( - "/api/file/getbloblist?container=" + - containerName + - "&casefolderID=" + - caseReference - ) + ...buildHashMetadataPaths({ + containerName, + casefolderID: caseReference, + blobname: appealBlobName + }) }); } @@ -1429,71 +1475,41 @@ export const getRepsFilesBlobs = async ( for await (const blob of containerClient.listBlobsFlat({ prefix: casefolderID + "/" + filenamePrefix + "/files/" })) { - let blobDocumentType = blob.name - .split("/")[2] - .slice(0, blob.name.split("/")[2].indexOf("_")); + const blobPathParts = blob.name.split("/"); + const casefolderPath = blobPathParts[0] + "/" + blobPathParts[1]; + const repFileName = blobPathParts[3]; + const contentLength = blob.properties.contentLength; - console.log(blob.name); + consoleLogger(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, + "contentLength": contentLength, "filenameprefix": filenamePrefix, - "filepath": - "/api/file/downloadblob?container=" + - containerName + - "&casefolderID=" + - encodeURIComponent( - blob.name.split("/")[0] + "/" + blob.name.split("/")[1] - ) + - "&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]) - ), - "deletepath": - "/api/file/deleteblob?container=" + - containerName + - "&casefolderID=" + - blob.name.split("/")[0] + - "/" + - blob.name.split("/")[1] + - "&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]) - ), - "hashgetblobs": hashAPIPath( - "/api/file/getbloblist?container=" + - containerName + - "&casefolderID=" + - blob.name.split("/")[0] + - "/" + - blob.name.split("/")[1] - ) + "filepath": buildDownloadBlobQueryPath({ + containerName, + casefolderID: casefolderPath, + blobname: repFileName + }), + ...buildHashMetadataPaths({ + containerName, + casefolderID: casefolderPath, + blobname: repFileName + }), + "deletepath": buildDeleteBlobQueryPath({ + containerName, + casefolderID: casefolderPath, + blobname: repFileName, + encodeCasefolderID: false, + encodeBlobname: false + }) }); } - console.log("blobObj:", blobObj); + consoleLogger("blobObj:", blobObj); return blobObj; }; 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/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/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..744c2da1 --- /dev/null +++ b/actions/clients/signedRequestClient.js @@ -0,0 +1,45 @@ +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 { baseUrl, ...requestConfig } = config; + const signedUrl = await buildSignedUrl(queryUrl, { baseUrl }); + + return requestJson({ + method, + url: signedUrl, + data, + ...requestConfig + }); +}; + +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/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/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/actions/services/portalDirectService.js b/actions/services/portalDirectService.js index 54b1eb92..8b6a9654 100644 --- a/actions/services/portalDirectService.js +++ b/actions/services/portalDirectService.js @@ -1,7 +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, + buildSignedUrl +} from "../clients/signedRequestClient"; import { buildFileQuery, withBaseUrl, @@ -146,24 +149,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) => { @@ -189,16 +185,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 ( @@ -220,7 +209,7 @@ export const sendCaseCompleteMessage = async ( inv }); - var signedQueryUrl = await buildHashedQueryUrl(hashQueryPath); + var signedQueryUrl = await buildSignedUrl(hashQueryPath); queryUrl = appendQuerySuffix( queryUrl, @@ -273,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 269a4093..c6e4d9b9 100644 --- a/memory-bank/change-log.md +++ b/memory-bank/change-log.md @@ -2725,3 +2725,472 @@ 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). + +--- + +### 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). + +--- + +### 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. + +--- + +### 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. + +--- + +### 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). + +--- + +### 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. + +--- + +### 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. + +--- + +### 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. + +--- + +### 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. + +--- + +### 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). + +--- + +### 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. + +--- + +### 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. + +--- + +### 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. + +--- + +### 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. 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/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/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" ); }); 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."); }; diff --git a/tests/phase22/portal-service-behaviour.test.cjs b/tests/phase22/portal-service-behaviour.test.cjs index f1f33b4b..26ac9b13 100644 --- a/tests/phase22/portal-service-behaviour.test.cjs +++ b/tests/phase22/portal-service-behaviour.test.cjs @@ -134,6 +134,68 @@ 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(); + 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/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 4cf494ff..aea3882b 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 @@ -126,6 +135,45 @@ 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 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]) => { @@ -152,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: {} }, @@ -172,12 +222,17 @@ 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, withBaseUrl: injected.withBaseUrl || defaultWithBaseUrl, appendQuerySuffix: injected.appendQuerySuffix || defaultAppendQuerySuffix, + appendHashSuffix: injected.appendHashSuffix || defaultAppendHashSuffix, ...injected };