diff --git a/actions/clients/README.md b/actions/clients/README.md index 9d2ccbc3..b5fe214c 100644 --- a/actions/clients/README.md +++ b/actions/clients/README.md @@ -1,6 +1,18 @@ # Actions Clients -This folder is reserved for extracted client wrappers from `actions/index.js` as part of the Priority 1 refactor plan. +This folder contains extracted client wrappers from `actions/index.js` as part of the Priority 1 refactor plan. -Phase 1 delivered core helper extraction and compatibility barrel support. -Client-level extraction (`relayClient`, `endpointClient`, `fileClient`, `notifyClient`) is planned for the next increment. +Current extracted clients: + +- `relayClient` (shared hash-signing helper used by account/portal/document direct services) +- `endpointClient` (shared JSON request helpers for GET and generic axios config requests) + +Current usage includes: + +- relay hash-signing helper reuse across account/portal/document direct services +- shared endpoint JSON request helpers reused by admin/notify/integration/reference-data direct services + +Notes: + +- Core helper extraction and compatibility barrel support remain in place. +- Additional client extraction (`fileClient`, `notifyClient`) can be layered in incrementally without changing public exports from `actions/index.js`. diff --git a/actions/clients/endpointClient.js b/actions/clients/endpointClient.js new file mode 100644 index 00000000..2e5ff950 --- /dev/null +++ b/actions/clients/endpointClient.js @@ -0,0 +1,11 @@ +import axios from "axios"; + +export const getJson = async (url, config) => { + const response = await axios.get(url, config); + return response.data; +}; + +export const requestJson = async (config) => { + const response = await axios(config); + return response.data; +}; diff --git a/actions/clients/fileClient.js b/actions/clients/fileClient.js new file mode 100644 index 00000000..e639bb30 --- /dev/null +++ b/actions/clients/fileClient.js @@ -0,0 +1,34 @@ +import { getJson, requestJson } from "./endpointClient"; +import { buildHashedQueryUrl } from "./relayClient"; + +export const getFileJson = (url) => { + return getJson(url); +}; + +export const getSignedFileJson = async (queryUrl) => { + const signedUrl = await buildHashedQueryUrl(queryUrl); + + return requestJson({ + method: "get", + url: signedUrl + }); +}; + +export const downloadFileBlob = (url) => { + return requestJson({ + method: "get", + url, + responseType: "blob" + }); +}; + +export const postSignedFileJson = async (queryUrl, data, config = {}) => { + const signedUrl = await buildHashedQueryUrl(queryUrl); + + return requestJson({ + method: "post", + url: signedUrl, + data, + ...config + }); +}; diff --git a/actions/clients/fileRouteBuilder.js b/actions/clients/fileRouteBuilder.js new file mode 100644 index 00000000..944c366f --- /dev/null +++ b/actions/clients/fileRouteBuilder.js @@ -0,0 +1,33 @@ +const encodeRouteParam = (value) => encodeURIComponent(String(value)); + +export const buildFileQuery = (path, params = {}, options = {}) => { + const { encode = false } = options; + + const entries = Object.entries(params).filter(([, value]) => { + return value !== undefined && value !== null; + }); + + if (entries.length === 0) { + return path; + } + + const query = entries + .map(([key, value]) => { + if (!encode) { + return `${key}=${String(value)}`; + } + + return `${encodeRouteParam(key)}=${encodeRouteParam(value)}`; + }) + .join("&"); + + return `${path}?${query}`; +}; + +export const withBaseUrl = (baseUrl, route) => { + return `${baseUrl}${route}`; +}; + +export const appendQuerySuffix = (route, suffix = "") => { + return `${route}${suffix}`; +}; diff --git a/actions/clients/index.js b/actions/clients/index.js new file mode 100644 index 00000000..3c9b8ee9 --- /dev/null +++ b/actions/clients/index.js @@ -0,0 +1,4 @@ +export * from "./relayClient"; +export * from "./endpointClient"; +export * from "./fileClient"; +export * from "./fileRouteBuilder"; diff --git a/actions/clients/relayClient.js b/actions/clients/relayClient.js new file mode 100644 index 00000000..3876b7b9 --- /dev/null +++ b/actions/clients/relayClient.js @@ -0,0 +1,22 @@ +import axios from "axios"; +import { hashAPIPath } from "../core/hash"; + +export const buildHashedQueryUrl = async (queryUrl) => { + try { + const signRes = await axios.get( + "/api/endpoint/gethash_api?path=" + encodeURIComponent(queryUrl) + ); + + if (!signRes?.data?.hash) { + throw new Error("Hash signature unavailable"); + } + + return queryUrl + signRes.data.hash; + } catch (error) { + if (typeof window === "undefined" && process.env.HASHKEY) { + return queryUrl + hashAPIPath(queryUrl); + } + + throw error; + } +}; diff --git a/actions/core/token.js b/actions/core/token.js index 98fbde02..92664dff 100644 --- a/actions/core/token.js +++ b/actions/core/token.js @@ -1,4 +1,4 @@ -import axios from "axios"; +import { requestJson } from "../clients/endpointClient"; import { ACCESS_TOKEN_ENDPOINT, TENANT_ID } from "./env"; import { consoleLogger } from "./logger"; @@ -23,20 +23,19 @@ const tokenConfig = { } }; -export const getToken = () => { - return axios - .post( - `${ACCESS_TOKEN_ENDPOINT}${TENANT_ID}/oauth2/v2.0/token`, - tokenBody, - tokenConfig - ) - .then((res) => res.data) - .then((data) => { - cache.tokenResponse = data; - return data; - }) - .catch((error) => { - consoleLogger(error); - return error; +export const getToken = async () => { + try { + const data = await requestJson({ + method: "post", + url: `${ACCESS_TOKEN_ENDPOINT}${TENANT_ID}/oauth2/v2.0/token`, + data: tokenBody, + ...tokenConfig }); + + cache.tokenResponse = data; + return data; + } catch (error) { + consoleLogger(error); + return error; + } }; diff --git a/actions/index.js b/actions/index.js index 4849f4a1..b7c500c3 100644 --- a/actions/index.js +++ b/actions/index.js @@ -5,4 +5,5 @@ export * from "./core/token"; export * from "./core/headers"; export * from "./core/guards"; +export * from "./clients"; export * from "./services"; diff --git a/actions/services/accountDirectService.js b/actions/services/accountDirectService.js index 5a236450..4284b1f9 100644 --- a/actions/services/accountDirectService.js +++ b/actions/services/accountDirectService.js @@ -1,55 +1,25 @@ -import axios from "axios"; import { BASE_URL } from "../core/env"; import { consoleLogger } from "../core/logger"; -import { hashAPIPath } from "../core/hash"; - -const buildHashedQueryUrl = async (queryUrl) => { - try { - const signRes = await axios.get( - "/api/endpoint/gethash_api?path=" + encodeURIComponent(queryUrl) - ); - - if (!signRes?.data?.hash) { - throw new Error("Hash signature unavailable"); - } - - return queryUrl + signRes.data.hash; - } catch (error) { - if (typeof window === "undefined" && process.env.HASHKEY) { - return queryUrl + hashAPIPath(queryUrl); - } - - throw error; - } -}; +import { buildHashedQueryUrl } from "../clients/relayClient"; +import { getJson, requestJson } from "../clients/endpointClient"; export const getPersonalAccount = (contactid) => { - return axios - .get( - BASE_URL + - "/api/endpoint/getpersonalaccount_api?contactid=" + - contactid - ) - .then((res) => { - return res.data; - }) - .catch((error) => { - consoleLogger(error); - }); + return getJson( + BASE_URL + "/api/endpoint/getpersonalaccount_api?contactid=" + contactid + ).catch((error) => { + consoleLogger(error); + }); }; export const getLogin = (emailAddress, pwd) => { - return axios - .get( - "/api/endpoint/getlogin_api?emailAddress=" + - emailAddress + - "&pwd=" + - pwd - ) - .then((res) => res.data) - .catch((error) => { - consoleLogger(error); - }); + return getJson( + "/api/endpoint/getlogin_api?emailAddress=" + + emailAddress + + "&pwd=" + + pwd + ).catch((error) => { + consoleLogger(error); + }); }; export const updatePassword = (contactId, newpassword) => { @@ -61,13 +31,9 @@ export const updatePassword = (contactId, newpassword) => { data: data }; - return axios(config) - .then((res) => { - return res.data; - }) - .catch((error) => { - consoleLogger(error); - }); + return requestJson(config).catch((error) => { + consoleLogger(error); + }); }; export const updateAccount = async (contactId, updateBody, ssr) => { @@ -81,13 +47,9 @@ export const updateAccount = async (contactId, updateBody, ssr) => { url: queryUrl, data: data }; - return axios(config) - .then((res) => { - return res.data; - }) - .catch((error) => { - consoleLogger(error); - }); + return requestJson(config).catch((error) => { + consoleLogger(error); + }); }; export const createAccount = (formValues) => { @@ -99,67 +61,50 @@ export const createAccount = (formValues) => { data: data }; - return axios(config) - .then((res) => { - return res.data; - }) - .catch((error) => { - consoleLogger(error); - }); + return requestJson(config).catch((error) => { + consoleLogger(error); + }); }; export const getEmailAccountCheck = (emailAddress) => { - return axios - .get( - "/api/endpoint/getemailaccountcheck_api?emailAddress=" + - emailAddress - ) - .then((res) => res.data) - .catch((error) => { - consoleLogger(error); - }); + return getJson( + "/api/endpoint/getemailaccountcheck_api?emailAddress=" + emailAddress + ).catch((error) => { + consoleLogger(error); + }); }; export const getPortalLogin = async (emailAddress) => { var queryUrl = "/api/endpoint/getportallogin_api?emailAddress=" + emailAddress; - return axios - .get(BASE_URL + (await buildHashedQueryUrl(queryUrl))) - .then((res) => res.data) - .catch((error) => { + return getJson(BASE_URL + (await buildHashedQueryUrl(queryUrl))).catch( + (error) => { consoleLogger(error); return JSON.stringify(error); - }); + } + ); }; export const getPortalLoginProxy = async (emailAddress) => { var queryUrl = "/api/endpoint/getportalloginproxy_api?emailAddress=" + emailAddress; - return axios - .get(queryUrl) - .then((res) => res.data) - .catch((error) => { - consoleLogger(error); + return getJson(queryUrl).catch((error) => { + consoleLogger(error); - return JSON.stringify(error); - }); + return JSON.stringify(error); + }); }; export const getPreferredLanguage = async (email) => { - return axios - .get( - BASE_URL + - "/api/endpoint/getpreferredlanguage_api?emailAddress=" + - email - ) - .then((res) => { - return res.data; - }) - .catch((error) => { - consoleLogger(error); - return error.response; - }); + return getJson( + BASE_URL + + "/api/endpoint/getpreferredlanguage_api?emailAddress=" + + email + ).catch((error) => { + consoleLogger(error); + return error.response; + }); }; diff --git a/actions/services/adminDirectService.js b/actions/services/adminDirectService.js index 94b14af8..93deb2ef 100644 --- a/actions/services/adminDirectService.js +++ b/actions/services/adminDirectService.js @@ -1,11 +1,10 @@ -import axios from "axios"; import { BASE_URL } from "../core/env"; import { logAndReturnResponse } from "./httpServiceUtils"; +import { getJson } from "../clients/endpointClient"; export const getNewAppeals = async (searchString) => { try { - const res = await axios.get(BASE_URL + "/api/admin/getnewappeals_api"); - return res.data; + return await getJson(BASE_URL + "/api/admin/getnewappeals_api"); } catch (error) { return logAndReturnResponse(error); } @@ -18,8 +17,8 @@ export const getNewAppealsPage = async ( fieldSort, showNumberOfRecords ) => { - return axios - .get( + try { + return await getJson( "/api/admin/getnewappeals_api?searchString=" + searchString + "&pageNumber=" + @@ -30,11 +29,10 @@ export const getNewAppealsPage = async ( fieldSort + "&showNumberOfRecords=" + showNumberOfRecords - ) - .then((res) => { - return res.data; - }) - .catch(logAndReturnResponse); + ); + } catch (error) { + return logAndReturnResponse(error); + } }; export const getNewDocumentsPaged = async ( @@ -46,8 +44,8 @@ export const getNewDocumentsPaged = async ( documentOrigin, selectedWeeks ) => { - return axios - .get( + try { + return await getJson( "/api/admin/getlatestdocuments_api?pageNumber=" + pageNumber + "&orderby=" + @@ -62,9 +60,8 @@ export const getNewDocumentsPaged = async ( selectedWeeks + "&documentOrigin=" + documentOrigin - ) - .then((res) => { - return res.data; - }) - .catch(logAndReturnResponse); + ); + } catch (error) { + return logAndReturnResponse(error); + } }; diff --git a/actions/services/caseDirectService.js b/actions/services/caseDirectService.js index ed9c2291..0b406534 100644 --- a/actions/services/caseDirectService.js +++ b/actions/services/caseDirectService.js @@ -1,70 +1,55 @@ -import axios from "axios"; import { BASE_URL } from "../core/env"; import { consoleLogger } from "../core/logger"; import { logAndReturnResponse } from "./httpServiceUtils"; +import { getJson, requestJson } from "../clients/endpointClient"; +import { buildFileQuery, withBaseUrl } from "../clients/fileRouteBuilder"; export const getCaseMessage = (searchString) => { - return axios - .get(BASE_URL + "/api/endpoint/getcasemessage_api?id=" + searchString) - .then((res) => { - return res.data; - }) - .catch(logAndReturnResponse); + const route = buildFileQuery("/api/endpoint/getcasemessage_api", { + id: searchString + }); + + return getJson(withBaseUrl(BASE_URL, route)).catch(logAndReturnResponse); }; export const getIncidentbyID = (searchString) => { - return axios - .get( - BASE_URL + - "/api/endpoint/getincidentbyid_api?searchString=" + - searchString - ) - .then((res) => { - return res.data; - }) - .catch(logAndReturnResponse); + const route = buildFileQuery("/api/endpoint/getincidentbyid_api", { + searchString + }); + + return getJson(withBaseUrl(BASE_URL, route)).catch(logAndReturnResponse); }; export const getIsPublishedbyID = (searchString) => { - return axios - .get( - "/api/endpoint/getispublishedbyid_api?searchString=" + searchString - ) - .then((res) => { - return res.data; - }) - .catch(logAndReturnResponse); + const route = buildFileQuery("/api/endpoint/getispublishedbyid_api", { + searchString + }); + + return getJson(route).catch(logAndReturnResponse); }; export const getPartSavedAppeal = (searchString) => { - return axios - .get( - BASE_URL + - "/api/endpoint/getpartsavedappeal_api?searchString=" + - searchString - ) - .then((res) => { - return res.data; - }) - .catch(logAndReturnResponse); + const route = buildFileQuery("/api/endpoint/getpartsavedappeal_api", { + searchString + }); + + return getJson(withBaseUrl(BASE_URL, route)).catch(logAndReturnResponse); }; export const getSIPSEvents = async (caseid) => { - return axios - .get(BASE_URL + "/api/endpoint/getsipsevents_api?caseid=" + caseid) - .then((res) => { - return res.data; - }) - .catch(logAndReturnResponse); + const route = buildFileQuery("/api/endpoint/getsipsevents_api", { + caseid + }); + + return getJson(withBaseUrl(BASE_URL, route)).catch(logAndReturnResponse); }; export const getSIPSMedia = async (caseid) => { - return axios - .get(BASE_URL + "/api/endpoint/getsipsmedia_api?caseid=" + caseid) - .then((res) => { - return res.data; - }) - .catch(logAndReturnResponse); + const route = buildFileQuery("/api/endpoint/getsipsmedia_api", { + caseid + }); + + return getJson(withBaseUrl(BASE_URL, route)).catch(logAndReturnResponse); }; export const getAppealID = ( @@ -72,17 +57,15 @@ export const getAppealID = ( updateFormCollection, primaryAttribute ) => { - return axios - .get( - "/api/endpoint/getappealid_api?updateFormCollection=" + - updateFormCollection + - "&primaryAttribute=" + - primaryAttribute + - "&caseReference=" + - caseReference - ) - .then((res) => { - const result = Object.entries(res.data.value[0]).filter( + const route = buildFileQuery("/api/endpoint/getappealid_api", { + updateFormCollection, + primaryAttribute, + caseReference + }); + + return getJson(route) + .then((data) => { + const result = Object.entries(data.value[0]).filter( ([key]) => !key.startsWith("_") )[0][1]; var appealID = result; @@ -104,29 +87,22 @@ export const createNewCase = ( var data = createBody; - var queryUrl = - "/api/endpoint/createcase_api?appealTypeId=" + - appealTypeId + - "&lpaID=" + - lpaID + - "&contactid=" + - contactid + - "&containername=" + - containerName; + var queryUrl = buildFileQuery("/api/endpoint/createcase_api", { + appealTypeId, + lpaID, + contactid, + containername: containerName + }); var config = { method: "post", - url: BASE_URL + queryUrl, + url: withBaseUrl(BASE_URL, queryUrl), data: data }; - return axios(config) - .then((res) => { - return res.data; - }) - .catch((error) => { - consoleLogger(error); - }); + return requestJson(config).catch((error) => { + consoleLogger(error); + }); }; export const createNewCaseBlob = ( @@ -144,15 +120,12 @@ export const createNewCaseBlob = ( data.pinswg_lpaname = lpaName; data.createdon = new Date(); - var queryUrl = - "/api/file/createcase_api?appealTypeId=" + - appealTypeId + - "&lpaID=" + - lpaID + - "&contactid=" + - contactid + - "&containername=" + - containerName; + var queryUrl = buildFileQuery("/api/file/createcase_api", { + appealTypeId, + lpaID, + contactid, + containername: containerName + }); var config = { method: "post", @@ -160,13 +133,9 @@ export const createNewCaseBlob = ( data: data }; - return axios(config) - .then((res) => { - return res.data; - }) - .catch((error) => { - consoleLogger(error); - }); + return requestJson(config).catch((error) => { + consoleLogger(error); + }); }; export const updateCase = async ( @@ -184,11 +153,10 @@ export const updateCase = async ( primaryAttribute ); - var queryUrl = - "/api/endpoint/updatecase_api?updateFormCollection=" + - updateFormCollection + - "&appealObj=" + - appealObj; + var queryUrl = buildFileQuery("/api/endpoint/updatecase_api", { + updateFormCollection, + appealObj + }); var config = { method: "post", @@ -196,13 +164,9 @@ export const updateCase = async ( data: updateBody }; - return axios(config) - .then((res) => { - return res.data; - }) - .catch((error) => { - consoleLogger(error); - }); + return requestJson(config).catch((error) => { + consoleLogger(error); + }); }; export const updateCaseBlob = async ( @@ -220,11 +184,10 @@ export const updateCaseBlob = async ( primaryAttribute ); - var queryUrl = - "/api/file/updatecase_api?updateFormCollection=" + - updateFormCollection + - "&appealObj=" + - appealObj; + var queryUrl = buildFileQuery("/api/file/updatecase_api", { + updateFormCollection, + appealObj + }); var config = { method: "post", @@ -232,113 +195,82 @@ export const updateCaseBlob = async ( data: updateBody }; - return axios(config) - .then((res) => { - return res.data; - }) - .catch((error) => { - consoleLogger(error); - }); + return requestJson(config).catch((error) => { + consoleLogger(error); + }); }; export const patchCase = async (incidentid) => { - var queryUrl = "/api/endpoint/patchcase_api?incidentid=" + incidentid; - var config = { - method: "get", - url: queryUrl - }; - - return axios(config) - .then((res) => { - return res.data; - }) - .catch((error) => { - //console.log("this error:", error); - }); + var queryUrl = buildFileQuery("/api/endpoint/patchcase_api", { + incidentid + }); + return getJson(queryUrl).catch((error) => { + //console.log("this error:", error); + }); }; export const getCase = (incidentID) => { - return axios - .get(BASE_URL + "/api/endpoint/getcase_api?incidentID=" + incidentID) - .then((res) => { - return res.data; - }) - .catch((error) => { - consoleLogger(error); - }); + const route = buildFileQuery("/api/endpoint/getcase_api", { + incidentID + }); + + return getJson(withBaseUrl(BASE_URL, route)).catch((error) => { + consoleLogger(error); + }); }; export const getCaseByID = (incidentID) => { - return axios - .get( - BASE_URL + "/api/endpoint/getcasebyid_api?incidentID=" + incidentID - ) - .then((res) => { - return res.data; - }) - .catch((error) => { - consoleLogger(error); - }); + const route = buildFileQuery("/api/endpoint/getcasebyid_api", { + incidentID + }); + + return getJson(withBaseUrl(BASE_URL, route)).catch((error) => { + consoleLogger(error); + }); }; export const getAppealPDFDocs = (incidentID) => { - return axios - .get( - BASE_URL + - "/api/endpoint/getappealpdfdocuments_api?incidentid=" + - incidentID - ) - .then((res) => res.data) - .catch((error) => { - consoleLogger(error); - }); + const route = buildFileQuery("/api/endpoint/getappealpdfdocuments_api", { + incidentid: incidentID + }); + + return getJson(withBaseUrl(BASE_URL, route)).catch((error) => { + consoleLogger(error); + }); }; export const getAppealPDFDocument = async (incidentid) => { - return axios - .get( - BASE_URL + - "/api/endpoint/getappealpdfdocuments_api?incidentid=" + - incidentid - ) - .then((res) => { - return res.data; - }) - .catch((error) => { - consoleLogger(error); - return error.response; - }); + const route = buildFileQuery("/api/endpoint/getappealpdfdocuments_api", { + incidentid + }); + + return getJson(withBaseUrl(BASE_URL, route)).catch((error) => { + consoleLogger(error); + return error.response; + }); }; export const getPortalModuleDetails = async (appealType, caseReference) => { - var config = { - method: "get", - url: - BASE_URL + - "/api/endpoint/getportalmoduledetails_api?appealType=" + - appealType + - "&caseReference=" + - encodeURI(caseReference) - }; + const route = buildFileQuery("/api/endpoint/getportalmoduledetails_api", { + appealType, + caseReference: encodeURI(caseReference) + }); - try { - const res = await axios(config); - return res.data; - } catch (error) { + return getJson(withBaseUrl(BASE_URL, route)).catch((error) => { consoleLogger(error); - } + }); }; export const getPortalModuleDetailsProxy = (appealType, caseReference) => { - return axios - .get( - "/api/endpoint/getportalmoduledetailsproxy_api?appealType=" + - appealType + - "&caseReference=" + - caseReference.replace(/\'/g, "''") - ) - .then((res) => res.data) - .catch((error) => { - consoleLogger(error); - }); + const route = buildFileQuery( + "/api/endpoint/getportalmoduledetailsproxy_api", + { + appealType, + caseReference: caseReference.replace(/\'/g, "''") + } + ); + + return getJson(route).catch((error) => { + consoleLogger(error); + }); }; diff --git a/actions/services/documentDirectService.js b/actions/services/documentDirectService.js index 567ebc7b..b3d0d3c6 100644 --- a/actions/services/documentDirectService.js +++ b/actions/services/documentDirectService.js @@ -1,110 +1,88 @@ -import axios from "axios"; import { BASE_URL } from "../core/env"; import { consoleLogger } from "../core/logger"; import { hashAPIPath } from "../core/hash"; - -const buildHashedQueryUrl = async (queryUrl) => { - try { - const signRes = await axios.get( - "/api/endpoint/gethash_api?path=" + encodeURIComponent(queryUrl) - ); - - if (!signRes?.data?.hash) { - throw new Error("Hash signature unavailable"); - } - - return queryUrl + signRes.data.hash; - } catch (error) { - if (typeof window === "undefined" && process.env.HASHKEY) { - return queryUrl + hashAPIPath(queryUrl); - } - - throw error; - } -}; +import { + buildFileQuery, + withBaseUrl, + appendQuerySuffix +} from "../clients/fileRouteBuilder"; +import { + getFileJson, + getSignedFileJson, + downloadFileBlob, + postSignedFileJson +} from "../clients/fileClient"; export const getAwaitingSubmissionFromBlob = (containerName) => { - return axios - .get( - BASE_URL + - "/api/file/getawaitingsubmissionfromblob?container=" + - containerName + - hashAPIPath( - "/api/file/getawaitingsubmissionfromblob?container=" + - containerName - ) - ) - .then((res) => { - return res.data; - }) - .catch((error) => { - consoleLogger(error); - }); + const route = buildFileQuery("/api/file/getawaitingsubmissionfromblob", { + container: containerName + }); + + return getFileJson( + withBaseUrl(BASE_URL, appendQuerySuffix(route, hashAPIPath(route))) + ).catch((error) => { + consoleLogger(error); + }); }; export const getRepsFromBlob = (containerName) => { - return axios - .get( - BASE_URL + - "/api/file/getrepsblob?container=" + - containerName + - hashAPIPath("/api/file/getrepsblob?container=" + containerName) - ) - .then((res) => { - return res.data; - }) - .catch((error) => { - consoleLogger(error); - }); + const route = buildFileQuery("/api/file/getrepsblob", { + container: containerName + }); + + return getFileJson( + withBaseUrl(BASE_URL, appendQuerySuffix(route, hashAPIPath(route))) + ).catch((error) => { + consoleLogger(error); + }); }; export const getRepsFromBlobProxy = async (containerName) => { - try { - const res = await axios.get( - "/api/file/getrepsblobproxy?container=" + containerName - ); - return res.data; - } catch (error) { + const route = buildFileQuery("/api/file/getrepsblobproxy", { + container: containerName + }); + + return getFileJson(route).catch((error) => { consoleLogger(error); - } + }); }; export const getAwaitingSubmissionFromBlobProxy = async (containerName) => { - try { - const res = await axios.get( - BASE_URL + - "/api/file/getawaitingsubmissionfromblobproxy?container=" + - containerName - ); - return res.data; - } catch (error) { + const route = buildFileQuery( + "/api/file/getawaitingsubmissionfromblobproxy", + { + container: containerName + } + ); + + return getFileJson(withBaseUrl(BASE_URL, route)).catch((error) => { consoleLogger(error); - } + }); +}; + +export const getFilesFromBlobproxy = (containerName, casefolderID) => { + const route = buildFileQuery("/api/file/getbloblistproxy", { + container: containerName, + casefolderID + }); + + return getFileJson(route).catch((error) => { + consoleLogger(error); + }); }; export const deleteAwaitingSubmissionsFromBlob = ( containerID, casefolderID ) => { - var queryUrl = - "/api/file/deleteblobcase?container=" + - containerID + - "&casefolderID=" + - casefolderID; + var queryUrl = buildFileQuery("/api/file/deleteblobcase", { + container: containerID, + casefolderID + }); - return buildHashedQueryUrl(queryUrl) - .then((signedUrl) => - axios({ - method: "get", - url: signedUrl - }) - ) - .then((res) => { - return res.data; - }) - .catch((error) => { - consoleLogger(error); - }); + return getSignedFileJson(queryUrl).catch((error) => { + consoleLogger(error); + }); }; export const deleteMyRepresentationsFromBlob = ( @@ -112,27 +90,15 @@ export const deleteMyRepresentationsFromBlob = ( casefolderID, repfile ) => { - var queryUrl = - "/api/file/deleteblobrep?container=" + - containerID + - "&casefolderID=" + - casefolderID + - "&repfile=" + - repfile; + var queryUrl = buildFileQuery("/api/file/deleteblobrep", { + container: containerID, + casefolderID, + repfile + }); - return buildHashedQueryUrl(queryUrl) - .then((signedUrl) => - axios({ - method: "get", - url: signedUrl - }) - ) - .then((res) => { - return res.data; - }) - .catch((error) => { - consoleLogger(error); - }); + return getSignedFileJson(queryUrl).catch((error) => { + consoleLogger(error); + }); }; export const uploadFiles = async ( @@ -156,18 +122,10 @@ export const uploadFiles = async ( var queryUrl = "/api/file/upload"; - const hashedUrl = await buildHashedQueryUrl(queryUrl); - - const config = { - method: "post", - url: hashedUrl, - data: formData, - headers: { "content-type": "multipart/form-data" } - }; - try { - const res = await axios(config); - return res.data; + return await postSignedFileJson(queryUrl, formData, { + headers: { "content-type": "multipart/form-data" } + }); } catch (error) { consoleLogger(error); } @@ -186,18 +144,10 @@ export const uploadSingleFile = async (filesObj, containerID, casefolderID) => { var queryUrl = "/api/file/uploadsinglefile"; - const hashedUrl = await buildHashedQueryUrl(queryUrl); - - const config = { - method: "post", - url: hashedUrl, - data: formData, - headers: { "content-type": "multipart/form-data" } - }; - try { - const res = await axios(config); - return res.data; + return await postSignedFileJson(queryUrl, formData, { + headers: { "content-type": "multipart/form-data" } + }); } catch (error) { consoleLogger(error); } @@ -217,18 +167,10 @@ export const uploadRepFiles = async ( var queryUrl = "/api/file/upload"; - const hashedUrl = await buildHashedQueryUrl(queryUrl); - - const config = { - method: "post", - url: hashedUrl, - data: formData, - headers: { "content-type": "multipart/form-data" } - }; - try { - const res = await axios(config); - return res.data; + return await postSignedFileJson(queryUrl, formData, { + headers: { "content-type": "multipart/form-data" } + }); } catch (error) { consoleLogger(error); } @@ -243,18 +185,10 @@ export const generateRepPDF = async ( var queryUrl = "/api/file/generatepdf" + (options.download ? "?download=true" : ""); - const hashedUrl = await buildHashedQueryUrl(queryUrl); - - const config = { - method: "post", - url: hashedUrl, - data: formValues, - ...(options.download ? { responseType: "blob" } : {}) - }; - try { - const res = await axios(config); - return res.data; + return await postSignedFileJson(queryUrl, formValues, { + ...(options.download ? { responseType: "blob" } : {}) + }); } catch (error) { consoleLogger(error); } @@ -277,60 +211,26 @@ export const generateAppealPDF = async ( appealType + (options.download ? "&download=true" : ""); - const hashedUrl = await buildHashedQueryUrl(queryUrl); - - const config = { - method: "post", - url: hashedUrl, - data: formValues, - ...(options.download ? { responseType: "blob" } : {}) - }; - try { - const res = await axios(config); - return res.data; + return await postSignedFileJson(queryUrl, formValues, { + ...(options.download ? { responseType: "blob" } : {}) + }); } catch (error) { consoleLogger(error); } }; export const getFilesFromBlob = (containerName, casefolderID) => { - return axios - .get( - BASE_URL + - "/api/file/getbloblist?container=" + - containerName + - "&casefolderID=" + - casefolderID + - hashAPIPath( - "/api/file/getbloblist?container=" + - containerName + - "&casefolderID=" + - casefolderID - ) - ) - .then((res) => { - return res.data; - }) - .catch((error) => { - consoleLogger(error); - }); -}; + const route = buildFileQuery("/api/file/getbloblist", { + container: containerName, + casefolderID + }); -export const getFilesFromBlobproxy = (containerName, casefolderID) => { - return axios - .get( - "/api/file/getbloblistproxy?container=" + - containerName + - "&casefolderID=" + - casefolderID - ) - .then((res) => { - return res.data; - }) - .catch((error) => { - consoleLogger(error); - }); + return getFileJson( + withBaseUrl(BASE_URL, appendQuerySuffix(route, hashAPIPath(route))) + ).catch((error) => { + consoleLogger(error); + }); }; export const getFilesFromBlobHashed = ( @@ -338,20 +238,16 @@ export const getFilesFromBlobHashed = ( getblobshash, casefolderID ) => { - return axios - .get( - "/api/file/getbloblist?container=" + - containerName + - "&casefolderID=" + - casefolderID + - getblobshash - ) - .then((res) => { - return res.data; - }) - .catch((error) => { + const route = buildFileQuery("/api/file/getbloblist", { + container: containerName, + casefolderID + }); + + return getFileJson(appendQuerySuffix(route, getblobshash)).catch( + (error) => { consoleLogger(error); - }); + } + ); }; export const deleteBlob = async ( @@ -360,20 +256,23 @@ export const deleteBlob = async ( deleteblobhash, casefolderID ) => { - try { - const res = await axios.get( - "/api/file/deleteblob?container=" + - containerName + - "&casefolderID=" + - encodeURIComponent(casefolderID) + - "&blobname=" + - encodeURIComponent(blobName) + - deleteblobhash - ); - return res.data; - } catch (error) { - consoleLogger(error); - } + const route = buildFileQuery( + "/api/file/deleteblob", + { + container: containerName, + casefolderID, + blobname: blobName + }, + { + encode: true + } + ); + + return getFileJson(appendQuerySuffix(route, deleteblobhash)).catch( + (error) => { + consoleLogger(error); + } + ); }; export const deleteRepBlob = async ( @@ -383,78 +282,67 @@ export const deleteRepBlob = async ( casefolderID, filenamePrefix ) => { - try { - const res = await axios.get( - "/api/file/deleteblob?container=" + - containerName + - "&casefolderID=" + - encodeURIComponent(casefolderID + "/" + filenamePrefix) + - "&blobname=" + - encodeURIComponent(blobName) + - deleteblobhash - ); - return res.data; - } catch (error) { - consoleLogger(error); - } + const route = buildFileQuery( + "/api/file/deleteblob", + { + container: containerName, + casefolderID: casefolderID + "/" + filenamePrefix, + blobname: blobName + }, + { + encode: true + } + ); + + return getFileJson(appendQuerySuffix(route, deleteblobhash)).catch( + (error) => { + consoleLogger(error); + } + ); }; export const downloadBlob = (containerName, blobName) => { - return axios - .get( - BASE_URL + - "/api/file/downloadblob?container=" + - containerName.toLowerCase() + - "&blobname=" + - blobName, - { responseType: "blob" } - ) - .then((response) => { - res.setHeader( - "content-disposition", - "attachment; filename=" + blobName - ); - - return res.status(200).send(response.data); + const queryUrl = withBaseUrl( + BASE_URL, + buildFileQuery("/api/file/downloadblob", { + container: containerName.toLowerCase(), + blobname: blobName }) - .catch((error) => { - consoleLogger(error); - }); + ); + + return downloadFileBlob(queryUrl).catch((error) => { + consoleLogger(error); + }); }; export const getProgressFromBlob = async (containerName, casereference) => { - try { - const res = await axios.get( - BASE_URL + - "/api/file/getprogressobjblob?container=" + - encodeURIComponent(containerName) + - "&casefolderID=" + - encodeURIComponent(casereference) + - hashAPIPath( - "/api/file/getprogressobjblob?container=" + - encodeURIComponent(containerName) + - "&casefolderID=" + - encodeURIComponent(casereference) - ) - ); - return res.data; - } catch (error) { + const route = buildFileQuery( + "/api/file/getprogressobjblob", + { + container: containerName, + casefolderID: casereference + }, + { + encode: true + } + ); + + return getFileJson( + withBaseUrl(BASE_URL, appendQuerySuffix(route, hashAPIPath(route))) + ).catch((error) => { consoleLogger(error); - } + }); }; export const createContainerProxy = (containerName) => { - var queryUrl = - "/api/file/setupcontainer?ident=" + - containerName + - hashAPIPath("/api/file/setupcontainer?ident=" + containerName); + var route = buildFileQuery("/api/file/setupcontainer", { + ident: containerName + }); + var queryUrl = appendQuerySuffix(route, hashAPIPath(route)); - return axios - .get(BASE_URL + queryUrl) - .then((res) => res.data) - .catch((error) => { - consoleLogger(error); + return getFileJson(withBaseUrl(BASE_URL, queryUrl)).catch((error) => { + consoleLogger(error); - return JSON.stringify(error); - }); + return JSON.stringify(error); + }); }; diff --git a/actions/services/integrationDirectService.js b/actions/services/integrationDirectService.js index 51a54602..b2ebe7f3 100644 --- a/actions/services/integrationDirectService.js +++ b/actions/services/integrationDirectService.js @@ -1,5 +1,5 @@ -import axios from "axios"; import { consoleLogger } from "../core/logger"; +import { requestJson } from "../clients/endpointClient"; export const createCRMTask = async (formValues) => { var queryUrl = "/api/endpoint/createcrmtask_api"; @@ -12,8 +12,7 @@ export const createCRMTask = async (formValues) => { }; try { - const res = await axios(config); - return res.data; + return await requestJson(config); } catch (error) { consoleLogger(error); } diff --git a/actions/services/notifyDirectService.js b/actions/services/notifyDirectService.js index 557ae2cc..b36b64de 100644 --- a/actions/services/notifyDirectService.js +++ b/actions/services/notifyDirectService.js @@ -1,5 +1,5 @@ -import axios from "axios"; import { consoleLogger } from "../core/logger"; +import { requestJson } from "../clients/endpointClient"; export const sendEmail = async ( templateId, @@ -15,8 +15,11 @@ export const sendEmail = async ( }; try { - const response = await axios.post("/api/email/notify", mailData); - return response.data; + return await requestJson({ + method: "post", + url: "/api/email/notify", + data: mailData + }); } catch (error) { consoleLogger(error); throw error; diff --git a/actions/services/portalDirectService.js b/actions/services/portalDirectService.js index 1e112810..54b1eb92 100644 --- a/actions/services/portalDirectService.js +++ b/actions/services/portalDirectService.js @@ -1,167 +1,132 @@ -import axios from "axios"; import { BASE_URL } from "../core/env"; import { consoleLogger } from "../core/logger"; -import { hashAPIPath } from "../core/hash"; - -const buildHashedQueryUrl = async (queryUrl) => { - try { - const signRes = await axios.get( - "/api/endpoint/gethash_api?path=" + encodeURIComponent(queryUrl) - ); - - if (!signRes?.data?.hash) { - throw new Error("Hash signature unavailable"); - } - - return queryUrl + signRes.data.hash; - } catch (error) { - if (typeof window === "undefined" && process.env.HASHKEY) { - return queryUrl + hashAPIPath(queryUrl); - } - - throw error; - } -}; +import { buildHashedQueryUrl } from "../clients/relayClient"; +import { getJson, requestJson } from "../clients/endpointClient"; +import { + buildFileQuery, + withBaseUrl, + appendQuerySuffix +} from "../clients/fileRouteBuilder"; export const getMyCases = (loggedInUserId) => { - return axios - .get( - BASE_URL + - "/api/endpoint/getmycases_api?loggedInUserId=" + - loggedInUserId - ) - .then((res) => { - return res.data; - }) - .catch((error) => { - consoleLogger(error); - }); + const route = buildFileQuery("/api/endpoint/getmycases_api", { + loggedInUserId + }); + + return getJson(withBaseUrl(BASE_URL, route)).catch((error) => { + consoleLogger(error); + }); }; export const getMyInvolvements = async (loggedInUserId) => { - try { - const res = await axios.get( - BASE_URL + - "/api/endpoint/getmyinvolvements_api?loggedInUserId=" + - loggedInUserId - ); - return res.data; - } catch (error) { + const route = buildFileQuery("/api/endpoint/getmyinvolvements_api", { + loggedInUserId + }); + + return getJson(withBaseUrl(BASE_URL, route)).catch((error) => { consoleLogger(error); - } + }); }; export const getMyLPACases = (lpaid) => { - return axios - .get(BASE_URL + "/api/endpoint/getmylpacases_api?lpaid=" + lpaid) - .then((res) => { - return res.data; - }) - .catch((error) => { - consoleLogger(error); - }); + const route = buildFileQuery("/api/endpoint/getmylpacases_api", { + lpaid + }); + + return getJson(withBaseUrl(BASE_URL, route)).catch((error) => { + consoleLogger(error); + }); }; export const getMyRepresentations = (loggedInUserId) => { - return axios - .get( - BASE_URL + - "/api/endpoint/getmyrepresentations_api?loggedInUserId=" + - loggedInUserId - ) - .then((res) => res.data) - .catch((error) => { - consoleLogger(error); - }); + const route = buildFileQuery("/api/endpoint/getmyrepresentations_api", { + loggedInUserId + }); + + return getJson(withBaseUrl(BASE_URL, route)).catch((error) => { + consoleLogger(error); + }); }; export const getMyRepresentationsProxy = (loggedInUserId) => { - return axios - .get( - "/api/endpoint/getmyrepresentationsproxy_api?loggedInUserId=" + - loggedInUserId - ) - .then((res) => res.data) - .catch((error) => { - consoleLogger(error); - }); + const route = buildFileQuery( + "/api/endpoint/getmyrepresentationsproxy_api", + { + loggedInUserId + } + ); + + return getJson(route).catch((error) => { + consoleLogger(error); + }); }; export const getRepresentations = (incidentID) => { - return axios - .get("/api/endpoint/getrepresentations_api?incidentID=" + incidentID) - .then((res) => res.data) - .catch((error) => { - consoleLogger(error); - }); + const route = buildFileQuery("/api/endpoint/getrepresentations_api", { + incidentID + }); + + return getJson(route).catch((error) => { + consoleLogger(error); + }); }; export const getRepresentationsProxy = (incidentID) => { - return axios - .get( - "/api/endpoint/getrepresentationsproxy_api?incidentID=" + incidentID - ) - .then((res) => res.data) - .catch((error) => { - consoleLogger(error); - }); + const route = buildFileQuery("/api/endpoint/getrepresentationsproxy_api", { + incidentID + }); + + return getJson(route).catch((error) => { + consoleLogger(error); + }); }; export const getWatchedCases = (loggedInUserId) => { - return axios - .get( - BASE_URL + - "/api/endpoint/getwatchedcases_api?loggedInUserId=" + - loggedInUserId - ) - .then((res) => res.data) - .catch((error) => { - consoleLogger(error); - }); + const route = buildFileQuery("/api/endpoint/getwatchedcases_api", { + loggedInUserId + }); + + return getJson(withBaseUrl(BASE_URL, route)).catch((error) => { + consoleLogger(error); + }); }; export const getWatchedCasesProxy = (loggedInUserId) => { - return axios - .get( - "/api/endpoint/getwatchedcasesproxy_api?loggedInUserId=" + - loggedInUserId - ) - .then((res) => res.data) - .catch((error) => { - consoleLogger(error); - }); + const route = buildFileQuery("/api/endpoint/getwatchedcasesproxy_api", { + loggedInUserId + }); + + return getJson(route).catch((error) => { + consoleLogger(error); + }); }; export const getAwaitingSubmissionProxy = (loggedInUserId) => { - return axios - .get( - "/api/endpoint/getawaitingsubmissionproxy_api?loggedInUserId=" + - loggedInUserId - ) - .then((res) => res.data) - .catch((error) => { - consoleLogger(error); - }); + const route = buildFileQuery( + "/api/endpoint/getawaitingsubmissionproxy_api", + { + loggedInUserId + } + ); + + return getJson(route).catch((error) => { + consoleLogger(error); + }); }; export const getAwaitingSubmission = (loggedInUserId) => { - return axios - .get( - BASE_URL + - "/api/endpoint/getawaitingsubmission_api?loggedInUserId=" + - loggedInUserId - ) - .then((res) => { - return res.data; - }) - .catch((error) => { - consoleLogger(error); - }); + const route = buildFileQuery("/api/endpoint/getawaitingsubmission_api", { + loggedInUserId + }); + + return getJson(withBaseUrl(BASE_URL, route)).catch((error) => { + consoleLogger(error); + }); }; export const createWatchedCases = async (formValues) => { var data = formValues; - var queryUrl = "/api/endpoint/createwatchedcases_api"; + var queryUrl = buildFileQuery("/api/endpoint/createwatchedcases_api"); var config = { method: "post", @@ -170,21 +135,20 @@ export const createWatchedCases = async (formValues) => { }; try { - const res = await axios(config); - return res.data; + return await requestJson(config); } catch (error) { consoleLogger(error); } }; export const deleteMyRepresentations = (myRepresentationsID) => { - var queryUrl = - "/api/endpoint/deletemyrepresentations_api?myRepresentationsID=" + - myRepresentationsID; + var queryUrl = buildFileQuery("/api/endpoint/deletemyrepresentations_api", { + myRepresentationsID + }); return buildHashedQueryUrl(queryUrl) .then((signedUrl) => - axios({ + requestJson({ method: "delete", url: signedUrl, headers: { @@ -197,46 +161,41 @@ export const deleteMyRepresentations = (myRepresentationsID) => { } }) ) - .then((res) => { - return res.data; - }) .catch((error) => { consoleLogger(error); }); }; export const deleteAwaitingSubmissions = (incidentID) => { - var queryUrl = - "/api/endpoint/deleteawaitingsubmissions_api?incidentID=" + incidentID; + var queryUrl = buildFileQuery( + "/api/endpoint/deleteawaitingsubmissions_api", + { + incidentID + } + ); var config = { method: "delete", url: queryUrl }; - return axios(config) - .then((res) => { - return res.data; - }) - .catch((error) => { - consoleLogger(error); - }); + return requestJson(config).catch((error) => { + consoleLogger(error); + }); }; export const deleteWatchedCases = async (watchedCaseID) => { - var queryUrl = - "/api/endpoint/deletewatchedcases_api?watchedCaseID=" + watchedCaseID; + var queryUrl = buildFileQuery("/api/endpoint/deletewatchedcases_api", { + watchedCaseID + }); return buildHashedQueryUrl(queryUrl) .then((signedUrl) => - axios({ + requestJson({ method: "delete", url: signedUrl }) ) - .then((res) => { - return res.data; - }) .catch((error) => { consoleLogger(error); }); @@ -247,60 +206,57 @@ export const sendCaseCompleteMessage = async ( caseReference, inv ) => { - var hashQueryPath = - "/api/file/createappealcompletemessage_api?container=" + - containerID + - "&tempcaseref=" + - caseReference; + var hashQueryPath = buildFileQuery( + "/api/file/createappealcompletemessage_api", + { + container: containerID, + tempcaseref: caseReference + } + ); - var queryUrl = - "/api/file/createappealcompletemessage_api?container=" + - containerID + - "&tempcaseref=" + - caseReference + - "&inv=" + - inv; + var queryUrl = buildFileQuery("/api/file/createappealcompletemessage_api", { + container: containerID, + tempcaseref: caseReference, + inv + }); var signedQueryUrl = await buildHashedQueryUrl(hashQueryPath); - queryUrl = queryUrl + signedQueryUrl.replace(hashQueryPath, ""); + queryUrl = appendQuerySuffix( + queryUrl, + signedQueryUrl.replace(hashQueryPath, "") + ); var config = { method: "get", url: queryUrl }; - return axios(config) - .then((res) => { - return res.data; - }) - .catch((error) => { - consoleLogger(error); - }); + return requestJson(config).catch((error) => { + consoleLogger(error); + }); }; export const sendCaseCompleteMessageProxy = async ( containerID, caseReference ) => { - var queryUrl = - "/api/file/createappealcompletemessageproxy_api?container=" + - containerID + - "&tempcaseref=" + - caseReference; + var queryUrl = buildFileQuery( + "/api/file/createappealcompletemessageproxy_api", + { + container: containerID, + tempcaseref: caseReference + } + ); var config = { method: "get", url: queryUrl }; - return axios(config) - .then((res) => { - return res.data; - }) - .catch((error) => { - consoleLogger(error); - }); + return requestJson(config).catch((error) => { + consoleLogger(error); + }); }; export const sendRepCompleteMessage = async ( @@ -308,13 +264,14 @@ export const sendRepCompleteMessage = async ( caseReference, fileName ) => { - var hashQueryPath = - "/api/file/createrepcompletemessage_api?container=" + - containerID + - "&tempcaseref=" + - caseReference + - "&repid=" + - fileName; + var hashQueryPath = buildFileQuery( + "/api/file/createrepcompletemessage_api", + { + container: containerID, + tempcaseref: caseReference, + repid: fileName + } + ); var queryUrl = await buildHashedQueryUrl(hashQueryPath); @@ -323,17 +280,13 @@ export const sendRepCompleteMessage = async ( url: queryUrl }; - return axios(config) - .then((res) => { - return res.data; - }) - .catch((error) => { - consoleLogger(error); - }); + return requestJson(config).catch((error) => { + consoleLogger(error); + }); }; export const setRepInvolvment = async (caseid, contactid) => { - var queryUrl = "/api/file/createrepinvolvement_api"; + var queryUrl = buildFileQuery("/api/file/createrepinvolvement_api"); var data = { "incidentid": caseid, "contactid": contactid }; var config = { @@ -343,15 +296,14 @@ export const setRepInvolvment = async (caseid, contactid) => { }; try { - const res = await axios(config); - return res.data; + return await requestJson(config); } catch (error) { consoleLogger(error); } }; export const setCaseInvolvment = async (caseid, contactid) => { - var queryUrl = "/api/file/createrepinvolvement_api"; + var queryUrl = buildFileQuery("/api/file/createrepinvolvement_api"); var data = { "incidentid": caseid, "contactid": contactid }; var config = { @@ -361,8 +313,7 @@ export const setCaseInvolvment = async (caseid, contactid) => { }; try { - const res = await axios(config); - return res.data; + return await requestJson(config); } catch (error) { consoleLogger(error); } diff --git a/actions/services/referenceDataDirectService.js b/actions/services/referenceDataDirectService.js index 406c8103..cafa9a7d 100644 --- a/actions/services/referenceDataDirectService.js +++ b/actions/services/referenceDataDirectService.js @@ -1,76 +1,58 @@ -import axios from "axios"; import { BASE_URL } from "../core/env"; import { consoleLogger } from "../core/logger"; import { logAndReturnEmptyValueErrorResponse } from "./httpServiceUtils"; +import { getJson } from "../clients/endpointClient"; export const getAppealsTypes = () => { - return axios - .get(BASE_URL + "/api/endpoint/getappealtypes_api") - .then((res) => res.data) - .catch(logAndReturnEmptyValueErrorResponse); + return getJson(BASE_URL + "/api/endpoint/getappealtypes_api").catch( + logAndReturnEmptyValueErrorResponse + ); }; export const getProjectTypes = () => { - return axios - .get(BASE_URL + "/api/endpoint/getprojecttypes_api") - .then((res) => res.data) - .catch(logAndReturnEmptyValueErrorResponse); + return getJson(BASE_URL + "/api/endpoint/getprojecttypes_api").catch( + logAndReturnEmptyValueErrorResponse + ); }; export const getAppealsTypesForNewAppeal = () => { - return axios - .get(BASE_URL + "/api/endpoint/getappealtypesfornewappeal_api") - .then((res) => res.data) - .catch(logAndReturnEmptyValueErrorResponse); + return getJson( + BASE_URL + "/api/endpoint/getappealtypesfornewappeal_api" + ).catch(logAndReturnEmptyValueErrorResponse); }; export const getLPA = () => { - return axios - .get(BASE_URL + "/api/endpoint/getlpa_api") - .then((res) => { - return res.data; - }) - .catch(logAndReturnEmptyValueErrorResponse); + return getJson(BASE_URL + "/api/endpoint/getlpa_api").catch( + logAndReturnEmptyValueErrorResponse + ); }; export const getFormData = (whichForm) => { - return axios - .get(BASE_URL + "/api/endpoint/getformdata_api?whichForm=" + whichForm) - .then((res) => res.data) - .catch((error) => { - consoleLogger(error); - }); + return getJson( + BASE_URL + "/api/endpoint/getformdata_api?whichForm=" + whichForm + ).catch((error) => { + consoleLogger(error); + }); }; export const getMandatoryFields = (whichForm) => { - return axios - .get( - BASE_URL + - "/api/endpoint/getmandatoryfields_api?whichForm=" + - whichForm - ) - .then((res) => res.data) - .catch((error) => { - consoleLogger(error); - }); + return getJson( + BASE_URL + "/api/endpoint/getmandatoryfields_api?whichForm=" + whichForm + ).catch((error) => { + consoleLogger(error); + }); }; export const getPickLists = (whichForm) => { - return axios - .get(BASE_URL + "/api/endpoint/getpicklists_api?whichForm=" + whichForm) - .then((res) => res.data) - .catch((error) => { - consoleLogger(error); - }); + return getJson( + BASE_URL + "/api/endpoint/getpicklists_api?whichForm=" + whichForm + ).catch((error) => { + consoleLogger(error); + }); }; export const getNotice = () => { - return axios - .get(BASE_URL + "/api/notices") - .then((res) => { - return res.data; - }) - .catch((error) => { - consoleLogger(error); - }); + return getJson(BASE_URL + "/api/notices").catch((error) => { + consoleLogger(error); + }); }; diff --git a/actions/services/searchDirectService.js b/actions/services/searchDirectService.js index 0037ea98..15742157 100644 --- a/actions/services/searchDirectService.js +++ b/actions/services/searchDirectService.js @@ -1,35 +1,25 @@ -import axios from "axios"; import { BASE_URL } from "../core/env"; import { consoleLogger } from "../core/logger"; +import { getJson } from "../clients/endpointClient"; import { logAndReturnResponse, logAndReturnEmptyValueErrorResponse } from "./httpServiceUtils"; export const getBasicSearch = (searchString) => { - return axios - .get( - BASE_URL + - "/api/endpoint/getbasicsearch_api?searchString=" + - searchString - ) - .then((res) => { - return res.data; - }) - .catch(logAndReturnResponse); + return getJson( + BASE_URL + + "/api/endpoint/getbasicsearch_api?searchString=" + + searchString + ).catch(logAndReturnResponse); }; export const getBasicDNSURLSearch = (searchString) => { - return axios - .get( - BASE_URL + - "/api/endpoint/getbasicdnsurlsearch_api?searchString=" + - searchString - ) - .then((res) => { - return res.data; - }) - .catch(logAndReturnResponse); + return getJson( + BASE_URL + + "/api/endpoint/getbasicdnsurlsearch_api?searchString=" + + searchString + ).catch(logAndReturnResponse); }; export const getAddressSearchPaged = ( @@ -39,23 +29,18 @@ export const getAddressSearchPaged = ( fieldSort, showNumberOfRecords ) => { - return axios - .get( - "/api/endpoint/getbasicsearch_by_address_paged_api?searchString=" + - searchString + - "&pageNumber=" + - pageNumber + - "&orderby=" + - orderBy + - "&fieldSort=" + - fieldSort + - "&showNumberOfRecords=" + - showNumberOfRecords - ) - .then((res) => { - return res.data; - }) - .catch(logAndReturnResponse); + return getJson( + "/api/endpoint/getbasicsearch_by_address_paged_api?searchString=" + + searchString + + "&pageNumber=" + + pageNumber + + "&orderby=" + + orderBy + + "&fieldSort=" + + fieldSort + + "&showNumberOfRecords=" + + showNumberOfRecords + ).catch(logAndReturnResponse); }; export const getBasicSearchPaged = ( @@ -65,46 +50,36 @@ export const getBasicSearchPaged = ( fieldSort, showNumberOfRecords ) => { - return axios - .get( - "/api/endpoint/getbasicsearchpaged_api?searchString=" + - searchString + - "&pageNumber=" + - pageNumber + - "&orderby=" + - orderBy + - "&fieldSort=" + - fieldSort + - "&showNumberOfRecords=" + - showNumberOfRecords - ) - .then((res) => { - return res.data; - }) - .catch(logAndReturnResponse); + return getJson( + "/api/endpoint/getbasicsearchpaged_api?searchString=" + + searchString + + "&pageNumber=" + + pageNumber + + "&orderby=" + + orderBy + + "&fieldSort=" + + fieldSort + + "&showNumberOfRecords=" + + showNumberOfRecords + ).catch(logAndReturnResponse); }; export const getDNSCoords = () => { - return axios - .get(BASE_URL + "/api/endpoint/getdnscoords_api") - .then((res) => { - return res.data; - }) - .catch(logAndReturnEmptyValueErrorResponse); + return getJson(BASE_URL + "/api/endpoint/getdnscoords_api").catch( + logAndReturnEmptyValueErrorResponse + ); }; export const getDNSList = (searchString) => { - return axios - .get(BASE_URL + "/api/endpoint/getdnslist_api") - .then((res) => res.data) - .catch(logAndReturnResponse); + return getJson(BASE_URL + "/api/endpoint/getdnslist_api").catch( + logAndReturnResponse + ); }; export const getBasicDNSSearch = (searchString) => { - return axios - .get(BASE_URL + "/api/endpoint/getbasicdnssearch_api") - .then((res) => res.data) - .catch(logAndReturnResponse); + return getJson(BASE_URL + "/api/endpoint/getbasicdnssearch_api").catch( + logAndReturnResponse + ); }; export const getBasicDNSSearchPaged = ( @@ -113,30 +88,24 @@ export const getBasicDNSSearchPaged = ( fieldSort, showNumberOfRecords ) => { - return axios - .get( - "/api/endpoint/getbasicdnssearchpaged_api?pageNumber=" + - pageNumber + - "&orderby=" + - orderBy + - "&fieldSort=" + - fieldSort + - "&showNumberOfRecords=" + - showNumberOfRecords - ) - .then((res) => res.data) - .catch(logAndReturnResponse); + return getJson( + "/api/endpoint/getbasicdnssearchpaged_api?pageNumber=" + + pageNumber + + "&orderby=" + + orderBy + + "&fieldSort=" + + fieldSort + + "&showNumberOfRecords=" + + showNumberOfRecords + ).catch(logAndReturnResponse); }; export const getAdvancedSearch = (searchString) => { - return axios - .get( - BASE_URL + - "/api/endpoint/getadvancedsearch_api?searchstring=" + - JSON.stringify(searchString) - ) - .then((res) => res.data) - .catch(logAndReturnEmptyValueErrorResponse); + return getJson( + BASE_URL + + "/api/endpoint/getadvancedsearch_api?searchstring=" + + JSON.stringify(searchString) + ).catch(logAndReturnEmptyValueErrorResponse); }; export const getAdvancedSearchPaged = ( @@ -146,21 +115,18 @@ export const getAdvancedSearchPaged = ( fieldSort, showNumberOfRecords ) => { - return axios - .get( - "/api/endpoint/getadvancedsearchpaged_api?searchstring=" + - JSON.stringify(searchString) + - "&pageNumber=" + - pageNumber + - "&orderby=" + - orderBy + - "&fieldSort=" + - fieldSort + - "&showNumberOfRecords=" + - showNumberOfRecords - ) - .then((res) => res.data) - .catch(logAndReturnEmptyValueErrorResponse); + return getJson( + "/api/endpoint/getadvancedsearchpaged_api?searchstring=" + + JSON.stringify(searchString) + + "&pageNumber=" + + pageNumber + + "&orderby=" + + orderBy + + "&fieldSort=" + + fieldSort + + "&showNumberOfRecords=" + + showNumberOfRecords + ).catch(logAndReturnEmptyValueErrorResponse); }; export const getBasicSearchDetails = async ( @@ -169,20 +135,17 @@ export const getBasicSearchDetails = async ( primaryIdAttribute, incidentID ) => { - return axios - .get( - BASE_URL + - "/api/endpoint/getbasicsearchdetails_api?appealTypeName=" + - appealTypeName + - "&primaryIdAttribute=" + - primaryIdAttribute + - "&incidentID=" + - incidentID - ) - .then((res) => res.data) - .catch((error) => { - consoleLogger(error); - }); + return getJson( + BASE_URL + + "/api/endpoint/getbasicsearchdetails_api?appealTypeName=" + + appealTypeName + + "&primaryIdAttribute=" + + primaryIdAttribute + + "&incidentID=" + + incidentID + ).catch((error) => { + consoleLogger(error); + }); }; export const getBasicPartSavedDetails = ( @@ -191,20 +154,17 @@ export const getBasicPartSavedDetails = ( primaryIdAttribute, incidentID ) => { - return axios - .get( - BASE_URL + - "/api/endpoint/getbasicpartsaveddetails_api?appealTypeName=" + - appealTypeName + - "&primaryIdAttribute=" + - primaryIdAttribute + - "&incidentID=" + - incidentID - ) - .then((res) => res.data) - .catch((error) => { - consoleLogger(error); - }); + return getJson( + BASE_URL + + "/api/endpoint/getbasicpartsaveddetails_api?appealTypeName=" + + appealTypeName + + "&primaryIdAttribute=" + + primaryIdAttribute + + "&incidentID=" + + incidentID + ).catch((error) => { + consoleLogger(error); + }); }; export const getBasicSearchDetailsPaged = async ( @@ -231,8 +191,8 @@ export const getBasicSearchDetailsPaged = async ( )}`; try { - const res = await axios.get(url); - return res.data.value || []; + const data = await getJson(url); + return data.value || []; } catch (error) { consoleLogger(error); return []; @@ -240,27 +200,20 @@ export const getBasicSearchDetailsPaged = async ( }; export const getSearchDocumentDetails = (incidentID) => { - return axios - .get( - "/api/endpoint/getsearchdocumentdetails_api?incidentid=" + - incidentID - ) - .then((res) => res.data) - .catch((error) => { - consoleLogger(error); - throw error; - }); + return getJson( + "/api/endpoint/getsearchdocumentdetails_api?incidentid=" + incidentID + ).catch((error) => { + consoleLogger(error); + throw error; + }); }; export const getSearchDocumentTypes = (incidentID) => { - return axios - .get( - "/api/endpoint/getsearchdocumentTypes_api?incidentid=" + incidentID - ) - .then((res) => res.data) - .catch((error) => { - consoleLogger(error); - }); + return getJson( + "/api/endpoint/getsearchdocumentTypes_api?incidentid=" + incidentID + ).catch((error) => { + consoleLogger(error); + }); }; export const getSearchDocumentDetailsPaged = ( @@ -271,39 +224,30 @@ export const getSearchDocumentDetailsPaged = ( showNumberOfRecords, documentType ) => { - return axios - .get( - "/api/endpoint/getsearchdocumentdetailspaged_api?incidentid=" + - incidentID + - "&pageNumber=" + - pageNumber + - "&orderby=" + - orderBy + - "&fieldSort=" + - fieldSort + - "&showNumberOfRecords=" + - showNumberOfRecords + - "&documentType=" + - documentType - ) - .then((res) => res.data) - .catch((error) => { - consoleLogger(error); - }); + return getJson( + "/api/endpoint/getsearchdocumentdetailspaged_api?incidentid=" + + incidentID + + "&pageNumber=" + + pageNumber + + "&orderby=" + + orderBy + + "&fieldSort=" + + fieldSort + + "&showNumberOfRecords=" + + showNumberOfRecords + + "&documentType=" + + documentType + ).catch((error) => { + consoleLogger(error); + }); }; export const getLinkedCases = (parentIncidentid) => { - return axios - .get( - "/api/endpoint/getlinkedcases_api?parentincidentid=" + - parentIncidentid - ) - .then((res) => { - return res.data; - }) - .catch((error) => { - consoleLogger(error); - }); + return getJson( + "/api/endpoint/getlinkedcases_api?parentincidentid=" + parentIncidentid + ).catch((error) => { + consoleLogger(error); + }); }; export const getAddressSearch = async (searchString) => { @@ -313,14 +257,5 @@ export const getAddressSearch = async (searchString) => { var queryUrl = BASE_URL + "/api/endpoint/getbasicsearch_by_address_api?" + str; - var config = { - method: "get", - url: queryUrl - }; - - return axios(config) - .then((res) => { - return res.data; - }) - .catch(logAndReturnEmptyValueErrorResponse); + return getJson(queryUrl).catch(logAndReturnEmptyValueErrorResponse); }; diff --git a/context/architecture.md b/context/architecture.md index 8fc7a59c..151886f2 100644 --- a/context/architecture.md +++ b/context/architecture.md @@ -75,3 +75,58 @@ Guidance: - Do not treat these files as active runtime architecture unless explicitly reactivated. - If reactivation is proposed, document rationale and rollout/rollback in `memory-bank/change-log.md` and `context/runbook.md`. + +## Current State Assessment and Prioritised Next Steps (2026-03-25) + +### Assessment summary + +The platform has moved into a stronger operational and architectural posture through sustained bounded refactor slices and contract hardening. + +Strengths: + +1. **Governance maturity is high** + - Guardrails are explicit for auth/session integrity, CSP/security headers, Prisma source-of-truth, relay hash integrity, and EN/CY parity. +2. **API reliability posture has improved materially** + - Endpoint contract hardening and phase21 contract test expansion have reduced inconsistency in negative-path handling. +3. **Relay operations are significantly more robust** + - Shared relay forwarding now includes bounded retry/timeout policy, structured redacted lifecycle logging, and documented rollout/rollback controls. +4. **Façade decomposition is delivering low-risk progress** + - `actions` layer migration to shared clients (`relayClient`, `endpointClient`) is reducing duplicated request boilerplate and lowering drift risk. + +Primary residual risks/gaps: + +1. **Remaining direct-service inconsistency** + - Some direct services still contain legacy axios/request patterns and bespoke signed-request blocks. +2. **Coverage concentration** + - Contract tests are strong in targeted slices, but end-to-end/high-value journey coverage in sensitive flows remains comparatively sparse. +3. **Logging hygiene variance** + - Structured redaction exists in relay paths, but broader codebase logging still has uneven consistency. +4. **i18n parity assurance remains process-heavy** + - EN/CY parity relies heavily on manual discipline rather than automated parity checks. + +### Prioritised next steps + +1. **Complete direct-service consistency sweep (low risk, high maintainability)** + - Prioritise `actions/services/searchDirectService.js` for `getJson`/`requestJson` adoption in bounded slices. + - Preserve existing error-return behavior contracts per function. +2. **Consolidate signed-request patterns (medium risk, high security clarity)** + - Introduce a focused signed-request helper for hash-based/signed delete/get pathways currently repeated in service modules. + - Keep existing hash/header semantics unchanged while reducing duplication. +3. **Add high-value regression automation (high value)** + - Add focused automated checks for: + - auth callback/redirect safety + - one signed-delete negative path + - one upload/document authorization negative path + - one EN/CY route parity check +4. **Perform targeted logging hardening in sensitive paths** + - Continue replacing direct/verbose logging in `auth`, `file`, `email`, and account-sensitive endpoint paths with redacted structured logging patterns. +5. **Introduce EN/CY parity CI checks** + - Add automated checks for route rewrite parity and locale key alignment to reduce drift and manual burden. +6. **Continue endpoint sprawl reduction** + - Keep collapsing duplicated proxy/request patterns behind shared helpers in bounded route clusters while preserving public response contracts. + +### Recommended execution sequence + +- **Sequence A (immediate):** Step 1 + Step 3 (fastest risk reduction per effort) +- **Sequence B (next):** Step 2 + Step 4 (security/logging consistency consolidation) +- **Sequence C (after):** Step 5 + Step 6 (institutionalise parity and reduce long-tail maintenance cost) diff --git a/memory-bank/change-log.md b/memory-bank/change-log.md index 51bf914b..269a4093 100644 --- a/memory-bank/change-log.md +++ b/memory-bank/change-log.md @@ -1444,3 +1444,1284 @@ Validation: Follow-ups: - P2-S3 planned slices are now complete; no further mandatory relay policy rollout slices remain for this stream. + +--- + +### CL-039: TASK22260 actions façade increment — shared relay client extraction + +date: 2026-03-25 +author: Cline +scope: `actions/clients/{relayClient,index}.js`, `actions/services/{accountDirectService,portalDirectService,documentDirectService}.js`, `actions/index.js`, `actions/clients/README.md` +type: change +rationale: Continue Priority 1 façade decomposition by extracting duplicated hash-signing relay helper logic into a dedicated client module while preserving existing service/public export contracts. +impact: Reduces duplication and drift risk in security-sensitive relay signing helper logic without changing call-site behavior. +status: completed + +Summary: + +- Added a new shared client wrapper: + - `actions/clients/relayClient.js` exporting `buildHashedQueryUrl` +- Added `actions/clients/index.js` barrel and exposed client exports via `actions/index.js`. +- Updated direct services to consume shared relay client helper instead of duplicating local helper implementations: + - `actions/services/accountDirectService.js` + - `actions/services/portalDirectService.js` + - `actions/services/documentDirectService.js` +- Updated `actions/clients/README.md` to reflect the now-implemented relay client extraction and future incremental client split path. + +Validation: + +- `npm run lint` -> warnings only (pre-existing `react-hooks/exhaustive-deps`; no new lint errors) +- Verified no remaining duplicated local `buildHashedQueryUrl` definitions across `actions/services/*DirectService.js` + +Follow-ups: + +- Optional next TASK22260 increment: extract common axios invocation helpers into dedicated clients (`endpointClient`, `fileClient`, `notifyClient`) while keeping `actions/index.js` API stable. + +Addendum (same TASK22260 slice): + +- Added shared `endpointClient` with `getJson` and `requestJson` helpers (`actions/clients/endpointClient.js`) and exported it via `actions/clients/index.js`. +- Migrated additional direct services to consume shared endpoint client helpers: + - `actions/services/notifyDirectService.js` (POST via `requestJson`) + - `actions/services/integrationDirectService.js` (POST via `requestJson`) + - `actions/services/adminDirectService.js` (GET flows via `getJson`) +- Updated `actions/clients/README.md` to include `endpointClient` in current extracted clients. + +--- + +### CL-040: TASK22260 next slice — reference-data direct service endpointClient adoption + +date: 2026-03-25 +author: Cline +scope: `actions/services/referenceDataDirectService.js`, `actions/clients/README.md` +type: change +rationale: Continue the incremental façade/client adoption stream by migrating another bounded direct-service module to shared endpoint request helpers. +impact: Reduces axios boilerplate and centralizes JSON extraction behavior for reference-data requests without changing public call signatures. +status: completed + +Summary: + +- Migrated `actions/services/referenceDataDirectService.js` from direct `axios.get(...).then(res => res.data)` patterns to shared `getJson(...)` helper from `actions/clients/endpointClient`. +- Preserved existing error behavior: + - `logAndReturnEmptyValueErrorResponse` for appeals/project/LPA fetches + - `consoleLogger` catch handling for form/mandatory/picklist/notice fetches +- Updated `actions/clients/README.md` usage notes to include reference-data service reuse. + +Validation: + +- `npm run lint` -> warnings only (pre-existing `react-hooks/exhaustive-deps`; no new lint errors) + +Follow-ups: + +- Next optional bounded slice: adopt `getJson`/`requestJson` for selected low-risk read paths in `searchDirectService` or `caseDirectService` while preserving per-function error semantics. + +--- + +### CL-041: TASK22260 next slice — account direct service endpointClient adoption + +date: 2026-03-25 +author: Cline +scope: `actions/services/accountDirectService.js` +type: change +rationale: Continue incremental façade migration by moving account direct-service request plumbing onto shared endpoint client helpers while preserving existing error-return behavior contracts. +impact: Reduces duplicated axios response extraction boilerplate and aligns account service request handling with the emerging client-layer pattern. +status: completed + +Summary: + +- Refactored `actions/services/accountDirectService.js` to consume shared endpoint client helpers: + - `getJson(...)` for GET requests + - `requestJson(...)` for config-based POST requests +- Kept existing relay hash-signing behavior unchanged via `buildHashedQueryUrl` from `relayClient`. +- Preserved existing catch-path semantics, including: + - logging with `consoleLogger` + - returning `JSON.stringify(error)` in portal login functions + - returning `error.response` in preferred-language failure path + +Validation: + +- `npm run lint` -> warnings only (pre-existing `react-hooks/exhaustive-deps`; no new lint errors) + +Follow-ups: + +- Optional next bounded slice: adopt endpoint client helpers in selected `portalDirectService` GET/POST helper paths while preserving delete/hash flow semantics. + +--- + +### CL-042: TASK22260 next slice — portal direct service partial endpointClient adoption + +date: 2026-03-25 +author: Cline +scope: `actions/services/portalDirectService.js` +type: change +rationale: Continue phased client-layer adoption by migrating low-risk portal direct-service read/create paths to shared endpoint request helpers while leaving hash-sensitive delete/message flows unchanged. +impact: Reduces duplicated axios response extraction on high-traffic portal retrieval paths and keeps hashed delete/message semantics stable. +status: completed + +Summary: + +- Refactored selected `portalDirectService` functions to use shared endpoint client helpers: + - `getJson(...)` for read/listing routes (`getMyCases`, `getMyInvolvements`, `getMyLPACases`, representations, watched, awaiting submission variants) + - `requestJson(...)` for `createWatchedCases` +- Preserved existing hash/delete/message flow implementations (`deleteMyRepresentations`, `deleteWatchedCases`, completion message functions) using existing axios + relay signing behavior. +- Preserved existing catch-path logging behavior for all migrated functions. + +Validation: + +- `npm run lint` -> warnings only (pre-existing `react-hooks/exhaustive-deps`; no new lint errors) + +Follow-ups: + +- Optional next bounded slice: migrate remaining non-hash POST helpers in `portalDirectService` (`setRepInvolvment`, `setCaseInvolvment`) to `requestJson` for full internal consistency. + +--- + +### CL-043: TASK22260 next slice — portal involvement helper endpointClient completion + +date: 2026-03-25 +author: Cline +scope: `actions/services/portalDirectService.js` +type: change +rationale: Complete the next bounded internal-consistency slice by migrating remaining non-hash portal involvement POST helpers to shared endpoint client request plumbing. +impact: Aligns portal service POST helper internals with established `requestJson` usage while preserving route semantics and error handling. +status: completed + +Summary: + +- Migrated remaining portal involvement helper POST functions to shared endpoint client: + - `setRepInvolvment` + - `setCaseInvolvment` +- Both now use `requestJson(config)` while preserving existing payload shape, endpoint URLs, and catch-path logging behavior. +- No changes made to hash-sensitive delete/message pathways. + +Validation: + +- `npm run lint` -> warnings only (pre-existing `react-hooks/exhaustive-deps`; no new lint errors) + +Follow-ups: + +- Optional next bounded slice: begin selective `requestJson` adoption for remaining config-based calls in `documentDirectService` where hash flow is already centralized. + +--- + +### CL-044: TASK22260 next slice — document direct service low-risk getJson adoption + +date: 2026-03-25 +author: Cline +scope: `actions/services/documentDirectService.js` +type: change +rationale: Continue incremental façade/client rollout by migrating low-risk document service GET wrappers that already return JSON and do not alter hash-signing semantics. +impact: Reduces duplicated axios `.get(...).then(res => res.data)` boilerplate and aligns document retrieval helpers with shared endpoint client usage. +status: completed + +Summary: + +- Added `getJson` usage in selected document service helpers: + - `getRepsFromBlobProxy` + - `getAwaitingSubmissionFromBlobProxy` + - `getFilesFromBlobproxy` + - `getFilesFromBlobHashed` + - `getProgressFromBlob` + - `createContainerProxy` +- Preserved existing behavior contracts: + - same query composition and hash query fragments + - same catch-path logging and return conventions (including JSON string return in `createContainerProxy` error path) +- Left hash-sensitive delete/upload/generation flows unchanged in this slice. + +Validation: + +- `npm run lint` -> warnings only (pre-existing `react-hooks/exhaustive-deps`; no new lint errors) + +Follow-ups: + +- Optional next bounded slice: migrate selected `requestJson`-eligible upload/generation helpers in `documentDirectService` (non-download paths) while preserving multipart/blob behavior. + +--- + +### CL-045: TASK22260 next slice — document direct service requestJson adoption (uploads + PDF generation) + +date: 2026-03-25 +author: Cline +scope: `actions/services/documentDirectService.js` +type: change +rationale: Continue bounded client migration by moving config-based multipart/PDF POST helpers in document service to shared `requestJson` while preserving hashed URL generation and responseType behavior. +impact: Reduces repeated axios config execution boilerplate and aligns document POST helper internals with shared endpoint client conventions. +status: completed + +Summary: + +- Migrated selected config-based document helper flows from `axios(config)` + `res.data` to `requestJson(config)`: + - `uploadFiles` + - `uploadSingleFile` + - `uploadRepFiles` + - `generateRepPDF` + - `generateAppealPDF` +- Preserved behavior-critical aspects: + - hashed URL creation via `buildHashedQueryUrl(...)` + - multipart headers for upload flows + - conditional `responseType: "blob"` for download PDF option paths + - existing catch-path logging semantics +- Left delete/download/hash-sensitive GET helpers unchanged in this slice. + +Validation: + +- `npm run lint` -> warnings only (pre-existing `react-hooks/exhaustive-deps`; no new lint errors) + +Follow-ups: + +- Optional next bounded slice: evaluate remaining legacy direct `axios.get(...).then(res.data)` helpers in `documentDirectService` (`getAwaitingSubmissionFromBlob`, `getRepsFromBlob`, `getFilesFromBlob`) for safe migration while preserving signed hash path behavior. + +--- + +### CL-046: TASK22260 next slice — document direct service remaining signed GET helper migration + +date: 2026-03-25 +author: Cline +scope: `actions/services/documentDirectService.js` +type: change +rationale: Complete the remaining low-risk read helper migration in document service by replacing final direct `axios.get(...).then(res.data)` patterns with shared `getJson` while retaining hash/signature query construction. +impact: Finishes consistency pass for JSON-returning document read helpers and reduces duplicated response extraction logic. +status: completed + +Summary: + +- Migrated the remaining signed document read helpers to `getJson(...)`: + - `getAwaitingSubmissionFromBlob` + - `getRepsFromBlob` + - `getFilesFromBlob` +- Preserved existing behavior: + - same BASE_URL and hash query composition via `hashAPIPath(...)` + - same catch-path logging behavior (`consoleLogger`) + - no changes to delete/download flows or hash-signing helper usage in mutation paths + +Validation: + +- `npm run lint` -> warnings only (pre-existing `react-hooks/exhaustive-deps`; no new lint errors) + +Follow-ups: + +- Optional next bounded slice: targeted hygiene in `documentDirectService` to isolate remaining non-migrated special-case flows (`downloadBlob`, delete helpers) and assess if any shared client abstraction is beneficial without altering behavior. + +--- + +### CL-047: TASK22260 next slice — case direct service low-risk getJson adoption + +date: 2026-03-25 +author: Cline +scope: `actions/services/caseDirectService.js` +type: change +rationale: Continue incremental endpoint client rollout with a bounded low-risk set of case service GET helpers that already return direct JSON payloads and have simple logging-only catch paths. +impact: Reduces repeated axios GET/response extraction boilerplate and aligns additional case retrieval helpers with shared client conventions. +status: completed + +Summary: + +- Added `getJson` usage in selected `caseDirectService` helper functions: + - `getAppealPDFDocs` + - `getPortalModuleDetails` + - `getPortalModuleDetailsProxy` +- Preserved existing behavior contracts: + - same request URL/query construction + - same catch-path logging via `consoleLogger` + - no change to handlers with bespoke error-return contracts (`getAppealPDFDocument`) or other non-targeted flows. + +Validation: + +- `npm run lint` -> warnings only (pre-existing `react-hooks/exhaustive-deps`; no new lint errors) + +Follow-ups: + +- Optional next bounded slice: migrate additional safe case service GET helpers that currently use `axios.get(...).then(res.data)` to `getJson` where custom catch behavior is compatible. + +--- + +### CL-048: TASK22260 next slice — case direct service GET cluster expansion + +date: 2026-03-25 +author: Cline +scope: `actions/services/caseDirectService.js` +type: change +rationale: Continue phased endpoint client adoption by migrating another bounded set of case service GET helpers that already use shared catch handling (`logAndReturnResponse`). +impact: Further reduces duplicated axios GET/response extraction boilerplate while preserving existing error-handling contracts for migrated paths. +status: completed + +Summary: + +- Migrated additional case retrieval helpers from `axios.get(...).then(res.data)` to `getJson(...)`: + - `getCaseMessage` + - `getIncidentbyID` + - `getIsPublishedbyID` + - `getPartSavedAppeal` + - `getSIPSEvents` + - `getSIPSMedia` +- Preserved behavior contracts: + - unchanged URLs/query parameter composition + - unchanged catch behavior via `logAndReturnResponse` + - left non-targeted/bespoke flows untouched (`getAppealID`, create/update/patch operations, and error-response-specialized helpers) + +Validation: + +- `npm run lint` -> warnings only (pre-existing `react-hooks/exhaustive-deps`; no new lint errors) + +Follow-ups: + +- Optional next bounded slice: evaluate remaining GET helpers in `caseDirectService` with custom catches (`getCase`, `getCaseByID`, `getAppealPDFDocument`) for selective migration where return-shape contracts remain stable. + +--- + +### CL-049: TASK22260 next slice — case direct service remaining GET helper migration + +date: 2026-03-25 +author: Cline +scope: `actions/services/caseDirectService.js` +type: change +rationale: Complete the remaining safe GET-helper client migration in case service by moving custom-catch read functions to `getJson` while preserving their existing return-shape/error handling behavior. +impact: Removes remaining direct axios GET response-extraction boilerplate in case read helpers and completes endpointClient read-path consistency for this service subset. +status: completed + +Summary: + +- Migrated remaining targeted case read helpers from direct `axios.get(...).then(res.data)` to `getJson(...)`: + - `getCase` + - `getCaseByID` + - `getAppealPDFDocument` +- Preserved behavior contracts: + - unchanged request URL/query construction + - unchanged catch semantics: + - `getCase` / `getCaseByID` still log via `consoleLogger` + - `getAppealPDFDocument` still logs and returns `error.response` on failure + - left non-targeted POST/update/create/patch flows unchanged. + +Validation: + +- `npm run lint` -> warnings only (pre-existing `react-hooks/exhaustive-deps`; no new lint errors) + +Follow-ups: + +- Optional next bounded slice: evaluate config-based POST helpers in `caseDirectService` (`createNewCase`, `createNewCaseBlob`, `updateCase`, `updateCaseBlob`) for selective `requestJson(...)` adoption while preserving existing side effects and error contracts. + +--- + +### CL-050: TASK22260 next slice — case direct service POST helper requestJson adoption + +date: 2026-03-25 +author: Cline +scope: `actions/services/caseDirectService.js` +type: change +rationale: Continue bounded client-layer migration by moving config-based case service POST helpers from direct `axios(config)` usage to shared `requestJson(...)` while preserving current behavior and error semantics. +impact: Reduces duplicated config-execution/response-extraction boilerplate and aligns case service write-helper internals with existing endpoint client conventions. +status: completed + +Summary: + +- Migrated selected case service POST helpers to `requestJson(config)`: + - `createNewCase` + - `createNewCaseBlob` + - `updateCase` + - `updateCaseBlob` +- Preserved behavior contracts: + - unchanged payload/query construction and URLs + - unchanged catch-path logging via `consoleLogger` + - no changes to non-targeted helper logic (`getAppealID`, `patchCase`, and already-migrated GET helpers) + +Validation: + +- `npm run lint` -> warnings only (pre-existing `react-hooks/exhaustive-deps`; no new lint errors) + +Follow-ups: + +- Optional next bounded slice: assess `patchCase` and `getAppealID` for migration opportunities (if/when preserving their specific behavior contracts remains straightforward). + +--- + +### CL-051: TASK22260 next slice — case direct service final axios dependency removal + +date: 2026-03-25 +author: Cline +scope: `actions/services/caseDirectService.js` +type: change +rationale: Complete the bounded case service migration by replacing the remaining direct axios usage (`getAppealID`, `patchCase`) with shared endpoint client reads, enabling removal of the direct axios import. +impact: Finalizes endpoint client consistency in case service and removes remaining direct axios dependency from this module without changing helper contracts. +status: completed + +Summary: + +- Migrated remaining direct axios-based read helpers to `getJson(...)`: + - `getAppealID` + - `patchCase` +- Preserved behavior contracts: + - `getAppealID` still extracts first non-underscore key value from returned entity and logs on failure + - `patchCase` still swallows failure (no explicit returned error contract introduced) +- Removed now-unused `axios` import from `caseDirectService`. + +Validation: + +- `npm run lint` -> warnings only (pre-existing `react-hooks/exhaustive-deps`; no new lint errors) + +Follow-ups: + +- Optional next bounded slice: review other direct service modules for any remaining legacy `axios` import usage now that case service migration is complete. + +--- + +### CL-052: TASK22260 next slice — portal direct service requestJson parity for config-based reads/deletes + +date: 2026-03-25 +author: Cline +scope: `actions/services/portalDirectService.js` +type: change +rationale: Continue bounded direct-service consistency by migrating remaining config-based portal helper calls from `axios(config)` to shared `requestJson(...)` where no signed-delete/header-specific behavior is required. +impact: Reduces repeated config execution/response extraction boilerplate and improves request helper consistency in portal service while preserving behavior. +status: completed + +Summary: + +- Migrated selected config-based helpers to `requestJson(config)`: + - `deleteAwaitingSubmissions` + - `sendCaseCompleteMessage` + - `sendCaseCompleteMessageProxy` + - `sendRepCompleteMessage` +- Preserved behavior contracts: + - unchanged query/hash composition and request methods + - unchanged catch-path logging via `consoleLogger` + - left signed delete helpers with bespoke axios/header behavior unchanged (`deleteMyRepresentations`, `deleteWatchedCases`) + +Validation: + +- `npm run lint` -> warnings only (pre-existing `react-hooks/exhaustive-deps`; no new lint errors) + +Follow-ups: + +- Optional next bounded slice: assess whether signed-delete helpers in `portalDirectService` should remain explicit axios calls (for clarity on headers/hash semantics) or move to a dedicated signed-request client helper. + +--- + +### CL-053: Architecture documentation update — current-state assessment + prioritised roadmap + +date: 2026-03-25 +author: Cline +scope: `context/architecture.md` +type: change +rationale: User requested the architectural assessment be documented in-project so current state, residual risks, and recommended sequencing are explicitly captured for team planning. +impact: Improves documentation clarity and planning alignment; no runtime behavior changes. +status: completed + +Summary: + +- Added a new section to `context/architecture.md`: + - **Current State Assessment and Prioritised Next Steps (2026-03-25)** +- Documented: + - strengths (governance maturity, API/relay hardening progress, façade decomposition progress) + - residual risks/gaps (remaining direct-service inconsistency, coverage concentration, logging variance, i18n parity automation gap) + - prioritised next steps with rationale + - recommended execution sequencing (immediate/next/after) + +Validation: + +- Manual coherence check against existing guardrails/runbook/integration-map and memory-bank context. + +Follow-ups: + +- Optional: convert the prioritised steps into a sprint-ready plan (tasks, acceptance criteria, validation matrix, rollback notes). + +--- + +### CL-054: Branch correction — include residual duplication/axios risk slices (search + signed-delete portal flows) + +date: 2026-03-25 +author: Cline +scope: `actions/services/{portalDirectService,searchDirectService}.js`, `context/architecture.md` +type: change +rationale: User requested the previously identified primary remaining technical risk slices be explicitly included on this branch, specifically residual axios/duplication in search direct service and signed-delete portal helper flows. +impact: Further reduces direct-service axios boilerplate/duplication and aligns additional high-use service paths to shared endpoint client patterns while preserving existing behavior contracts. +status: completed + +Summary: + +- Implemented the requested risk slices on branch: + 1. **Portal signed-delete flow parity** (`portalDirectService`) + - migrated signed delete helpers from direct `axios({...}).then(res.data)` to `requestJson({...})`: + - `deleteMyRepresentations` + - `deleteWatchedCases` + - removed now-unused `axios` import from module + - preserved hash-signing flow and custom headers semantics + 2. **Search direct service axios reduction** (`searchDirectService`) + - migrated service GET helpers from direct `axios.get(...).then(res.data)` and `axios(config)` to `getJson(...)` + - preserved existing catch semantics (`logAndReturnResponse`, `logAndReturnEmptyValueErrorResponse`, and explicit throw path in `getSearchDocumentDetails`) + - removed direct `axios` import from module +- Documentation alignment: + - retained architecture assessment section in `context/architecture.md` that calls out these residual-risk slices and prioritisation. + +Validation: + +- `npm run lint` -> warnings only (pre-existing `react-hooks/exhaustive-deps`; no new lint errors) + +Follow-ups: + +- Optional next bounded slice: assess whether a dedicated signed-request client helper should encapsulate hash + headers + method conventions to prevent future drift in remaining signed flows. + +--- + +### CL-055: TASK22260 next slice — document direct service signed/delete axios reduction + +date: 2026-03-25 +author: Cline +scope: `actions/services/documentDirectService.js` +type: change +rationale: Execute next bounded risk-reduction slice by migrating remaining non-download document-service delete and signed-get helper calls away from direct axios response extraction to shared endpoint clients. +impact: Further reduces duplicated axios boilerplate and aligns document service internals with shared request-client conventions while preserving existing hash/query and catch-path behavior. +status: completed + +Summary: + +- Migrated signed hashed delete-helper flows from direct `axios({...}).then(res.data)` to `requestJson({...})`: + - `deleteAwaitingSubmissionsFromBlob` + - `deleteMyRepresentationsFromBlob` +- Migrated delete helper GET calls from `axios.get(...).then(res.data)` to `getJson(...)`: + - `deleteBlob` + - `deleteRepBlob` +- Preserved behavior contracts: + - unchanged query/hash composition and endpoint URLs + - unchanged catch-path logging with `consoleLogger` +- Left `downloadBlob` unchanged in this slice (special-case behavior path retained for separate focused handling). + +Validation: + +- `npm run lint` -> warnings only (pre-existing `react-hooks/exhaustive-deps`; no new lint errors) + +Follow-ups: + +- Optional next bounded slice: isolate and correct `downloadBlob` behavior in `documentDirectService` (including legacy `res` usage) behind an explicit, tested contract. + +--- + +### CL-056: TASK22260 next slice — document direct service download helper contract fix + +date: 2026-03-25 +author: Cline +scope: `actions/services/documentDirectService.js` +type: change +rationale: Execute the next bounded follow-up by correcting the legacy `downloadBlob` service helper path that still relied on invalid `res` references and direct axios usage, aligning it to shared request client behavior. +impact: Fixes a service-layer contract defect risk in document download helper and improves consistency by using shared request client patterns; no endpoint contract change. +status: completed + +Summary: + +- Refactored `downloadBlob(containerName, blobName)` in `documentDirectService`: + - removed legacy direct `axios.get(...).then(response => res.status(...))` pattern that referenced undefined `res` in service layer + - now returns blob response data via `requestJson({ method: "get", url, responseType: "blob" })` + - preserved catch-path logging (`consoleLogger`) +- Removed now-unused module-level `axios` import from `documentDirectService`. + +Validation: + +- `npm run lint` -> warnings only (pre-existing `react-hooks/exhaustive-deps`; no new lint errors) + +Follow-ups: + +- Optional next bounded slice: add a focused test (or integration harness assertion) around `downloadBlob` service return contract to prevent regression to response-object assumptions. + +--- + +### CL-057: TASK22260 next slice — phase7 behavioural harness compatibility update + +date: 2026-03-25 +author: Cline +scope: `tests/phase7/service-behaviour.test.cjs` +type: change +rationale: After service-layer client migration (`getJson`/`requestJson`/`buildHashedQueryUrl`), phase7 behavioural harness still assumed direct axios imports only; update harness defaults so legacy behavior assertions remain executable. +impact: Restores service behavioural regression coverage (12/12) without changing production runtime code. +status: completed + +Summary: + +- Enhanced phase7 VM loader default injections for migrated service helpers: + - added default `getJson(...)` mock delegating to `axios.get(...).then(res.data)` + - added default `requestJson(...)` mock delegating to `axios(config).then(res.data)` + - added default `buildHashedQueryUrl(...)` mock resolving hash via `/api/endpoint/gethash_api` compatibility path +- Updated notify behavior assertions to align with shared request client usage (`requestJson` invokes axios config-style call): + - switched notify test handlers from `axios.postHandler` to `axios.requestHandler` + - assertions now inspect `axios.calls[0].config.{url,method,data}` + +Validation: + +- `node tests/phase7/service-behaviour.test.cjs` -> pass (12/12) + +Follow-ups: + +- Optional next bounded slice: add a small shared test utility for service harness client mocks to reduce future per-file drift as façade migration continues. + +--- + +### CL-058: TASK22260 next slice — phase6 behavioural harness compatibility parity + +date: 2026-03-25 +author: Cline +scope: `tests/phase6/service-behaviour.test.cjs` +type: change +rationale: Keep older phase6 behavioural harness aligned with service client-wrapper migration by adding default helper injections required by `getJson`/`requestJson`-based direct services. +impact: Restores phase6 behavioural regression execution parity (8/8) with no runtime code changes. +status: completed + +Summary: + +- Updated `loadServiceModule` default context in `tests/phase6/service-behaviour.test.cjs`: + - added default `getJson(...)` mock backed by `axios.get(...).then(res.data)` + - added default `requestJson(...)` mock backed by `axios(config).then(res.data)` +- Preserved existing test assertions and behavior semantics; this is harness-compatibility only. + +Validation: + +- `node tests/phase6/service-behaviour.test.cjs` -> pass (8/8) + +Follow-ups: + +- Optional consolidation: extract shared phase6/phase7 VM loader helpers into a single test utility to reduce duplication. + +--- + +### CL-059: TASK22260 next slice — shared service harness extraction (continued bounded risk-reduction) + +date: 2026-03-25 +author: Cline +scope: `tests/{serviceHarness,phase6/service-behaviour,phase7/service-behaviour}.cjs` +type: change +rationale: Continue the bounded risk-reduction stream by removing duplicated test harness infrastructure across phase6/phase7 service behavioural suites and centralizing client-wrapper-compatible mocks. +impact: Reduces test harness drift risk and keeps client-wrapper migration verification stable across multiple suites, without runtime code changes. +status: completed + +Summary: + +- Added shared helper module `tests/serviceHarness.cjs` with reusable: + - `createAxiosMock` + - `createLoggerMock` + - `createAxiosError` + - `loadServiceModule` (with default `getJson`/`requestJson`/`buildHashedQueryUrl` injections) + - `normalize` +- Refactored `tests/phase6/service-behaviour.test.cjs` to import shared harness utilities and remove duplicated local harness implementation. +- Refactored `tests/phase7/service-behaviour.test.cjs` to import shared harness utilities and remove duplicated local harness implementation. + +Validation: + +- `node tests/phase6/service-behaviour.test.cjs` -> pass (8/8) +- `node tests/phase7/service-behaviour.test.cjs` -> pass (12/12) +- `npm run lint` -> warnings only (pre-existing `react-hooks/exhaustive-deps`; no new lint errors) + +Follow-ups: + +- Optional next bounded risk-reduction slice: evaluate whether other legacy service test suites can adopt `tests/serviceHarness.cjs` to standardize migration-era service mocking behavior. + +--- + +### CL-060: TASK22260 next slice — core token helper client-wrapper migration (continued bounded risk-reduction) + +date: 2026-03-25 +author: Cline +scope: `actions/core/token.js` +type: change +rationale: Include the identified remaining candidate outside `actions/services` and continue the bounded risk-reduction stream by removing direct axios response extraction from core token retrieval. +impact: Aligns token helper request execution with shared endpoint client conventions while preserving existing token caching and error-return behavior. +status: completed + +Summary: + +- Refactored `getToken` in `actions/core/token.js`: + - replaced direct `axios.post(...).then(res => res.data)` chain with shared `requestJson({...})` + - migrated function to `async/await` with equivalent `try/catch` behavior + - retained existing semantics: + - successful token payload cached in `cache.tokenResponse` + - failures logged via `consoleLogger` and returned to caller +- Removed direct `axios` dependency from `actions/core/token.js` in favor of `actions/clients/endpointClient`. + +Validation: + +- `node tests/phase21/api-contract-slice1.test.cjs` -> pass + - helper: 4/4 + - file-handler: 53/53 + - email-handler: 12/12 + - endpoint-handler: 164/164 + - documents-handler: 3/3 +- `npm run lint` -> warnings only (pre-existing `react-hooks/exhaustive-deps`; no new lint errors) + +Follow-ups: + +- Optional next bounded risk-reduction slice: assess whether any remaining non-service utility modules still use promise-chain axios extraction patterns and migrate them to shared clients where behavior contracts remain unchanged. + +--- + +### CL-061: TASK22260 next slice — core token regression coverage addition (bounded hardening) + +date: 2026-03-25 +author: Cline +scope: `tests/phase22/core-token-behaviour.test.cjs` +type: change +rationale: Follow the previous core token migration with a bounded verification slice to lock the request-client contract and prevent regression to direct axios extraction patterns. +impact: Improves confidence in `actions/core/token.js` behavior (success + failure semantics) without runtime code changes. +status: completed + +Summary: + +- Added new focused Phase 22 behavioural test suite: + - `tests/phase22/core-token-behaviour.test.cjs` +- Coverage asserts: + - `getToken` success path returns token payload and calls `requestJson` with expected URL/method/body/headers + - failure path logs via `consoleLogger` and returns the original error object +- Test harness uses VM import stripping consistent with existing phase behavioural suites. + +Validation: + +- `node tests/phase22/core-token-behaviour.test.cjs` -> pass (2/2) +- `npm run lint` -> warnings only (pre-existing `react-hooks/exhaustive-deps`; no new lint errors) + +Follow-ups: + +- Optional next bounded slice: add this phase22 suite to any aggregate test runner used in CI if/when phase-level suites are centrally orchestrated. + +--- + +### CL-062: TASK22260 next slice — phase22 runner export guard parity (bounded test-harness consistency) + +date: 2026-03-25 +author: Cline +scope: `tests/phase22/core-token-behaviour.test.cjs` +type: change +rationale: Continue bounded test-hardening by aligning phase22 test entry behavior with established suite conventions so it can be executed both directly and from aggregate runners. +impact: Improves test harness composability and reduces accidental double-execution risk when importing phase22 test suites. +status: completed + +Summary: + +- Updated `tests/phase22/core-token-behaviour.test.cjs` to export `run` and add `require.main === module` guard. +- Preserved direct CLI execution behavior while enabling safe module import by aggregate runners. + +Validation: + +- `node tests/phase22/core-token-behaviour.test.cjs` -> pass (2/2) +- `npm run lint` -> warnings only (pre-existing `react-hooks/exhaustive-deps`; no new lint errors) + +Follow-ups: + +- Optional next bounded slice: add a dedicated `tests/phase22/index.test.cjs` aggregate entrypoint if additional phase22 suites are introduced. + +--- + +### CL-063: TASK22260 next slice — phase22 aggregate runner entrypoint (bounded test-runner hardening) + +date: 2026-03-25 +author: Cline +scope: `tests/phase22/index.test.cjs` +type: change +rationale: Continue the bounded test-harness stream by introducing a phase-level aggregate runner for phase22, matching established conventions used in other phase suites. +impact: Improves consistency and future scalability of phase22 tests by enabling a single entry command as additional phase22 suites are added. +status: completed + +Summary: + +- Added `tests/phase22/index.test.cjs` aggregate runner. +- Runner currently executes `core-token-behaviour.test.cjs` and prints a phase-level completion line. +- Exported `run` and kept direct CLI execution guard parity (`require.main === module`). + +Validation: + +- `node tests/phase22/index.test.cjs` -> pass + - core-token: 2/2 + - phase22 combined: pass +- `npm run lint` -> warnings only (pre-existing `react-hooks/exhaustive-deps`; no new lint errors) + +Follow-ups: + +- Optional next bounded slice: add additional phase22 suites (e.g., shared-client utility behaviour tests) under this aggregate runner as migration coverage expands. + +--- + +### CL-064: TASK22260 next slice — phase22 client utility behaviour coverage expansion + +date: 2026-03-25 +author: Cline +scope: `tests/phase22/{client-utils-behaviour,index}.test.cjs` +type: change +rationale: Execute the next bounded phase22 slice by adding focused behavioural coverage for shared client utilities to reduce regression risk as direct-service/client-wrapper migration continues. +impact: Improves confidence in shared client helper contracts (`endpointClient`, `relayClient`) and keeps phase22 aggregate suite aligned with new coverage. +status: completed + +Summary: + +- Added new suite: `tests/phase22/client-utils-behaviour.test.cjs`. +- Added assertions for shared client utility behavior: + - `endpointClient.getJson` returns `axios.get(...).data` + - `endpointClient.requestJson` returns `axios(config).data` + - `relayClient.buildHashedQueryUrl` appends browser hash-service response + - server fallback path uses `hashAPIPath` when `HASHKEY` is present + - browser/no-HASHKEY failure path rethrows hash-service error +- Updated `tests/phase22/index.test.cjs` to include the new client-utils suite in the phase aggregate runner. + +Validation: + +- `node tests/phase22/client-utils-behaviour.test.cjs` -> pass (5/5) +- `node tests/phase22/index.test.cjs` -> pass + - core-token: 2/2 + - client-utils: 5/5 + - phase22 combined: pass +- `npm run lint` -> warnings only (pre-existing `react-hooks/exhaustive-deps`; no new lint errors) + +Follow-ups: + +- Optional next bounded slice: add focused phase22 behavioural coverage for any future shared client wrappers introduced beyond `endpointClient`/`relayClient`. + +--- + +### CL-065: TASK22260 next slice — fileClient extraction + phase22 behavioural coverage + +date: 2026-03-25 +author: Cline +scope: `actions/clients/{fileClient,index}.js`, `actions/services/documentDirectService.js`, `tests/phase22/{file-client-behaviour,index}.test.cjs` +type: change +rationale: Continue bounded shared-client decomposition by extracting repeated file-route request patterns into `fileClient` and hardening behaviour with dedicated phase22 tests. +impact: Reduces request-boilerplate duplication in document service and increases regression confidence for extracted file client helper contracts. +status: completed + +Summary: + +- Added new shared client wrapper: `actions/clients/fileClient.js`: + - `getFileJson(url)` + - `getSignedFileJson(queryUrl)` + - `downloadFileBlob(url)` +- Exported `fileClient` from `actions/clients/index.js` (and therefore via `actions/index.js` barrel path). +- Migrated a bounded subset of `actions/services/documentDirectService.js` call sites to `fileClient` while preserving catch-path logging behavior: + - `getFilesFromBlobproxy` -> `getFileJson` + - `deleteAwaitingSubmissionsFromBlob` -> `getSignedFileJson` + - `deleteMyRepresentationsFromBlob` -> `getSignedFileJson` + - `deleteBlob` -> `getFileJson` + - `deleteRepBlob` -> `getFileJson` + - `downloadBlob` -> `downloadFileBlob` +- Added `tests/phase22/file-client-behaviour.test.cjs` covering: + - delegation to `getJson` + - signed URL generation + `requestJson` invocation + - blob download config contract (`responseType: "blob"`) +- Updated aggregate runner `tests/phase22/index.test.cjs` to include file-client suite. + +Validation: + +- `node tests/phase22/file-client-behaviour.test.cjs` -> pass (3/3) +- `node tests/phase22/index.test.cjs` -> pass + - core-token: 2/2 + - client-utils: 5/5 + - file-client: 3/3 + - phase22 combined: pass +- `npm run lint` -> warnings only (pre-existing `react-hooks/exhaustive-deps`; no new lint errors) + +Follow-ups: + +- Optional next bounded slice: evaluate whether additional `documentDirectService` file-route call sites can adopt `fileClient` without altering current behavior contracts. + +--- + +### CL-066: TASK22260 next slice — service harness parity for fileClient helper injections + +date: 2026-03-25 +author: Cline +scope: `tests/serviceHarness.cjs` +type: change +rationale: Follow the previous `fileClient` extraction with a bounded harness-compatibility slice so legacy VM-based service behavioural suites continue to execute without requiring per-test manual injections. +impact: Restores migration-era behavioural regression stability (phase7) by aligning shared harness defaults with newly introduced file client helper symbols. +status: completed + +Summary: + +- Updated shared test harness defaults in `tests/serviceHarness.cjs` to inject file-client compatible helpers when not explicitly provided: + - `getFileJson` (delegates to default `getJson`) + - `getSignedFileJson` (signs via `buildHashedQueryUrl` then calls `requestJson` with GET config) + - `downloadFileBlob` (calls `requestJson` with GET + `responseType: "blob"`) +- This preserves existing VM import-stripping strategy while preventing `ReferenceError` in migrated services that import from `../clients/fileClient`. + +Validation: + +- `node tests/phase7/service-behaviour.test.cjs` -> pass (12/12) +- `node tests/phase22/index.test.cjs` -> pass + - core-token: 2/2 + - client-utils: 5/5 + - file-client: 3/3 + - phase22 combined: pass +- `npm run lint` -> warnings only (pre-existing `react-hooks/exhaustive-deps`; no new lint errors) + +Follow-ups: + +- Optional next bounded slice: add a focused phase6/phase7 behavioural assertion for `documentDirectService.downloadBlob` to explicitly lock the `downloadFileBlob` delegation contract. + +--- + +### CL-067: TASK22260 next slice — phase7 downloadBlob delegation behavioural lock + +date: 2026-03-25 +author: Cline +scope: `tests/phase7/service-behaviour.test.cjs` +type: change +rationale: Execute the queued follow-up by adding explicit phase7 behavioural coverage for `documentDirectService.downloadBlob` so file-client delegation and blob request config remain contract-stable. +impact: Increases regression confidence for document download helper behaviour after `fileClient` extraction, without runtime code changes. +status: completed + +Summary: + +- Added new phase7 behavioural test: + - `document/downloadBlob delegates blob request config via file client helper` +- The test asserts: + - `downloadBlob` lower-cases container in URL composition + - request is issued through config-style request path (`axios.request` in harness) + - request method is `get` + - `responseType` is `blob` + - returned payload contract is preserved from request helper data. + +Validation: + +- `node tests/phase7/service-behaviour.test.cjs` -> pass (13/13) +- `node tests/phase22/index.test.cjs` -> pass + - core-token: 2/2 + - client-utils: 5/5 + - file-client: 3/3 + - phase22 combined: pass +- `npm run lint` -> warnings only (pre-existing `react-hooks/exhaustive-deps`; no new lint errors) + +Follow-ups: + +- Optional next bounded slice: evaluate adding equivalent focused behavioural coverage in phase6 if download helper behavior becomes part of that suite’s scope. + +--- + +### CL-068: TASK22260 widened module-completion slice — documentDirectService signed POST flows via fileClient + +date: 2026-03-25 +author: Cline +scope: `actions/clients/fileClient.js`, `actions/services/documentDirectService.js`, `tests/phase22/file-client-behaviour.test.cjs` +type: change +rationale: Per user request to widen slices, complete a larger coherent module-level increment by moving the remaining signed POST file-route flows in document service onto `fileClient`. +impact: Further reduces request/signing boilerplate in document service and centralizes signed file-route behavior in client wrapper layer with added regression coverage. +status: completed + +Summary: + +- Extended `actions/clients/fileClient.js` with: + - `postSignedFileJson(queryUrl, data, config = {})` + - signs query URL via `buildHashedQueryUrl` and executes POST via `requestJson` +- Migrated all remaining signed POST helper paths in `actions/services/documentDirectService.js` to `postSignedFileJson`: + - `uploadFiles` + - `uploadSingleFile` + - `uploadRepFiles` + - `generateRepPDF` + - `generateAppealPDF` +- Preserved existing catch-path logging and request-option semantics: + - multipart headers for upload flows + - conditional `responseType: "blob"` for download variants +- Expanded phase22 file-client behavior suite with explicit POST-signed contract test: + - URL signing + POST method + - data passthrough + - config/header passthrough + +Validation: + +- `node tests/phase22/file-client-behaviour.test.cjs` -> pass (4/4) +- `node tests/phase22/index.test.cjs` -> pass + - core-token: 2/2 + - client-utils: 5/5 + - file-client: 4/4 + - phase22 combined: pass +- `node tests/phase7/service-behaviour.test.cjs` -> pass (13/13) +- `npm run lint` -> warnings only (pre-existing `react-hooks/exhaustive-deps`; no new lint errors) + +Follow-ups: + +- Optional next widened slice: evaluate consolidating remaining direct `getJson` file-read flows in `documentDirectService` behind `fileClient` for full per-module client symmetry. + +--- + +### CL-069: TASK22260 widened module-completion slice — documentDirectService file-read client symmetry + +date: 2026-03-25 +author: Cline +scope: `actions/services/documentDirectService.js` +type: change +rationale: Continue widened slice cadence by completing per-module client symmetry in `documentDirectService`, moving all file-read helper calls to `fileClient` instead of mixed endpoint client usage. +impact: Simplifies module dependency shape and centralizes file-route read behavior through a single client abstraction without changing runtime contracts. +status: completed + +Summary: + +- Removed mixed `endpointClient` usage from `documentDirectService` for file reads. +- Migrated remaining file-read/helper routes from `getJson` to `getFileJson`: + - `getAwaitingSubmissionFromBlob` + - `getRepsFromBlob` + - `getRepsFromBlobProxy` + - `getAwaitingSubmissionFromBlobProxy` + - `getFilesFromBlob` + - `getFilesFromBlobHashed` + - `getProgressFromBlob` + - `createContainerProxy` +- Removed now-unused imports from `documentDirectService`: + - `buildHashedQueryUrl` + - `getJson` + - `requestJson` + +Validation: + +- `node tests/phase22/index.test.cjs` -> pass + - core-token: 2/2 + - client-utils: 5/5 + - file-client: 4/4 + - phase22 combined: pass +- `node tests/phase7/service-behaviour.test.cjs` -> pass (13/13) +- `npm run lint` -> warnings only (pre-existing `react-hooks/exhaustive-deps`; no new lint errors) + +Follow-ups: + +- Optional next widened slice: introduce a small `fileClient` URL-builder helper set for repeated query-string composition in `documentDirectService` (container/casefolder/blob parameters) to reduce string-concat drift risk. + +--- + +### CL-070: TASK22260 widened cross-file slice — file route builder extraction + document service query normalization + +date: 2026-03-25 +author: Cline +scope: `actions/clients/{fileRouteBuilder,index}.js`, `actions/services/documentDirectService.js`, `tests/{serviceHarness,phase22/client-utils-behaviour}.cjs` +type: change +rationale: Deliver a wider-than-previous slice by extracting reusable file-route query composition helpers and applying them across document service paths, reducing repeated string concatenation and encoding drift risk. +impact: Improves maintainability and consistency of file-route URL construction while preserving existing runtime contracts and hash behavior. +status: completed + +Summary: + +- Added new shared helper module: + - `actions/clients/fileRouteBuilder.js` + - exports: + - `buildFileQuery(path, params, options)` (supports optional encoded query composition) + - `withBaseUrl(baseUrl, route)` + - `appendQuerySuffix(route, suffix)` +- Exported route-builder helpers via `actions/clients/index.js`. +- Refactored `actions/services/documentDirectService.js` to use route-builder helpers across read/delete/download/query flows: + - normalized composition for file routes and hash suffix append behavior + - preserved encoded-path behavior for sensitive params (`casefolderID`, `blobname`) where previously encoded + - preserved base URL prefix behavior and existing logger/catch semantics +- Updated test harness defaults in `tests/serviceHarness.cjs` for new helper symbols: + - `buildFileQuery` + - `withBaseUrl` + - `appendQuerySuffix` +- Expanded phase22 utility coverage in `tests/phase22/client-utils-behaviour.test.cjs` with file route-builder behavior assertions. + +Validation: + +- `node tests/phase22/index.test.cjs` -> pass + - core-token: 2/2 + - client-utils: 6/6 + - file-client: 4/4 + - phase22 combined: pass +- `node tests/phase7/service-behaviour.test.cjs` -> pass (13/13) +- `npm run lint` -> warnings only (pre-existing `react-hooks/exhaustive-deps`; no new lint errors) + +Follow-ups: + +- Optional next widened slice: evaluate applying `fileRouteBuilder` to portal/case service file-route call sites for cross-module query-builder consistency. + +--- + +### CL-071: TASK22260 next widened cross-module slice — portal service query normalization via fileRouteBuilder + +date: 2026-03-25 +author: Cline +scope: `actions/services/portalDirectService.js` +type: change +rationale: Deliver the requested next wider slice by extending `fileRouteBuilder` adoption beyond document service into portal service, reducing duplicated query string concatenation and improving consistency in signed/unsigned route construction. +impact: Improves maintainability and query-construction consistency across high-use portal service flows while preserving existing runtime behavior and hash-signing contracts. +status: completed + +Summary: + +- Refactored `actions/services/portalDirectService.js` to use shared route helpers: + - `buildFileQuery` + - `withBaseUrl` + - `appendQuerySuffix` +- Normalized query composition across portal service GET/DELETE/message flows: + - read/list endpoints (`getMyCases`, `getMyInvolvements`, `getMyLPACases`, `getMyRepresentations`, proxy and watched/awaiting variants) + - delete endpoints (`deleteMyRepresentations`, `deleteAwaitingSubmissions`, `deleteWatchedCases`) + - file-message endpoints (`sendCaseCompleteMessage`, `sendCaseCompleteMessageProxy`, `sendRepCompleteMessage`) +- Preserved behavior contracts: + - retained BASE_URL usage patterns for existing BASE_URL-prefixed routes + - retained hash-signing flow via `buildHashedQueryUrl` + - retained append semantics for signed suffixes in `sendCaseCompleteMessage` + - retained request methods, headers, payloads, and catch-path logging + +Validation: + +- `node tests/phase7/service-behaviour.test.cjs` -> pass (13/13) +- `node tests/phase22/index.test.cjs` -> pass + - core-token: 2/2 + - client-utils: 6/6 + - file-client: 4/4 + - phase22 combined: pass +- `npm run lint` -> warnings only (pre-existing `react-hooks/exhaustive-deps`; no new lint errors) + +Follow-ups: + +- Optional next widened slice: apply the same query-normalization helpers in `caseDirectService` and add dedicated phase22 behavioral assertions for `portalDirectService` route-building/signing composition. + +--- + +### CL-072: TASK22260 next widened cross-module slice — case service query normalization via fileRouteBuilder + +date: 2026-03-25 +author: Cline +scope: `actions/services/caseDirectService.js`, `tests/phase6/service-behaviour.test.cjs` +type: change +rationale: Continue widened cross-module rollout by applying shared query/route composition helpers to `caseDirectService`, reducing repeated string concatenation and aligning route construction style with document/portal services. +impact: Improves maintainability and consistency in case service URL/query composition while preserving existing runtime behavior and error contracts. +status: completed + +Summary: + +- Refactored `actions/services/caseDirectService.js` to use `fileRouteBuilder` helpers: + - `buildFileQuery` + - `withBaseUrl` +- Normalized route composition for read and write helpers, including: + - case retrieval/search flows (`getCaseMessage`, `getIncidentbyID`, `getIsPublishedbyID`, `getPartSavedAppeal`, `getSIPSEvents`, `getSIPSMedia`) + - appeal resolution/update/create flows (`getAppealID`, `createNewCase`, `createNewCaseBlob`, `updateCase`, `updateCaseBlob`, `patchCase`) + - case/detail/document/module reads (`getCase`, `getCaseByID`, `getAppealPDFDocs`, `getAppealPDFDocument`, `getPortalModuleDetails`, `getPortalModuleDetailsProxy`) +- Preserved existing contracts: + - BASE_URL usage patterns where previously applied + - method/payload semantics for `requestJson` paths + - catch-path logging and return behavior (`logAndReturnResponse`, `consoleLogger`, `error.response` paths) +- Expanded phase6 behavioural coverage with a focused assertion for case route composition: + - `case/getPortalModuleDetails composes BASE_URL route with encoded case reference` + +Validation: + +- `node tests/phase6/service-behaviour.test.cjs` -> pass (9/9) +- `node tests/phase7/service-behaviour.test.cjs` -> pass (13/13) +- `node tests/phase22/index.test.cjs` -> pass + - core-token: 2/2 + - client-utils: 6/6 + - file-client: 4/4 + - phase22 combined: pass +- `npm run lint` -> warnings only (pre-existing `react-hooks/exhaustive-deps`; no new lint errors) + +Follow-ups: + +- Optional next widened slice: add a focused phase22 behavioral suite for `caseDirectService` and normalize any remaining specialized encoding usage behind explicit helper options where appropriate. + +--- + +### CL-073: TASK22260 essential condensed slice — case phase22 behavioural coverage + encoding contract lock + +date: 2026-03-25 +author: Cline +scope: `tests/phase22/{case-service-behaviour,index}.test.cjs` +type: change +rationale: Condense remaining core work into one essential slice by adding explicit phase22 behavioural coverage for `caseDirectService` route composition and encoding-sensitive contracts. +impact: Improves regression confidence for case service route-building behavior (BASE_URL composition, case-reference encoding/escaping, error-return contracts) without runtime behavior changes. +status: completed + +Summary: + +- Added new suite `tests/phase22/case-service-behaviour.test.cjs` with focused behavioural assertions for: + - `getPortalModuleDetails` BASE_URL + encoded case reference composition + - `getPortalModuleDetailsProxy` apostrophe escape behavior in case reference + - `getAppealID` query composition and non-underscore value extraction + - `getAppealPDFDocument` failure-path logging + `error.response` passthrough +- Updated `tests/phase22/index.test.cjs` aggregate runner to include `case-service-behaviour` suite. + +Validation: + +- `node tests/phase22/index.test.cjs` -> pass + - core-token: 2/2 + - client-utils: 6/6 + - file-client: 4/4 + - case-service: 4/4 + - phase22 combined: pass +- `node tests/phase6/service-behaviour.test.cjs` -> pass (9/9) +- `node tests/phase7/service-behaviour.test.cjs` -> pass (13/13) +- `npm run lint` -> warnings only (pre-existing `react-hooks/exhaustive-deps`; no new lint errors) + +Follow-ups: + +- Optional closure slice: add a dedicated phase22 portal-service behavioural suite and finish harmonization of remaining direct query string composition in `portalDirectService` write helpers. + +--- + +### CL-074: TASK22260 optional closure condensed slice — portal phase22 behavioural coverage + final write-helper query harmonization + +date: 2026-03-25 +author: Cline +scope: `actions/services/portalDirectService.js`, `tests/phase22/{portal-service-behaviour,index}.test.cjs` +type: change +rationale: Complete optional closure work as one condensed slice by adding explicit portal behavioral coverage and removing remaining direct literal query strings in portal write helpers. +impact: Improves regression confidence for signed portal flows and closes remaining route-composition harmonization gap in portal service write helpers without changing runtime behavior contracts. +status: completed + +Summary: + +- Harmonized remaining portal write-helper route literals to `buildFileQuery(...)`: + - `createWatchedCases` + - `setRepInvolvment` + - `setCaseInvolvment` +- Added new phase22 suite `tests/phase22/portal-service-behaviour.test.cjs` covering: + - signed hash suffix append path in `sendCaseCompleteMessage` + - signed delete request contract + headers in `deleteMyRepresentations` + - harmonized route helper URL in `createWatchedCases` + - signed pre-request rejection contract in `sendRepCompleteMessage` when hash-signing fails +- Updated `tests/phase22/index.test.cjs` aggregate runner to include `portal-service-behaviour`. + +Validation: + +- `node tests/phase22/portal-service-behaviour.test.cjs` -> pass (4/4) +- `node tests/phase22/index.test.cjs` -> pass + - core-token: 2/2 + - client-utils: 6/6 + - file-client: 4/4 + - case-service: 4/4 + - portal-service: 4/4 + - phase22 combined: pass +- `node tests/phase7/service-behaviour.test.cjs` -> pass (13/13) +- `npm run lint` -> warnings only (pre-existing `react-hooks/exhaustive-deps`; no new lint errors) + +Follow-ups: + +- Closure for this condensed stream complete; any further work should be a separate expansion stream (e.g., additional service-level phase22 coverage breadth). + +--- + +### CL-075: TASK22260 sequence-A step3 completion slice — auth redirect safety + EN/CY route parity automation + +date: 2026-03-25 +author: Cline +scope: `tests/phase22/{auth-redirect-safety,i18n-route-parity,index}.test.cjs` +type: change +rationale: Continue on this branch to complete the remaining sequence-A step3 gaps by adding explicit automated checks for auth callback/redirect safety and EN/CY route parity. +impact: Improves confidence in auth redirect safety behavior and bilingual rewrite parity with targeted, low-risk regression checks and no runtime code changes. +status: completed + +Summary: + +- Added `tests/phase22/auth-redirect-safety.test.cjs`: + - validates locale resolution precedence (`query -> body -> cookie -> default en`) + - validates redirect callback behavior for: + - relative URL to same base + - same-origin absolute URL passthrough + - external URL rewritten to locale-safe base origin with preserved path/query +- Added `tests/phase22/i18n-route-parity.test.cjs`: + - asserts presence of required CY rewrite aliases for auth/policy routes in `next.config.js` + - includes checks for signin/email/error/verify-request + privacy/accessibility/terms routes +- Updated `tests/phase22/index.test.cjs` to include both new suites in aggregate phase22 execution. + +Validation: + +- `node tests/phase22/auth-redirect-safety.test.cjs` -> pass (4/4) +- `node tests/phase22/i18n-route-parity.test.cjs` -> pass (1/1) +- `node tests/phase22/index.test.cjs` -> pass + - core-token: 2/2 + - client-utils: 6/6 + - file-client: 4/4 + - case-service: 4/4 + - portal-service: 4/4 + - auth-redirect: 4/4 + - i18n-route: 1/1 + - phase22 combined: pass +- `npm run lint` -> warnings only (pre-existing `react-hooks/exhaustive-deps`; no new lint errors) + +Follow-ups: + +- Sequence A step3 targeted gaps are now covered; further test expansion should be treated as new scope (e.g., deeper end-to-end journey assertions). diff --git a/tests/phase22/auth-redirect-safety.test.cjs b/tests/phase22/auth-redirect-safety.test.cjs new file mode 100644 index 00000000..7020109c --- /dev/null +++ b/tests/phase22/auth-redirect-safety.test.cjs @@ -0,0 +1,167 @@ +const assert = require("assert"); +const fs = require("fs"); +const path = require("path"); +const vm = require("vm"); + +const tests = []; +const test = (name, fn) => tests.push({ name, fn }); + +const loadAuthInternals = () => { + const filePath = path.join( + __dirname, + "..", + "..", + "pages", + "api", + "auth", + "[...nextauth].js" + ); + + let source = fs.readFileSync(filePath, "utf8"); + source = source.replace(/import[\s\S]*?from\s+"[^"]+";\n?/g, ""); + source = source.replace( + /export default NextAuthPEDW;\s*$/, + "module.exports = { appendParamsAndPathToNewUrl, resolveLocale, authOptions, NextAuthPEDW };" + ); + + const context = { + module: { exports: {} }, + exports: {}, + require: (id) => { + if (id === "notifications-node-client") { + return { + NotifyClient: function NotifyClient() { + return { + sendEmail: async () => ({}) + }; + } + }; + } + + throw new Error(`Unexpected require in auth test: ${id}`); + }, + URL, + URLSearchParams, + process: { + env: { + NEXTAUTH_URL: "https://english.example", + CY_API_ROOT: "https://welsh.example", + NEXTAUTH_SECRET: "test-secret" + } + }, + PrismaAdapter: () => ({}), + PrismaClient: function PrismaClient() { + return {}; + }, + NextAuth: () => ({}), + EmailProvider: () => ({}), + consoleLogger: () => {}, + console: { + log: () => {}, + info: () => {}, + warn: () => {}, + error: () => {} + } + }; + + vm.runInNewContext(source, context, { filename: filePath }); + return context.module.exports; +}; + +test("auth/resolveLocale prefers query then body then cookie then default", async () => { + const mod = loadAuthInternals(); + + assert.strictEqual( + mod.resolveLocale({ + query: { locale: "cy" }, + body: { locale: "en" }, + cookies: { pedw_locale: "en" } + }), + "cy" + ); + + assert.strictEqual( + mod.resolveLocale({ + body: { locale: "cy" }, + cookies: { pedw_locale: "en" } + }), + "cy" + ); + + assert.strictEqual( + mod.resolveLocale({ + cookies: { pedw_locale: "cy" } + }), + "cy" + ); + + assert.strictEqual(mod.resolveLocale({}), "en"); +}); + +test("auth/redirect callback keeps relative URLs on same base", async () => { + const mod = loadAuthInternals(); + const req = { query: { locale: "en" }, body: {}, cookies: {} }; + + const options = mod.authOptions(req, {}); + const result = options.callbacks.redirect({ + url: "/account/register", + baseUrl: "https://pedw.example" + }); + + assert.strictEqual(result, "https://pedw.example/account/register"); +}); + +test("auth/redirect callback keeps same-origin absolute URLs unchanged", async () => { + const mod = loadAuthInternals(); + const req = { query: { locale: "en" }, body: {}, cookies: {} }; + + const options = mod.authOptions(req, {}); + const result = options.callbacks.redirect({ + url: "https://pedw.example/auth/verify-request?token=abc", + baseUrl: "https://pedw.example" + }); + + assert.strictEqual( + result, + "https://pedw.example/auth/verify-request?token=abc" + ); +}); + +test("auth/redirect callback rewrites external URL to locale-safe base origin", async () => { + const mod = loadAuthInternals(); + const req = { query: { locale: "cy" }, body: {}, cookies: {} }; + + const options = mod.authOptions(req, {}); + const result = options.callbacks.redirect({ + url: "https://malicious.example/auth/signin?callbackUrl=%2Fdashboard&token=abc", + baseUrl: "https://pedw.example" + }); + + const parsed = new URL(result); + assert.strictEqual(parsed.origin, "https://welsh.example"); + assert.strictEqual(parsed.pathname, "/auth/signin"); + assert.strictEqual(parsed.searchParams.get("callbackUrl"), "/dashboard"); + assert.strictEqual(parsed.searchParams.get("token"), "abc"); +}); + +const run = async () => { + let passed = 0; + + for (const currentTest of tests) { + await currentTest.fn(); + passed += 1; + } + + console.log( + `Phase 22 auth-redirect tests passed (${passed}/${tests.length}).` + ); +}; + +module.exports = run; + +if (require.main === module) { + run().catch((error) => { + console.error(error); + process.exit(1); + }); +} diff --git a/tests/phase22/case-service-behaviour.test.cjs b/tests/phase22/case-service-behaviour.test.cjs new file mode 100644 index 00000000..5c37e232 --- /dev/null +++ b/tests/phase22/case-service-behaviour.test.cjs @@ -0,0 +1,130 @@ +const assert = require("assert"); +const { + createAxiosMock, + createLoggerMock, + createAxiosError, + loadServiceModule, + normalize +} = require("../serviceHarness.cjs"); + +const tests = []; +const test = (name, fn) => tests.push({ name, fn }); + +test("case/getPortalModuleDetails uses BASE_URL route and encoded case reference", async () => { + const axios = createAxiosMock(); + const logger = createLoggerMock(); + + axios.getHandler = async () => ({ data: { value: [] } }); + + const caseService = loadServiceModule("caseDirectService.js", { + axios, + BASE_URL: "http://example.local", + consoleLogger: logger.consoleLogger + }); + + const result = await caseService.getPortalModuleDetails( + "appeal", + "REF A/B" + ); + + assert.deepStrictEqual(normalize(result), { value: [] }); + assert.strictEqual( + axios.calls[0].url, + "http://example.local/api/endpoint/getportalmoduledetails_api?appealType=appeal&caseReference=REF%20A/B" + ); +}); + +test("case/getPortalModuleDetailsProxy escapes apostrophes in case reference", async () => { + const axios = createAxiosMock(); + const logger = createLoggerMock(); + + axios.getHandler = async () => ({ data: { value: [] } }); + + const caseService = loadServiceModule("caseDirectService.js", { + axios, + BASE_URL: "", + consoleLogger: logger.consoleLogger + }); + + const result = await caseService.getPortalModuleDetailsProxy( + "appeal", + "REF'O" + ); + + assert.deepStrictEqual(normalize(result), { value: [] }); + assert.strictEqual( + axios.calls[0].url, + "/api/endpoint/getportalmoduledetailsproxy_api?appealType=appeal&caseReference=REF''O" + ); +}); + +test("case/getAppealID composes lookup route and extracts first non-underscore value", async () => { + const axios = createAxiosMock(); + const logger = createLoggerMock(); + + axios.getHandler = async () => ({ + data: { + value: [ + { + _internal: "ignore", + pinswg_appeal: "appeal-123" + } + ] + } + }); + + const caseService = loadServiceModule("caseDirectService.js", { + axios, + BASE_URL: "", + consoleLogger: logger.consoleLogger + }); + + const result = await caseService.getAppealID("REF-1", "pinswg_cases", "id"); + + assert.strictEqual(result, "appeal-123"); + assert.strictEqual( + axios.calls[0].url, + "/api/endpoint/getappealid_api?updateFormCollection=pinswg_cases&primaryAttribute=id&caseReference=REF-1" + ); +}); + +test("case/getAppealPDFDocument logs and returns error.response on failure", async () => { + const axios = createAxiosMock(); + const logger = createLoggerMock(); + const error = createAxiosError(404, "Not Found"); + + axios.getHandler = async () => Promise.reject(error); + + const caseService = loadServiceModule("caseDirectService.js", { + axios, + BASE_URL: "http://example.local", + consoleLogger: logger.consoleLogger + }); + + const result = await caseService.getAppealPDFDocument("inc-123"); + + assert.strictEqual(result, error.response); + assert.strictEqual(logger.calls.length, 1); +}); + +const run = async () => { + let passed = 0; + + for (const currentTest of tests) { + await currentTest.fn(); + passed += 1; + } + + console.log( + `Phase 22 case-service tests passed (${passed}/${tests.length}).` + ); +}; + +module.exports = run; + +if (require.main === module) { + run().catch((error) => { + console.error(error); + process.exit(1); + }); +} diff --git a/tests/phase22/client-utils-behaviour.test.cjs b/tests/phase22/client-utils-behaviour.test.cjs new file mode 100644 index 00000000..76bad6fe --- /dev/null +++ b/tests/phase22/client-utils-behaviour.test.cjs @@ -0,0 +1,236 @@ +const fs = require("fs"); +const path = require("path"); +const vm = require("vm"); +const assert = require("assert"); + +const rootDir = path.resolve(__dirname, "..", ".."); + +const loadEndpointClientModule = (injected = {}) => { + const filePath = path.join( + rootDir, + "actions", + "clients", + "endpointClient.js" + ); + let source = fs.readFileSync(filePath, "utf8"); + + source = source.replace(/import[\s\S]*?from\s+"[^"]+";\n?/g, ""); + source = source.replace(/export const\s+/g, "const "); + source += "\nmodule.exports = { getJson, requestJson };\n"; + + const context = { + module: { exports: {} }, + exports: {}, + require, + axios: () => { + throw new Error("axios not injected"); + }, + ...injected + }; + + vm.runInNewContext(source, context, { filename: filePath }); + return context.module.exports; +}; + +const loadRelayClientModule = (injected = {}) => { + const filePath = path.join(rootDir, "actions", "clients", "relayClient.js"); + let source = fs.readFileSync(filePath, "utf8"); + + source = source.replace(/import[\s\S]*?from\s+"[^"]+";\n?/g, ""); + source = source.replace(/export const\s+/g, "const "); + source += "\nmodule.exports = { buildHashedQueryUrl };\n"; + + const context = { + module: { exports: {} }, + exports: {}, + require, + axios: { + get: async () => { + throw new Error("axios.get not injected"); + } + }, + hashAPIPath: () => "&hash=fallback", + process: { env: {} }, + ...injected + }; + + vm.runInNewContext(source, context, { filename: filePath }); + return context.module.exports; +}; + +const loadFileRouteBuilderModule = (injected = {}) => { + const filePath = path.join( + rootDir, + "actions", + "clients", + "fileRouteBuilder.js" + ); + let source = fs.readFileSync(filePath, "utf8"); + + source = source.replace(/import[\s\S]*?from\s+"[^"]+";\n?/g, ""); + source = source.replace(/export const\s+/g, "const "); + source += + "\nmodule.exports = { buildFileQuery, withBaseUrl, appendQuerySuffix };\n"; + + const context = { + module: { exports: {} }, + exports: {}, + require, + encodeURIComponent, + ...injected + }; + + vm.runInNewContext(source, context, { filename: filePath }); + return context.module.exports; +}; + +const tests = []; +const test = (name, fn) => tests.push({ name, fn }); + +test("clients/endpointClient getJson returns response.data from axios.get", async () => { + const calls = []; + const axios = { + get: async (url, config) => { + calls.push({ url, config }); + return { data: { ok: true } }; + } + }; + + const mod = loadEndpointClientModule({ axios }); + const result = await mod.getJson("/x", { headers: { a: 1 } }); + + assert.deepStrictEqual(JSON.parse(JSON.stringify(result)), { ok: true }); + assert.strictEqual(calls.length, 1); + assert.strictEqual(calls[0].url, "/x"); + assert.strictEqual(calls[0].config.headers.a, 1); +}); + +test("clients/endpointClient requestJson returns response.data from axios(config)", async () => { + const calls = []; + const axios = async (config) => { + calls.push(config); + return { data: { saved: true } }; + }; + + const mod = loadEndpointClientModule({ axios }); + const result = await mod.requestJson({ method: "post", url: "/y" }); + + assert.deepStrictEqual(JSON.parse(JSON.stringify(result)), { saved: true }); + assert.strictEqual(calls.length, 1); + assert.strictEqual(calls[0].method, "post"); +}); + +test("clients/relayClient buildHashedQueryUrl appends browser hash response", async () => { + const calls = []; + + const mod = loadRelayClientModule({ + axios: { + get: async (url) => { + calls.push(url); + return { data: { hash: "&hash=abc" } }; + } + }, + process: { env: {} } + }); + + const result = await mod.buildHashedQueryUrl("/api/file/deleteblob?x=1"); + + assert.strictEqual(result, "/api/file/deleteblob?x=1&hash=abc"); + assert.strictEqual(calls.length, 1); + assert.strictEqual( + calls[0], + "/api/endpoint/gethash_api?path=%2Fapi%2Ffile%2Fdeleteblob%3Fx%3D1" + ); +}); + +test("clients/relayClient falls back to hashAPIPath on server when HASHKEY present", async () => { + const mod = loadRelayClientModule({ + axios: { + get: async () => { + throw new Error("network unavailable"); + } + }, + hashAPIPath: () => "&hash=server-fallback", + process: { env: { HASHKEY: "secret" } } + }); + + const result = await mod.buildHashedQueryUrl("/api/file/deleteblob?x=1"); + assert.strictEqual(result, "/api/file/deleteblob?x=1&hash=server-fallback"); +}); + +test("clients/relayClient rethrows when browser hash call fails and no HASHKEY", async () => { + const expected = new Error("hash service down"); + + const mod = loadRelayClientModule({ + axios: { + get: async () => { + throw expected; + } + }, + process: { env: {} } + }); + + await assert.rejects( + () => mod.buildHashedQueryUrl("/api/file/deleteblob?x=1"), + (error) => error === expected + ); +}); + +test("clients/fileRouteBuilder builds query with optional encoding and suffix helpers", async () => { + const mod = loadFileRouteBuilderModule(); + + const unencoded = mod.buildFileQuery("/api/file/getbloblist", { + container: "abc", + casefolderID: "x/y" + }); + const encoded = mod.buildFileQuery( + "/api/file/deleteblob", + { + container: "abc", + casefolderID: "x/y", + blobname: "doc one.pdf" + }, + { + encode: true + } + ); + + assert.strictEqual( + unencoded, + "/api/file/getbloblist?container=abc&casefolderID=x/y" + ); + assert.strictEqual( + encoded, + "/api/file/deleteblob?container=abc&casefolderID=x%2Fy&blobname=doc%20one.pdf" + ); + assert.strictEqual( + mod.withBaseUrl("http://example.local", unencoded), + "http://example.local/api/file/getbloblist?container=abc&casefolderID=x/y" + ); + assert.strictEqual( + mod.appendQuerySuffix(unencoded, "&hash=123"), + "/api/file/getbloblist?container=abc&casefolderID=x/y&hash=123" + ); +}); + +const run = async () => { + let passed = 0; + + for (const currentTest of tests) { + await currentTest.fn(); + passed += 1; + } + + console.log( + `Phase 22 client-utils tests passed (${passed}/${tests.length}).` + ); +}; + +module.exports = run; + +if (require.main === module) { + run().catch((error) => { + console.error(error); + process.exit(1); + }); +} diff --git a/tests/phase22/core-token-behaviour.test.cjs b/tests/phase22/core-token-behaviour.test.cjs new file mode 100644 index 00000000..2fefe54c --- /dev/null +++ b/tests/phase22/core-token-behaviour.test.cjs @@ -0,0 +1,114 @@ +const fs = require("fs"); +const path = require("path"); +const vm = require("vm"); +const assert = require("assert"); + +const rootDir = path.resolve(__dirname, "..", ".."); + +const loadTokenModule = (injected = {}) => { + const filePath = path.join(rootDir, "actions", "core", "token.js"); + let source = fs.readFileSync(filePath, "utf8"); + + source = source.replace(/import[\s\S]*?from\s+"[^"]+";\n?/g, ""); + source = source.replace(/export const\s+/g, "const "); + source += "\nmodule.exports = { getToken };\n"; + + const context = { + module: { exports: {} }, + exports: {}, + require, + process: { + env: { + GRANT_TYPE: "client_credentials", + CLIENT_ID: "client-id", + CLIENT_SECRET: "client-secret", + RELAYURI: "relay.local" + } + }, + ACCESS_TOKEN_ENDPOINT: "https://login.example/", + TENANT_ID: "tenant-123", + consoleLogger: () => {}, + requestJson: async () => { + throw new Error("requestJson not injected"); + }, + ...injected + }; + + vm.runInNewContext(source, context, { filename: filePath }); + return context.module.exports; +}; + +const tests = []; +const test = (name, fn) => tests.push({ name, fn }); + +test("core/getToken returns token payload and calls requestJson with expected config", async () => { + const calls = []; + + const mod = loadTokenModule({ + requestJson: async (config) => { + calls.push(config); + return { access_token: "abc", expires_in: 3600 }; + } + }); + + const result = await mod.getToken(); + + assert.deepStrictEqual(JSON.parse(JSON.stringify(result)), { + access_token: "abc", + expires_in: 3600 + }); + assert.strictEqual(calls.length, 1); + assert.strictEqual( + calls[0].url, + "https://login.example/tenant-123/oauth2/v2.0/token" + ); + assert.strictEqual(calls[0].method, "post"); + assert.strictEqual( + calls[0].data, + "grant_type=client_credentials&client_id=client-id&client_secret=client-secret&scope=https://relay.local/.default" + ); + assert.strictEqual( + calls[0].headers["Content-Type"], + "application/x-www-form-urlencoded" + ); +}); + +test("core/getToken logs and returns error object when requestJson throws", async () => { + const loggerCalls = []; + const error = new Error("token failed"); + + const mod = loadTokenModule({ + consoleLogger: (err) => loggerCalls.push(err), + requestJson: async () => { + throw error; + } + }); + + const result = await mod.getToken(); + + assert.strictEqual(result, error); + assert.strictEqual(loggerCalls.length, 1); + assert.strictEqual(loggerCalls[0], error); +}); + +const run = async () => { + let passed = 0; + + for (const currentTest of tests) { + await currentTest.fn(); + passed += 1; + } + + console.log( + `Phase 22 core-token tests passed (${passed}/${tests.length}).` + ); +}; + +module.exports = run; + +if (require.main === module) { + run().catch((error) => { + console.error(error); + process.exit(1); + }); +} diff --git a/tests/phase22/file-client-behaviour.test.cjs b/tests/phase22/file-client-behaviour.test.cjs new file mode 100644 index 00000000..767aad88 --- /dev/null +++ b/tests/phase22/file-client-behaviour.test.cjs @@ -0,0 +1,171 @@ +const fs = require("fs"); +const path = require("path"); +const vm = require("vm"); +const assert = require("assert"); + +const rootDir = path.resolve(__dirname, "..", ".."); + +const loadFileClientModule = (injected = {}) => { + const filePath = path.join(rootDir, "actions", "clients", "fileClient.js"); + let source = fs.readFileSync(filePath, "utf8"); + + source = source.replace(/import[\s\S]*?from\s+"[^"]+";\n?/g, ""); + source = source.replace(/export const\s+/g, "const "); + source += + "\nmodule.exports = { getFileJson, getSignedFileJson, downloadFileBlob, postSignedFileJson };\n"; + + const context = { + module: { exports: {} }, + exports: {}, + require, + getJson: async () => { + throw new Error("getJson not injected"); + }, + requestJson: async () => { + throw new Error("requestJson not injected"); + }, + buildHashedQueryUrl: async () => { + throw new Error("buildHashedQueryUrl not injected"); + }, + ...injected + }; + + vm.runInNewContext(source, context, { filename: filePath }); + return context.module.exports; +}; + +const tests = []; +const test = (name, fn) => tests.push({ name, fn }); + +test("clients/fileClient getFileJson delegates to getJson", async () => { + const calls = []; + + const mod = loadFileClientModule({ + getJson: async (url) => { + calls.push(url); + return { ok: true }; + } + }); + + const result = await mod.getFileJson("/api/file/getbloblistproxy?x=1"); + + assert.deepStrictEqual(JSON.parse(JSON.stringify(result)), { ok: true }); + assert.strictEqual(calls.length, 1); + assert.strictEqual(calls[0], "/api/file/getbloblistproxy?x=1"); +}); + +test("clients/fileClient getSignedFileJson signs url and requests json", async () => { + const signedCalls = []; + const requestCalls = []; + + const mod = loadFileClientModule({ + buildHashedQueryUrl: async (queryUrl) => { + signedCalls.push(queryUrl); + return queryUrl + "&hash=signed"; + }, + requestJson: async (config) => { + requestCalls.push(config); + return { deleted: true }; + } + }); + + const result = await mod.getSignedFileJson( + "/api/file/deleteblobcase?container=a&casefolderID=b" + ); + + assert.deepStrictEqual(JSON.parse(JSON.stringify(result)), { + deleted: true + }); + assert.strictEqual(signedCalls.length, 1); + assert.strictEqual(requestCalls.length, 1); + assert.strictEqual(requestCalls[0].method, "get"); + assert.strictEqual( + requestCalls[0].url, + "/api/file/deleteblobcase?container=a&casefolderID=b&hash=signed" + ); +}); + +test("clients/fileClient downloadFileBlob requests blob response", async () => { + const requestCalls = []; + + const mod = loadFileClientModule({ + requestJson: async (config) => { + requestCalls.push(config); + return "blob-data"; + } + }); + + const result = await mod.downloadFileBlob("/api/file/downloadblob?x=1"); + + assert.strictEqual(result, "blob-data"); + assert.strictEqual(requestCalls.length, 1); + assert.strictEqual(requestCalls[0].method, "get"); + assert.strictEqual(requestCalls[0].responseType, "blob"); + assert.strictEqual(requestCalls[0].url, "/api/file/downloadblob?x=1"); +}); + +test("clients/fileClient postSignedFileJson signs url and posts payload", async () => { + const signedCalls = []; + const requestCalls = []; + + const mod = loadFileClientModule({ + buildHashedQueryUrl: async (queryUrl) => { + signedCalls.push(queryUrl); + return queryUrl + "&hash=signed-post"; + }, + requestJson: async (config) => { + requestCalls.push(config); + return { uploaded: true }; + } + }); + + const payload = { name: "doc" }; + const result = await mod.postSignedFileJson( + "/api/file/uploadsinglefile", + payload, + { + headers: { "content-type": "multipart/form-data" } + } + ); + + assert.deepStrictEqual(JSON.parse(JSON.stringify(result)), { + uploaded: true + }); + assert.strictEqual(signedCalls.length, 1); + assert.strictEqual(requestCalls.length, 1); + assert.strictEqual(requestCalls[0].method, "post"); + assert.strictEqual( + requestCalls[0].url, + "/api/file/uploadsinglefile&hash=signed-post" + ); + assert.deepStrictEqual( + JSON.parse(JSON.stringify(requestCalls[0].data)), + payload + ); + assert.strictEqual( + requestCalls[0].headers["content-type"], + "multipart/form-data" + ); +}); + +const run = async () => { + let passed = 0; + + for (const currentTest of tests) { + await currentTest.fn(); + passed += 1; + } + + console.log( + `Phase 22 file-client tests passed (${passed}/${tests.length}).` + ); +}; + +module.exports = run; + +if (require.main === module) { + run().catch((error) => { + console.error(error); + process.exit(1); + }); +} diff --git a/tests/phase22/i18n-route-parity.test.cjs b/tests/phase22/i18n-route-parity.test.cjs new file mode 100644 index 00000000..683ab513 --- /dev/null +++ b/tests/phase22/i18n-route-parity.test.cjs @@ -0,0 +1,65 @@ +const assert = require("assert"); +const path = require("path"); + +const tests = []; +const test = (name, fn) => tests.push({ name, fn }); + +const loadRewrites = async () => { + const configPath = path.join(__dirname, "..", "..", "next.config.js"); + // eslint-disable-next-line global-require, import/no-dynamic-require + const nextConfig = require(configPath); + return nextConfig.rewrites(); +}; + +test("i18n/rewrites include required CY aliases for auth and policy routes", async () => { + const rewrites = await loadRewrites(); + + const requiredPairs = [ + { source: "/awd/mewngofnodi", destination: "/auth/signin" }, + { source: "/awd/mewngofnodi/ebost", destination: "/auth/signin/email" }, + { source: "/awd/gwall", destination: "/auth/error" }, + { source: "/awd/gwirio-cais", destination: "/auth/verify-request" }, + { source: "/preifatrwydd", destination: "/privacy" }, + { source: "/hygyrchedd", destination: "/accessibility" }, + { + source: "/telerau-ac-amodau", + destination: "/terms-and-conditions" + } + ]; + + for (const pair of requiredPairs) { + const found = rewrites.some( + (entry) => + entry.source === pair.source && + entry.destination === pair.destination + ); + + assert.strictEqual( + found, + true, + `Missing required CY rewrite ${pair.source} -> ${pair.destination}` + ); + } +}); + +const run = async () => { + let passed = 0; + + for (const currentTest of tests) { + await currentTest.fn(); + passed += 1; + } + + console.log( + `Phase 22 i18n-route tests passed (${passed}/${tests.length}).` + ); +}; + +module.exports = run; + +if (require.main === module) { + run().catch((error) => { + console.error(error); + process.exit(1); + }); +} diff --git a/tests/phase22/index.test.cjs b/tests/phase22/index.test.cjs new file mode 100644 index 00000000..f5f3f3eb --- /dev/null +++ b/tests/phase22/index.test.cjs @@ -0,0 +1,27 @@ +const runCoreTokenTests = require("./core-token-behaviour.test.cjs"); +const runClientUtilsTests = require("./client-utils-behaviour.test.cjs"); +const runFileClientTests = require("./file-client-behaviour.test.cjs"); +const runCaseServiceTests = require("./case-service-behaviour.test.cjs"); +const runPortalServiceTests = require("./portal-service-behaviour.test.cjs"); +const runAuthRedirectSafetyTests = require("./auth-redirect-safety.test.cjs"); +const runI18nRouteParityTests = require("./i18n-route-parity.test.cjs"); + +const run = async () => { + await runCoreTokenTests(); + await runClientUtilsTests(); + await runFileClientTests(); + await runCaseServiceTests(); + await runPortalServiceTests(); + await runAuthRedirectSafetyTests(); + await runI18nRouteParityTests(); + console.log("Phase 22 combined suite passed."); +}; + +if (require.main === module) { + run().catch((error) => { + console.error(error); + process.exit(1); + }); +} + +module.exports = run; diff --git a/tests/phase22/portal-service-behaviour.test.cjs b/tests/phase22/portal-service-behaviour.test.cjs new file mode 100644 index 00000000..f1f33b4b --- /dev/null +++ b/tests/phase22/portal-service-behaviour.test.cjs @@ -0,0 +1,157 @@ +const assert = require("assert"); +const { + createAxiosMock, + createLoggerMock, + createAxiosError, + loadServiceModule, + normalize +} = require("../serviceHarness.cjs"); + +const tests = []; +const test = (name, fn) => tests.push({ name, fn }); + +test("portal/sendCaseCompleteMessage appends signed hash suffix to inv query", async () => { + const axios = createAxiosMock(); + const logger = createLoggerMock(); + + const requestCalls = []; + const requestJson = async (config) => { + requestCalls.push(config); + return { status: "ok" }; + }; + + const portal = loadServiceModule("portalDirectService.js", { + axios, + BASE_URL: "", + consoleLogger: logger.consoleLogger, + buildHashedQueryUrl: async (queryUrl) => `${queryUrl}&hash=abc123`, + requestJson + }); + + const result = await portal.sendCaseCompleteMessage( + "container1", + "CASE-1", + "yes" + ); + + assert.deepStrictEqual(normalize(result), { status: "ok" }); + assert.strictEqual(requestCalls.length, 1); + assert.strictEqual(requestCalls[0].method, "get"); + assert.strictEqual( + requestCalls[0].url, + "/api/file/createappealcompletemessage_api?container=container1&tempcaseref=CASE-1&inv=yes&hash=abc123" + ); +}); + +test("portal/deleteMyRepresentations uses signed delete request with headers", async () => { + const axios = createAxiosMock(); + const logger = createLoggerMock(); + + const requestCalls = []; + const requestJson = async (config) => { + requestCalls.push(config); + return { deleted: true }; + }; + + const portal = loadServiceModule("portalDirectService.js", { + axios, + BASE_URL: "", + consoleLogger: logger.consoleLogger, + buildHashedQueryUrl: async (queryUrl) => `${queryUrl}&hash=xyz`, + requestJson + }); + + const result = await portal.deleteMyRepresentations("rep-1"); + + assert.deepStrictEqual(normalize(result), { deleted: true }); + assert.strictEqual(requestCalls.length, 1); + assert.strictEqual(requestCalls[0].method, "delete"); + assert.strictEqual( + requestCalls[0].url, + "/api/endpoint/deletemyrepresentations_api?myRepresentationsID=rep-1&hash=xyz" + ); + assert.strictEqual( + requestCalls[0].headers["Content-Type"], + "application/json" + ); +}); + +test("portal/createWatchedCases posts to harmonized route helper URL", async () => { + const axios = createAxiosMock(); + const logger = createLoggerMock(); + + const requestCalls = []; + const requestJson = async (config) => { + requestCalls.push(config); + return { created: true }; + }; + + const portal = loadServiceModule("portalDirectService.js", { + axios, + BASE_URL: "", + consoleLogger: logger.consoleLogger, + requestJson + }); + + const payload = { foo: "bar" }; + const result = await portal.createWatchedCases(payload); + + assert.deepStrictEqual(normalize(result), { created: true }); + assert.strictEqual(requestCalls.length, 1); + assert.strictEqual(requestCalls[0].method, "post"); + assert.strictEqual( + requestCalls[0].url, + "/api/endpoint/createwatchedcases_api" + ); + assert.deepStrictEqual(normalize(requestCalls[0].data), payload); +}); + +test("portal/sendRepCompleteMessage rejects when hash signing fails before request stage", async () => { + const axios = createAxiosMock(); + const logger = createLoggerMock(); + const error = createAxiosError(500, "Failed"); + + const portal = loadServiceModule("portalDirectService.js", { + axios, + BASE_URL: "", + consoleLogger: logger.consoleLogger, + buildHashedQueryUrl: async () => { + throw error; + }, + requestJson: async () => { + throw new Error("should not be called"); + } + }); + + await assert.rejects( + () => portal.sendRepCompleteMessage("c", "r", "f"), + (caught) => { + assert.strictEqual(caught, error); + return true; + } + ); + + assert.strictEqual(logger.calls.length, 0); +}); + +const run = async () => { + let passed = 0; + + for (const currentTest of tests) { + await currentTest.fn(); + passed += 1; + } + + console.log( + `Phase 22 portal-service tests passed (${passed}/${tests.length}).` + ); +}; + +module.exports = run; + +if (require.main === module) { + run().catch((error) => { + console.error(error); + process.exit(1); + }); +} diff --git a/tests/phase6/service-behaviour.test.cjs b/tests/phase6/service-behaviour.test.cjs index e130c93f..c869a445 100644 --- a/tests/phase6/service-behaviour.test.cjs +++ b/tests/phase6/service-behaviour.test.cjs @@ -1,96 +1,11 @@ -const fs = require("fs"); -const path = require("path"); -const vm = require("vm"); const assert = require("assert"); - -const rootDir = path.resolve(__dirname, "..", ".."); -const servicesDir = path.join(rootDir, "actions", "services"); - -const createAxiosMock = () => { - const axios = (config) => axios.request(config); - - axios.calls = []; - axios.requestHandler = async () => { - throw new Error("No axios.request handler configured"); - }; - axios.getHandler = async () => { - throw new Error("No axios.get handler configured"); - }; - axios.postHandler = async () => { - throw new Error("No axios.post handler configured"); - }; - - axios.request = (config) => { - axios.calls.push({ type: "request", config }); - return axios.requestHandler(config); - }; - - axios.get = (url, config) => { - axios.calls.push({ type: "get", url, config }); - return axios.getHandler(url, config); - }; - - axios.post = (url, data, config) => { - axios.calls.push({ type: "post", url, data, config }); - return axios.postHandler(url, data, config); - }; - - return axios; -}; - -const createLoggerMock = () => { - const calls = []; - const consoleLogger = (error) => { - calls.push(error); - }; - return { consoleLogger, calls }; -}; - -const createAxiosError = (status = 500, statusText = "Server Error") => { - return { - response: { - status, - statusText, - data: {} - }, - config: { - url: "/mock-url" - } - }; -}; - -const loadServiceModule = (fileName, injected = {}) => { - const filePath = path.join(servicesDir, fileName); - let source = fs.readFileSync(filePath, "utf8"); - - source = source.replace(/import[\s\S]*?from\s+"[^"]+";\n?/g, ""); - - const exportNames = Array.from( - source.matchAll(/export const\s+(\w+)\s*=/g) - ).map((match) => match[1]); - - source = source.replace(/export const\s+/g, "const "); - source += `\nmodule.exports = { ${exportNames.join(", ")} };\n`; - - const context = { - module: { exports: {} }, - exports: {}, - require, - URLSearchParams, - encodeURIComponent, - FormData: global.FormData, - console: { - log: () => {}, - info: () => {}, - warn: () => {}, - error: () => {} - }, - ...injected - }; - - vm.runInNewContext(source, context, { filename: filePath }); - return context.module.exports; -}; +const { + createAxiosMock, + createLoggerMock, + createAxiosError, + loadServiceModule, + normalize +} = require("../serviceHarness.cjs"); const tests = []; @@ -98,8 +13,6 @@ const test = (name, fn) => { tests.push({ name, fn }); }; -const normalize = (value) => JSON.parse(JSON.stringify(value)); - test("search/getBasicSearch returns res.data on success", async () => { const axios = createAxiosMock(); const logger = createLoggerMock(); @@ -225,6 +138,30 @@ test("case/getCaseMessage returns error.response on failure", async () => { assert.strictEqual(logger.calls.length, 1); }); +test("case/getPortalModuleDetails composes BASE_URL route with encoded case reference", async () => { + const axios = createAxiosMock(); + const logger = createLoggerMock(); + + axios.getHandler = async () => ({ data: { value: [] } }); + + const caseService = loadServiceModule("caseDirectService.js", { + axios, + BASE_URL: "http://example.local", + consoleLogger: logger.consoleLogger + }); + + const result = await caseService.getPortalModuleDetails( + "appeal", + "REF A/B" + ); + + assert.deepStrictEqual(normalize(result), { value: [] }); + assert.strictEqual( + axios.calls[0].url, + "http://example.local/api/endpoint/getportalmoduledetails_api?appealType=appeal&caseReference=REF%20A/B" + ); +}); + test("admin/getNewAppealsPage returns res.data on success", async () => { const axios = createAxiosMock(); const logger = createLoggerMock(); diff --git a/tests/phase7/service-behaviour.test.cjs b/tests/phase7/service-behaviour.test.cjs index 305ac424..fb008ae4 100644 --- a/tests/phase7/service-behaviour.test.cjs +++ b/tests/phase7/service-behaviour.test.cjs @@ -1,96 +1,11 @@ -const fs = require("fs"); -const path = require("path"); -const vm = require("vm"); const assert = require("assert"); - -const rootDir = path.resolve(__dirname, "..", ".."); -const servicesDir = path.join(rootDir, "actions", "services"); - -const createAxiosMock = () => { - const axios = (config) => axios.request(config); - - axios.calls = []; - axios.requestHandler = async () => { - throw new Error("No axios.request handler configured"); - }; - axios.getHandler = async () => { - throw new Error("No axios.get handler configured"); - }; - axios.postHandler = async () => { - throw new Error("No axios.post handler configured"); - }; - - axios.request = (config) => { - axios.calls.push({ type: "request", config }); - return axios.requestHandler(config); - }; - - axios.get = (url, config) => { - axios.calls.push({ type: "get", url, config }); - return axios.getHandler(url, config); - }; - - axios.post = (url, data, config) => { - axios.calls.push({ type: "post", url, data, config }); - return axios.postHandler(url, data, config); - }; - - return axios; -}; - -const createLoggerMock = () => { - const calls = []; - const consoleLogger = (error) => { - calls.push(error); - }; - return { consoleLogger, calls }; -}; - -const createAxiosError = (status = 500, statusText = "Server Error") => { - return { - response: { - status, - statusText, - data: {} - }, - config: { - url: "/mock-url" - } - }; -}; - -const loadServiceModule = (fileName, injected = {}) => { - const filePath = path.join(servicesDir, fileName); - let source = fs.readFileSync(filePath, "utf8"); - - source = source.replace(/import[\s\S]*?from\s+"[^"]+";\n?/g, ""); - - const exportNames = Array.from( - source.matchAll(/export const\s+(\w+)\s*=/g) - ).map((match) => match[1]); - - source = source.replace(/export const\s+/g, "const "); - source += `\nmodule.exports = { ${exportNames.join(", ")} };\n`; - - const context = { - module: { exports: {} }, - exports: {}, - require, - URLSearchParams, - encodeURIComponent, - FormData: global.FormData, - console: { - log: () => {}, - info: () => {}, - warn: () => {}, - error: () => {} - }, - ...injected - }; - - vm.runInNewContext(source, context, { filename: filePath }); - return context.module.exports; -}; +const { + createAxiosMock, + createLoggerMock, + createAxiosError, + loadServiceModule, + normalize +} = require("../serviceHarness.cjs"); const tests = []; @@ -98,8 +13,6 @@ const test = (name, fn) => { tests.push({ name, fn }); }; -const normalize = (value) => JSON.parse(JSON.stringify(value)); - test("document/getAwaitingSubmissionFromBlob includes hash token and returns data", async () => { const axios = createAxiosMock(); const logger = createLoggerMock(); @@ -302,6 +215,36 @@ test("document delete blob flows use signer hash", async () => { ); }); +test("document/downloadBlob delegates blob request config via file client helper", async () => { + const axios = createAxiosMock(); + const logger = createLoggerMock(); + + axios.requestHandler = async (config) => { + return { data: { type: "blob", url: config.url } }; + }; + + const document = loadServiceModule("documentDirectService.js", { + axios, + BASE_URL: "http://example.local", + consoleLogger: logger.consoleLogger, + hashAPIPath: () => "" + }); + + const result = await document.downloadBlob( + "MyContainer", + "folder/file.pdf" + ); + + assert.deepStrictEqual(normalize(result), { + type: "blob", + url: "http://example.local/api/file/downloadblob?container=mycontainer&blobname=folder/file.pdf" + }); + assert.strictEqual(axios.calls.length, 1); + assert.strictEqual(axios.calls[0].type, "request"); + assert.strictEqual(axios.calls[0].config.method, "get"); + assert.strictEqual(axios.calls[0].config.responseType, "blob"); +}); + test("account/getPortalLogin appends hash and returns res.data", async () => { const axios = createAxiosMock(); const logger = createLoggerMock(); @@ -367,7 +310,7 @@ test("notify/sendEmail posts payload and returns response data", async () => { const axios = createAxiosMock(); const logger = createLoggerMock(); - axios.postHandler = async () => ({ data: { id: "msg-1" } }); + axios.requestHandler = async () => ({ data: { id: "msg-1" } }); const notify = loadServiceModule("notifyDirectService.js", { axios, @@ -382,8 +325,9 @@ test("notify/sendEmail posts payload and returns response data", async () => { ); assert.deepStrictEqual(normalize(result), { id: "msg-1" }); - assert.strictEqual(axios.calls[0].url, "/api/email/notify"); - assert.deepStrictEqual(normalize(axios.calls[0].data), { + assert.strictEqual(axios.calls[0].config.url, "/api/email/notify"); + assert.strictEqual(axios.calls[0].config.method, "post"); + assert.deepStrictEqual(normalize(axios.calls[0].config.data), { templateId: "template-1", emailAddress: "person@example.com", reference: "ref-123", @@ -396,7 +340,7 @@ test("notify/sendEmail logs and rethrows on failure", async () => { const logger = createLoggerMock(); const error = createAxiosError(429, "Too Many Requests"); - axios.postHandler = async () => Promise.reject(error); + axios.requestHandler = async () => Promise.reject(error); const notify = loadServiceModule("notifyDirectService.js", { axios, diff --git a/tests/serviceHarness.cjs b/tests/serviceHarness.cjs new file mode 100644 index 00000000..4cf494ff --- /dev/null +++ b/tests/serviceHarness.cjs @@ -0,0 +1,202 @@ +const fs = require("fs"); +const path = require("path"); +const vm = require("vm"); + +const rootDir = path.resolve(__dirname, ".."); +const servicesDir = path.join(rootDir, "actions", "services"); + +const createAxiosMock = () => { + const axios = (config) => axios.request(config); + + axios.calls = []; + axios.requestHandler = async () => { + throw new Error("No axios.request handler configured"); + }; + axios.getHandler = async () => { + throw new Error("No axios.get handler configured"); + }; + axios.postHandler = async () => { + throw new Error("No axios.post handler configured"); + }; + + axios.request = (config) => { + axios.calls.push({ type: "request", config }); + return axios.requestHandler(config); + }; + + axios.get = (url, config) => { + axios.calls.push({ type: "get", url, config }); + return axios.getHandler(url, config); + }; + + axios.post = (url, data, config) => { + axios.calls.push({ type: "post", url, data, config }); + return axios.postHandler(url, data, config); + }; + + return axios; +}; + +const createLoggerMock = () => { + const calls = []; + const consoleLogger = (error) => { + calls.push(error); + }; + return { consoleLogger, calls }; +}; + +const createAxiosError = (status = 500, statusText = "Server Error") => { + return { + response: { + status, + statusText, + data: {} + }, + config: { + url: "/mock-url" + } + }; +}; + +const loadServiceModule = (fileName, injected = {}) => { + const filePath = path.join(servicesDir, fileName); + let source = fs.readFileSync(filePath, "utf8"); + + source = source.replace(/import[\s\S]*?from\s+"[^"]+";\n?/g, ""); + + const exportNames = Array.from( + source.matchAll(/export const\s+(\w+)\s*=/g) + ).map((match) => match[1]); + + source = source.replace(/export const\s+/g, "const "); + source += `\nmodule.exports = { ${exportNames.join(", ")} };\n`; + + const defaultGetJson = (url, config) => { + if (!injected.axios || !injected.axios.get) { + throw new Error("Missing axios.get for default getJson mock"); + } + + return injected.axios + .get(url, config) + .then((response) => response.data); + }; + + const defaultRequestJson = (config) => { + if (!injected.axios) { + throw new Error("Missing axios for default requestJson mock"); + } + + return injected.axios(config).then((response) => response.data); + }; + + const defaultGetFileJson = (url) => { + return defaultGetJson(url); + }; + + const defaultBuildHashedQueryUrl = async (queryUrl) => { + if (!injected.axios || !injected.axios.get) { + throw new Error( + "Missing axios.get for default buildHashedQueryUrl mock" + ); + } + + const hashResponse = await injected.axios.get( + "/api/endpoint/gethash_api?path=" + encodeURIComponent(queryUrl) + ); + + return queryUrl + hashResponse.data.hash; + }; + + const defaultGetSignedFileJson = async (queryUrl) => { + const hashedUrl = await ( + injected.buildHashedQueryUrl || defaultBuildHashedQueryUrl + )(queryUrl); + + return (injected.requestJson || defaultRequestJson)({ + method: "get", + url: hashedUrl + }); + }; + + const defaultDownloadFileBlob = (url) => { + return (injected.requestJson || defaultRequestJson)({ + method: "get", + url, + responseType: "blob" + }); + }; + + const defaultBuildFileQuery = (pathValue, params = {}, options = {}) => { + const { encode = false } = options; + const entries = Object.entries(params).filter(([, value]) => { + return value !== undefined && value !== null; + }); + + if (entries.length === 0) { + return pathValue; + } + + const query = entries + .map(([key, value]) => { + if (!encode) { + return `${key}=${String(value)}`; + } + + return `${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`; + }) + .join("&"); + + return `${pathValue}?${query}`; + }; + + const defaultWithBaseUrl = (baseUrl, route) => `${baseUrl}${route}`; + const defaultAppendQuerySuffix = (route, suffix = "") => + `${route}${suffix}`; + + const context = { + module: { exports: {} }, + exports: {}, + require, + URLSearchParams, + encodeURIComponent, + FormData: global.FormData, + console: { + log: () => {}, + info: () => {}, + warn: () => {}, + error: () => {} + }, + getJson: injected.getJson || defaultGetJson, + requestJson: injected.requestJson || defaultRequestJson, + getFileJson: injected.getFileJson || defaultGetFileJson, + getSignedFileJson: + injected.getSignedFileJson || defaultGetSignedFileJson, + downloadFileBlob: injected.downloadFileBlob || defaultDownloadFileBlob, + buildHashedQueryUrl: + injected.buildHashedQueryUrl || defaultBuildHashedQueryUrl, + buildFileQuery: injected.buildFileQuery || defaultBuildFileQuery, + withBaseUrl: injected.withBaseUrl || defaultWithBaseUrl, + appendQuerySuffix: + injected.appendQuerySuffix || defaultAppendQuerySuffix, + ...injected + }; + + vm.runInNewContext(source, context, { filename: filePath }); + return context.module.exports; +}; + +const normalize = (value) => { + if (value === undefined || value === null) { + return value; + } + + return JSON.parse(JSON.stringify(value)); +}; + +module.exports = { + createAxiosMock, + createLoggerMock, + createAxiosError, + loadServiceModule, + normalize +};