Merged PR 2168: update client side api calls with hardened hash
Related work items: #22028
This commit is contained in:
@@ -92,11 +92,13 @@ export const deleteAwaitingSubmissionsFromBlob = (
|
|||||||
"&casefolderID=" +
|
"&casefolderID=" +
|
||||||
casefolderID;
|
casefolderID;
|
||||||
|
|
||||||
var config = {
|
return buildHashedQueryUrl(queryUrl)
|
||||||
method: "get",
|
.then((signedUrl) =>
|
||||||
url: queryUrl + hashAPIPath(queryUrl)
|
axios({
|
||||||
};
|
method: "get",
|
||||||
return axios(config)
|
url: signedUrl
|
||||||
|
})
|
||||||
|
)
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
return res.data;
|
return res.data;
|
||||||
})
|
})
|
||||||
@@ -118,11 +120,13 @@ export const deleteMyRepresentationsFromBlob = (
|
|||||||
"&repfile=" +
|
"&repfile=" +
|
||||||
repfile;
|
repfile;
|
||||||
|
|
||||||
var config = {
|
return buildHashedQueryUrl(queryUrl)
|
||||||
method: "get",
|
.then((signedUrl) =>
|
||||||
url: queryUrl + hashAPIPath(queryUrl)
|
axios({
|
||||||
};
|
method: "get",
|
||||||
return axios(config)
|
url: signedUrl
|
||||||
|
})
|
||||||
|
)
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
return res.data;
|
return res.data;
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -182,19 +182,21 @@ export const deleteMyRepresentations = (myRepresentationsID) => {
|
|||||||
"/api/endpoint/deletemyrepresentations_api?myRepresentationsID=" +
|
"/api/endpoint/deletemyrepresentations_api?myRepresentationsID=" +
|
||||||
myRepresentationsID;
|
myRepresentationsID;
|
||||||
|
|
||||||
var config = {
|
return buildHashedQueryUrl(queryUrl)
|
||||||
method: "delete",
|
.then((signedUrl) =>
|
||||||
url: queryUrl + hashAPIPath(queryUrl),
|
axios({
|
||||||
headers: {
|
method: "delete",
|
||||||
"OData-MaxVersion": "4.0",
|
url: signedUrl,
|
||||||
"OData-Version": "4.0",
|
headers: {
|
||||||
"Accept": "application/json;odata.metadata=none",
|
"OData-MaxVersion": "4.0",
|
||||||
"Prefer": 'odata.include-annotations="*",return=representation',
|
"OData-Version": "4.0",
|
||||||
"Content-Type": "application/json"
|
"Accept": "application/json;odata.metadata=none",
|
||||||
}
|
"Prefer":
|
||||||
};
|
'odata.include-annotations="*",return=representation',
|
||||||
|
"Content-Type": "application/json"
|
||||||
return axios(config)
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
return res.data;
|
return res.data;
|
||||||
})
|
})
|
||||||
@@ -224,17 +226,20 @@ export const deleteAwaitingSubmissions = (incidentID) => {
|
|||||||
export const deleteWatchedCases = async (watchedCaseID) => {
|
export const deleteWatchedCases = async (watchedCaseID) => {
|
||||||
var queryUrl =
|
var queryUrl =
|
||||||
"/api/endpoint/deletewatchedcases_api?watchedCaseID=" + watchedCaseID;
|
"/api/endpoint/deletewatchedcases_api?watchedCaseID=" + watchedCaseID;
|
||||||
var config = {
|
|
||||||
method: "delete",
|
|
||||||
url: queryUrl
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
return buildHashedQueryUrl(queryUrl)
|
||||||
const res = await axios(config);
|
.then((signedUrl) =>
|
||||||
return res.data;
|
axios({
|
||||||
} catch (error) {
|
method: "delete",
|
||||||
consoleLogger(error);
|
url: signedUrl
|
||||||
}
|
})
|
||||||
|
)
|
||||||
|
.then((res) => {
|
||||||
|
return res.data;
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
consoleLogger(error);
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const sendCaseCompleteMessage = async (
|
export const sendCaseCompleteMessage = async (
|
||||||
|
|||||||
@@ -273,8 +273,7 @@ const TopThree = (props) => {
|
|||||||
deleteRepItem(
|
deleteRepItem(
|
||||||
props.accountDetails
|
props.accountDetails
|
||||||
.containerID,
|
.containerID,
|
||||||
showTopThreeArr[key]
|
showTopThreeArr[key].caseRef,
|
||||||
.ticketnumber,
|
|
||||||
showTopThreeArr[key]
|
showTopThreeArr[key]
|
||||||
.repfile_name
|
.repfile_name
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -686,7 +686,7 @@ const ViewAllResults = (props) => {
|
|||||||
] || "N/A"}
|
] || "N/A"}
|
||||||
</dd>
|
</dd>
|
||||||
{isAwaitingSubmissionDetails && (
|
{isAwaitingSubmissionDetails && (
|
||||||
<dd className="govuk-summary-list__value govuk-!-font-size-16 cardModuleItem ">
|
<dd className="govuk-summary-list__value govuk-!-font-size-16 ">
|
||||||
<button
|
<button
|
||||||
title={t(
|
title={t(
|
||||||
"case:do-you-want-to-delete-case-label"
|
"case:do-you-want-to-delete-case-label"
|
||||||
@@ -714,7 +714,7 @@ const ViewAllResults = (props) => {
|
|||||||
</dd>
|
</dd>
|
||||||
)}
|
)}
|
||||||
{isMyRepresentations && (
|
{isMyRepresentations && (
|
||||||
<dd className="govuk-summary-list__value govuk-!-font-size-16 cardModuleItem ">
|
<dd className="govuk-summary-list__value govuk-!-font-size-16 ">
|
||||||
<button
|
<button
|
||||||
title={t(
|
title={t(
|
||||||
"case:do-you-want-to-delete-rep-label"
|
"case:do-you-want-to-delete-rep-label"
|
||||||
@@ -733,7 +733,7 @@ const ViewAllResults = (props) => {
|
|||||||
) &&
|
) &&
|
||||||
deleteRepItem(
|
deleteRepItem(
|
||||||
props.accountDetails.containerID,
|
props.accountDetails.containerID,
|
||||||
item.pinswg_name,
|
item.caseRef,
|
||||||
item.repfile_name
|
item.repfile_name
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
@@ -744,12 +744,12 @@ const ViewAllResults = (props) => {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{isWatchedCases && (
|
{isWatchedCases && (
|
||||||
<dd className="govuk-summary-list__value govuk-!-font-size-16 cardModuleItem ">
|
<dd className="govuk-summary-list__value govuk-!-font-size-14 govuk-summary-list__action">
|
||||||
<button
|
<button
|
||||||
title={t(
|
title={t(
|
||||||
"case:do-you-want-to-delete-case-label"
|
"case:do-you-want-to-delete-case-label"
|
||||||
)}
|
)}
|
||||||
className=" cardModuleRemoveCase govuk-link govuk-link--no-underline govuk-link--inverse "
|
className=" cardModuleRemoveCase govuk-summary-list__actionLink watched_link"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
confirm(
|
confirm(
|
||||||
t(
|
t(
|
||||||
@@ -769,7 +769,7 @@ const ViewAllResults = (props) => {
|
|||||||
</button>
|
</button>
|
||||||
{item.pinswg_emailnotifications === true ? (
|
{item.pinswg_emailnotifications === true ? (
|
||||||
<button
|
<button
|
||||||
className="govuk-summary-list__actionLink mailLink watched_link"
|
className="govuk-summary-list__actionLink mailLink"
|
||||||
title={t("common:unsubscribe-title-label")}
|
title={t("common:unsubscribe-title-label")}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
confirm(
|
confirm(
|
||||||
@@ -928,8 +928,26 @@ const ViewAllResults = (props) => {
|
|||||||
.then((data) => data)
|
.then((data) => data)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
getWatchedCasesProxy(cookies.pinsUser).then((data) => {
|
getWatchedCasesProxy(cookies.pinsUser).then((data) => {
|
||||||
(setWatchedCases(data),
|
let showWatchedCases = (submittedArr) => {
|
||||||
getDetailsProxy(data, "myWatchedCases")
|
const required = submittedArr.value.filter((el) => {
|
||||||
|
return (
|
||||||
|
el.pinswg_representationsubmitted == null
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
let newObj = {};
|
||||||
|
return Object.assign(newObj, {
|
||||||
|
"@odata.count": required.length,
|
||||||
|
"value": required
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
let filteredWatchedCases = showWatchedCases(data);
|
||||||
|
(setWatchedCases(filteredWatchedCases),
|
||||||
|
getDetailsProxy(
|
||||||
|
filteredWatchedCases,
|
||||||
|
"myWatchedCases"
|
||||||
|
)
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
setWatchedCasesDetails(data);
|
setWatchedCasesDetails(data);
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ module.exports = {
|
|||||||
"/myportal/[appealtypes]": ["newappeal", "home", "myportal"],
|
"/myportal/[appealtypes]": ["newappeal", "home", "myportal"],
|
||||||
"/newappeal": ["newappeal", "home"],
|
"/newappeal": ["newappeal", "home"],
|
||||||
"/newappeal/*": ["newappeal", "home"],
|
"/newappeal/*": ["newappeal", "home"],
|
||||||
"/newappeal/[appealtypes]": ["newappeal", "home"],
|
"/newappeal/[appealtypes]": ["newappeal", "home", "myportal"],
|
||||||
"/newappeal/selectappeal": ["newappeal", "home"],
|
"/newappeal/selectappeal": ["newappeal", "home"],
|
||||||
"/dns/*": ["dnsCommon", "case", "common"],
|
"/dns/*": ["dnsCommon", "case", "common"],
|
||||||
"/dns/[developmentName]": ["dnsCommon", "case", "common"],
|
"/dns/[developmentName]": ["dnsCommon", "case", "common"],
|
||||||
|
|||||||
@@ -458,6 +458,97 @@
|
|||||||
- Response-shape contract stability preserved for touched handlers/services.
|
- 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.
|
- Remaining risk is primarily external dependency behavior on valid-hash happy paths in local environments (relay/storage), not hash-bypass behavior.
|
||||||
|
|
||||||
|
## Phase 15 hardening status (2026-03-13)
|
||||||
|
|
||||||
|
- Working branch created from `origin/SIPS-Development`:
|
||||||
|
- `TASK22028-phase15-hardening-slice`
|
||||||
|
- Delivered consistency-only hardening across 4 additional sensitive file handlers:
|
||||||
|
- `pages/api/file/upload.js`
|
||||||
|
- `pages/api/file/uploadsinglefile.js`
|
||||||
|
- `pages/api/file/createappealcompletemessage_api.js`
|
||||||
|
- `pages/api/file/setupcontainer.js`
|
||||||
|
- Hardening posture updates:
|
||||||
|
- standardized explicit early 400 on missing hash where validation was already required
|
||||||
|
- standardized early hash-mismatch return path style in `setupcontainer`
|
||||||
|
- preserved response-shape/signature contracts (no intended contract changes)
|
||||||
|
- Added focused phase-15 tests:
|
||||||
|
- `tests/phase15/service-behaviour.test.cjs`
|
||||||
|
- negative-path checks for missing/invalid hash across all selected handlers
|
||||||
|
- required-input negative-path checks where applicable
|
||||||
|
- one valid-hash mocked happy-path check (`setupcontainer` response shape)
|
||||||
|
|
||||||
|
### Phase 15 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)
|
||||||
|
- `node tests/phase14/service-behaviour.test.cjs` -> pass (5/5)
|
||||||
|
- `node tests/phase15/service-behaviour.test.cjs` -> pass (5/5)
|
||||||
|
- `npm run lint` -> warnings only (pre-existing `react-hooks/exhaustive-deps` warnings)
|
||||||
|
|
||||||
|
### Phase 15 manual HTTP snapshot
|
||||||
|
|
||||||
|
- Dev server run on `http://localhost:3002`.
|
||||||
|
- Missing/invalid hash negative-path checks for selected handlers -> **400**.
|
||||||
|
- Missing required params where applicable -> **400**.
|
||||||
|
- Valid-hash spot-check:
|
||||||
|
- `GET /api/file/setupcontainer?ident=c1&hash=<valid>` -> **200**.
|
||||||
|
|
||||||
|
## Phase 16 hardening status (2026-03-13)
|
||||||
|
|
||||||
|
- Continued on branch:
|
||||||
|
- `TASK22028-phase15-hardening-slice`
|
||||||
|
- Added a consistency-only file-proxy hardening slice across:
|
||||||
|
- `pages/api/file/getbloblistproxy.js`
|
||||||
|
- `pages/api/file/getrepsblobproxy.js`
|
||||||
|
- `pages/api/file/getawaitingsubmissionfromblobproxy.js`
|
||||||
|
- Changes focused on:
|
||||||
|
- standardized required-input guards via `hasValue`
|
||||||
|
- removed unused imports/dead constants from touched proxy handlers
|
||||||
|
- preserved existing response contracts and status semantics
|
||||||
|
- Added focused test pack:
|
||||||
|
- `tests/phase16/service-behaviour.test.cjs`
|
||||||
|
|
||||||
|
### Phase 16 validation evidence snapshot
|
||||||
|
|
||||||
|
- phase6 parity + phase6–phase15 behavioural suites: pass
|
||||||
|
- phase16 behavioural suite: pass (4/4)
|
||||||
|
- lint: warnings only (pre-existing)
|
||||||
|
|
||||||
|
## Follow-up status (2026-03-13)
|
||||||
|
|
||||||
|
- Applied targeted signer allow-list fix for browser delete-awaiting-submission path.
|
||||||
|
- Updated `pages/api/endpoint/gethash_api.js` allow-list with:
|
||||||
|
- `/api/file/deleteblobcase`
|
||||||
|
- Added focused behavioural test:
|
||||||
|
- `tests/phase14/service-behaviour.test.cjs` now includes allow-list coverage for deleteblobcase path.
|
||||||
|
|
||||||
|
## Follow-up status (2026-03-13 — additional delete flows)
|
||||||
|
|
||||||
|
- Expanded signer allow-list for additional browser delete paths:
|
||||||
|
- `/api/endpoint/deletemyrepresentations_api`
|
||||||
|
- `/api/endpoint/deletewatchedcases_api`
|
||||||
|
- `/api/file/deleteblobrep`
|
||||||
|
- Migrated affected direct-service delete calls to signer-based hash retrieval:
|
||||||
|
- `portalDirectService.deleteWatchedCases`
|
||||||
|
- `portalDirectService.deleteMyRepresentations`
|
||||||
|
- `documentDirectService.deleteAwaitingSubmissionsFromBlob`
|
||||||
|
- `documentDirectService.deleteMyRepresentationsFromBlob`
|
||||||
|
|
||||||
|
## Follow-up status (2026-03-13 — myportal + i18n alignment)
|
||||||
|
|
||||||
|
- Updated myportal representation delete call sites to pass `caseRef`:
|
||||||
|
- `components/myportal/topthree_reps.js`
|
||||||
|
- `components/myportal/viewall.js`
|
||||||
|
- Applied small view-all presentation cleanup and watched-case refresh filtering adjustment.
|
||||||
|
- Added missing `myportal` namespace mapping in `i18n.js` for `/newappeal/[appealtypes]`.
|
||||||
|
|
||||||
## Likely next steps
|
## Likely next steps
|
||||||
|
|
||||||
1. Stabilize and simplify breadcrumb/back-link decision logic with focused regression checks.
|
1. Stabilize and simplify breadcrumb/back-link decision logic with focused regression checks.
|
||||||
|
|||||||
@@ -929,3 +929,186 @@ Follow-ups:
|
|||||||
- Keep signer allow-list expansion minimal and task-driven.
|
- Keep signer allow-list expansion minimal and task-driven.
|
||||||
- Continue replacing remaining browser-side direct hash assumptions only where flows require it.
|
- 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.
|
- Confirm work-item/branch naming alignment for any subsequent phase slices if strict tracker continuity is required.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### CL-026: Phase 15 hardening — consistency guards for additional hash-sensitive file handlers
|
||||||
|
|
||||||
|
date: 2026-03-13
|
||||||
|
author: Cline
|
||||||
|
scope: `pages/api/file/{upload,uploadsinglefile,createappealcompletemessage_api,setupcontainer}.js`, `tests/phase15/service-behaviour.test.cjs`, `memory-bank/*`
|
||||||
|
type: change
|
||||||
|
rationale: Deliver a small reversible consistency-only hardening slice by standardizing missing/invalid hash negative paths and required input handling in additional sensitive file handlers.
|
||||||
|
impact: Consistent early 400 behavior for missing/invalid hash and missing required params while preserving existing response shapes/signatures.
|
||||||
|
status: completed
|
||||||
|
|
||||||
|
Summary:
|
||||||
|
|
||||||
|
- Started new branch from `origin/SIPS-Development`: `TASK22028-phase15-hardening-slice`.
|
||||||
|
- Applied minimal hardening updates:
|
||||||
|
- `upload.js`: explicit missing-hash guard before hash comparison.
|
||||||
|
- `uploadsinglefile.js`: explicit missing-hash guard before hash comparison.
|
||||||
|
- `createappealcompletemessage_api.js`: added missing-hash guard to existing required-input guard block.
|
||||||
|
- `setupcontainer.js`: standardized to early-return on hash mismatch; contract and status behavior preserved.
|
||||||
|
- Added focused tests:
|
||||||
|
- `tests/phase15/service-behaviour.test.cjs`
|
||||||
|
- covers missing-hash negative paths for all selected handlers
|
||||||
|
- includes required-input negative paths and one valid-hash happy-path shape check.
|
||||||
|
|
||||||
|
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)
|
||||||
|
- `node tests/phase14/service-behaviour.test.cjs` -> pass (5/5)
|
||||||
|
- `node tests/phase15/service-behaviour.test.cjs` -> pass (5/5)
|
||||||
|
- `npm run lint` -> warnings only (pre-existing hook dependency warnings)
|
||||||
|
- Manual checks on `localhost:3002`:
|
||||||
|
- invalid/missing hash for each selected handler -> 400
|
||||||
|
- missing required params where applicable -> 400
|
||||||
|
- valid-hash spot-check (`setupcontainer`) -> 200
|
||||||
|
|
||||||
|
Follow-ups:
|
||||||
|
|
||||||
|
- Continue small-batch consistency hardening for remaining sensitive handlers where guard patterns are still inconsistent.
|
||||||
|
- Keep manual happy-path checks constrained to feasible local dependencies; document downstream/environment-caused failures separately.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### CL-027: Phase 16 hardening — file proxy required-input guard consistency slice
|
||||||
|
|
||||||
|
date: 2026-03-13
|
||||||
|
author: Cline
|
||||||
|
scope: `pages/api/file/{getbloblistproxy,getrepsblobproxy,getawaitingsubmissionfromblobproxy}.js`, `tests/phase16/service-behaviour.test.cjs`, `memory-bank/*`
|
||||||
|
type: change
|
||||||
|
rationale: Continue within the same branch using separate commits to apply a small consistency-only hardening slice for sensitive file proxy handlers.
|
||||||
|
impact: More consistent required-input validation and cleaner proxy handler internals without changing response-shape/signature contracts.
|
||||||
|
status: completed
|
||||||
|
|
||||||
|
Summary:
|
||||||
|
|
||||||
|
- Continued on `TASK22028-phase15-hardening-slice` as a separate commit.
|
||||||
|
- Standardized required-input guards using a local `hasValue` helper in 3 file proxy handlers.
|
||||||
|
- Removed unused imports/constants from touched files.
|
||||||
|
- Added `tests/phase16/service-behaviour.test.cjs` with negative-path coverage and one valid-input contract check.
|
||||||
|
|
||||||
|
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)
|
||||||
|
- `node tests/phase14/service-behaviour.test.cjs` -> pass (5/5)
|
||||||
|
- `node tests/phase15/service-behaviour.test.cjs` -> pass (5/5)
|
||||||
|
- `node tests/phase16/service-behaviour.test.cjs` -> pass (4/4)
|
||||||
|
- `npm run lint` -> warnings only (pre-existing)
|
||||||
|
- Manual checks (`localhost:3002`): missing required inputs across selected proxies -> 400
|
||||||
|
|
||||||
|
Follow-ups:
|
||||||
|
|
||||||
|
- Continue incremental endpoint/file proxy consistency slices with same contract-preserving constraints.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### CL-028: Follow-up fix — signer allow-list expanded for deleteblobcase browser path
|
||||||
|
|
||||||
|
date: 2026-03-13
|
||||||
|
author: Cline
|
||||||
|
scope: `pages/api/endpoint/gethash_api.js`, `tests/phase14/service-behaviour.test.cjs`
|
||||||
|
type: change
|
||||||
|
rationale: Resolve runtime failure where browser-side `deleteAwaitingSubmissionsFromBlob` hash signing requested a path not present in signer allow-list.
|
||||||
|
impact: Restores browser-safe hash signing for delete-blob-case flow without weakening auth/session guard on signer endpoint.
|
||||||
|
status: completed
|
||||||
|
|
||||||
|
Summary:
|
||||||
|
|
||||||
|
- Added `/api/file/deleteblobcase` to `gethash_api` allow-list.
|
||||||
|
- Added focused test coverage in phase-14 suite for the new allow-listed path.
|
||||||
|
|
||||||
|
Validation:
|
||||||
|
|
||||||
|
- `node tests/phase14/service-behaviour.test.cjs` -> pass (6/6)
|
||||||
|
- `node tests/phase7/service-behaviour.test.cjs` -> pass (10/10)
|
||||||
|
- `npm run lint` -> warnings only (pre-existing)
|
||||||
|
|
||||||
|
Follow-ups:
|
||||||
|
|
||||||
|
- Keep signer allow-list additions minimal and path-specific.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### CL-029: Follow-up fix — signer coverage for additional delete flows
|
||||||
|
|
||||||
|
date: 2026-03-13
|
||||||
|
author: Cline
|
||||||
|
scope: `actions/services/{portalDirectService,documentDirectService}.js`, `pages/api/endpoint/gethash_api.js`, `tests/phase7/service-behaviour.test.cjs`, `tests/phase14/service-behaviour.test.cjs`
|
||||||
|
type: change
|
||||||
|
rationale: Resolve browser hash-signing gaps for additional delete operations still using direct client hash generation.
|
||||||
|
impact: Restores browser-safe signing for delete watched cases, delete my representations, and delete blob representation/awaiting-submission paths while retaining authenticated allow-list signer model.
|
||||||
|
status: completed
|
||||||
|
|
||||||
|
Summary:
|
||||||
|
|
||||||
|
- Updated signer allow-list to include:
|
||||||
|
- `/api/endpoint/deletemyrepresentations_api`
|
||||||
|
- `/api/endpoint/deletewatchedcases_api`
|
||||||
|
- `/api/file/deleteblobrep`
|
||||||
|
- Migrated direct service calls to signer-based hash retrieval for:
|
||||||
|
- `portalDirectService.deleteWatchedCases`
|
||||||
|
- `portalDirectService.deleteMyRepresentations`
|
||||||
|
- `documentDirectService.deleteAwaitingSubmissionsFromBlob`
|
||||||
|
- `documentDirectService.deleteMyRepresentationsFromBlob`
|
||||||
|
- Extended focused tests:
|
||||||
|
- `tests/phase7/service-behaviour.test.cjs` (service signer usage)
|
||||||
|
- `tests/phase14/service-behaviour.test.cjs` (allow-list coverage)
|
||||||
|
|
||||||
|
Validation:
|
||||||
|
|
||||||
|
- `node tests/phase7/service-behaviour.test.cjs` -> pass (12/12)
|
||||||
|
- `node tests/phase14/service-behaviour.test.cjs` -> pass (9/9)
|
||||||
|
- `npm run lint` -> warnings only (pre-existing)
|
||||||
|
|
||||||
|
Follow-ups:
|
||||||
|
|
||||||
|
- Keep allow-list expansion path-scoped and driven by concrete browser-call requirements only.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### CL-030: Follow-up fix — myportal delete caseRef alignment + i18n namespace
|
||||||
|
|
||||||
|
date: 2026-03-13
|
||||||
|
author: Cline
|
||||||
|
scope: `components/myportal/{viewall,topthree_reps}.js`, `i18n.js`, `memory-bank/*`
|
||||||
|
type: change
|
||||||
|
rationale: Align representation delete calls to stable case reference identifier and resolve missing locale namespace mapping for new-appeal route context.
|
||||||
|
impact: Prevents delete-call identifier mismatches in myportal representation flows and restores expected translation namespace availability for `/newappeal/[appealtypes]`.
|
||||||
|
status: completed
|
||||||
|
|
||||||
|
Summary:
|
||||||
|
|
||||||
|
- Updated delete-representation callers to pass `caseRef` instead of display/ticket values in:
|
||||||
|
- `components/myportal/topthree_reps.js`
|
||||||
|
- `components/myportal/viewall.js`
|
||||||
|
- Included related view-all styling/class tidy updates and watched-case list refresh filtering adjustment.
|
||||||
|
- Added missing `myportal` namespace for route key:
|
||||||
|
- `i18n.js` -> `/newappeal/[appealtypes]`
|
||||||
|
|
||||||
|
Validation:
|
||||||
|
|
||||||
|
- `node tests/phase7/service-behaviour.test.cjs` -> pass (12/12)
|
||||||
|
- `npm run lint` -> warnings only (pre-existing)
|
||||||
|
|
||||||
|
Follow-ups:
|
||||||
|
|
||||||
|
- Keep delete calls consistently keyed on case reference across portal/view-all/top-three variants.
|
||||||
|
|||||||
@@ -588,6 +588,162 @@
|
|||||||
3. Remove `tests/phase8/service-behaviour.test.cjs` if full slice rollback required.
|
3. Remove `tests/phase8/service-behaviour.test.cjs` if full slice rollback required.
|
||||||
4. Re-run phase6/7 baseline tests + lint after rollback.
|
4. Re-run phase6/7 baseline tests + lint after rollback.
|
||||||
|
|
||||||
|
## Latest update (2026-03-13 — Phase 15 hardening slice)
|
||||||
|
|
||||||
|
- New branch created from `origin/SIPS-Development` with confirmed work item prefix:
|
||||||
|
- `TASK22028-phase15-hardening-slice`
|
||||||
|
- Delivered a small consistency-only hardening slice across 4 additional sensitive file handlers:
|
||||||
|
- `pages/api/file/upload.js`
|
||||||
|
- `pages/api/file/uploadsinglefile.js`
|
||||||
|
- `pages/api/file/createappealcompletemessage_api.js`
|
||||||
|
- `pages/api/file/setupcontainer.js`
|
||||||
|
- Scope delivered:
|
||||||
|
- standardized explicit early 400 handling for missing hash in handlers where hash validation was present but missing-hash guard was inconsistent
|
||||||
|
- standardized guard style in `setupcontainer` to early-return on hash mismatch (same status/contract)
|
||||||
|
- standardized missing required hash handling in `createappealcompletemessage_api`
|
||||||
|
- preserved response shapes and signatures (no contract changes)
|
||||||
|
- Added focused phase-15 tests:
|
||||||
|
- `tests/phase15/service-behaviour.test.cjs`
|
||||||
|
- negative-path coverage for missing hash across all 4 handlers
|
||||||
|
- additional required-input checks where applicable
|
||||||
|
- one valid-hash happy-path contract check (`setupcontainer` -> 200 with existing response shape)
|
||||||
|
|
||||||
|
### Validation snapshot (Phase 15)
|
||||||
|
|
||||||
|
- `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)
|
||||||
|
- `node tests/phase15/service-behaviour.test.cjs` -> **pass** (5/5)
|
||||||
|
- `npm run lint` -> **warnings only** (pre-existing `react-hooks/exhaustive-deps` warnings)
|
||||||
|
|
||||||
|
### Manual HTTP snapshot (Phase 15)
|
||||||
|
|
||||||
|
- Dev server run on `http://localhost:3002`.
|
||||||
|
- Negative-path checks:
|
||||||
|
- `POST /api/file/upload` missing hash -> **400**
|
||||||
|
- `POST /api/file/upload?hash=wrong` -> **400**
|
||||||
|
- `POST /api/file/uploadsinglefile` missing hash -> **400**
|
||||||
|
- `POST /api/file/uploadsinglefile?hash=wrong` -> **400**
|
||||||
|
- `GET /api/file/createappealcompletemessage_api?container=c1&tempcaseref=t1` missing hash -> **400**
|
||||||
|
- `GET /api/file/createappealcompletemessage_api?container=c1&tempcaseref=t1&hash=wrong` -> **400**
|
||||||
|
- `GET /api/file/createappealcompletemessage_api?container=c1` missing required params/hash -> **400**
|
||||||
|
- `GET /api/file/setupcontainer?ident=c1` missing hash -> **400**
|
||||||
|
- `GET /api/file/setupcontainer?ident=c1&hash=wrong` -> **400**
|
||||||
|
- `GET /api/file/setupcontainer?hash=<valid>` missing required `ident` -> **400**
|
||||||
|
- Valid-hash spot-check:
|
||||||
|
- `GET /api/file/setupcontainer?ident=c1&hash=<valid>` -> **200**
|
||||||
|
|
||||||
|
### Rollback plan (Phase 15)
|
||||||
|
|
||||||
|
1. Revert these files:
|
||||||
|
- `pages/api/file/upload.js`
|
||||||
|
- `pages/api/file/uploadsinglefile.js`
|
||||||
|
- `pages/api/file/createappealcompletemessage_api.js`
|
||||||
|
- `pages/api/file/setupcontainer.js`
|
||||||
|
- `tests/phase15/service-behaviour.test.cjs`
|
||||||
|
2. Re-run phase6–phase15 tests and lint to confirm parity.
|
||||||
|
|
||||||
|
## Latest update (2026-03-13 — Phase 16 file-proxy consistency slice)
|
||||||
|
|
||||||
|
- Continued on branch:
|
||||||
|
- `TASK22028-phase15-hardening-slice`
|
||||||
|
- Delivered a small consistency-only hardening slice across 3 sensitive file proxy handlers:
|
||||||
|
- `pages/api/file/getbloblistproxy.js`
|
||||||
|
- `pages/api/file/getrepsblobproxy.js`
|
||||||
|
- `pages/api/file/getawaitingsubmissionfromblobproxy.js`
|
||||||
|
- Scope delivered:
|
||||||
|
- removed unused imports/constants in touched handlers to reduce noisy/unsafe dead code
|
||||||
|
- standardized required input checks using shared local `hasValue` pattern
|
||||||
|
- preserved response shape/signature/status behavior contracts
|
||||||
|
- Added focused tests:
|
||||||
|
- `tests/phase16/service-behaviour.test.cjs`
|
||||||
|
- negative-path checks for missing required container/casefolder inputs
|
||||||
|
- one valid-input happy-path contract check for `getbloblistproxy` (200 + passthrough shape)
|
||||||
|
|
||||||
|
### Validation snapshot (Phase 16)
|
||||||
|
|
||||||
|
- `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)
|
||||||
|
- `node tests/phase15/service-behaviour.test.cjs` -> **pass** (5/5)
|
||||||
|
- `node tests/phase16/service-behaviour.test.cjs` -> **pass** (4/4)
|
||||||
|
- `npm run lint` -> **warnings only** (pre-existing `react-hooks/exhaustive-deps` warnings)
|
||||||
|
|
||||||
|
### Manual HTTP checks (Phase 16)
|
||||||
|
|
||||||
|
- Dev server on `http://localhost:3002`.
|
||||||
|
- Negative-path checks:
|
||||||
|
- `/api/file/getbloblistproxy?casefolderID=case-1` -> **400**
|
||||||
|
- `/api/file/getbloblistproxy?container=c1` -> **400**
|
||||||
|
- `/api/file/getrepsblobproxy` -> **400**
|
||||||
|
- `/api/file/getawaitingsubmissionfromblobproxy` -> **400**
|
||||||
|
- Feasible valid-input spot-check:
|
||||||
|
- `/api/file/getbloblistproxy?container=c1&casefolderID=case-1` -> **400** (expected downstream/local dependency behavior; input guard passed)
|
||||||
|
|
||||||
|
### Rollback plan (Phase 16)
|
||||||
|
|
||||||
|
1. Revert commit `c196edb` or files:
|
||||||
|
- `pages/api/file/getbloblistproxy.js`
|
||||||
|
- `pages/api/file/getrepsblobproxy.js`
|
||||||
|
- `pages/api/file/getawaitingsubmissionfromblobproxy.js`
|
||||||
|
- `tests/phase16/service-behaviour.test.cjs`
|
||||||
|
2. Re-run phase6–phase16 tests and lint.
|
||||||
|
|
||||||
|
## Latest update (2026-03-13 — signer allow-list follow-up)
|
||||||
|
|
||||||
|
- Branch: `TASK22028-phase15-hardening-slice`
|
||||||
|
- Fix applied:
|
||||||
|
- added `/api/file/deleteblobcase` to signer endpoint allow-list in `pages/api/endpoint/gethash_api.js`
|
||||||
|
- Test update:
|
||||||
|
- extended `tests/phase14/service-behaviour.test.cjs` with deleteblobcase allow-list coverage
|
||||||
|
|
||||||
|
### Validation snapshot (follow-up)
|
||||||
|
|
||||||
|
- `node tests/phase14/service-behaviour.test.cjs` -> **pass** (6/6)
|
||||||
|
- `node tests/phase7/service-behaviour.test.cjs` -> **pass** (10/10)
|
||||||
|
- `npm run lint` -> **warnings only** (pre-existing)
|
||||||
|
|
||||||
|
## Latest update (2026-03-13 — signer delete-flow follow-up)
|
||||||
|
|
||||||
|
- Branch: `TASK22028-phase15-hardening-slice`
|
||||||
|
- Extended signer allow-list and browser signer usage for additional delete flows:
|
||||||
|
- `deleteWatchedCases`
|
||||||
|
- `deleteMyRepresentations`
|
||||||
|
- `deleteAwaitingSubmissionsFromBlob`
|
||||||
|
- `deleteMyRepresentationsFromBlob`
|
||||||
|
|
||||||
|
### Validation snapshot (delete-flow follow-up)
|
||||||
|
|
||||||
|
- `node tests/phase7/service-behaviour.test.cjs` -> **pass** (12/12)
|
||||||
|
- `node tests/phase14/service-behaviour.test.cjs` -> **pass** (9/9)
|
||||||
|
- `npm run lint` -> **warnings only** (pre-existing)
|
||||||
|
|
||||||
|
## Latest update (2026-03-13 — myportal + i18n follow-up)
|
||||||
|
|
||||||
|
- Branch: `TASK22028-phase15-hardening-slice`
|
||||||
|
- Delivered follow-up fixes:
|
||||||
|
- myportal representation delete calls now use `caseRef` in top-three and view-all flows
|
||||||
|
- missing `myportal` namespace added for `/newappeal/[appealtypes]` in `i18n.js`
|
||||||
|
|
||||||
|
### Validation snapshot (myportal + i18n follow-up)
|
||||||
|
|
||||||
|
- `node tests/phase7/service-behaviour.test.cjs` -> **pass** (12/12)
|
||||||
|
- `npm run lint` -> **warnings only** (pre-existing)
|
||||||
|
|
||||||
## Outstanding risks / gaps
|
## Outstanding risks / gaps
|
||||||
|
|
||||||
- Navigation regressions across EN/CY + route query combinations.
|
- Navigation regressions across EN/CY + route query combinations.
|
||||||
|
|||||||
@@ -467,6 +467,116 @@ Status key: `[x] done`, `[ ] pending`
|
|||||||
- Revert `b8fa514` to remove getrepsblob contract-alignment + Quill import fix.
|
- Revert `b8fa514` to remove getrepsblob contract-alignment + Quill import fix.
|
||||||
- Re-run phase6–phase14 tests and lint after rollback.
|
- Re-run phase6–phase14 tests and lint after rollback.
|
||||||
|
|
||||||
|
### Phase 15 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: `TASK22028-phase15-hardening-slice`
|
||||||
|
2. `[x]` Apply minimal consistency-only hardening in 4 additional sensitive handlers
|
||||||
|
- `pages/api/file/upload.js`
|
||||||
|
- `pages/api/file/uploadsinglefile.js`
|
||||||
|
- `pages/api/file/createappealcompletemessage_api.js`
|
||||||
|
- `pages/api/file/setupcontainer.js`
|
||||||
|
3. `[x]` Standardize negative-path behavior
|
||||||
|
- explicit early 400 for missing/invalid hash in selected handlers
|
||||||
|
- explicit missing required-input checks where route contract requires them
|
||||||
|
- preserved response-shape contracts and function signatures
|
||||||
|
4. `[x]` Add focused Phase 15 tests
|
||||||
|
- added `tests/phase15/service-behaviour.test.cjs`
|
||||||
|
- includes negative-path tests for missing/invalid hash + required-input checks
|
||||||
|
- includes one valid-hash mocked happy-path response-shape check
|
||||||
|
5. `[x]` Execute required validation bundle
|
||||||
|
- phase6 parity + phase6–phase14 behaviour tests -> pass
|
||||||
|
- phase15 behaviour tests -> pass
|
||||||
|
- lint -> warnings only (pre-existing)
|
||||||
|
6. `[x]` Execute targeted manual checks
|
||||||
|
- missing/invalid hash across selected handlers -> 400
|
||||||
|
- missing required params where applicable -> 400
|
||||||
|
- valid-hash `setupcontainer` spot-check -> 200
|
||||||
|
|
||||||
|
### Phase 15 rollback notes
|
||||||
|
|
||||||
|
- Revert these files to rollback the full hardening slice:
|
||||||
|
- `pages/api/file/upload.js`
|
||||||
|
- `pages/api/file/uploadsinglefile.js`
|
||||||
|
- `pages/api/file/createappealcompletemessage_api.js`
|
||||||
|
- `pages/api/file/setupcontainer.js`
|
||||||
|
- `tests/phase15/service-behaviour.test.cjs`
|
||||||
|
- Re-run phase6–phase15 baseline tests and lint after rollback to confirm parity.
|
||||||
|
|
||||||
|
### Phase 16 completion snapshot (2026-03-13)
|
||||||
|
|
||||||
|
Status key: `[x] done`, `[ ] pending`
|
||||||
|
|
||||||
|
1. `[x]` Continue on current branch with separate commit for additional small slice
|
||||||
|
- branch: `TASK22028-phase15-hardening-slice`
|
||||||
|
2. `[x]` Apply minimal consistency hardening in 3 file proxy handlers
|
||||||
|
- `pages/api/file/getbloblistproxy.js`
|
||||||
|
- `pages/api/file/getrepsblobproxy.js`
|
||||||
|
- `pages/api/file/getawaitingsubmissionfromblobproxy.js`
|
||||||
|
3. `[x]` Standardize required-input guard pattern
|
||||||
|
- added local `hasValue` checks for required params
|
||||||
|
- removed unused/dead imports/constants in touched files
|
||||||
|
4. `[x]` Add focused Phase 16 tests
|
||||||
|
- added `tests/phase16/service-behaviour.test.cjs`
|
||||||
|
- missing-input negative-path checks + one valid-input contract check
|
||||||
|
5. `[x]` Execute validation bundle
|
||||||
|
- phase6 parity + phase6–phase16 behaviour tests pass
|
||||||
|
- lint warnings only (pre-existing)
|
||||||
|
6. `[x]` Execute manual checks
|
||||||
|
- missing required-input proxy calls -> 400
|
||||||
|
|
||||||
|
### Phase 16 rollback notes
|
||||||
|
|
||||||
|
- Revert commit `c196edb` (or files):
|
||||||
|
- `pages/api/file/getbloblistproxy.js`
|
||||||
|
- `pages/api/file/getrepsblobproxy.js`
|
||||||
|
- `pages/api/file/getawaitingsubmissionfromblobproxy.js`
|
||||||
|
- `tests/phase16/service-behaviour.test.cjs`
|
||||||
|
- Re-run phase6–phase16 baseline tests and lint.
|
||||||
|
|
||||||
|
### Follow-up completion snapshot (2026-03-13)
|
||||||
|
|
||||||
|
1. `[x]` Address browser signer failure for delete-awaiting-submission path
|
||||||
|
- added signer allow-list entry: `/api/file/deleteblobcase`
|
||||||
|
2. `[x]` Add focused behavioural test coverage
|
||||||
|
- extended `tests/phase14/service-behaviour.test.cjs`
|
||||||
|
3. `[x]` Validate
|
||||||
|
- phase14 behaviour tests pass (6/6)
|
||||||
|
- phase7 behaviour tests pass (10/10)
|
||||||
|
- lint warnings only (pre-existing)
|
||||||
|
|
||||||
|
### Follow-up rollback notes
|
||||||
|
|
||||||
|
- Revert commit `49dd990` to undo allow-list/test follow-up.
|
||||||
|
|
||||||
|
### Follow-up completion snapshot (2026-03-13 — additional delete flows)
|
||||||
|
|
||||||
|
1. `[x]` Expand signer allow-list for required browser delete paths
|
||||||
|
2. `[x]` Move remaining delete service calls to signer-based hash retrieval
|
||||||
|
3. `[x]` Extend focused service/signer tests
|
||||||
|
4. `[x]` Validate
|
||||||
|
- phase7 behaviour tests pass (12/12)
|
||||||
|
- phase14 behaviour tests pass (9/9)
|
||||||
|
- lint warnings only (pre-existing)
|
||||||
|
|
||||||
|
### Follow-up rollback notes (additional delete flows)
|
||||||
|
|
||||||
|
- Revert the follow-up commit for this slice to restore prior hashing behavior.
|
||||||
|
|
||||||
|
### Follow-up completion snapshot (2026-03-13 — myportal + i18n alignment)
|
||||||
|
|
||||||
|
1. `[x]` Align myportal delete-representation calls to use `caseRef`
|
||||||
|
2. `[x]` Add missing i18n namespace mapping for `/newappeal/[appealtypes]`
|
||||||
|
3. `[x]` Validate
|
||||||
|
- phase7 behaviour tests pass (12/12)
|
||||||
|
- lint warnings only (pre-existing)
|
||||||
|
|
||||||
|
### Follow-up rollback notes (myportal + i18n alignment)
|
||||||
|
|
||||||
|
- Revert commit `d32d7d0` to restore prior myportal/i18n state.
|
||||||
|
|
||||||
## Safe execution mode for migration chunks (required)
|
## 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.
|
To reduce terminal hangs during bulk migration work, run refactor chunks in **safe stepwise mode** instead of long chained commands.
|
||||||
|
|||||||
@@ -14,8 +14,12 @@ ApiProxy.get(async (req, res) => {
|
|||||||
const queryPath = req.query.path;
|
const queryPath = req.query.path;
|
||||||
const allowedPrefix = [
|
const allowedPrefix = [
|
||||||
"/api/endpoint/getportallogin_api",
|
"/api/endpoint/getportallogin_api",
|
||||||
|
"/api/endpoint/deletemyrepresentations_api",
|
||||||
|
"/api/endpoint/deletewatchedcases_api",
|
||||||
"/api/file/upload",
|
"/api/file/upload",
|
||||||
"/api/file/uploadsinglefile",
|
"/api/file/uploadsinglefile",
|
||||||
|
"/api/file/deleteblobcase",
|
||||||
|
"/api/file/deleteblobrep",
|
||||||
"/api/file/createrepcompletemessage_api",
|
"/api/file/createrepcompletemessage_api",
|
||||||
"/api/file/createappealcompletemessage_api"
|
"/api/file/createappealcompletemessage_api"
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -26,7 +26,9 @@ ApiProxy.get(async (req, res) => {
|
|||||||
typeof containerName === "undefined" ||
|
typeof containerName === "undefined" ||
|
||||||
containerName.length === 0 ||
|
containerName.length === 0 ||
|
||||||
typeof tempCaseRef === "undefined" ||
|
typeof tempCaseRef === "undefined" ||
|
||||||
tempCaseRef.length === 0
|
tempCaseRef.length === 0 ||
|
||||||
|
typeof checkHash === "undefined" ||
|
||||||
|
checkHash.length === 0
|
||||||
) {
|
) {
|
||||||
return res.status(400).json();
|
return res.status(400).json();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,30 +1,18 @@
|
|||||||
import {
|
|
||||||
downloadAllProgressFiles,
|
|
||||||
getAllProgressBlobs
|
|
||||||
} from "../../../actions/azurestorage";
|
|
||||||
import _ from "lodash";
|
|
||||||
import nextConnect from "next-connect";
|
|
||||||
import middleware from "../middleware/middleware";
|
|
||||||
import { getToken } from "../../../actions/core/token";
|
import { getToken } from "../../../actions/core/token";
|
||||||
import { azureHeaders } from "../../../actions/core/headers";
|
import { azureHeaders } from "../../../actions/core/headers";
|
||||||
import { consoleLogger } from "../../../actions/core/logger";
|
import { consoleLogger } from "../../../actions/core/logger";
|
||||||
import { hashAPIPath } from "../../../actions/core/hash";
|
import { hashAPIPath } from "../../../actions/core/hash";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import CryptoJS from "crypto-js";
|
|
||||||
|
|
||||||
const WORDKEY = process.env.HASHKEY;
|
|
||||||
|
|
||||||
const WEBAPI_URL =
|
|
||||||
process.env.RELAY_ROOT ||
|
|
||||||
"https://dev-pedw-ns.servicebus.windows.net/dev-pedw-hc/";
|
|
||||||
|
|
||||||
const BASE_URL = process.env.API_ROOT || `http://localhost:${port}`;
|
const BASE_URL = process.env.API_ROOT || `http://localhost:${port}`;
|
||||||
|
|
||||||
|
const hasValue = (value) =>
|
||||||
|
typeof value === "string" && value.trim().length > 0;
|
||||||
|
|
||||||
export default async function ApiProxy(req, res) {
|
export default async function ApiProxy(req, res) {
|
||||||
var containerName = req.query.container;
|
var containerName = req.query.container;
|
||||||
var checkHash = req.query.hash;
|
|
||||||
|
|
||||||
if (typeof containerName === "undefined" || containerName.length === 0) {
|
if (!hasValue(containerName)) {
|
||||||
return res.status(400).json();
|
return res.status(400).json();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,36 +1,19 @@
|
|||||||
import {
|
|
||||||
downloadAllProgressFiles,
|
|
||||||
getAllProgressBlobs
|
|
||||||
} from "../../../actions/azurestorage";
|
|
||||||
import _ from "lodash";
|
|
||||||
import nextConnect from "next-connect";
|
|
||||||
import middleware from "../middleware/middleware";
|
|
||||||
import { getToken } from "../../../actions/core/token";
|
import { getToken } from "../../../actions/core/token";
|
||||||
import { azureHeaders } from "../../../actions/core/headers";
|
import { azureHeaders } from "../../../actions/core/headers";
|
||||||
import { consoleLogger } from "../../../actions/core/logger";
|
import { consoleLogger } from "../../../actions/core/logger";
|
||||||
import { hashAPIPath } from "../../../actions/core/hash";
|
import { hashAPIPath } from "../../../actions/core/hash";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import CryptoJS from "crypto-js";
|
|
||||||
|
|
||||||
const WORDKEY = process.env.HASHKEY;
|
|
||||||
|
|
||||||
const WEBAPI_URL =
|
|
||||||
process.env.RELAY_ROOT ||
|
|
||||||
"https://dev-pedw-ns.servicebus.windows.net/dev-pedw-hc/";
|
|
||||||
|
|
||||||
const BASE_URL = process.env.API_ROOT || `http://localhost:${port}`;
|
const BASE_URL = process.env.API_ROOT || `http://localhost:${port}`;
|
||||||
|
|
||||||
|
const hasValue = (value) =>
|
||||||
|
typeof value === "string" && value.trim().length > 0;
|
||||||
|
|
||||||
export default async function ApiProxy(req, res) {
|
export default async function ApiProxy(req, res) {
|
||||||
var containerName = req.query.container;
|
var containerName = req.query.container;
|
||||||
var casefolderID = req.query.casefolderID;
|
var casefolderID = req.query.casefolderID;
|
||||||
var checkHash = req.query.hash;
|
|
||||||
|
|
||||||
if (
|
if (!hasValue(containerName) || !hasValue(casefolderID)) {
|
||||||
typeof containerName === "undefined" ||
|
|
||||||
containerName.length === 0 ||
|
|
||||||
typeof casefolderID === "undefined" ||
|
|
||||||
casefolderID.length === 0
|
|
||||||
) {
|
|
||||||
return res.status(400).json();
|
return res.status(400).json();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,30 +1,18 @@
|
|||||||
import {
|
|
||||||
downloadAllProgressFiles,
|
|
||||||
getAllProgressBlobs
|
|
||||||
} from "../../../actions/azurestorage";
|
|
||||||
import _ from "lodash";
|
|
||||||
import nextConnect from "next-connect";
|
|
||||||
import middleware from "../middleware/middleware";
|
|
||||||
import { getToken } from "../../../actions/core/token";
|
import { getToken } from "../../../actions/core/token";
|
||||||
import { azureHeaders } from "../../../actions/core/headers";
|
import { azureHeaders } from "../../../actions/core/headers";
|
||||||
import { consoleLogger } from "../../../actions/core/logger";
|
import { consoleLogger } from "../../../actions/core/logger";
|
||||||
import { hashAPIPath } from "../../../actions/core/hash";
|
import { hashAPIPath } from "../../../actions/core/hash";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import CryptoJS from "crypto-js";
|
|
||||||
|
|
||||||
const WORDKEY = process.env.HASHKEY;
|
|
||||||
|
|
||||||
const WEBAPI_URL =
|
|
||||||
process.env.RELAY_ROOT ||
|
|
||||||
"https://dev-pedw-ns.servicebus.windows.net/dev-pedw-hc/";
|
|
||||||
|
|
||||||
const BASE_URL = process.env.API_ROOT || `http://localhost:${port}`;
|
const BASE_URL = process.env.API_ROOT || `http://localhost:${port}`;
|
||||||
|
|
||||||
|
const hasValue = (value) =>
|
||||||
|
typeof value === "string" && value.trim().length > 0;
|
||||||
|
|
||||||
export default async function ApiProxy(req, res) {
|
export default async function ApiProxy(req, res) {
|
||||||
var containerName = req.query.container;
|
var containerName = req.query.container;
|
||||||
var checkHash = req.query.hash;
|
|
||||||
|
|
||||||
if (typeof containerName === "undefined" || containerName.length === 0) {
|
if (!hasValue(containerName)) {
|
||||||
return res.status(400).json();
|
return res.status(400).json();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -29,18 +29,18 @@ ApiProxy.get(async (req, res) => {
|
|||||||
|
|
||||||
var checkquerypath = "/api/file/setupcontainer?ident=" + containerName;
|
var checkquerypath = "/api/file/setupcontainer?ident=" + containerName;
|
||||||
|
|
||||||
if (hashAPIPath(checkquerypath) == "&hash=" + checkHash) {
|
if (hashAPIPath(checkquerypath) != "&hash=" + checkHash) {
|
||||||
await createContainer(containerName)
|
|
||||||
.then((data) => {
|
|
||||||
return res.status(200).json({ data: "success", output: data });
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
consoleLogger(error);
|
|
||||||
res.status(400).json(error);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
return res.status(400).json();
|
return res.status(400).json();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await createContainer(containerName)
|
||||||
|
.then((data) => {
|
||||||
|
return res.status(200).json({ data: "success", output: data });
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
consoleLogger(error);
|
||||||
|
res.status(400).json(error);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
export const config = {
|
export const config = {
|
||||||
|
|||||||
@@ -14,6 +14,10 @@ ApiProxy.use(middleware);
|
|||||||
ApiProxy.post(async (req, res) => {
|
ApiProxy.post(async (req, res) => {
|
||||||
var checkHash = req.query.hash;
|
var checkHash = req.query.hash;
|
||||||
|
|
||||||
|
if (typeof checkHash === "undefined" || checkHash.length === 0) {
|
||||||
|
return res.status(400).json();
|
||||||
|
}
|
||||||
|
|
||||||
var checkquerypath = "/api/file/upload";
|
var checkquerypath = "/api/file/upload";
|
||||||
|
|
||||||
if (hashAPIPath(checkquerypath) != "?hash=" + checkHash) {
|
if (hashAPIPath(checkquerypath) != "?hash=" + checkHash) {
|
||||||
|
|||||||
@@ -37,6 +37,11 @@ ApiProxy.use(middleware);
|
|||||||
|
|
||||||
ApiProxy.post(async (req, res) => {
|
ApiProxy.post(async (req, res) => {
|
||||||
var checkHash = req.query.hash;
|
var checkHash = req.query.hash;
|
||||||
|
|
||||||
|
if (typeof checkHash === "undefined" || checkHash.length === 0) {
|
||||||
|
return res.status(400).json();
|
||||||
|
}
|
||||||
|
|
||||||
var checkquerypath = "/api/file/uploadsinglefile";
|
var checkquerypath = "/api/file/uploadsinglefile";
|
||||||
|
|
||||||
if (hashAPIPath(checkquerypath) != "?hash=" + checkHash) {
|
if (hashAPIPath(checkquerypath) != "?hash=" + checkHash) {
|
||||||
|
|||||||
@@ -132,6 +132,92 @@ test("gethash_api returns hash for allow-listed getportallogin path", async () =
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("gethash_api returns hash for allow-listed deleteblobcase path", async () => {
|
||||||
|
const mod = loadModule("pages/api/endpoint/gethash_api.js", {
|
||||||
|
hashAPIPath: () => "&hash=deletecase",
|
||||||
|
getSession: async () => ({ user: { id: "u1" } }),
|
||||||
|
nextConnect: createNextConnectMock(),
|
||||||
|
middleware: () => {}
|
||||||
|
});
|
||||||
|
|
||||||
|
const req = {
|
||||||
|
query: { path: "/api/file/deleteblobcase?container=c1&casefolderID=r1" }
|
||||||
|
};
|
||||||
|
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=deletecase"
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("gethash_api returns hash for allow-listed deleteblobrep path", async () => {
|
||||||
|
const mod = loadModule("pages/api/endpoint/gethash_api.js", {
|
||||||
|
hashAPIPath: () => "&hash=deleterep",
|
||||||
|
getSession: async () => ({ user: { id: "u1" } }),
|
||||||
|
nextConnect: createNextConnectMock(),
|
||||||
|
middleware: () => {}
|
||||||
|
});
|
||||||
|
|
||||||
|
const req = {
|
||||||
|
query: {
|
||||||
|
path: "/api/file/deleteblobrep?container=c1&casefolderID=r1&repfile=f1"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
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=deleterep"
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("gethash_api returns hash for allow-listed deletewatchedcases path", async () => {
|
||||||
|
const mod = loadModule("pages/api/endpoint/gethash_api.js", {
|
||||||
|
hashAPIPath: () => "&hash=watch",
|
||||||
|
getSession: async () => ({ user: { id: "u1" } }),
|
||||||
|
nextConnect: createNextConnectMock(),
|
||||||
|
middleware: () => {}
|
||||||
|
});
|
||||||
|
|
||||||
|
const req = {
|
||||||
|
query: {
|
||||||
|
path: "/api/endpoint/deletewatchedcases_api?watchedCaseID=123"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
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=watch"
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("gethash_api returns hash for allow-listed deletemyrepresentations path", async () => {
|
||||||
|
const mod = loadModule("pages/api/endpoint/gethash_api.js", {
|
||||||
|
hashAPIPath: () => "&hash=delrep",
|
||||||
|
getSession: async () => ({ user: { id: "u1" } }),
|
||||||
|
nextConnect: createNextConnectMock(),
|
||||||
|
middleware: () => {}
|
||||||
|
});
|
||||||
|
|
||||||
|
const req = {
|
||||||
|
query: {
|
||||||
|
path: "/api/endpoint/deletemyrepresentations_api?myRepresentationsID=abc"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
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=delrep"
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
test("gethash_api rejects unauthenticated requests with 401", async () => {
|
test("gethash_api rejects unauthenticated requests with 401", async () => {
|
||||||
const mod = loadModule("pages/api/endpoint/gethash_api.js", {
|
const mod = loadModule("pages/api/endpoint/gethash_api.js", {
|
||||||
hashAPIPath: () => "&hash=expected",
|
hashAPIPath: () => "&hash=expected",
|
||||||
|
|||||||
@@ -0,0 +1,245 @@
|
|||||||
|
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("upload rejects missing hash with 400", async () => {
|
||||||
|
const calls = [];
|
||||||
|
const nextConnect = createNextConnectMock();
|
||||||
|
|
||||||
|
const mod = loadModule("pages/api/file/upload.js", {
|
||||||
|
hashAPIPath: () => "?hash=expected",
|
||||||
|
createBlob: async (...args) => {
|
||||||
|
calls.push(args);
|
||||||
|
return { ok: true };
|
||||||
|
},
|
||||||
|
createRepBlob: async (...args) => {
|
||||||
|
calls.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(calls.length, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("uploadsinglefile rejects missing 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: {},
|
||||||
|
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("createappealcompletemessage_api rejects missing hash with 400", async () => {
|
||||||
|
const calls = [];
|
||||||
|
const nextConnect = createNextConnectMock();
|
||||||
|
|
||||||
|
const mod = loadModule(
|
||||||
|
"pages/api/file/createappealcompletemessage_api.js",
|
||||||
|
{
|
||||||
|
hashAPIPath: () => "&hash=expected",
|
||||||
|
getProgressBlobs: async (...args) => {
|
||||||
|
calls.push(args);
|
||||||
|
return { path: "p1" };
|
||||||
|
},
|
||||||
|
downloadProgressFile: async () => ({}),
|
||||||
|
createBlob: async () => ({}),
|
||||||
|
getCaseBlob: async () => ({}),
|
||||||
|
createCaseCompleteMessage: async () => ({}),
|
||||||
|
updateAccount: async () => ({}),
|
||||||
|
consoleLogger: () => {},
|
||||||
|
_: { isEmpty: (value) => !value },
|
||||||
|
nextConnect,
|
||||||
|
middleware: () => {}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const req = {
|
||||||
|
query: { container: "c1", tempcaseref: "tmp-1", inv: 846040001 }
|
||||||
|
};
|
||||||
|
const res = createRes();
|
||||||
|
|
||||||
|
await mod.default.handler(req, res);
|
||||||
|
assert.strictEqual(res.state.statusCode, 400);
|
||||||
|
assert.strictEqual(calls.length, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("setupcontainer rejects missing 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" } };
|
||||||
|
const res = createRes();
|
||||||
|
|
||||||
|
await mod.default.handler(req, res);
|
||||||
|
assert.strictEqual(res.state.statusCode, 400);
|
||||||
|
assert.strictEqual(createCalls.length, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("setupcontainer valid hash returns 200 with expected response shape", async () => {
|
||||||
|
const nextConnect = createNextConnectMock();
|
||||||
|
|
||||||
|
const mod = loadModule("pages/api/file/setupcontainer.js", {
|
||||||
|
hashAPIPath: () => "&hash=expected",
|
||||||
|
createContainer: async () => ({ container: "container-1" }),
|
||||||
|
createContainerSas: async () => {},
|
||||||
|
getContainers: async () => {},
|
||||||
|
getBlobs: async () => {},
|
||||||
|
uploadFile: async () => {},
|
||||||
|
consoleLogger: () => {},
|
||||||
|
nextConnect,
|
||||||
|
middleware: () => {}
|
||||||
|
});
|
||||||
|
|
||||||
|
const req = { query: { ident: "container-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)), {
|
||||||
|
data: "success",
|
||||||
|
output: { container: "container-1" }
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const run = async () => {
|
||||||
|
let passed = 0;
|
||||||
|
|
||||||
|
for (const currentTest of tests) {
|
||||||
|
await currentTest.fn();
|
||||||
|
passed += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`Phase 15 behavioural tests passed (${passed}/${tests.length}).`
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
run().catch((error) => {
|
||||||
|
console.error(error);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
@@ -0,0 +1,147 @@
|
|||||||
|
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 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("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("getrepsblobproxy rejects missing container with 400", async () => {
|
||||||
|
const mod = loadModule("pages/api/file/getrepsblobproxy.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);
|
||||||
|
});
|
||||||
|
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("getbloblistproxy valid input returns 200 with existing response shape", async () => {
|
||||||
|
const mod = loadModule("pages/api/file/getbloblistproxy.js", {
|
||||||
|
getToken: async () => ({ access_token: "t" }),
|
||||||
|
hashAPIPath: () => "&hash=expected",
|
||||||
|
azureHeaders: () => ({}),
|
||||||
|
axios: { get: async () => ({ data: [{ name: "a" }] }) },
|
||||||
|
consoleLogger: () => {}
|
||||||
|
});
|
||||||
|
|
||||||
|
const req = { query: { container: "c1", casefolderID: "case-1" } };
|
||||||
|
const res = createRes();
|
||||||
|
await mod.default(req, res);
|
||||||
|
assert.strictEqual(res.state.statusCode, 200);
|
||||||
|
assert.deepStrictEqual(res.state.jsonBody, [{ name: "a" }]);
|
||||||
|
});
|
||||||
|
|
||||||
|
const run = async () => {
|
||||||
|
let passed = 0;
|
||||||
|
for (const t of tests) {
|
||||||
|
await t.fn();
|
||||||
|
passed += 1;
|
||||||
|
}
|
||||||
|
console.log(
|
||||||
|
`Phase 16 behavioural tests passed (${passed}/${tests.length}).`
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
run().catch((error) => {
|
||||||
|
console.error(error);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
@@ -155,10 +155,15 @@ test("document/getAwaitingSubmissionFromBlob logs and returns undefined on failu
|
|||||||
test("portal/deleteMyRepresentations appends hash and returns data", async () => {
|
test("portal/deleteMyRepresentations appends hash and returns data", async () => {
|
||||||
const axios = createAxiosMock();
|
const axios = createAxiosMock();
|
||||||
const logger = createLoggerMock();
|
const logger = createLoggerMock();
|
||||||
const hashCalls = [];
|
const signCalls = [];
|
||||||
const hashAPIPath = (queryPath) => {
|
|
||||||
hashCalls.push(queryPath);
|
axios.getHandler = async (url) => {
|
||||||
return "&hash=portal123";
|
if (url.startsWith("/api/endpoint/gethash_api?path=")) {
|
||||||
|
signCalls.push(url);
|
||||||
|
return { data: { hash: "&hash=portal123" } };
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error("Unexpected get url: " + url);
|
||||||
};
|
};
|
||||||
|
|
||||||
axios.requestHandler = async () => ({ data: { removed: true } });
|
axios.requestHandler = async () => ({ data: { removed: true } });
|
||||||
@@ -167,28 +172,39 @@ test("portal/deleteMyRepresentations appends hash and returns data", async () =>
|
|||||||
axios,
|
axios,
|
||||||
BASE_URL: "",
|
BASE_URL: "",
|
||||||
consoleLogger: logger.consoleLogger,
|
consoleLogger: logger.consoleLogger,
|
||||||
hashAPIPath
|
hashAPIPath: () => "&hash=fallback"
|
||||||
});
|
});
|
||||||
|
|
||||||
const result = await portal.deleteMyRepresentations("rep-1");
|
const result = await portal.deleteMyRepresentations("rep-1");
|
||||||
|
|
||||||
assert.deepStrictEqual(normalize(result), { removed: true });
|
assert.deepStrictEqual(normalize(result), { removed: true });
|
||||||
assert.strictEqual(hashCalls.length, 1);
|
|
||||||
assert.strictEqual(
|
assert.strictEqual(
|
||||||
hashCalls[0],
|
signCalls[0],
|
||||||
"/api/endpoint/deletemyrepresentations_api?myRepresentationsID=rep-1"
|
"/api/endpoint/gethash_api?path=%2Fapi%2Fendpoint%2Fdeletemyrepresentations_api%3FmyRepresentationsID%3Drep-1"
|
||||||
);
|
);
|
||||||
assert.strictEqual(
|
assert.strictEqual(
|
||||||
axios.calls[0].config.url,
|
axios.calls[0].url,
|
||||||
|
"/api/endpoint/gethash_api?path=%2Fapi%2Fendpoint%2Fdeletemyrepresentations_api%3FmyRepresentationsID%3Drep-1"
|
||||||
|
);
|
||||||
|
assert.strictEqual(
|
||||||
|
axios.calls[1].config.url,
|
||||||
"/api/endpoint/deletemyrepresentations_api?myRepresentationsID=rep-1&hash=portal123"
|
"/api/endpoint/deletemyrepresentations_api?myRepresentationsID=rep-1&hash=portal123"
|
||||||
);
|
);
|
||||||
assert.strictEqual(axios.calls[0].config.method, "delete");
|
assert.strictEqual(axios.calls[1].config.method, "delete");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("portal/deleteMyRepresentations logs and returns undefined on failure", async () => {
|
test("portal/deleteMyRepresentations logs and returns undefined on failure", async () => {
|
||||||
const axios = createAxiosMock();
|
const axios = createAxiosMock();
|
||||||
const logger = createLoggerMock();
|
const logger = createLoggerMock();
|
||||||
|
|
||||||
|
axios.getHandler = async (url) => {
|
||||||
|
if (url.startsWith("/api/endpoint/gethash_api?path=")) {
|
||||||
|
return { data: { hash: "&hash=portal-fail" } };
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error("Unexpected get url: " + url);
|
||||||
|
};
|
||||||
|
|
||||||
axios.requestHandler = async () =>
|
axios.requestHandler = async () =>
|
||||||
Promise.reject(createAxiosError(401, "Unauthorized"));
|
Promise.reject(createAxiosError(401, "Unauthorized"));
|
||||||
|
|
||||||
@@ -196,7 +212,7 @@ test("portal/deleteMyRepresentations logs and returns undefined on failure", asy
|
|||||||
axios,
|
axios,
|
||||||
BASE_URL: "",
|
BASE_URL: "",
|
||||||
consoleLogger: logger.consoleLogger,
|
consoleLogger: logger.consoleLogger,
|
||||||
hashAPIPath: () => "&hash=portal-fail"
|
hashAPIPath: () => "&hash=fallback"
|
||||||
});
|
});
|
||||||
|
|
||||||
const result = await portal.deleteMyRepresentations("rep-2");
|
const result = await portal.deleteMyRepresentations("rep-2");
|
||||||
@@ -205,6 +221,87 @@ test("portal/deleteMyRepresentations logs and returns undefined on failure", asy
|
|||||||
assert.strictEqual(logger.calls.length, 1);
|
assert.strictEqual(logger.calls.length, 1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("portal/deleteWatchedCases uses signer hash and returns data", async () => {
|
||||||
|
const axios = createAxiosMock();
|
||||||
|
const logger = createLoggerMock();
|
||||||
|
const signCalls = [];
|
||||||
|
|
||||||
|
axios.getHandler = async (url) => {
|
||||||
|
if (url.startsWith("/api/endpoint/gethash_api?path=")) {
|
||||||
|
signCalls.push(url);
|
||||||
|
return { data: { hash: "&hash=watch123" } };
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error("Unexpected get url: " + url);
|
||||||
|
};
|
||||||
|
|
||||||
|
axios.requestHandler = async () => ({ data: { removed: true } });
|
||||||
|
|
||||||
|
const portal = loadServiceModule("portalDirectService.js", {
|
||||||
|
axios,
|
||||||
|
BASE_URL: "",
|
||||||
|
consoleLogger: logger.consoleLogger,
|
||||||
|
hashAPIPath: () => "&hash=fallback"
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await portal.deleteWatchedCases("watch-1");
|
||||||
|
|
||||||
|
assert.deepStrictEqual(normalize(result), { removed: true });
|
||||||
|
assert.strictEqual(
|
||||||
|
signCalls[0],
|
||||||
|
"/api/endpoint/gethash_api?path=%2Fapi%2Fendpoint%2Fdeletewatchedcases_api%3FwatchedCaseID%3Dwatch-1"
|
||||||
|
);
|
||||||
|
assert.strictEqual(
|
||||||
|
axios.calls[1].config.url,
|
||||||
|
"/api/endpoint/deletewatchedcases_api?watchedCaseID=watch-1&hash=watch123"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("document delete blob flows use signer hash", async () => {
|
||||||
|
const axios = createAxiosMock();
|
||||||
|
const logger = createLoggerMock();
|
||||||
|
const signCalls = [];
|
||||||
|
|
||||||
|
axios.getHandler = async (url) => {
|
||||||
|
if (url.startsWith("/api/endpoint/gethash_api?path=")) {
|
||||||
|
signCalls.push(url);
|
||||||
|
return { data: { hash: "&hash=doc123" } };
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error("Unexpected get url: " + url);
|
||||||
|
};
|
||||||
|
|
||||||
|
axios.requestHandler = async () => ({ data: { ok: true } });
|
||||||
|
|
||||||
|
const document = loadServiceModule("documentDirectService.js", {
|
||||||
|
axios,
|
||||||
|
BASE_URL: "",
|
||||||
|
consoleLogger: logger.consoleLogger,
|
||||||
|
hashAPIPath: () => "&hash=fallback"
|
||||||
|
});
|
||||||
|
|
||||||
|
const one = await document.deleteAwaitingSubmissionsFromBlob(
|
||||||
|
"c1",
|
||||||
|
"case-1"
|
||||||
|
);
|
||||||
|
const two = await document.deleteMyRepresentationsFromBlob(
|
||||||
|
"c1",
|
||||||
|
"case-1",
|
||||||
|
"rep-a"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.deepStrictEqual(normalize(one), { ok: true });
|
||||||
|
assert.deepStrictEqual(normalize(two), { ok: true });
|
||||||
|
assert.strictEqual(
|
||||||
|
signCalls[0],
|
||||||
|
"/api/endpoint/gethash_api?path=%2Fapi%2Ffile%2Fdeleteblobcase%3Fcontainer%3Dc1%26casefolderID%3Dcase-1"
|
||||||
|
);
|
||||||
|
assert.strictEqual(
|
||||||
|
signCalls[1],
|
||||||
|
"/api/endpoint/gethash_api?path=%2Fapi%2Ffile%2Fdeleteblobrep%3Fcontainer%3Dc1%26casefolderID%3Dcase-1%26repfile%3Drep-a"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
test("account/getPortalLogin appends hash and returns res.data", async () => {
|
test("account/getPortalLogin appends hash and returns res.data", async () => {
|
||||||
const axios = createAxiosMock();
|
const axios = createAxiosMock();
|
||||||
const logger = createLoggerMock();
|
const logger = createLoggerMock();
|
||||||
|
|||||||
Reference in New Issue
Block a user