From fffe1d19dd12a5db9d06b2ad3b3d89b201353cfb Mon Sep 17 00:00:00 2001 From: robbond Date: Sat, 14 Mar 2026 06:52:17 +0000 Subject: [PATCH] TASK22057: phase18 additional consolidation slices --- memory-bank/activeContext.md | 27 ++ memory-bank/change-log.md | 58 +++++ memory-bank/refactor-plan-actions-index.md | 44 ++++ pages/api/endpoint/deletewatchedcases_api.js | 7 +- pages/api/endpoint/getaccounts_api.js | 32 ++- .../api/endpoint/getappealpdfdocuments_api.js | 9 +- pages/api/endpoint/getbasicsearchpaged_api.js | 7 +- pages/api/file/createcase_api.js | 15 +- .../file/deleteawaitingsubmissionfromblob.js | 11 +- pages/api/file/deleteblob.js | 11 +- pages/api/file/editRepJson.js | 5 +- pages/api/file/getbloblist.js | 24 +- pages/api/file/getprogressobjblob.js | 20 +- pages/api/file/updatecase_api.js | 13 + tests/phase18/service-behaviour.test.cjs | 243 ++++++++++++++++++ 15 files changed, 460 insertions(+), 66 deletions(-) create mode 100644 tests/phase18/service-behaviour.test.cjs diff --git a/memory-bank/activeContext.md b/memory-bank/activeContext.md index e0409ff9..d3127d98 100644 --- a/memory-bank/activeContext.md +++ b/memory-bank/activeContext.md @@ -568,6 +568,33 @@ - Broader consistency scan note: - no additional broad hash/400 consistency gaps were identified in `pages/api/endpoint/**` in this pass. +## Phase 18 additional consolidation slices (2026-03-14) + +- Continued on branch: + - `TASK22057-phase17-hardening-closeout` +- Completed all three requested additional slices: + 1. endpoint logging/input hygiene + 2. file hash-guard flow consistency + 3. create/update input-guard + logging hygiene +- Touched handlers: + - `pages/api/endpoint/deletewatchedcases_api.js` + - `pages/api/endpoint/getaccounts_api.js` + - `pages/api/endpoint/getbasicsearchpaged_api.js` + - `pages/api/endpoint/getappealpdfdocuments_api.js` + - `pages/api/file/deleteblob.js` + - `pages/api/file/deleteawaitingsubmissionfromblob.js` + - `pages/api/file/getprogressobjblob.js` + - `pages/api/file/getbloblist.js` + - `pages/api/file/createcase_api.js` + - `pages/api/file/updatecase_api.js` + - `pages/api/file/editRepJson.js` +- Outcome: + - consistent early 400 guard behavior for missing/invalid required inputs/hash in selected handlers + - noisy direct logs removed in touched sensitive flows + - no response-shape/signature contract changes intended +- Added focused test pack: + - `tests/phase18/service-behaviour.test.cjs` (8/8 pass) + ## Likely next steps 1. Stabilize and simplify breadcrumb/back-link decision logic with focused regression checks. diff --git a/memory-bank/change-log.md b/memory-bank/change-log.md index 1115146e..30d0ec7c 100644 --- a/memory-bank/change-log.md +++ b/memory-bank/change-log.md @@ -1156,3 +1156,61 @@ Validation: Follow-ups: - If desired, run additional integrated HTTP/manual checks in an environment with full relay/storage dependencies for end-to-end happy-path confirmation beyond mocked behaviour tests. + +--- + +### CL-032: Phase 18 additional consolidation slices — endpoint/file consistency and guard hygiene + +date: 2026-03-14 +author: Cline +scope: `pages/api/endpoint/{deletewatchedcases_api,getaccounts_api,getbasicsearchpaged_api,getappealpdfdocuments_api}.js`, `pages/api/file/{deleteblob,deleteawaitingsubmissionfromblob,getprogressobjblob,getbloblist,createcase_api,updatecase_api,editRepJson}.js`, `tests/phase18/service-behaviour.test.cjs`, `memory-bank/*` +type: change +rationale: Complete the user-requested three additional small, reversible hardening slices (logging hygiene, hash-guard flow consistency, and required-input guard consistency) without broad refactor or contract drift. +impact: Improves consistency of negative-path behavior and reduces noisy logging in selected sensitive handlers while preserving response/signature contracts. +status: completed + +Summary: + +- Implemented all three requested additional slices: + 1. endpoint logging/input hygiene + 2. file hash-guard flow consistency + 3. create/update input-guard + logging hygiene +- Endpoint updates: + - `deletewatchedcases_api`: added required `watchedCaseID` early 400 guard and removed direct URL logging. + - `getaccounts_api`: added required `emailAddress` early 400 guard and removed direct query logging. + - `getbasicsearchpaged_api`: added required `searchString` early 400 guard and removed noisy query logging. + - `getappealpdfdocuments_api`: added required `incidentid` early 400 guard and removed direct URL logging. +- File handler updates: + - standardized hash-check flow by removing redundant post-validation branches in: + - `deleteblob` + - `deleteawaitingsubmissionfromblob` + - `getprogressobjblob` + - `getbloblist` + - added required-input early 400 guards in: + - `createcase_api` (contact/appealType/container/lpa) + - `updatecase_api` (collection/appealObj/incident/body) + - removed noisy tag logging in `editRepJson`. +- Added focused test pack: + - `tests/phase18/service-behaviour.test.cjs` with negative-path and contract-preserving happy-path checks (8/8 pass). + +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 (12/12) +- `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 (9/9) +- `node tests/phase15/service-behaviour.test.cjs` -> pass (5/5) +- `node tests/phase16/service-behaviour.test.cjs` -> pass (4/4) +- `node tests/phase17/service-behaviour.test.cjs` -> pass (6/6) +- `node tests/phase18/service-behaviour.test.cjs` -> pass (8/8) +- `npm run lint` -> warnings only (pre-existing `react-hooks/exhaustive-deps` warnings) + +Follow-ups: + +- Optional: execute integrated manual HTTP matrix for phase-18 touched handlers in an environment with full relay/storage dependencies for non-mocked happy-path verification. diff --git a/memory-bank/refactor-plan-actions-index.md b/memory-bank/refactor-plan-actions-index.md index e6826577..0f6f1e06 100644 --- a/memory-bank/refactor-plan-actions-index.md +++ b/memory-bank/refactor-plan-actions-index.md @@ -612,6 +612,50 @@ Status key: `[x] done`, `[ ] pending` - `tests/phase17/service-behaviour.test.cjs` - Re-run phase6–phase17 baseline tests and lint after rollback to confirm parity. +### Phase 18 additional consolidation slices snapshot (2026-03-14) + +Status key: `[x] done`, `[ ] pending` + +1. `[x]` Confirm requested scope expansion to implement all 3 additional slices +2. `[x]` Apply endpoint logging/input hygiene slice + - `pages/api/endpoint/deletewatchedcases_api.js` + - `pages/api/endpoint/getaccounts_api.js` + - `pages/api/endpoint/getbasicsearchpaged_api.js` + - `pages/api/endpoint/getappealpdfdocuments_api.js` +3. `[x]` Apply file hash-guard flow consistency slice + - `pages/api/file/deleteblob.js` + - `pages/api/file/deleteawaitingsubmissionfromblob.js` + - `pages/api/file/getprogressobjblob.js` + - `pages/api/file/getbloblist.js` +4. `[x]` Apply create/update input-guard + logging hygiene slice + - `pages/api/file/createcase_api.js` + - `pages/api/file/updatecase_api.js` + - `pages/api/file/editRepJson.js` +5. `[x]` Add focused phase-18 behavioural tests + - added `tests/phase18/service-behaviour.test.cjs` + - negative-path coverage for required inputs/hash + - contract-preserving happy-path checks on selected handlers +6. `[x]` Execute validation bundle + - phase6 parity + phase6–phase18 behavioural tests all pass + - lint warnings only (pre-existing `react-hooks/exhaustive-deps` set) + +### Phase 18 rollback notes + +- Revert files in this slice: + - `pages/api/endpoint/deletewatchedcases_api.js` + - `pages/api/endpoint/getaccounts_api.js` + - `pages/api/endpoint/getbasicsearchpaged_api.js` + - `pages/api/endpoint/getappealpdfdocuments_api.js` + - `pages/api/file/deleteblob.js` + - `pages/api/file/deleteawaitingsubmissionfromblob.js` + - `pages/api/file/getprogressobjblob.js` + - `pages/api/file/getbloblist.js` + - `pages/api/file/createcase_api.js` + - `pages/api/file/updatecase_api.js` + - `pages/api/file/editRepJson.js` + - `tests/phase18/service-behaviour.test.cjs` +- Re-run phase6–phase18 baseline tests and lint after rollback to confirm parity. + ## 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. diff --git a/pages/api/endpoint/deletewatchedcases_api.js b/pages/api/endpoint/deletewatchedcases_api.js index e569e72a..0830dc6a 100644 --- a/pages/api/endpoint/deletewatchedcases_api.js +++ b/pages/api/endpoint/deletewatchedcases_api.js @@ -35,11 +35,14 @@ const hashAPIPath = (queryPath) => { export default async function ApiProxy(req, res) { var watchedCaseID = req.query.watchedCaseID; + + if (typeof watchedCaseID === "undefined" || watchedCaseID.length === 0) { + return res.status(400).json(); + } + var token = await getToken(); var queryUrl = "pinswg_watchlists(" + watchedCaseID + ")"; - console.log(WEBAPI_URL + queryUrl + hashAPIPath(queryUrl)); - var config = { method: "delete", url: WEBAPI_URL + queryUrl + hashAPIPath(queryUrl), diff --git a/pages/api/endpoint/getaccounts_api.js b/pages/api/endpoint/getaccounts_api.js index d960601a..dbffd30c 100644 --- a/pages/api/endpoint/getaccounts_api.js +++ b/pages/api/endpoint/getaccounts_api.js @@ -39,6 +39,11 @@ const hashAPIPath = (queryPath) => { export default async function ApiProxy(req, res) { var emailAddress = req.query.emailAddress; + + if (typeof emailAddress === "undefined" || emailAddress.length === 0) { + return res.status(400).json(); + } + var token = await getToken(); var queryUrl = @@ -46,25 +51,24 @@ export default async function ApiProxy(req, res) { emailAddress + "'&$count=true&$select=emailaddress1, contactid"; - console.log(queryUrl); // var queryUrl = "contacts/?$count=true&$select=emailaddress1, contactid"; var apiResponse = _.isEmpty(req.query) ? res.status(400).json() : typeof emailAddress != "undefined" && emailAddress.length > 0 - ? axios - .get( - WEBAPI_URL + queryUrl + hashAPIPath(queryUrl), - azureHeaders(token.access_token) - ) - .then(({ data }) => { - res.status(200).json(data); - }) - .catch((error) => { - consoleLogger(error); - res.status(400).json(error); - }) - : res.status(400).json(); + ? axios + .get( + WEBAPI_URL + queryUrl + hashAPIPath(queryUrl), + azureHeaders(token.access_token) + ) + .then(({ data }) => { + res.status(200).json(data); + }) + .catch((error) => { + consoleLogger(error); + res.status(400).json(error); + }) + : res.status(400).json(); return apiResponse; } diff --git a/pages/api/endpoint/getappealpdfdocuments_api.js b/pages/api/endpoint/getappealpdfdocuments_api.js index 2da5265d..36832906 100644 --- a/pages/api/endpoint/getappealpdfdocuments_api.js +++ b/pages/api/endpoint/getappealpdfdocuments_api.js @@ -60,7 +60,7 @@ const groupArray = (arr) => { obj[ "pinswg_isharedocumentlocations@OData.Community.Display.V1.FormattedValue" ], // Change 'name' to 'userName' - count: count, // Add 'count' as 'occurrences' + count: count // Add 'count' as 'occurrences' }; result.push(transformedObj); @@ -71,6 +71,11 @@ const groupArray = (arr) => { export default async function ApiProxy(req, res) { var incidentID = req.query.incidentid; + + if (typeof incidentID === "undefined" || incidentID.length === 0) { + return res.status(400).json(); + } + var token = await getToken(); var queryUrl = @@ -78,8 +83,6 @@ export default async function ApiProxy(req, res) { incidentID + " and not(contains(pinswg_name,'_Appeal_Form.pdf'))&$select=pinswg_name,pinswg_isharedocumentlocations"; - console.log("docu: ", WEBAPI_URL + queryUrl + hashAPIPath(queryUrl)); - return axios .get( WEBAPI_URL + queryUrl + hashAPIPath(queryUrl), diff --git a/pages/api/endpoint/getbasicsearchpaged_api.js b/pages/api/endpoint/getbasicsearchpaged_api.js index 4835d506..547f5846 100644 --- a/pages/api/endpoint/getbasicsearchpaged_api.js +++ b/pages/api/endpoint/getbasicsearchpaged_api.js @@ -56,6 +56,11 @@ const WEBAPI_URL = export default async function ApiProxy(req, res) { var searchString = req.query.searchString; var pageNumber = req.query.pageNumber; + + if (typeof searchString === "undefined" || searchString.length === 0) { + return res.status(400).json(); + } + var token = await getToken(); var orderby = req.query.orderby; @@ -84,8 +89,6 @@ export default async function ApiProxy(req, res) { ? "&$skiptoken=" + '' : ""); - console.log("basic search paged:", queryUrl); - var apiResponse = searchString.length > 0 ? axios diff --git a/pages/api/file/createcase_api.js b/pages/api/file/createcase_api.js index 7c972240..b3aed571 100644 --- a/pages/api/file/createcase_api.js +++ b/pages/api/file/createcase_api.js @@ -36,6 +36,19 @@ export default async function ApiProxy(req, res) { var token = await getToken(); var tempCaseRef = getTempCaseRef(); + if ( + typeof contactid === "undefined" || + contactid.length === 0 || + typeof appealTypeId === "undefined" || + appealTypeId.length === 0 || + typeof containerName === "undefined" || + containerName.length === 0 || + typeof lpaID === "undefined" || + lpaID.length === 0 + ) { + return res.status(400).json(); + } + var data = { "title": tempCaseRef, "ticketnumber": tempCaseRef, @@ -53,8 +66,6 @@ export default async function ApiProxy(req, res) { delete newData.lpaTypes; delete newData.appealTypes; - console.log("/////Create Case:\n", newData, "\n//////////////"); - var newAppealObj = { "pinswg_name": tempCaseRef, "pinswg_appealcasetype": appealTypeId, diff --git a/pages/api/file/deleteawaitingsubmissionfromblob.js b/pages/api/file/deleteawaitingsubmissionfromblob.js index 33dfe05b..cc99f15e 100644 --- a/pages/api/file/deleteawaitingsubmissionfromblob.js +++ b/pages/api/file/deleteawaitingsubmissionfromblob.js @@ -38,14 +38,11 @@ ApiProxy.get(async (req, res) => { return res.status(400).json(); } - if (hashAPIPath(checkquerypath) == "&hash=" + checkHash) { - await deleteBlob( - containerName, - casefolderID + "/files/" + blobName - ).then((data) => { + await deleteBlob(containerName, casefolderID + "/files/" + blobName).then( + (data) => { return res.status(200).json({ data: data }); - }); - } + } + ); }); export const config = { diff --git a/pages/api/file/deleteblob.js b/pages/api/file/deleteblob.js index 9d99e045..7ff99c4f 100644 --- a/pages/api/file/deleteblob.js +++ b/pages/api/file/deleteblob.js @@ -38,14 +38,11 @@ ApiProxy.get(async (req, res) => { return res.status(400).json(); } - if (hashAPIPath(checkquerypath) == "&hash=" + checkHash) { - await deleteBlob( - containerName, - casefolderID + "/files/" + blobName - ).then((data) => { + await deleteBlob(containerName, casefolderID + "/files/" + blobName).then( + (data) => { return res.status(200).json({ data: data }); - }); - } + } + ); }); export const config = { diff --git a/pages/api/file/editRepJson.js b/pages/api/file/editRepJson.js index 65080769..20a19c4f 100644 --- a/pages/api/file/editRepJson.js +++ b/pages/api/file/editRepJson.js @@ -42,7 +42,7 @@ export default async function handler(req, res) { const tags = { containerid: json.containerID, caseID: json.ticketnumber, - blobType: "Representation", + blobType: "Representation" }; // Convert back to string @@ -53,11 +53,10 @@ export default async function handler(req, res) { updatedContent, Buffer.byteLength(updatedContent), { - blobHTTPHeaders: { blobContentType: "application/json" }, + blobHTTPHeaders: { blobContentType: "application/json" } } ); - console.log("the tags:", tags); const withTags = await blockBlobClient.setTags(tags); const withMeta = await blockBlobClient.setMetadata(tags); diff --git a/pages/api/file/getbloblist.js b/pages/api/file/getbloblist.js index a36baa52..ff56f48c 100644 --- a/pages/api/file/getbloblist.js +++ b/pages/api/file/getbloblist.js @@ -65,19 +65,17 @@ ApiProxy.get(async (req, res) => { return res.status(400).json(); } - if (hashAPIPath(checkquerypath) == "&hash=" + checkHash) { - casefolderID.split("/").length > 1 - ? await getRepsFilesBlobs( - containerName, - casefolderID.split("/")[0], - casefolderID.split("/")[1] - ).then((data) => { - return res.status(200).json({ "value": [data] }); - }) - : await getBlobs(containerName, casefolderID).then((data) => { - return res.status(200).json({ "value": [data] }); - }); - } + casefolderID.split("/").length > 1 + ? await getRepsFilesBlobs( + containerName, + casefolderID.split("/")[0], + casefolderID.split("/")[1] + ).then((data) => { + return res.status(200).json({ "value": [data] }); + }) + : await getBlobs(containerName, casefolderID).then((data) => { + return res.status(200).json({ "value": [data] }); + }); }); export const config = { diff --git a/pages/api/file/getprogressobjblob.js b/pages/api/file/getprogressobjblob.js index 8f5ef041..6df0a3ac 100644 --- a/pages/api/file/getprogressobjblob.js +++ b/pages/api/file/getprogressobjblob.js @@ -37,19 +37,13 @@ ApiProxy.get(async (req, res) => { return res.status(400).json(); } - if (hashAPIPath(checkquerypath) == "&hash=" + checkHash) { - const blobObj = await getProgressBlobs(containerName, casefolderID) - .then((data) => { - return downloadProgressFile( - containerName, - data.path, - casefolderID - ); - }) - .then((data) => { - return res.status(200).json(data); - }); - } + await getProgressBlobs(containerName, casefolderID) + .then((data) => { + return downloadProgressFile(containerName, data.path, casefolderID); + }) + .then((data) => { + return res.status(200).json(data); + }); }); export const config = { diff --git a/pages/api/file/updatecase_api.js b/pages/api/file/updatecase_api.js index 754dc1d0..9f36fd16 100644 --- a/pages/api/file/updatecase_api.js +++ b/pages/api/file/updatecase_api.js @@ -29,6 +29,19 @@ export default async function ApiProxy(req, res) { var updateFormCollection = req.query.updateFormCollection; var queryUrl = updateFormCollection + "(" + appealObj + ")"; + if ( + typeof updateFormCollection === "undefined" || + updateFormCollection.length === 0 || + typeof appealObj === "undefined" || + appealObj.length === 0 || + typeof incidentID === "undefined" || + incidentID.length === 0 || + typeof req.body === "undefined" || + req.body === null + ) { + return res.status(400).json(); + } + var token = await getToken(); var config = { diff --git a/tests/phase18/service-behaviour.test.cjs b/tests/phase18/service-behaviour.test.cjs new file mode 100644 index 00000000..3f1480da --- /dev/null +++ b/tests/phase18/service-behaviour.test.cjs @@ -0,0 +1,243 @@ +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("deletewatchedcases_api rejects missing watchedCaseID with 400", async () => { + let axiosCalls = 0; + const mod = loadModule("pages/api/endpoint/deletewatchedcases_api.js", { + axios: async () => { + axiosCalls += 1; + return { data: {} }; + }, + getToken: async () => ({ access_token: "token" }) + }); + + const req = { query: {} }; + const res = createRes(); + await mod.default(req, res); + + assert.strictEqual(res.state.statusCode, 400); + assert.strictEqual(axiosCalls, 0); +}); + +test("getaccounts_api rejects missing emailAddress with 400", async () => { + const mod = loadModule("pages/api/endpoint/getaccounts_api.js", { + _: { isEmpty: () => true }, + axios: { get: async () => ({ data: { value: [] } }) }, + getToken: async () => ({ access_token: "token" }), + hashAPIPath: () => "&hash=expected", + azureHeaders: () => ({}) + }); + + const req = { query: {} }; + const res = createRes(); + await mod.default(req, res); + + assert.strictEqual(res.state.statusCode, 400); +}); + +test("getbasicsearchpaged_api rejects missing searchString with 400", async () => { + const mod = loadModule("pages/api/endpoint/getbasicsearchpaged_api.js", { + axios: { get: async () => ({ data: { value: [] } }) }, + getToken: async () => ({ access_token: "token" }), + hashAPIPath: () => "&hash=expected", + azureHeadersPagedCustom: () => ({}) + }); + + const req = { query: { pageNumber: "1" } }; + const res = createRes(); + await mod.default(req, res); + + assert.strictEqual(res.state.statusCode, 400); +}); + +test("getappealpdfdocuments_api rejects missing incidentid with 400", async () => { + const mod = loadModule("pages/api/endpoint/getappealpdfdocuments_api.js", { + axios: { get: async () => ({ data: { value: [] } }) }, + getToken: async () => ({ access_token: "token" }), + hashAPIPath: () => "&hash=expected", + azureHeaders: () => ({}) + }); + + const req = { query: {} }; + const res = createRes(); + await mod.default(req, res); + + assert.strictEqual(res.state.statusCode, 400); +}); + +test("createcase_api rejects missing required query params with 400", async () => { + const blobCalls = []; + const mod = loadModule("pages/api/file/createcase_api.js", { + _: { isEmpty: () => true }, + getToken: async () => ({ access_token: "token" }), + getTempCaseRef: () => "TMP-1", + uuidv4: () => "uuid-1", + getCaseBlob: (...args) => blobCalls.push(args), + createBlob: (...args) => blobCalls.push(args) + }); + + const req = { query: {}, body: {} }; + const res = createRes(); + await mod.default(req, res); + + assert.strictEqual(res.state.statusCode, 400); + assert.strictEqual(blobCalls.length, 0); +}); + +test("updatecase_api rejects missing required inputs with 400", async () => { + const mod = loadModule("pages/api/file/updatecase_api.js", { + getToken: async () => ({ access_token: "token" }), + axios: async () => ({ data: { ok: true } }) + }); + + const req = { + query: { incident: "inc-1", appealObj: "appeal-1" }, + body: { title: "x" } + }; + const res = createRes(); + await mod.default(req, res); + + assert.strictEqual(res.state.statusCode, 400); +}); + +test("deleteblob valid hash still returns 200", async () => { + const mod = loadModule("pages/api/file/deleteblob.js", { + hashAPIPath: () => "&hash=expected", + deleteBlob: async () => true, + 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("getprogressobjblob valid hash still returns 200", async () => { + const mod = loadModule("pages/api/file/getprogressobjblob.js", { + hashAPIPath: () => "&hash=expected", + getProgressBlobs: async () => ({ path: "p1" }), + downloadProgressFile: async () => ({ filesList: [] }), + 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)), { + filesList: [] + }); +}); + +const run = async () => { + let passed = 0; + + for (const currentTest of tests) { + await currentTest.fn(); + passed += 1; + } + + console.log( + `Phase 18 behavioural tests passed (${passed}/${tests.length}).` + ); +}; + +run().catch((error) => { + console.error(error); + process.exit(1); +});