feat(relay): apply endpoint-level relayPolicy overrides across account and search flows
This commit is contained in:
@@ -1300,3 +1300,40 @@ Validation:
|
|||||||
Follow-ups:
|
Follow-ups:
|
||||||
|
|
||||||
- Future non-GET relay adoption should explicitly opt in/out per route using `relayPolicy` and include targeted negative-path tests.
|
- Future non-GET relay adoption should explicitly opt in/out per route using `relayPolicy` and include targeted negative-path tests.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### CL-035: TASK22242 apply relayPolicy overrides across broader endpoint cluster
|
||||||
|
|
||||||
|
date: 2026-03-24
|
||||||
|
author: Cline
|
||||||
|
scope: `pages/api/endpoint/{getaccounts_api,getemailaccountcheck_api,getpreferredlanguage_api,getpersonalaccount_api,getlogin_api,getbasicsearch_api,getbasicsearchpaged_api,getadvancedsearch_api,getadvancedsearchpaged_api}.js`, `tests/phase21/endpoint-handler-contract.test.cjs`
|
||||||
|
type: change
|
||||||
|
rationale: Expand practical adoption of per-endpoint relay policy tuning so high-traffic account/login/search handlers explicitly declare timeout/retry posture rather than relying only on global defaults.
|
||||||
|
impact: Better operational control and predictable retry behavior per endpoint cluster, with no API contract changes.
|
||||||
|
status: completed
|
||||||
|
|
||||||
|
Summary:
|
||||||
|
|
||||||
|
- Added explicit `relayPolicy` usage to a broader endpoint set:
|
||||||
|
- account/login: `getaccounts`, `getemailaccountcheck`, `getpreferredlanguage`, `getpersonalaccount`, `getlogin`
|
||||||
|
- search: `getbasicsearch`, `getbasicsearchpaged`, `getadvancedsearch`, `getadvancedsearchpaged`
|
||||||
|
- Applied conservative policy profiles by flow:
|
||||||
|
- login endpoint (`getlogin`): no retries (`maxRetries: 0`) and tighter timeout
|
||||||
|
- account lookup endpoints: low retry posture (`maxRetries: 1`)
|
||||||
|
- search endpoints: bounded retry posture (`maxRetries: 2`) for transient resilience
|
||||||
|
- Kept method explicit as `GET` in policy for clarity and future-proofing.
|
||||||
|
- Extended phase21 endpoint tests with relayPolicy propagation assertions:
|
||||||
|
- `getaccounts` relayPolicy pass-through
|
||||||
|
- `getlogin` strict relayPolicy pass-through
|
||||||
|
- `getbasicsearchpaged` relayPolicy pass-through
|
||||||
|
|
||||||
|
Validation:
|
||||||
|
|
||||||
|
- `node tests/phase21/endpoint-handler-contract.test.cjs` -> pass (155/155)
|
||||||
|
- `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: apply relayPolicy declarations to remaining relayGet endpoints in coherent batches (portal module/documents/DNS groups) and standardize policy presets in one shared constants module.
|
||||||
|
|||||||
@@ -34,9 +34,18 @@ export default async function ApiProxy(req, res) {
|
|||||||
emailAddress +
|
emailAddress +
|
||||||
"'&$count=true&$select=emailaddress1, contactid";
|
"'&$count=true&$select=emailaddress1, contactid";
|
||||||
|
|
||||||
|
const relayPolicy = {
|
||||||
|
method: "GET",
|
||||||
|
timeoutMs: 6000,
|
||||||
|
maxRetries: 1,
|
||||||
|
retryBaseDelayMs: 100,
|
||||||
|
retryMaxDelayMs: 500
|
||||||
|
};
|
||||||
|
|
||||||
return relayGet({
|
return relayGet({
|
||||||
queryUrl,
|
queryUrl,
|
||||||
res,
|
res,
|
||||||
|
relayPolicy,
|
||||||
errorResponse: {
|
errorResponse: {
|
||||||
status: 400,
|
status: 400,
|
||||||
code: "ACCOUNTS_FETCH_FAILED",
|
code: "ACCOUNTS_FETCH_FAILED",
|
||||||
|
|||||||
@@ -103,10 +103,19 @@ export default async function ApiProxy(req, res) {
|
|||||||
queryString +
|
queryString +
|
||||||
" pinswg_appealcasetype ne null and pinswg_publishtoweb eq true&$orderby=createdon desc&$count=true";
|
" pinswg_appealcasetype ne null and pinswg_publishtoweb eq true&$orderby=createdon desc&$count=true";
|
||||||
|
|
||||||
|
const relayPolicy = {
|
||||||
|
method: "GET",
|
||||||
|
timeoutMs: 9000,
|
||||||
|
maxRetries: 2,
|
||||||
|
retryBaseDelayMs: 150,
|
||||||
|
retryMaxDelayMs: 1000
|
||||||
|
};
|
||||||
|
|
||||||
return relayGet({
|
return relayGet({
|
||||||
queryUrl,
|
queryUrl,
|
||||||
res,
|
res,
|
||||||
requestOptionsBuilder: (accessToken) => azureHeadersPaged(accessToken),
|
requestOptionsBuilder: (accessToken) => azureHeadersPaged(accessToken),
|
||||||
|
relayPolicy,
|
||||||
transformData: async (data, accessToken) => {
|
transformData: async (data, accessToken) => {
|
||||||
if (Object.prototype.hasOwnProperty.call(data, "@odata.nextLink")) {
|
if (Object.prototype.hasOwnProperty.call(data, "@odata.nextLink")) {
|
||||||
const dataStr = JSON.stringify(data["@odata.nextLink"]);
|
const dataStr = JSON.stringify(data["@odata.nextLink"]);
|
||||||
|
|||||||
@@ -173,11 +173,20 @@ export default async function ApiProxy(req, res) {
|
|||||||
: "");
|
: "");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const relayPolicy = {
|
||||||
|
method: "GET",
|
||||||
|
timeoutMs: 9000,
|
||||||
|
maxRetries: 2,
|
||||||
|
retryBaseDelayMs: 150,
|
||||||
|
retryMaxDelayMs: 1000
|
||||||
|
};
|
||||||
|
|
||||||
return relayGet({
|
return relayGet({
|
||||||
queryUrl,
|
queryUrl,
|
||||||
res,
|
res,
|
||||||
requestOptionsBuilder: (accessToken) =>
|
requestOptionsBuilder: (accessToken) =>
|
||||||
azureHeadersPagedCustom(accessToken, showNumberOfRecords),
|
azureHeadersPagedCustom(accessToken, showNumberOfRecords),
|
||||||
|
relayPolicy,
|
||||||
transformData: (data) => {
|
transformData: (data) => {
|
||||||
let dataStr;
|
let dataStr;
|
||||||
_.has(data, "@odata.nextLink") === true &&
|
_.has(data, "@odata.nextLink") === true &&
|
||||||
|
|||||||
@@ -45,10 +45,19 @@ export default async function ApiProxy(req, res) {
|
|||||||
: "") +
|
: "") +
|
||||||
"and pinswg_publishtoweb eq true&$orderby=createdon desc&$count=true";
|
"and pinswg_publishtoweb eq true&$orderby=createdon desc&$count=true";
|
||||||
|
|
||||||
|
const relayPolicy = {
|
||||||
|
method: "GET",
|
||||||
|
timeoutMs: 8000,
|
||||||
|
maxRetries: 2,
|
||||||
|
retryBaseDelayMs: 150,
|
||||||
|
retryMaxDelayMs: 800
|
||||||
|
};
|
||||||
|
|
||||||
return relayGet({
|
return relayGet({
|
||||||
queryUrl,
|
queryUrl,
|
||||||
res,
|
res,
|
||||||
requestOptionsBuilder: (accessToken) => azureHeadersPaged(accessToken),
|
requestOptionsBuilder: (accessToken) => azureHeadersPaged(accessToken),
|
||||||
|
relayPolicy,
|
||||||
transformData: (data) => {
|
transformData: (data) => {
|
||||||
if (Object.prototype.hasOwnProperty.call(data, "@odata.nextLink")) {
|
if (Object.prototype.hasOwnProperty.call(data, "@odata.nextLink")) {
|
||||||
const dataStr = JSON.stringify(data["@odata.nextLink"]);
|
const dataStr = JSON.stringify(data["@odata.nextLink"]);
|
||||||
|
|||||||
@@ -111,11 +111,20 @@ export default async function ApiProxy(req, res) {
|
|||||||
? "&$skiptoken=" + '<cookie pagenumber="' + pageNumber + '" />'
|
? "&$skiptoken=" + '<cookie pagenumber="' + pageNumber + '" />'
|
||||||
: "");
|
: "");
|
||||||
|
|
||||||
|
const relayPolicy = {
|
||||||
|
method: "GET",
|
||||||
|
timeoutMs: 9000,
|
||||||
|
maxRetries: 2,
|
||||||
|
retryBaseDelayMs: 150,
|
||||||
|
retryMaxDelayMs: 1000
|
||||||
|
};
|
||||||
|
|
||||||
return relayGet({
|
return relayGet({
|
||||||
queryUrl,
|
queryUrl,
|
||||||
res,
|
res,
|
||||||
requestOptionsBuilder: (accessToken) =>
|
requestOptionsBuilder: (accessToken) =>
|
||||||
azureHeadersPagedCustom(accessToken, showNumberOfRecords),
|
azureHeadersPagedCustom(accessToken, showNumberOfRecords),
|
||||||
|
relayPolicy,
|
||||||
transformData: (data) => {
|
transformData: (data) => {
|
||||||
if (_.has(data, "@odata.nextLink") === true) {
|
if (_.has(data, "@odata.nextLink") === true) {
|
||||||
const dataStr = JSON.stringify(data["@odata.nextLink"]);
|
const dataStr = JSON.stringify(data["@odata.nextLink"]);
|
||||||
|
|||||||
@@ -34,9 +34,18 @@ export default async function ApiProxy(req, res) {
|
|||||||
emailAddress +
|
emailAddress +
|
||||||
"'&$count=true&$select=emailaddress1, contactid";
|
"'&$count=true&$select=emailaddress1, contactid";
|
||||||
|
|
||||||
|
const relayPolicy = {
|
||||||
|
method: "GET",
|
||||||
|
timeoutMs: 6000,
|
||||||
|
maxRetries: 1,
|
||||||
|
retryBaseDelayMs: 100,
|
||||||
|
retryMaxDelayMs: 500
|
||||||
|
};
|
||||||
|
|
||||||
return relayGet({
|
return relayGet({
|
||||||
queryUrl,
|
queryUrl,
|
||||||
res,
|
res,
|
||||||
|
relayPolicy,
|
||||||
errorResponse: {
|
errorResponse: {
|
||||||
status: 400,
|
status: 400,
|
||||||
code: "EMAIL_ACCOUNT_CHECK_FAILED",
|
code: "EMAIL_ACCOUNT_CHECK_FAILED",
|
||||||
|
|||||||
@@ -52,10 +52,19 @@ export default async function ApiProxy(req, res) {
|
|||||||
pwd +
|
pwd +
|
||||||
"'&$count=true&$select=emailaddress1,contactid,pinswg_custom_password,yomifullname,firstname,lastname";
|
"'&$count=true&$select=emailaddress1,contactid,pinswg_custom_password,yomifullname,firstname,lastname";
|
||||||
|
|
||||||
|
const relayPolicy = {
|
||||||
|
method: "GET",
|
||||||
|
timeoutMs: 5000,
|
||||||
|
maxRetries: 0,
|
||||||
|
retryBaseDelayMs: 0,
|
||||||
|
retryMaxDelayMs: 0
|
||||||
|
};
|
||||||
|
|
||||||
return relayGet({
|
return relayGet({
|
||||||
queryUrl,
|
queryUrl,
|
||||||
res,
|
res,
|
||||||
requestOptionsBuilder: (accessToken) => azureHeadersPaged(accessToken),
|
requestOptionsBuilder: (accessToken) => azureHeadersPaged(accessToken),
|
||||||
|
relayPolicy,
|
||||||
errorResponse: {
|
errorResponse: {
|
||||||
status: 400,
|
status: 400,
|
||||||
code: "LOGIN_FETCH_FAILED",
|
code: "LOGIN_FETCH_FAILED",
|
||||||
|
|||||||
@@ -36,9 +36,18 @@ export default async function ApiProxy(req, res) {
|
|||||||
contactid +
|
contactid +
|
||||||
")?$select=firstname, lastname, emailaddress1, telephone1, company, address1_line1,address1_line2,address1_city, address1_county,address1_postalcode,pinswg_typeofinvolvement,pinswg_contact_associatedlpa,pinswg_preferredlanguage&$count=true";
|
")?$select=firstname, lastname, emailaddress1, telephone1, company, address1_line1,address1_line2,address1_city, address1_county,address1_postalcode,pinswg_typeofinvolvement,pinswg_contact_associatedlpa,pinswg_preferredlanguage&$count=true";
|
||||||
|
|
||||||
|
const relayPolicy = {
|
||||||
|
method: "GET",
|
||||||
|
timeoutMs: 6000,
|
||||||
|
maxRetries: 1,
|
||||||
|
retryBaseDelayMs: 100,
|
||||||
|
retryMaxDelayMs: 500
|
||||||
|
};
|
||||||
|
|
||||||
return relayGet({
|
return relayGet({
|
||||||
queryUrl,
|
queryUrl,
|
||||||
res,
|
res,
|
||||||
|
relayPolicy,
|
||||||
errorResponse: {
|
errorResponse: {
|
||||||
status: 400,
|
status: 400,
|
||||||
code: "PERSONAL_ACCOUNT_FETCH_FAILED",
|
code: "PERSONAL_ACCOUNT_FETCH_FAILED",
|
||||||
|
|||||||
@@ -39,9 +39,18 @@ export default async function ApiProxy(req, res) {
|
|||||||
escapeODataString(emailAddress) +
|
escapeODataString(emailAddress) +
|
||||||
"'&$count=true&$select=pinswg_preferredlanguage,contactid";
|
"'&$count=true&$select=pinswg_preferredlanguage,contactid";
|
||||||
|
|
||||||
|
const relayPolicy = {
|
||||||
|
method: "GET",
|
||||||
|
timeoutMs: 6000,
|
||||||
|
maxRetries: 1,
|
||||||
|
retryBaseDelayMs: 100,
|
||||||
|
retryMaxDelayMs: 500
|
||||||
|
};
|
||||||
|
|
||||||
return relayGet({
|
return relayGet({
|
||||||
queryUrl,
|
queryUrl,
|
||||||
res,
|
res,
|
||||||
|
relayPolicy,
|
||||||
errorResponse: {
|
errorResponse: {
|
||||||
status: 400,
|
status: 400,
|
||||||
code: "PREFERRED_LANGUAGE_FETCH_FAILED",
|
code: "PREFERRED_LANGUAGE_FETCH_FAILED",
|
||||||
|
|||||||
@@ -54,6 +54,32 @@ test("getaccounts catch path returns ACCOUNTS_FETCH_FAILED", async () => {
|
|||||||
assert.strictEqual(res.state.jsonBody.error.code, "ACCOUNTS_FETCH_FAILED");
|
assert.strictEqual(res.state.jsonBody.error.code, "ACCOUNTS_FETCH_FAILED");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("getaccounts passes relayPolicy overrides to relayGet", async () => {
|
||||||
|
let capturedRelayPolicy = null;
|
||||||
|
|
||||||
|
const mod = loadModule("pages/api/endpoint/getaccounts_api.js", {
|
||||||
|
respondError: respondErrorMock,
|
||||||
|
respondSuccess: respondSuccessMock,
|
||||||
|
relayGet: async ({ relayPolicy, res }) => {
|
||||||
|
capturedRelayPolicy = relayPolicy;
|
||||||
|
return respondSuccessMock(res, { value: [] });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const req = { query: { emailAddress: "user@test.local" } };
|
||||||
|
const res = createRes();
|
||||||
|
await mod.default(req, res);
|
||||||
|
|
||||||
|
assert.strictEqual(res.state.statusCode, 200);
|
||||||
|
assert.deepStrictEqual(JSON.parse(JSON.stringify(capturedRelayPolicy)), {
|
||||||
|
method: "GET",
|
||||||
|
timeoutMs: 6000,
|
||||||
|
maxRetries: 1,
|
||||||
|
retryBaseDelayMs: 100,
|
||||||
|
retryMaxDelayMs: 500
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
test("getemailaccountcheck returns EMAIL_ADDRESS_REQUIRED when emailAddress missing", async () => {
|
test("getemailaccountcheck returns EMAIL_ADDRESS_REQUIRED when emailAddress missing", async () => {
|
||||||
const mod = loadModule("pages/api/endpoint/getemailaccountcheck_api.js", {
|
const mod = loadModule("pages/api/endpoint/getemailaccountcheck_api.js", {
|
||||||
respondError: respondErrorMock,
|
respondError: respondErrorMock,
|
||||||
@@ -270,6 +296,33 @@ test("getlogin catch path returns LOGIN_FETCH_FAILED", async () => {
|
|||||||
assert.strictEqual(res.state.jsonBody.error.code, "LOGIN_FETCH_FAILED");
|
assert.strictEqual(res.state.jsonBody.error.code, "LOGIN_FETCH_FAILED");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("getlogin passes strict relayPolicy overrides to relayGet", async () => {
|
||||||
|
let capturedRelayPolicy = null;
|
||||||
|
|
||||||
|
const mod = loadModule("pages/api/endpoint/getlogin_api.js", {
|
||||||
|
respondError: respondErrorMock,
|
||||||
|
respondSuccess: respondSuccessMock,
|
||||||
|
relayGet: async ({ relayPolicy, res }) => {
|
||||||
|
capturedRelayPolicy = relayPolicy;
|
||||||
|
return respondSuccessMock(res, { value: [] });
|
||||||
|
},
|
||||||
|
azureHeadersPaged: () => ({})
|
||||||
|
});
|
||||||
|
|
||||||
|
const req = { query: { emailAddress: "user@test.local", pwd: "abc123" } };
|
||||||
|
const res = createRes();
|
||||||
|
await mod.default(req, res);
|
||||||
|
|
||||||
|
assert.strictEqual(res.state.statusCode, 200);
|
||||||
|
assert.deepStrictEqual(JSON.parse(JSON.stringify(capturedRelayPolicy)), {
|
||||||
|
method: "GET",
|
||||||
|
timeoutMs: 5000,
|
||||||
|
maxRetries: 0,
|
||||||
|
retryBaseDelayMs: 0,
|
||||||
|
retryMaxDelayMs: 0
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
test("getportallogin returns HASH_REQUIRED when hash missing", async () => {
|
test("getportallogin returns HASH_REQUIRED when hash missing", async () => {
|
||||||
const mod = loadModule("pages/api/endpoint/getportallogin_api.js", {
|
const mod = loadModule("pages/api/endpoint/getportallogin_api.js", {
|
||||||
respondError: respondErrorMock,
|
respondError: respondErrorMock,
|
||||||
@@ -1976,6 +2029,41 @@ test("getbasicsearchpaged catch path returns BASIC_SEARCH_PAGED_FETCH_FAILED", a
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("getbasicsearchpaged passes relayPolicy overrides to relayGet", async () => {
|
||||||
|
let capturedRelayPolicy = null;
|
||||||
|
|
||||||
|
const mod = loadModule("pages/api/endpoint/getbasicsearchpaged_api.js", {
|
||||||
|
respondError: respondErrorMock,
|
||||||
|
respondSuccess: respondSuccessMock,
|
||||||
|
relayGet: async ({ relayPolicy, res }) => {
|
||||||
|
capturedRelayPolicy = relayPolicy;
|
||||||
|
return respondSuccessMock(res, { value: [] });
|
||||||
|
},
|
||||||
|
azureHeadersPagedCustom: () => ({}),
|
||||||
|
_: { has: () => false }
|
||||||
|
});
|
||||||
|
|
||||||
|
const req = {
|
||||||
|
query: {
|
||||||
|
searchString: "cas",
|
||||||
|
orderby: "createdon",
|
||||||
|
fieldSort: "asc",
|
||||||
|
showNumberOfRecords: "10"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const res = createRes();
|
||||||
|
await mod.default(req, res);
|
||||||
|
|
||||||
|
assert.strictEqual(res.state.statusCode, 200);
|
||||||
|
assert.deepStrictEqual(JSON.parse(JSON.stringify(capturedRelayPolicy)), {
|
||||||
|
method: "GET",
|
||||||
|
timeoutMs: 9000,
|
||||||
|
maxRetries: 2,
|
||||||
|
retryBaseDelayMs: 150,
|
||||||
|
retryMaxDelayMs: 1000
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
test("getbasicsearch_by_lparref returns LPA_REF_REQUIRED when lpaRef missing", async () => {
|
test("getbasicsearch_by_lparref returns LPA_REF_REQUIRED when lpaRef missing", async () => {
|
||||||
const mod = loadModule(
|
const mod = loadModule(
|
||||||
"pages/api/endpoint/getbasicsearch_by_lparref_api.js",
|
"pages/api/endpoint/getbasicsearch_by_lparref_api.js",
|
||||||
|
|||||||
Reference in New Issue
Block a user