diff --git a/memory-bank/change-log.md b/memory-bank/change-log.md index a8866a71..c75647eb 100644 --- a/memory-bank/change-log.md +++ b/memory-bank/change-log.md @@ -1369,3 +1369,41 @@ Validation: Follow-ups: - Optional next slice: extract shared relay policy presets into constants to reduce duplication and enforce profile consistency across remaining relayGet endpoints. + +--- + +### CL-037: TASK22242 portal/my-cases relayPolicy parity (my portal + representations) + +date: 2026-03-24 +author: Cline +scope: `pages/api/endpoint/{getmycases_api,getmyrepresentations_api,getwatchedcases_api,getawaitingsubmission_api,getrepresentations_api}.js`, `tests/phase21/endpoint-handler-contract.test.cjs` +type: change +rationale: Continue the branch-by-branch relay policy rollout by applying explicit policy posture to core my-portal retrieval endpoints. +impact: Improves consistency and operational predictability of relay behavior for portal case/representation listing flows without changing endpoint contracts. +status: completed + +Summary: + +- Added explicit `relayPolicy` declarations to: + - `getmycases_api` + - `getmyrepresentations_api` + - `getwatchedcases_api` + - `getawaitingsubmission_api` + - `getrepresentations_api` +- Applied bounded read profile across the batch: + - `method: "GET"` + - `timeoutMs: 8000` + - `maxRetries: 2` + - `retryBaseDelayMs: 150` + - `retryMaxDelayMs: 800` +- Extended phase21 endpoint contract tests with relayPolicy pass-through assertions for each endpoint. + +Validation: + +- `node tests/phase21/endpoint-handler-contract.test.cjs` -> pass (164/164) +- `node tests/phase21/relay-forwarding-hardening.test.cjs` -> pass (10/10) +- `npm run lint` -> warnings only (pre-existing `react-hooks/exhaustive-deps`; no new lint errors) + +Follow-ups: + +- Optional next slice: extract shared relay policy presets into a constants module and reference them from all relayGet endpoints to reduce duplication. diff --git a/pages/api/endpoint/getawaitingsubmission_api.js b/pages/api/endpoint/getawaitingsubmission_api.js index b48bf1fc..543eca66 100644 --- a/pages/api/endpoint/getawaitingsubmission_api.js +++ b/pages/api/endpoint/getawaitingsubmission_api.js @@ -39,9 +39,18 @@ export default async function ApiProxy(req, res) { loggedInUserId + " and servicestage eq 1 and pinswg_appealcasetype ne null&$orderby=createdon desc&$count=true"; + const relayPolicy = { + method: "GET", + timeoutMs: 8000, + maxRetries: 2, + retryBaseDelayMs: 150, + retryMaxDelayMs: 800 + }; + return relayGet({ queryUrl, res, + relayPolicy, transformData: (data) => { data.value.forEach(function (element) { element.pinswg_title = element.title; diff --git a/pages/api/endpoint/getmycases_api.js b/pages/api/endpoint/getmycases_api.js index c5e521bc..bd712165 100644 --- a/pages/api/endpoint/getmycases_api.js +++ b/pages/api/endpoint/getmycases_api.js @@ -39,9 +39,18 @@ export default async function ApiProxy(req, res) { loggedInUserId + " and pinswg_appealcasetype ne null&$orderby=createdon desc&$count=true"; + const relayPolicy = { + method: "GET", + timeoutMs: 8000, + maxRetries: 2, + retryBaseDelayMs: 150, + retryMaxDelayMs: 800 + }; + return relayGet({ queryUrl, res, + relayPolicy, transformData: (data) => { data.value.forEach(function (element) { element.pinswg_title = element.title; diff --git a/pages/api/endpoint/getmyrepresentations_api.js b/pages/api/endpoint/getmyrepresentations_api.js index 0b4d738e..5bbb251d 100644 --- a/pages/api/endpoint/getmyrepresentations_api.js +++ b/pages/api/endpoint/getmyrepresentations_api.js @@ -39,9 +39,18 @@ export default async function ApiProxy(req, res) { loggedInUserId + "&$count=true&$orderby=createdon desc"; + const relayPolicy = { + method: "GET", + timeoutMs: 8000, + maxRetries: 2, + retryBaseDelayMs: 150, + retryMaxDelayMs: 800 + }; + return relayGet({ queryUrl, res, + relayPolicy, errorResponse: { status: 400, code: "MY_REPRESENTATIONS_FETCH_FAILED", diff --git a/pages/api/endpoint/getrepresentations_api.js b/pages/api/endpoint/getrepresentations_api.js index 9d8eb54b..55a32596 100644 --- a/pages/api/endpoint/getrepresentations_api.js +++ b/pages/api/endpoint/getrepresentations_api.js @@ -36,9 +36,18 @@ export default async function ApiProxy(req, res) { incidentID + " and pinswg_publishtoweb eq true&$count=true&$orderby=createdon desc"; + const relayPolicy = { + method: "GET", + timeoutMs: 8000, + maxRetries: 2, + retryBaseDelayMs: 150, + retryMaxDelayMs: 800 + }; + return relayGet({ queryUrl, res, + relayPolicy, errorResponse: { status: 400, code: "REPRESENTATIONS_FETCH_FAILED", diff --git a/pages/api/endpoint/getwatchedcases_api.js b/pages/api/endpoint/getwatchedcases_api.js index ed90cf73..a168fe76 100644 --- a/pages/api/endpoint/getwatchedcases_api.js +++ b/pages/api/endpoint/getwatchedcases_api.js @@ -39,9 +39,18 @@ export default async function ApiProxy(req, res) { loggedInUserId + "&$select=pinswg_emailnotifications,modifiedon,pinswg_appealcasetype,pinswg_watchlistid,_pinswg_watchedcase_value,statuscode,pinswg_representationsubmitted,pinswg_representationtype&$count=true&$orderby=createdon desc&$expand=pinswg_WatchedCase($select=pinswg_AssociatedLPA)"; + const relayPolicy = { + method: "GET", + timeoutMs: 8000, + maxRetries: 2, + retryBaseDelayMs: 150, + retryMaxDelayMs: 800 + }; + return relayGet({ queryUrl, res, + relayPolicy, transformData: (data) => { data.value.forEach(function (element) { element.ticketnumber = element.pinswg_WatchedCase?.ticketnumber; diff --git a/tests/phase21/endpoint-handler-contract.test.cjs b/tests/phase21/endpoint-handler-contract.test.cjs index 16c1b95e..90a48c71 100644 --- a/tests/phase21/endpoint-handler-contract.test.cjs +++ b/tests/phase21/endpoint-handler-contract.test.cjs @@ -1725,6 +1725,32 @@ test("getmycases catch path returns MY_CASES_FETCH_FAILED", async () => { assert.strictEqual(res.state.jsonBody.error.code, "MY_CASES_FETCH_FAILED"); }); +test("getmycases passes relayPolicy overrides to relayGet", async () => { + let capturedRelayPolicy = null; + + const mod = loadModule("pages/api/endpoint/getmycases_api.js", { + respondError: respondErrorMock, + respondSuccess: respondSuccessMock, + relayGet: async ({ relayPolicy, res }) => { + capturedRelayPolicy = relayPolicy; + return respondSuccessMock(res, { value: [] }); + } + }); + + const req = { query: { loggedInUserId: "c1" } }; + const res = createRes(); + await mod.default(req, res); + + assert.strictEqual(res.state.statusCode, 200); + assert.deepStrictEqual(JSON.parse(JSON.stringify(capturedRelayPolicy)), { + method: "GET", + timeoutMs: 8000, + maxRetries: 2, + retryBaseDelayMs: 150, + retryMaxDelayMs: 800 + }); +}); + test("getmyrepresentations returns LOGGED_IN_USER_ID_REQUIRED when loggedInUserId missing", async () => { const mod = loadModule("pages/api/endpoint/getmyrepresentations_api.js", { respondError: respondErrorMock, @@ -1776,6 +1802,32 @@ test("getmyrepresentations catch path returns MY_REPRESENTATIONS_FETCH_FAILED", ); }); +test("getmyrepresentations passes relayPolicy overrides to relayGet", async () => { + let capturedRelayPolicy = null; + + const mod = loadModule("pages/api/endpoint/getmyrepresentations_api.js", { + respondError: respondErrorMock, + respondSuccess: respondSuccessMock, + relayGet: async ({ relayPolicy, res }) => { + capturedRelayPolicy = relayPolicy; + return respondSuccessMock(res, { value: [] }); + } + }); + + const req = { query: { loggedInUserId: "c1" } }; + const res = createRes(); + await mod.default(req, res); + + assert.strictEqual(res.state.statusCode, 200); + assert.deepStrictEqual(JSON.parse(JSON.stringify(capturedRelayPolicy)), { + method: "GET", + timeoutMs: 8000, + maxRetries: 2, + retryBaseDelayMs: 150, + retryMaxDelayMs: 800 + }); +}); + test("getwatchedcases returns LOGGED_IN_USER_ID_REQUIRED when loggedInUserId missing", async () => { const mod = loadModule("pages/api/endpoint/getwatchedcases_api.js", { respondError: respondErrorMock, @@ -1827,6 +1879,32 @@ test("getwatchedcases catch path returns WATCHED_CASES_FETCH_FAILED", async () = ); }); +test("getwatchedcases passes relayPolicy overrides to relayGet", async () => { + let capturedRelayPolicy = null; + + const mod = loadModule("pages/api/endpoint/getwatchedcases_api.js", { + respondError: respondErrorMock, + respondSuccess: respondSuccessMock, + relayGet: async ({ relayPolicy, res }) => { + capturedRelayPolicy = relayPolicy; + return respondSuccessMock(res, { value: [] }); + } + }); + + const req = { query: { loggedInUserId: "c1" } }; + const res = createRes(); + await mod.default(req, res); + + assert.strictEqual(res.state.statusCode, 200); + assert.deepStrictEqual(JSON.parse(JSON.stringify(capturedRelayPolicy)), { + method: "GET", + timeoutMs: 8000, + maxRetries: 2, + retryBaseDelayMs: 150, + retryMaxDelayMs: 800 + }); +}); + test("getawaitingsubmission returns LOGGED_IN_USER_ID_REQUIRED when loggedInUserId missing", async () => { const mod = loadModule("pages/api/endpoint/getawaitingsubmission_api.js", { respondError: respondErrorMock, @@ -1878,6 +1956,32 @@ test("getawaitingsubmission catch path returns AWAITING_SUBMISSION_FETCH_FAILED" ); }); +test("getawaitingsubmission passes relayPolicy overrides to relayGet", async () => { + let capturedRelayPolicy = null; + + const mod = loadModule("pages/api/endpoint/getawaitingsubmission_api.js", { + respondError: respondErrorMock, + respondSuccess: respondSuccessMock, + relayGet: async ({ relayPolicy, res }) => { + capturedRelayPolicy = relayPolicy; + return respondSuccessMock(res, { value: [] }); + } + }); + + const req = { query: { loggedInUserId: "c1" } }; + const res = createRes(); + await mod.default(req, res); + + assert.strictEqual(res.state.statusCode, 200); + assert.deepStrictEqual(JSON.parse(JSON.stringify(capturedRelayPolicy)), { + method: "GET", + timeoutMs: 8000, + maxRetries: 2, + retryBaseDelayMs: 150, + retryMaxDelayMs: 800 + }); +}); + test("getbasicsearchdetails returns APPEAL_TYPE_NAME_REQUIRED when appealTypeName missing", async () => { const mod = loadModule("pages/api/endpoint/getbasicsearchdetails_api.js", { respondError: respondErrorMock, @@ -3609,6 +3713,32 @@ test("getrepresentations catch path returns REPRESENTATIONS_FETCH_FAILED", async ); }); +test("getrepresentations passes relayPolicy overrides to relayGet", async () => { + let capturedRelayPolicy = null; + + const mod = loadModule("pages/api/endpoint/getrepresentations_api.js", { + respondError: respondErrorMock, + respondSuccess: respondSuccessMock, + relayGet: async ({ relayPolicy, res }) => { + capturedRelayPolicy = relayPolicy; + return respondSuccessMock(res, { value: [] }); + } + }); + + const req = { query: { incidentID: "i1" } }; + const res = createRes(); + await mod.default(req, res); + + assert.strictEqual(res.state.statusCode, 200); + assert.deepStrictEqual(JSON.parse(JSON.stringify(capturedRelayPolicy)), { + method: "GET", + timeoutMs: 8000, + maxRetries: 2, + retryBaseDelayMs: 150, + retryMaxDelayMs: 800 + }); +}); + test("getdnscoords catch path returns DNS_COORDS_FETCH_FAILED", async () => { const mod = loadModule("pages/api/endpoint/getdnscoords_api.js", { respondError: respondErrorMock,