diff --git a/actions/services/accountDirectService.js b/actions/services/accountDirectService.js index 5f719fd2..5a236450 100644 --- a/actions/services/accountDirectService.js +++ b/actions/services/accountDirectService.js @@ -3,6 +3,26 @@ 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; + } +}; + export const getPersonalAccount = (contactid) => { return axios .get( @@ -105,7 +125,7 @@ export const getPortalLogin = async (emailAddress) => { "/api/endpoint/getportallogin_api?emailAddress=" + emailAddress; return axios - .get(BASE_URL + queryUrl + hashAPIPath(queryUrl)) + .get(BASE_URL + (await buildHashedQueryUrl(queryUrl))) .then((res) => res.data) .catch((error) => { consoleLogger(error); diff --git a/actions/services/documentDirectService.js b/actions/services/documentDirectService.js index d44bac8c..d0afabb8 100644 --- a/actions/services/documentDirectService.js +++ b/actions/services/documentDirectService.js @@ -3,6 +3,26 @@ 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; + } +}; + export const getAwaitingSubmissionFromBlob = (containerName) => { return axios .get( @@ -132,9 +152,11 @@ export const uploadFiles = async ( var queryUrl = "/api/file/upload"; + const hashedUrl = await buildHashedQueryUrl(queryUrl); + const config = { method: "post", - url: queryUrl, + url: hashedUrl, data: formData, headers: { "content-type": "multipart/form-data" } }; @@ -160,9 +182,11 @@ export const uploadSingleFile = async (filesObj, containerID, casefolderID) => { var queryUrl = "/api/file/uploadsinglefile"; + const hashedUrl = await buildHashedQueryUrl(queryUrl); + const config = { method: "post", - url: queryUrl, + url: hashedUrl, data: formData, headers: { "content-type": "multipart/form-data" } }; @@ -189,9 +213,11 @@ export const uploadRepFiles = async ( var queryUrl = "/api/file/upload"; + const hashedUrl = await buildHashedQueryUrl(queryUrl); + const config = { method: "post", - url: queryUrl, + url: hashedUrl, data: formData, headers: { "content-type": "multipart/form-data" } }; diff --git a/actions/services/portalDirectService.js b/actions/services/portalDirectService.js index 39b7b937..9bf17190 100644 --- a/actions/services/portalDirectService.js +++ b/actions/services/portalDirectService.js @@ -3,6 +3,26 @@ 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; + } +}; + export const getMyCases = (loggedInUserId) => { return axios .get( @@ -234,8 +254,11 @@ export const sendCaseCompleteMessage = async ( "&tempcaseref=" + caseReference + "&inv=" + - inv + - hashAPIPath(hashQueryPath); + inv; + + var signedQueryUrl = await buildHashedQueryUrl(hashQueryPath); + + queryUrl = queryUrl + signedQueryUrl.replace(hashQueryPath, ""); var config = { method: "get", @@ -280,7 +303,7 @@ export const sendRepCompleteMessage = async ( caseReference, fileName ) => { - var queryUrl = + var hashQueryPath = "/api/file/createrepcompletemessage_api?container=" + containerID + "&tempcaseref=" + @@ -288,6 +311,8 @@ export const sendRepCompleteMessage = async ( "&repid=" + fileName; + var queryUrl = await buildHashedQueryUrl(hashQueryPath); + var config = { method: "get", url: queryUrl diff --git a/components/elements/index.js b/components/elements/index.js index 5e5d10ad..ebe58c29 100644 --- a/components/elements/index.js +++ b/components/elements/index.js @@ -20,7 +20,11 @@ import fieldLookup from "../../data/crmfieldlookuptranslations.json"; import pickListLookup from "../../data/picklistLookups.json"; import dynamic from "next/dynamic"; -import { bytesToSize, getThumbnailIconByExtension } from "../utils"; +import { + bytesToSize, + getThumbnailIconByExtension, + updateLinks +} from "../utils"; import { setFileCount } from "../../store/appealType/action"; import { diff --git a/memory-bank/activeContext.md b/memory-bank/activeContext.md new file mode 100644 index 00000000..5ae5f9f9 --- /dev/null +++ b/memory-bank/activeContext.md @@ -0,0 +1,479 @@ +# Active Context — PEDW FrontEnd + +## Current development focus (from recent commits) + +- Search/case navigation correctness, especially breadcrumb and back-link behavior. +- My Portal "view all" and DNS application path handling. +- Welsh/English email behavior for specific notification templates. +- PDF output formatting and hyperlink behavior. + +## Recently changed areas (high signal) + +- `components/breadcrumbs.js` +- `components/case/summary.js` +- `components/search/searchresults.js` +- `components/search/addresssearchresults.js` +- `components/myportal/viewall.js` +- `pages/myportal/case/[ticketnumber].js` +- `pages/api/email/notify.js` +- `actions/index.js` +- `actions/core/env.js` +- `actions/core/logger.js` +- `actions/core/hash.js` +- `actions/core/token.js` +- `actions/core/headers.js` +- `actions/services/legacyActionsService.js` +- `i18n.js` + +## Active concerns + +- Breadcrumb and route-state logic is complex and query-dependent (`va`, `adv`, `ads`, `key`), so regressions are easy. +- Locale-specific route and label behavior is still a high-risk area due to rewrite + component logic coupling. +- Sensitive logging remains present in auth/email/actions paths and should be reduced/redacted over time. +- Hash validation on some file endpoints appears inconsistent (some checks active, some commented), requiring careful change discipline. + +## Refactor status update (2026-03-12) + +- Priority 1 refactor (Phase 1) is in place: + - `actions/index.js` now acts as a compatibility barrel. + - Core helper concerns were extracted into `actions/core/*`. + - Existing API/file/domain wrappers were moved into `actions/services/legacyActionsService.js` with backward-compatible exports retained via the barrel. +- No intentional behavior changes were introduced in this phase; focus was structural risk reduction. +- Priority 1 refactor (Phase 2) is now in place: + - Domain service grouping modules added under `actions/services/*`. + - `actions/services/index.js` now provides grouped service barrel exports. + - `actions/index.js` now re-exports from `./services` and `./core/*` while preserving existing public action names. +- `legacyActionsService` remains as a compatibility backing module and should be slimmed in later increments as consumers move to direct service imports. + +## Refactor status update (Priority 1 execution pass — 2026-03-12) + +- Completed an additional Priority 1 consumer-migration pass focused on high-use portal/form helpers. +- Updated these files to use focused service/core imports instead of broad `actions` barrel imports: + - `components/case/representation/representationElements.js` + - `components/elements/index.js` + - `components/utils/index.js` + - `lib/newappeal/loadNewAppealPage.js` + - `lib/myportal/loadMyPortalAppealPage.js` + - `components/myportal/topthree.js` + - `components/myportal/topthree_reps.js` + - `components/myportal/awaitingsubmissionfromblob.js` +- Compatibility model remains in place via `actions/index.js`; migration is incremental and non-breaking by design. + +## Refactor status update (Priority 1 execution pass — viewall/search/unsubscribe) + +- Completed another focused import migration pass to reduce broad `actions` barrel coupling in user-facing high-use areas: + - `components/myportal/viewall.js` + - `components/search/addresssearchresults.js` + - `pages/unsubscribe/[watchlistid].js` + - `pages/unsubscribeall/[watchlistid].js` +- These files now use explicit imports from `actions/services/*` and `actions/core/*` modules. +- Remaining non-comment broad `actions` imports are now primarily in selected `pages/api/endpoint/*` and `pages/api/file/*` handlers. + +## Refactor status update (Priority 1 execution pass — endpoint/file proxies) + +- Completed focused import migration for selected endpoint and file proxy handlers: + - `pages/api/endpoint/getmylpacases_api.js` + - `pages/api/endpoint/getbasicdnssearchpaged_api.js` + - `pages/api/endpoint/getsearchdocumentdetailspaged_api.js` + - `pages/api/endpoint/getbasicsearchpaged_api.js` + - `pages/api/file/getawaitingsubmissionfromblobproxy.js` + - `pages/api/file/getrepsblobproxy.js` + - `pages/api/file/getbloblistproxy.js` + - `pages/api/file/createappealcompletemessageproxy_api.js` +- Replaced broad `../../../actions` imports with focused `actions/core/*` and service-module imports. +- Eliminated duplicated local hash helper in `getmylpacases_api.js` by reusing shared `actions/core/hash`. +- Current broad-import scan indicates only comment-only legacy references remain. + +## Delivery handover status (2026-03-13) + +- Current import-migration wave has been checked in and completed through PR. +- This branch is now treated as the completed migration baseline. +- Next implementation chunk should start on a **new branch** and target decomposition of `actions/services/legacyActionsService.js`. +- Immediate focus for next branch: + 1. extract low-risk reference/search wrappers into direct service/client modules, + 2. migrate portal/document internals in small parity-checked batches, + 3. slim compatibility layer once parity is proven. + +## Phase 5 kickoff status (2026-03-13) + +- Branch in progress: `TASK21997-phase5-legacyactions-split`. +- First decomposition slice completed: + - search wrappers extracted to `actions/services/searchDirectService.js` + - reference-data wrappers extracted to `actions/services/referenceDataDirectService.js` + - `searchService` and `referenceDataService` now route through these direct modules. +- `legacyActionsService` remains in place for non-migrated domains (portal/document/account/case/admin/notify/integration) pending next slices. + +## Phase 5 status update (2026-03-13 — document slice) + +- Added `actions/services/documentDirectService.js` and migrated `documentService.js` to route through it. +- `legacyActionsService` now remains for: + - portal, account, case, admin, notify, integration domains. +- Search/reference/document grouped services now point at dedicated direct modules. + +## Phase 5 completion status (2026-03-13) + +- Grouped services now route through focused direct modules for all domains: + - search, reference-data, document, portal, account, case, admin, integration, notify. +- `actions/services/legacyActionsService.js` has been removed. +- Current architecture baseline: + - `actions/index.js` -> compatibility barrel re-exporting `actions/services` + core modules + - `actions/services/*Service.js` -> grouped public service boundaries + - `actions/services/*DirectService.js` -> implementation modules per domain +- Validation checkpoints completed: + - zero `legacyActionsService` imports in `actions/services/*.js` + - lint passes with warnings only + - build succeeds + +## Phase 6 hardening status (2026-03-13 — in progress) + +- Working branch: `TASK21998-phase6-postphase5-hardening` (created from `SIPS-Development`). +- Main hardening actions completed so far: + - Added focused parity test harness: + - `tests/phase6/service-parity.test.cjs` + - verifies grouped service import/export parity and `actions/services/index.js` re-export stability. + - Added shared error-path helper: + - `actions/services/httpServiceUtils.js` + - consolidates repeated `consoleLogger + error.response` and `ErrResponse` generation patterns. + - Applied safe duplication reduction in: + - `searchDirectService.js` + - `referenceDataDirectService.js` + - `caseDirectService.js` + - `adminDirectService.js` + - Logging cleanup for sensitive/noisy service paths (removed debug `console.log` usage) in: + - `searchDirectService.js` + - `documentDirectService.js` + - `portalDirectService.js` + - `caseDirectService.js` + - `adminDirectService.js` + +### Current validation evidence + +- `node tests/phase6/service-parity.test.cjs` -> pass +- `node tests/phase6/service-behaviour.test.cjs` -> pass (8/8) +- `npm run lint` -> warnings only +- Targeted local smoke snapshots: + - `GET /advancedsearch` and `GET /cy/advancedsearch` -> 200 + - `GET /case` and `GET /cy/case` -> 200 + - `GET /myportal` and `GET /cy/myportal` -> 307 redirect to `/auth/signin` (unauthenticated negative path) + - `GET /api/file/getbloblistproxy?container=test&casefolderID=test` -> 400 (negative path) + - `POST /api/email/notify` with empty payload -> 400 (negative path) + +### Active caveat + +- `GET /searchresults` and `GET /cy/searchresults` return 500 in current local dev run due pre-existing SSR serialization issue: + - `initialState.search.searchString` is undefined in `getServerSideProps` payload. + - captured in `/tmp/phase6-dev.log`. + +## Phase 7 hardening status (2026-03-13) + +- Working branch created from `origin/SIPS-Development`: + - `TASK21988a-phase7-postphase6-hardening` +- Pre-existing search results SSR serialization issue now addressed: + - `pages/searchresults.js` + - changed `setSearch(query.q)` to `setSearch(query?.q || "")` in `getServerSideProps`. + - local smoke result: both `/searchresults` and `/cy/searchresults` return 200 in this run. +- Behavioural test expansion delivered for remaining service domains: + - `tests/phase7/service-behaviour.test.cjs` + - covers document, portal, account, notify, integration. + +### Phase 7 validation evidence snapshot + +- `node tests/phase6/service-parity.test.cjs` -> pass +- `node tests/phase6/service-behaviour.test.cjs` -> pass (8/8) +- `node tests/phase7/service-behaviour.test.cjs` -> pass (10/10) +- `npm run lint` -> warnings only (existing react-hooks dependency warnings; no new lint errors) + +### Phase 7 smoke/negative-path snapshot + +- Dev server auto-bound to `localhost:3001` (3000 already in use). +- `GET /searchresults`, `GET /cy/searchresults` -> 200 +- `GET /case`, `GET /cy/case` -> 200 +- `GET /myportal`, `GET /cy/myportal` -> 307 redirect to `/auth/signin` +- `GET /api/file/getbloblistproxy?container=test&casefolderID=test` -> 400 +- `POST /api/email/notify` with `{}` -> 400 + +### Testing strategy decision (captured) + +- Current phase includes **both**: + 1. migration/parity guard tests, + 2. focused behavioural unit tests for critical response/error contracts. +- Broader behavioural coverage remains a follow-on expansion item after this focused baseline. + +## Phase 8 hardening status (2026-03-13) + +- Working branch created from `origin/SIPS-Development`: + - `TASK22017-phase8-hardening-slice` +- Sensitive API hardening slice completed across: + - `pages/api/file/deleteblobcase.js` + - `pages/api/file/deleteblobrep.js` + - `pages/api/file/createappealcompletemessage_api.js` + - `pages/api/endpoint/getportallogin_api.js` +- Hash/integrity posture updates: + - re-enabled hash validation on previously bypassed/commented file handlers + - added standardized early 400 handling for missing/invalid hash and missing key params + - preserved response-shape contracts and function signatures +- Logging discipline updates: + - removed noisy direct `console.log` usage in `createappealcompletemessage_api` + - routed error paths through `consoleLogger` in updated sensitive flow +- Required consumer parity updates applied: + - `actions/services/documentDirectService.js` + - now appends hash for `deleteblobcase` and `deleteblobrep` calls + - `actions/services/portalDirectService.js` + - now appends hash for `createappealcompletemessage_api` call using existing hash path contract + +### Phase 8 validation evidence snapshot + +- `node tests/phase6/service-parity.test.cjs` -> pass +- `node tests/phase6/service-behaviour.test.cjs` -> pass (8/8) +- `node tests/phase7/service-behaviour.test.cjs` -> pass (10/10) +- `node tests/phase8/service-behaviour.test.cjs` -> pass (5/5) +- `npm run lint` -> warnings only (pre-existing hook dependency warnings) + +### Phase 8 manual HTTP snapshot + +- Dev server on `http://localhost:3001`. +- Negative-path checks for invalid/missing hash (and missing required params where relevant) return **400** across all 4 target handlers. +- Valid-hash spot-check for `getportallogin_api` reached hash-validated path but still returned **400** due local upstream relay/CRM dependency behavior. + +## Phase 9 hardening status (2026-03-13) + +- Working branch created from `origin/SIPS-Development`: + - `TASK22019-phase-9-hardening` +- Additional sensitive API hardening slice completed across: + - `pages/api/file/createrepcompletemessage_api.js` + - `pages/api/file/upload.js` + - `pages/api/file/uploadsinglefile.js` + - `pages/api/file/setupcontainer.js` +- Hash/integrity posture updates: + - re-enabled/enforced hash checks in handlers where hash validation was bypassed/commented + - standardized early 400 handling for missing/invalid hash and missing key query/body values + - preserved response-shape contracts and function signatures +- Logging discipline updates: + - removed noisy `console.log` traces from sensitive upload/rep-complete message paths + - retained centralized error logging via `consoleLogger` where present +- Required caller parity updates applied: + - `actions/services/portalDirectService.js` + - `sendRepCompleteMessage` now appends hash + - `actions/services/documentDirectService.js` + - `uploadFiles`, `uploadRepFiles`, `uploadSingleFile` now append hash + +### Phase 9 validation evidence snapshot + +- `node tests/phase6/service-parity.test.cjs` -> pass +- `node tests/phase6/service-behaviour.test.cjs` -> pass (8/8) +- `node tests/phase7/service-behaviour.test.cjs` -> pass (10/10) +- `node tests/phase8/service-behaviour.test.cjs` -> pass (5/5) +- `node tests/phase9/service-behaviour.test.cjs` -> pass (5/5) +- `npm run lint` -> warnings only (pre-existing hook dependency warnings) + +### Phase 9 manual HTTP snapshot + +- Negative-path checks for missing/invalid hash and invalid/missing key params on all 4 phase-9 handlers return **400** on local server (`localhost:3000`). +- Valid-hash happy-path spot-check completed using `.env.local` hash key: + - `POST /api/file/uploadsinglefile?hash=` -> **200** + +## Phase 10 hardening status (2026-03-13) + +- Continued on branch: + - `TASK22019-phase-9-hardening` +- Additional sensitive API hardening slice completed across: + - `pages/api/file/getawaitingsubmissionfromblob.js` + - `pages/api/file/getprogressobjblob.js` + - `pages/api/file/getbloblist.js` + - `pages/api/file/getrepsblob.js` +- Hash/integrity posture updates: + - explicit early 400 guards for missing/empty required query inputs and hash + - standardized early hash-mismatch 400 behaviour in all 4 handlers + - preserved response-shape contracts and function signatures +- Logging discipline updates: + - removed old commented debug traces in touched handlers +- Caller parity: + - no additional service-layer updates required in this slice; existing direct services already append hash for these APIs. + +### Phase 10 validation evidence snapshot + +- `node tests/phase6/service-parity.test.cjs` -> pass +- `node tests/phase6/service-behaviour.test.cjs` -> pass (8/8) +- `node tests/phase7/service-behaviour.test.cjs` -> pass (10/10) +- `node tests/phase8/service-behaviour.test.cjs` -> pass (5/5) +- `node tests/phase9/service-behaviour.test.cjs` -> pass (5/5) +- `node tests/phase10/service-behaviour.test.cjs` -> pass (5/5) +- `npm run lint` -> warnings only (pre-existing hook dependency warnings) + +### Phase 10 manual HTTP snapshot + +- Missing-hash negative-path checks for all 4 handlers return **400** (`localhost:3000`). +- Valid-hash spot-check result: + - `getbloblist` returned **500** with valid hash in local environment (consistent with downstream/local dependency constraints; hash gate passed). + +## Phase 11 hardening status (2026-03-13) + +- Continued on branch: + - `TASK22019-phase-9-hardening` +- Additional sensitive API hardening slice completed across: + - `pages/api/file/getbloblist.js` + - `pages/api/file/deleteblob.js` + - `pages/api/file/deleteawaitingsubmissionfromblob.js` +- Hash/integrity posture updates: + - explicit early 400 guards for missing/empty required query inputs and hash + - standardized early hash-mismatch 400 behaviour in all touched handlers + - preserved response-shape contracts and function signatures +- Logging discipline updates: + - removed old commented debug traces in touched handlers +- Caller parity: + - no additional service-layer updates required in this slice. + +### Phase 11 validation evidence snapshot + +- `node tests/phase6/service-parity.test.cjs` -> pass +- `node tests/phase6/service-behaviour.test.cjs` -> pass (8/8) +- `node tests/phase7/service-behaviour.test.cjs` -> pass (10/10) +- `node tests/phase8/service-behaviour.test.cjs` -> pass (5/5) +- `node tests/phase9/service-behaviour.test.cjs` -> pass (5/5) +- `node tests/phase10/service-behaviour.test.cjs` -> pass (5/5) +- `node tests/phase11/service-behaviour.test.cjs` -> pass (4/4) +- `npm run lint` -> warnings only (pre-existing hook dependency warnings) + +### Phase 11 manual HTTP snapshot + +- Missing/invalid-hash negative-path checks for selected handlers return **400** (`localhost:3000`). +- Valid-hash spot-check result: + - `getbloblist` returned **500** with valid hash in local environment (consistent with downstream/local dependency constraints; hash gate passed). + +## Phase 12 hardening status (2026-03-13) + +- Continued on branch: + - `TASK22019-phase-9-hardening` +- Additional sensitive API hardening slice completed across: + - `pages/api/file/downloadblob.js` + - `pages/api/file/getbloblistproxy.js` + - `pages/api/file/getrepsblobproxy.js` + - `pages/api/file/getawaitingsubmissionfromblobproxy.js` +- Hash/integrity posture updates: + - explicit early 400 guards for missing/empty required query inputs + - standardized early hash-mismatch 400 behaviour in `downloadblob` + - preserved response-shape contracts and function signatures + +### Phase 12 validation evidence snapshot + +- `node tests/phase6/service-parity.test.cjs` -> pass +- `node tests/phase6/service-behaviour.test.cjs` -> pass (8/8) +- `node tests/phase7/service-behaviour.test.cjs` -> pass (10/10) +- `node tests/phase8/service-behaviour.test.cjs` -> pass (5/5) +- `node tests/phase9/service-behaviour.test.cjs` -> pass (5/5) +- `node tests/phase10/service-behaviour.test.cjs` -> pass (5/5) +- `node tests/phase11/service-behaviour.test.cjs` -> pass (4/4) +- `node tests/phase12/service-behaviour.test.cjs` -> pass (4/4) +- `npm run lint` -> warnings only (pre-existing hook dependency warnings) + +### Phase 12 manual HTTP snapshot + +- Missing/invalid-input negative-path checks for selected handlers return **400** (`localhost:3000`). +- Valid-hash spot-check result: + - `downloadblob` returned **500** with valid hash in local environment (consistent with downstream/local dependency constraints; hash gate passed). + +## Phase 13 hardening status (2026-03-13) + +- Continued on branch: + - `TASK22019-phase-9-hardening` +- Additional sensitive API hardening slice completed across: + - `pages/api/file/deleteblobcase.js` + - `pages/api/file/deleteblobrep.js` + - `pages/api/file/createcaseinvolvement_api.js` + - `pages/api/file/createrepinvolvement_api.js` +- Hash/integrity posture updates: + - explicit early 400 guards for missing/empty required query/body inputs + - standardized early hash-mismatch 400 behaviour in delete-blob handlers + - preserved response-shape contracts and function signatures +- Logging discipline updates: + - removed noisy direct body/query logging from involvement handlers + +### Phase 13 validation evidence snapshot + +- `node tests/phase6/service-parity.test.cjs` -> pass +- `node tests/phase6/service-behaviour.test.cjs` -> pass (8/8) +- `node tests/phase7/service-behaviour.test.cjs` -> pass (10/10) +- `node tests/phase8/service-behaviour.test.cjs` -> pass (5/5) +- `node tests/phase9/service-behaviour.test.cjs` -> pass (5/5) +- `node tests/phase10/service-behaviour.test.cjs` -> pass (5/5) +- `node tests/phase11/service-behaviour.test.cjs` -> pass (4/4) +- `node tests/phase12/service-behaviour.test.cjs` -> pass (4/4) +- `node tests/phase13/service-behaviour.test.cjs` -> pass (7/7) +- `npm run lint` -> warnings only (pre-existing hook dependency warnings) + +### Phase 13 manual HTTP snapshot + +- Missing/invalid-input negative-path checks for selected handlers return **400** (`localhost:3000`). +- Valid-hash spot-check result: + - `deleteblobcase` returned **500** with valid hash in local environment (consistent with downstream/local dependency constraints; hash gate passed). + +## Phase 14 hardening status (2026-03-13) + +- Continued on branch: + - `TASK22019-phase-9-hardening` +- Addressed browser runtime error in authenticated new-appeal journey: + - `TypeError` in `hashAPIPath` caused by client-side code attempting to parse server-only `HASHKEY`. +- Implemented server-side hash signing bridge for browser callers: + - new endpoint `pages/api/endpoint/gethash_api.js` + - session-gated (`getSession`), allow-listed supported API prefixes, returns `{ hash }`. +- Updated direct service callers to use signer endpoint instead of direct client hash computation: + - `actions/services/documentDirectService.js` + - `actions/services/portalDirectService.js` +- Maintained contract compatibility for target file APIs and existing response shapes. + +### Phase 14 validation evidence snapshot + +- phase6 parity/behaviour tests -> pass +- phase7/8/9/10/11/12/13 behaviour tests -> pass +- phase14 behaviour tests -> pass (4/4) +- lint -> warnings only (pre-existing hook dependency warnings) + +## Phase 14 closeout follow-up status (2026-03-13) + +- Continued on branch: + - `TASK22019-phase-9-hardening` +- Browser-safe hash signing expanded for portal login flow: + - `actions/services/accountDirectService.js` + - `getPortalLogin` now requests signed hash from `/api/endpoint/gethash_api` + - server fallback retained for non-browser contexts when `HASHKEY` is present + - `pages/api/endpoint/gethash_api.js` + - allow-list now includes `/api/endpoint/getportallogin_api` +- Contract alignment + runtime fix updates: + - `pages/api/file/getrepsblob.js` + - removed hard requirement for `casefolderID` to match actual route contract (`container + hash`) + - `components/elements/index.js` + - restored missing `updateLinks` import required by Quill-related runtime path + +### Follow-up validation evidence + +- `node tests/phase7/service-behaviour.test.cjs` -> pass (10/10) +- `node tests/phase14/service-behaviour.test.cjs` -> pass (5/5) +- `node tests/phase9/service-behaviour.test.cjs` -> pass (5/5) +- full previously-run phase6–phase14 suite remains passing +- `npm run lint` -> warnings only (pre-existing `react-hooks/exhaustive-deps` set) + +### Active risk posture after follow-up + +- Signer endpoint scope remains controlled by strict allow-list (single new path added). +- Response-shape contract stability preserved for touched handlers/services. +- Remaining risk is primarily external dependency behavior on valid-hash happy paths in local environments (relay/storage), not hash-bypass behavior. + +## Likely next steps + +1. Stabilize and simplify breadcrumb/back-link decision logic with focused regression checks. +2. Standardize hash-check enforcement patterns across file and endpoint APIs. +3. Reduce verbose logging in sensitive flows (auth/email/file/account). +4. Expand automated tests for navigation and representation eligibility edge-cases (currently minimal `tests/`). + +See `memory-bank/refactor-backlog.md` for the prioritized refactor plan and sequencing. +Priority 1 detailed execution plan is documented in `memory-bank/refactor-plan-actions-index.md`. + +## Areas currently important for contributors + +- `components/breadcrumbs.js` +- `components/case/summary.js` +- `pages/api/endpoint/**` +- `pages/api/file/**` +- `pages/api/auth/[...nextauth].js` +- `pages/api/email/notify.js` +- `next.config.js`, `i18n.js`, `locales/**` diff --git a/memory-bank/change-log.md b/memory-bank/change-log.md new file mode 100644 index 00000000..d5f61e80 --- /dev/null +++ b/memory-bank/change-log.md @@ -0,0 +1,931 @@ +# Change Log (AI/Human Curated) + +## Entry Template + +``` +date: YYYY-MM-DD +author: +scope: +type: change +rationale: +impact: +status: completed|rolled-back|partial + +Summary: +Validation: +Follow-ups: +``` + +--- + +### CL-001: Initial AI Enablement Bundle + +date: 2026-03-11 +author: Cline +scope: `.clinerules`, `ai-prompts/`, `context/`, `memory-bank/` +type: change +rationale: Establish consistent AI collaboration, delivery guardrails, and durable project context. +impact: Higher consistency/safety in future changes, especially around auth/i18n/public-service reliability. +status: completed + +Summary: +Created repository-specific governance, prompt templates, context documentation, and memory protocols with seed entries. + +Validation: + +- File structure and contents created in-repo. +- Content aligned to detected stack and key paths (`next-auth`, Prisma, middleware, i18n rewrites, Redux HYDRATE/persist). + +Follow-ups: + +- Confirm owner assignments for open questions. +- Add automation checks to enforce key guardrails over time. + +--- + +### CL-002: Priority 1 Refactor (Phase 1) — `actions/index.js` split foundation + +date: 2026-03-12 +author: Cline +scope: `actions/index.js`, `actions/core/*`, `actions/services/legacyActionsService.js`, `actions/clients/README.md`, `memory-bank/*` +type: change +rationale: Reduce coupling in the actions monolith by extracting core helpers while keeping backward-compatible exports for existing consumers. +impact: Lower regression blast radius for future actions-related changes; no intended functional behavior change. +status: completed + +Summary: +Extracted helper concerns from `actions/index.js` into focused `actions/core` modules (`env`, `logger`, `hash`, `token`, `headers`). Replaced `actions/index.js` with a compatibility barrel and moved operational wrappers into `actions/services/legacyActionsService.js` with imports from core helpers. + +Validation: + +- Branch created: `0000-update-actions-monolith`. +- Compatibility preserved by re-exporting legacy service functions plus core exports from `actions/index.js`. +- Lint run for regression check. + +Follow-ups: + +- Phase 2: split `legacyActionsService` by domain into dedicated service/client modules. +- Add helper-focused automated tests for core modules and export parity checks. + +--- + +### CL-003: Priority 1 Refactor (Phase 2) — service grouping via compatibility layer + +date: 2026-03-12 +author: Cline +scope: `actions/services/*`, `actions/index.js`, `memory-bank/*` +type: change +rationale: Continue reducing monolith risk by introducing domain service boundaries while preserving public action exports. +impact: Maintains backward compatibility for existing `../actions` imports; improves maintainability and safer future migration to targeted imports. +status: completed + +Summary: +Added grouped service modules (`searchService`, `caseService`, `accountService`, `portalService`, `documentService`, `referenceDataService`, `notifyService`, `adminService`, `integrationService`) plus `actions/services/index.js` barrel. Updated `actions/index.js` to export from `./services` (and core modules), keeping legacy function names/signatures available. + +Validation: + +- Service export coverage check confirms all legacy exports are represented in grouped services. +- `npm run lint` still fails due legacy Next/ESLint option incompatibility (tooling issue, unchanged by this refactor). + +Follow-ups: + +- Incrementally migrate high-churn consumers to direct service imports. +- Add automated export-parity and helper unit tests. + +--- + +### CL-004: Priority 1 Refactor (Phase 3) — targeted consumer import migration + +date: 2026-03-12 +author: Cline +scope: `components/case/summary.js`, `pages/api/email/notify.js`, selected `pages/api/endpoint/*`, `memory-bank/*` +type: change +rationale: Begin replacing broad `actions` barrel imports with focused service/core module imports in high-churn and representative endpoint areas. +impact: Lower coupling to monolithic action entrypoint while preserving behavior and public compatibility. +status: completed + +Summary: +Migrated targeted consumers to direct imports from `actions/services/*` and `actions/core/*` without changing function signatures or route contracts. + +Validation: + +- Lint still blocked by repository ESLint/Next option compatibility issue (pre-existing tooling configuration). +- Manual testing remains required for search/case/myportal/auth/notify/file matrix. + +Follow-ups: + +- Continue phased import migration for additional `pages/api/endpoint/**` handlers. +- Add export-parity and helper-level tests as planned. + +--- + +### CL-005: Priority 1 Refactor (Phase 4) — helper hardening + redacted logging baseline + +date: 2026-03-12 +author: Cline +scope: `actions/core/{guards,logger}.js`, `actions/index.js`, selected API handlers +type: change +rationale: Introduce low-risk guard and redaction primitives, then apply them to sensitive/high-churn API paths to reduce injection/privacy risk while preserving behavior. +impact: Better input hygiene and safer diagnostics in selected notify/login/contact flows; no intended contract changes. +status: completed + +Summary: +Added `guards` helper module (`isNonEmptyString`, `sanitizeString`, `escapeODataString`) and logger redaction utility (`redactSensitive`). Adopted these helpers in `notify`, `getpreferredlanguage_api`, and `createcrmtask_api` handlers. + +Validation: + +- `npm run lint` remains blocked by existing Next/ESLint options incompatibility in repository tooling. +- Targeted diff review confirms changes are scoped to helper hardening and selected handlers. + +Follow-ups: + +- Continue Phase 4 adoption for additional sensitive file/email/auth handlers. +- Add focused unit tests for `guards` and `redactSensitive` behavior. + +--- + +### CL-006: Priority 1 Refactor — consumer migration pass (portal/form/admin utilities) + +date: 2026-03-12 +author: Cline +scope: `components/admin/*`, `components/case/*`, `components/myportal/*`, `components/elements/index.js`, `components/utils/index.js`, `lib/*`, `memory-bank/*` +type: change +rationale: Continue Phase 3-style decoupling by replacing broad `actions` barrel imports with focused service/core imports in high-use consumer files while preserving behavior. +impact: Lower coupling to monolithic action barrel and clearer dependency boundaries; no intended functional behavior change. +status: completed + +Summary: +Migrated a targeted set of portal/form/admin utility consumers from `../../actions`/`../../../actions` to explicit imports from `actions/services/*` and `actions/core/*`. Also resolved migration-side compile issues in `components/myportal/topthree_reps.js` (duplicate import source and invalid `res.status` usage in client context). + +Validation: + +- In-scope consumer files now have no broad `from "../../actions"` / `from "../../../actions"` imports. +- `npm run lint` re-run; result unchanged and blocked by pre-existing Next/ESLint option incompatibility. + +Follow-ups: + +- Continue migrating remaining broad `actions` imports in `components/myportal/viewall.js`, `components/search/addresssearchresults.js`, unsubscribe flows, and selected API endpoints. +- Add focused manual smoke checks for myportal representations and awaiting-submission deletion/edit flows after this import migration. + +--- + +### CL-007: Priority 1 Refactor — consumer migration pass (viewall/search/unsubscribe) + +date: 2026-03-12 +author: Cline +scope: `components/myportal/viewall.js`, `components/search/addresssearchresults.js`, `pages/unsubscribe/[watchlistid].js`, `pages/unsubscribeall/[watchlistid].js`, `memory-bank/*` +type: change +rationale: Continue Phase 3 decoupling by removing additional broad `actions` barrel imports in high-use myportal/search/unsubscribe flows and replacing them with focused service/core imports. +impact: Reduced coupling to monolithic action barrel with intended behavior parity; no API contract changes intended. +status: completed + +Summary: +Migrated four additional consumers away from `../../actions`: + +- `components/myportal/viewall.js` -> `actions/services/documentService`, `actions/services/portalService`, `actions/core/logger` +- `components/search/addresssearchresults.js` -> `actions/services/portalService` +- `pages/unsubscribe/[watchlistid].js` -> `actions/core/{hash,token,headers,logger}` +- `pages/unsubscribeall/[watchlistid].js` -> `actions/core/{hash,token,headers,logger}` + +Validation: + +- Regex verification confirms these four files no longer import from the broad `actions` barrel. +- Repository-wide broad-import scan now reports 11 matches total, including comment-only occurrences and remaining endpoint/file handlers pending migration. + +Follow-ups: + +- Migrate remaining non-comment broad imports in selected `pages/api/endpoint/*` and `pages/api/file/*` handlers. +- Run focused manual smoke checks for myportal view-all watch/unwatch + unsubscribe routes (EN/CY path sanity and negative paths). + +--- + +### CL-008: Priority 1 Refactor — endpoint/file proxy import migration chunk + +date: 2026-03-12 +author: Cline +scope: `pages/api/endpoint/*`, `pages/api/file/*` (selected handlers), `memory-bank/*` +type: change +rationale: Continue Phase 3 migration by replacing broad `actions` barrel imports in selected endpoint/file proxy handlers with focused `actions/core/*` and service module imports. +impact: Further reduces monolithic import coupling in API paths while preserving existing request/response behavior and hash/token/header patterns. +status: completed + +Summary: +Migrated these handlers away from broad `../../../actions` imports: + +- `pages/api/endpoint/getmylpacases_api.js` + - `getLPA` -> `actions/services/referenceDataService` + - `azureHeaders`, `consoleLogger`, `getToken`, `hashAPIPath` -> `actions/core/*` + - removed local duplicated `hashAPIPath` implementation +- `pages/api/endpoint/getbasicdnssearchpaged_api.js` +- `pages/api/endpoint/getsearchdocumentdetailspaged_api.js` +- `pages/api/endpoint/getbasicsearchpaged_api.js` +- `pages/api/file/getawaitingsubmissionfromblobproxy.js` +- `pages/api/file/getrepsblobproxy.js` +- `pages/api/file/getbloblistproxy.js` +- `pages/api/file/createappealcompletemessageproxy_api.js` + +Validation: + +- Regex scans over `pages/api/endpoint` and `pages/api/file` show **0** remaining broad `from "../../../actions"` imports. +- `git status --short` confirms this chunk is isolated to the targeted endpoint/file handlers. + +Follow-ups: + +- Run focused manual smoke checks for touched endpoint/file flows (paged search APIs, LPA case retrieval, blob proxy endpoints). +- Continue hardening pass for sensitive handlers with guard/redaction helpers where not yet applied. + +--- + +### CL-009: Priority 1 import-migration wave checked in + PR completed; handover to next branch + +date: 2026-03-13 +author: Cline +scope: `actions/services/*` consumer and endpoint migrations (completed), planning handover docs +type: change +rationale: Record that the current import-migration wave has been checked in and completed via pull request, and set the next refactor slice to start on a new branch. +impact: Clear project continuity and reduced risk of overlap between completed import migration work and upcoming `legacyActionsService` decomposition. +status: completed + +Summary: + +- Import migration wave is now completed, checked in, and PR’d. +- Latest commits in this wave include: + - `f0202bb` — viewall/search/unsubscribe focused imports + - `fa98535` — endpoint/file proxy focused imports + - `bef8f25` — stale legacy comment cleanup +- Next chunk is explicitly designated as a new-branch activity focused on splitting `actions/services/legacyActionsService.js` into smaller direct service/client modules. + +Validation: + +- Broad `.../actions` import scans now show no active matches in `.js` files (legacy references are comment-only/removed). +- Branch history confirms migration commits are present and ordered for safe rollback. + +Follow-ups: + +- Create new branch for Phase 5 decomposition work. +- Start with low-risk extraction slices from `legacyActionsService.js` (reference/search first), then portal/document batches. + +--- + +### CL-010: Phase 5 kickoff — first legacy split slice (search/reference direct modules) + +date: 2026-03-13 +author: Cline +scope: `actions/services/searchDirectService.js`, `actions/services/referenceDataDirectService.js`, `actions/services/{searchService,referenceDataService}.js` +type: change +rationale: Start Phase 5 decomposition on a new branch by extracting low-risk search/reference wrappers from `legacyActionsService.js` into direct domain modules while keeping service exports stable. +impact: Search/reference service exports now route through focused direct modules; compatibility signatures preserved for callers importing from service barrels. +status: completed + +Summary: + +- Created branch `TASK21997-phase5-legacyactions-split` from `SIPS-Development`. +- Added `searchDirectService.js` containing extracted search/address/document-search wrapper implementations. +- Added `referenceDataDirectService.js` containing extracted reference-data wrapper implementations. +- Updated `searchService.js` and `referenceDataService.js` to import from the new direct modules instead of `legacyActionsService`. + +Validation: + +- `npm run lint` (result unchanged): still fails due pre-existing Next/ESLint options incompatibility in repository tooling. +- Import checks confirm: + - `searchService.js` -> `./searchDirectService` + - `referenceDataService.js` -> `./referenceDataDirectService` +- Branch/status check confirms only Phase 5 slice files are modified on the new branch. + +Follow-ups: + +- Continue portal/document extraction slices from `legacyActionsService.js` in small batches. +- After each slice, re-point corresponding grouped service modules and remove dead wrappers once no longer referenced. + +--- + +### CL-011: Phase 5 document slice — direct document module extraction + +date: 2026-03-13 +author: Cline +scope: `actions/services/documentDirectService.js`, `actions/services/documentService.js`, `memory-bank/*` +type: change +rationale: Continue Phase 5 decomposition by extracting document/file wrappers from `legacyActionsService.js` into a dedicated direct module while preserving grouped service API compatibility. +impact: Document/file grouped service exports now route through focused direct implementation; legacy coupling reduced for document domain with no expected caller contract changes. +status: completed + +Summary: + +- Added `documentDirectService.js` with extracted blob/file upload, download, pdf, and container helpers. +- Re-pointed `documentService.js` imports to `./documentDirectService`. +- Kept export names and signatures unchanged at grouped service boundary. + +Validation: + +- Service import scan confirms `documentService.js` no longer imports from `legacyActionsService`. +- Remaining legacy-backed grouped modules: `portalService`, `accountService`, `caseService`, `adminService`, `notifyService`, `integrationService`. +- `npm run lint` completes with warnings only (no blocking errors). + +Follow-ups: + +- Next recommended slice: `portalDirectService` extraction, then `account/case`. +- Continue per-slice parity checks and legacy import reduction scan. + +--- + +### CL-012: Phase 5 portal slice — direct portal module extraction + +date: 2026-03-13 +author: Cline +scope: `actions/services/portalDirectService.js`, `actions/services/portalService.js` +type: change +rationale: Continue Phase 5 decomposition by extracting portal-domain wrappers from `legacyActionsService` into a direct module while preserving grouped service contract stability. +impact: Portal grouped exports now route through a focused direct module; reduced coupling to legacy monolith. +status: completed + +Summary: + +- Added `portalDirectService.js`. +- Updated `portalService.js` to import from `./portalDirectService`. + +Validation: + +- Legacy import scan reduced remaining `legacyActionsService`-backed grouped modules. +- `npm run lint` completes with warnings only. + +Follow-ups: + +- Extract account + case direct modules. + +--- + +### CL-013: Phase 5 account/case slice — direct module extraction + +date: 2026-03-13 +author: Cline +scope: `actions/services/{accountDirectService,caseDirectService,accountService,caseService}.js` +type: change +rationale: Continue Phase 5 by moving account and case wrappers out of legacy module into dedicated direct modules while keeping grouped service API stable. +impact: Account/case grouped services now point to direct implementations; further legacy decoupling. +status: completed + +Summary: + +- Added `accountDirectService.js` and `caseDirectService.js`. +- Re-pointed `accountService.js` and `caseService.js`. + +Validation: + +- Legacy import scan left only admin/integration/notify using legacy. +- `npm run lint` completes with warnings only. + +Follow-ups: + +- Extract admin + integration + notify direct modules. + +--- + +### CL-014: Phase 5 final extraction slice — admin/integration/notify direct modules + +date: 2026-03-13 +author: Cline +scope: `actions/services/{adminDirectService,integrationDirectService,notifyDirectService,adminService,integrationService,notifyService}.js` +type: change +rationale: Complete Phase 5 domain decomposition by extracting final legacy-backed grouped services into direct modules. +impact: All grouped service modules now route through direct implementations. +status: completed + +Summary: + +- Added `adminDirectService.js`, `integrationDirectService.js`, `notifyDirectService.js`. +- Re-pointed `adminService.js`, `integrationService.js`, `notifyService.js`. + +Validation: + +- `actions/services` scan reports zero `./legacyActionsService` imports. +- `npm run lint` completes with warnings only. + +Follow-ups: + +- Remove dead `legacyActionsService.js` implementation. + +--- + +### CL-015: Phase 5 cleanup — remove legacy actions service + +date: 2026-03-13 +author: Cline +scope: `actions/services/legacyActionsService.js` (deleted) +type: change +rationale: Finalize Phase 5 after full routing migration by removing dead legacy implementation module. +impact: Monolithic legacy service removed; architecture now uses grouped service façade + direct per-domain modules. +status: completed + +Summary: + +- Deleted `actions/services/legacyActionsService.js` after zero-reference verification. + +Validation: + +- Project scan confirms no `legacyActionsService` references in `.js` files. +- `npm run lint` completes with warnings only. +- `npm run build` succeeds. + +Follow-ups: + +- Begin next phase: post-split hardening/tests/logging cleanup. + +--- + +### CL-016: Phase 6 hardening — parity tests, safe dedupe, and logging cleanup + +date: 2026-03-13 +author: Cline +scope: `actions/services/*DirectService.js`, `actions/services/httpServiceUtils.js`, `tests/phase6/service-parity.test.cjs`, `memory-bank/*` +type: change +rationale: Start post-Phase-5 hardening by adding focused parity checks, reducing repeated axios/error-handling patterns without contract changes, and removing noisy logs in sensitive service paths. +impact: Improved maintainability and safety posture in service layer with preserved grouped-service signatures/return shapes; added regression guard for service export stability. +status: completed + +Summary: + +- Added `tests/phase6/service-parity.test.cjs` to verify: + - grouped service import/export parity for search/reference/document/portal/account/case/admin/integration/notify + - `actions/services/index.js` re-export list stability +- Added `actions/services/httpServiceUtils.js` with shared helpers: + - `logAndReturnResponse` + - `buildEmptyValueErrorResponse` + - `logAndReturnEmptyValueErrorResponse` +- Applied low-risk dedupe refactors in: + - `searchDirectService.js` + - `referenceDataDirectService.js` + - `caseDirectService.js` + - `adminDirectService.js` +- Removed debug `console.log` traces from sensitive/noisy direct service flows in: + - `searchDirectService.js` + - `documentDirectService.js` + - `portalDirectService.js` + - `caseDirectService.js` + - `adminDirectService.js` +- Added focused behavioural service test pack: + - `tests/phase6/service-behaviour.test.cjs` + - mocked axios and logger checks to assert success/error contract handling for selected critical functions. + +Validation: + +- `node tests/phase6/service-parity.test.cjs` -> pass +- `node tests/phase6/service-behaviour.test.cjs` -> pass (8/8) +- `npm run lint` -> warnings only (pre-existing warning set) +- Targeted smoke/negative checks on local dev server: + - `GET /advancedsearch` and `GET /cy/advancedsearch` -> 200 + - `GET /case` and `GET /cy/case` -> 200 + - `GET /myportal` and `GET /cy/myportal` -> 307 to `/auth/signin` (unauthenticated negative path) + - `GET /api/file/getbloblistproxy?container=test&casefolderID=test` -> 400 (negative path) + - `POST /api/email/notify` with `{}` -> 400 (negative path) + - `GET /searchresults` and `/cy/searchresults` -> 500 due pre-existing SSR serialization issue (`initialState.search.searchString` undefined) + +Follow-ups: + +- Investigate/fix pre-existing `/searchresults` SSR serialization issue before broader search smoke confidence sign-off. +- Optional next increment: extend behavioural coverage to document/portal/account/notify/integration direct-service contracts. + +--- + +### CL-017: Phase 7 hardening — search SSR fix + behavioural expansion for remaining service domains + +date: 2026-03-13 +author: Cline +scope: `pages/searchresults.js`, `tests/phase7/service-behaviour.test.cjs`, `memory-bank/*` +type: change +rationale: Start Phase 7 from a new branch based on `origin/SIPS-Development`, fix known pre-existing `/searchresults` SSR serialization failure, and extend behavioural/negative-path coverage for remaining direct-service domains. +impact: Search results SSR now safely serializes when query `q` is absent; behavioural confidence improved across document/portal/account/notify/integration service contracts with no signature/return-shape changes. +status: completed + +Summary: + +- Created branch: `TASK21988a-phase7-postphase6-hardening` (from `origin/SIPS-Development`). +- Fixed pre-existing SSR issue in `pages/searchresults.js` by changing: + - `setSearch(query.q)` -> `setSearch(query?.q || "")` +- Added `tests/phase7/service-behaviour.test.cjs` with mocked-axios behavioural assertions for: + - document + - portal + - account + - notify + - integration +- Included feasible negative-path checks for relay/hash/token-sensitive behavior patterns (hashed URL composition and existing per-function error contracts). + +Validation: + +- `node tests/phase6/service-parity.test.cjs` -> pass +- `node tests/phase6/service-behaviour.test.cjs` -> pass (8/8) +- `node tests/phase7/service-behaviour.test.cjs` -> pass (10/10) +- `npm run lint` -> warnings only, no new lint errors introduced +- Manual smoke/negative-path checks (local dev on `localhost:3001` due `3000` in use): + - `/searchresults`, `/cy/searchresults` -> 200 + - `/case`, `/cy/case` -> 200 + - `/myportal`, `/cy/myportal` -> 307 to `/auth/signin` + - `/api/file/getbloblistproxy?container=test&casefolderID=test` -> 400 + - `POST /api/email/notify` with `{}` -> 400 + +Follow-ups: + +- Continue incremental hardening in sensitive relay/hash/token flows without changing external contracts. +- Keep lint warning backlog separate from this refactor stream unless explicitly scoped. + +Rollback plan: + +- Revert `pages/searchresults.js` and remove `tests/phase7/service-behaviour.test.cjs` if regression is observed. + +--- + +### CL-018: Phase 8 hardening — hash enforcement and negative-path standardization in sensitive APIs + +date: 2026-03-13 +author: Cline +scope: `pages/api/file/{deleteblobcase,deleteblobrep,createappealcompletemessage_api}.js`, `pages/api/endpoint/getportallogin_api.js`, `actions/services/{documentDirectService,portalDirectService}.js`, `tests/phase8/service-behaviour.test.cjs`, `memory-bank/*` +type: change +rationale: Deliver a small reversible hardening slice by re-enforcing hash guards in high-risk handlers, standardizing invalid-input negative paths, and reducing noisy/sensitive logging while preserving external contracts. +impact: Stronger hash/integrity guard posture and consistent 400 negative paths for invalid/missing hash and required params; no intended response-shape or signature changes. +status: completed + +Summary: + +- Started from latest `origin/SIPS-Development` on new branch `TASK22017-phase8-hardening-slice`. +- Re-enabled/enforced hash validation in: + - `deleteblobcase` + - `deleteblobrep` + - `createappealcompletemessage_api` +- Standardized early 400 handling for missing required query params where feasible: + - container/casefolder/repfile/tempcaseref/emailAddress checks as applicable +- Hardened `getportallogin_api`: + - explicit early validation for email/hash before token fetch + - removed unnecessary lodash branch complexity while preserving output behavior +- Improved logging discipline in sensitive flow: + - removed noisy `console.log` traces from `createappealcompletemessage_api` + - switched error logging to `consoleLogger` +- Added parity updates required for existing callers after hash re-enforcement: + - `documentDirectService` now appends hash on `deleteblobcase` and `deleteblobrep` + - `portalDirectService` now appends hash for `createappealcompletemessage_api` call using existing hash path contract +- Added focused phase-8 tests: + - `tests/phase8/service-behaviour.test.cjs` + - covers invalid/missing hash and missing key inputs as applicable across the 4 target handlers + - includes one valid-hash happy-path contract check for `getportallogin_api` via mocks + +Validation: + +- `node tests/phase6/service-parity.test.cjs` -> pass +- `node tests/phase6/service-behaviour.test.cjs` -> pass (8/8) +- `node tests/phase7/service-behaviour.test.cjs` -> pass (10/10) +- `node tests/phase8/service-behaviour.test.cjs` -> pass (5/5) +- `npm run lint` -> warnings only (pre-existing `react-hooks/exhaustive-deps` warnings; no new lint errors) +- Manual HTTP checks (local server `http://localhost:3001`): + - invalid/missing hash for all 4 handlers -> 400 + - missing required key params where tested -> 400 + - valid-hash spot-check (`getportallogin_api`) still returned 400 due upstream relay/CRM behavior in local env (hash gate passed, downstream dependency failed) + +Follow-ups: + +- Investigate local relay/CRM dependency behavior for `getportallogin_api` happy path in integrated environment. +- Continue incremental hash/negative-path hardening for other sensitive `pages/api/file/**` handlers still carrying commented bypass patterns. + +Rollback plan: + +- Revert the four hardened API files and the two direct-service hash append updates. +- Remove `tests/phase8/service-behaviour.test.cjs` if the full phase-8 slice is rolled back. +- Re-run phase6/phase7 baseline tests and lint to confirm rollback parity. + +--- + +### CL-019: Phase 9 hardening — additional file-handler hash enforcement slice + +date: 2026-03-13 +author: Cline +scope: `pages/api/file/{createrepcompletemessage_api,upload,uploadsinglefile,setupcontainer}.js`, `actions/services/{portalDirectService,documentDirectService}.js`, `tests/phase9/service-behaviour.test.cjs`, `memory-bank/*` +type: change +rationale: Continue incremental hardening with a small reversible slice focused on additional sensitive file handlers still showing bypassed/commented hash checks or inconsistent negative-path behavior. +impact: Stronger hash/integrity guard posture and standardized 400 negative paths for invalid/missing hash and key params; no intended signature or response-shape contract changes. +status: completed + +Summary: + +- Started from latest `origin/SIPS-Development` on branch `TASK22019-phase-9-hardening`. +- Re-enabled/enforced hash validation and early 400 guards in: + - `createrepcompletemessage_api` + - `upload` + - `uploadsinglefile` + - `setupcontainer` +- Reduced noisy logs in sensitive paths and retained structured error logging where applicable. +- Added required caller parity updates: + - `portalDirectService.sendRepCompleteMessage` now appends hash + - `documentDirectService.uploadFiles/uploadRepFiles/uploadSingleFile` now append hash +- Added focused phase-9 tests: + - `tests/phase9/service-behaviour.test.cjs` + - negative-path coverage for the 4 selected phase-9 handlers + +Validation: + +- `node tests/phase6/service-parity.test.cjs` -> pass +- `node tests/phase6/service-behaviour.test.cjs` -> pass (8/8) +- `node tests/phase7/service-behaviour.test.cjs` -> pass (10/10) +- `node tests/phase8/service-behaviour.test.cjs` -> pass (5/5) +- `node tests/phase9/service-behaviour.test.cjs` -> pass (5/5) +- `npm run lint` -> warnings only (pre-existing `react-hooks/exhaustive-deps` warnings; no new lint errors) +- Manual HTTP negative-path checks on `localhost:3000`: + - missing/invalid hash for each selected handler -> 400 + - missing required key params where tested -> 400 +- Valid-hash HTTP spot-check: + - `POST /api/file/uploadsinglefile?hash=` -> 200 (hash generated from `.env.local` key) + +Follow-ups: + +- Continue incremental hardening slices for remaining sensitive handlers with commented/bypassed hash checks. + +Rollback plan: + +- Revert the 4 hardened API handlers and 2 caller parity service files. +- Remove `tests/phase9/service-behaviour.test.cjs` if rolling back full phase-9 slice. +- Re-run phase6/7/8/9 tests and lint to confirm rollback parity. + +--- + +### CL-020: Phase 10 hardening — file retrieval handlers negative-path standardization + +date: 2026-03-13 +author: Cline +scope: `pages/api/file/{getawaitingsubmissionfromblob,getprogressobjblob,getbloblist,getrepsblob}.js`, `tests/phase10/service-behaviour.test.cjs`, `memory-bank/*` +type: change +rationale: Continue connected hardening slices on the same work-item branch, targeting additional sensitive file retrieval handlers with inconsistent early guards and legacy commented hash/debug handling. +impact: Stronger and more consistent 400 negative-path behavior for missing/invalid hash and missing key params, with no intended signature or response-shape contract changes. +status: completed + +Summary: + +- Continued from Phase 9 on branch `TASK22019-phase-9-hardening`. +- Hardened 4 additional handlers: + - `getawaitingsubmissionfromblob` + - `getprogressobjblob` + - `getbloblist` + - `getrepsblob` +- Added explicit early guards for required query/hash inputs. +- Standardized hash mismatch handling to early 400 return. +- Removed legacy commented debug blocks in touched handlers. +- Added focused phase-10 tests: + - `tests/phase10/service-behaviour.test.cjs` + - includes negative-path coverage for all 4 handlers and one valid-hash mocked happy path. + +Validation: + +- `node tests/phase6/service-parity.test.cjs` -> pass +- `node tests/phase6/service-behaviour.test.cjs` -> pass (8/8) +- `node tests/phase7/service-behaviour.test.cjs` -> pass (10/10) +- `node tests/phase8/service-behaviour.test.cjs` -> pass (5/5) +- `node tests/phase9/service-behaviour.test.cjs` -> pass (5/5) +- `node tests/phase10/service-behaviour.test.cjs` -> pass (5/5) +- `npm run lint` -> warnings only (pre-existing `react-hooks/exhaustive-deps` warnings) +- Manual HTTP checks (`localhost:3000`): + - missing hash for each selected phase-10 handler -> 400 + - valid hash `getbloblist` spot-check -> 500 due local downstream/storage dependency constraints + +Follow-ups: + +- Continue next incremental slice for remaining sensitive handlers with commented/bypassed hash checks. + +Rollback plan: + +- Revert the 4 hardened API handlers. +- Remove `tests/phase10/service-behaviour.test.cjs` if rolling back full phase-10 slice. +- Re-run phase6/7/8/9/10 tests + lint to confirm rollback parity. + +--- + +### CL-021: Phase 11 hardening — delete/bloblist hash guard standardization + +date: 2026-03-13 +author: Cline +scope: `pages/api/file/{getbloblist,deleteblob,deleteawaitingsubmissionfromblob}.js`, `tests/phase11/service-behaviour.test.cjs`, `memory-bank/*` +type: change +rationale: Continue connected hardening slices on the same work-item branch, targeting additional sensitive blob/delete handlers with inconsistent early guards and legacy commented traces. +impact: More consistent and explicit 400 negative-path handling for missing/invalid hash and key params, with no intended signature or response-shape contract changes. +status: completed + +Summary: + +- Continued on branch `TASK22019-phase-9-hardening`. +- Hardened 3 additional handlers: + - `getbloblist` + - `deleteblob` + - `deleteawaitingsubmissionfromblob` +- Added explicit early guards for required query/hash inputs. +- Standardized hash mismatch handling to early 400 return. +- Removed legacy commented debug traces in touched handlers. +- Added focused phase-11 tests: + - `tests/phase11/service-behaviour.test.cjs` + - includes negative-path coverage and one valid-hash mocked happy path. + +Validation: + +- `node tests/phase6/service-parity.test.cjs` -> pass +- `node tests/phase6/service-behaviour.test.cjs` -> pass (8/8) +- `node tests/phase7/service-behaviour.test.cjs` -> pass (10/10) +- `node tests/phase8/service-behaviour.test.cjs` -> pass (5/5) +- `node tests/phase9/service-behaviour.test.cjs` -> pass (5/5) +- `node tests/phase10/service-behaviour.test.cjs` -> pass (5/5) +- `node tests/phase11/service-behaviour.test.cjs` -> pass (4/4) +- `npm run lint` -> warnings only (pre-existing `react-hooks/exhaustive-deps` warnings) +- Manual HTTP checks (`localhost:3000`): + - missing/invalid hash for selected phase-11 handlers -> 400 + - valid-hash `getbloblist` spot-check -> 500 due local downstream/storage dependency constraints + +Follow-ups: + +- Continue next incremental slice for remaining sensitive handlers with commented/bypassed hash checks. + +Rollback plan: + +- Revert the 3 hardened API handlers. +- Remove `tests/phase11/service-behaviour.test.cjs` if rolling back full phase-11 slice. +- Re-run phase6/7/8/9/10/11 tests + lint to confirm rollback parity. + +--- + +### CL-022: Phase 12 hardening — download/proxy param and hash guard standardization + +date: 2026-03-13 +author: Cline +scope: `pages/api/file/{downloadblob,getbloblistproxy,getrepsblobproxy,getawaitingsubmissionfromblobproxy}.js`, `tests/phase12/service-behaviour.test.cjs`, `memory-bank/*` +type: change +rationale: Continue connected hardening slices on the same work-item branch, targeting additional sensitive download/proxy handlers with inconsistent early guards. +impact: More consistent and explicit 400 negative-path handling for missing/invalid hash and key params, with no intended signature or response-shape contract changes. +status: completed + +Summary: + +- Continued on branch `TASK22019-phase-9-hardening`. +- Hardened 4 additional handlers: + - `downloadblob` + - `getbloblistproxy` + - `getrepsblobproxy` + - `getawaitingsubmissionfromblobproxy` +- Added explicit early guards for required query/hash inputs. +- Standardized hash mismatch handling to early 400 return for `downloadblob`. +- Added focused phase-12 tests: + - `tests/phase12/service-behaviour.test.cjs` + - includes negative-path coverage and one valid-hash mocked happy path. + +Validation: + +- `node tests/phase6/service-parity.test.cjs` -> pass +- `node tests/phase6/service-behaviour.test.cjs` -> pass (8/8) +- `node tests/phase7/service-behaviour.test.cjs` -> pass (10/10) +- `node tests/phase8/service-behaviour.test.cjs` -> pass (5/5) +- `node tests/phase9/service-behaviour.test.cjs` -> pass (5/5) +- `node tests/phase10/service-behaviour.test.cjs` -> pass (5/5) +- `node tests/phase11/service-behaviour.test.cjs` -> pass (4/4) +- `node tests/phase12/service-behaviour.test.cjs` -> pass (4/4) +- `npm run lint` -> warnings only (pre-existing `react-hooks/exhaustive-deps` warnings) +- Manual HTTP checks (`localhost:3000`): + - missing/invalid inputs for selected phase-12 handlers -> 400 + - valid-hash `downloadblob` spot-check -> 500 due local downstream/storage dependency constraints + +Follow-ups: + +- Continue next incremental slice for remaining sensitive handlers with commented/bypassed hash checks. + +Rollback plan: + +- Revert the 4 hardened API handlers. +- Remove `tests/phase12/service-behaviour.test.cjs` if rolling back full phase-12 slice. +- Re-run phase6/7/8/9/10/11/12 tests + lint to confirm rollback parity. + +--- + +### CL-023: Phase 13 hardening — delete and involvement handler guard standardization + +date: 2026-03-13 +author: Cline +scope: `pages/api/file/{deleteblobcase,deleteblobrep,createcaseinvolvement_api,createrepinvolvement_api}.js`, `tests/phase13/service-behaviour.test.cjs`, `memory-bank/*` +type: change +rationale: Continue connected hardening slices on the same work-item branch, targeting additional sensitive delete/involvement handlers with inconsistent early guard and logging behavior. +impact: More consistent and explicit 400 negative-path handling for missing/invalid hash and required body keys, with no intended signature or response-shape contract changes. +status: completed + +Summary: + +- Continued on branch `TASK22019-phase-9-hardening`. +- Hardened 4 additional handlers: + - `deleteblobcase` + - `deleteblobrep` + - `createcaseinvolvement_api` + - `createrepinvolvement_api` +- Added explicit early guards for required hash/body inputs. +- Standardized hash mismatch handling to early 400 return for delete handlers. +- Removed noisy body/query logs from involvement handlers. +- Added focused phase-13 tests: + - `tests/phase13/service-behaviour.test.cjs` + - includes negative-path coverage and valid-input mocked happy paths. + +Validation: + +- `node tests/phase6/service-parity.test.cjs` -> pass +- `node tests/phase6/service-behaviour.test.cjs` -> pass (8/8) +- `node tests/phase7/service-behaviour.test.cjs` -> pass (10/10) +- `node tests/phase8/service-behaviour.test.cjs` -> pass (5/5) +- `node tests/phase9/service-behaviour.test.cjs` -> pass (5/5) +- `node tests/phase10/service-behaviour.test.cjs` -> pass (5/5) +- `node tests/phase11/service-behaviour.test.cjs` -> pass (4/4) +- `node tests/phase12/service-behaviour.test.cjs` -> pass (4/4) +- `node tests/phase13/service-behaviour.test.cjs` -> pass (7/7) +- `npm run lint` -> warnings only (pre-existing `react-hooks/exhaustive-deps` warnings) +- Manual HTTP checks (`localhost:3000`): + - missing/invalid inputs for selected phase-13 handlers -> 400 + - valid-hash `deleteblobcase` spot-check -> 500 due local downstream/storage dependency constraints + +Follow-ups: + +- Continue next incremental slice for remaining sensitive handlers with commented/bypassed hash checks. + +Rollback plan: + +- Revert the 4 hardened API handlers. +- Remove `tests/phase13/service-behaviour.test.cjs` if rolling back full phase-13 slice. +- Re-run phase6/7/8/9/10/11/12/13 tests + lint to confirm rollback parity. + +--- + +### CL-024: Phase 14 fix — authenticated server-side hash signing for browser callers + +date: 2026-03-13 +author: Cline +scope: `actions/services/{documentDirectService,portalDirectService}.js`, `pages/api/endpoint/gethash_api.js`, `tests/phase14/service-behaviour.test.cjs`, `memory-bank/*` +type: change +rationale: Fix runtime break in authenticated new-appeal journey where client-side hash generation attempted to use server-only `HASHKEY`. +impact: Restores browser flow while preserving hash-guard posture by moving hash generation to authenticated server endpoint; no intended external API contract changes. +status: completed + +Summary: + +- Added `gethash_api` endpoint to sign allow-listed API paths server-side. +- Endpoint enforces authenticated session and rejects unsupported paths. +- Updated direct services to fetch hash from signer endpoint for browser calls. +- Added server-side fallback hashing only when `HASHKEY` exists (non-browser contexts). +- Added phase-14 tests for missing-path, invalid-path, unauthenticated, and happy-path signer behavior. + +Validation: + +- `node tests/phase6/service-parity.test.cjs` -> pass +- `node tests/phase6/service-behaviour.test.cjs` -> pass +- `node tests/phase7/service-behaviour.test.cjs` -> pass +- `node tests/phase8/service-behaviour.test.cjs` -> pass +- `node tests/phase9/service-behaviour.test.cjs` -> pass +- `node tests/phase10/service-behaviour.test.cjs` -> pass +- `node tests/phase11/service-behaviour.test.cjs` -> pass +- `node tests/phase12/service-behaviour.test.cjs` -> pass +- `node tests/phase13/service-behaviour.test.cjs` -> pass +- `node tests/phase14/service-behaviour.test.cjs` -> pass (4/4) +- `npm run lint` -> warnings only (pre-existing warnings) + +Follow-ups: + +- Keep signer allow-list tight and expand only for explicitly required browser-side hash use cases. +- Continue removing legacy client-side direct hash assumptions as encountered. + +--- + +### CL-025: Phase 14 closeout follow-up — portal-login signer adoption + contract alignment fixes + +date: 2026-03-13 +author: Cline +scope: `actions/services/accountDirectService.js`, `pages/api/endpoint/gethash_api.js`, `tests/phase7/service-behaviour.test.cjs`, `tests/phase14/service-behaviour.test.cjs`, `pages/api/file/getrepsblob.js`, `components/elements/index.js`, `memory-bank/*` +type: change +rationale: Extend browser-safe hash signing to portal-login call path, keep signer allow-list explicit, and align an over-tightened file guard with actual endpoint contract discovered during runtime verification. +impact: Removes remaining browser-side hash risk in portal login path; preserves API response contracts while restoring route-accurate parameter validation and resolving Quill runtime import issue. +status: completed + +Summary: + +- Updated `accountDirectService.getPortalLogin` to use `/api/endpoint/gethash_api` for signed hash retrieval in browser contexts. +- Added `/api/endpoint/getportallogin_api` to signer endpoint allow-list in `gethash_api.js`. +- Updated tests to reflect signer-driven behavior and allow-list coverage: + - `tests/phase7/service-behaviour.test.cjs` + - `tests/phase14/service-behaviour.test.cjs` +- Applied follow-up runtime/contract fixes confirmed during manual verification: + - `pages/api/file/getrepsblob.js`: removed `casefolderID` hard requirement and pass-through argument to align with route usage (`container + hash`). + - `components/elements/index.js`: restored missing `updateLinks` import required by Quill-related flow. +- Branch pushed with follow-up commits: + - `6094874` + - `b8fa514` + +Validation: + +- `node tests/phase6/service-parity.test.cjs` -> pass +- `node tests/phase6/service-behaviour.test.cjs` -> pass +- `node tests/phase7/service-behaviour.test.cjs` -> pass (10/10) +- `node tests/phase8/service-behaviour.test.cjs` -> pass (5/5) +- `node tests/phase9/service-behaviour.test.cjs` -> pass (5/5) +- `node tests/phase10/service-behaviour.test.cjs` -> pass (5/5) +- `node tests/phase11/service-behaviour.test.cjs` -> pass (4/4) +- `node tests/phase12/service-behaviour.test.cjs` -> pass (4/4) +- `node tests/phase13/service-behaviour.test.cjs` -> pass (7/7) +- `node tests/phase14/service-behaviour.test.cjs` -> pass (5/5) +- `npm run lint` -> warnings only (pre-existing hook dependency warnings) + +Follow-ups: + +- Keep signer allow-list expansion minimal and task-driven. +- Continue replacing remaining browser-side direct hash assumptions only where flows require it. +- Confirm work-item/branch naming alignment for any subsequent phase slices if strict tracker continuity is required. diff --git a/memory-bank/progress.md b/memory-bank/progress.md new file mode 100644 index 00000000..8ed83923 --- /dev/null +++ b/memory-bank/progress.md @@ -0,0 +1,632 @@ +# Progress — PEDW FrontEnd + +## Completed / evidenced milestones + +- Bilingual routing structure is established with extensive Welsh rewrites and locale namespace mapping. +- Next-auth + Prisma SQL Server persistence is integrated for user/session lifecycle. +- Broad API surface exists for search, case, portal, admin, and file/document operations. +- CRM/relay hash pattern is implemented across many endpoint handlers. +- Recent shipped updates focused on: + - search/case back-navigation and breadcrumb behavior, + - DNS/view-all navigation logic, + - preferred-language handling for notify template selection, + - PDF formatting/hyperlink fixes, + - upload validation character updates. + +## Work in progress signals + +- Ongoing iterative fixes in breadcrumb/case-summary flows suggest navigation consistency is still being tuned. +- Mixed proxy/non-proxy endpoint implementations continue to evolve in parallel. +- Priority 1 refactor plan execution started for `actions/index.js` split (Phase 1 foundation complete). + +## Planned / partially implemented (inferred from code state) + +- Partial migration or coexistence of deployment/runtime patterns (Next runtime plus custom server artifacts). +- Continued refinement of document/email workflow behavior (template/language and formatting quality). +- Prioritized structural refactor backlog documented in `memory-bank/refactor-backlog.md`. + +## Known technical debt + +- `actions/services/legacyActionsService.js` still holds broad domain wrappers after Phase 1 extraction (core helpers now separated). +- Significant duplication of token/hash/relay logic across endpoint files. +- Inconsistent security/hash guard enforcement in some file routes. +- Sparse automated test coverage (`tests/` currently empty). +- Verbose logging in sensitive flows (auth/email/actions/file) increases privacy risk. + +## Latest update (2026-03-12) + +- Branch `0000-update-actions-monolith` created from development branch for Priority 1 refactor implementation. +- `actions/index.js` reduced to a compatibility barrel. +- New core helper modules added under `actions/core/`: + - `env.js` + - `logger.js` + - `hash.js` + - `token.js` + - `headers.js` +- Existing action wrappers moved to `actions/services/legacyActionsService.js` and continue to be exported via the barrel for compatibility. +- `actions/clients/` scaffolded for upcoming client extraction phase. +- Phase 2 service grouping completed with dedicated service files and `actions/services/index.js` barrel. +- Export parity check confirms grouped service modules cover all legacy exported action names. +- Phase 3 migration started by moving selected high-churn consumers and representative endpoint handlers to direct `actions/services/*` and `actions/core/*` imports. +- Phase 4 hardening baseline added with new guard helpers and redacted logging, adopted in selected notify/login/contact API handlers. +- Additional Priority 1 consumer migration pass completed for high-use portal/form/representation helpers: + - Updated direct service/core imports in: + - `components/admin/tabs/documents.js` + - `components/admin/tabs/storage.js` + - `components/admin/utils/serverside.js` + - `components/case/documents.js` + - `components/case/representation/representationComplete.js` + - `components/case/representation/representationElements.js` + - `components/elements/index.js` + - `components/myportal/awaitingsubmissionfromblob.js` + - `components/myportal/topthree.js` + - `components/myportal/topthree_reps.js` + - `components/utils/index.js` + - `lib/myportal/loadMyPortalAppealPage.js` + - `lib/newappeal/loadNewAppealPage.js` +- Additional Priority 1 consumer migration pass completed for view-all/search/unsubscribe flows: + - Updated direct service/core imports in: + - `components/myportal/viewall.js` + - `components/search/addresssearchresults.js` + - `pages/unsubscribe/[watchlistid].js` + - `pages/unsubscribeall/[watchlistid].js` +- Broad `actions` barrel import count reduced again; remaining non-comment usages are now concentrated in selected `pages/api/endpoint/*` and `pages/api/file/*` handlers. +- Additional Priority 1 endpoint/file proxy migration chunk completed: + - Updated imports in: + - `pages/api/endpoint/getmylpacases_api.js` + - `pages/api/endpoint/getbasicdnssearchpaged_api.js` + - `pages/api/endpoint/getsearchdocumentdetailspaged_api.js` + - `pages/api/endpoint/getbasicsearchpaged_api.js` + - `pages/api/file/getawaitingsubmissionfromblobproxy.js` + - `pages/api/file/getrepsblobproxy.js` + - `pages/api/file/getbloblistproxy.js` + - `pages/api/file/createappealcompletemessageproxy_api.js` + - Migrated from broad `../../../actions` imports to focused `actions/core/*` + service imports. + - Removed duplicated local hash helper in `getmylpacases_api.js` and reused `actions/core/hash`. +- Broad `actions` import scan now reports only comment-only references (no active broad imports in `.js` files). +- Lint check re-run outcome unchanged: `next lint` fails due legacy/unsupported ESLint options in repo tooling config (not introduced by this refactor pass). + +## Handover update (2026-03-13) + +- Priority 1 import-migration wave has been checked in and completed via pull request. +- Recent migration commits captured in this wave: + - `f0202bb` + - `fa98535` + - `bef8f25` +- Active broad `.../actions` imports are now eliminated from live `.js` code paths. +- Next refactor phase will begin on a **new branch** and focus on splitting `actions/services/legacyActionsService.js` into smaller direct implementation modules. +- Recommended decomposition order for the new branch: + 1. reference/search extraction + 2. portal/document extraction + 3. compatibility layer slimming + dead wrapper removal + +## Latest update (2026-03-13 — Phase 5 kickoff) + +- New branch created from `SIPS-Development`: `TASK21997-phase5-legacyactions-split`. +- Completed first Phase 5 decomposition slice from `legacyActionsService`: + - Added `actions/services/searchDirectService.js` + - Added `actions/services/referenceDataDirectService.js` + - Updated `actions/services/searchService.js` to source from `searchDirectService` + - Updated `actions/services/referenceDataService.js` to source from `referenceDataDirectService` +- Compatibility approach preserved: public grouped service exports and function signatures remain unchanged for callers. +- Validation status unchanged: + - `npm run lint` still fails due pre-existing Next/ESLint option incompatibility in repo tooling configuration. + +## Latest update (2026-03-13 — Phase 5 document slice) + +- Continued on branch `TASK21997-phase5-legacyactions-split`. +- Completed second decomposition slice from `legacyActionsService`: + - Added `actions/services/documentDirectService.js` + - Updated `actions/services/documentService.js` to source from `documentDirectService` +- Compatibility approach preserved: + - grouped service export names/signatures unchanged for consumers. +- Verification: + - service import scan now shows remaining legacy-backed grouped modules are: + - `portalService`, `accountService`, `caseService`, `adminService`, `notifyService`, `integrationService` + - `npm run lint` passes with warnings only (no blocking errors). + +## Latest update (2026-03-13 — Phase 5 completion) + +- Continued decomposition and completed all remaining slices on `TASK21997-phase5-legacyactions-split`: + - portal direct extraction + - account + case direct extraction + - admin + integration + notify direct extraction + - legacy cleanup/removal +- New direct modules now in place: + - `portalDirectService.js` + - `accountDirectService.js` + - `caseDirectService.js` + - `adminDirectService.js` + - `integrationDirectService.js` + - `notifyDirectService.js` +- `actions/services/legacyActionsService.js` removed after import parity checks. +- Validation evidence: + - `actions/services` import scan: zero `./legacyActionsService` references + - `npm run lint`: warnings only, no blocking errors + - `npm run build`: successful production build + +## Latest update (2026-03-13 — Phase 6 post-Phase-5 hardening, in progress) + +- Branch created from `SIPS-Development`: `TASK21998-phase6-postphase5-hardening`. +- Added focused parity checks for grouped/direct service stability and service index barrel stability: + - `tests/phase6/service-parity.test.cjs` + - validates grouped service import/export parity across: + - search, referenceData, document, portal, account, case, admin, integration, notify + - validates `actions/services/index.js` export list stability/order. +- Added shared service error helper: + - `actions/services/httpServiceUtils.js` + - centralizes common `consoleLogger + return error.response` and `ErrResponse` shape patterns. +- Applied low-risk duplication reduction in direct services (no signature/shape changes intended): + - `searchDirectService.js` + - `referenceDataDirectService.js` + - `caseDirectService.js` + - `adminDirectService.js` +- Logging cleanup/redaction-oriented hardening (sensitive-flow noisy logs removed): + - removed debug `console.log` statements from: + - `searchDirectService.js` + - `documentDirectService.js` + - `portalDirectService.js` + - `caseDirectService.js` + - `adminDirectService.js` + +### Validation snapshot (Phase 6) + +- `node tests/phase6/service-parity.test.cjs` -> **pass** +- `node tests/phase6/service-behaviour.test.cjs` -> **pass** (8/8) +- `npm run lint` -> **warnings only** (same pre-existing warnings) + +### Decision captured (Phase 6) + +- Agreed approach: keep parity guard tests **and** add focused behavioural unit tests in the current refactor stream (instead of deferring all behavioural checks to long-term roadmap). +- Implemented now: + - `tests/phase6/service-behaviour.test.cjs` + - mocked-axios behavioural checks for critical success/error contracts across search/reference/case/admin direct services. +- Wider behavioural expansion remains on roadmap for additional domains/functions. + +### Targeted smoke snapshot (local dev server) + +- Search flow: + - `GET /searchresults` (EN/CY) -> **500** in local env due existing serialization issue (`initialState.search.searchString` undefined in SSR payload), observed in `/tmp/phase6-dev.log`. + - `GET /advancedsearch` and `GET /cy/advancedsearch` -> **200** +- Case flow: + - `GET /case` and `GET /cy/case` -> **200** +- My Portal flow (negative-path): + - `GET /myportal` and `GET /cy/myportal` -> **307** redirect to `/auth/signin` (expected unauthenticated behavior) +- Document flow (negative-path): + - `GET /api/file/getbloblistproxy?container=test&casefolderID=test` -> **400** +- Notify flow (negative-path): + - `POST /api/email/notify` with `{}` -> **400** + +## Latest update (2026-03-13 — Phase 8 hardening slice) + +- New branch created from `origin/SIPS-Development` with work item prefix: + - `TASK22017-phase8-hardening-slice` +- Hardened four sensitive API handlers with minimal reversible guards: + - `pages/api/file/deleteblobcase.js` + - `pages/api/file/deleteblobrep.js` + - `pages/api/file/createappealcompletemessage_api.js` + - `pages/api/endpoint/getportallogin_api.js` +- Scope delivered: + - re-enabled/enforced hash checks where bypassed/commented + - standardized early 400 negative paths for missing/invalid required query inputs + - reduced noisy sensitive-path logging and routed errors through `consoleLogger` + - preserved function signatures and existing response-shape contracts +- Consumer parity updates (required to preserve behaviour after hash enforcement): + - `actions/services/documentDirectService.js` + - append hash for delete-blob-case/delete-blob-rep calls + - `actions/services/portalDirectService.js` + - append hash for create-appeal-complete-message call (hash path preserved to existing API contract) +- Added focused tests: + - `tests/phase8/service-behaviour.test.cjs` + - covers negative paths for all 4 handlers (missing/invalid hash and missing key params as applicable) + - includes one happy-path check for `getportallogin_api` with valid hash via mocked dependencies + +### Validation snapshot (Phase 8) + +- `node tests/phase6/service-parity.test.cjs` -> **pass** +- `node tests/phase6/service-behaviour.test.cjs` -> **pass** (8/8) +- `node tests/phase7/service-behaviour.test.cjs` -> **pass** (10/10) +- `node tests/phase8/service-behaviour.test.cjs` -> **pass** (5/5) +- `npm run lint` -> **warnings only** (pre-existing react-hooks dependency warnings; no new lint errors) + +### Manual HTTP checks (Phase 8) + +- Local dev server on `http://localhost:3001`. +- Negative-path checks: + - `GET /api/file/deleteblobcase?...` missing hash -> **400** + - `GET /api/file/deleteblobcase?...&hash=bad` -> **400** + - `GET /api/file/deleteblobrep?...` missing hash -> **400** + - `GET /api/file/deleteblobrep?...&hash=bad` -> **400** + - `GET /api/file/deleteblobrep?...` missing `repfile` -> **400** + - `GET /api/file/createappealcompletemessage_api?...` missing hash -> **400** + - `GET /api/file/createappealcompletemessage_api?...&hash=bad` -> **400** + - `GET /api/file/createappealcompletemessage_api?...` missing `tempcaseref` -> **400** + - `GET /api/endpoint/getportallogin_api?...` missing hash -> **400** + - `GET /api/endpoint/getportallogin_api?...&hash=bad` -> **400** + - `GET /api/endpoint/getportallogin_api?...` missing `emailAddress` -> **400** +- Valid-hash spot-check: + - `GET /api/endpoint/getportallogin_api?...&hash=` reached handler with valid hash but returned **400** from upstream relay/CRM call in local environment (expected environmental dependency risk, not hash-guard bypass). + +## Latest update (2026-03-13 — Phase 9 hardening slice) + +- New branch created from `origin/SIPS-Development` with work item prefix: + - `TASK22019-phase-9-hardening` +- Hardened four additional sensitive file handlers with minimal reversible changes: + - `pages/api/file/createrepcompletemessage_api.js` + - `pages/api/file/upload.js` + - `pages/api/file/uploadsinglefile.js` + - `pages/api/file/setupcontainer.js` +- Scope delivered: + - re-enabled/enforced hash validation where bypassed/commented + - added/standardized early 400 negative paths for missing/invalid hash and missing key parameters + - reduced noisy logging in sensitive file-upload/message paths + - preserved handler signatures and external response-shape contracts +- Consumer parity updates: + - `actions/services/portalDirectService.js` + - append hash for `createrepcompletemessage_api` call + - `actions/services/documentDirectService.js` + - append hash for `upload` and `uploadsinglefile` calls +- Added focused tests: + - `tests/phase9/service-behaviour.test.cjs` + - covers negative paths across all 4 phase-9 handlers + +### Validation snapshot (Phase 9) + +- `node tests/phase6/service-parity.test.cjs` -> **pass** +- `node tests/phase6/service-behaviour.test.cjs` -> **pass** (8/8) +- `node tests/phase7/service-behaviour.test.cjs` -> **pass** (10/10) +- `node tests/phase8/service-behaviour.test.cjs` -> **pass** (5/5) +- `node tests/phase9/service-behaviour.test.cjs` -> **pass** (5/5) +- `npm run lint` -> **warnings only** (pre-existing react-hooks warnings; no new lint errors) + +### Manual HTTP checks (Phase 9) + +- Negative-path checks on `localhost:3000`: + - `GET /api/file/createrepcompletemessage_api?...` missing hash -> **400** + - `GET /api/file/createrepcompletemessage_api?...&hash=bad` -> **400** + - `POST /api/file/upload` missing hash -> **400** + - `POST /api/file/upload?hash=bad` -> **400** + - `POST /api/file/uploadsinglefile` missing hash -> **400** + - `POST /api/file/uploadsinglefile?hash=bad` -> **400** + - `GET /api/file/setupcontainer?ident=...` missing hash -> **400** + - `GET /api/file/setupcontainer?ident=...&hash=bad` -> **400** + - `GET /api/file/setupcontainer?hash=bad` (missing ident) -> **400** +- Valid-hash happy-path spot-check: + - Completed using `.env.local` runtime key: `POST /api/file/uploadsinglefile?hash=` -> **200** + +### Rollback plan (Phase 9) + +1. Revert the four hardened API handlers. +2. Revert direct-service hash append changes in `documentDirectService` and `portalDirectService`. +3. Remove `tests/phase9/service-behaviour.test.cjs` if full slice rollback required. +4. Re-run phase6/7/8/9 baseline tests + lint after rollback. + +## Latest update (2026-03-13 — Phase 10 hardening slice) + +- Continued on branch: + - `TASK22019-phase-9-hardening` +- Hardened four additional sensitive file handlers with minimal reversible changes: + - `pages/api/file/getawaitingsubmissionfromblob.js` + - `pages/api/file/getprogressobjblob.js` + - `pages/api/file/getbloblist.js` + - `pages/api/file/getrepsblob.js` +- Scope delivered: + - added explicit early 400 handling for missing/empty required inputs (`container`, `casefolderID` where applicable, `hash`) + - standardized hash-mismatch negative-path checks to early return 400 + - removed legacy/noisy commented debug blocks from touched handlers + - preserved existing handler signatures and response-shape contracts +- Caller parity impact: + - no new caller changes required in this slice because direct-service consumers already append hash for these APIs. +- Added focused tests: + - `tests/phase10/service-behaviour.test.cjs` + - covers negative paths for all 4 selected phase-10 handlers + - includes one valid-hash contract-preserving happy-path check (`getbloblist` via mocked dependencies) + +### Validation snapshot (Phase 10) + +- `node tests/phase6/service-parity.test.cjs` -> **pass** +- `node tests/phase6/service-behaviour.test.cjs` -> **pass** (8/8) +- `node tests/phase7/service-behaviour.test.cjs` -> **pass** (10/10) +- `node tests/phase8/service-behaviour.test.cjs` -> **pass** (5/5) +- `node tests/phase9/service-behaviour.test.cjs` -> **pass** (5/5) +- `node tests/phase10/service-behaviour.test.cjs` -> **pass** (5/5) +- `npm run lint` -> **warnings only** (pre-existing react-hooks warnings; no new lint errors) + +### Manual HTTP checks (Phase 10) + +- Negative-path checks on `localhost:3000`: + - `GET /api/file/getawaitingsubmissionfromblob?container=test` (missing hash) -> **400** + - `GET /api/file/getprogressobjblob?container=test&casefolderID=case-1` (missing hash) -> **400** + - `GET /api/file/getbloblist?container=test&casefolderID=case-1` (missing hash) -> **400** + - `GET /api/file/getrepsblob?container=test&casefolderID=case-1` (missing hash) -> **400** +- Valid-hash happy-path spot-check: + - `GET /api/file/getbloblist?container=test&casefolderID=case-1&hash=` -> **500** + - expected due local storage/upstream dependency constraints, while confirming hash gate is no longer the failing check. + +### Rollback plan (Phase 10) + +1. Revert the four hardened API handlers. +2. Remove `tests/phase10/service-behaviour.test.cjs` if full phase-10 rollback required. +3. Re-run phase6/7/8/9/10 baseline tests + lint after rollback. + +## Latest update (2026-03-13 — Phase 11 hardening slice) + +- Continued on branch: + - `TASK22019-phase-9-hardening` +- Hardened three additional sensitive file handlers with minimal reversible changes: + - `pages/api/file/getbloblist.js` + - `pages/api/file/deleteblob.js` + - `pages/api/file/deleteawaitingsubmissionfromblob.js` +- Scope delivered: + - added explicit early 400 handling for missing/empty required query inputs (`container`, `casefolderID`, `blobname`, `hash` as applicable) + - standardized hash-mismatch negative-path checks to early return 400 + - removed legacy/noisy commented debug traces in touched handlers + - preserved existing handler signatures and response-shape contracts +- Caller parity impact: + - no new caller changes required in this slice; direct-service consumers already pass expected query/hash data. +- Added focused tests: + - `tests/phase11/service-behaviour.test.cjs` + - covers negative paths for selected phase-11 handlers + - includes one valid-hash contract-preserving happy-path check (`getbloblist` via mocked dependencies) + +### Validation snapshot (Phase 11) + +- `node tests/phase6/service-parity.test.cjs` -> **pass** +- `node tests/phase6/service-behaviour.test.cjs` -> **pass** (8/8) +- `node tests/phase7/service-behaviour.test.cjs` -> **pass** (10/10) +- `node tests/phase8/service-behaviour.test.cjs` -> **pass** (5/5) +- `node tests/phase9/service-behaviour.test.cjs` -> **pass** (5/5) +- `node tests/phase10/service-behaviour.test.cjs` -> **pass** (5/5) +- `node tests/phase11/service-behaviour.test.cjs` -> **pass** (4/4) +- `npm run lint` -> **warnings only** (pre-existing react-hooks warnings; no new lint errors) + +### Manual HTTP checks (Phase 11) + +- Negative-path checks on `localhost:3000`: + - `GET /api/file/getbloblist?container=test&casefolderID=case-1` (missing hash) -> **400** + - `GET /api/file/deleteblob?container=test&casefolderID=case-1&blobname=file.pdf` (missing hash) -> **400** + - `GET /api/file/deleteawaitingsubmissionfromblob?container=test&casefolderID=case-1&blobname=file.pdf` (missing hash) -> **400** + - `GET /api/file/deleteblob?container=test&casefolderID=case-1&hash=bad` (missing blobname/invalid hash) -> **400** +- Valid-hash happy-path spot-check: + - `GET /api/file/getbloblist?container=test&casefolderID=case-1&hash=` -> **500** + - expected due local storage/upstream dependency constraints, while confirming hash gate is no longer the failing check. + +### Rollback plan (Phase 11) + +1. Revert the three hardened API handlers. +2. Remove `tests/phase11/service-behaviour.test.cjs` if full phase-11 rollback required. +3. Re-run phase6/7/8/9/10/11 baseline tests + lint after rollback. + +## Latest update (2026-03-13 — Phase 12 hardening slice) + +- Continued on branch: + - `TASK22019-phase-9-hardening` +- Hardened four additional sensitive handlers with minimal reversible changes: + - `pages/api/file/downloadblob.js` + - `pages/api/file/getbloblistproxy.js` + - `pages/api/file/getrepsblobproxy.js` + - `pages/api/file/getawaitingsubmissionfromblobproxy.js` +- Scope delivered: + - added explicit early 400 handling for missing/empty required query inputs (`container`, `casefolderID`, `blobname`, `hash` as applicable) + - standardized hash-mismatch negative-path checks for `downloadblob` + - preserved existing function signatures and response-shape contracts +- Added focused tests: + - `tests/phase12/service-behaviour.test.cjs` + - covers negative paths for selected phase-12 handlers + - includes one valid-hash contract-preserving happy-path check (`downloadblob` via mocked dependencies) + +### Validation snapshot (Phase 12) + +- `node tests/phase6/service-parity.test.cjs` -> **pass** +- `node tests/phase6/service-behaviour.test.cjs` -> **pass** (8/8) +- `node tests/phase7/service-behaviour.test.cjs` -> **pass** (10/10) +- `node tests/phase8/service-behaviour.test.cjs` -> **pass** (5/5) +- `node tests/phase9/service-behaviour.test.cjs` -> **pass** (5/5) +- `node tests/phase10/service-behaviour.test.cjs` -> **pass** (5/5) +- `node tests/phase11/service-behaviour.test.cjs` -> **pass** (4/4) +- `node tests/phase12/service-behaviour.test.cjs` -> **pass** (4/4) +- `npm run lint` -> **warnings only** (pre-existing react-hooks warnings; no new lint errors) + +### Manual HTTP checks (Phase 12) + +- Negative-path checks on `localhost:3000`: + - `GET /api/file/downloadblob?container=test&casefolderID=case-1&blobname=file.pdf` (missing hash) -> **400** + - `GET /api/file/downloadblob?container=test&casefolderID=case-1&blobname=file.pdf&hash=bad` -> **400** + - `GET /api/file/getbloblistproxy?casefolderID=case-1` (missing container) -> **400** + - `GET /api/file/getrepsblobproxy` (missing container) -> **400** + - `GET /api/file/getawaitingsubmissionfromblobproxy` (missing container) -> **400** +- Valid-hash happy-path spot-check: + - `GET /api/file/downloadblob?container=test&casefolderID=case-1&blobname=file.pdf&hash=` -> **500** + - expected due local storage/upstream dependency constraints, while confirming hash gate is no longer the failing check. + +### Rollback plan (Phase 12) + +1. Revert the four hardened API handlers. +2. Remove `tests/phase12/service-behaviour.test.cjs` if full phase-12 rollback required. +3. Re-run phase6/7/8/9/10/11/12 baseline tests + lint after rollback. + +## Latest update (2026-03-13 — Phase 13 hardening slice) + +- Continued on branch: + - `TASK22019-phase-9-hardening` +- Hardened four additional sensitive handlers with minimal reversible changes: + - `pages/api/file/deleteblobcase.js` + - `pages/api/file/deleteblobrep.js` + - `pages/api/file/createcaseinvolvement_api.js` + - `pages/api/file/createrepinvolvement_api.js` +- Scope delivered: + - added explicit early 400 handling for missing/empty required query/body inputs (`hash`, `contactid`, `incidentid` as applicable) + - standardized hash-mismatch negative-path checks for delete-blob handlers + - removed noisy body/query logging in involvement handlers + - preserved existing function signatures and response-shape contracts +- Added focused tests: + - `tests/phase13/service-behaviour.test.cjs` + - covers negative paths for selected phase-13 handlers + - includes valid-hash/valid-body contract-preserving happy-path checks via mocked dependencies + +### Validation snapshot (Phase 13) + +- `node tests/phase6/service-parity.test.cjs` -> **pass** +- `node tests/phase6/service-behaviour.test.cjs` -> **pass** (8/8) +- `node tests/phase7/service-behaviour.test.cjs` -> **pass** (10/10) +- `node tests/phase8/service-behaviour.test.cjs` -> **pass** (5/5) +- `node tests/phase9/service-behaviour.test.cjs` -> **pass** (5/5) +- `node tests/phase10/service-behaviour.test.cjs` -> **pass** (5/5) +- `node tests/phase11/service-behaviour.test.cjs` -> **pass** (4/4) +- `node tests/phase12/service-behaviour.test.cjs` -> **pass** (4/4) +- `node tests/phase13/service-behaviour.test.cjs` -> **pass** (7/7) +- `npm run lint` -> **warnings only** (pre-existing react-hooks warnings; no new lint errors) + +### Manual HTTP checks (Phase 13) + +- Negative-path checks on `localhost:3000`: + - `GET /api/file/deleteblobcase?container=test&casefolderID=case-1` (missing hash) -> **400** + - `GET /api/file/deleteblobcase?container=test&casefolderID=case-1&hash=bad` -> **400** + - `GET /api/file/deleteblobrep?container=test&casefolderID=case-1&repfile=r.pdf` (missing hash) -> **400** + - `POST /api/file/createcaseinvolvement_api` with `{}` -> **400** + - `POST /api/file/createrepinvolvement_api` with `{}` -> **400** +- Valid-hash happy-path spot-check: + - `GET /api/file/deleteblobcase?container=test&casefolderID=case-1&hash=` -> **500** + - expected due local storage/upstream dependency constraints, while confirming hash gate is no longer the failing check. + +### Rollback plan (Phase 13) + +1. Revert the four hardened API handlers. +2. Remove `tests/phase13/service-behaviour.test.cjs` if full phase-13 rollback required. +3. Re-run phase6/7/8/9/10/11/12/13 baseline tests + lint after rollback. + +## Latest update (2026-03-13 — Phase 14 client-hash signing fix) + +- Continued on branch: + - `TASK22019-phase-9-hardening` +- Fixed authenticated user-journey runtime error during new appeal flow where client-side hashing tried to use server-only `HASHKEY`. +- Added authenticated server-side hash signer endpoint: + - `pages/api/endpoint/gethash_api.js` + - requires session (`getSession`), allow-lists supported API paths, returns `{ hash }`. +- Updated direct services to request hash from server signer for browser calls, with server-side fallback only when `HASHKEY` exists: + - `actions/services/documentDirectService.js` + - `actions/services/portalDirectService.js` +- Preserved existing API contracts and response shapes in file handlers. +- Added focused tests: + - `tests/phase14/service-behaviour.test.cjs` + - covers signer endpoint negative paths + authenticated happy path. + +### Validation snapshot (Phase 14) + +- `node tests/phase6/service-parity.test.cjs` -> **pass** +- `node tests/phase6/service-behaviour.test.cjs` -> **pass** (8/8) +- `node tests/phase7/service-behaviour.test.cjs` -> **pass** (10/10) +- `node tests/phase8/service-behaviour.test.cjs` -> **pass** (5/5) +- `node tests/phase9/service-behaviour.test.cjs` -> **pass** (5/5) +- `node tests/phase10/service-behaviour.test.cjs` -> **pass** (5/5) +- `node tests/phase11/service-behaviour.test.cjs` -> **pass** (4/4) +- `node tests/phase12/service-behaviour.test.cjs` -> **pass** (4/4) +- `node tests/phase13/service-behaviour.test.cjs` -> **pass** (7/7) +- `node tests/phase14/service-behaviour.test.cjs` -> **pass** (4/4) +- `npm run lint` -> **warnings only** (pre-existing hook dependency warnings) + +### Rollback plan (Phase 14) + +1. Revert `pages/api/endpoint/gethash_api.js`. +2. Revert direct-service hash signer usage in `documentDirectService.js` and `portalDirectService.js`. +3. Remove `tests/phase14/service-behaviour.test.cjs` if rolling back full phase-14 slice. +4. Re-run phase6–phase14 tests + lint. + +## Latest update (2026-03-13 — Phase 14 closeout follow-up) + +- Continued on branch: + - `TASK22019-phase-9-hardening` +- Completed follow-up hardening and contract-alignment after runtime verification: + - `actions/services/accountDirectService.js` + - `getPortalLogin` now uses authenticated signer endpoint (`/api/endpoint/gethash_api`) for browser-safe hash generation + - retains server-only fallback to local `hashAPIPath` when `HASHKEY` is available + - `pages/api/endpoint/gethash_api.js` + - allow-list expanded for `/api/endpoint/getportallogin_api` + - `pages/api/file/getrepsblob.js` + - removed enforced `casefolderID` guard and argument pass-through to align with route contract (`/api/file/getrepsblob?container=...`) + - `components/elements/index.js` + - added missing `updateLinks` import from `components/utils` to resolve Quill runtime error +- Tests updated: + - `tests/phase7/service-behaviour.test.cjs` + - updated `getPortalLogin` expectations for signer endpoint behavior + - `tests/phase14/service-behaviour.test.cjs` + - added allow-list coverage for signer path `/api/endpoint/getportallogin_api` + +### Validation snapshot (Phase 14 follow-up) + +- `node tests/phase6/service-parity.test.cjs` -> **pass** +- `node tests/phase6/service-behaviour.test.cjs` -> **pass** (8/8) +- `node tests/phase7/service-behaviour.test.cjs` -> **pass** (10/10) +- `node tests/phase8/service-behaviour.test.cjs` -> **pass** (5/5) +- `node tests/phase9/service-behaviour.test.cjs` -> **pass** (5/5) +- `node tests/phase10/service-behaviour.test.cjs` -> **pass** (5/5) +- `node tests/phase11/service-behaviour.test.cjs` -> **pass** (4/4) +- `node tests/phase12/service-behaviour.test.cjs` -> **pass** (4/4) +- `node tests/phase13/service-behaviour.test.cjs` -> **pass** (7/7) +- `node tests/phase14/service-behaviour.test.cjs` -> **pass** (5/5) +- `npm run lint` -> **warnings only** (pre-existing hook dependency warnings) + +### Risks + mitigations (follow-up) + +- Risk: signer endpoint over-expansion could broaden hash issuance. + - Mitigation: strict allow-list retained; only required `getportallogin_api` path added. +- Risk: tightened/changed guards could break existing caller contracts. + - Mitigation: `getrepsblob` guard aligned back to actual route contract; response shape unchanged. +- Risk: UI runtime dependency regressions during hardening verification. + - Mitigation: missing `updateLinks` import restored with minimal diff. + +### Rollback plan (follow-up) + +1. Revert signer-path migration commit `6094874` (or files: `accountDirectService.js`, `gethash_api.js`, `tests/phase7`, `tests/phase14`). +2. Revert contract/import alignment commit `b8fa514` (or files: `getrepsblob.js`, `components/elements/index.js`). +3. Re-run phase6–phase14 tests and lint to confirm rollback parity. + +### Rollback plan (Phase 8) + +1. Revert the four hardened API handlers. +2. Revert direct-service hash append changes in `documentDirectService` and `portalDirectService`. +3. Remove `tests/phase8/service-behaviour.test.cjs` if full slice rollback required. +4. Re-run phase6/7 baseline tests + lint after rollback. + +## Outstanding risks / gaps + +- Navigation regressions across EN/CY + route query combinations. +- Drift between rewrite config and component-level locale path logic. +- Deployment ambiguity due to multiple CI/CD artifacts (Azure pipeline, Jenkins, Docker) with unclear active source of truth. +- Security posture variability where hash checks are bypassed/commented in selected handlers. + +## Latest update (2026-03-13 — Phase 7 post-Phase-6 hardening) + +- New branch created from `origin/SIPS-Development` with required work item prefix: + - `TASK21988a-phase7-postphase6-hardening` +- Fixed known pre-existing SSR serialization issue on search results route: + - `pages/searchresults.js` + - updated SSR dispatch fallback from `setSearch(query.q)` to `setSearch(query?.q || "")` + - result: `/searchresults` and `/cy/searchresults` now return 200 in local smoke checks (no 500 observed in this run). +- Added expanded behavioural coverage for remaining direct-service domains: + - new test file: `tests/phase7/service-behaviour.test.cjs` + - includes document, portal, account, notify, integration behavioural checks + - includes negative-path assertions for relay/hash/token-sensitive behaviors where feasible: + - hashed URL append checks (`hashAPIPath`) + - error handling contracts (undefined/error string/rethrow depending on existing function contract) + +### Validation snapshot (Phase 7) + +- `node tests/phase6/service-parity.test.cjs` -> **pass** +- `node tests/phase6/service-behaviour.test.cjs` -> **pass** (8/8) +- `node tests/phase7/service-behaviour.test.cjs` -> **pass** (10/10) +- `npm run lint` -> **warnings only** (pre-existing hook dependency warnings; no new lint errors) + +### Targeted smoke snapshot (local dev server) + +- Note: dev server auto-started on `http://localhost:3001` because port `3000` was in use. +- Search flow: + - `GET /searchresults` and `GET /cy/searchresults` -> **200** +- Case flow: + - `GET /case` and `GET /cy/case` -> **200** +- My Portal flow (negative-path): + - `GET /myportal` and `GET /cy/myportal` -> **307** redirect to `/auth/signin` +- Document flow (negative-path): + - `GET /api/file/getbloblistproxy?container=test&casefolderID=test` -> **400** +- Notify flow (negative-path): + - `POST /api/email/notify` with `{}` -> **400** diff --git a/memory-bank/refactor-plan-actions-index.md b/memory-bank/refactor-plan-actions-index.md new file mode 100644 index 00000000..bc73a5bc --- /dev/null +++ b/memory-bank/refactor-plan-actions-index.md @@ -0,0 +1,747 @@ +# Refactor Plan — Priority 1 (`actions/index.js` split) + +Last updated: 2026-03-13 + +## Handover note (2026-03-13) + +- Import-migration wave from `actions` barrel to focused modules has been checked in and completed via PR. +- Next chunk will be delivered from a **new branch** and should start at legacy decomposition. + +### Next branch kickoff scope (Phase 5) + +Primary objective: split `actions/services/legacyActionsService.js` into smaller direct implementations while preserving function signatures. + +Recommended first slice: + +1. Extract reference/search internals from `legacyActionsService.js` into dedicated modules. +2. Keep `actions/services/*` exports stable and route through new direct implementations. +3. Add focused parity checks per extracted function group (inputs, headers, hash behavior, return shapes). + +Then continue with: + +4. Portal/document extraction in small batches. +5. Remove dead wrappers from `legacyActionsService.js` once call paths are fully migrated. + +### Phase 5 slice roadmap (completed) + +Status key: `[x] done`, `[ ] pending` + +1. `[x]` Search + reference direct extraction + - `searchDirectService.js`, `referenceDataDirectService.js` + - `searchService.js` / `referenceDataService.js` re-pointed +2. `[x]` Document direct extraction + - create `documentDirectService.js` + - re-point `documentService.js` + - parity checks: blob paths/hash behavior/return shapes +3. `[x]` Portal direct extraction + - create `portalDirectService.js` + - re-point `portalService.js` + - parity checks: watched-case and completion-message flows +4. `[x]` Account + case direct extraction + - create `accountDirectService.js`, `caseDirectService.js` + - re-point grouped services +5. `[x]` Notify + integration + admin direct extraction + - create `notifyDirectService.js`, `integrationDirectService.js`, `adminDirectService.js` +6. `[x]` Legacy slim-down pass + - remove dead wrappers from `legacyActionsService.js` + - keep only temporary compatibility exports still required +7. `[x]` Final cleanup + validation pass + - grep checks for remaining `./legacyActionsService` imports + - lint/manual smoke checks + +### Phase 5 completion snapshot (2026-03-13) + +- All grouped service modules now route through focused direct modules under `actions/services/*DirectService.js`. +- `actions/services/legacyActionsService.js` has been removed. +- Verification completed: + - `actions/services` scan returns zero `./legacyActionsService` imports. + - `npm run lint` completes with warnings only. + - `npm run build` completes successfully. + +### Next phase kickoff recommendation + +Focus on post-split hardening and cleanup: + +1. Add parity-focused tests for extracted direct services and `actions/services/index.js` export stability. +2. Consolidate repeated axios/error-handling patterns into shared clients/utilities where safe. +3. Reduce verbose debug logging in direct services (especially account/case/document paths) with redaction discipline. +4. Run targeted manual smoke matrix for search/case/myportal/document/notify with EN/CY parity checks. + +### Phase 6 completion snapshot (2026-03-13) + +Status key: `[x] done`, `[ ] pending` + +1. `[x]` Add parity-focused tests for extracted services and barrel stability + - `tests/phase6/service-parity.test.cjs` + - validates grouped/direct service export parity + - validates `actions/services/index.js` export stability +2. `[x]` Reduce repeated axios/error-handling patterns safely + - added shared helper: `actions/services/httpServiceUtils.js` + - adopted in selected direct modules without signature/return-shape changes +3. `[x]` Logging cleanup in sensitive direct-service flows + - removed noisy debug logs from search/document/portal/case/admin direct services +4. `[x]` Add focused behavioural contract tests in current phase + - `tests/phase6/service-behaviour.test.cjs` + - success/error contract checks with mocked axios/logger (8/8 passing) +5. `[x]` Run targeted validation/smoke checks + - parity test: pass + - behavioural test: pass + - manual smoke matrix executed (search/case/myportal/document/notify, EN/CY + negative paths) + +Notes: + +- Known pre-existing caveat during smoke checks: + - `/searchresults` and `/cy/searchresults` return 500 in local dev due SSR serialization issue (`initialState.search.searchString` undefined). + - treated as existing issue, not introduced by this refactor phase. + +### Future requirements / next-phase backlog (post-Phase-6) + +1. Investigate and fix `/searchresults` SSR serialization issue (EN/CY parity). +2. Expand behavioural service tests to remaining domains: + - document + - portal + - account + - notify + - integration +3. Add focused negative-path tests for token/hash/relay-sensitive handlers. +4. Continue small-batch hardening with reversible commits and parity checks per batch. +5. Prepare PR evidence bundle for each increment: + - lint + test commands + - behavioural/parity test outputs + - manual EN/CY + negative-path smoke matrix + +### Phase 7 completion snapshot (2026-03-13) + +Status key: `[x] done`, `[ ] pending` + +1. `[x]` Create new branch from `origin/SIPS-Development` with required work item prefix + - branch: `TASK21988a-phase7-postphase6-hardening` +2. `[x]` Fix pre-existing `/searchresults` SSR serialization issue with EN/CY parity + - updated `pages/searchresults.js`: + - `setSearch(query.q)` -> `setSearch(query?.q || "")` +3. `[x]` Expand behavioural tests to remaining service domains + - added `tests/phase7/service-behaviour.test.cjs` + - coverage includes: document, portal, account, notify, integration +4. `[x]` Add feasible negative-path tests for token/hash/relay-sensitive flows + - validated hashed path composition (e.g., `hashAPIPath` usage) + - validated existing function error contracts (undefined/error string/rethrow) +5. `[x]` Execute validation bundle + - `node tests/phase6/service-parity.test.cjs` -> pass + - `node tests/phase6/service-behaviour.test.cjs` -> pass (8/8) + - `node tests/phase7/service-behaviour.test.cjs` -> pass (10/10) + - `npm run lint` -> warnings only (no new errors) +6. `[x]` Execute targeted manual smoke matrix (EN/CY + negative paths) + - local server on `localhost:3001` (3000 occupied) + - `/searchresults`, `/cy/searchresults` -> 200 + - `/case`, `/cy/case` -> 200 + - `/myportal`, `/cy/myportal` -> 307 -> `/auth/signin` + - `/api/file/getbloblistproxy?container=test&casefolderID=test` -> 400 + - `POST /api/email/notify` with `{}` -> 400 + +### Phase 7 rollback notes + +- Revert `pages/searchresults.js` fallback change if search behaviour regresses unexpectedly. +- Remove `tests/phase7/service-behaviour.test.cjs` if test scope needs to be rolled back. +- Reset branch to pre-Phase-7 commit if full rollback required. + +### Phase 8 completion snapshot (2026-03-13) + +Status key: `[x] done`, `[ ] pending` + +1. `[x]` Create new branch from `origin/SIPS-Development` with required work item prefix + - branch: `TASK22017-phase8-hardening-slice` +2. `[x]` Re-enable/enforce hash validation in 4 sensitive target handlers + - `pages/api/file/deleteblobcase.js` + - `pages/api/file/deleteblobrep.js` + - `pages/api/file/createappealcompletemessage_api.js` + - `pages/api/endpoint/getportallogin_api.js` +3. `[x]` Standardize negative-path behavior for invalid/missing hash and required keys + - added early 400 guards for missing required query params where applicable + - preserved response-shape contracts/signatures +4. `[x]` Improve logging discipline in sensitive flow + - removed noisy direct console output in `createappealcompletemessage_api` + - routed updated error path logging via `consoleLogger` +5. `[x]` Keep caller behavior compatible after hash re-enforcement + - `actions/services/documentDirectService.js` now appends hash for: + - `deleteblobcase` + - `deleteblobrep` + - `actions/services/portalDirectService.js` now appends hash for: + - `createappealcompletemessage_api` (existing hash-path contract retained) +6. `[x]` Add focused Phase 8 tests + - added `tests/phase8/service-behaviour.test.cjs` + - includes negative-path coverage for all 4 target handlers + - includes one valid-hash happy-path contract check (`getportallogin_api`) via mocks +7. `[x]` Execute validation bundle + - `node tests/phase6/service-parity.test.cjs` -> pass + - `node tests/phase6/service-behaviour.test.cjs` -> pass (8/8) + - `node tests/phase7/service-behaviour.test.cjs` -> pass (10/10) + - `node tests/phase8/service-behaviour.test.cjs` -> pass (5/5) + - `npm run lint` -> warnings only (pre-existing react-hooks dependency warnings) +8. `[x]` Execute manual HTTP negative-path matrix + feasible happy-path spot-check + - invalid/missing hash across 4 target handlers -> 400 + - missing required params where tested -> 400 + - valid-hash `getportallogin_api` spot-check returned 400 in local env due upstream relay/CRM dependency + +### Phase 8 rollback notes + +- Revert these files to rollback the full hardening slice: + - `pages/api/file/deleteblobcase.js` + - `pages/api/file/deleteblobrep.js` + - `pages/api/file/createappealcompletemessage_api.js` + - `pages/api/endpoint/getportallogin_api.js` + - `actions/services/documentDirectService.js` + - `actions/services/portalDirectService.js` + - `tests/phase8/service-behaviour.test.cjs` +- Re-run phase6/phase7 baseline tests and lint after rollback to confirm parity. + +### Phase 9 completion snapshot (2026-03-13) + +Status key: `[x] done`, `[ ] pending` + +1. `[x]` Create new branch from `origin/SIPS-Development` with required work item prefix + - branch: `TASK22019-phase-9-hardening` +2. `[x]` Re-enable/enforce hash validation in 4 additional sensitive file handlers + - `pages/api/file/createrepcompletemessage_api.js` + - `pages/api/file/upload.js` + - `pages/api/file/uploadsinglefile.js` + - `pages/api/file/setupcontainer.js` +3. `[x]` Standardize negative-path behavior for invalid/missing hash and key params + - added early 400 handling for missing required query/body values where applicable +4. `[x]` Improve logging discipline in sensitive paths + - removed noisy direct console logging in touched handlers +5. `[x]` Keep caller behavior compatible after hash re-enforcement + - `actions/services/portalDirectService.js` + - `sendRepCompleteMessage` now appends hash + - `actions/services/documentDirectService.js` + - upload-related calls now append hash +6. `[x]` Add focused Phase 9 tests + - added `tests/phase9/service-behaviour.test.cjs` + - negative-path tests for all 4 selected handlers +7. `[x]` Execute validation bundle + - `node tests/phase6/service-parity.test.cjs` -> pass + - `node tests/phase6/service-behaviour.test.cjs` -> pass (8/8) + - `node tests/phase7/service-behaviour.test.cjs` -> pass (10/10) + - `node tests/phase8/service-behaviour.test.cjs` -> pass (5/5) + - `node tests/phase9/service-behaviour.test.cjs` -> pass (5/5) + - `npm run lint` -> warnings only (no new lint errors) +8. `[x]` Execute manual HTTP negative-path matrix + - missing/invalid hash checks across selected handlers -> 400 + - missing key params where tested -> 400 +9. `[x]` Valid-hash HTTP happy-path spot-check + - `POST /api/file/uploadsinglefile?hash=` -> 200 (hash generated from `.env.local` key) + +### Phase 9 rollback notes + +- Revert these files to rollback the full hardening slice: + - `pages/api/file/createrepcompletemessage_api.js` + - `pages/api/file/upload.js` + - `pages/api/file/uploadsinglefile.js` + - `pages/api/file/setupcontainer.js` + - `actions/services/portalDirectService.js` + - `actions/services/documentDirectService.js` + - `tests/phase9/service-behaviour.test.cjs` +- Re-run phase6/phase7/phase8/phase9 baseline tests and lint after rollback to confirm parity. + +### Phase 10 completion snapshot (2026-03-13) + +Status key: `[x] done`, `[ ] pending` + +1. `[x]` Continue connected hardening slices on same branch + - branch: `TASK22019-phase-9-hardening` +2. `[x]` Re-enable/standardize hash + param guard handling in 4 additional file retrieval handlers + - `pages/api/file/getawaitingsubmissionfromblob.js` + - `pages/api/file/getprogressobjblob.js` + - `pages/api/file/getbloblist.js` + - `pages/api/file/getrepsblob.js` +3. `[x]` Standardize negative-path behavior + - added explicit early 400 for missing required query/hash values + - standardized early 400 for hash mismatch +4. `[x]` Improve logging discipline in touched handlers + - removed old commented debug traces +5. `[x]` Add focused Phase 10 tests + - added `tests/phase10/service-behaviour.test.cjs` + - includes negative-path tests for all 4 handlers +6. `[x]` Execute validation bundle + - `node tests/phase6/service-parity.test.cjs` -> pass + - `node tests/phase6/service-behaviour.test.cjs` -> pass (8/8) + - `node tests/phase7/service-behaviour.test.cjs` -> pass (10/10) + - `node tests/phase8/service-behaviour.test.cjs` -> pass (5/5) + - `node tests/phase9/service-behaviour.test.cjs` -> pass (5/5) + - `node tests/phase10/service-behaviour.test.cjs` -> pass (5/5) + - `npm run lint` -> warnings only (no new lint errors) +7. `[x]` Execute manual HTTP negative-path matrix + valid-hash spot-check + - missing hash across selected handlers -> 400 + - valid hash `getbloblist` spot-check -> 500 (downstream/local dependency) + +### Phase 10 rollback notes + +- Revert these files to rollback the full hardening slice: + - `pages/api/file/getawaitingsubmissionfromblob.js` + - `pages/api/file/getprogressobjblob.js` + - `pages/api/file/getbloblist.js` + - `pages/api/file/getrepsblob.js` + - `tests/phase10/service-behaviour.test.cjs` +- Re-run phase6/phase7/phase8/phase9/phase10 baseline tests and lint after rollback to confirm parity. + +### Phase 11 completion snapshot (2026-03-13) + +Status key: `[x] done`, `[ ] pending` + +1. `[x]` Continue connected hardening slices on same branch + - branch: `TASK22019-phase-9-hardening` +2. `[x]` Re-enable/standardize hash + param guard handling in additional sensitive handlers + - `pages/api/file/getbloblist.js` + - `pages/api/file/deleteblob.js` + - `pages/api/file/deleteawaitingsubmissionfromblob.js` +3. `[x]` Standardize negative-path behavior + - added explicit early 400 for missing required query/hash values + - standardized early 400 for hash mismatch +4. `[x]` Improve logging discipline in touched handlers + - removed old commented debug traces +5. `[x]` Add focused Phase 11 tests + - added `tests/phase11/service-behaviour.test.cjs` + - includes negative-path tests for selected handlers +6. `[x]` Execute validation bundle + - `node tests/phase6/service-parity.test.cjs` -> pass + - `node tests/phase6/service-behaviour.test.cjs` -> pass (8/8) + - `node tests/phase7/service-behaviour.test.cjs` -> pass (10/10) + - `node tests/phase8/service-behaviour.test.cjs` -> pass (5/5) + - `node tests/phase9/service-behaviour.test.cjs` -> pass (5/5) + - `node tests/phase10/service-behaviour.test.cjs` -> pass (5/5) + - `node tests/phase11/service-behaviour.test.cjs` -> pass (4/4) + - `npm run lint` -> warnings only (no new lint errors) +7. `[x]` Execute manual HTTP negative-path matrix + valid-hash spot-check + - missing/invalid hash across selected handlers -> 400 + - valid hash `getbloblist` spot-check -> 500 (downstream/local dependency) + +### Phase 11 rollback notes + +- Revert these files to rollback the full hardening slice: + - `pages/api/file/getbloblist.js` + - `pages/api/file/deleteblob.js` + - `pages/api/file/deleteawaitingsubmissionfromblob.js` + - `tests/phase11/service-behaviour.test.cjs` +- Re-run phase6/phase7/phase8/phase9/phase10/phase11 baseline tests and lint after rollback to confirm parity. + +### Phase 12 completion snapshot (2026-03-13) + +Status key: `[x] done`, `[ ] pending` + +1. `[x]` Continue connected hardening slices on same branch + - branch: `TASK22019-phase-9-hardening` +2. `[x]` Re-enable/standardize hash + param guard handling in additional sensitive handlers + - `pages/api/file/downloadblob.js` + - `pages/api/file/getbloblistproxy.js` + - `pages/api/file/getrepsblobproxy.js` + - `pages/api/file/getawaitingsubmissionfromblobproxy.js` +3. `[x]` Standardize negative-path behavior + - added explicit early 400 for missing required query/hash values + - standardized early 400 for hash mismatch in `downloadblob` +4. `[x]` Add focused Phase 12 tests + - added `tests/phase12/service-behaviour.test.cjs` + - includes negative-path tests for selected handlers +5. `[x]` Execute validation bundle + - `node tests/phase6/service-parity.test.cjs` -> pass + - `node tests/phase6/service-behaviour.test.cjs` -> pass (8/8) + - `node tests/phase7/service-behaviour.test.cjs` -> pass (10/10) + - `node tests/phase8/service-behaviour.test.cjs` -> pass (5/5) + - `node tests/phase9/service-behaviour.test.cjs` -> pass (5/5) + - `node tests/phase10/service-behaviour.test.cjs` -> pass (5/5) + - `node tests/phase11/service-behaviour.test.cjs` -> pass (4/4) + - `node tests/phase12/service-behaviour.test.cjs` -> pass (4/4) + - `npm run lint` -> warnings only (no new lint errors) +6. `[x]` Execute manual HTTP negative-path matrix + valid-hash spot-check + - missing/invalid inputs across selected handlers -> 400 + - valid hash `downloadblob` spot-check -> 500 (downstream/local dependency) + +### Phase 12 rollback notes + +- Revert these files to rollback the full hardening slice: + - `pages/api/file/downloadblob.js` + - `pages/api/file/getbloblistproxy.js` + - `pages/api/file/getrepsblobproxy.js` + - `pages/api/file/getawaitingsubmissionfromblobproxy.js` + - `tests/phase12/service-behaviour.test.cjs` +- Re-run phase6/phase7/phase8/phase9/phase10/phase11/phase12 baseline tests and lint after rollback to confirm parity. + +### Phase 13 completion snapshot (2026-03-13) + +Status key: `[x] done`, `[ ] pending` + +1. `[x]` Continue connected hardening slices on same branch + - branch: `TASK22019-phase-9-hardening` +2. `[x]` Re-enable/standardize hash + param guard handling in additional sensitive handlers + - `pages/api/file/deleteblobcase.js` + - `pages/api/file/deleteblobrep.js` + - `pages/api/file/createcaseinvolvement_api.js` + - `pages/api/file/createrepinvolvement_api.js` +3. `[x]` Standardize negative-path behavior + - added explicit early 400 for missing required query/hash/body values + - standardized early 400 for hash mismatch in delete handlers +4. `[x]` Improve logging discipline in touched handlers + - removed noisy body/query logging in involvement handlers +5. `[x]` Add focused Phase 13 tests + - added `tests/phase13/service-behaviour.test.cjs` + - includes negative-path tests and valid-input mocked happy paths +6. `[x]` Execute validation bundle + - `node tests/phase6/service-parity.test.cjs` -> pass + - `node tests/phase6/service-behaviour.test.cjs` -> pass (8/8) + - `node tests/phase7/service-behaviour.test.cjs` -> pass (10/10) + - `node tests/phase8/service-behaviour.test.cjs` -> pass (5/5) + - `node tests/phase9/service-behaviour.test.cjs` -> pass (5/5) + - `node tests/phase10/service-behaviour.test.cjs` -> pass (5/5) + - `node tests/phase11/service-behaviour.test.cjs` -> pass (4/4) + - `node tests/phase12/service-behaviour.test.cjs` -> pass (4/4) + - `node tests/phase13/service-behaviour.test.cjs` -> pass (7/7) + - `npm run lint` -> warnings only (no new lint errors) +7. `[x]` Execute manual HTTP negative-path matrix + valid-hash spot-check + - missing/invalid inputs across selected handlers -> 400 + - valid hash `deleteblobcase` spot-check -> 500 (downstream/local dependency) + +### Phase 13 rollback notes + +- Revert these files to rollback the full hardening slice: + - `pages/api/file/deleteblobcase.js` + - `pages/api/file/deleteblobrep.js` + - `pages/api/file/createcaseinvolvement_api.js` + - `pages/api/file/createrepinvolvement_api.js` + - `tests/phase13/service-behaviour.test.cjs` +- Re-run phase6/phase7/phase8/phase9/phase10/phase11/phase12/phase13 baseline tests and lint after rollback to confirm parity. + +### Phase 14 completion snapshot (2026-03-13) + +Status key: `[x] done`, `[ ] pending` + +1. `[x]` Diagnose authenticated browser journey runtime error + - traced to client-side `hashAPIPath` usage reading server-only `HASHKEY` +2. `[x]` Implement minimal reversible signing bridge + - added `pages/api/endpoint/gethash_api.js` + - session-gated with allow-listed API path prefixes +3. `[x]` Keep client/service contracts stable + - updated `documentDirectService` and `portalDirectService` to request hash from signer endpoint + - kept server-side fallback hashing only when env key exists +4. `[x]` Add focused phase-14 tests + - added `tests/phase14/service-behaviour.test.cjs` + - covers 400/401 negative paths and authenticated happy path +5. `[x]` Execute validation bundle + - phase6–phase14 test set all pass + - lint warnings only (pre-existing) + +### Phase 14 rollback notes + +- Revert these files to rollback the fix slice: + - `pages/api/endpoint/gethash_api.js` + - `actions/services/documentDirectService.js` + - `actions/services/portalDirectService.js` + - `tests/phase14/service-behaviour.test.cjs` +- Re-run phase6–phase14 tests + lint after rollback. + +### Phase 14 closeout follow-up snapshot (2026-03-13) + +Status key: `[x] done`, `[ ] pending` + +1. `[x]` Expand signer usage to remaining browser-sensitive hashed call in account domain + - `actions/services/accountDirectService.js` + - `getPortalLogin` now requests hash from authenticated signer endpoint +2. `[x]` Extend signer allow-list minimally for required route + - `pages/api/endpoint/gethash_api.js` + - added `/api/endpoint/getportallogin_api` only +3. `[x]` Preserve and verify behavioural contract coverage + - `tests/phase7/service-behaviour.test.cjs` updated for signer flow + - `tests/phase14/service-behaviour.test.cjs` extended with allow-list path test +4. `[x]` Apply runtime-discovered contract alignment fixes (minimal/reversible) + - `pages/api/file/getrepsblob.js` + - removed unnecessary `casefolderID` requirement to match endpoint contract + - `components/elements/index.js` + - restored missing `updateLinks` import for Quill path +5. `[x]` Re-run required validation + - phase6 parity + behaviour -> pass + - phase7/8/9/10/11/12/13/14 behaviour -> pass + - lint -> warnings only (pre-existing) +6. `[x]` Push branch for review continuity + - `TASK22019-phase-9-hardening` pushed to origin + +### Phase 14 closeout follow-up rollback notes + +- Revert `6094874` to remove portal-login signer migration and tests. +- Revert `b8fa514` to remove getrepsblob contract-alignment + Quill import fix. +- Re-run phase6–phase14 tests and lint after rollback. + +## Safe execution mode for migration chunks (required) + +To reduce terminal hangs during bulk migration work, run refactor chunks in **safe stepwise mode** instead of long chained commands. + +Required command pattern: + +1. Read/inspect target files. +2. Apply edits only. +3. Verify with targeted grep for remaining broad imports. +4. Check `git status`. +5. Stage files. +6. Commit. +7. Re-run verification and then continue to the next chunk. + +Rules: + +- Do **not** combine edit + verify + add + commit + log in one long chained command. +- Keep each terminal call short and single-purpose. +- If a command is interrupted, re-check `git status` and resume from the next incomplete step. +- Preserve smallest viable diff and behavior parity. + +## Implementation status snapshot (2026-03-12) + +- Phase 1 completed: + - Extracted core helpers into `actions/core/{env,logger,hash,token,headers}.js`. + - Moved wrapper functions into `actions/services/legacyActionsService.js`. + - Reduced `actions/index.js` to compatibility barrel re-exports. + - Added `actions/clients/README.md` scaffold for upcoming client extraction. +- Validation status: + - `npm run lint` currently fails at framework/tooling option level (legacy ESLint options), not due to this refactor logic. + - Manual test matrix is required next (search/case/myportal/auth/notify/file). + +## Implementation status snapshot (Phase 2 update — 2026-03-12) + +- Phase 2 completed: + - Added grouped service modules under `actions/services/`: + - `searchService.js` + - `caseService.js` + - `accountService.js` + - `portalService.js` + - `documentService.js` + - `referenceDataService.js` + - `notifyService.js` + - `adminService.js` + - `integrationService.js` + - Added `actions/services/index.js` barrel. + - Updated `actions/index.js` to export from `./services` and `./core/*`. + - Verified export parity: all `legacyActionsService` exports are represented by grouped service modules. +- Current compatibility model: + - Consumers can continue importing from `../actions` with unchanged function names/signatures. + - `legacyActionsService` remains an internal compatibility implementation module until Phase 3 consumer migration. + +## Implementation status snapshot (Phase 3 update — 2026-03-12) + +- Phase 3 started with targeted consumer migration to focused imports. +- Updated high-churn consumers: + - `components/case/summary.js` + - `createWatchedCases`, `deleteWatchedCases`, `getWatchedCasesProxy`, `setCaseInvolvment` -> `actions/services/portalService` + - `getLinkedCases` -> `actions/services/searchService` + - `getPortalModuleDetailsProxy` -> `actions/services/caseService` + - `pages/api/email/notify.js` + - `consoleLogger` -> `actions/core/logger` + - `getPreferredLanguage` -> `actions/services/accountService` +- Updated selected endpoint handlers to core imports: + - `pages/api/endpoint/getwatchedcases_api.js` + - `pages/api/endpoint/getadvancedsearchpaged_api.js` + - `pages/api/endpoint/getpreferredlanguage_api.js` + - `pages/api/endpoint/createcrmtask_api.js` +- Compatibility remains preserved via `actions/index.js` barrel while migration proceeds incrementally. + +## Implementation status snapshot (Phase 4 update — 2026-03-12) + +- Phase 4 started with targeted hardening helpers and sensitive-path adoption. +- Added `actions/core/guards.js` with: + - `isNonEmptyString` + - `sanitizeString` + - `escapeODataString` +- Expanded `actions/core/logger.js` with redaction support: + - `redactSensitive` + - masking for likely email/token/secret patterns in logged content +- Updated `actions/index.js` compatibility barrel to export `./core/guards`. +- Applied hardening in selected sensitive/high-churn handlers: + - `pages/api/email/notify.js` + - email input sanitization + required-field guard + - redacted logging for outbound payload diagnostics + - `pages/api/endpoint/getpreferredlanguage_api.js` + - sanitized/validated `emailAddress` + - OData string escaping for query construction + - redacted query logging + - `pages/api/endpoint/createcrmtask_api.js` + - sanitized/validated request inputs for subject/email/body + - centralized structured error logging via `consoleLogger` + +## Implementation status snapshot (Priority 1 execution pass — 2026-03-12) + +- Completed another targeted consumer migration pass from broad `actions` barrel imports to focused service/core imports in these files: + - `components/admin/tabs/documents.js` + - `components/admin/tabs/storage.js` + - `components/admin/utils/serverside.js` + - `components/case/documents.js` + - `components/case/representation/representationComplete.js` + - `components/case/representation/representationElements.js` + - `components/elements/index.js` + - `components/myportal/awaitingsubmissionfromblob.js` + - `components/myportal/topthree.js` + - `components/myportal/topthree_reps.js` + - `components/utils/index.js` + - `lib/myportal/loadMyPortalAppealPage.js` + - `lib/newappeal/loadNewAppealPage.js` +- Kept compatibility behavior via existing `actions/index.js` barrel. +- Validation note: `npm run lint` remains blocked by repository ESLint/Next option incompatibility (pre-existing tooling configuration). + +## Goal + +Reduce coupling and regression risk by splitting `actions/index.js` into focused modules while preserving existing behavior and call signatures during migration. + +## Why this is first + +`actions/index.js` is currently a high-risk hotspot: API wrappers, relay/hash helpers, token acquisition, logging, file helpers, and notification helpers are mixed in one module. This increases blast radius for every change. + +## Scope (planned) + +- In scope: + - Module extraction and internal architecture cleanup. + - Backward-compatible export strategy. + - Migration plan for consumers under `components/**`, `pages/**`, and `pages/api/**`. +- Out of scope (for this refactor phase): + - Functional changes to business logic. + - Contract changes to API handlers. + - Dependency swaps. + +## Proposed target structure + +```text +actions/ + index.js # compatibility barrel (temporary) + core/ + env.js # BASE_URL/API_ROOT/relay roots + logger.js # consoleLogger/conLog with redaction helpers + hash.js # hashAPIPath/hashString/dehashString + token.js # getToken + token request config + headers.js # azureHeaders* helpers + clients/ + relayClient.js # signed relay calls + shared axios behavior + endpointClient.js # endpoint route wrappers + fileClient.js # file route wrappers + notifyClient.js # email/notify wrappers + services/ + caseService.js + searchService.js + portalService.js + accountService.js + documentService.js +``` + +Notes: + +- Keep `actions/index.js` as a façade initially to avoid breaking imports. +- Move internals first, then update call sites incrementally. + +## Migration strategy (phased) + +### Phase 1 — Safe extraction with no consumer changes + +1. Create new modules under `actions/core/**` and `actions/clients/**`. +2. Move utility functions (`hash`, `headers`, `token`, `logger`, env helpers). +3. Re-export all existing functions from `actions/index.js` unchanged. +4. Verify parity with lint + smoke checks. + +### Phase 2 — Service grouping + +1. Group route wrapper functions into service modules by domain (case/search/portal/account/document). +2. Keep identical function names/signatures. +3. Add thin unit tests for pure helpers first (`hash`, `env`, route builder helpers). + +### Phase 3 — Consumer migration + +1. Replace broad imports from `actions/index.js` with targeted imports from new modules. +2. Migrate high-churn areas first: + - `components/breadcrumbs.js` + - `components/case/summary.js` + - `pages/api/email/notify.js` + - selected `pages/api/endpoint/**` handlers +3. Keep index barrel until migration completion. + +### Phase 4 — Harden + slim + +1. Introduce typed/validated request helper boundaries (JS doc + runtime guard checks). +2. Remove dead exports and duplicate wrappers. +3. Finalize redacted logging policy in sensitive flows. + +## Backward compatibility rules + +- Do not change public function names/signatures during Phases 1–2. +- Keep return shapes identical unless explicitly planned and validated. +- Preserve existing hash and relay behavior contract. +- Preserve EN/CY behavior where helper functions influence locale-sensitive flows. + +## Validation plan + +- `npm run lint` +- Manual smoke paths: + - search -> results -> case summary -> back nav + - myportal viewall -> case summary -> breadcrumb return path + - auth email sign-in flow + - notify new case reference language selection path + - file/document fetch paths touched by moved wrappers +- Negative-path checks: + - invalid hash or malformed query for sensitive handlers + - token acquisition failures and relay timeout handling + +## Regression tests and coverage plan (to add during implementation) + +### Test tooling recommendation + +Current repo has no active automated test runner configured in `package.json`. For this refactor, introduce a minimal unit test setup (recommended: Jest) focused on pure logic first. + +Suggested scripts: + +- `test`: run all unit tests +- `test:watch`: local watch mode +- `test:coverage`: coverage output for CI and PR evidence + +### Minimum tests for Priority 1 + +1. `actions/core/hash.js` + - deterministic hash for known input + - query string handling (`?hash=` vs `&hash=`) + - malformed/edge inputs +2. `actions/core/headers.js` + - expected OData/auth headers produced for each helper +3. `actions/core/token.js` + - token request config generation + - success and error mapping behavior (mock axios) +4. `actions/core/env.js` + - server vs browser base URL resolution +5. compatibility barrel (`actions/index.js`) + - exports parity test to ensure existing function names remain available during migration + +### Integration-style safeguards (mocked external calls) + +- Relay client request assembly test: + - signed URL includes hash + - token is attached + - timeout/error behavior is consistent +- Notify client wrapper test: + - language/template routing remains unchanged for PEDW-NEW-CASEREF path + +### Coverage targets for this refactor + +- `actions/core/**`: >= 90% lines/functions +- `actions/clients/**`: >= 80% lines/functions +- Global coverage gate for this phase: >= 70% (new tests only; avoid blocking unrelated legacy code) + +### Regression gate in PR + +Required evidence for each phase: + +1. `npm run lint` +2. `npm run test` +3. `npm run test:coverage` (attach summary) +4. Manual smoke matrix from this plan (search/case/myportal/auth/notify/file) + +## Risks and mitigations + +- Risk: import breakage due to broad existing usage. + - Mitigation: keep compatibility barrel and migrate in small batches. +- Risk: hidden behavior differences from refactor-only moves. + - Mitigation: freeze signatures + add helper tests + run route smoke matrix each phase. +- Risk: sensitive logging leakage while touching shared helpers. + - Mitigation: centralize logger early and enforce redaction helper. + +## Definition of done (for this priority) + +1. `actions/index.js` reduced to compatibility exports only (minimal logic). +2. Core helper modules and service modules exist and are used by migrated consumers. +3. Lint and manual validation matrix pass. +4. Memory docs updated (`activeContext`, `progress`, `change-log`). diff --git a/pages/api/endpoint/gethash_api.js b/pages/api/endpoint/gethash_api.js new file mode 100644 index 00000000..5927cf87 --- /dev/null +++ b/pages/api/endpoint/gethash_api.js @@ -0,0 +1,36 @@ +import { hashAPIPath } from "../../../actions/core/hash"; +import nextConnect from "next-connect"; +import { getSession } from "next-auth/react"; + +const ApiProxy = nextConnect(); + +ApiProxy.get(async (req, res) => { + const session = await getSession({ req }); + + if (!session) { + return res.status(401).json(); + } + + const queryPath = req.query.path; + const allowedPrefix = [ + "/api/endpoint/getportallogin_api", + "/api/file/upload", + "/api/file/uploadsinglefile", + "/api/file/createrepcompletemessage_api", + "/api/file/createappealcompletemessage_api" + ]; + + if ( + typeof queryPath !== "string" || + queryPath.length === 0 || + !queryPath.startsWith("/api/") || + !allowedPrefix.some((prefix) => queryPath.startsWith(prefix)) || + queryPath.includes("/api/endpoint/gethash_api") + ) { + return res.status(400).json(); + } + + return res.status(200).json({ hash: hashAPIPath(queryPath) }); +}); + +export default ApiProxy; diff --git a/pages/api/file/createcaseinvolvement_api.js b/pages/api/file/createcaseinvolvement_api.js index 2fcc597b..f3553eb0 100644 --- a/pages/api/file/createcaseinvolvement_api.js +++ b/pages/api/file/createcaseinvolvement_api.js @@ -34,6 +34,16 @@ const hashAPIPath = (queryPath) => { }; export default async function ApiProxy(req, res) { + if ( + !req.body || + typeof req.body.contactid === "undefined" || + req.body.contactid.length === 0 || + typeof req.body.incidentid === "undefined" || + req.body.incidentid.length === 0 + ) { + return res.status(400).json(); + } + var token = await getToken(); var contactid = req.body.contactid; var queryUrl = @@ -41,8 +51,6 @@ export default async function ApiProxy(req, res) { req.body.incidentid + ")/pinswg_incident_contact_case_involvement/$ref"; - console.log(req.body, queryUrl); - var crmUrl = "https://" + process.env.CRMURL; var data = { diff --git a/pages/api/file/createrepcompletemessage_api.js b/pages/api/file/createrepcompletemessage_api.js index e55ac766..1f30911a 100644 --- a/pages/api/file/createrepcompletemessage_api.js +++ b/pages/api/file/createrepcompletemessage_api.js @@ -2,6 +2,8 @@ import { getBlobs, createRepCompleteMessage } from "../../../actions/azurestorage"; +import { hashAPIPath } from "../../../actions/core/hash"; +import { consoleLogger } from "../../../actions/core/logger"; import nextConnect from "next-connect"; import middleware from "../middleware/middleware"; @@ -13,26 +15,41 @@ ApiProxy.get(async (req, res) => { var containerName = req.query.container; var tempCaseRef = req.query.tempcaseref; var filename = req.query.repid; + var checkHash = req.query.hash; - //console.log(hashAPIPath(checkquerypath), checkHash); - //console.log(hashAPIPath(checkquerypath) == "&hash=" + checkHash); + if ( + typeof containerName === "undefined" || + containerName.length === 0 || + typeof tempCaseRef === "undefined" || + tempCaseRef.length === 0 || + typeof filename === "undefined" || + filename.length === 0 || + typeof checkHash === "undefined" || + checkHash.length === 0 + ) { + return res.status(400).json(); + } - //if (hashAPIPath(checkquerypath) == "&hash=" + checkHash) { - await createRepCompleteMessage(containerName, tempCaseRef, filename).then( - (data) => { - console.log( - "/////Create Rep complete Message:\n" + tempCaseRef, - "\n" + "insertedOn:" + data.insertedOn, - "\n" + "messageId:" + data.messageId, - "\n" + "response.status:" + data._response.status, - "\n//////////////" - ); + var checkquerypath = + "/api/file/createrepcompletemessage_api?container=" + + containerName + + "&tempcaseref=" + + tempCaseRef + + "&repid=" + + filename; + + if (hashAPIPath(checkquerypath) != "&hash=" + checkHash) { + return res.status(400).json(); + } + + await createRepCompleteMessage(containerName, tempCaseRef, filename) + .then((data) => { return res.status(200).json(data); - } - ); - // } else { - // return res.status(400).json(); - // } + }) + .catch((error) => { + consoleLogger(error); + return res.status(400).json(); + }); }); export const config = { diff --git a/pages/api/file/createrepinvolvement_api.js b/pages/api/file/createrepinvolvement_api.js index 3fe378d9..90732604 100644 --- a/pages/api/file/createrepinvolvement_api.js +++ b/pages/api/file/createrepinvolvement_api.js @@ -34,6 +34,16 @@ const hashAPIPath = (queryPath) => { }; export default async function ApiProxy(req, res) { + if ( + !req.body || + typeof req.body.contactid === "undefined" || + req.body.contactid.length === 0 || + typeof req.body.incidentid === "undefined" || + req.body.incidentid.length === 0 + ) { + return res.status(400).json(); + } + var token = await getToken(); var contactid = req.body.contactid; var queryUrl = @@ -41,8 +51,6 @@ export default async function ApiProxy(req, res) { req.body.incidentid + ")/pinswg_incident_contact_case_involvement/$ref"; - console.log(req.body, queryUrl); - var crmUrl = "https://" + process.env.CRMURL; var crmVersion = process.env.CRMURL_VERSION; diff --git a/pages/api/file/deleteawaitingsubmissionfromblob.js b/pages/api/file/deleteawaitingsubmissionfromblob.js index dd459590..33dfe05b 100644 --- a/pages/api/file/deleteawaitingsubmissionfromblob.js +++ b/pages/api/file/deleteawaitingsubmissionfromblob.js @@ -13,6 +13,19 @@ ApiProxy.get(async (req, res) => { var blobName = req.query.blobname; var checkHash = req.query.hash; + if ( + typeof containerName === "undefined" || + containerName.length === 0 || + typeof casefolderID === "undefined" || + casefolderID.length === 0 || + typeof blobName === "undefined" || + blobName.length === 0 || + typeof checkHash === "undefined" || + checkHash.length === 0 + ) { + return res.status(400).json(); + } + var checkquerypath = "/api/file/deleteblob?container=" + containerName + @@ -21,8 +34,9 @@ ApiProxy.get(async (req, res) => { "&blobname=" + blobName; - //console.log(hashAPIPath(checkquerypath), checkHash); - //console.log(hashAPIPath(checkquerypath) == "&hash=" + checkHash); + if (hashAPIPath(checkquerypath) != "&hash=" + checkHash) { + return res.status(400).json(); + } if (hashAPIPath(checkquerypath) == "&hash=" + checkHash) { await deleteBlob( @@ -31,8 +45,6 @@ ApiProxy.get(async (req, res) => { ).then((data) => { return res.status(200).json({ data: data }); }); - } else { - return res.status(400).json(); } }); diff --git a/pages/api/file/deleteblob.js b/pages/api/file/deleteblob.js index 2a57c3b6..9d99e045 100644 --- a/pages/api/file/deleteblob.js +++ b/pages/api/file/deleteblob.js @@ -13,6 +13,19 @@ ApiProxy.get(async (req, res) => { var blobName = req.query.blobname; var checkHash = req.query.hash; + if ( + typeof containerName === "undefined" || + containerName.length === 0 || + typeof casefolderID === "undefined" || + casefolderID.length === 0 || + typeof blobName === "undefined" || + blobName.length === 0 || + typeof checkHash === "undefined" || + checkHash.length === 0 + ) { + return res.status(400).json(); + } + var checkquerypath = "/api/file/deleteblob?container=" + containerName + @@ -21,6 +34,10 @@ ApiProxy.get(async (req, res) => { "&blobname=" + encodeURIComponent(blobName); + if (hashAPIPath(checkquerypath) != "&hash=" + checkHash) { + return res.status(400).json(); + } + if (hashAPIPath(checkquerypath) == "&hash=" + checkHash) { await deleteBlob( containerName, @@ -28,8 +45,6 @@ ApiProxy.get(async (req, res) => { ).then((data) => { return res.status(200).json({ data: data }); }); - } else { - return res.status(400).json(); } }); diff --git a/pages/api/file/deleteblobcase.js b/pages/api/file/deleteblobcase.js index bedd0fb0..39e2cfde 100644 --- a/pages/api/file/deleteblobcase.js +++ b/pages/api/file/deleteblobcase.js @@ -16,7 +16,9 @@ ApiProxy.get(async (req, res) => { typeof containerName === "undefined" || containerName.length === 0 || typeof casefolderID === "undefined" || - casefolderID.length === 0 + casefolderID.length === 0 || + typeof checkHash === "undefined" || + checkHash.length === 0 ) { return res.status(400).json(); } @@ -27,12 +29,14 @@ ApiProxy.get(async (req, res) => { "&casefolderID=" + casefolderID; + if (hashAPIPath(checkquerypath) != "&hash=" + checkHash) { + return res.status(400).json(); + } + if (hashAPIPath(checkquerypath) == "&hash=" + checkHash) { await deleteBlobCase(containerName, casefolderID).then((data) => { return res.status(200).json({ data: data }); }); - } else { - return res.status(400).json(); } }); diff --git a/pages/api/file/deleteblobrep.js b/pages/api/file/deleteblobrep.js index 271cc21d..5d55a259 100644 --- a/pages/api/file/deleteblobrep.js +++ b/pages/api/file/deleteblobrep.js @@ -19,7 +19,9 @@ ApiProxy.get(async (req, res) => { typeof casefolderID === "undefined" || casefolderID.length === 0 || typeof repfile === "undefined" || - repfile.length === 0 + repfile.length === 0 || + typeof checkHash === "undefined" || + checkHash.length === 0 ) { return res.status(400).json(); } @@ -32,14 +34,16 @@ ApiProxy.get(async (req, res) => { "&repfile=" + repfile; + if (hashAPIPath(checkquerypath) != "&hash=" + checkHash) { + return res.status(400).json(); + } + casefolderID = casefolderID + "/" + repfile; if (hashAPIPath(checkquerypath) == "&hash=" + checkHash) { await deleteBlobRep(containerName, casefolderID).then((data) => { return res.status(200).json({ data: data }); }); - } else { - return res.status(400).json(); } }); diff --git a/pages/api/file/downloadblob.js b/pages/api/file/downloadblob.js index fe2ab520..9cbe11f4 100644 --- a/pages/api/file/downloadblob.js +++ b/pages/api/file/downloadblob.js @@ -16,6 +16,19 @@ ApiProxy.get(async (req, res) => { var blobName = req.query.blobname; var checkHash = req.query.hash; + if ( + typeof containerName === "undefined" || + containerName.length === 0 || + typeof casefolderID === "undefined" || + casefolderID.length === 0 || + typeof blobName === "undefined" || + blobName.length === 0 || + typeof checkHash === "undefined" || + checkHash.length === 0 + ) { + return res.status(400).json(); + } + var checkquerypath = "/api/file/downloadblob?container=" + containerName + @@ -24,6 +37,10 @@ ApiProxy.get(async (req, res) => { "&blobname=" + blobName.trim(); + if (hashAPIPath(checkquerypath) != "&hash=" + checkHash) { + return res.status(400).json(); + } + if (hashAPIPath(checkquerypath) == "&hash=" + checkHash) { const bloblocation = casefolderID + (blobName.indexOf(".json") > 0 ? "/" : "/files/"); @@ -38,8 +55,6 @@ ApiProxy.get(async (req, res) => { "attachment; filename=" + decodeURI(blobName) ); return res.status(200).send(downloaded); - } else { - return res.status(400).json(); } }); diff --git a/pages/api/file/getawaitingsubmissionfromblob.js b/pages/api/file/getawaitingsubmissionfromblob.js index 86f0a0f0..c1949040 100644 --- a/pages/api/file/getawaitingsubmissionfromblob.js +++ b/pages/api/file/getawaitingsubmissionfromblob.js @@ -14,16 +14,21 @@ ApiProxy.get(async (req, res) => { var containerName = req.query.container; var checkHash = req.query.hash; + if ( + typeof containerName === "undefined" || + containerName.length === 0 || + typeof checkHash === "undefined" || + checkHash.length === 0 + ) { + return res.status(400).json(); + } + var checkquerypath = "/api/file/getawaitingsubmissionfromblob?container=" + containerName; - // console.log( - // "///////////////////////\ngetawaitingsubmissionblob url :", - // req.url, - // "\n///////////////////////\n" - // ); - //console.log(hashAPIPath(checkquerypath), checkHash); - //console.log(hashAPIPath(checkquerypath) == "&hash=" + checkHash); + if (hashAPIPath(checkquerypath) != "&hash=" + checkHash) { + return res.status(400).json(); + } if (hashAPIPath(checkquerypath) == "&hash=" + checkHash) { const blobObj = await getAllProgressBlobs(containerName) @@ -33,8 +38,6 @@ ApiProxy.get(async (req, res) => { .then((data) => { return res.status(200).json(data); }); - } else { - return res.status(400).json(); } }); diff --git a/pages/api/file/getawaitingsubmissionfromblobproxy.js b/pages/api/file/getawaitingsubmissionfromblobproxy.js index f7f1b2f8..2e05852a 100644 --- a/pages/api/file/getawaitingsubmissionfromblobproxy.js +++ b/pages/api/file/getawaitingsubmissionfromblobproxy.js @@ -23,6 +23,11 @@ const BASE_URL = process.env.API_ROOT || `http://localhost:${port}`; export default async function ApiProxy(req, res) { var containerName = req.query.container; var checkHash = req.query.hash; + + if (typeof containerName === "undefined" || containerName.length === 0) { + return res.status(400).json(); + } + var token = await getToken(); var queryUrl = diff --git a/pages/api/file/getbloblist.js b/pages/api/file/getbloblist.js index 0154b8fa..a36baa52 100644 --- a/pages/api/file/getbloblist.js +++ b/pages/api/file/getbloblist.js @@ -43,18 +43,27 @@ ApiProxy.get(async (req, res) => { var casefolderID = req.query.casefolderID; var checkHash = req.query.hash; + + if ( + typeof containerName === "undefined" || + containerName.length === 0 || + typeof casefolderID === "undefined" || + casefolderID.length === 0 || + typeof checkHash === "undefined" || + checkHash.length === 0 + ) { + return res.status(400).json(); + } + var checkquerypath = "/api/file/getbloblist?container=" + containerName + "&casefolderID=" + casefolderID; - // console.log("-----", casefolderID); - // console.log("-----", req.query); - // console.log("-----", checkquerypath); - // console.log("-----", hashAPIPath(checkquerypath)); - // console.log("-----", checkHash); - // console.log(hashAPIPath(checkquerypath) == "&hash=" + checkHash); + if (hashAPIPath(checkquerypath) != "&hash=" + checkHash) { + return res.status(400).json(); + } if (hashAPIPath(checkquerypath) == "&hash=" + checkHash) { casefolderID.split("/").length > 1 @@ -68,8 +77,6 @@ ApiProxy.get(async (req, res) => { : await getBlobs(containerName, casefolderID).then((data) => { return res.status(200).json({ "value": [data] }); }); - } else { - return res.status(400).json(); } }); diff --git a/pages/api/file/getbloblistproxy.js b/pages/api/file/getbloblistproxy.js index 5e9f66e3..8032d1c8 100644 --- a/pages/api/file/getbloblistproxy.js +++ b/pages/api/file/getbloblistproxy.js @@ -24,6 +24,16 @@ export default async function ApiProxy(req, res) { var containerName = req.query.container; var casefolderID = req.query.casefolderID; var checkHash = req.query.hash; + + if ( + typeof containerName === "undefined" || + containerName.length === 0 || + typeof casefolderID === "undefined" || + casefolderID.length === 0 + ) { + return res.status(400).json(); + } + var token = await getToken(); var queryUrl = diff --git a/pages/api/file/getprogressobjblob.js b/pages/api/file/getprogressobjblob.js index aac8bfd5..8f5ef041 100644 --- a/pages/api/file/getprogressobjblob.js +++ b/pages/api/file/getprogressobjblob.js @@ -15,23 +15,31 @@ ApiProxy.get(async (req, res) => { var casefolderID = req.query.casefolderID; var checkHash = req.query.hash; + + if ( + typeof containerName === "undefined" || + containerName.length === 0 || + typeof casefolderID === "undefined" || + casefolderID.length === 0 || + typeof checkHash === "undefined" || + checkHash.length === 0 + ) { + return res.status(400).json(); + } + var checkquerypath = "/api/file/getprogressobjblob?container=" + containerName + "&casefolderID=" + casefolderID; - // console.log("-----", casefolderID); - // console.log("-----", req.query); - // console.log("-----", checkquerypath); - // console.log("-----", hashAPIPath(checkquerypath)); - // console.log("-----", checkHash); - // console.log(hashAPIPath(checkquerypath) == "&hash=" + checkHash); + if (hashAPIPath(checkquerypath) != "&hash=" + checkHash) { + return res.status(400).json(); + } if (hashAPIPath(checkquerypath) == "&hash=" + checkHash) { const blobObj = await getProgressBlobs(containerName, casefolderID) .then((data) => { - //console.log("Progress blob path:", data.path); return downloadProgressFile( containerName, data.path, @@ -41,8 +49,6 @@ ApiProxy.get(async (req, res) => { .then((data) => { return res.status(200).json(data); }); - } else { - return res.status(400).json(); } }); diff --git a/pages/api/file/getrepsblob.js b/pages/api/file/getrepsblob.js index 51bf1df3..a7177949 100644 --- a/pages/api/file/getrepsblob.js +++ b/pages/api/file/getrepsblob.js @@ -47,17 +47,24 @@ ApiProxy.get(async (req, res) => { var casefolderID = req.query.casefolderID; var checkHash = req.query.hash; + + if ( + typeof containerName === "undefined" || + containerName.length === 0 || + typeof checkHash === "undefined" || + checkHash.length === 0 + ) { + return res.status(400).json(); + } + var checkquerypath = "/api/file/getrepsblob?container=" + containerName; - //console.log("-----", casefolderID); - //console.log("-----", req.query); - //console.log("-----", checkquerypath); - //console.log("-----", hashAPIPath(checkquerypath)); - //console.log("-----", checkHash); - //console.log(hashAPIPath(checkquerypath) == "&hash=" + checkHash); + if (hashAPIPath(checkquerypath) != "&hash=" + checkHash) { + return res.status(400).json(); + } if (hashAPIPath(checkquerypath) == "&hash=" + checkHash) { - const blobObj = await getRepsBlobs(containerName, casefolderID) + const blobObj = await getRepsBlobs(containerName) .then(async (data) => { return data; }) @@ -72,8 +79,6 @@ ApiProxy.get(async (req, res) => { .catch((error) => { consoleLogger(error); }); - } else { - return res.status(400).json(); } }); diff --git a/pages/api/file/getrepsblobproxy.js b/pages/api/file/getrepsblobproxy.js index 0b199f1b..7b377b2d 100644 --- a/pages/api/file/getrepsblobproxy.js +++ b/pages/api/file/getrepsblobproxy.js @@ -23,6 +23,11 @@ const BASE_URL = process.env.API_ROOT || `http://localhost:${port}`; export default async function ApiProxy(req, res) { var containerName = req.query.container; var checkHash = req.query.hash; + + if (typeof containerName === "undefined" || containerName.length === 0) { + return res.status(400).json(); + } + var token = await getToken(); var queryUrl = "/api/file/getrepsblob?container=" + containerName; diff --git a/pages/api/file/setupcontainer.js b/pages/api/file/setupcontainer.js index 86aa9bf0..1d251c2f 100644 --- a/pages/api/file/setupcontainer.js +++ b/pages/api/file/setupcontainer.js @@ -17,10 +17,17 @@ ApiProxy.use(middleware); ApiProxy.get(async (req, res) => { var containerName = req.query.ident; var checkHash = req.query.hash; - var checkquerypath = "/api/file/setupcontainer?ident=" + containerName; - //console.log(checkquerypath, hashAPIPath(checkquerypath), checkHash); - //console.log(hashAPIPath(checkquerypath) == "&hash=" + checkHash); + if ( + typeof containerName === "undefined" || + containerName.length === 0 || + typeof checkHash === "undefined" || + checkHash.length === 0 + ) { + return res.status(400).json(); + } + + var checkquerypath = "/api/file/setupcontainer?ident=" + containerName; if (hashAPIPath(checkquerypath) == "&hash=" + checkHash) { await createContainer(containerName) @@ -32,11 +39,6 @@ ApiProxy.get(async (req, res) => { res.status(400).json(error); }); } else { - consoleLogger({ - name: "setupcontainer", - code: "bad hash", - query: JSON.stringify(req.query) - }); return res.status(400).json(); } }); diff --git a/pages/api/file/upload.js b/pages/api/file/upload.js index 4219319a..2d8afbd2 100644 --- a/pages/api/file/upload.js +++ b/pages/api/file/upload.js @@ -1,8 +1,9 @@ import { createBlob, createRepBlob, - uploadFile, + uploadFile } from "../../../actions/azurestorage"; +import { hashAPIPath } from "../../../actions/core/hash"; import nextConnect from "next-connect"; import middleware from "../middleware/middleware"; @@ -12,26 +13,26 @@ ApiProxy.use(middleware); ApiProxy.post(async (req, res) => { var checkHash = req.query.hash; - //console.log(JSON.stringify(req.body)); - //console.log(JSON.stringify(req.body.appealData)); - //console.log(req.files); + + var checkquerypath = "/api/file/upload"; + + if (hashAPIPath(checkquerypath) != "?hash=" + checkHash) { + return res.status(400).json(); + } const appealData = req.body.appealData; - const containerID = req.body.containerID[0]; - const casefolderID = req.body.casefolderID[0]; + const containerID = req.body?.containerID?.[0]; + const casefolderID = req.body?.casefolderID?.[0]; const repOrAppeal = req.body.repOrAppeal || false; - console.log("there are files:", Object.keys(req.files).length); - - // var checkquerypath = "/api/file/upload"; - - //console.log(hashAPIPath(checkquerypath), checkHash); - //console.log(hashAPIPath(checkquerypath) == "?hash=" + checkHash); - - // if (hashAPIPath(checkquerypath) == "?hash=" + checkHash) { - //createContainer(containerID).then((containerName) => { - - console.log("does this get folder name:", containerID, casefolderID); + if ( + typeof containerID === "undefined" || + containerID.length === 0 || + typeof casefolderID === "undefined" || + casefolderID.length === 0 + ) { + return res.status(400).json(); + } repOrAppeal ? createRepBlob(appealData, containerID, casefolderID).then((data) => { @@ -41,7 +42,6 @@ ApiProxy.post(async (req, res) => { // return res.status(200).json({ data }); // } // ); - console.log("================================\nRepfile updated"); return res.status(200).json({ data }); }) : createBlob(appealData, containerID, casefolderID).then((data) => { @@ -51,22 +51,14 @@ ApiProxy.post(async (req, res) => { // return res.status(200).json({ data }); // } // ); - console.log( - "================================\nAppeal file updated" - ); return res.status(200).json({ data }); }); - //}); - - // } else { - // return res.status(400).json(); - // } }); export const config = { api: { - bodyParser: false, - }, + bodyParser: false + } }; export default ApiProxy; diff --git a/pages/api/file/uploadsinglefile.js b/pages/api/file/uploadsinglefile.js index cd8e45ff..e0ebc854 100644 --- a/pages/api/file/uploadsinglefile.js +++ b/pages/api/file/uploadsinglefile.js @@ -7,6 +7,7 @@ import { import nextConnect from "next-connect"; import middleware from "../middleware/middleware"; import { consoleLogger } from "../../../actions/core/logger"; +import { hashAPIPath } from "../../../actions/core/hash"; import { fileTypeFromBuffer } from "file-type"; import fs from "fs"; import path from "path"; @@ -36,11 +37,25 @@ ApiProxy.use(middleware); ApiProxy.post(async (req, res) => { var checkHash = req.query.hash; + var checkquerypath = "/api/file/uploadsinglefile"; - const containerID = req.body.containerID[0]; - const casefolderID = req.body.casefolderID[0]; + if (hashAPIPath(checkquerypath) != "?hash=" + checkHash) { + return res.status(400).json(); + } - console.log("there are files:", Object.keys(req.files).length); + const containerID = req.body?.containerID?.[0]; + const casefolderID = req.body?.casefolderID?.[0]; + + if ( + typeof containerID === "undefined" || + containerID.length === 0 || + typeof casefolderID === "undefined" || + casefolderID.length === 0 + ) { + return res.status(400).json(); + } + + const uploadedFiles = req.files || {}; // Add other mimetypes here const allowedMimeTypes = [ @@ -57,7 +72,7 @@ ApiProxy.post(async (req, res) => { var allowedFilesFormData = {}; var invalidFiles = []; // To store the names of invalid files - for (const [fileName, fileDetails] of Object.entries(req.files)) { + for (const [fileName, fileDetails] of Object.entries(uploadedFiles)) { const file = fileDetails[0]; const filePath = file.path; @@ -65,9 +80,6 @@ ApiProxy.post(async (req, res) => { const fnCheck = validateFilenameServer(file.originalFilename); if (!fnCheck.ok) { - console.log( - `File ${file.originalFilename} rejected: ${fnCheck.reason}` - ); invalidFiles.push(`${file.originalFilename} - Invalid filename`); continue; // do not process further } @@ -80,7 +92,6 @@ ApiProxy.post(async (req, res) => { try { const type = await fileTypeFromBuffer(buffer); // Correct usage of fileTypeFromBuffer - console.log("checking mime type ", type); if (type && allowedMimeTypes.includes(type.mime)) { // If the MIME type from the file signature matches the allowed list allowedFilesFormData[fileName] = fileDetails.map( @@ -94,21 +105,12 @@ ApiProxy.post(async (req, res) => { }) ); } else { - console.log( - `\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n\nFile ${fileName} has an invalid MIME type based on its content.\n\n\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\` - ); invalidFiles.push(fileName); // Track invalid file } } catch (error) { - console.log( - `Error reading file ${fileName} for MIME type validation`, - error - ); + consoleLogger(error); } } else { - console.log( - `\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\n\nFile ${fileName} has an unsupported MIME type: ${fileMimeType}\n\n\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\` - ); invalidFiles.push(fileName); // Track invalid file } } diff --git a/tests/phase10/service-behaviour.test.cjs b/tests/phase10/service-behaviour.test.cjs new file mode 100644 index 00000000..c034b001 --- /dev/null +++ b/tests/phase10/service-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 loadModule = (relativePath, injected = {}) => { + const filePath = path.join(rootDir, relativePath); + let source = fs.readFileSync(filePath, "utf8"); + + source = source.replace(/import[\s\S]*?from\s+"[^"]+";\n?/g, ""); + source = source.replace( + /export default async function\s+(\w+)\s*\(/, + "async function $1(" + ); + source = source.replace(/export const\s+/g, "const "); + source = source.replace( + /export default\s+(\w+);/g, + "module.exports.default = $1;" + ); + + source += + '\nif (typeof ApiProxy !== "undefined" && !module.exports.default) module.exports.default = ApiProxy;\n'; + + const context = { + module: { exports: {} }, + exports: {}, + require, + process, + console: { + log: () => {}, + info: () => {}, + warn: () => {}, + error: () => {} + }, + ...injected + }; + + vm.runInNewContext(source, context, { filename: filePath }); + return context.module.exports; +}; + +const createNextConnectMock = () => { + const router = { + handler: null, + use: () => {}, + get(fn) { + this.handler = fn; + } + }; + + return () => router; +}; + +const createRes = () => { + const state = { + statusCode: null, + jsonBody: undefined + }; + + return { + state, + setHeader: () => {}, + status(code) { + state.statusCode = code; + return this; + }, + json(payload) { + state.jsonBody = payload; + return payload; + } + }; +}; + +const tests = []; +const test = (name, fn) => tests.push({ name, fn }); + +test("getawaitingsubmissionfromblob rejects missing hash with 400", async () => { + const calls = []; + const nextConnect = createNextConnectMock(); + + const mod = loadModule("pages/api/file/getawaitingsubmissionfromblob.js", { + hashAPIPath: () => "&hash=expected", + getAllProgressBlobs: async (...args) => { + calls.push(args); + return []; + }, + downloadAllProgressFiles: async () => [], + _: { isEmpty: (value) => !value }, + nextConnect, + middleware: () => {} + }); + + const req = { query: { container: "c1" } }; + const res = createRes(); + + await mod.default.handler(req, res); + + assert.strictEqual(res.state.statusCode, 400); + assert.strictEqual(calls.length, 0); +}); + +test("getprogressobjblob rejects missing casefolderID with 400", async () => { + const calls = []; + const nextConnect = createNextConnectMock(); + + const mod = loadModule("pages/api/file/getprogressobjblob.js", { + hashAPIPath: () => "&hash=expected", + getProgressBlobs: async (...args) => { + calls.push(args); + return { path: "x" }; + }, + downloadProgressFile: async () => ({}), + _: { isEmpty: (value) => !value }, + nextConnect, + middleware: () => {} + }); + + const req = { + query: { container: "c1", hash: "expected" } + }; + const res = createRes(); + + await mod.default.handler(req, res); + + assert.strictEqual(res.state.statusCode, 400); + assert.strictEqual(calls.length, 0); +}); + +test("getbloblist rejects invalid hash with 400", async () => { + const calls = []; + const nextConnect = createNextConnectMock(); + + const mod = loadModule("pages/api/file/getbloblist.js", { + hashAPIPath: () => "&hash=expected", + getBlobs: async (...args) => { + calls.push(args); + return []; + }, + getRepsFilesBlobs: async () => [], + nextConnect, + middleware: () => {} + }); + + const req = { + query: { + container: "c1", + casefolderID: "case-1", + hash: "wrong" + } + }; + const res = createRes(); + + await mod.default.handler(req, res); + + assert.strictEqual(res.state.statusCode, 400); + assert.strictEqual(calls.length, 0); +}); + +test("getrepsblob rejects missing hash with 400", async () => { + const calls = []; + const nextConnect = createNextConnectMock(); + + const mod = loadModule("pages/api/file/getrepsblob.js", { + hashAPIPath: () => "&hash=expected", + getRepsBlobs: async (...args) => { + calls.push(args); + return []; + }, + downloadAllRepsFiles: async () => [], + consoleLogger: () => {}, + _: { isEmpty: (value) => !value }, + nextConnect, + middleware: () => {} + }); + + const req = { + query: { + container: "c1", + casefolderID: "case-1" + } + }; + const res = createRes(); + + await mod.default.handler(req, res); + + assert.strictEqual(res.state.statusCode, 400); + assert.strictEqual(calls.length, 0); +}); + +test("getbloblist happy path with valid hash returns 200", async () => { + const nextConnect = createNextConnectMock(); + + const mod = loadModule("pages/api/file/getbloblist.js", { + hashAPIPath: () => "&hash=expected", + getBlobs: async () => ["file-1"], + getRepsFilesBlobs: async () => ["file-rep"], + nextConnect, + middleware: () => {} + }); + + const req = { + query: { + container: "c1", + casefolderID: "case-1", + hash: "expected" + } + }; + const res = createRes(); + + await mod.default.handler(req, res); + + assert.strictEqual(res.state.statusCode, 200); + assert.deepStrictEqual(JSON.parse(JSON.stringify(res.state.jsonBody)), { + value: [["file-1"]] + }); +}); + +const run = async () => { + let passed = 0; + + for (const currentTest of tests) { + await currentTest.fn(); + passed += 1; + } + + console.log( + `Phase 10 behavioural tests passed (${passed}/${tests.length}).` + ); +}; + +run().catch((error) => { + console.error(error); + process.exit(1); +}); diff --git a/tests/phase11/service-behaviour.test.cjs b/tests/phase11/service-behaviour.test.cjs new file mode 100644 index 00000000..6ff5b9f7 --- /dev/null +++ b/tests/phase11/service-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 loadModule = (relativePath, injected = {}) => { + const filePath = path.join(rootDir, relativePath); + let source = fs.readFileSync(filePath, "utf8"); + + source = source.replace(/import[\s\S]*?from\s+"[^"]+";\n?/g, ""); + source = source.replace( + /export default async function\s+(\w+)\s*\(/, + "async function $1(" + ); + source = source.replace(/export const\s+/g, "const "); + source = source.replace( + /export default\s+(\w+);/g, + "module.exports.default = $1;" + ); + source += + '\nif (typeof ApiProxy !== "undefined" && !module.exports.default) module.exports.default = ApiProxy;\n'; + + const context = { + module: { exports: {} }, + exports: {}, + require, + process, + console: { log: () => {}, error: () => {} }, + ...injected + }; + + vm.runInNewContext(source, context, { filename: filePath }); + return context.module.exports; +}; + +const createNextConnectMock = () => { + const router = { + handler: null, + use: () => {}, + get(fn) { + this.handler = fn; + } + }; + return () => router; +}; + +const createRes = () => { + const state = { statusCode: null, jsonBody: undefined }; + return { + state, + status(code) { + state.statusCode = code; + return this; + }, + json(payload) { + state.jsonBody = payload; + return payload; + } + }; +}; + +const tests = []; +const test = (name, fn) => tests.push({ name, fn }); + +test("getbloblist rejects missing hash with 400", async () => { + const calls = []; + const mod = loadModule("pages/api/file/getbloblist.js", { + hashAPIPath: () => "&hash=expected", + getBlobs: async (...args) => { + calls.push(args); + return []; + }, + getRepsFilesBlobs: async () => [], + nextConnect: createNextConnectMock(), + middleware: () => {} + }); + + const req = { query: { container: "c1", casefolderID: "case-1" } }; + const res = createRes(); + await mod.default.handler(req, res); + assert.strictEqual(res.state.statusCode, 400); + assert.strictEqual(calls.length, 0); +}); + +test("deleteblob rejects missing blobname with 400", async () => { + const calls = []; + const mod = loadModule("pages/api/file/deleteblob.js", { + hashAPIPath: () => "&hash=expected", + deleteBlob: async (...args) => { + calls.push(args); + return true; + }, + nextConnect: createNextConnectMock(), + middleware: () => {} + }); + + const req = { + query: { container: "c1", casefolderID: "case-1", hash: "expected" } + }; + const res = createRes(); + await mod.default.handler(req, res); + assert.strictEqual(res.state.statusCode, 400); + assert.strictEqual(calls.length, 0); +}); + +test("deleteawaitingsubmissionfromblob rejects invalid hash with 400", async () => { + const calls = []; + const mod = loadModule( + "pages/api/file/deleteawaitingsubmissionfromblob.js", + { + hashAPIPath: () => "&hash=expected", + deleteBlob: async (...args) => { + calls.push(args); + return true; + }, + nextConnect: createNextConnectMock(), + middleware: () => {} + } + ); + + const req = { + query: { + container: "c1", + casefolderID: "case-1", + blobname: "f.pdf", + hash: "wrong" + } + }; + const res = createRes(); + await mod.default.handler(req, res); + assert.strictEqual(res.state.statusCode, 400); + assert.strictEqual(calls.length, 0); +}); + +test("getbloblist valid hash returns 200", async () => { + const mod = loadModule("pages/api/file/getbloblist.js", { + hashAPIPath: () => "&hash=expected", + getBlobs: async () => ["f1"], + getRepsFilesBlobs: async () => ["rf1"], + nextConnect: createNextConnectMock(), + middleware: () => {} + }); + + const req = { + query: { container: "c1", casefolderID: "case-1", hash: "expected" } + }; + const res = createRes(); + await mod.default.handler(req, res); + assert.strictEqual(res.state.statusCode, 200); + assert.deepStrictEqual(JSON.parse(JSON.stringify(res.state.jsonBody)), { + value: [["f1"]] + }); +}); + +const run = async () => { + let passed = 0; + for (const t of tests) { + await t.fn(); + passed += 1; + } + console.log( + `Phase 11 behavioural tests passed (${passed}/${tests.length}).` + ); +}; + +run().catch((error) => { + console.error(error); + process.exit(1); +}); diff --git a/tests/phase12/service-behaviour.test.cjs b/tests/phase12/service-behaviour.test.cjs new file mode 100644 index 00000000..b906582a --- /dev/null +++ b/tests/phase12/service-behaviour.test.cjs @@ -0,0 +1,169 @@ +const fs = require("fs"); +const path = require("path"); +const vm = require("vm"); +const assert = require("assert"); + +const rootDir = path.resolve(__dirname, "..", ".."); + +const loadModule = (relativePath, injected = {}) => { + const filePath = path.join(rootDir, relativePath); + let source = fs.readFileSync(filePath, "utf8"); + + source = source.replace(/import[\s\S]*?from\s+"[^"]+";\n?/g, ""); + source = source.replace( + /export default async function\s+(\w+)\s*\(/, + "async function $1(" + ); + source = source.replace(/export const\s+/g, "const "); + source = source.replace( + /export default\s+(\w+);/g, + "module.exports.default = $1;" + ); + source += + '\nif (typeof ApiProxy !== "undefined" && !module.exports.default) module.exports.default = ApiProxy;\n'; + + const context = { + module: { exports: {} }, + exports: {}, + require, + process, + console: { log: () => {}, error: () => {} }, + port: 3000, + ...injected + }; + + vm.runInNewContext(source, context, { filename: filePath }); + return context.module.exports; +}; + +const createNextConnectMock = () => { + const router = { + handler: null, + use: () => {}, + get(fn) { + this.handler = fn; + } + }; + return () => router; +}; + +const createRes = () => { + const state = { + statusCode: null, + jsonBody: undefined, + sentBody: undefined, + headers: {} + }; + return { + state, + status(code) { + state.statusCode = code; + return this; + }, + json(payload) { + state.jsonBody = payload; + return payload; + }, + send(payload) { + state.sentBody = payload; + return payload; + }, + setHeader(name, value) { + state.headers[name] = value; + } + }; +}; + +const tests = []; +const test = (name, fn) => tests.push({ name, fn }); + +test("downloadblob rejects missing hash with 400", async () => { + const calls = []; + const mod = loadModule("pages/api/file/downloadblob.js", { + hashAPIPath: () => "&hash=expected", + downloadFile: async (...args) => { + calls.push(args); + return Buffer.from("x"); + }, + nextConnect: createNextConnectMock(), + middleware: () => {} + }); + + const req = { + query: { container: "c1", casefolderID: "case-1", blobname: "f.pdf" } + }; + const res = createRes(); + await mod.default.handler(req, res); + assert.strictEqual(res.state.statusCode, 400); + assert.strictEqual(calls.length, 0); +}); + +test("downloadblob valid hash returns 200", async () => { + const mod = loadModule("pages/api/file/downloadblob.js", { + hashAPIPath: () => "&hash=expected", + downloadFile: async () => Buffer.from("file"), + nextConnect: createNextConnectMock(), + middleware: () => {} + }); + + const req = { + query: { + container: "c1", + casefolderID: "case-1", + blobname: "f.pdf", + hash: "expected" + } + }; + const res = createRes(); + await mod.default.handler(req, res); + assert.strictEqual(res.state.statusCode, 200); +}); + +test("getbloblistproxy rejects missing container with 400", async () => { + const mod = loadModule("pages/api/file/getbloblistproxy.js", { + getToken: async () => ({ access_token: "t" }), + hashAPIPath: () => "&hash=expected", + azureHeaders: () => ({}), + axios: { get: async () => ({ data: { ok: true } }) }, + consoleLogger: () => {} + }); + + const req = { query: { casefolderID: "case-1" } }; + const res = createRes(); + await mod.default(req, res); + assert.strictEqual(res.state.statusCode, 400); +}); + +test("getawaitingsubmissionfromblobproxy rejects missing container with 400", async () => { + const mod = loadModule( + "pages/api/file/getawaitingsubmissionfromblobproxy.js", + { + getToken: async () => ({ access_token: "t" }), + hashAPIPath: () => "&hash=expected", + azureHeaders: () => ({}), + axios: { get: async () => ({ data: { ok: true } }) }, + consoleLogger: () => {} + } + ); + + const req = { query: {} }; + const res = createRes(); + await mod.default(req, res); + assert.strictEqual(res.state.statusCode, 400); +}); + +const run = async () => { + let passed = 0; + for (const t of tests) { + await t.fn(); + passed += 1; + } + console.log( + `Phase 12 behavioural tests passed (${passed}/${tests.length}).` + ); +}; + +run().catch((error) => { + console.error(error); + process.exit(1); +}); diff --git a/tests/phase13/service-behaviour.test.cjs b/tests/phase13/service-behaviour.test.cjs new file mode 100644 index 00000000..4c0164da --- /dev/null +++ b/tests/phase13/service-behaviour.test.cjs @@ -0,0 +1,197 @@ +const fs = require("fs"); +const path = require("path"); +const vm = require("vm"); +const assert = require("assert"); + +const rootDir = path.resolve(__dirname, "..", ".."); + +const loadModule = (relativePath, injected = {}) => { + const filePath = path.join(rootDir, relativePath); + let source = fs.readFileSync(filePath, "utf8"); + + source = source.replace(/import[\s\S]*?from\s+"[^"]+";\n?/g, ""); + source = source.replace( + /export default async function\s+(\w+)\s*\(/, + "async function $1(" + ); + source = source.replace(/export const\s+/g, "const "); + source = source.replace( + /export default\s+(\w+);/g, + "module.exports.default = $1;" + ); + source += + '\nif (typeof ApiProxy !== "undefined" && !module.exports.default) module.exports.default = ApiProxy;\n'; + + const context = { + module: { exports: {} }, + exports: {}, + require, + process, + console: { log: () => {}, error: () => {} }, + ...injected + }; + + vm.runInNewContext(source, context, { filename: filePath }); + return context.module.exports; +}; + +const createNextConnectMock = () => { + const router = { + handler: null, + use: () => {}, + get(fn) { + this.handler = fn; + } + }; + return () => router; +}; + +const createRes = () => { + const state = { statusCode: null, jsonBody: undefined }; + return { + state, + status(code) { + state.statusCode = code; + return this; + }, + json(payload) { + state.jsonBody = payload; + return payload; + } + }; +}; + +const tests = []; +const test = (name, fn) => tests.push({ name, fn }); + +test("deleteblobcase rejects missing hash with 400", async () => { + const calls = []; + const mod = loadModule("pages/api/file/deleteblobcase.js", { + hashAPIPath: () => "&hash=expected", + deleteBlobCase: async (...args) => { + calls.push(args); + return true; + }, + nextConnect: createNextConnectMock(), + middleware: () => {} + }); + + const req = { query: { container: "c1", casefolderID: "case-1" } }; + const res = createRes(); + await mod.default.handler(req, res); + assert.strictEqual(res.state.statusCode, 400); + assert.strictEqual(calls.length, 0); +}); + +test("deleteblobrep rejects missing hash with 400", async () => { + const calls = []; + const mod = loadModule("pages/api/file/deleteblobrep.js", { + hashAPIPath: () => "&hash=expected", + deleteBlobRep: async (...args) => { + calls.push(args); + return true; + }, + nextConnect: createNextConnectMock(), + middleware: () => {} + }); + + const req = { + query: { container: "c1", casefolderID: "case-1", repfile: "r.pdf" } + }; + const res = createRes(); + await mod.default.handler(req, res); + assert.strictEqual(res.state.statusCode, 400); + assert.strictEqual(calls.length, 0); +}); + +test("createcaseinvolvement_api rejects missing required body values with 400", async () => { + const mod = loadModule("pages/api/file/createcaseinvolvement_api.js", { + getToken: async () => ({ access_token: "token" }), + axios: async () => ({ data: { ok: true } }), + consoleLogger: () => {} + }); + + const req = { body: {} }; + const res = createRes(); + await mod.default(req, res); + assert.strictEqual(res.state.statusCode, 400); +}); + +test("createrepinvolvement_api rejects missing required body values with 400", async () => { + const mod = loadModule("pages/api/file/createrepinvolvement_api.js", { + getToken: async () => ({ access_token: "token" }), + axios: async () => ({ data: { ok: true } }), + consoleLogger: () => {} + }); + + const req = { body: {} }; + const res = createRes(); + await mod.default(req, res); + assert.strictEqual(res.state.statusCode, 400); +}); + +test("deleteblobcase valid hash returns 200", async () => { + const mod = loadModule("pages/api/file/deleteblobcase.js", { + hashAPIPath: () => "&hash=expected", + deleteBlobCase: async () => ({ ok: true }), + nextConnect: createNextConnectMock(), + middleware: () => {} + }); + + const req = { + query: { container: "c1", casefolderID: "case-1", hash: "expected" } + }; + const res = createRes(); + await mod.default.handler(req, res); + assert.strictEqual(res.state.statusCode, 200); +}); + +test("createcaseinvolvement_api with valid body returns 200", async () => { + const mod = loadModule("pages/api/file/createcaseinvolvement_api.js", { + getToken: async () => ({ access_token: "token" }), + CryptoJS: { + HmacSHA256: () => ({ toString: () => "signed" }), + enc: { Hex: { parse: () => "" } } + }, + axios: async () => ({ data: { ok: true } }), + consoleLogger: () => {} + }); + + const req = { body: { contactid: "c1", incidentid: "i1" } }; + const res = createRes(); + await mod.default(req, res); + assert.strictEqual(res.state.statusCode, 200); +}); + +test("createrepinvolvement_api with valid body returns 200", async () => { + const mod = loadModule("pages/api/file/createrepinvolvement_api.js", { + getToken: async () => ({ access_token: "token" }), + CryptoJS: { + HmacSHA256: () => ({ toString: () => "signed" }), + enc: { Hex: { parse: () => "" } } + }, + axios: async () => ({ data: { ok: true } }), + consoleLogger: () => {} + }); + + const req = { body: { contactid: "c1", incidentid: "i1" } }; + const res = createRes(); + await mod.default(req, res); + assert.strictEqual(res.state.statusCode, 200); +}); + +const run = async () => { + let passed = 0; + for (const t of tests) { + await t.fn(); + passed += 1; + } + console.log( + `Phase 13 behavioural tests passed (${passed}/${tests.length}).` + ); +}; + +run().catch((error) => { + console.error(error); + process.exit(1); +}); diff --git a/tests/phase14/service-behaviour.test.cjs b/tests/phase14/service-behaviour.test.cjs new file mode 100644 index 00000000..86bb0714 --- /dev/null +++ b/tests/phase14/service-behaviour.test.cjs @@ -0,0 +1,164 @@ +const fs = require("fs"); +const path = require("path"); +const vm = require("vm"); +const assert = require("assert"); + +const rootDir = path.resolve(__dirname, "..", ".."); + +const loadModule = (relativePath, injected = {}) => { + const filePath = path.join(rootDir, relativePath); + let source = fs.readFileSync(filePath, "utf8"); + + source = source.replace(/import[\s\S]*?from\s+"[^"]+";\n?/g, ""); + source = source.replace( + /export default async function\s+(\w+)\s*\(/, + "async function $1(" + ); + source = source.replace(/export const\s+/g, "const "); + source = source.replace( + /export default\s+(\w+);/g, + "module.exports.default = $1;" + ); + source += + '\nif (typeof ApiProxy !== "undefined" && !module.exports.default) module.exports.default = ApiProxy;\n'; + + const context = { + module: { exports: {} }, + exports: {}, + require, + process, + console: { log: () => {}, error: () => {} }, + ...injected + }; + + vm.runInNewContext(source, context, { filename: filePath }); + return context.module.exports; +}; + +const createNextConnectMock = () => { + const router = { + handler: null, + use: () => {}, + get(fn) { + this.handler = fn; + } + }; + return () => router; +}; + +const createRes = () => { + const state = { statusCode: null, jsonBody: undefined }; + return { + state, + status(code) { + state.statusCode = code; + return this; + }, + json(payload) { + state.jsonBody = payload; + return payload; + } + }; +}; + +const tests = []; +const test = (name, fn) => tests.push({ name, fn }); + +test("gethash_api rejects missing path with 400", async () => { + const mod = loadModule("pages/api/endpoint/gethash_api.js", { + hashAPIPath: () => "?hash=expected", + getSession: async () => ({ user: { id: "u1" } }), + nextConnect: createNextConnectMock(), + middleware: () => {} + }); + + const req = { query: {} }; + const res = createRes(); + await mod.default.handler(req, res); + + assert.strictEqual(res.state.statusCode, 400); +}); + +test("gethash_api rejects non-api path with 400", async () => { + const mod = loadModule("pages/api/endpoint/gethash_api.js", { + hashAPIPath: () => "?hash=expected", + getSession: async () => ({ user: { id: "u1" } }), + nextConnect: createNextConnectMock(), + middleware: () => {} + }); + + const req = { query: { path: "/not-api/path" } }; + const res = createRes(); + await mod.default.handler(req, res); + + assert.strictEqual(res.state.statusCode, 400); +}); + +test("gethash_api returns hash for valid api path", async () => { + const mod = loadModule("pages/api/endpoint/gethash_api.js", { + hashAPIPath: () => "&hash=expected", + getSession: async () => ({ user: { id: "u1" } }), + nextConnect: createNextConnectMock(), + middleware: () => {} + }); + + const req = { query: { path: "/api/file/upload" } }; + const res = createRes(); + await mod.default.handler(req, res); + + assert.strictEqual(res.state.statusCode, 200); + assert.deepStrictEqual(JSON.parse(JSON.stringify(res.state.jsonBody)), { + hash: "&hash=expected" + }); +}); + +test("gethash_api returns hash for allow-listed getportallogin path", async () => { + const mod = loadModule("pages/api/endpoint/gethash_api.js", { + hashAPIPath: () => "&hash=login", + getSession: async () => ({ user: { id: "u1" } }), + nextConnect: createNextConnectMock(), + middleware: () => {} + }); + + const req = { + query: { path: "/api/endpoint/getportallogin_api?emailAddress=a@b.com" } + }; + const res = createRes(); + await mod.default.handler(req, res); + + assert.strictEqual(res.state.statusCode, 200); + assert.deepStrictEqual(JSON.parse(JSON.stringify(res.state.jsonBody)), { + hash: "&hash=login" + }); +}); + +test("gethash_api rejects unauthenticated requests with 401", async () => { + const mod = loadModule("pages/api/endpoint/gethash_api.js", { + hashAPIPath: () => "&hash=expected", + getSession: async () => null, + nextConnect: createNextConnectMock(), + middleware: () => {} + }); + + const req = { query: { path: "/api/file/upload" } }; + const res = createRes(); + await mod.default.handler(req, res); + + assert.strictEqual(res.state.statusCode, 401); +}); + +const run = async () => { + let passed = 0; + for (const t of tests) { + await t.fn(); + passed += 1; + } + console.log( + `Phase 14 behavioural tests passed (${passed}/${tests.length}).` + ); +}; + +run().catch((error) => { + console.error(error); + process.exit(1); +}); diff --git a/tests/phase7/service-behaviour.test.cjs b/tests/phase7/service-behaviour.test.cjs index 381cc31f..00da59c0 100644 --- a/tests/phase7/service-behaviour.test.cjs +++ b/tests/phase7/service-behaviour.test.cjs @@ -208,31 +208,34 @@ test("portal/deleteMyRepresentations logs and returns undefined on failure", asy test("account/getPortalLogin appends hash and returns res.data", async () => { const axios = createAxiosMock(); const logger = createLoggerMock(); - const hashCalls = []; - const hashAPIPath = (queryPath) => { - hashCalls.push(queryPath); - return "&hash=login123"; - }; + const signCalls = []; - axios.getHandler = async () => ({ data: { value: [{ id: "user-1" }] } }); + axios.getHandler = async (url) => { + if (url.startsWith("/api/endpoint/gethash_api?path=")) { + signCalls.push(url); + return { data: { hash: "&hash=login123" } }; + } + + return { data: { value: [{ id: "user-1" }] } }; + }; const account = loadServiceModule("accountDirectService.js", { axios, BASE_URL: "http://example.local", consoleLogger: logger.consoleLogger, - hashAPIPath + hashAPIPath: () => "&hash=fallback" }); const result = await account.getPortalLogin("person@example.com"); assert.deepStrictEqual(normalize(result), { value: [{ id: "user-1" }] }); - assert.strictEqual(hashCalls.length, 1); + assert.strictEqual(signCalls.length, 1); assert.strictEqual( - hashCalls[0], - "/api/endpoint/getportallogin_api?emailAddress=person@example.com" + signCalls[0], + "/api/endpoint/gethash_api?path=%2Fapi%2Fendpoint%2Fgetportallogin_api%3FemailAddress%3Dperson%40example.com" ); assert.strictEqual( - axios.calls[0].url, + axios.calls[1].url, "http://example.local/api/endpoint/getportallogin_api?emailAddress=person@example.com&hash=login123" ); }); @@ -242,7 +245,13 @@ test("account/getPortalLogin returns JSON stringified error on failure", async ( const logger = createLoggerMock(); const error = createAxiosError(500, "Broken"); - axios.getHandler = async () => Promise.reject(error); + axios.getHandler = async (url) => { + if (url.startsWith("/api/endpoint/gethash_api?path=")) { + return { data: { hash: "&hash=err" } }; + } + + return Promise.reject(error); + }; const account = loadServiceModule("accountDirectService.js", { axios, diff --git a/tests/phase9/service-behaviour.test.cjs b/tests/phase9/service-behaviour.test.cjs new file mode 100644 index 00000000..c5059efe --- /dev/null +++ b/tests/phase9/service-behaviour.test.cjs @@ -0,0 +1,250 @@ +const fs = require("fs"); +const path = require("path"); +const vm = require("vm"); +const assert = require("assert"); + +const rootDir = path.resolve(__dirname, "..", ".."); + +const loadModule = (relativePath, injected = {}) => { + const filePath = path.join(rootDir, relativePath); + let source = fs.readFileSync(filePath, "utf8"); + + source = source.replace(/import[\s\S]*?from\s+"[^"]+";\n?/g, ""); + source = source.replace( + /export default async function\s+(\w+)\s*\(/, + "async function $1(" + ); + source = source.replace(/export const\s+/g, "const "); + source = source.replace( + /export default\s+(\w+);/g, + "module.exports.default = $1;" + ); + + source += + '\nif (typeof ApiProxy !== "undefined" && !module.exports.default) module.exports.default = ApiProxy;\n'; + + const context = { + module: { exports: {} }, + exports: {}, + require, + process, + console: { + log: () => {}, + info: () => {}, + warn: () => {}, + error: () => {} + }, + ...injected + }; + + vm.runInNewContext(source, context, { filename: filePath }); + return context.module.exports; +}; + +const createNextConnectMock = () => { + const router = { + handler: null, + use: () => {}, + get(fn) { + this.handler = fn; + }, + post(fn) { + this.handler = fn; + } + }; + + return () => router; +}; + +const createRes = () => { + const state = { + statusCode: null, + jsonBody: undefined + }; + + return { + state, + status(code) { + state.statusCode = code; + return this; + }, + json(payload) { + state.jsonBody = payload; + return payload; + } + }; +}; + +const tests = []; +const test = (name, fn) => tests.push({ name, fn }); + +test("createrepcompletemessage_api rejects missing hash with 400", async () => { + const calls = []; + const nextConnect = createNextConnectMock(); + + const mod = loadModule("pages/api/file/createrepcompletemessage_api.js", { + hashAPIPath: () => "&hash=expected", + consoleLogger: () => {}, + createRepCompleteMessage: async (...args) => { + calls.push(args); + return { ok: true }; + }, + nextConnect, + middleware: () => {} + }); + + const req = { + query: { + container: "c1", + tempcaseref: "temp-1", + repid: "rep-1" + } + }; + const res = createRes(); + + await mod.default.handler(req, res); + + assert.strictEqual(res.state.statusCode, 400); + assert.strictEqual(calls.length, 0); +}); + +test("upload rejects missing hash with 400", async () => { + const blobCalls = []; + const nextConnect = createNextConnectMock(); + + const mod = loadModule("pages/api/file/upload.js", { + hashAPIPath: () => "?hash=expected", + createBlob: async (...args) => { + blobCalls.push(args); + return { ok: true }; + }, + createRepBlob: async (...args) => { + blobCalls.push(args); + return { ok: true }; + }, + uploadFile: async () => {}, + nextConnect, + middleware: () => {} + }); + + const req = { + query: {}, + body: { + appealData: {}, + containerID: ["c1"], + casefolderID: ["case-1"] + }, + files: {} + }; + const res = createRes(); + + await mod.default.handler(req, res); + + assert.strictEqual(res.state.statusCode, 400); + assert.strictEqual(blobCalls.length, 0); +}); + +test("uploadsinglefile rejects invalid hash with 400", async () => { + const uploadCalls = []; + const nextConnect = createNextConnectMock(); + + const mod = loadModule("pages/api/file/uploadsinglefile.js", { + hashAPIPath: () => "?hash=expected", + uploadSingleFile: async (...args) => { + uploadCalls.push(args); + return { ok: true }; + }, + consoleLogger: () => {}, + fileTypeFromBuffer: async () => ({ mime: "application/pdf" }), + fs: { readFileSync: () => Buffer.from("file") }, + path: { basename: (value) => value }, + nextConnect, + middleware: () => {} + }); + + const req = { + query: { hash: "wrong" }, + body: { containerID: ["c1"], casefolderID: ["case-1"] }, + files: {} + }; + const res = createRes(); + + await mod.default.handler(req, res); + + assert.strictEqual(res.state.statusCode, 400); + assert.strictEqual(uploadCalls.length, 0); +}); + +test("setupcontainer rejects missing ident with 400", async () => { + const createCalls = []; + const nextConnect = createNextConnectMock(); + + const mod = loadModule("pages/api/file/setupcontainer.js", { + hashAPIPath: () => "&hash=expected", + createContainer: async (...args) => { + createCalls.push(args); + return { ok: true }; + }, + createContainerSas: async () => {}, + getContainers: async () => {}, + getBlobs: async () => {}, + uploadFile: async () => {}, + consoleLogger: () => {}, + nextConnect, + middleware: () => {} + }); + + const req = { query: { hash: "expected" } }; + const res = createRes(); + + await mod.default.handler(req, res); + + assert.strictEqual(res.state.statusCode, 400); + assert.strictEqual(createCalls.length, 0); +}); + +test("setupcontainer rejects invalid hash with 400", async () => { + const createCalls = []; + const nextConnect = createNextConnectMock(); + + const mod = loadModule("pages/api/file/setupcontainer.js", { + hashAPIPath: () => "&hash=expected", + createContainer: async (...args) => { + createCalls.push(args); + return { ok: true }; + }, + createContainerSas: async () => {}, + getContainers: async () => {}, + getBlobs: async () => {}, + uploadFile: async () => {}, + consoleLogger: () => {}, + nextConnect, + middleware: () => {} + }); + + const req = { query: { ident: "container-1", hash: "wrong" } }; + const res = createRes(); + + await mod.default.handler(req, res); + + assert.strictEqual(res.state.statusCode, 400); + assert.strictEqual(createCalls.length, 0); +}); + +const run = async () => { + let passed = 0; + + for (const currentTest of tests) { + await currentTest.fn(); + passed += 1; + } + + console.log( + `Phase 9 behavioural tests passed (${passed}/${tests.length}).` + ); +}; + +run().catch((error) => { + console.error(error); + process.exit(1); +});