From 6edbc431f86dbef290e5ddec39c5c9c6bbbecd9f Mon Sep 17 00:00:00 2001 From: robbond Date: Tue, 24 Mar 2026 06:58:50 +0000 Subject: [PATCH] TASK22229: P2-S2 batch 1 migrate account/login relay GET endpoints --- memory-bank/change-log.md | 37 ++++++++++++++++ pages/api/endpoint/getaccounts_api.js | 38 ++++++----------- .../api/endpoint/getemailaccountcheck_api.js | 38 ++++++----------- pages/api/endpoint/getlogin_api.js | 42 +++++++------------ pages/api/endpoint/getportalloginproxy_api.js | 38 ++++++----------- .../api/endpoint/getpreferredlanguage_api.js | 39 ++++++----------- pages/api/middleware/relayForwarding.js | 7 +++- .../endpoint-handler-contract.test.cjs | 33 +++++++++++++++ 8 files changed, 139 insertions(+), 133 deletions(-) diff --git a/memory-bank/change-log.md b/memory-bank/change-log.md index ffcdef22..f187d393 100644 --- a/memory-bank/change-log.md +++ b/memory-bank/change-log.md @@ -764,3 +764,40 @@ Follow-ups: - P2-S2 rollout: migrate the next relay-heavy endpoint batch onto `relayGet`/shared forwarding utility pattern. - P2-S3 hardening: add shared timeout/retry config and redacted structured relay error logging policy. + +--- + +### CL-020: TASK22229 P2-S2 Batch 1 (account/login relay GET cluster) + +date: 2026-03-24 +author: Cline +scope: `pages/api/middleware/relayForwarding.js`, `pages/api/endpoint/{getaccounts_api,getemailaccountcheck_api,getpreferredlanguage_api,getlogin_api,getpersonalaccount_api,getportalloginproxy_api}.js`, `tests/phase21/endpoint-handler-contract.test.cjs` +type: change +rationale: Deliver first P2-S2 batch as a dedicated commit by migrating a bounded account/login endpoint cluster onto shared `relayGet` while preserving response contracts. +impact: Reduced relay boilerplate and improved consistency with no intended endpoint contract changes. +status: completed + +Summary: + +- Extended `relayGet` to support optional `requestOptionsBuilder` for handlers requiring paged header variants. +- Migrated Batch 1 endpoints to `relayGet`: + - `getaccounts_api.js` + - `getemailaccountcheck_api.js` + - `getpreferredlanguage_api.js` + - `getlogin_api.js` (uses `azureHeadersPaged` via `requestOptionsBuilder`) + - `getpersonalaccount_api.js` + - `getportalloginproxy_api.js` (uses `azureHeadersPaged` via `requestOptionsBuilder`) +- Updated phase21 endpoint contract tests to mock `relayGet` for migrated handlers, preserving existing guard/catch/success assertions. + +Validation: + +- `node tests/phase21/api-contract-slice1.test.cjs` -> pass + - helper: 4/4 + - file-handler: 53/53 + - email-handler: 12/12 + - endpoint-handler: 152/152 + - documents-handler: 3/3 + +Follow-ups: + +- Continue P2-S2 with Batch 2 as next dedicated commit on this same branch. diff --git a/pages/api/endpoint/getaccounts_api.js b/pages/api/endpoint/getaccounts_api.js index e48cb56c..f1316370 100644 --- a/pages/api/endpoint/getaccounts_api.js +++ b/pages/api/endpoint/getaccounts_api.js @@ -15,16 +15,8 @@ * 200: * description: hello world */ -import axios from "axios"; -import { azureHeaders } from "../../../actions/core/headers"; -import { consoleLogger } from "../../../actions/core/logger"; -import { getToken } from "../../../actions/core/token"; -import { hashAPIPath } from "../../../actions/core/hash"; -import { respondError, respondSuccess } from "../middleware/apiResponse"; - -const WEBAPI_URL = - process.env.RELAY_ROOT || - "https://dev-pedw-ns.servicebus.windows.net/dev-pedw-hc/"; +import { respondError } from "../middleware/apiResponse"; +import { relayGet } from "../middleware/relayForwarding"; export default async function ApiProxy(req, res) { const emailAddress = req.query.emailAddress; @@ -37,24 +29,18 @@ export default async function ApiProxy(req, res) { }); } - try { - const token = await getToken(); - const queryUrl = - "contacts?$filter=emailaddress1 eq '" + - emailAddress + - "'&$count=true&$select=emailaddress1, contactid"; - const { data } = await axios.get( - WEBAPI_URL + queryUrl + hashAPIPath(queryUrl), - azureHeaders(token.access_token) - ); + const queryUrl = + "contacts?$filter=emailaddress1 eq '" + + emailAddress + + "'&$count=true&$select=emailaddress1, contactid"; - return respondSuccess(res, data); - } catch (error) { - consoleLogger(error); - return respondError(res, { + return relayGet({ + queryUrl, + res, + errorResponse: { status: 400, code: "ACCOUNTS_FETCH_FAILED", message: "Failed to fetch accounts" - }); - } + } + }); } diff --git a/pages/api/endpoint/getemailaccountcheck_api.js b/pages/api/endpoint/getemailaccountcheck_api.js index ce07f52f..1751d5eb 100644 --- a/pages/api/endpoint/getemailaccountcheck_api.js +++ b/pages/api/endpoint/getemailaccountcheck_api.js @@ -15,16 +15,8 @@ * 200: * description: hello world */ -import axios from "axios"; -import { azureHeaders } from "../../../actions/core/headers"; -import { consoleLogger } from "../../../actions/core/logger"; -import { getToken } from "../../../actions/core/token"; -import { hashAPIPath } from "../../../actions/core/hash"; -import { respondError, respondSuccess } from "../middleware/apiResponse"; - -const WEBAPI_URL = - process.env.RELAY_ROOT || - "https://dev-pedw-ns.servicebus.windows.net/dev-pedw-hc/"; +import { respondError } from "../middleware/apiResponse"; +import { relayGet } from "../middleware/relayForwarding"; export default async function ApiProxy(req, res) { const emailAddress = req.query.emailAddress; @@ -37,24 +29,18 @@ export default async function ApiProxy(req, res) { }); } - try { - const token = await getToken(); - const queryUrl = - "contacts?$filter=emailaddress1 eq '" + - emailAddress + - "'&$count=true&$select=emailaddress1, contactid"; - const { data } = await axios.get( - WEBAPI_URL + queryUrl + hashAPIPath(queryUrl), - azureHeaders(token.access_token) - ); + const queryUrl = + "contacts?$filter=emailaddress1 eq '" + + emailAddress + + "'&$count=true&$select=emailaddress1, contactid"; - return respondSuccess(res, data); - } catch (error) { - consoleLogger(error); - return respondError(res, { + return relayGet({ + queryUrl, + res, + errorResponse: { status: 400, code: "EMAIL_ACCOUNT_CHECK_FAILED", message: "Failed to check email account" - }); - } + } + }); } diff --git a/pages/api/endpoint/getlogin_api.js b/pages/api/endpoint/getlogin_api.js index bac72976..cf828194 100644 --- a/pages/api/endpoint/getlogin_api.js +++ b/pages/api/endpoint/getlogin_api.js @@ -21,16 +21,9 @@ * description: hello world */ -import axios from "axios"; import { azureHeadersPaged } from "../../../actions/core/headers"; -import { consoleLogger } from "../../../actions/core/logger"; -import { getToken } from "../../../actions/core/token"; -import { hashAPIPath } from "../../../actions/core/hash"; -import { respondError, respondSuccess } from "../middleware/apiResponse"; - -const WEBAPI_URL = - process.env.RELAY_ROOT || - "https://dev-pedw-ns.servicebus.windows.net/dev-pedw-hc/"; +import { respondError } from "../middleware/apiResponse"; +import { relayGet } from "../middleware/relayForwarding"; export default async function ApiProxy(req, res) { const emailAddress = req.query.emailAddress; @@ -52,26 +45,21 @@ export default async function ApiProxy(req, res) { }); } - try { - const token = await getToken(); - const queryUrl = - "contacts?$filter=emailaddress1 eq '" + - emailAddress + - "' and pinswg_custom_password eq '" + - pwd + - "'&$count=true&$select=emailaddress1,contactid,pinswg_custom_password,yomifullname,firstname,lastname"; - const { data } = await axios.get( - WEBAPI_URL + queryUrl + hashAPIPath(queryUrl), - azureHeadersPaged(token.access_token) - ); + const queryUrl = + "contacts?$filter=emailaddress1 eq '" + + emailAddress + + "' and pinswg_custom_password eq '" + + pwd + + "'&$count=true&$select=emailaddress1,contactid,pinswg_custom_password,yomifullname,firstname,lastname"; - return respondSuccess(res, data); - } catch (error) { - consoleLogger(error); - return respondError(res, { + return relayGet({ + queryUrl, + res, + requestOptionsBuilder: (accessToken) => azureHeadersPaged(accessToken), + errorResponse: { status: 400, code: "LOGIN_FETCH_FAILED", message: "Failed to fetch login details" - }); - } + } + }); } diff --git a/pages/api/endpoint/getportalloginproxy_api.js b/pages/api/endpoint/getportalloginproxy_api.js index 0a1a3b61..b1c734d1 100644 --- a/pages/api/endpoint/getportalloginproxy_api.js +++ b/pages/api/endpoint/getportalloginproxy_api.js @@ -16,16 +16,9 @@ * description: hello world */ -import axios from "axios"; import { azureHeadersPaged } from "../../../actions/core/headers"; -import { consoleLogger } from "../../../actions/core/logger"; -import { getToken } from "../../../actions/core/token"; -import { hashAPIPath } from "../../../actions/core/hash"; -import { respondError, respondSuccess } from "../middleware/apiResponse"; - -const WEBAPI_URL = - process.env.RELAY_ROOT || - "https://dev-pedw-ns.servicebus.windows.net/dev-pedw-hc/"; +import { respondError } from "../middleware/apiResponse"; +import { relayGet } from "../middleware/relayForwarding"; export default async function ApiProxy(req, res) { const emailAddress = req.query.emailAddress; @@ -38,24 +31,19 @@ export default async function ApiProxy(req, res) { }); } - try { - const token = await getToken(); - const queryUrl = - "contacts?$filter=emailaddress1 eq '" + - emailAddress + - "'&$count=true&$select=emailaddress1,contactid,yomifullname,firstname,lastname"; - const { data } = await axios.get( - WEBAPI_URL + queryUrl + hashAPIPath(queryUrl), - azureHeadersPaged(token.access_token) - ); + const queryUrl = + "contacts?$filter=emailaddress1 eq '" + + emailAddress + + "'&$count=true&$select=emailaddress1,contactid,yomifullname,firstname,lastname"; - return respondSuccess(res, data); - } catch (error) { - consoleLogger(error); - return respondError(res, { + return relayGet({ + queryUrl, + res, + requestOptionsBuilder: (accessToken) => azureHeadersPaged(accessToken), + errorResponse: { status: 400, code: "PORTAL_LOGIN_PROXY_FETCH_FAILED", message: "Failed to fetch portal login proxy details" - }); - } + } + }); } diff --git a/pages/api/endpoint/getpreferredlanguage_api.js b/pages/api/endpoint/getpreferredlanguage_api.js index ddc7ae4d..0051dc77 100644 --- a/pages/api/endpoint/getpreferredlanguage_api.js +++ b/pages/api/endpoint/getpreferredlanguage_api.js @@ -15,21 +15,13 @@ * 200: * description: hello world */ -import axios from "axios"; -import { azureHeaders } from "../../../actions/core/headers"; import { escapeODataString, isNonEmptyString, sanitizeString } from "../../../actions/core/guards"; -import { consoleLogger } from "../../../actions/core/logger"; -import { getToken } from "../../../actions/core/token"; -import { hashAPIPath } from "../../../actions/core/hash"; -import { respondError, respondSuccess } from "../middleware/apiResponse"; - -const WEBAPI_URL = - process.env.RELAY_ROOT || - "https://dev-pedw-ns.servicebus.windows.net/dev-pedw-hc/"; +import { respondError } from "../middleware/apiResponse"; +import { relayGet } from "../middleware/relayForwarding"; export default async function ApiProxy(req, res) { const emailAddress = sanitizeString(req.query.emailAddress); @@ -42,25 +34,18 @@ export default async function ApiProxy(req, res) { }); } - try { - const token = await getToken(); - const queryUrl = - "contacts?$filter=emailaddress1 eq '" + - escapeODataString(emailAddress) + - "'&$count=true&$select=pinswg_preferredlanguage,contactid"; + const queryUrl = + "contacts?$filter=emailaddress1 eq '" + + escapeODataString(emailAddress) + + "'&$count=true&$select=pinswg_preferredlanguage,contactid"; - const { data } = await axios.get( - WEBAPI_URL + queryUrl + hashAPIPath(queryUrl), - azureHeaders(token.access_token) - ); - - return respondSuccess(res, data); - } catch (error) { - consoleLogger(error); - return respondError(res, { + return relayGet({ + queryUrl, + res, + errorResponse: { status: 400, code: "PREFERRED_LANGUAGE_FETCH_FAILED", message: "Failed to fetch preferred language" - }); - } + } + }); } diff --git a/pages/api/middleware/relayForwarding.js b/pages/api/middleware/relayForwarding.js index 2130a063..45f07dd8 100644 --- a/pages/api/middleware/relayForwarding.js +++ b/pages/api/middleware/relayForwarding.js @@ -13,13 +13,16 @@ export const relayGet = async ({ queryUrl, res, errorResponse, - transformData + transformData, + requestOptionsBuilder }) => { try { const token = await getToken(); const { data } = await axios.get( WEBAPI_URL + queryUrl + hashAPIPath(queryUrl), - azureHeaders(token.access_token) + typeof requestOptionsBuilder === "function" + ? requestOptionsBuilder(token.access_token) + : azureHeaders(token.access_token) ); return respondSuccess( diff --git a/tests/phase21/endpoint-handler-contract.test.cjs b/tests/phase21/endpoint-handler-contract.test.cjs index 5e9f8f56..41b958f9 100644 --- a/tests/phase21/endpoint-handler-contract.test.cjs +++ b/tests/phase21/endpoint-handler-contract.test.cjs @@ -13,6 +13,7 @@ test("getaccounts returns EMAIL_ADDRESS_REQUIRED when emailAddress missing", asy const mod = loadModule("pages/api/endpoint/getaccounts_api.js", { respondError: respondErrorMock, respondSuccess: respondSuccessMock, + relayGet: async () => ({}), getToken: async () => ({ access_token: "token" }), hashAPIPath: () => "&hash=expected", azureHeaders: () => ({}), @@ -32,6 +33,8 @@ test("getaccounts catch path returns ACCOUNTS_FETCH_FAILED", async () => { const mod = loadModule("pages/api/endpoint/getaccounts_api.js", { respondError: respondErrorMock, respondSuccess: respondSuccessMock, + relayGet: async ({ res, errorResponse }) => + respondErrorMock(res, errorResponse), getToken: async () => ({ access_token: "token" }), hashAPIPath: () => "&hash=expected", azureHeaders: () => ({}), @@ -55,6 +58,7 @@ test("getemailaccountcheck returns EMAIL_ADDRESS_REQUIRED when emailAddress miss const mod = loadModule("pages/api/endpoint/getemailaccountcheck_api.js", { respondError: respondErrorMock, respondSuccess: respondSuccessMock, + relayGet: async () => ({}), getToken: async () => ({ access_token: "token" }), hashAPIPath: () => "&hash=expected", azureHeaders: () => ({}), @@ -74,6 +78,8 @@ test("getemailaccountcheck catch path returns EMAIL_ACCOUNT_CHECK_FAILED", async const mod = loadModule("pages/api/endpoint/getemailaccountcheck_api.js", { respondError: respondErrorMock, respondSuccess: respondSuccessMock, + relayGet: async ({ res, errorResponse }) => + respondErrorMock(res, errorResponse), getToken: async () => ({ access_token: "token" }), hashAPIPath: () => "&hash=expected", azureHeaders: () => ({}), @@ -100,6 +106,7 @@ test("getpreferredlanguage returns EMAIL_ADDRESS_REQUIRED when emailAddress miss const mod = loadModule("pages/api/endpoint/getpreferredlanguage_api.js", { respondError: respondErrorMock, respondSuccess: respondSuccessMock, + relayGet: async () => ({}), sanitizeString: () => "", isNonEmptyString: () => false, escapeODataString: (value) => value, @@ -122,6 +129,8 @@ test("getpreferredlanguage catch path returns PREFERRED_LANGUAGE_FETCH_FAILED", const mod = loadModule("pages/api/endpoint/getpreferredlanguage_api.js", { respondError: respondErrorMock, respondSuccess: respondSuccessMock, + relayGet: async ({ res, errorResponse }) => + respondErrorMock(res, errorResponse), sanitizeString: (value) => value, isNonEmptyString: () => true, escapeODataString: (value) => value, @@ -151,6 +160,15 @@ test("getpreferredlanguage success returns existing data payload contract", asyn const mod = loadModule("pages/api/endpoint/getpreferredlanguage_api.js", { respondError: respondErrorMock, respondSuccess: respondSuccessMock, + relayGet: async ({ res }) => + respondSuccessMock(res, { + value: [ + { + contactid: "c1", + pinswg_preferredlanguage: 807570001 + } + ] + }), sanitizeString: (value) => value, isNonEmptyString: () => true, escapeODataString: (value) => value, @@ -191,6 +209,7 @@ test("getlogin returns EMAIL_ADDRESS_REQUIRED when emailAddress missing", async const mod = loadModule("pages/api/endpoint/getlogin_api.js", { respondError: respondErrorMock, respondSuccess: respondSuccessMock, + relayGet: async () => ({}), getToken: async () => ({ access_token: "token" }), hashAPIPath: () => "&hash=expected", azureHeadersPaged: () => ({}), @@ -210,6 +229,7 @@ test("getlogin returns PASSWORD_REQUIRED when pwd missing", async () => { const mod = loadModule("pages/api/endpoint/getlogin_api.js", { respondError: respondErrorMock, respondSuccess: respondSuccessMock, + relayGet: async () => ({}), getToken: async () => ({ access_token: "token" }), hashAPIPath: () => "&hash=expected", azureHeadersPaged: () => ({}), @@ -229,6 +249,8 @@ test("getlogin catch path returns LOGIN_FETCH_FAILED", async () => { const mod = loadModule("pages/api/endpoint/getlogin_api.js", { respondError: respondErrorMock, respondSuccess: respondSuccessMock, + relayGet: async ({ res, errorResponse }) => + respondErrorMock(res, errorResponse), getToken: async () => ({ access_token: "token" }), hashAPIPath: () => "&hash=expected", azureHeadersPaged: () => ({}), @@ -414,6 +436,8 @@ test("getportalloginproxy catch path returns PORTAL_LOGIN_PROXY_FETCH_FAILED", a const mod = loadModule("pages/api/endpoint/getportalloginproxy_api.js", { respondError: respondErrorMock, respondSuccess: respondSuccessMock, + relayGet: async ({ res, errorResponse }) => + respondErrorMock(res, errorResponse), getToken: async () => ({ access_token: "token" }), hashAPIPath: () => "&hash=expected", azureHeadersPaged: () => ({}), @@ -440,6 +464,7 @@ test("getpersonalaccount returns CONTACT_ID_REQUIRED when contactid missing", as const mod = loadModule("pages/api/endpoint/getpersonalaccount_api.js", { respondError: respondErrorMock, respondSuccess: respondSuccessMock, + relayGet: async () => ({}), getToken: async () => ({ access_token: "token" }), hashAPIPath: () => "&hash=expected", azureHeaders: () => ({}), @@ -459,6 +484,8 @@ test("getpersonalaccount catch path returns PERSONAL_ACCOUNT_FETCH_FAILED", asyn const mod = loadModule("pages/api/endpoint/getpersonalaccount_api.js", { respondError: respondErrorMock, respondSuccess: respondSuccessMock, + relayGet: async ({ res, errorResponse }) => + respondErrorMock(res, errorResponse), getToken: async () => ({ access_token: "token" }), hashAPIPath: () => "&hash=expected", azureHeaders: () => ({}), @@ -485,6 +512,8 @@ test("getaccounts success returns existing data payload contract", async () => { const mod = loadModule("pages/api/endpoint/getaccounts_api.js", { respondError: respondErrorMock, respondSuccess: respondSuccessMock, + relayGet: async ({ res }) => + respondSuccessMock(res, { value: [{ contactid: "c1" }] }), getToken: async () => ({ access_token: "token" }), hashAPIPath: () => "&hash=expected", azureHeaders: () => ({}), @@ -508,6 +537,10 @@ test("getemailaccountcheck success returns existing data payload contract", asyn const mod = loadModule("pages/api/endpoint/getemailaccountcheck_api.js", { respondError: respondErrorMock, respondSuccess: respondSuccessMock, + relayGet: async ({ res }) => + respondSuccessMock(res, { + value: [{ emailaddress1: "u@test" }] + }), getToken: async () => ({ access_token: "token" }), hashAPIPath: () => "&hash=expected", azureHeaders: () => ({}),