From 10f221af38dbd46ddb59b170f1a30162e9109de1 Mon Sep 17 00:00:00 2001 From: robbond Date: Wed, 25 Mar 2026 12:04:11 +0000 Subject: [PATCH] test(portal): add phase22 behavioural closure and route harmonization --- actions/services/portalDirectService.js | 6 +- memory-bank/change-log.md | 42 +++++ tests/phase22/index.test.cjs | 2 + .../phase22/portal-service-behaviour.test.cjs | 157 ++++++++++++++++++ 4 files changed, 204 insertions(+), 3 deletions(-) create mode 100644 tests/phase22/portal-service-behaviour.test.cjs diff --git a/actions/services/portalDirectService.js b/actions/services/portalDirectService.js index 9adc69bc..54b1eb92 100644 --- a/actions/services/portalDirectService.js +++ b/actions/services/portalDirectService.js @@ -126,7 +126,7 @@ export const getAwaitingSubmission = (loggedInUserId) => { export const createWatchedCases = async (formValues) => { var data = formValues; - var queryUrl = "/api/endpoint/createwatchedcases_api"; + var queryUrl = buildFileQuery("/api/endpoint/createwatchedcases_api"); var config = { method: "post", @@ -286,7 +286,7 @@ export const sendRepCompleteMessage = async ( }; export const setRepInvolvment = async (caseid, contactid) => { - var queryUrl = "/api/file/createrepinvolvement_api"; + var queryUrl = buildFileQuery("/api/file/createrepinvolvement_api"); var data = { "incidentid": caseid, "contactid": contactid }; var config = { @@ -303,7 +303,7 @@ export const setRepInvolvment = async (caseid, contactid) => { }; export const setCaseInvolvment = async (caseid, contactid) => { - var queryUrl = "/api/file/createrepinvolvement_api"; + var queryUrl = buildFileQuery("/api/file/createrepinvolvement_api"); var data = { "incidentid": caseid, "contactid": contactid }; var config = { diff --git a/memory-bank/change-log.md b/memory-bank/change-log.md index 9808613a..28faf449 100644 --- a/memory-bank/change-log.md +++ b/memory-bank/change-log.md @@ -2639,3 +2639,45 @@ Validation: Follow-ups: - Optional closure slice: add a dedicated phase22 portal-service behavioural suite and finish harmonization of remaining direct query string composition in `portalDirectService` write helpers. + +--- + +### CL-074: TASK22260 optional closure condensed slice — portal phase22 behavioural coverage + final write-helper query harmonization + +date: 2026-03-25 +author: Cline +scope: `actions/services/portalDirectService.js`, `tests/phase22/{portal-service-behaviour,index}.test.cjs` +type: change +rationale: Complete optional closure work as one condensed slice by adding explicit portal behavioral coverage and removing remaining direct literal query strings in portal write helpers. +impact: Improves regression confidence for signed portal flows and closes remaining route-composition harmonization gap in portal service write helpers without changing runtime behavior contracts. +status: completed + +Summary: + +- Harmonized remaining portal write-helper route literals to `buildFileQuery(...)`: + - `createWatchedCases` + - `setRepInvolvment` + - `setCaseInvolvment` +- Added new phase22 suite `tests/phase22/portal-service-behaviour.test.cjs` covering: + - signed hash suffix append path in `sendCaseCompleteMessage` + - signed delete request contract + headers in `deleteMyRepresentations` + - harmonized route helper URL in `createWatchedCases` + - signed pre-request rejection contract in `sendRepCompleteMessage` when hash-signing fails +- Updated `tests/phase22/index.test.cjs` aggregate runner to include `portal-service-behaviour`. + +Validation: + +- `node tests/phase22/portal-service-behaviour.test.cjs` -> pass (4/4) +- `node tests/phase22/index.test.cjs` -> pass + - core-token: 2/2 + - client-utils: 6/6 + - file-client: 4/4 + - case-service: 4/4 + - portal-service: 4/4 + - phase22 combined: pass +- `node tests/phase7/service-behaviour.test.cjs` -> pass (13/13) +- `npm run lint` -> warnings only (pre-existing `react-hooks/exhaustive-deps`; no new lint errors) + +Follow-ups: + +- Closure for this condensed stream complete; any further work should be a separate expansion stream (e.g., additional service-level phase22 coverage breadth). diff --git a/tests/phase22/index.test.cjs b/tests/phase22/index.test.cjs index bacc8e98..7a2102d6 100644 --- a/tests/phase22/index.test.cjs +++ b/tests/phase22/index.test.cjs @@ -2,12 +2,14 @@ const runCoreTokenTests = require("./core-token-behaviour.test.cjs"); const runClientUtilsTests = require("./client-utils-behaviour.test.cjs"); const runFileClientTests = require("./file-client-behaviour.test.cjs"); const runCaseServiceTests = require("./case-service-behaviour.test.cjs"); +const runPortalServiceTests = require("./portal-service-behaviour.test.cjs"); const run = async () => { await runCoreTokenTests(); await runClientUtilsTests(); await runFileClientTests(); await runCaseServiceTests(); + await runPortalServiceTests(); 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 new file mode 100644 index 00000000..f1f33b4b --- /dev/null +++ b/tests/phase22/portal-service-behaviour.test.cjs @@ -0,0 +1,157 @@ +const assert = require("assert"); +const { + createAxiosMock, + createLoggerMock, + createAxiosError, + loadServiceModule, + normalize +} = require("../serviceHarness.cjs"); + +const tests = []; +const test = (name, fn) => tests.push({ name, fn }); + +test("portal/sendCaseCompleteMessage appends signed hash suffix to inv query", async () => { + const axios = createAxiosMock(); + const logger = createLoggerMock(); + + const requestCalls = []; + const requestJson = async (config) => { + requestCalls.push(config); + return { status: "ok" }; + }; + + const portal = loadServiceModule("portalDirectService.js", { + axios, + BASE_URL: "", + consoleLogger: logger.consoleLogger, + buildHashedQueryUrl: async (queryUrl) => `${queryUrl}&hash=abc123`, + requestJson + }); + + const result = await portal.sendCaseCompleteMessage( + "container1", + "CASE-1", + "yes" + ); + + assert.deepStrictEqual(normalize(result), { status: "ok" }); + assert.strictEqual(requestCalls.length, 1); + assert.strictEqual(requestCalls[0].method, "get"); + assert.strictEqual( + requestCalls[0].url, + "/api/file/createappealcompletemessage_api?container=container1&tempcaseref=CASE-1&inv=yes&hash=abc123" + ); +}); + +test("portal/deleteMyRepresentations uses signed delete request with headers", async () => { + const axios = createAxiosMock(); + const logger = createLoggerMock(); + + const requestCalls = []; + const requestJson = async (config) => { + requestCalls.push(config); + return { deleted: true }; + }; + + const portal = loadServiceModule("portalDirectService.js", { + axios, + BASE_URL: "", + consoleLogger: logger.consoleLogger, + buildHashedQueryUrl: async (queryUrl) => `${queryUrl}&hash=xyz`, + requestJson + }); + + const result = await portal.deleteMyRepresentations("rep-1"); + + assert.deepStrictEqual(normalize(result), { deleted: true }); + assert.strictEqual(requestCalls.length, 1); + assert.strictEqual(requestCalls[0].method, "delete"); + assert.strictEqual( + requestCalls[0].url, + "/api/endpoint/deletemyrepresentations_api?myRepresentationsID=rep-1&hash=xyz" + ); + assert.strictEqual( + requestCalls[0].headers["Content-Type"], + "application/json" + ); +}); + +test("portal/createWatchedCases posts to harmonized route helper URL", async () => { + const axios = createAxiosMock(); + const logger = createLoggerMock(); + + const requestCalls = []; + const requestJson = async (config) => { + requestCalls.push(config); + return { created: true }; + }; + + const portal = loadServiceModule("portalDirectService.js", { + axios, + BASE_URL: "", + consoleLogger: logger.consoleLogger, + requestJson + }); + + const payload = { foo: "bar" }; + const result = await portal.createWatchedCases(payload); + + assert.deepStrictEqual(normalize(result), { created: true }); + assert.strictEqual(requestCalls.length, 1); + assert.strictEqual(requestCalls[0].method, "post"); + assert.strictEqual( + requestCalls[0].url, + "/api/endpoint/createwatchedcases_api" + ); + assert.deepStrictEqual(normalize(requestCalls[0].data), payload); +}); + +test("portal/sendRepCompleteMessage rejects when hash signing fails before request stage", async () => { + const axios = createAxiosMock(); + const logger = createLoggerMock(); + const error = createAxiosError(500, "Failed"); + + const portal = loadServiceModule("portalDirectService.js", { + axios, + BASE_URL: "", + consoleLogger: logger.consoleLogger, + buildHashedQueryUrl: async () => { + throw error; + }, + requestJson: async () => { + throw new Error("should not be called"); + } + }); + + await assert.rejects( + () => portal.sendRepCompleteMessage("c", "r", "f"), + (caught) => { + assert.strictEqual(caught, error); + return true; + } + ); + + assert.strictEqual(logger.calls.length, 0); +}); + +const run = async () => { + let passed = 0; + + for (const currentTest of tests) { + await currentTest.fn(); + passed += 1; + } + + console.log( + `Phase 22 portal-service tests passed (${passed}/${tests.length}).` + ); +}; + +module.exports = run; + +if (require.main === module) { + run().catch((error) => { + console.error(error); + process.exit(1); + }); +}