Merged PR 2205: addin timeouts and retries for stability
Related work items: #22242
This commit is contained in:
@@ -1222,3 +1222,225 @@ Validation:
|
||||
Follow-ups:
|
||||
|
||||
- Optional next iteration: add a PR template block in Azure DevOps mirroring the runbook governance gate checklist.
|
||||
|
||||
---
|
||||
|
||||
### CL-033: TASK22242 relay telemetry enrichment (lifecycle events + correlation fields)
|
||||
|
||||
date: 2026-03-24
|
||||
author: Cline
|
||||
scope: `pages/api/middleware/relayForwarding.js`, `tests/phase21/relay-forwarding-hardening.test.cjs`
|
||||
type: change
|
||||
rationale: Add richer relay observability so operations can correlate retries and outcomes per request and track latency/status patterns without changing endpoint contracts.
|
||||
impact: Improves operational diagnostics and trend analysis for relay traffic while preserving existing API behavior.
|
||||
status: completed
|
||||
|
||||
Summary:
|
||||
|
||||
- Enriched relay middleware telemetry with request lifecycle events:
|
||||
- `relay_request_started`
|
||||
- `relay_request_retrying`
|
||||
- `relay_request_succeeded`
|
||||
- `relay_request_failed`
|
||||
- Added shared telemetry fields for correlation and analysis:
|
||||
- `relayRequestId` (per request correlation id)
|
||||
- `attemptsMade`, `retryCountUsed`, `remainingRetries`
|
||||
- `elapsedMs`
|
||||
- `statusClass` (`2xx/4xx/5xx` style buckets)
|
||||
- resolved runtime knobs included at start event
|
||||
- Kept existing retry policy and endpoint response contracts unchanged.
|
||||
- Expanded phase21 relay hardening tests to assert telemetry behavior:
|
||||
- started/retrying/succeeded event presence
|
||||
- failed event telemetry fields
|
||||
- stable `relayRequestId` across lifecycle events for one request
|
||||
|
||||
Validation:
|
||||
|
||||
- `node tests/phase21/relay-forwarding-hardening.test.cjs` -> pass (7/7)
|
||||
- `node tests/phase21/endpoint-handler-contract.test.cjs` -> pass (152/152)
|
||||
- `npm run lint` -> warnings only (pre-existing `react-hooks/exhaustive-deps`; no new lint errors)
|
||||
|
||||
Follow-ups:
|
||||
|
||||
- Optional next step: map these lifecycle fields into central dashboards/alerts (retry rate, status-class distribution, p95 elapsedMs).
|
||||
|
||||
---
|
||||
|
||||
### CL-034: TASK22242 per-endpoint relay overrides + idempotency-aware retry gating
|
||||
|
||||
date: 2026-03-24
|
||||
author: Cline
|
||||
scope: `pages/api/middleware/relayForwarding.js`, `tests/phase21/relay-forwarding-hardening.test.cjs`
|
||||
type: change
|
||||
rationale: Deliver the next functional relay enhancement by enabling route-level retry tuning while adding safe-by-default retry gating for non-idempotent methods.
|
||||
impact: Improves control and safety of relay retries without breaking existing endpoint contracts.
|
||||
status: completed
|
||||
|
||||
Summary:
|
||||
|
||||
- Added **relay policy override support** (`relayPolicy`) to shared relay helpers (`relayGet`, `relayGetData`, `forwardGetData`):
|
||||
- per-call override of `timeoutMs`, `maxRetries`, `retryBaseDelayMs`, `retryMaxDelayMs`
|
||||
- optional method override via `relayPolicy.method`
|
||||
- Added **idempotency-aware retry gating scaffolding**:
|
||||
- retries allowed by default only for idempotent methods (`GET`, `HEAD`, `OPTIONS`)
|
||||
- non-idempotent retry behavior controlled by:
|
||||
- env flag `RELAY_ALLOW_NON_IDEMPOTENT_RETRIES` (default false)
|
||||
- per-call override `relayPolicy.allowNonIdempotentRetries`
|
||||
- Extended relay telemetry fields to include method and non-idempotent policy posture in start/failure/retry events.
|
||||
- Preserved existing route behavior:
|
||||
- existing GET endpoint flows continue to use retries per configured bounds
|
||||
- no endpoint response contract changes
|
||||
|
||||
Validation:
|
||||
|
||||
- `node tests/phase21/relay-forwarding-hardening.test.cjs` -> pass (10/10)
|
||||
- `node tests/phase21/endpoint-handler-contract.test.cjs` -> pass (152/152)
|
||||
- `npm run lint` -> warnings only (pre-existing `react-hooks/exhaustive-deps`; no new lint errors)
|
||||
|
||||
Follow-ups:
|
||||
|
||||
- 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.
|
||||
|
||||
---
|
||||
|
||||
### CL-036: TASK22242 portal-facing relayPolicy parity (login + module endpoints)
|
||||
|
||||
date: 2026-03-24
|
||||
author: Cline
|
||||
scope: `pages/api/endpoint/{getportallogin_api,getportalloginproxy_api,getportalmoduledetails_api,getportalmoduledetailsproxy_api}.js`, `tests/phase21/endpoint-handler-contract.test.cjs`
|
||||
type: change
|
||||
rationale: Complete the next practical relay policy rollout slice by bringing portal-facing login/module endpoints onto explicit per-endpoint timeout/retry posture.
|
||||
impact: Improves predictability and operational tuning for portal-facing relay GET flows without changing API contracts.
|
||||
status: completed
|
||||
|
||||
Summary:
|
||||
|
||||
- Added explicit `relayPolicy` for four portal-facing endpoints:
|
||||
- `getportallogin_api` -> strict/no-retry profile (`maxRetries: 0`, tighter timeout)
|
||||
- `getportalloginproxy_api` -> low-retry account lookup profile (`maxRetries: 1`)
|
||||
- `getportalmoduledetails_api` -> bounded read profile (`maxRetries: 2`)
|
||||
- `getportalmoduledetailsproxy_api` -> bounded read profile (`maxRetries: 2`)
|
||||
- Kept method explicit (`GET`) in each endpoint policy object.
|
||||
- Extended phase21 endpoint tests with relayPolicy propagation assertions for all four endpoints.
|
||||
|
||||
Validation:
|
||||
|
||||
- `node tests/phase21/endpoint-handler-contract.test.cjs` -> pass (159/159)
|
||||
- `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 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.
|
||||
|
||||
---
|
||||
|
||||
### CL-038: TASK22242 P2-S3 closure slice — shared relay policy presets extraction
|
||||
|
||||
date: 2026-03-24
|
||||
author: Cline
|
||||
scope: `pages/api/middleware/relayPolicyPresets.js`, `pages/api/endpoint/{getaccounts_api,getemailaccountcheck_api,getpreferredlanguage_api,getpersonalaccount_api,getlogin_api,getportallogin_api,getportalloginproxy_api,getbasicsearch_api,getbasicsearchpaged_api,getadvancedsearch_api,getadvancedsearchpaged_api,getportalmoduledetails_api,getportalmoduledetailsproxy_api,getmycases_api,getmyrepresentations_api,getawaitingsubmission_api,getrepresentations_api,getwatchedcases_api}.js`, `tests/phase21/_shared.cjs`
|
||||
type: change
|
||||
rationale: Complete the planned P2-S3 final maintainability slice by centralizing repeated relay timeout/retry policy objects into shared presets used consistently across all targeted relayGet endpoints.
|
||||
impact: Eliminates duplicated policy literals, reduces drift risk, and preserves endpoint contracts/behavior by reusing equivalent policy values.
|
||||
status: completed
|
||||
|
||||
Summary:
|
||||
|
||||
- Added new middleware constants module:
|
||||
- `pages/api/middleware/relayPolicyPresets.js`
|
||||
- `RELAY_POLICY_STRICT_LOGIN`
|
||||
- `RELAY_POLICY_LOOKUP`
|
||||
- `RELAY_POLICY_BOUNDED_READ`
|
||||
- `RELAY_POLICY_SEARCH_PAGED`
|
||||
- Refactored 18 relayGet endpoints to import and use shared presets instead of inline `relayPolicy` object literals:
|
||||
- lookup profile: `getaccounts`, `getemailaccountcheck`, `getpreferredlanguage`, `getpersonalaccount`, `getportalloginproxy`
|
||||
- strict login profile: `getlogin`, `getportallogin`
|
||||
- bounded read profile: `getbasicsearch`, `getportalmoduledetails`, `getportalmoduledetailsproxy`, `getmycases`, `getmyrepresentations`, `getawaitingsubmission`, `getrepresentations`, `getwatchedcases`
|
||||
- search paged profile: `getbasicsearchpaged`, `getadvancedsearch`, `getadvancedsearchpaged`
|
||||
- Updated phase21 VM test harness (`tests/phase21/_shared.cjs`) to inject preset constants so endpoint contract tests continue to execute with import-stripped modules.
|
||||
|
||||
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:
|
||||
|
||||
- P2-S3 planned slices are now complete; no further mandatory relay policy rollout slices remain for this stream.
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
*/
|
||||
import { respondError } from "../middleware/apiResponse";
|
||||
import { relayGet } from "../middleware/relayForwarding";
|
||||
import { RELAY_POLICY_LOOKUP } from "../middleware/relayPolicyPresets";
|
||||
|
||||
export default async function ApiProxy(req, res) {
|
||||
const emailAddress = req.query.emailAddress;
|
||||
@@ -34,9 +35,12 @@ export default async function ApiProxy(req, res) {
|
||||
emailAddress +
|
||||
"'&$count=true&$select=emailaddress1, contactid";
|
||||
|
||||
const relayPolicy = RELAY_POLICY_LOOKUP;
|
||||
|
||||
return relayGet({
|
||||
queryUrl,
|
||||
res,
|
||||
relayPolicy,
|
||||
errorResponse: {
|
||||
status: 400,
|
||||
code: "ACCOUNTS_FETCH_FAILED",
|
||||
|
||||
@@ -25,6 +25,7 @@ import { azureHeadersPaged } from "../../../actions/core/headers";
|
||||
import { consoleLogger } from "../../../actions/core/logger";
|
||||
import { respondError, respondSuccess } from "../middleware/apiResponse";
|
||||
import { relayGet, relayGetData } from "../middleware/relayForwarding";
|
||||
import { RELAY_POLICY_SEARCH_PAGED } from "../middleware/relayPolicyPresets";
|
||||
|
||||
export default async function ApiProxy(req, res) {
|
||||
const encodedSearchString = req.query.searchstring;
|
||||
@@ -103,10 +104,13 @@ export default async function ApiProxy(req, res) {
|
||||
queryString +
|
||||
" pinswg_appealcasetype ne null and pinswg_publishtoweb eq true&$orderby=createdon desc&$count=true";
|
||||
|
||||
const relayPolicy = RELAY_POLICY_SEARCH_PAGED;
|
||||
|
||||
return relayGet({
|
||||
queryUrl,
|
||||
res,
|
||||
requestOptionsBuilder: (accessToken) => azureHeadersPaged(accessToken),
|
||||
relayPolicy,
|
||||
transformData: async (data, accessToken) => {
|
||||
if (Object.prototype.hasOwnProperty.call(data, "@odata.nextLink")) {
|
||||
const dataStr = JSON.stringify(data["@odata.nextLink"]);
|
||||
|
||||
@@ -51,6 +51,7 @@ import _ from "lodash";
|
||||
import { azureHeadersPagedCustom } from "../../../actions/core/headers";
|
||||
import { respondError } from "../middleware/apiResponse";
|
||||
import { relayGet } from "../middleware/relayForwarding";
|
||||
import { RELAY_POLICY_SEARCH_PAGED } from "../middleware/relayPolicyPresets";
|
||||
|
||||
export default async function ApiProxy(req, res) {
|
||||
const rawSearchString = req.query.searchstring;
|
||||
@@ -173,11 +174,14 @@ export default async function ApiProxy(req, res) {
|
||||
: "");
|
||||
}
|
||||
|
||||
const relayPolicy = RELAY_POLICY_SEARCH_PAGED;
|
||||
|
||||
return relayGet({
|
||||
queryUrl,
|
||||
res,
|
||||
requestOptionsBuilder: (accessToken) =>
|
||||
azureHeadersPagedCustom(accessToken, showNumberOfRecords),
|
||||
relayPolicy,
|
||||
transformData: (data) => {
|
||||
let dataStr;
|
||||
_.has(data, "@odata.nextLink") === true &&
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
|
||||
import { respondError } from "../middleware/apiResponse";
|
||||
import { relayGet } from "../middleware/relayForwarding";
|
||||
import { RELAY_POLICY_BOUNDED_READ } from "../middleware/relayPolicyPresets";
|
||||
|
||||
export default async function ApiProxy(req, res) {
|
||||
const loggedInUserId = req.query.loggedInUserId;
|
||||
@@ -39,9 +40,12 @@ export default async function ApiProxy(req, res) {
|
||||
loggedInUserId +
|
||||
" and servicestage eq 1 and pinswg_appealcasetype ne null&$orderby=createdon desc&$count=true";
|
||||
|
||||
const relayPolicy = RELAY_POLICY_BOUNDED_READ;
|
||||
|
||||
return relayGet({
|
||||
queryUrl,
|
||||
res,
|
||||
relayPolicy,
|
||||
transformData: (data) => {
|
||||
data.value.forEach(function (element) {
|
||||
element.pinswg_title = element.title;
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
import { azureHeadersPaged } from "../../../actions/core/headers";
|
||||
import { respondError } from "../middleware/apiResponse";
|
||||
import { relayGet } from "../middleware/relayForwarding";
|
||||
import { RELAY_POLICY_BOUNDED_READ } from "../middleware/relayPolicyPresets";
|
||||
|
||||
export default async function ApiProxy(req, res) {
|
||||
const searchString = req.query.searchString;
|
||||
@@ -45,10 +46,13 @@ export default async function ApiProxy(req, res) {
|
||||
: "") +
|
||||
"and pinswg_publishtoweb eq true&$orderby=createdon desc&$count=true";
|
||||
|
||||
const relayPolicy = RELAY_POLICY_BOUNDED_READ;
|
||||
|
||||
return relayGet({
|
||||
queryUrl,
|
||||
res,
|
||||
requestOptionsBuilder: (accessToken) => azureHeadersPaged(accessToken),
|
||||
relayPolicy,
|
||||
transformData: (data) => {
|
||||
if (Object.prototype.hasOwnProperty.call(data, "@odata.nextLink")) {
|
||||
const dataStr = JSON.stringify(data["@odata.nextLink"]);
|
||||
|
||||
@@ -46,6 +46,7 @@ import _ from "lodash";
|
||||
import { azureHeadersPagedCustom } from "../../../actions/core/headers";
|
||||
import { respondError } from "../middleware/apiResponse";
|
||||
import { relayGet } from "../middleware/relayForwarding";
|
||||
import { RELAY_POLICY_SEARCH_PAGED } from "../middleware/relayPolicyPresets";
|
||||
|
||||
export default async function ApiProxy(req, res) {
|
||||
let searchString = req.query.searchString;
|
||||
@@ -111,11 +112,14 @@ export default async function ApiProxy(req, res) {
|
||||
? "&$skiptoken=" + '<cookie pagenumber="' + pageNumber + '" />'
|
||||
: "");
|
||||
|
||||
const relayPolicy = RELAY_POLICY_SEARCH_PAGED;
|
||||
|
||||
return relayGet({
|
||||
queryUrl,
|
||||
res,
|
||||
requestOptionsBuilder: (accessToken) =>
|
||||
azureHeadersPagedCustom(accessToken, showNumberOfRecords),
|
||||
relayPolicy,
|
||||
transformData: (data) => {
|
||||
if (_.has(data, "@odata.nextLink") === true) {
|
||||
const dataStr = JSON.stringify(data["@odata.nextLink"]);
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
*/
|
||||
import { respondError } from "../middleware/apiResponse";
|
||||
import { relayGet } from "../middleware/relayForwarding";
|
||||
import { RELAY_POLICY_LOOKUP } from "../middleware/relayPolicyPresets";
|
||||
|
||||
export default async function ApiProxy(req, res) {
|
||||
const emailAddress = req.query.emailAddress;
|
||||
@@ -34,9 +35,12 @@ export default async function ApiProxy(req, res) {
|
||||
emailAddress +
|
||||
"'&$count=true&$select=emailaddress1, contactid";
|
||||
|
||||
const relayPolicy = RELAY_POLICY_LOOKUP;
|
||||
|
||||
return relayGet({
|
||||
queryUrl,
|
||||
res,
|
||||
relayPolicy,
|
||||
errorResponse: {
|
||||
status: 400,
|
||||
code: "EMAIL_ACCOUNT_CHECK_FAILED",
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
import { azureHeadersPaged } from "../../../actions/core/headers";
|
||||
import { respondError } from "../middleware/apiResponse";
|
||||
import { relayGet } from "../middleware/relayForwarding";
|
||||
import { RELAY_POLICY_STRICT_LOGIN } from "../middleware/relayPolicyPresets";
|
||||
|
||||
export default async function ApiProxy(req, res) {
|
||||
const emailAddress = req.query.emailAddress;
|
||||
@@ -52,10 +53,13 @@ export default async function ApiProxy(req, res) {
|
||||
pwd +
|
||||
"'&$count=true&$select=emailaddress1,contactid,pinswg_custom_password,yomifullname,firstname,lastname";
|
||||
|
||||
const relayPolicy = RELAY_POLICY_STRICT_LOGIN;
|
||||
|
||||
return relayGet({
|
||||
queryUrl,
|
||||
res,
|
||||
requestOptionsBuilder: (accessToken) => azureHeadersPaged(accessToken),
|
||||
relayPolicy,
|
||||
errorResponse: {
|
||||
status: 400,
|
||||
code: "LOGIN_FETCH_FAILED",
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
|
||||
import { respondError } from "../middleware/apiResponse";
|
||||
import { relayGet } from "../middleware/relayForwarding";
|
||||
import { RELAY_POLICY_BOUNDED_READ } from "../middleware/relayPolicyPresets";
|
||||
|
||||
export default async function ApiProxy(req, res) {
|
||||
const loggedInUserId = req.query.loggedInUserId;
|
||||
@@ -39,9 +40,12 @@ export default async function ApiProxy(req, res) {
|
||||
loggedInUserId +
|
||||
" and pinswg_appealcasetype ne null&$orderby=createdon desc&$count=true";
|
||||
|
||||
const relayPolicy = RELAY_POLICY_BOUNDED_READ;
|
||||
|
||||
return relayGet({
|
||||
queryUrl,
|
||||
res,
|
||||
relayPolicy,
|
||||
transformData: (data) => {
|
||||
data.value.forEach(function (element) {
|
||||
element.pinswg_title = element.title;
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
|
||||
import { respondError } from "../middleware/apiResponse";
|
||||
import { relayGet } from "../middleware/relayForwarding";
|
||||
import { RELAY_POLICY_BOUNDED_READ } from "../middleware/relayPolicyPresets";
|
||||
|
||||
export default async function ApiProxy(req, res) {
|
||||
const loggedInUserId = req.query.loggedInUserId;
|
||||
@@ -39,9 +40,12 @@ export default async function ApiProxy(req, res) {
|
||||
loggedInUserId +
|
||||
"&$count=true&$orderby=createdon desc";
|
||||
|
||||
const relayPolicy = RELAY_POLICY_BOUNDED_READ;
|
||||
|
||||
return relayGet({
|
||||
queryUrl,
|
||||
res,
|
||||
relayPolicy,
|
||||
errorResponse: {
|
||||
status: 400,
|
||||
code: "MY_REPRESENTATIONS_FETCH_FAILED",
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
|
||||
import { respondError } from "../middleware/apiResponse";
|
||||
import { relayGet } from "../middleware/relayForwarding";
|
||||
import { RELAY_POLICY_LOOKUP } from "../middleware/relayPolicyPresets";
|
||||
|
||||
export default async function ApiProxy(req, res) {
|
||||
const contactid = req.query.contactid;
|
||||
@@ -36,9 +37,12 @@ export default async function ApiProxy(req, res) {
|
||||
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";
|
||||
|
||||
const relayPolicy = RELAY_POLICY_LOOKUP;
|
||||
|
||||
return relayGet({
|
||||
queryUrl,
|
||||
res,
|
||||
relayPolicy,
|
||||
errorResponse: {
|
||||
status: 400,
|
||||
code: "PERSONAL_ACCOUNT_FETCH_FAILED",
|
||||
|
||||
@@ -20,6 +20,7 @@ import { azureHeadersPaged } from "../../../actions/core/headers";
|
||||
import { hashAPIPath } from "../../../actions/core/hash";
|
||||
import { respondError } from "../middleware/apiResponse";
|
||||
import { relayGet } from "../middleware/relayForwarding";
|
||||
import { RELAY_POLICY_STRICT_LOGIN } from "../middleware/relayPolicyPresets";
|
||||
|
||||
export default async function ApiProxy(req, res) {
|
||||
const emailAddress = req.query.emailAddress;
|
||||
@@ -65,10 +66,13 @@ export default async function ApiProxy(req, res) {
|
||||
emailAddress +
|
||||
"' and statuscode eq 1&$count=true&$select=emailaddress1,contactid,yomifullname,firstname,lastname";
|
||||
|
||||
const relayPolicy = RELAY_POLICY_STRICT_LOGIN;
|
||||
|
||||
return relayGet({
|
||||
queryUrl,
|
||||
res,
|
||||
requestOptionsBuilder: (accessToken) => azureHeadersPaged(accessToken),
|
||||
relayPolicy,
|
||||
errorResponse: {
|
||||
status: 400,
|
||||
code: "PORTAL_LOGIN_FETCH_FAILED",
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
import { azureHeadersPaged } from "../../../actions/core/headers";
|
||||
import { respondError } from "../middleware/apiResponse";
|
||||
import { relayGet } from "../middleware/relayForwarding";
|
||||
import { RELAY_POLICY_LOOKUP } from "../middleware/relayPolicyPresets";
|
||||
|
||||
export default async function ApiProxy(req, res) {
|
||||
const emailAddress = req.query.emailAddress;
|
||||
@@ -36,10 +37,13 @@ export default async function ApiProxy(req, res) {
|
||||
emailAddress +
|
||||
"'&$count=true&$select=emailaddress1,contactid,yomifullname,firstname,lastname";
|
||||
|
||||
const relayPolicy = RELAY_POLICY_LOOKUP;
|
||||
|
||||
return relayGet({
|
||||
queryUrl,
|
||||
res,
|
||||
requestOptionsBuilder: (accessToken) => azureHeadersPaged(accessToken),
|
||||
relayPolicy,
|
||||
errorResponse: {
|
||||
status: 400,
|
||||
code: "PORTAL_LOGIN_PROXY_FETCH_FAILED",
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
import { getSelectQuery } from "../../../actions/selectQueryTypes";
|
||||
import { respondError } from "../middleware/apiResponse";
|
||||
import { relayGet } from "../middleware/relayForwarding";
|
||||
import { RELAY_POLICY_BOUNDED_READ } from "../middleware/relayPolicyPresets";
|
||||
|
||||
export default async function ApiProxy(req, res) {
|
||||
const appealType = req.query.appealType;
|
||||
@@ -60,9 +61,12 @@ export default async function ApiProxy(req, res) {
|
||||
|
||||
queryUrl = queryUrl + getSelectQuery(appealType);
|
||||
|
||||
const relayPolicy = RELAY_POLICY_BOUNDED_READ;
|
||||
|
||||
return relayGet({
|
||||
queryUrl,
|
||||
res,
|
||||
relayPolicy,
|
||||
errorResponse: {
|
||||
status: 400,
|
||||
code: "PORTAL_MODULE_DETAILS_FETCH_FAILED",
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
import { getSelectQuery } from "../../../actions/selectQueryTypes";
|
||||
import { respondError } from "../middleware/apiResponse";
|
||||
import { relayGet } from "../middleware/relayForwarding";
|
||||
import { RELAY_POLICY_BOUNDED_READ } from "../middleware/relayPolicyPresets";
|
||||
|
||||
export default async function ApiProxy(req, res) {
|
||||
const appealType = req.query.appealType;
|
||||
@@ -60,9 +61,12 @@ export default async function ApiProxy(req, res) {
|
||||
|
||||
queryUrl = queryUrl + getSelectQuery(appealType);
|
||||
|
||||
const relayPolicy = RELAY_POLICY_BOUNDED_READ;
|
||||
|
||||
return relayGet({
|
||||
queryUrl,
|
||||
res,
|
||||
relayPolicy,
|
||||
errorResponse: {
|
||||
status: 400,
|
||||
code: "PORTAL_MODULE_DETAILS_PROXY_FETCH_FAILED",
|
||||
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
} from "../../../actions/core/guards";
|
||||
import { respondError } from "../middleware/apiResponse";
|
||||
import { relayGet } from "../middleware/relayForwarding";
|
||||
import { RELAY_POLICY_LOOKUP } from "../middleware/relayPolicyPresets";
|
||||
|
||||
export default async function ApiProxy(req, res) {
|
||||
const emailAddress = sanitizeString(req.query.emailAddress);
|
||||
@@ -39,9 +40,12 @@ export default async function ApiProxy(req, res) {
|
||||
escapeODataString(emailAddress) +
|
||||
"'&$count=true&$select=pinswg_preferredlanguage,contactid";
|
||||
|
||||
const relayPolicy = RELAY_POLICY_LOOKUP;
|
||||
|
||||
return relayGet({
|
||||
queryUrl,
|
||||
res,
|
||||
relayPolicy,
|
||||
errorResponse: {
|
||||
status: 400,
|
||||
code: "PREFERRED_LANGUAGE_FETCH_FAILED",
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
|
||||
import { respondError } from "../middleware/apiResponse";
|
||||
import { relayGet } from "../middleware/relayForwarding";
|
||||
import { RELAY_POLICY_BOUNDED_READ } from "../middleware/relayPolicyPresets";
|
||||
|
||||
export default async function ApiProxy(req, res) {
|
||||
const incidentID = req.query.incidentID;
|
||||
@@ -36,9 +37,12 @@ export default async function ApiProxy(req, res) {
|
||||
incidentID +
|
||||
" and pinswg_publishtoweb eq true&$count=true&$orderby=createdon desc";
|
||||
|
||||
const relayPolicy = RELAY_POLICY_BOUNDED_READ;
|
||||
|
||||
return relayGet({
|
||||
queryUrl,
|
||||
res,
|
||||
relayPolicy,
|
||||
errorResponse: {
|
||||
status: 400,
|
||||
code: "REPRESENTATIONS_FETCH_FAILED",
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
|
||||
import { respondError } from "../middleware/apiResponse";
|
||||
import { relayGet } from "../middleware/relayForwarding";
|
||||
import { RELAY_POLICY_BOUNDED_READ } from "../middleware/relayPolicyPresets";
|
||||
|
||||
export default async function ApiProxy(req, res) {
|
||||
const loggedInUserId = req.query.loggedInUserId;
|
||||
@@ -39,9 +40,12 @@ 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 = RELAY_POLICY_BOUNDED_READ;
|
||||
|
||||
return relayGet({
|
||||
queryUrl,
|
||||
res,
|
||||
relayPolicy,
|
||||
transformData: (data) => {
|
||||
data.value.forEach(function (element) {
|
||||
element.ticketnumber = element.pinswg_WatchedCase?.ticketnumber;
|
||||
|
||||
@@ -13,6 +13,7 @@ const DEFAULT_TIMEOUT_MS = 8000;
|
||||
const DEFAULT_MAX_RETRIES = 2;
|
||||
const DEFAULT_RETRY_BASE_DELAY_MS = 200;
|
||||
const DEFAULT_RETRY_MAX_DELAY_MS = 1200;
|
||||
const DEFAULT_ALLOW_NON_IDEMPOTENT_RETRIES = false;
|
||||
const MAX_TIMEOUT_MS = 30000;
|
||||
const MAX_RETRIES = 4;
|
||||
const MAX_RETRY_DELAY_MS = 5000;
|
||||
@@ -29,11 +30,24 @@ const RETRYABLE_ERROR_CODES = new Set([
|
||||
"EPIPE"
|
||||
]);
|
||||
|
||||
const IDEMPOTENT_RETRY_METHODS = new Set(["GET", "HEAD", "OPTIONS"]);
|
||||
|
||||
const parsePositiveInt = (value, fallback) => {
|
||||
const parsed = Number.parseInt(value, 10);
|
||||
return Number.isFinite(parsed) && parsed >= 0 ? parsed : fallback;
|
||||
};
|
||||
|
||||
const parseBoolean = (value, fallback) => {
|
||||
if (typeof value === "boolean") return value;
|
||||
if (typeof value === "string") {
|
||||
const normalized = value.trim().toLowerCase();
|
||||
if (normalized === "true") return true;
|
||||
if (normalized === "false") return false;
|
||||
}
|
||||
|
||||
return fallback;
|
||||
};
|
||||
|
||||
const clamp = (value, min, max) => {
|
||||
return Math.min(max, Math.max(min, value));
|
||||
};
|
||||
@@ -65,6 +79,24 @@ const resolveNumericOverride = ({
|
||||
return clamp(Math.floor(overrideValue), lowerBound, max);
|
||||
};
|
||||
|
||||
const resolveBooleanOverride = ({ overrideValue, fallbackValue }) => {
|
||||
if (typeof overrideValue === "boolean") return overrideValue;
|
||||
return fallbackValue;
|
||||
};
|
||||
|
||||
const resolveRelayMethod = (value) => {
|
||||
if (typeof value !== "string" || value.trim().length === 0) {
|
||||
return "GET";
|
||||
}
|
||||
|
||||
return value.trim().toUpperCase();
|
||||
};
|
||||
|
||||
const canRetryForMethod = ({ method, allowNonIdempotentRetries }) => {
|
||||
if (allowNonIdempotentRetries) return true;
|
||||
return IDEMPOTENT_RETRY_METHODS.has(method);
|
||||
};
|
||||
|
||||
const getRelayConfig = () => {
|
||||
return {
|
||||
timeoutMs: sanitizeNumberConfig({
|
||||
@@ -93,7 +125,11 @@ const getRelayConfig = () => {
|
||||
min: 0,
|
||||
max: MAX_RETRY_DELAY_MS,
|
||||
allowZero: true
|
||||
})
|
||||
}),
|
||||
allowNonIdempotentRetries: parseBoolean(
|
||||
process.env.RELAY_ALLOW_NON_IDEMPOTENT_RETRIES,
|
||||
DEFAULT_ALLOW_NON_IDEMPOTENT_RETRIES
|
||||
)
|
||||
};
|
||||
};
|
||||
|
||||
@@ -109,8 +145,28 @@ const structuredRelayLog = (event, payload) => {
|
||||
console.info(event, redactSensitive(payload));
|
||||
};
|
||||
|
||||
const shouldRetryRelayError = ({ error, attempt, maxRetries }) => {
|
||||
const buildRelayRequestId = () => {
|
||||
return `relay_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`;
|
||||
};
|
||||
|
||||
const getElapsedMs = (startedAtMs) => {
|
||||
return Math.max(0, Date.now() - startedAtMs);
|
||||
};
|
||||
|
||||
const getStatusClass = (status) => {
|
||||
if (typeof status !== "number") return "none";
|
||||
return `${Math.floor(status / 100)}xx`;
|
||||
};
|
||||
|
||||
const shouldRetryRelayError = ({
|
||||
error,
|
||||
attempt,
|
||||
maxRetries,
|
||||
method,
|
||||
allowNonIdempotentRetries
|
||||
}) => {
|
||||
if (attempt >= maxRetries) return false;
|
||||
if (!canRetryForMethod({ method, allowNonIdempotentRetries })) return false;
|
||||
|
||||
const status = error?.response?.status;
|
||||
if (NON_RETRYABLE_STATUS_CODES.has(status)) return false;
|
||||
@@ -126,12 +182,14 @@ export const relayGet = async ({
|
||||
res,
|
||||
errorResponse,
|
||||
transformData,
|
||||
requestOptionsBuilder
|
||||
requestOptionsBuilder,
|
||||
relayPolicy
|
||||
}) => {
|
||||
try {
|
||||
const { data, accessToken } = await relayGetData({
|
||||
queryUrl,
|
||||
requestOptionsBuilder
|
||||
requestOptionsBuilder,
|
||||
relayPolicy
|
||||
});
|
||||
|
||||
return respondSuccess(
|
||||
@@ -151,12 +209,14 @@ export const relayGet = async ({
|
||||
export const relayGetData = async ({
|
||||
queryUrl,
|
||||
requestOptionsBuilder,
|
||||
accessToken
|
||||
accessToken,
|
||||
relayPolicy
|
||||
}) => {
|
||||
return forwardGetData({
|
||||
queryUrl,
|
||||
requestOptionsBuilder,
|
||||
accessToken
|
||||
accessToken,
|
||||
relayPolicy
|
||||
});
|
||||
};
|
||||
|
||||
@@ -169,19 +229,26 @@ export const forwardGetData = async ({
|
||||
timeoutMs,
|
||||
maxRetries,
|
||||
retryBaseDelayMs,
|
||||
retryMaxDelayMs
|
||||
retryMaxDelayMs,
|
||||
method,
|
||||
relayPolicy
|
||||
}) => {
|
||||
const relayConfig = getRelayConfig();
|
||||
const relayPolicyConfig = relayPolicy || {};
|
||||
|
||||
const resolvedMethod = resolveRelayMethod(
|
||||
relayPolicyConfig.method || method
|
||||
);
|
||||
|
||||
const resolvedTimeoutMs = resolveNumericOverride({
|
||||
overrideValue: timeoutMs,
|
||||
overrideValue: relayPolicyConfig.timeoutMs ?? timeoutMs,
|
||||
fallbackValue: relayConfig.timeoutMs,
|
||||
min: 100,
|
||||
max: MAX_TIMEOUT_MS
|
||||
});
|
||||
|
||||
const resolvedMaxRetries = resolveNumericOverride({
|
||||
overrideValue: maxRetries,
|
||||
overrideValue: relayPolicyConfig.maxRetries ?? maxRetries,
|
||||
fallbackValue: relayConfig.maxRetries,
|
||||
min: 0,
|
||||
max: MAX_RETRIES,
|
||||
@@ -189,7 +256,7 @@ export const forwardGetData = async ({
|
||||
});
|
||||
|
||||
const resolvedRetryBaseDelayMs = resolveNumericOverride({
|
||||
overrideValue: retryBaseDelayMs,
|
||||
overrideValue: relayPolicyConfig.retryBaseDelayMs ?? retryBaseDelayMs,
|
||||
fallbackValue: relayConfig.retryBaseDelayMs,
|
||||
min: 0,
|
||||
max: MAX_RETRY_DELAY_MS,
|
||||
@@ -197,13 +264,20 @@ export const forwardGetData = async ({
|
||||
});
|
||||
|
||||
const resolvedRetryMaxDelayMs = resolveNumericOverride({
|
||||
overrideValue: retryMaxDelayMs,
|
||||
overrideValue: relayPolicyConfig.retryMaxDelayMs ?? retryMaxDelayMs,
|
||||
fallbackValue: relayConfig.retryMaxDelayMs,
|
||||
min: 0,
|
||||
max: MAX_RETRY_DELAY_MS,
|
||||
allowZero: true
|
||||
});
|
||||
|
||||
const resolvedAllowNonIdempotentRetries = resolveBooleanOverride({
|
||||
overrideValue:
|
||||
relayPolicyConfig.allowNonIdempotentRetries ??
|
||||
relayPolicyConfig.allowRetriesForNonIdempotent,
|
||||
fallbackValue: relayConfig.allowNonIdempotentRetries
|
||||
});
|
||||
|
||||
const tokenAccessToken =
|
||||
typeof accessToken === "string" && accessToken.length > 0
|
||||
? accessToken
|
||||
@@ -226,11 +300,37 @@ export const forwardGetData = async ({
|
||||
: resolvedTimeoutMs
|
||||
};
|
||||
|
||||
const relayRequestId = buildRelayRequestId();
|
||||
const startedAtMs = Date.now();
|
||||
|
||||
structuredRelayLog("relay_request_started", {
|
||||
relayRequestId,
|
||||
queryUrl,
|
||||
baseUrl: resolvedBaseUrl,
|
||||
method: resolvedMethod,
|
||||
timeoutMs: resolvedTimeoutMs,
|
||||
maxRetries: resolvedMaxRetries,
|
||||
retryBaseDelayMs: resolvedRetryBaseDelayMs,
|
||||
retryMaxDelayMs: resolvedRetryMaxDelayMs,
|
||||
allowNonIdempotentRetries: resolvedAllowNonIdempotentRetries
|
||||
});
|
||||
|
||||
let lastError;
|
||||
|
||||
for (let attempt = 0; attempt <= resolvedMaxRetries; attempt += 1) {
|
||||
try {
|
||||
const { data } = await axios.get(finalUrl, axiosOptions);
|
||||
const { data, status } = await axios.get(finalUrl, axiosOptions);
|
||||
|
||||
structuredRelayLog("relay_request_succeeded", {
|
||||
relayRequestId,
|
||||
queryUrl,
|
||||
baseUrl: resolvedBaseUrl,
|
||||
status,
|
||||
statusClass: getStatusClass(status),
|
||||
attemptsMade: attempt + 1,
|
||||
retryCountUsed: attempt,
|
||||
elapsedMs: getElapsedMs(startedAtMs)
|
||||
});
|
||||
|
||||
return {
|
||||
data,
|
||||
@@ -242,19 +342,27 @@ export const forwardGetData = async ({
|
||||
const retryEligible = shouldRetryRelayError({
|
||||
error,
|
||||
attempt,
|
||||
maxRetries: resolvedMaxRetries
|
||||
maxRetries: resolvedMaxRetries,
|
||||
method: resolvedMethod,
|
||||
allowNonIdempotentRetries: resolvedAllowNonIdempotentRetries
|
||||
});
|
||||
|
||||
if (!retryEligible) {
|
||||
error.__relayAlreadyLogged = true;
|
||||
structuredRelayLog("relay_request_failed", {
|
||||
relayRequestId,
|
||||
queryUrl,
|
||||
baseUrl: resolvedBaseUrl,
|
||||
method: resolvedMethod,
|
||||
attempt: attempt + 1,
|
||||
maxAttempts: resolvedMaxRetries + 1,
|
||||
attemptsMade: attempt + 1,
|
||||
retryCountUsed: attempt,
|
||||
status: error?.response?.status,
|
||||
statusClass: getStatusClass(error?.response?.status),
|
||||
code: error?.code,
|
||||
message: error?.message
|
||||
message: error?.message,
|
||||
elapsedMs: getElapsedMs(startedAtMs)
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
@@ -266,14 +374,21 @@ export const forwardGetData = async ({
|
||||
});
|
||||
|
||||
structuredRelayLog("relay_request_retrying", {
|
||||
relayRequestId,
|
||||
queryUrl,
|
||||
baseUrl: resolvedBaseUrl,
|
||||
method: resolvedMethod,
|
||||
attempt: attempt + 1,
|
||||
maxAttempts: resolvedMaxRetries + 1,
|
||||
attemptsMade: attempt + 1,
|
||||
retryCountUsed: attempt,
|
||||
remainingRetries: resolvedMaxRetries - attempt,
|
||||
delayMs,
|
||||
status: error?.response?.status,
|
||||
statusClass: getStatusClass(error?.response?.status),
|
||||
code: error?.code,
|
||||
message: error?.message
|
||||
message: error?.message,
|
||||
elapsedMs: getElapsedMs(startedAtMs)
|
||||
});
|
||||
|
||||
await wait(delayMs);
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
export const RELAY_POLICY_STRICT_LOGIN = {
|
||||
method: "GET",
|
||||
timeoutMs: 5000,
|
||||
maxRetries: 0,
|
||||
retryBaseDelayMs: 0,
|
||||
retryMaxDelayMs: 0
|
||||
};
|
||||
|
||||
export const RELAY_POLICY_LOOKUP = {
|
||||
method: "GET",
|
||||
timeoutMs: 6000,
|
||||
maxRetries: 1,
|
||||
retryBaseDelayMs: 100,
|
||||
retryMaxDelayMs: 500
|
||||
};
|
||||
|
||||
export const RELAY_POLICY_BOUNDED_READ = {
|
||||
method: "GET",
|
||||
timeoutMs: 8000,
|
||||
maxRetries: 2,
|
||||
retryBaseDelayMs: 150,
|
||||
retryMaxDelayMs: 800
|
||||
};
|
||||
|
||||
export const RELAY_POLICY_SEARCH_PAGED = {
|
||||
method: "GET",
|
||||
timeoutMs: 9000,
|
||||
maxRetries: 2,
|
||||
retryBaseDelayMs: 150,
|
||||
retryMaxDelayMs: 1000
|
||||
};
|
||||
@@ -55,6 +55,34 @@ const loadModule = (relativePath, injected = {}) => {
|
||||
error: () => {}
|
||||
},
|
||||
relayGet: defaultRelayGet,
|
||||
RELAY_POLICY_STRICT_LOGIN: {
|
||||
method: "GET",
|
||||
timeoutMs: 5000,
|
||||
maxRetries: 0,
|
||||
retryBaseDelayMs: 0,
|
||||
retryMaxDelayMs: 0
|
||||
},
|
||||
RELAY_POLICY_LOOKUP: {
|
||||
method: "GET",
|
||||
timeoutMs: 6000,
|
||||
maxRetries: 1,
|
||||
retryBaseDelayMs: 100,
|
||||
retryMaxDelayMs: 500
|
||||
},
|
||||
RELAY_POLICY_BOUNDED_READ: {
|
||||
method: "GET",
|
||||
timeoutMs: 8000,
|
||||
maxRetries: 2,
|
||||
retryBaseDelayMs: 150,
|
||||
retryMaxDelayMs: 800
|
||||
},
|
||||
RELAY_POLICY_SEARCH_PAGED: {
|
||||
method: "GET",
|
||||
timeoutMs: 9000,
|
||||
maxRetries: 2,
|
||||
retryBaseDelayMs: 150,
|
||||
retryMaxDelayMs: 1000
|
||||
},
|
||||
...injected
|
||||
};
|
||||
|
||||
|
||||
@@ -54,6 +54,32 @@ test("getaccounts catch path returns ACCOUNTS_FETCH_FAILED", async () => {
|
||||
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 () => {
|
||||
const mod = loadModule("pages/api/endpoint/getemailaccountcheck_api.js", {
|
||||
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");
|
||||
});
|
||||
|
||||
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 () => {
|
||||
const mod = loadModule("pages/api/endpoint/getportallogin_api.js", {
|
||||
respondError: respondErrorMock,
|
||||
@@ -336,6 +389,39 @@ test("getportallogin catch path returns PORTAL_LOGIN_FETCH_FAILED", async () =>
|
||||
);
|
||||
});
|
||||
|
||||
test("getportallogin passes strict relayPolicy overrides to relayGet", async () => {
|
||||
let capturedRelayPolicy = null;
|
||||
|
||||
const mod = loadModule("pages/api/endpoint/getportallogin_api.js", {
|
||||
respondError: respondErrorMock,
|
||||
respondSuccess: respondSuccessMock,
|
||||
relayGet: async ({ relayPolicy, res }) => {
|
||||
capturedRelayPolicy = relayPolicy;
|
||||
return respondSuccessMock(res, { value: [] });
|
||||
},
|
||||
hashAPIPath: (input) =>
|
||||
input && input.startsWith("/api/endpoint/getportallogin_api")
|
||||
? "&hash=expected"
|
||||
: "&hash=relay",
|
||||
azureHeadersPaged: () => ({})
|
||||
});
|
||||
|
||||
const req = {
|
||||
query: { emailAddress: "user@test.local", hash: "expected" }
|
||||
};
|
||||
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 accepts encoded email hash variant", async () => {
|
||||
const mod = loadModule("pages/api/endpoint/getportallogin_api.js", {
|
||||
respondError: respondErrorMock,
|
||||
@@ -448,6 +534,33 @@ test("getportalloginproxy catch path returns PORTAL_LOGIN_PROXY_FETCH_FAILED", a
|
||||
);
|
||||
});
|
||||
|
||||
test("getportalloginproxy passes relayPolicy overrides to relayGet", async () => {
|
||||
let capturedRelayPolicy = null;
|
||||
|
||||
const mod = loadModule("pages/api/endpoint/getportalloginproxy_api.js", {
|
||||
respondError: respondErrorMock,
|
||||
respondSuccess: respondSuccessMock,
|
||||
relayGet: async ({ relayPolicy, res }) => {
|
||||
capturedRelayPolicy = relayPolicy;
|
||||
return respondSuccessMock(res, { value: [] });
|
||||
},
|
||||
azureHeadersPaged: () => ({})
|
||||
});
|
||||
|
||||
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("getpersonalaccount returns CONTACT_ID_REQUIRED when contactid missing", async () => {
|
||||
const mod = loadModule("pages/api/endpoint/getpersonalaccount_api.js", {
|
||||
respondError: respondErrorMock,
|
||||
@@ -1612,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,
|
||||
@@ -1663,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,
|
||||
@@ -1714,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,
|
||||
@@ -1765,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,
|
||||
@@ -1976,6 +2193,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 () => {
|
||||
const mod = loadModule(
|
||||
"pages/api/endpoint/getbasicsearch_by_lparref_api.js",
|
||||
@@ -2322,6 +2574,38 @@ test("getportalmoduledetails catch path returns PORTAL_MODULE_DETAILS_FETCH_FAIL
|
||||
);
|
||||
});
|
||||
|
||||
test("getportalmoduledetails passes relayPolicy overrides to relayGet", async () => {
|
||||
let capturedRelayPolicy = null;
|
||||
|
||||
const mod = loadModule("pages/api/endpoint/getportalmoduledetails_api.js", {
|
||||
respondError: respondErrorMock,
|
||||
respondSuccess: respondSuccessMock,
|
||||
relayGet: async ({ relayPolicy, res }) => {
|
||||
capturedRelayPolicy = relayPolicy;
|
||||
return respondSuccessMock(res, { value: [] });
|
||||
},
|
||||
getSelectQuery: () => "&$select=pinswg_name"
|
||||
});
|
||||
|
||||
const req = {
|
||||
query: {
|
||||
appealType: "pinswg_planningappeals78s",
|
||||
caseReference: "CAS-1"
|
||||
}
|
||||
};
|
||||
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("getportalmoduledetailsproxy returns APPEAL_TYPE_REQUIRED when appealType missing", async () => {
|
||||
const mod = loadModule(
|
||||
"pages/api/endpoint/getportalmoduledetailsproxy_api.js",
|
||||
@@ -2383,6 +2667,41 @@ test("getportalmoduledetailsproxy catch path returns PORTAL_MODULE_DETAILS_PROXY
|
||||
);
|
||||
});
|
||||
|
||||
test("getportalmoduledetailsproxy passes relayPolicy overrides to relayGet", async () => {
|
||||
let capturedRelayPolicy = null;
|
||||
|
||||
const mod = loadModule(
|
||||
"pages/api/endpoint/getportalmoduledetailsproxy_api.js",
|
||||
{
|
||||
respondError: respondErrorMock,
|
||||
respondSuccess: respondSuccessMock,
|
||||
relayGet: async ({ relayPolicy, res }) => {
|
||||
capturedRelayPolicy = relayPolicy;
|
||||
return respondSuccessMock(res, { value: [] });
|
||||
},
|
||||
getSelectQuery: () => "&$select=pinswg_name"
|
||||
}
|
||||
);
|
||||
|
||||
const req = {
|
||||
query: {
|
||||
appealType: "pinswg_planningappeals78s",
|
||||
caseReference: "CAS-1"
|
||||
}
|
||||
};
|
||||
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("getmylpacases returns LPA_ID_REQUIRED when lpaid missing", async () => {
|
||||
const mod = loadModule("pages/api/endpoint/getmylpacases_api.js", {
|
||||
respondError: respondErrorMock,
|
||||
@@ -3394,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,
|
||||
|
||||
@@ -43,6 +43,7 @@ const test = (name, fn) => tests.push({ name, fn });
|
||||
|
||||
test("forwardGetData retries retryable HTTP status and then succeeds", async () => {
|
||||
let callCount = 0;
|
||||
const infoLogs = [];
|
||||
|
||||
const mod = loadRelayForwardingModule({
|
||||
axios: {
|
||||
@@ -61,7 +62,13 @@ test("forwardGetData retries retryable HTTP status and then succeeds", async ()
|
||||
hashAPIPath: () => "&hash=abc",
|
||||
azureHeaders: () => ({ headers: { Authorization: "Bearer token" } }),
|
||||
redactSensitive: (value) => value,
|
||||
consoleLogger: () => {}
|
||||
consoleLogger: () => {},
|
||||
console: {
|
||||
log: () => {},
|
||||
info: (...args) => infoLogs.push(args),
|
||||
warn: () => {},
|
||||
error: () => {}
|
||||
}
|
||||
});
|
||||
|
||||
const result = await mod.forwardGetData({
|
||||
@@ -74,10 +81,39 @@ test("forwardGetData retries retryable HTTP status and then succeeds", async ()
|
||||
assert.strictEqual(callCount, 2);
|
||||
assert.deepStrictEqual(result.data, { ok: true });
|
||||
assert.strictEqual(result.accessToken, "token");
|
||||
|
||||
const eventNames = infoLogs.map((entry) => entry[0]);
|
||||
assert.ok(eventNames.includes("relay_request_started"));
|
||||
assert.ok(eventNames.includes("relay_request_retrying"));
|
||||
assert.ok(eventNames.includes("relay_request_succeeded"));
|
||||
|
||||
const retryEvent = infoLogs.find(
|
||||
(entry) => entry[0] === "relay_request_retrying"
|
||||
);
|
||||
assert.ok(retryEvent);
|
||||
|
||||
const retryPayload = retryEvent[1];
|
||||
assert.strictEqual(retryPayload.statusClass, "5xx");
|
||||
assert.strictEqual(retryPayload.attemptsMade, 1);
|
||||
assert.strictEqual(retryPayload.retryCountUsed, 0);
|
||||
assert.strictEqual(retryPayload.remainingRetries, 1);
|
||||
assert.ok(typeof retryPayload.relayRequestId === "string");
|
||||
|
||||
const successEvent = infoLogs.find(
|
||||
(entry) => entry[0] === "relay_request_succeeded"
|
||||
);
|
||||
assert.ok(successEvent);
|
||||
|
||||
const successPayload = successEvent[1];
|
||||
assert.strictEqual(successPayload.statusClass, "none");
|
||||
assert.strictEqual(successPayload.attemptsMade, 2);
|
||||
assert.strictEqual(successPayload.retryCountUsed, 1);
|
||||
assert.ok(typeof successPayload.elapsedMs === "number");
|
||||
});
|
||||
|
||||
test("forwardGetData does not retry non-retryable HTTP status", async () => {
|
||||
let callCount = 0;
|
||||
const infoLogs = [];
|
||||
|
||||
const mod = loadRelayForwardingModule({
|
||||
axios: {
|
||||
@@ -92,7 +128,13 @@ test("forwardGetData does not retry non-retryable HTTP status", async () => {
|
||||
hashAPIPath: () => "&hash=abc",
|
||||
azureHeaders: () => ({ headers: {} }),
|
||||
redactSensitive: (value) => value,
|
||||
consoleLogger: () => {}
|
||||
consoleLogger: () => {},
|
||||
console: {
|
||||
log: () => {},
|
||||
info: (...args) => infoLogs.push(args),
|
||||
warn: () => {},
|
||||
error: () => {}
|
||||
}
|
||||
});
|
||||
|
||||
let thrown = null;
|
||||
@@ -109,6 +151,17 @@ test("forwardGetData does not retry non-retryable HTTP status", async () => {
|
||||
|
||||
assert.ok(thrown);
|
||||
assert.strictEqual(callCount, 1);
|
||||
|
||||
const failedEvent = infoLogs.find(
|
||||
(entry) => entry[0] === "relay_request_failed"
|
||||
);
|
||||
assert.ok(failedEvent);
|
||||
|
||||
const failedPayload = failedEvent[1];
|
||||
assert.strictEqual(failedPayload.statusClass, "4xx");
|
||||
assert.strictEqual(failedPayload.attemptsMade, 1);
|
||||
assert.strictEqual(failedPayload.retryCountUsed, 0);
|
||||
assert.ok(typeof failedPayload.elapsedMs === "number");
|
||||
});
|
||||
|
||||
test("forwardGetData does not retry unauthorized status", async () => {
|
||||
@@ -258,6 +311,210 @@ test("forwardGetData applies timeout and appendHash=false behavior", async () =>
|
||||
assert.strictEqual(capturedOptions.headers.Accept, "application/json");
|
||||
});
|
||||
|
||||
test("forwardGetData telemetry request id is stable across lifecycle events", async () => {
|
||||
const infoLogs = [];
|
||||
let callCount = 0;
|
||||
|
||||
const mod = loadRelayForwardingModule({
|
||||
axios: {
|
||||
get: async () => {
|
||||
callCount += 1;
|
||||
if (callCount === 1) {
|
||||
const error = new Error("service unavailable");
|
||||
error.response = { status: 503 };
|
||||
throw error;
|
||||
}
|
||||
|
||||
return { data: { ok: true }, status: 200 };
|
||||
}
|
||||
},
|
||||
getToken: async () => ({ access_token: "token" }),
|
||||
hashAPIPath: () => "&hash=abc",
|
||||
azureHeaders: () => ({ headers: {} }),
|
||||
redactSensitive: (value) => value,
|
||||
consoleLogger: () => {},
|
||||
console: {
|
||||
log: () => {},
|
||||
info: (...args) => infoLogs.push(args),
|
||||
warn: () => {},
|
||||
error: () => {}
|
||||
}
|
||||
});
|
||||
|
||||
await mod.forwardGetData({
|
||||
queryUrl: "incidents?$top=1",
|
||||
maxRetries: 1,
|
||||
retryBaseDelayMs: 0,
|
||||
retryMaxDelayMs: 0
|
||||
});
|
||||
|
||||
const lifecycleEvents = infoLogs
|
||||
.filter((entry) =>
|
||||
[
|
||||
"relay_request_started",
|
||||
"relay_request_retrying",
|
||||
"relay_request_succeeded"
|
||||
].includes(entry[0])
|
||||
)
|
||||
.map((entry) => entry[1]);
|
||||
|
||||
assert.strictEqual(lifecycleEvents.length, 3);
|
||||
|
||||
const relayRequestIds = lifecycleEvents.map(
|
||||
(event) => event.relayRequestId
|
||||
);
|
||||
assert.ok(
|
||||
relayRequestIds.every((id) => typeof id === "string" && id.length > 0)
|
||||
);
|
||||
assert.strictEqual(new Set(relayRequestIds).size, 1);
|
||||
});
|
||||
|
||||
test("forwardGetData relayPolicy override controls retry behavior", async () => {
|
||||
let callCount = 0;
|
||||
const infoLogs = [];
|
||||
|
||||
const mod = loadRelayForwardingModule({
|
||||
axios: {
|
||||
get: async () => {
|
||||
callCount += 1;
|
||||
if (callCount < 3) {
|
||||
const error = new Error("temporary outage");
|
||||
error.response = { status: 503 };
|
||||
throw error;
|
||||
}
|
||||
|
||||
return { data: { ok: true }, status: 200 };
|
||||
}
|
||||
},
|
||||
getToken: async () => ({ access_token: "token" }),
|
||||
hashAPIPath: () => "&hash=abc",
|
||||
azureHeaders: () => ({ headers: {} }),
|
||||
redactSensitive: (value) => value,
|
||||
consoleLogger: () => {},
|
||||
console: {
|
||||
log: () => {},
|
||||
info: (...args) => infoLogs.push(args),
|
||||
warn: () => {},
|
||||
error: () => {}
|
||||
}
|
||||
});
|
||||
|
||||
const result = await mod.forwardGetData({
|
||||
queryUrl: "incidents?$top=1",
|
||||
maxRetries: 0,
|
||||
relayPolicy: {
|
||||
maxRetries: 2,
|
||||
retryBaseDelayMs: 0,
|
||||
retryMaxDelayMs: 0,
|
||||
method: "GET"
|
||||
}
|
||||
});
|
||||
|
||||
assert.strictEqual(callCount, 3);
|
||||
assert.deepStrictEqual(result.data, { ok: true });
|
||||
|
||||
const startEvent = infoLogs.find(
|
||||
(entry) => entry[0] === "relay_request_started"
|
||||
);
|
||||
assert.ok(startEvent);
|
||||
assert.strictEqual(startEvent[1].maxRetries, 2);
|
||||
assert.strictEqual(startEvent[1].method, "GET");
|
||||
});
|
||||
|
||||
test("forwardGetData blocks retries for non-idempotent methods by default", async () => {
|
||||
let callCount = 0;
|
||||
const infoLogs = [];
|
||||
|
||||
const mod = loadRelayForwardingModule({
|
||||
axios: {
|
||||
get: async () => {
|
||||
callCount += 1;
|
||||
const error = new Error("temporary outage");
|
||||
error.response = { status: 503 };
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
getToken: async () => ({ access_token: "token" }),
|
||||
hashAPIPath: () => "&hash=abc",
|
||||
azureHeaders: () => ({ headers: {} }),
|
||||
redactSensitive: (value) => value,
|
||||
consoleLogger: () => {},
|
||||
console: {
|
||||
log: () => {},
|
||||
info: (...args) => infoLogs.push(args),
|
||||
warn: () => {},
|
||||
error: () => {}
|
||||
}
|
||||
});
|
||||
|
||||
let thrown = null;
|
||||
try {
|
||||
await mod.forwardGetData({
|
||||
queryUrl: "incidents?$top=1",
|
||||
relayPolicy: {
|
||||
method: "POST",
|
||||
maxRetries: 3,
|
||||
retryBaseDelayMs: 0,
|
||||
retryMaxDelayMs: 0
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
thrown = error;
|
||||
}
|
||||
|
||||
assert.ok(thrown);
|
||||
assert.strictEqual(callCount, 1);
|
||||
|
||||
const retryEvents = infoLogs.filter(
|
||||
(entry) => entry[0] === "relay_request_retrying"
|
||||
);
|
||||
assert.strictEqual(retryEvents.length, 0);
|
||||
|
||||
const failedEvent = infoLogs.find(
|
||||
(entry) => entry[0] === "relay_request_failed"
|
||||
);
|
||||
assert.ok(failedEvent);
|
||||
assert.strictEqual(failedEvent[1].method, "POST");
|
||||
});
|
||||
|
||||
test("forwardGetData can allow retries for non-idempotent methods when explicitly enabled", async () => {
|
||||
let callCount = 0;
|
||||
|
||||
const mod = loadRelayForwardingModule({
|
||||
axios: {
|
||||
get: async () => {
|
||||
callCount += 1;
|
||||
if (callCount === 1) {
|
||||
const error = new Error("temporary outage");
|
||||
error.response = { status: 503 };
|
||||
throw error;
|
||||
}
|
||||
|
||||
return { data: { ok: true }, status: 200 };
|
||||
}
|
||||
},
|
||||
getToken: async () => ({ access_token: "token" }),
|
||||
hashAPIPath: () => "&hash=abc",
|
||||
azureHeaders: () => ({ headers: {} }),
|
||||
redactSensitive: (value) => value,
|
||||
consoleLogger: () => {}
|
||||
});
|
||||
|
||||
const result = await mod.forwardGetData({
|
||||
queryUrl: "incidents?$top=1",
|
||||
relayPolicy: {
|
||||
method: "PATCH",
|
||||
maxRetries: 1,
|
||||
retryBaseDelayMs: 0,
|
||||
retryMaxDelayMs: 0,
|
||||
allowNonIdempotentRetries: true
|
||||
}
|
||||
});
|
||||
|
||||
assert.strictEqual(callCount, 2);
|
||||
assert.deepStrictEqual(result.data, { ok: true });
|
||||
});
|
||||
|
||||
const run = async () => {
|
||||
let passed = 0;
|
||||
for (const currentTest of tests) {
|
||||
|
||||
Reference in New Issue
Block a user