diff --git a/actions/azurestorage.js b/actions/azurestorage.js index ea104db1..f75d6000 100644 --- a/actions/azurestorage.js +++ b/actions/azurestorage.js @@ -1384,14 +1384,27 @@ export const getRepsBlobs = async (containerName) => { listOptions )) { const blobClient = containerClient.getBlobClient(blob.name); - //console.log("getreps blob:", blob); - blob.name.split("/")[2].indexOf("_rep.json") > 0 && - blob.name.split("/")[2].indexOf("undefined") < 0 && + + // Filter out soft-deleted/stale tag entries and malformed names. + const namePart = blob.name.split("/")[2] ?? ""; + if ( + namePart.indexOf("_rep.json") <= 0 || + namePart.indexOf("undefined") >= 0 + ) + continue; + + try { + const properties = await blobClient.getProperties(); + blobObj.push({ - "name": blob.name.split("/")[2], + "name": namePart, "path": blob.name, - "size": blobClient.getProperties().contentLength + "size": properties.contentLength }); + } catch (error) { + if (error?.statusCode === 404) continue; + throw error; + } } //console.log("blobObjwwwww:", blobObj); diff --git a/components/myportal.js b/components/myportal.js index 526df56a..4597b5a8 100644 --- a/components/myportal.js +++ b/components/myportal.js @@ -130,7 +130,6 @@ const MyPortal = (props) => { {!isLPA && ( )} - {/* {props.awaitingSubmission.awaitingSubmission[ "@odata.count" ] > 0 && ( @@ -147,9 +146,8 @@ const MyPortal = (props) => { "@odata.count" ]) && props.awaitingSubmission - .awaitingSubmissionFromBlob[ - "@odata.count" - ] > 0 && ( + .awaitingSubmissionFromBlob.case.length > + 0 && ( { isLPA={isLPA} /> )} - {props.myRepresentations.mySubmittedReps .mySubmittedReps.length > 0 && ( pass + - helper: 4/4 + - file-handler: 17/17 + - email-handler: 12/12 + - endpoint-handler: 149/149 +- `npm run lint` -> warnings only (pre-existing `react-hooks/exhaustive-deps`; no new lint errors) + +Follow-ups: + +- If desired, next slice can target remaining file-route parity candidates outside this bundle, but this closes the planned TASK22224 scope. + +--- + +### CL-005: TASK22224 downloadblob hotfix closure (path normalization + hash compatibility) + +date: 2026-03-23 +author: Cline +scope: `pages/api/file/downloadblob.js` +type: change +rationale: Close post-merge runtime regressions reported on live links where download URLs alternated between filename-only/full-path blob names and mixed encoded/raw hash input variants. +impact: Restored reliable blob downloads without relaxing hash security guarantees (still HMAC validated), and preserved existing caller compatibility across legacy/new URL encodings. +status: completed + +Summary: + +- Hotfix 1 (`f09f3b7`): normalized blob path resolution + - accepts both forms of `blobname` input: + - filename only (legacy) + - full prefixed path (already includes `casefolderID/...`) + - prevents double-prefix lookup failures + - sets attachment filename from final path segment only +- Hotfix 2 (`bd3bf68`): hash compatibility validation + - validates against a bounded set of canonical query-path variants (raw/encoded combinations for `casefolderID` and `blobname`) + - fixes `INVALID_HASH` false negatives for legitimate caller-generated links + - keeps strict HMAC requirement in place (no unauthenticated bypass) + +Validation: + +- `node tests/phase21/file-handler-contract.test.cjs` -> pass (17/17) +- `node tests/phase21/api-contract-slice1.test.cjs` -> pass + - helper: 4/4 + - file-handler: 17/17 + - email-handler: 12/12 + - endpoint-handler: 149/149 +- User confirmation: "downloadblob now works" + +Follow-ups: + +- Next recommended slice on this branch: complete file-route guard parity for `deleteblob.js`, `deleteblobcase.js`, and `deleteblobrep.js` by aligning hash validation canonicalization and explicit `respondError` contracts (`MISSING_REQUIRED_QUERY`, `INVALID_HASH`, operation-specific `*_FAILED`). +- Extend `tests/phase21/file-handler-contract.test.cjs` for the above routes with mixed encoded/raw hash cases to lock compatibility. + +--- + +### CL-006: TASK22224 file delete-route guard parity slice + +date: 2026-03-23 +author: Cline +scope: `pages/api/file/{deleteblob,deleteblobcase,deleteblobrep}.js`, `tests/phase21/file-handler-contract.test.cjs` +type: change +rationale: Execute the next planned slice to align hash/canonicalization behavior and negative-path contracts across high-risk file delete routes, matching the compatibility posture established for `downloadblob`. +impact: Reduces false `INVALID_HASH` failures for legitimate encoded/raw caller variants while preserving strict hash enforcement and improving resilience via explicit catch-path contracts. +status: completed + +Summary: + +- `deleteblob.js` + - added bounded hash candidate validation for encoded/raw combinations of `casefolderID` and `blobname` + - normalized delete path handling for both filename-only and already-prefixed blob paths + - added explicit catch-path contract: `DELETE_BLOB_FAILED` +- `deleteblobcase.js` + - added hash candidate validation for raw/encoded `casefolderID` + - added explicit catch-path contract: `DELETE_BLOB_CASE_FAILED` +- `deleteblobrep.js` + - added hash candidate validation for encoded/raw `casefolderID` + `repfile` + - added explicit catch-path contract: `DELETE_BLOB_REP_FAILED` +- Phase21 tests expanded (`file-handler-contract.test.cjs`): + - encoded hash-variant acceptance cases for all three delete routes + - explicit dependency-failure contract assertions for all three delete routes + +Validation: + +- `node tests/phase21/file-handler-contract.test.cjs` -> pass (23/23) +- `node tests/phase21/api-contract-slice1.test.cjs` -> pass + - helper: 4/4 + - file-handler: 23/23 + - email-handler: 12/12 + - endpoint-handler: 149/149 + +Follow-ups: + +- Optional next slice: apply same bounded hash-canonicalization parity to remaining high-sensitivity file routes where mixed encoded/raw callers may exist (`getbloblist`, `getprogressobjblob`) and add regression cases to phase21. + +--- + +### CL-007: TASK22224 getrepsblob stability hotfix after delete representation flow + +date: 2026-03-23 +author: Cline +scope: `actions/azurestorage.js` (`getRepsBlobs`), `tests/phase21/file-handler-contract.test.cjs` +type: change +rationale: Resolve reported runtime 400 (`GET_REPS_BLOB_FAILED`) after delete representation actions, caused by stale soft-deleted blob tag hits during representation blob enumeration. +impact: Prevents transient/stale Azure tag index entries from breaking representation retrieval, improving reliability of post-delete refresh without relaxing route security contracts. +status: completed + +Summary: + +- Hardened `getRepsBlobs(containerName)` in `actions/azurestorage.js`: + - fixed async misuse (`blobClient.getProperties().contentLength` without await) + - added existence/property guard with explicit `await blobClient.getProperties()` + - skips 404s (soft-deleted/stale tag index results) instead of throwing + - preserves behavior for non-404 failures (rethrow for proper error visibility) + - kept existing `_rep.json`/`undefined` name filtering intact +- Added phase21 contract coverage for `getrepsblob` route: + - success payload contract test + - dependency failure contract test (`GET_REPS_BLOB_FAILED`) + +Validation: + +- `node tests/phase21/file-handler-contract.test.cjs` -> pass (25/25) +- `node tests/phase21/api-contract-slice1.test.cjs` -> pass + - helper: 4/4 + - file-handler: 25/25 + - email-handler: 12/12 + - endpoint-handler: 149/149 + +Follow-ups: + +- Optional: add the same stale-tag existence guard pattern to any remaining Azure tag-list readers that still consume `findBlobsByTags` results without property existence verification. + +--- + +### CL-008: TASK22224 awaiting-submission route resilience parity hardening + +date: 2026-03-23 +author: Cline +scope: `pages/api/file/getawaitingsubmissionfromblob.js`, `tests/phase21/file-handler-contract.test.cjs` +type: change +rationale: Add explicit catch-path contract parity for awaiting-submission blob retrieval route so unexpected dependency failures return consistent, actionable error envelopes. +impact: Improves reliability/diagnostics for post-delete case refresh and aligns file-route error handling style without changing success payload contract or hash verification behavior. +status: completed + +Summary: + +- Refactored `getawaitingsubmissionfromblob` handler to structured `try/catch` flow. +- Preserved existing guard behavior: + - `MISSING_REQUIRED_QUERY` for missing container/hash + - `INVALID_HASH` for signature mismatch +- Added explicit dependency failure contract: + - `GET_AWAITING_SUBMISSION_BLOB_FAILED` (400) + - message: `Failed to retrieve awaiting submission blobs` +- Added phase21 coverage for this route: + - success payload pass-through contract + - dependency failure contract assertion + +Validation: + +- `node tests/phase21/file-handler-contract.test.cjs` -> pass (27/27) +- `node tests/phase21/api-contract-slice1.test.cjs` -> pass + - helper: 4/4 + - file-handler: 27/27 + - email-handler: 12/12 + - endpoint-handler: 149/149 + +Follow-ups: + +- Optional parity sweep: apply the same explicit catch-path contract pattern to remaining file routes that still rely on implicit promise-chain errors. + +--- + +### CL-009: TASK22224 proxy-route resilience and encoding parity bundle + +date: 2026-03-23 +author: Cline +scope: `pages/api/file/{getbloblistproxy,getrepsblobproxy,getawaitingsubmissionfromblobproxy,createappealcompletemessageproxy_api}.js`, `tests/phase21/file-handler-contract.test.cjs` +type: change +rationale: Continue larger-slice hardening by aligning proxy handlers with explicit async error handling and safer encoded upstream query forwarding for hash-based downstream calls. +impact: Improves proxy reliability and compatibility for encoded query values while preserving existing proxy error contracts and response behavior. +status: completed + +Summary: + +- `getbloblistproxy.js` + - converted `.then/.catch` chain to explicit `try/catch` + - encoded forwarded `container` and `casefolderID` query values + - preserved error contract: `GET_BLOB_LIST_PROXY_FAILED` +- `getrepsblobproxy.js` + - converted `.then/.catch` chain to explicit `try/catch` + - encoded forwarded `container` + - preserved error contract: `GET_REPS_BLOB_PROXY_FAILED` +- `getawaitingsubmissionfromblobproxy.js` + - converted `.then/.catch` chain to explicit `try/catch` + - preserved error contract: `GET_AWAITING_SUBMISSION_PROXY_FAILED` +- `createappealcompletemessageproxy_api.js` + - converted `.then/.catch` chain to explicit `try/catch` + - encoded forwarded `container` and `tempcaseref` + - preserved error contract: `CREATE_APPEAL_COMPLETE_MESSAGE_PROXY_FAILED` +- Phase21 tests expanded for proxy paths: + - getbloblistproxy success + dependency failure + - getrepsblobproxy success + - getawaitingsubmissionfromblobproxy dependency failure + - createappealcompletemessageproxy dependency failure + +Validation: + +- `node tests/phase21/file-handler-contract.test.cjs` -> pass (46/46) +- `node tests/phase21/api-contract-slice1.test.cjs` -> pass + - helper: 4/4 + - file-handler: 46/46 + - email-handler: 12/12 + - endpoint-handler: 149/149 + +Follow-ups: + +- Optional next big slice: bring remaining proxy/message routes using raw axios promise chains (`createcaseinvolvement_api.js`, `createrepinvolvement_api.js`, `updatecase_api.js`) onto the same async/await + explicit contract pattern. + +--- + +### CL-010: TASK22224 involvement/update route async contract hardening bundle + +date: 2026-03-23 +author: Cline +scope: `pages/api/file/{createcaseinvolvement_api,createrepinvolvement_api,updatecase_api}.js`, `tests/phase21/file-handler-contract.test.cjs` +type: change +rationale: Execute the next requested slice by modernizing remaining relay-backed involvement/update handlers that still used axios promise chains and legacy dead imports. +impact: Improves reliability/readability and preserves existing response contracts, including 412 "record exists" semantics for involvement creation flows. +status: completed + +Summary: + +- `createcaseinvolvement_api.js` + - removed unused `CryptoJS` import + - refactored axios `.then/.catch` to explicit `try/catch` + - preserved conflict behavior: status 412 -> success `{ record: "exists" }` + - preserved failure contract: `CREATE_CASE_INVOLVEMENT_FAILED` +- `createrepinvolvement_api.js` + - removed unused `CryptoJS` import + - refactored axios `.then/.catch` to explicit `try/catch` + - preserved conflict behavior: status 412 -> success `{ record: "exists" }` + - preserved failure contract: `CREATE_REP_INVOLVEMENT_FAILED` +- `updatecase_api.js` + - removed unused `CryptoJS` import + - refactored axios `.then/.catch` to explicit `try/catch` + - preserved failure contract: `UPDATE_CASE_FAILED` +- Phase21 tests expanded: + - createcaseinvolvement 412 conflict success contract + - createrepinvolvement dependency failure contract + - updatecase dependency failure contract + +Validation: + +- `node tests/phase21/file-handler-contract.test.cjs` -> pass (49/49) +- `node tests/phase21/api-contract-slice1.test.cjs` -> pass + - helper: 4/4 + - file-handler: 49/49 + - email-handler: 12/12 + - endpoint-handler: 149/149 + +Follow-ups: + +- Optional: apply equivalent modernization to any remaining relay-backed handlers outside `pages/api/file/` that still use raw axios promise chains and have no explicit phase21 contract assertions. + +--- + +### CL-011: TASK22224 aggressive non-file bundle (email/admin/endpoint parity) + +date: 2026-03-23 +author: Cline +scope: `pages/api/email/{getmailinglist,getcaseref,notify}.js`, `pages/api/admin/{getnewappeals_api,getlatestdocuments_api}.js`, `pages/api/endpoint/getportallogin_api.js`, `tests/phase21/endpoint-handler-contract.test.cjs` +type: change +rationale: Execute requested aggressive bundling for remaining non-file modernization/parity candidates: remove legacy promise chains and improve hash compatibility on login endpoint while preserving existing contracts. +impact: Improves consistency and resilience across email/admin/endpoint routes with no contract regressions; adds encoded hash-variant compatibility for portal login hash checks. +status: completed + +Summary: + +- `pages/api/email/getmailinglist.js` + - converted axios `.then/.catch` to `try/catch` + - preserved flattening behavior and error contract `MAILING_LIST_FETCH_FAILED` +- `pages/api/email/getcaseref.js` + - converted axios `.then/.catch` to `try/catch` + - preserved flattening behavior and error contract `CASE_REF_FETCH_FAILED` +- `pages/api/email/notify.js` + - converted notify client `.then/.catch` to `try/catch` + - preserved success payload and error contract `EMAIL_NOTIFY_FAILED` +- `pages/api/admin/getnewappeals_api.js` + - removed unused `CryptoJS` import + - converted axios `.then/.catch` to `try/catch` + - preserved `@odata.nextLink` normalization and error contract `ADMIN_NEW_APPEALS_FETCH_FAILED` +- `pages/api/admin/getlatestdocuments_api.js` + - converted axios `.then/.catch` to `try/catch` + - preserved flatten/enrich behavior and error contract `ADMIN_LATEST_DOCS_FETCH_FAILED` +- `pages/api/endpoint/getportallogin_api.js` + - retained required query/hash guards + - expanded hash validation to accept raw + encoded `emailAddress` query-path candidates + - preserved error contract `PORTAL_LOGIN_FETCH_FAILED` +- phase21 endpoint tests expanded: + - `getportallogin` encoded hash variant success path + - `getnewappeals_api` catch contract + - `getlatestdocuments_api` catch contract + +Validation: + +- `node tests/phase21/api-contract-slice1.test.cjs` -> pass + - helper: 4/4 + - file-handler: 49/49 + - email-handler: 12/12 + - endpoint-handler: 152/152 + +Follow-ups: + +- Remaining major modernization candidate is `pages/api/file/generateappealpdf.js` (+ optional `pages/api/file/generatepdf.js`) if we continue final closure slices. + +--- + +### CL-012: TASK22224 generatepdf/generateappealpdf async hardening slice + +date: 2026-03-23 +author: Cline +scope: `pages/api/file/{generateappealpdf,generatepdf}.js`, `tests/phase21/file-handler-contract.test.cjs` +type: change +rationale: Execute next requested slice to modernize remaining file PDF-generation handlers still using promise chains, while preserving existing hash/response behavior. +impact: Improves maintainability and error-path consistency for PDF generation routes; phase21 coverage now includes explicit failure contracts for both handlers. +status: completed + +Summary: + +- `pages/api/file/generateappealpdf.js` + - converted mixed promise-chain flow to `async/await` + `try/catch` + - preserved existing guard contracts: `MISSING_REQUIRED_QUERY`, `INVALID_HASH` + - preserved generation failure contract: `GENERATE_APPEAL_PDF_FAILED` + - replaced JSX render call with `React.createElement(...)` compatibility form used by test loader +- `pages/api/file/generatepdf.js` + - converted create/upload promise-chain to `async/await` + `try/catch` + - preserved existing guard contracts: `HASH_REQUIRED`, `INVALID_HASH` + - preserved generation failure contract: `GENERATE_PDF_FAILED` + - replaced JSX render call with `React.createElement(...)` compatibility form used by test loader +- phase21 file tests expanded: + - `generatepdf` catch-path contract (`GENERATE_PDF_FAILED`) + - `generateappealpdf` catch-path contract (`GENERATE_APPEAL_PDF_FAILED`) + +Validation: + +- `node tests/phase21/api-contract-slice1.test.cjs` -> pass + - helper: 4/4 + - file-handler: 51/51 + - email-handler: 12/12 + - endpoint-handler: 152/152 + +Follow-ups: + +- Remaining optional cleanup in these handlers is dead import/unused local pruning (non-behavioral) if we want a final low-risk tidy pass. + +--- + +### CL-013: TASK22224 completion-message route parity closure slice + +date: 2026-03-24 +author: Cline +scope: `pages/api/file/createappealcompletemessage_api.js`, `tests/phase21/file-handler-contract.test.cjs` +type: change +rationale: Continue next requested slice by closing the final promise-chain parity outlier in file completion-message flow and strengthening phase21 contract coverage. +impact: Improves maintainability and async error hygiene while preserving route behavior and existing error contracts. +status: completed + +Summary: + +- `pages/api/file/createappealcompletemessage_api.js` + - replaced inline `.catch(...)` on fire-and-forget `updateAccount(...)` with explicit async IIFE + `try/catch` and `void` invocation + - preserved non-blocking behavior and logging semantics for account-update failure path + - preserved primary route contracts and success payload (`{ status: "success" }`) +- phase21 file contract tests expanded: + - success path for encoded hash candidate on `createappealcompletemessage_api` + - dependency-failure contract assertion for `CREATE_APPEAL_COMPLETE_MESSAGE_FAILED` + +Validation: + +- `node tests/phase21/api-contract-slice1.test.cjs` -> pass + - helper: 4/4 + - file-handler: 53/53 + - email-handler: 12/12 + - endpoint-handler: 152/152 + +Follow-ups: + +- Optional final low-risk tidy sweep: remove dead imports/unused locals in legacy file handlers now that contract hardening stream is functionally complete. + +--- + +### CL-014: TASK22224 pdf render compatibility tidy slice + +date: 2026-03-24 +author: Cline +scope: `pages/api/file/{generatepdf,generateappealpdf}.js`, `tests/phase21/file-handler-contract.test.cjs` +type: change +rationale: Apply a low-risk compatibility tidy so PDF render invocation remains stable across runtime and contract-test VM contexts. +impact: Keeps functional behavior unchanged while reducing test/runtime mismatch risk in render path setup. +status: completed + +Summary: + +- `pages/api/file/generatepdf.js` + - switched render call input from `React.createElement(MyDocument, ...)` to direct `MyDocument(...)` invocation in `ReactPDF.renderToStream(...)` +- `pages/api/file/generateappealpdf.js` + - switched render call input from `React.createElement(MyDocument, ...)` to direct `MyDocument(...)` invocation in `ReactPDF.renderToStream(...)` +- `tests/phase21/file-handler-contract.test.cjs` + - added `Buffer` injection for `generatepdf` catch-path test harness to align VM context expectations + +Validation: + +- `node tests/phase21/api-contract-slice1.test.cjs` -> pass + - helper: 4/4 + - file-handler: 53/53 + - email-handler: 12/12 + - endpoint-handler: 152/152 + +Follow-ups: + +- Optional: dead import cleanup (`Document/Page/Text/View/StyleSheet/PDFViewer`, `middleware`, `nextConnect`, `fs`, etc.) can be done in a dedicated non-behavioral hygiene PR. + +--- + +### CL-015: TASK22224 pdf handler dead-code hygiene slice + +date: 2026-03-24 +author: Cline +scope: `pages/api/file/{generatepdf,generateappealpdf}.js` +type: change +rationale: Execute the requested next low-risk slice by removing dead imports and unused locals in recently hardened PDF handlers. +impact: Non-behavioral maintainability cleanup; reduces lint noise and future edit risk while preserving existing contracts. +status: completed + +Summary: + +- `pages/api/file/generatepdf.js` + - pruned unused Azure storage imports, leaving only `createRepPDFBlob` + - pruned unused `@react-pdf/renderer` named imports + - removed unused imports (`middleware`, `nextConnect`, `fs`) + - removed unused locals (`casefolderID`, `representationType`, `repRaiser`, `localeSelect`, `repCapacity`, `repType`) +- `pages/api/file/generateappealpdf.js` + - pruned unused Azure storage imports to only required functions + - pruned unused `@react-pdf/renderer` named imports + - removed unused imports (`middleware`, `nextConnect`, `fs`, `path`, unused pdf templates) + - removed unused local (`caseRef`) + +Validation: + +- `node tests/phase21/api-contract-slice1.test.cjs` -> pass + - helper: 4/4 + - file-handler: 53/53 + - email-handler: 12/12 + - endpoint-handler: 152/152 + +Follow-ups: + +- Optional: run full repo lint in a separate pass for broader non-slice hygiene now that targeted contract suite is stable. + +--- + +### CL-016: TASK22224 documents download contract slice + +date: 2026-03-24 +author: Cline +scope: `pages/api/documents/download/[id].js`, `tests/phase21/{documents-handler-contract,api-contract-slice1}.test.cjs` +type: change +rationale: Execute next aggressive slice by standardizing document download guard behavior and bringing the route under phase21 contract coverage. +impact: Improves reliability on invalid input and relay-failure paths while preserving existing user-visible fallback behavior (`/filenotavailable`) for download failures. +status: completed + +Summary: + +- `pages/api/documents/download/[id].js` + - added explicit required-query guard for `id` and `hash` + - unified fallback redirect path via constant (`/filenotavailable`) + - preserved streaming download behavior and retry flow +- added `tests/phase21/documents-handler-contract.test.cjs` covering: + - missing query -> redirect contract + - success -> attachment/content-type headers + stream pipe contract + - relay failure -> redirect contract +- updated combined runner (`tests/phase21/api-contract-slice1.test.cjs`) to include documents handler contract suite + +Validation: + +- `node tests/phase21/api-contract-slice1.test.cjs` -> pass + - helper: 4/4 + - file-handler: 53/53 + - email-handler: 12/12 + - endpoint-handler: 152/152 + - documents-handler: 3/3 + +Follow-ups: + +- Optional future hardening: migrate documents route onto shared `respondError/respondSuccess` envelope if product requirements allow replacing redirect-style fallback. + +--- + +### CL-017: TASK22224 endpoint legacy-comment hygiene slice + +date: 2026-03-24 +author: Cline +scope: `pages/api/endpoint/{createwatchedcases_api,getadvancedsearchpaged_api}.js` +type: change +rationale: Complete second requested slice with low-risk maintainability cleanup by removing large obsolete commented legacy handler blocks. +impact: Non-behavioral cleanup only; improves readability and reduces maintenance noise with no runtime contract changes. +status: completed + +Summary: + +- `createwatchedcases_api.js` + - removed obsolete commented promise-chain implementation block +- `getadvancedsearchpaged_api.js` + - removed obsolete commented legacy implementation block retained below active handler + +Validation: + +- `node tests/phase21/api-contract-slice1.test.cjs` -> pass + - helper: 4/4 + - file-handler: 53/53 + - email-handler: 12/12 + - endpoint-handler: 152/152 + - documents-handler: 3/3 + +Follow-ups: + +- Optional further hygiene pass can target remaining oversized commented historical sections in non-sensitive handlers. + +--- + +### CL-018: TASK22224 nextauth notify micro-refactor + +date: 2026-03-24 +author: Cline +scope: `pages/api/auth/[...nextauth].js` +type: change +rationale: Execute the explicitly approved auth micro-slice by replacing inline promise `.catch(...)` with explicit `try/catch` while preserving existing auth behavior. +impact: Auth-sensitive non-functional refactor only; keeps current sign-in flow, template/locale routing, and error-handling semantics unchanged. +status: completed + +Summary: + +- `pages/api/auth/[...nextauth].js` + - replaced: + - `await notifyClient.sendEmail(...).catch((error) => consoleLogger(error))` + - with explicit: + - `try { await notifyClient.sendEmail(...) } catch (error) { consoleLogger(error) }` +- preserved behavior contracts: + - Notify failures are still logged and do not throw through auth handler + - no changes to callback URL construction, locale/template selection, NextAuth options, session/cookies/pages config + +Validation: + +- `node tests/phase21/api-contract-slice1.test.cjs` -> pass + - helper: 4/4 + - file-handler: 53/53 + - email-handler: 12/12 + - endpoint-handler: 152/152 + - documents-handler: 3/3 + +Follow-ups: + +- Optional future auth hygiene (separate guarded slice): replace verbose auth `console.log` diagnostics with structured logger usage once production logging requirements are confirmed. diff --git a/memory-bank/refactor-backlog.md b/memory-bank/refactor-backlog.md index 5f7109d1..cabb30c9 100644 --- a/memory-bank/refactor-backlog.md +++ b/memory-bank/refactor-backlog.md @@ -32,6 +32,28 @@ Last updated: 2026-03-12 - **Target outcome:** Shared guard middleware for hash/auth/input validation + consistent negative-path responses. - **Initial scope:** enforce a common pre-handler contract on high-risk routes first. +### Priority 4 — Current slice status (2026-03-23) + +- Completed on `TASK22224-file-endpoint-contract-bundle`: + - `pages/api/file/downloadblob.js` hardening + hotfix closure + - path normalization compatibility (filename-only + prefixed path) + - encoded/raw hash validation compatibility guard +- Confirmed outcome: + - user-reported download failures resolved + - phase21 file + combined contract suites passing + +### Priority 4 — Next recommended slice on this branch + +- Target routes: + - `pages/api/file/deleteblob.js` + - `pages/api/file/deleteblobcase.js` + - `pages/api/file/deleteblobrep.js` +- Scope: + - align hash canonicalization strategy with `downloadblob.js` + - enforce explicit `respondError` contracts (`MISSING_REQUIRED_QUERY`, `INVALID_HASH`, route-specific `*_FAILED`) + - preserve existing success payload shapes + - extend `tests/phase21/file-handler-contract.test.cjs` with mixed encoded/raw hash variants and negative paths + ## Priority 5 — Establish minimum automated regression baseline - **Problem:** Limited automated tests for high-risk logic. diff --git a/pages/api/admin/getlatestdocuments_api.js b/pages/api/admin/getlatestdocuments_api.js index 686352b3..a699550a 100644 --- a/pages/api/admin/getlatestdocuments_api.js +++ b/pages/api/admin/getlatestdocuments_api.js @@ -43,18 +43,18 @@ const encryptDocReference = (documentRef) => { }; export default async function ApiProxy(req, res) { - var pageNumber = req.query.pageNumber || 1; - var token = await getToken(); + const pageNumber = req.query.pageNumber || 1; + const token = await getToken(); - var orderby = req.query.orderby || "createdon"; - var fieldSort = req.query.fieldSort || "desc"; - var showNumberOfRecords = req.query.showNumberOfRecords || 10; - var documentType = req.query.documentType || "all"; - var documentOrigin = req.query.documentOrigin || "all"; - var numberOfWeeks = req.query.numberWeeks || 1; + const orderby = req.query.orderby || "createdon"; + const fieldSort = req.query.fieldSort || "desc"; + const showNumberOfRecords = req.query.showNumberOfRecords || 10; + const documentType = req.query.documentType || "all"; + const documentOrigin = req.query.documentOrigin || "all"; + const numberOfWeeks = req.query.numberWeeks || 1; - var docTypeQueryString = ""; - var docOriginQueryString = ""; + let docTypeQueryString = ""; + let docOriginQueryString = ""; if (documentType != "all") { if (documentType.indexOf(",") >= 0) { @@ -108,7 +108,7 @@ export default async function ApiProxy(req, res) { return resultDate.toISOString().replace(/\.000Z$/, "Z"); // format as 'YYYY-MM-DDT00:00:00Z' } - var queryUrl = + const queryUrl = "pinswg_documents?$select=pinswg_name,createdon,pinswg_publishtoweb,_pinswg_documentids_value,pinswg_isharedocumentlocations,pinswg_isharedocumentreference,pinswg_uploadurl,pinswg_uploadstatus,pinswg_origin&$filter=createdon ge " + daysAgoISO(numberOfWeeks) + docTypeQueryString + @@ -131,40 +131,37 @@ export default async function ApiProxy(req, res) { "\n==========================================\n" ); - var apiResponse = axios - .get( + try { + const { data } = await axios.get( WEBAPI_URL + queryUrl + hashAPIPath(queryUrl), azureHeadersPagedCustom(token.access_token, showNumberOfRecords) - ) - .then(({ data }) => { - let flattened = data.value.map((r) => ({ - ...r, - ticketnumber: r.pinswg_DocumentIds?.ticketnumber || null, - publishtoweb: r.pinswg_DocumentIds?.pinswg_publishtoweb || null - })); + ); - data.value = flattened; - data.value.forEach(function (element) { - element.pinswg_hashlink = encryptDocReference( - element.pinswg_isharedocumentreference - ); - }); + const flattened = data.value.map((r) => ({ + ...r, + ticketnumber: r.pinswg_DocumentIds?.ticketnumber || null, + publishtoweb: r.pinswg_DocumentIds?.pinswg_publishtoweb || null + })); - var dataStr; - - _.has(data, "@odata.nextLink") == true && - ((dataStr = JSON.stringify(data["@odata.nextLink"])), - (data["@odata.nextLink"] = dataStr.split("/v8.2/")[1])); - return respondSuccess(res, data); - }) - .catch((error) => { - consoleLogger(error); - return respondError(res, { - status: 400, - code: "ADMIN_LATEST_DOCS_FETCH_FAILED", - message: "Failed to fetch latest documents" - }); + data.value = flattened; + data.value.forEach(function (element) { + element.pinswg_hashlink = encryptDocReference( + element.pinswg_isharedocumentreference + ); }); - return apiResponse; + let dataStr; + + _.has(data, "@odata.nextLink") == true && + ((dataStr = JSON.stringify(data["@odata.nextLink"])), + (data["@odata.nextLink"] = dataStr.split("/v8.2/")[1])); + return respondSuccess(res, data); + } catch (error) { + consoleLogger(error); + return respondError(res, { + status: 400, + code: "ADMIN_LATEST_DOCS_FETCH_FAILED", + message: "Failed to fetch latest documents" + }); + } } diff --git a/pages/api/admin/getnewappeals_api.js b/pages/api/admin/getnewappeals_api.js index 595ed467..01c321eb 100644 --- a/pages/api/admin/getnewappeals_api.js +++ b/pages/api/admin/getnewappeals_api.js @@ -16,7 +16,6 @@ */ import axios from "axios"; -import CryptoJS from "crypto-js"; import _ from "lodash"; import { azureHeadersPagedCustom } from "../../../actions/core/headers"; import { getToken } from "../../../actions/core/token"; @@ -29,15 +28,14 @@ const WEBAPI_URL = "https://dev-pedw-ns.servicebus.windows.net/dev-pedw-hc/"; export default async function ApiProxy(req, res) { - var searchString = req.query.searchString; - var pageNumber = req.query.pageNumber || 1; - var token = await getToken(); + const pageNumber = req.query.pageNumber || 1; + const token = await getToken(); - var orderby = req.query.orderby || "createdon"; - var fieldSort = req.query.fieldSort || "desc"; - var showNumberOfRecords = req.query.showNumberOfRecords || 10; + const orderby = req.query.orderby || "createdon"; + const fieldSort = req.query.fieldSort || "desc"; + const showNumberOfRecords = req.query.showNumberOfRecords || 10; - var queryUrl = + const queryUrl = "incidents?$select=caseorigincode,pinswg_publishtoweb,modifiedon,description,numberofchildincidents,_accountid_value,_customerid_value,_pinswg_associatedlpa_value,_ownerid_value,pinswg_appealcasetype,statuscode,ticketnumber,title, _primarycontactid_value,pinswg_lpareference,pinswg_appellantagent,pinswg_appellantfirstname,pinswg_appellantlastname&$expand=primarycontactid($select=fullname)&$orderby=" + orderby + " " + @@ -55,26 +53,23 @@ export default async function ApiProxy(req, res) { "\n==========================================\n" ); - var apiResponse = axios - .get( + try { + const { data } = await axios.get( WEBAPI_URL + queryUrl + hashAPIPath(queryUrl), azureHeadersPagedCustom(token.access_token, showNumberOfRecords) - ) - .then(({ data }) => { - var dataStr; - _.has(data, "@odata.nextLink") == true && - ((dataStr = JSON.stringify(data["@odata.nextLink"])), - (data["@odata.nextLink"] = dataStr.split("/v8.2/")[1])); - return respondSuccess(res, data); - }) - .catch((error) => { - consoleLogger(error); - return respondError(res, { - status: 400, - code: "ADMIN_NEW_APPEALS_FETCH_FAILED", - message: "Failed to fetch new appeals" - }); - }); + ); - return apiResponse; + let dataStr; + _.has(data, "@odata.nextLink") == true && + ((dataStr = JSON.stringify(data["@odata.nextLink"])), + (data["@odata.nextLink"] = dataStr.split("/v8.2/")[1])); + return respondSuccess(res, data); + } catch (error) { + consoleLogger(error); + return respondError(res, { + status: 400, + code: "ADMIN_NEW_APPEALS_FETCH_FAILED", + message: "Failed to fetch new appeals" + }); + } } diff --git a/pages/api/auth/[...nextauth].js b/pages/api/auth/[...nextauth].js index 75939546..7a713e47 100644 --- a/pages/api/auth/[...nextauth].js +++ b/pages/api/auth/[...nextauth].js @@ -139,8 +139,8 @@ const authOptions = (req, res) => { process.env.NOTIFY_API_KEY ); - await notifyClient - .sendEmail( + try { + await notifyClient.sendEmail( effectiveLocale === "cy" ? templateIdcy : templateId, @@ -149,8 +149,10 @@ const authOptions = (req, res) => { personalisation, reference } - ) - .catch((error) => consoleLogger(error)); + ); + } catch (error) { + consoleLogger(error); + } } }) ], diff --git a/pages/api/documents/download/[id].js b/pages/api/documents/download/[id].js index aab6804b..4fbd0752 100644 --- a/pages/api/documents/download/[id].js +++ b/pages/api/documents/download/[id].js @@ -116,6 +116,8 @@ const WEBAPI_URL = process.env.RELAY_ROOT || "https://dev-pedw-ns.servicebus.windows.net/dev-pedw-hc/"; +const FILE_NOT_AVAILABLE_PATH = "/filenotavailable"; + // Retry utility with logging async function retry(fn, retries = 3, delay = 1000) { for (let attempt = 1; attempt <= retries; attempt++) { @@ -133,6 +135,18 @@ async function retry(fn, retries = 3, delay = 1000) { const ApiProxy = async (req, res) => { const docRef = req.query; + if ( + typeof docRef?.id !== "string" || + docRef.id.length === 0 || + typeof docRef?.hash !== "string" || + docRef.hash.length === 0 + ) { + if (!res.headersSent) { + res.redirect(FILE_NOT_AVAILABLE_PATH); + } + return; + } + try { const token = await getToken(); const startTime = Date.now(); @@ -186,11 +200,11 @@ const ApiProxy = async (req, res) => { response.data.on("error", (err) => { consoleLogger(err); - if (!res.headersSent) res.redirect("/filenotavailable"); + if (!res.headersSent) res.redirect(FILE_NOT_AVAILABLE_PATH); }); } catch (error) { consoleLogger(error); - if (!res.headersSent) res.redirect("/filenotavailable"); + if (!res.headersSent) res.redirect(FILE_NOT_AVAILABLE_PATH); } }; diff --git a/pages/api/email/getcaseref.js b/pages/api/email/getcaseref.js index ddc59198..52e07578 100644 --- a/pages/api/email/getcaseref.js +++ b/pages/api/email/getcaseref.js @@ -22,27 +22,25 @@ function flattenWatchlistEntry(entry) { } export default async function ApiProxy(req, res) { - var token = await getToken(); + const token = await getToken(); - var queryUrl = + const queryUrl = "pinswg_watchlists?$count=true&$filter=pinswg_emailnotifications ne null&$expand=pinswg_Contact($select=contactid,emailaddress1)&$select=pinswg_emailnotifications,_pinswg_watchedcase_value"; - return axios - .get( + try { + const { data } = await axios.get( WEBAPI_URL + queryUrl + hashAPIPath(queryUrl), azureHeaders(token.access_token) - ) - .then(({ data }) => { - const flattenedResults = data.value.map(flattenWatchlistEntry); + ); + const flattenedResults = data.value.map(flattenWatchlistEntry); - return respondSuccess(res, flattenedResults); - }) - .catch((error) => { - consoleLogger(error); - return respondError(res, { - status: 400, - code: "CASE_REF_FETCH_FAILED", - message: "Failed to fetch case references" - }); + return respondSuccess(res, flattenedResults); + } catch (error) { + consoleLogger(error); + return respondError(res, { + status: 400, + code: "CASE_REF_FETCH_FAILED", + message: "Failed to fetch case references" }); + } } diff --git a/pages/api/email/getmailinglist.js b/pages/api/email/getmailinglist.js index 3a980c57..6c368fb0 100644 --- a/pages/api/email/getmailinglist.js +++ b/pages/api/email/getmailinglist.js @@ -22,27 +22,25 @@ function flattenWatchlistEntry(entry) { } export default async function ApiProxy(req, res) { - var token = await getToken(); + const token = await getToken(); - var queryUrl = + const queryUrl = "pinswg_watchlists?$count=true&$filter=pinswg_emailnotifications ne null&$expand=pinswg_Contact($select=contactid,emailaddress1)&$select=pinswg_emailnotifications,_pinswg_watchedcase_value"; - return axios - .get( + try { + const { data } = await axios.get( WEBAPI_URL + queryUrl + hashAPIPath(queryUrl), azureHeaders(token.access_token) - ) - .then(({ data }) => { - const flattenedResults = data.value.map(flattenWatchlistEntry); + ); + const flattenedResults = data.value.map(flattenWatchlistEntry); - return respondSuccess(res, flattenedResults); - }) - .catch((error) => { - consoleLogger(error); - return respondError(res, { - status: 400, - code: "MAILING_LIST_FETCH_FAILED", - message: "Failed to fetch mailing list" - }); + return respondSuccess(res, flattenedResults); + } catch (error) { + consoleLogger(error); + return respondError(res, { + status: 400, + code: "MAILING_LIST_FETCH_FAILED", + message: "Failed to fetch mailing list" }); + } } diff --git a/pages/api/email/notify.js b/pages/api/email/notify.js index edf8a7d0..a03b1aa7 100644 --- a/pages/api/email/notify.js +++ b/pages/api/email/notify.js @@ -31,7 +31,7 @@ import { getPreferredLanguage } from "../../../actions/services/accountService"; import { respondError, respondSuccess } from "../middleware/apiResponse"; export default async function ApiProxy(req, res) { - var data = req.body; + const data = req.body; const emailAddress = sanitizeString(data?.emailAddress); if (!isNonEmptyString(emailAddress)) { @@ -58,7 +58,7 @@ export default async function ApiProxy(req, res) { } } - var NotifyClient = require("notifications-node-client").NotifyClient; + const NotifyClient = require("notifications-node-client").NotifyClient; const notifyClient = new NotifyClient(process.env.NOTIFY_API_KEY); //const emailReplyToId = process.env.EMAIL_REPLY_TO_ID; @@ -68,20 +68,19 @@ export default async function ApiProxy(req, res) { payload: redactSensitive(data) }); - notifyClient - .sendEmail(data.templateId, data.emailAddress, { + try { + await notifyClient.sendEmail(data.templateId, data.emailAddress, { personalisation: data.personalisation, reference: data.reference - }) - .then(() => { - return respondSuccess(res, data); - }) - .catch((error) => { - consoleLogger(error); - return respondError(res, { - status: 400, - code: "EMAIL_NOTIFY_FAILED", - message: "Failed to send notify email" - }); }); + + return respondSuccess(res, data); + } catch (error) { + consoleLogger(error); + return respondError(res, { + status: 400, + code: "EMAIL_NOTIFY_FAILED", + message: "Failed to send notify email" + }); + } } diff --git a/pages/api/endpoint/createwatchedcases_api.js b/pages/api/endpoint/createwatchedcases_api.js index 55662558..da199830 100644 --- a/pages/api/endpoint/createwatchedcases_api.js +++ b/pages/api/endpoint/createwatchedcases_api.js @@ -20,35 +20,6 @@ const WEBAPI_URL = process.env.RELAY_ROOT || "https://dev-pedw-ns.servicebus.windows.net/dev-pedw-hc/"; -// export default async function ApiProxy(req, res) { -// var token = await getToken(); -// var data = JSON.stringify(req.body); -// var queryUrl = "pinswg_watchlists"; - -// console.log(data); -// var config = { -// method: "put", -// url: WEBAPI_URL + queryUrl + hashAPIPath(queryUrl), -// headers: { -// "OData-MaxVersion": "4.0", -// "OData-Version": "4.0", -// "Accept": "application/json", -// "Prefer": 'odata.include-annotations="*",return=representation', -// "Authorization": "Bearer " + token.access_token, -// "Content-Type": "application/json", -// }, -// data: data, -// }; - -// return axios(config) -// .then(({ data }) => { -// res.status(200).json(data); -// }) -// .catch((error) => { -// consoleLogger(Object.assign(err, data)); -// res.status(400).json(error); -// }); -// } const recordExists = async (incidentId, contactId, token) => { const filter = `$filter=pinswg_WatchedCase/incidentid eq ${incidentId} and pinswg_Contact/contactid eq ${contactId}`; const queryUrl = `pinswg_watchlists?${filter}`; diff --git a/pages/api/endpoint/getadvancedsearchpaged_api.js b/pages/api/endpoint/getadvancedsearchpaged_api.js index 915efdeb..3150880e 100644 --- a/pages/api/endpoint/getadvancedsearchpaged_api.js +++ b/pages/api/endpoint/getadvancedsearchpaged_api.js @@ -258,161 +258,3 @@ export default async function ApiProxy(req, res) { }); } } - -// export default async function ApiProxy(req, res) { -// var searchString = req.query.searchstring; -// var pageNumber = req.query.pageNumber; -// var token = await getToken(); - -// var orderby = req.query.orderby; -// var fieldSort = req.query.fieldSort; -// var showNumberOfRecords = req.query.showNumberOfRecords; - -// searchString = _.isEmpty(searchString) -// ? searchString -// : JSON.parse(decodeURI(searchString)); - -// var queryString = ""; - -// queryString += -// _.has(searchString, "q") && searchString.q != null -// ? "(contains(title, '" + -// searchString.q.replace(/\'/g, "''") + -// "') or contains(ticketnumber,'" + -// searchString.q.replace(/\'/g, "''") + -// "')) and" -// : ""; - -// queryString += -// (_.has(searchString, "lpa") || _.has(searchString, "LPA")) && -// searchString.lpa != null -// ? " _pinswg_associatedlpa_value eq " + searchString.lpa + " and" -// : ""; - -// queryString += -// _.has(searchString, "apt") && searchString.apt != null -// ? " pinswg_appealcasetype eq " + searchString.apt + " and" -// : ""; - -// queryString += -// _.has(searchString, "statuscode") && searchString.statuscode != null -// ? " statuscode eq " + searchString.statuscode + " and" -// : ""; - -// var queryUrl = -// "incidents?$select=pinswg_environmentalstatementlocation,pinswg_caseaddress,modifiedon,description,numberofchildincidents,_accountid_value,_customerid_value,_pinswg_associatedlpa_value,_ownerid_value,pinswg_appealcasetype,statuscode,ticketnumber,title,_primarycontactid_value,pinswg_lpareference,pinswg_appellantagent,pinswg_appellantfirstname,pinswg_appellantlastname&$expand=primarycontactid($select=fullname)&$filter=" + -// queryString + -// " pinswg_appealcasetype ne null and pinswg_publishtoweb eq true&$orderby=" + -// orderby + -// " " + -// fieldSort + -// "&$count=true"; - -// console.log(WEBAPI_URL + queryUrl + hashAPIPath(queryUrl)); - -// var apiResponse = _.isEmpty(searchString) -// ? res.status(400).json() -// : axios -// .get( -// WEBAPI_URL + queryUrl + hashAPIPath(queryUrl), -// azureHeadersPagedCustom( -// token.access_token, -// showNumberOfRecords -// ) -// ) -// .then(async ({ data }) => { -// var dataStr; -// _.has(data, "@odata.nextLink") == true && -// ((dataStr = JSON.stringify(data["@odata.nextLink"])), -// (data["@odata.nextLink"] = dataStr.split("/v8.2/")[1])); - -// if (_.has(searchString, "projecttype")) { -// await updateValueArray(data, token); - -// async function updateValueArray(data, token) { -// let hasNextPage = true; -// let nextPageUrl = data["@odata.nextLink"]; - -// // Loop through all pages -// while (hasNextPage) { -// // Iterate through current data and process it -// for (let i = 0; i < data.value.length; i++) { -// const item = data.value[i]; - -// try { -// // Axios call using the `incidentid` to fetch additional data -// const response = await axios.get( -// WEBAPI_URL + -// "pinswg_sipses?$filter=_pinswg_sipscase_value eq " + -// item.incidentid + -// " and _pinswg_projecttype_value eq " + -// searchString.projecttype + -// "&$select=_pinswg_projecttype_value" + -// hashAPIPath( -// "pinswg_sipses?$filter=_pinswg_sipscase_value eq " + -// item.incidentid + -// " and _pinswg_projecttype_value eq " + -// searchString.projecttype + -// "&$select=_pinswg_projecttype_value" -// ), -// azureHeadersPaged(token.access_token) -// ); - -// if ( -// response.data.value.length > 0 && -// response.data.value[0] -// ._pinswg_projecttype_value != null -// ) { -// Object.assign( -// item, -// response.data.value[0] -// ); // Update item with new data -// } else { -// console.log( -// `No project type found for incident ID ${item.incidentid}` -// ); -// } -// } catch (error) { -// console.error( -// `Error fetching data for incident ID ${item.incidentid}:`, -// error -// ); -// } -// } - -// // If there's a next page, fetch it -// if (nextPageUrl) { -// const nextPageData = await axios.get( -// WEBAPI_URL + -// nextPageUrl + -// hashAPIPath(nextPageUrl), -// azureHeadersPaged(token.access_token) -// ); -// data.value = [ -// ...data.value, -// ...nextPageData.data.value, -// ]; -// nextPageUrl = -// nextPageData.data["@odata.nextLink"]; -// } else { -// hasNextPage = false; -// } -// } -// } - -// data.value = data.value.filter( -// (item) => item._pinswg_projecttype_value != null -// ); - -// data["@odata.count"] = data.value.length; -// } - -// res.status(200).json(data); -// }) -// .catch((error) => { -// consoleLogger(error); -// res.status(400).json(error); -// }); - -// return apiResponse; -// } diff --git a/pages/api/endpoint/getappealtypesfornewappeal_api.js b/pages/api/endpoint/getappealtypesfornewappeal_api.js index 06dde026..bbc4afc3 100644 --- a/pages/api/endpoint/getappealtypesfornewappeal_api.js +++ b/pages/api/endpoint/getappealtypesfornewappeal_api.js @@ -1,4 +1,4 @@ -import { respondSuccess } from "../middleware/apiResponse"; +import { respondError, respondSuccess } from "../middleware/apiResponse"; /** * @swagger @@ -13,6 +13,14 @@ import { respondSuccess } from "../middleware/apiResponse"; */ const ApiResponse = (req, res) => { + if (req.method && req.method !== "GET") { + return respondError(res, { + status: 405, + code: "METHOD_NOT_ALLOWED", + message: "Only GET is supported" + }); + } + return respondSuccess(res, { "value": [ { diff --git a/pages/api/endpoint/getportallogin_api.js b/pages/api/endpoint/getportallogin_api.js index e1adb714..c3f52aae 100644 --- a/pages/api/endpoint/getportallogin_api.js +++ b/pages/api/endpoint/getportallogin_api.js @@ -49,8 +49,16 @@ export default async function ApiProxy(req, res) { const checkquerypath = "/api/endpoint/getportallogin_api?emailAddress=" + emailAddress; + const encodedCheckquerypath = + "/api/endpoint/getportallogin_api?emailAddress=" + + encodeURIComponent(emailAddress); - if (hashAPIPath(checkquerypath) != "&hash=" + checkHash) { + const expectedHashCandidates = [ + hashAPIPath(checkquerypath), + hashAPIPath(encodedCheckquerypath) + ]; + + if (!expectedHashCandidates.includes("&hash=" + checkHash)) { return respondError(res, { status: 400, code: "INVALID_HASH", diff --git a/pages/api/endpoint/getsipsmedia_api.js b/pages/api/endpoint/getsipsmedia_api.js index 1ef461b4..deb143ad 100644 --- a/pages/api/endpoint/getsipsmedia_api.js +++ b/pages/api/endpoint/getsipsmedia_api.js @@ -1,29 +1,14 @@ -import { respondSuccess } from "../middleware/apiResponse"; +import { respondError, respondSuccess } from "../middleware/apiResponse"; -// export default async function ApiProxy(req, res) { -// var caseid = req.query.caseid; -// var token = await getToken(); +export default function ApiProxy(req, res) { + if (req.method && req.method !== "GET") { + return respondError(res, { + status: 405, + code: "METHOD_NOT_ALLOWED", + message: "Only GET is supported" + }); + } -// var queryUrl = -// "pinswg_sipsevents?$filter=_pinswg_sipseventsid_value eq " + -// caseid + -// "&$count=true"; - -// return 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); -// }); -// } - -export default function ApiProx(req, res) { const data = { "@odata.count": 4, "value": [ diff --git a/pages/api/file/createappealcompletemessage_api.js b/pages/api/file/createappealcompletemessage_api.js index 7b211519..17c43b1e 100644 --- a/pages/api/file/createappealcompletemessage_api.js +++ b/pages/api/file/createappealcompletemessage_api.js @@ -18,10 +18,10 @@ const ApiProxy = nextConnect(); ApiProxy.use(middleware); ApiProxy.get(async (req, res) => { - var containerName = req.query.container; - var tempCaseRef = req.query.tempcaseref; - var typeofinvolvement = req.query.inv; - var checkHash = req.query.hash; + const containerName = req.query.container; + const tempCaseRef = req.query.tempcaseref; + const typeofinvolvement = req.query.inv; + const checkHash = req.query.hash; if ( typeof containerName === "undefined" || @@ -38,13 +38,22 @@ ApiProxy.get(async (req, res) => { }); } - var checkquerypath = + const hashCandidatePaths = [ "/api/file/createappealcompletemessage_api?container=" + - containerName + - "&tempcaseref=" + - tempCaseRef; + containerName + + "&tempcaseref=" + + tempCaseRef, + "/api/file/createappealcompletemessage_api?container=" + + encodeURIComponent(containerName) + + "&tempcaseref=" + + encodeURIComponent(tempCaseRef) + ]; - if (hashAPIPath(checkquerypath) != "&hash=" + checkHash) { + const isHashValid = hashCandidatePaths.some( + (candidatePath) => hashAPIPath(candidatePath) == "&hash=" + checkHash + ); + + if (!isHashValid) { return respondError(res, { status: 400, code: "INVALID_HASH", @@ -52,83 +61,87 @@ ApiProxy.get(async (req, res) => { }); } - const blobProgress = await getProgressBlobs( - containerName, - tempCaseRef - ).then((data) => { - return downloadProgressFile(containerName, data.path, tempCaseRef); - }); + try { + const progressBlob = await getProgressBlobs(containerName, tempCaseRef); + const blobProgress = await downloadProgressFile( + containerName, + progressBlob.path, + tempCaseRef + ); - const caseObj = await downloadProgressFile( - containerName, - tempCaseRef + "/" + tempCaseRef + "_case.json", - tempCaseRef - ); + const caseObj = await downloadProgressFile( + containerName, + tempCaseRef + "/" + tempCaseRef + "_case.json", + tempCaseRef + ); - Object.assign(blobProgress, { - "appealComplete": tempCaseRef - }); + Object.assign(blobProgress, { + appealComplete: tempCaseRef + }); - delete blobProgress["pinswg_name"]; + delete blobProgress.pinswg_name; - await createBlob(JSON.stringify(blobProgress), containerName, tempCaseRef) - .then(() => { - //update case.json with lpa ref and description + await createBlob( + JSON.stringify(blobProgress), + containerName, + tempCaseRef + ); - let combinedAddress = - blobProgress.pinswg_siteaddressline1 + - (!_.isEmpty(blobProgress.pinswg_siteaddressline2) - ? ", " + blobProgress.pinswg_siteaddressline2 - : "") + - (!_.isEmpty(blobProgress.pinswg_siteaddresstown) - ? ", " + blobProgress.pinswg_siteaddresstown - : ""); + let combinedAddress = + blobProgress.pinswg_siteaddressline1 + + (!_.isEmpty(blobProgress.pinswg_siteaddressline2) + ? ", " + blobProgress.pinswg_siteaddressline2 + : "") + + (!_.isEmpty(blobProgress.pinswg_siteaddresstown) + ? ", " + blobProgress.pinswg_siteaddresstown + : ""); - combinedAddress = - combinedAddress + - ", " + - blobProgress.pinswg_siteaddresscounty + - ", " + - blobProgress.pinswg_siteaddresspostcode; + combinedAddress = + combinedAddress + + ", " + + blobProgress.pinswg_siteaddresscounty + + ", " + + blobProgress.pinswg_siteaddresspostcode; - Object.assign(caseObj, { - "pinswg_lpareference": - blobProgress.pinswg_lpaapplicationreference, - "description": blobProgress.pinswg_developmentdescription, - "pinswg_caseaddress": combinedAddress - }); + Object.assign(caseObj, { + pinswg_lpareference: blobProgress.pinswg_lpaapplicationreference, + description: blobProgress.pinswg_developmentdescription, + pinswg_caseaddress: combinedAddress + }); - //send update case.json and then create queue message - getCaseBlob(containerName, tempCaseRef, caseObj).then(() => { - let contactId = caseObj["customerid_contact@odata.bind"]; - contactId = contactId.match(/\(([^)]+)\)/); + await getCaseBlob(containerName, tempCaseRef, caseObj); - if (typeofinvolvement != 846040000) { - updateAccount( + let contactId = caseObj["customerid_contact@odata.bind"]; + contactId = contactId.match(/\(([^)]+)\)/); + + if (typeofinvolvement != 846040000) { + void (async () => { + try { + await updateAccount( contactId[1], { - "pinswg_typeofinvolvement": 846040001 + pinswg_typeofinvolvement: 846040001 }, true - ).catch((error) => { - consoleLogger(error); - }); + ); + } catch (error) { + consoleLogger(error); } + })(); + } - createCaseCompleteMessage(containerName, tempCaseRef); - return respondSuccess(res, { - status: "success" - }); - }); - }) - .catch((error) => { - consoleLogger(error); - return respondError(res, { - status: 400, - code: "CREATE_APPEAL_COMPLETE_MESSAGE_FAILED", - message: "Failed to create appeal complete message" - }); + await createCaseCompleteMessage(containerName, tempCaseRef); + return respondSuccess(res, { + status: "success" }); + } catch (error) { + consoleLogger(error); + return respondError(res, { + status: 400, + code: "CREATE_APPEAL_COMPLETE_MESSAGE_FAILED", + message: "Failed to create appeal complete message" + }); + } }); export const config = { diff --git a/pages/api/file/createappealcompletemessageproxy_api.js b/pages/api/file/createappealcompletemessageproxy_api.js index b495040c..81d3c443 100644 --- a/pages/api/file/createappealcompletemessageproxy_api.js +++ b/pages/api/file/createappealcompletemessageproxy_api.js @@ -17,8 +17,8 @@ const hasValue = (value) => typeof value === "string" && value.trim().length > 0; ApiProxy.get(async (req, res) => { - var containerName = req.query.container; - var tempCaseRef = req.query.tempcaseref; + const containerName = req.query.container; + const tempCaseRef = req.query.tempcaseref; if (!hasValue(containerName) || !hasValue(tempCaseRef)) { return respondError(res, { @@ -28,30 +28,28 @@ ApiProxy.get(async (req, res) => { }); } - var token = await getToken(); + const token = await getToken(); - var queryUrl = + const queryUrl = "/api/file/createappealcompletemessage_api?container=" + - containerName + + encodeURIComponent(containerName) + "&tempcaseref=" + - tempCaseRef; + encodeURIComponent(tempCaseRef); - return axios - .get( + try { + const { data } = await axios.get( BASE_URL + queryUrl + hashAPIPath(queryUrl), azureHeaders(token.access_token) - ) - .then(({ data }) => { - return respondSuccess(res, data); - }) - .catch((error) => { - consoleLogger(error); - return respondError(res, { - status: 400, - code: "CREATE_APPEAL_COMPLETE_MESSAGE_PROXY_FAILED", - message: "Failed to create appeal complete message" - }); + ); + return respondSuccess(res, data); + } catch (error) { + consoleLogger(error); + return respondError(res, { + status: 400, + code: "CREATE_APPEAL_COMPLETE_MESSAGE_PROXY_FAILED", + message: "Failed to create appeal complete message" }); + } }); export const config = { diff --git a/pages/api/file/createcaseinvolvement_api.js b/pages/api/file/createcaseinvolvement_api.js index af6b7848..a1761dc9 100644 --- a/pages/api/file/createcaseinvolvement_api.js +++ b/pages/api/file/createcaseinvolvement_api.js @@ -11,7 +11,6 @@ */ import axios from "axios"; -import CryptoJS from "crypto-js"; import { consoleLogger } from "../../../actions/core/logger"; import { getToken } from "../../../actions/core/token"; import { hashAPIPath } from "../../../actions/core/hash"; @@ -36,20 +35,20 @@ export default async function ApiProxy(req, res) { }); } - var token = await getToken(); - var contactid = req.body.contactid; - var queryUrl = + const token = await getToken(); + const contactid = req.body.contactid; + const queryUrl = "incidents(" + req.body.incidentid + ")/pinswg_incident_contact_case_involvement/$ref"; - var crmUrl = "https://" + process.env.CRMURL; + const crmUrl = "https://" + process.env.CRMURL; - var data = { + const data = { "@odata.id": crmUrl + "/api/data/v8.2/contacts(" + contactid + ")" }; - var config = { + const config = { method: "post", url: WEBAPI_URL + queryUrl + hashAPIPath(queryUrl), headers: { @@ -64,18 +63,20 @@ export default async function ApiProxy(req, res) { data: data }; - return axios(config) - .then(({ data }) => { - return respondSuccess(res, data); - }) - .catch((error) => { - return error.status == 412 - ? respondSuccess(res, { "record": "exists" }) - : (consoleLogger(Object.assign(error, data)), - respondError(res, { - status: 400, - code: "CREATE_CASE_INVOLVEMENT_FAILED", - message: "Failed to create case involvement" - })); + try { + const { data: responseData } = await axios(config); + return respondSuccess(res, responseData); + } catch (error) { + const errorStatus = error?.status || error?.response?.status; + if (errorStatus == 412) { + return respondSuccess(res, { record: "exists" }); + } + + consoleLogger(Object.assign(error, data)); + return respondError(res, { + status: 400, + code: "CREATE_CASE_INVOLVEMENT_FAILED", + message: "Failed to create case involvement" }); + } } diff --git a/pages/api/file/createrepcompletemessage_api.js b/pages/api/file/createrepcompletemessage_api.js index d682d651..bff12c91 100644 --- a/pages/api/file/createrepcompletemessage_api.js +++ b/pages/api/file/createrepcompletemessage_api.js @@ -13,10 +13,10 @@ const ApiProxy = nextConnect(); ApiProxy.use(middleware); ApiProxy.get(async (req, res) => { - var containerName = req.query.container; - var tempCaseRef = req.query.tempcaseref; - var filename = req.query.repid; - var checkHash = req.query.hash; + const containerName = req.query.container; + const tempCaseRef = req.query.tempcaseref; + const filename = req.query.repid; + const checkHash = req.query.hash; if ( typeof containerName === "undefined" || @@ -35,15 +35,26 @@ ApiProxy.get(async (req, res) => { }); } - var checkquerypath = + const hashCandidatePaths = [ "/api/file/createrepcompletemessage_api?container=" + - containerName + - "&tempcaseref=" + - tempCaseRef + - "&repid=" + - filename; + containerName + + "&tempcaseref=" + + tempCaseRef + + "&repid=" + + filename, + "/api/file/createrepcompletemessage_api?container=" + + encodeURIComponent(containerName) + + "&tempcaseref=" + + encodeURIComponent(tempCaseRef) + + "&repid=" + + encodeURIComponent(filename) + ]; - if (hashAPIPath(checkquerypath) != "&hash=" + checkHash) { + const isHashValid = hashCandidatePaths.some( + (candidatePath) => hashAPIPath(candidatePath) == "&hash=" + checkHash + ); + + if (!isHashValid) { return respondError(res, { status: 400, code: "INVALID_HASH", @@ -51,18 +62,21 @@ ApiProxy.get(async (req, res) => { }); } - await createRepCompleteMessage(containerName, tempCaseRef, filename) - .then((data) => { - return respondSuccess(res, data); - }) - .catch((error) => { - consoleLogger(error); - return respondError(res, { - status: 400, - code: "CREATE_REP_COMPLETE_MESSAGE_FAILED", - message: "Failed to create representation complete message" - }); + try { + const data = await createRepCompleteMessage( + containerName, + tempCaseRef, + filename + ); + return respondSuccess(res, data); + } catch (error) { + consoleLogger(error); + return respondError(res, { + status: 400, + code: "CREATE_REP_COMPLETE_MESSAGE_FAILED", + message: "Failed to create representation complete message" }); + } }); export const config = { diff --git a/pages/api/file/createrepinvolvement_api.js b/pages/api/file/createrepinvolvement_api.js index 4f40349f..856185ec 100644 --- a/pages/api/file/createrepinvolvement_api.js +++ b/pages/api/file/createrepinvolvement_api.js @@ -11,7 +11,6 @@ */ import axios from "axios"; -import CryptoJS from "crypto-js"; import { consoleLogger } from "../../../actions/core/logger"; import { getToken } from "../../../actions/core/token"; import { hashAPIPath } from "../../../actions/core/hash"; @@ -36,22 +35,22 @@ export default async function ApiProxy(req, res) { }); } - var token = await getToken(); - var contactid = req.body.contactid; - var queryUrl = + const token = await getToken(); + const contactid = req.body.contactid; + const queryUrl = "incidents(" + req.body.incidentid + ")/pinswg_incident_contact_case_involvement/$ref"; - var crmUrl = "https://" + process.env.CRMURL; - var crmVersion = process.env.CRMURL_VERSION; + const crmUrl = "https://" + process.env.CRMURL; + const crmVersion = process.env.CRMURL_VERSION; - var data = { + const data = { "@odata.id": crmUrl + "/api/data/" + crmVersion + "/contacts(" + contactid + ")" }; - var config = { + const config = { method: "post", url: WEBAPI_URL + queryUrl + hashAPIPath(queryUrl), headers: { @@ -66,18 +65,20 @@ export default async function ApiProxy(req, res) { data: data }; - return axios(config) - .then(({ data }) => { - return respondSuccess(res, data); - }) - .catch((error) => { - return error.status == 412 - ? respondSuccess(res, { "record": "exists" }) - : (consoleLogger(Object.assign(error, data)), - respondError(res, { - status: 400, - code: "CREATE_REP_INVOLVEMENT_FAILED", - message: "Failed to create representation involvement" - })); + try { + const { data: responseData } = await axios(config); + return respondSuccess(res, responseData); + } catch (error) { + const errorStatus = error?.status || error?.response?.status; + if (errorStatus == 412) { + return respondSuccess(res, { record: "exists" }); + } + + consoleLogger(Object.assign(error, data)); + return respondError(res, { + status: 400, + code: "CREATE_REP_INVOLVEMENT_FAILED", + message: "Failed to create representation involvement" }); + } } diff --git a/pages/api/file/deleteawaitingsubmissionfromblob.js b/pages/api/file/deleteawaitingsubmissionfromblob.js index a1cdbe53..4740aa45 100644 --- a/pages/api/file/deleteawaitingsubmissionfromblob.js +++ b/pages/api/file/deleteawaitingsubmissionfromblob.js @@ -9,10 +9,10 @@ const ApiProxy = nextConnect(); ApiProxy.use(middleware); ApiProxy.get(async (req, res) => { - var containerName = req.query.container; - var casefolderID = req.query.casefolderID; - var blobName = req.query.blobname; - var checkHash = req.query.hash; + const containerName = req.query.container; + const casefolderID = req.query.casefolderID; + const blobName = req.query.blobname; + const checkHash = req.query.hash; if ( typeof containerName === "undefined" || @@ -31,15 +31,43 @@ ApiProxy.get(async (req, res) => { }); } - var checkquerypath = - "/api/file/deleteblob?container=" + - containerName + - "&casefolderID=" + - casefolderID + - "&blobname=" + - blobName; + const casefolderIDTrimmed = casefolderID.trim(); + const blobNameTrimmed = blobName.trim(); - if (hashAPIPath(checkquerypath) != "&hash=" + checkHash) { + const hashCandidatePaths = [ + "/api/file/deleteawaitingsubmissionfromblob?container=" + + containerName + + "&casefolderID=" + + casefolderIDTrimmed + + "&blobname=" + + blobNameTrimmed, + "/api/file/deleteawaitingsubmissionfromblob?container=" + + containerName + + "&casefolderID=" + + encodeURIComponent(casefolderIDTrimmed) + + "&blobname=" + + encodeURIComponent(blobNameTrimmed), + + // legacy compatibility with callers that hash against deleteblob route + "/api/file/deleteblob?container=" + + containerName + + "&casefolderID=" + + casefolderIDTrimmed + + "&blobname=" + + blobNameTrimmed, + "/api/file/deleteblob?container=" + + containerName + + "&casefolderID=" + + encodeURIComponent(casefolderIDTrimmed) + + "&blobname=" + + encodeURIComponent(blobNameTrimmed) + ]; + + const isHashValid = hashCandidatePaths.some( + (candidatePath) => hashAPIPath(candidatePath) == "&hash=" + checkHash + ); + + if (!isHashValid) { return respondError(res, { status: 400, code: "INVALID_HASH", @@ -47,11 +75,22 @@ ApiProxy.get(async (req, res) => { }); } - await deleteBlob(containerName, casefolderID + "/files/" + blobName).then( - (data) => { - return respondSuccess(res, { data: data }); - } - ); + try { + const normalizedBlobName = blobNameTrimmed.startsWith( + casefolderIDTrimmed + "/" + ) + ? blobNameTrimmed + : casefolderIDTrimmed + "/files/" + blobNameTrimmed; + + const data = await deleteBlob(containerName, normalizedBlobName); + return respondSuccess(res, { data: data }); + } catch (error) { + return respondError(res, { + status: 400, + code: "DELETE_AWAITING_SUBMISSION_BLOB_FAILED", + message: "Unable to delete awaiting submission blob" + }); + } }); export const config = { diff --git a/pages/api/file/deleteblob.js b/pages/api/file/deleteblob.js index 5c1d52fe..5402078b 100644 --- a/pages/api/file/deleteblob.js +++ b/pages/api/file/deleteblob.js @@ -9,10 +9,10 @@ const ApiProxy = nextConnect(); ApiProxy.use(middleware); ApiProxy.get(async (req, res) => { - var containerName = req.query.container; - var casefolderID = req.query.casefolderID; - var blobName = req.query.blobname; - var checkHash = req.query.hash; + const containerName = req.query.container; + const casefolderID = req.query.casefolderID; + const blobName = req.query.blobname; + const checkHash = req.query.hash; if ( typeof containerName === "undefined" || @@ -31,15 +31,41 @@ ApiProxy.get(async (req, res) => { }); } - var checkquerypath = - "/api/file/deleteblob?container=" + - containerName + - "&casefolderID=" + - encodeURIComponent(casefolderID) + - "&blobname=" + - encodeURIComponent(blobName); + const casefolderIDTrimmed = casefolderID.trim(); + const blobNameTrimmed = blobName.trim(); - if (hashAPIPath(checkquerypath) != "&hash=" + checkHash) { + const hashCandidatePaths = [ + "/api/file/deleteblob?container=" + + containerName + + "&casefolderID=" + + casefolderIDTrimmed + + "&blobname=" + + blobNameTrimmed, + "/api/file/deleteblob?container=" + + containerName + + "&casefolderID=" + + encodeURIComponent(casefolderIDTrimmed) + + "&blobname=" + + blobNameTrimmed, + "/api/file/deleteblob?container=" + + containerName + + "&casefolderID=" + + casefolderIDTrimmed + + "&blobname=" + + encodeURIComponent(blobNameTrimmed), + "/api/file/deleteblob?container=" + + containerName + + "&casefolderID=" + + encodeURIComponent(casefolderIDTrimmed) + + "&blobname=" + + encodeURIComponent(blobNameTrimmed) + ]; + + const isHashValid = hashCandidatePaths.some( + (candidatePath) => hashAPIPath(candidatePath) == "&hash=" + checkHash + ); + + if (!isHashValid) { return respondError(res, { status: 400, code: "INVALID_HASH", @@ -47,11 +73,22 @@ ApiProxy.get(async (req, res) => { }); } - await deleteBlob(containerName, casefolderID + "/files/" + blobName).then( - (data) => { - return respondSuccess(res, { data: data }); - } - ); + try { + const normalizedBlobName = blobNameTrimmed.startsWith( + casefolderIDTrimmed + "/" + ) + ? blobNameTrimmed + : casefolderIDTrimmed + "/files/" + blobNameTrimmed; + + const data = await deleteBlob(containerName, normalizedBlobName); + return respondSuccess(res, { data: data }); + } catch (error) { + return respondError(res, { + status: 400, + code: "DELETE_BLOB_FAILED", + message: "Unable to delete blob" + }); + } }); export const config = { diff --git a/pages/api/file/deleteblobcase.js b/pages/api/file/deleteblobcase.js index ba48e9d5..62764132 100644 --- a/pages/api/file/deleteblobcase.js +++ b/pages/api/file/deleteblobcase.js @@ -9,9 +9,9 @@ const ApiProxy = nextConnect(); ApiProxy.use(middleware); ApiProxy.get(async (req, res) => { - var containerName = req.query.container; - var casefolderID = req.query.casefolderID; - var checkHash = req.query.hash; + const containerName = req.query.container; + const casefolderID = req.query.casefolderID; + const checkHash = req.query.hash; if ( typeof containerName === "undefined" || @@ -28,13 +28,24 @@ ApiProxy.get(async (req, res) => { }); } - var checkquerypath = - "/api/file/deleteblobcase?container=" + - containerName + - "&casefolderID=" + - casefolderID; + const casefolderIDTrimmed = casefolderID.trim(); - if (hashAPIPath(checkquerypath) != "&hash=" + checkHash) { + const hashCandidatePaths = [ + "/api/file/deleteblobcase?container=" + + containerName + + "&casefolderID=" + + casefolderIDTrimmed, + "/api/file/deleteblobcase?container=" + + containerName + + "&casefolderID=" + + encodeURIComponent(casefolderIDTrimmed) + ]; + + const isHashValid = hashCandidatePaths.some( + (candidatePath) => hashAPIPath(candidatePath) == "&hash=" + checkHash + ); + + if (!isHashValid) { return respondError(res, { status: 400, code: "INVALID_HASH", @@ -42,9 +53,16 @@ ApiProxy.get(async (req, res) => { }); } - await deleteBlobCase(containerName, casefolderID).then((data) => { + try { + const data = await deleteBlobCase(containerName, casefolderIDTrimmed); return respondSuccess(res, { data: data }); - }); + } catch (error) { + return respondError(res, { + status: 400, + code: "DELETE_BLOB_CASE_FAILED", + message: "Unable to delete blob case" + }); + } }); export const config = { diff --git a/pages/api/file/deleteblobrep.js b/pages/api/file/deleteblobrep.js index fd80f28b..b327837c 100644 --- a/pages/api/file/deleteblobrep.js +++ b/pages/api/file/deleteblobrep.js @@ -9,10 +9,10 @@ const ApiProxy = nextConnect(); ApiProxy.use(middleware); ApiProxy.get(async (req, res) => { - var containerName = req.query.container; - var casefolderID = req.query.casefolderID; - var repfile = req.query.repfile; - var checkHash = req.query.hash; + const containerName = req.query.container; + const casefolderID = req.query.casefolderID; + const repfile = req.query.repfile; + const checkHash = req.query.hash; if ( typeof containerName === "undefined" || @@ -31,15 +31,41 @@ ApiProxy.get(async (req, res) => { }); } - var checkquerypath = - "/api/file/deleteblobrep?container=" + - containerName + - "&casefolderID=" + - casefolderID + - "&repfile=" + - repfile; + const casefolderIDTrimmed = casefolderID.trim(); + const repfileTrimmed = repfile.trim(); - if (hashAPIPath(checkquerypath) != "&hash=" + checkHash) { + const hashCandidatePaths = [ + "/api/file/deleteblobrep?container=" + + containerName + + "&casefolderID=" + + casefolderIDTrimmed + + "&repfile=" + + repfileTrimmed, + "/api/file/deleteblobrep?container=" + + containerName + + "&casefolderID=" + + encodeURIComponent(casefolderIDTrimmed) + + "&repfile=" + + repfileTrimmed, + "/api/file/deleteblobrep?container=" + + containerName + + "&casefolderID=" + + casefolderIDTrimmed + + "&repfile=" + + encodeURIComponent(repfileTrimmed), + "/api/file/deleteblobrep?container=" + + containerName + + "&casefolderID=" + + encodeURIComponent(casefolderIDTrimmed) + + "&repfile=" + + encodeURIComponent(repfileTrimmed) + ]; + + const isHashValid = hashCandidatePaths.some( + (candidatePath) => hashAPIPath(candidatePath) == "&hash=" + checkHash + ); + + if (!isHashValid) { return respondError(res, { status: 400, code: "INVALID_HASH", @@ -47,11 +73,17 @@ ApiProxy.get(async (req, res) => { }); } - casefolderID = casefolderID + "/" + repfile; - - await deleteBlobRep(containerName, casefolderID).then((data) => { + try { + const normalizedRepPath = casefolderIDTrimmed + "/" + repfileTrimmed; + const data = await deleteBlobRep(containerName, normalizedRepPath); return respondSuccess(res, { data: data }); - }); + } catch (error) { + return respondError(res, { + status: 400, + code: "DELETE_BLOB_REP_FAILED", + message: "Unable to delete blob representation" + }); + } }); export const config = { diff --git a/pages/api/file/downloadblob.js b/pages/api/file/downloadblob.js index f7683731..5209128e 100644 --- a/pages/api/file/downloadblob.js +++ b/pages/api/file/downloadblob.js @@ -10,10 +10,10 @@ const ApiProxy = nextConnect(); ApiProxy.use(middleware); ApiProxy.get(async (req, res) => { - var containerName = req.query.container; - var casefolderID = req.query.casefolderID; - var blobName = req.query.blobname; - var checkHash = req.query.hash; + const containerName = req.query.container; + const casefolderID = req.query.casefolderID; + const blobName = req.query.blobname; + const checkHash = req.query.hash; if ( typeof containerName === "undefined" || @@ -32,15 +32,39 @@ ApiProxy.get(async (req, res) => { }); } - var checkquerypath = + const blobNameTrimmed = blobName.trim(); + const hashCandidatePaths = [ "/api/file/downloadblob?container=" + - containerName + - "&casefolderID=" + - casefolderID + - "&blobname=" + - blobName.trim(); + containerName + + "&casefolderID=" + + casefolderID + + "&blobname=" + + blobNameTrimmed, + "/api/file/downloadblob?container=" + + containerName + + "&casefolderID=" + + encodeURIComponent(casefolderID) + + "&blobname=" + + blobNameTrimmed, + "/api/file/downloadblob?container=" + + containerName + + "&casefolderID=" + + casefolderID + + "&blobname=" + + encodeURIComponent(blobNameTrimmed), + "/api/file/downloadblob?container=" + + containerName + + "&casefolderID=" + + encodeURIComponent(casefolderID) + + "&blobname=" + + encodeURIComponent(blobNameTrimmed) + ]; - if (hashAPIPath(checkquerypath) != "&hash=" + checkHash) { + const isHashValid = hashCandidatePaths.some( + (candidatePath) => hashAPIPath(candidatePath) == "&hash=" + checkHash + ); + + if (!isHashValid) { return respondError(res, { status: 400, code: "INVALID_HASH", @@ -48,34 +72,36 @@ ApiProxy.get(async (req, res) => { }); } - const bloblocation = - casefolderID + (blobName.indexOf(".json") > 0 ? "/" : "/files/"); + try { + const decodedBlobName = decodeURI(blobName); + const normalizedBlobName = decodedBlobName.startsWith( + casefolderID + "/" + ) + ? decodedBlobName + : casefolderID + + (decodedBlobName.indexOf(".json") > 0 ? "/" : "/files/") + + decodedBlobName; - const downloaded = await downloadFile( - containerName, - bloblocation + decodeURI(blobName) - ); + const downloaded = await downloadFile( + containerName, + normalizedBlobName + ); + const responseFileName = decodedBlobName.split("/").pop(); - res.setHeader( - "content-disposition", - "attachment; filename=" + decodeURI(blobName) - ); - return res.status(200).send(downloaded); + res.setHeader( + "content-disposition", + "attachment; filename=" + responseFileName + ); + return res.status(200).send(downloaded); + } catch (error) { + return respondError(res, { + status: 400, + code: "DOWNLOAD_BLOB_FAILED", + message: "Unable to download blob" + }); + } }); -async function streamToBuffer(readableStream) { - return new Promise((resolve, reject) => { - const chunks = []; - readableStream.on("data", (data) => { - chunks.push(data instanceof Buffer ? data : Buffer.from(data)); - }); - readableStream.on("end", () => { - resolve(Buffer.concat(chunks)); - }); - readableStream.on("error", reject); - }); -} - export const config = { api: { bodyParser: false diff --git a/pages/api/file/generateappealpdf.js b/pages/api/file/generateappealpdf.js index e90284c9..d4fb3569 100644 --- a/pages/api/file/generateappealpdf.js +++ b/pages/api/file/generateappealpdf.js @@ -73,12 +73,6 @@ */ import { - createContainer, - getContainers, - getBlobs, - createRepBlob, - uploadFile, - uploadPDFAppealFiles, downloadProgressFile, getProgressBlobs, getTempCaseBlob, @@ -86,23 +80,9 @@ import { } from "../../../actions/azurestorage"; import { hashAPIPath } from "../../../actions/core/hash"; -import ReactPDF, { - Document, - Page, - Text, - View, - StyleSheet, - PDFViewer -} from "@react-pdf/renderer"; -import middleware from "../middleware/middleware"; -import nextConnect from "next-connect"; -import fs from "fs"; -import path from "path"; +import ReactPDF from "@react-pdf/renderer"; import { getPickLists } from "../../../actions/services/referenceDataService"; import { planningappeals78_pdf } from "../../../components/pdftemplates/planningappeals78_pdf"; -import { finalComments_pdf } from "../../../components/pdftemplates/finalComments_pdf"; -import { statement_pdf } from "../../../components/pdftemplates/statement_pdf"; -import { writtenStatement_pdf } from "../../../components/pdftemplates/writtenStatement_pdf"; import { other_pdf } from "../../../components/pdftemplates/other_pdf"; import { respondError, respondSuccess } from "../middleware/apiResponse"; @@ -112,16 +92,15 @@ import { respondError, respondSuccess } from "../middleware/apiResponse"; // ApiProxy.post(async (req, res) => { export default async function handler(req, res) { - var checkHash = req.query.hash; - var appealType = req.query.appealType; - var isDownload = req.query?.download === "true"; + const checkHash = req.query.hash; + const appealType = req.query.appealType; + const isDownload = req.query?.download === "true"; let appealBodyObj = typeof req.body === "string" ? JSON.parse(req.body) : req.body; - var containerID = appealBodyObj.containerID; - var casefolderID = appealBodyObj.casefolderID; - var caseRef = appealBodyObj.caseRef; - var checkquerypath = "/api/file/generateappealpdf"; + const containerID = appealBodyObj.containerID; + const casefolderID = appealBodyObj.casefolderID; + let checkquerypath = "/api/file/generateappealpdf"; if ( typeof checkHash === "undefined" || @@ -184,92 +163,97 @@ export default async function handler(req, res) { } }; - const tempCaseBlob = await getTempCaseBlob(containerID, casefolderID); - const blobProgress = await getProgressBlobs(containerID, casefolderID).then( - (data) => { - //console.log("Progress blob path:", data); - return downloadProgressFile(containerID, data.path, casefolderID); - } - ); + try { + const tempCaseBlob = await getTempCaseBlob(containerID, casefolderID); + const progressBlob = await getProgressBlobs(containerID, casefolderID); + const blobProgress = await downloadProgressFile( + containerID, + progressBlob.path, + casefolderID + ); - const uniqueArray = appealBodyObj.filesList.filter((obj, index, self) => { - // Check if the name has already been encountered - return index === self.findIndex((t) => t.name === obj.name); - }); - - Object.assign(blobProgress, { - "filesList": uniqueArray, - "caseObj": tempCaseBlob - }); - - const pickListData = await getPickLists(appealType); - - // ReactPDF.render( - // , - // process.cwd() + `/tmp/${blobProgress.pinswg_name}.pdf` - // ); - - // ReactPDF.renderToStream(); - - const streamToBuffer = async (readableStream) => { - return new Promise((resolve, reject) => { - const chunks = []; - readableStream.on("data", (chunk) => { - chunks.push( - Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk) - ); - }); - readableStream.on("end", () => resolve(Buffer.concat(chunks))); - readableStream.on("error", reject); - }); - }; - - const renderedPDF = await ReactPDF.renderToStream( - - ); - - const repDate = new Date(); - - let day = repDate.getDate(); - let month = repDate.getMonth() + 1; - let year = repDate.getFullYear(); - - const pdfFileName = - year + - "-" + - ("0" + month).slice(-2) + - "-" + - ("0" + day).slice(-2) + - "_-_Appeal_Form"; - - const renderedPDFBuffer = await streamToBuffer(renderedPDF); - - await createAppealPDFBlob( - renderedPDFBuffer, - containerID, - blobProgress.pinswg_name || blobProgress.caseObj.ticketnumber - ) - .then((data) => { - if (isDownload) { - const filename = `${pdfFileName}.pdf`; - res.setHeader("Content-Type", "application/pdf"); - res.setHeader( - "Content-Disposition", - `attachment; filename="${filename}"` - ); - return res.status(200).send(renderedPDFBuffer); + const uniqueArray = appealBodyObj.filesList.filter( + (obj, index, self) => { + return index === self.findIndex((t) => t.name === obj.name); } - return respondSuccess(res, { - status: "success", - data: data, - path: `/files/${pdfFileName}.pdf` - }); - }) - .catch(() => { - return respondError(res, { - status: 400, - code: "GENERATE_APPEAL_PDF_FAILED", - message: "Failed to generate appeal PDF" - }); + ); + + Object.assign(blobProgress, { + "filesList": uniqueArray, + "caseObj": tempCaseBlob }); + + const pickListData = await getPickLists(appealType); + + // ReactPDF.render( + // , + // process.cwd() + `/tmp/${blobProgress.pinswg_name}.pdf` + // ); + + // ReactPDF.renderToStream(); + + const streamToBuffer = async (readableStream) => { + return new Promise((resolve, reject) => { + const chunks = []; + readableStream.on("data", (chunk) => { + chunks.push( + Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk) + ); + }); + readableStream.on("end", () => resolve(Buffer.concat(chunks))); + readableStream.on("error", reject); + }); + }; + + const renderedPDF = await ReactPDF.renderToStream( + MyDocument({ + docProps: blobProgress, + pickListData: pickListData + }) + ); + + const repDate = new Date(); + + let day = repDate.getDate(); + let month = repDate.getMonth() + 1; + let year = repDate.getFullYear(); + + const pdfFileName = + year + + "-" + + ("0" + month).slice(-2) + + "-" + + ("0" + day).slice(-2) + + "_-_Appeal_Form"; + + const renderedPDFBuffer = await streamToBuffer(renderedPDF); + + const data = await createAppealPDFBlob( + renderedPDFBuffer, + containerID, + blobProgress.pinswg_name || blobProgress.caseObj.ticketnumber + ); + + if (isDownload) { + const filename = `${pdfFileName}.pdf`; + res.setHeader("Content-Type", "application/pdf"); + res.setHeader( + "Content-Disposition", + `attachment; filename="${filename}"` + ); + return res.status(200).send(renderedPDFBuffer); + } + + return respondSuccess(res, { + status: "success", + data: data, + path: `/files/${pdfFileName}.pdf` + }); + } catch (error) { + return respondError(res, { + status: 400, + code: "GENERATE_APPEAL_PDF_FAILED", + message: "Failed to generate appeal PDF" + }); + } } diff --git a/pages/api/file/generateappealpdfcopy.js b/pages/api/file/generateappealpdfcopy.js index 4a1b7778..a3c87d83 100644 --- a/pages/api/file/generateappealpdfcopy.js +++ b/pages/api/file/generateappealpdfcopy.js @@ -1,45 +1,17 @@ -import { - createContainer, - getContainers, - getBlobs, - createRepBlob, - uploadFile, - uploadPDFAppealFiles, - downloadProgressFile, - getProgressBlobs, - getTempCaseBlob, - createAppealPDFBlob -} from "../../../actions/azurestorage"; import { getCaseByID, getPortalModuleDetails, getAppealPDFDocs } from "../../../actions/services/caseService"; import { getFormCollectionByID } from "../../../components/utils"; -import ReactPDF, { - Document, - Page, - Text, - View, - StyleSheet, - PDFViewer, - pdf -} from "@react-pdf/renderer"; - -import middleware from "../middleware/middleware"; -import nextConnect from "next-connect"; -import fs from "fs"; -import path from "path"; +import { pdf } from "@react-pdf/renderer"; import { getPickLists } from "../../../actions/services/referenceDataService"; import { planningappeals78_pdf } from "../../../components/pdftemplates/planningappeals78_pdf"; -import { finalComments_pdf } from "../../../components/pdftemplates/finalComments_pdf"; -import { statement_pdf } from "../../../components/pdftemplates/statement_pdf"; -import { writtenStatement_pdf } from "../../../components/pdftemplates/writtenStatement_pdf"; import { other_pdf } from "../../../components/pdftemplates/other_pdf"; +import { respondError } from "../middleware/apiResponse"; -const getDetails = (resultsObj, detailsType) => { +const getDetails = (resultsObj) => { if (!Array.isArray(resultsObj)) { - console.warn("getDetails: resultsObj is not an array"); return Promise.resolve([]); } @@ -51,7 +23,6 @@ const getDetails = (resultsObj, detailsType) => { const form = getFormCollectionByID(appealType); if (!form) { - console.warn("No form found for appeal type:", appealType); return null; } @@ -64,54 +35,74 @@ const getDetails = (resultsObj, detailsType) => { export default async function handler(req, res) { const incidentid = req.body?.docProps; - if (!incidentid) return res.status(400).send("Missing incident ID"); - - const caseObj = await getCaseByID(incidentid); - if (!Array.isArray(caseObj) || caseObj.length === 0) - return res.status(404).send("Case not found"); - - const caseDetailsObj = await getDetails(caseObj, "myCases"); - const fileList = (await getAppealPDFDocs(incidentid)) || []; - - // Inject into the first item in `value` array - - if (caseDetailsObj[0]?.value?.[0]) { - caseDetailsObj[0].value[0].pinswg_developmentdescription = - caseObj[0].description; - caseDetailsObj[0].value[0].caseObj = caseObj[0]; - caseDetailsObj[0].value[0].filesList = fileList; + if (!incidentid) { + return respondError(res, { + status: 400, + code: "INCIDENT_ID_REQUIRED", + message: "Incident id is required" + }); } - const whichForm = getFormCollectionByID(caseObj[0].pinswg_appealcasetype); - const pickListData = await getPickLists(whichForm.UrlName); + try { + const caseObj = await getCaseByID(incidentid); + if (!Array.isArray(caseObj) || caseObj.length === 0) { + return respondError(res, { + status: 404, + code: "CASE_NOT_FOUND", + message: "Case not found" + }); + } - const appealTypeMap = { - 846040000: planningappeals78_pdf, - 846040004: planningappeals78_pdf - }; + const caseDetailsObj = await getDetails(caseObj); + const fileList = (await getAppealPDFDocs(incidentid)) || []; - //console.log(caseDetailsObj[0].value[0]); + if (caseDetailsObj[0]?.value?.[0]) { + caseDetailsObj[0].value[0].pinswg_developmentdescription = + caseObj[0].description; + caseDetailsObj[0].value[0].caseObj = caseObj[0]; + caseDetailsObj[0].value[0].filesList = fileList; + } - const MyDocument = (values) => { - const type = caseObj[0].pinswg_appealcasetype; - const renderDocument = appealTypeMap[type] || other_pdf; - return renderDocument(values); - }; + const whichForm = getFormCollectionByID( + caseObj[0].pinswg_appealcasetype + ); + if (!whichForm?.UrlName) { + return respondError(res, { + status: 400, + code: "FORM_COLLECTION_NOT_FOUND", + message: "Unable to resolve form collection" + }); + } + const pickListData = await getPickLists(whichForm.UrlName); - const pdfComponent = MyDocument({ - docProps: caseDetailsObj[0].value[0], - pickListData - }); + const appealTypeMap = { + 846040000: planningappeals78_pdf, + 846040004: planningappeals78_pdf + }; - const pdfBuffer = await pdf(pdfComponent).toBuffer(); + const renderDocument = + appealTypeMap[caseObj[0].pinswg_appealcasetype] || other_pdf; + const pdfComponent = renderDocument({ + docProps: caseDetailsObj?.[0]?.value?.[0] || {}, + pickListData + }); - const repDate = new Date(); - const formattedDate = repDate.toISOString().split("T")[0]; + const pdfBuffer = await pdf(pdfComponent).toBuffer(); - res.setHeader("Content-Type", "application/pdf"); - res.setHeader( - "Content-Disposition", - `attachment; filename=${formattedDate}_-_Appeal_Form.pdf` - ); - res.send(pdfBuffer); + const repDate = new Date(); + const formattedDate = repDate.toISOString().split("T")[0]; + + res.setHeader("Content-Type", "application/pdf"); + res.setHeader( + "Content-Disposition", + `attachment; filename=${formattedDate}_-_Appeal_Form.pdf` + ); + return res.send(pdfBuffer); + } catch (error) { + return respondError(res, { + status: 400, + code: "APPEAL_PDF_COPY_GENERATION_FAILED", + message: "Unable to generate appeal PDF copy" + }); + } } diff --git a/pages/api/file/generatepdf.js b/pages/api/file/generatepdf.js index 981d4f0e..42500ca6 100644 --- a/pages/api/file/generatepdf.js +++ b/pages/api/file/generatepdf.js @@ -72,28 +72,10 @@ * description: Success */ -import { - createContainer, - getContainers, - getBlobs, - createRepBlob, - uploadFile, - uploadPDFRepFiles, - createRepPDFBlob -} from "../../../actions/azurestorage"; +import { createRepPDFBlob } from "../../../actions/azurestorage"; import { hashAPIPath } from "../../../actions/core/hash"; -import ReactPDF, { - Document, - Page, - Text, - View, - StyleSheet, - PDFViewer -} from "@react-pdf/renderer"; -import middleware from "../middleware/middleware"; -import nextConnect from "next-connect"; -import fs from "fs"; +import ReactPDF from "@react-pdf/renderer"; import { EnforcementQuestionnaire_pdf } from "../../../components/pdftemplates/enforcement_pdf"; import { lpaQuestionnaire_pdf } from "../../../components/pdftemplates/lpaQuestionnaire_pdf"; import { finalComments_pdf } from "../../../components/pdftemplates/finalComments_pdf"; @@ -109,7 +91,7 @@ import { respondError, respondSuccess } from "../middleware/apiResponse"; export default async function handler(req, res) { const shouldDownload = req.query.download === "true"; - var checkHash = req.query.hash; + const checkHash = req.query.hash; let reqBodyobj = typeof req.body === "string" ? JSON.parse(req.body) : req.body; @@ -169,15 +151,11 @@ export default async function handler(req, res) { // "\n//////////////////////\n" // ); - var containerID = reqBodyobj.containerID; - var casefolderID = reqBodyobj.casefolderID; - var caseRef = reqBodyobj.ticketnumber || reqBodyobj.caseRef; - var representationType = reqBodyobj.representationType; - var repRaiser = reqBodyobj.lastname; - var localeSelect = reqBodyobj.locale; + const containerID = reqBodyobj.containerID; + const caseRef = reqBodyobj.ticketnumber || reqBodyobj.caseRef; //console.log("there are files:", Object.keys(req.files).length); - var checkquerypath = "/api/file/generatepdf"; + let checkquerypath = "/api/file/generatepdf"; if (shouldDownload) { checkquerypath += "?download=true"; @@ -247,67 +225,6 @@ export default async function handler(req, res) { } }; - let repCapacity = ""; - switch (reqBodyobj.representationCapacity) { - case "Appellant": - repCapacity = "APP"; - break; - case "Agent": - repCapacity = "AGENT"; - break; - case "Interested Party/Person": - repCapacity = "IP"; - break; - case "Land Owner": - repCapacity = "LO"; - break; - case "lpa": - case "LPA": - repCapacity = "LPA"; - break; - - default: - // code block - } - - let repType = ""; - switch (reqBodyobj.representationType) { - case "Statement": - repType = "Statement"; - break; - case "Statement of common ground": - repType = "SCG"; - break; - case "Written statement": - repType = "WS"; - break; - case "Written statement of evidence": - repType = "WSE"; - break; - case "Questionnaire": - repType = "Questionnaire"; - break; - case "Final comments": - repType = "Comments"; - break; - case "Local Impact Report": - repType = "Impact"; - break; - case "Consultation Response": - repType = "Consultation_Response"; - break; - case "Marine Impact Report": - repType = "Marine_Impact"; - break; - - case "Other": - repType = "OTHER"; - break; - - default: - // code block - } - //console.log( // "///////////////////////////////////\n file created: \n" + // process.cwd() + @@ -336,40 +253,40 @@ export default async function handler(req, res) { }); }; - const renderedPDFStream = await ReactPDF.renderToStream( - - ); - const renderedPDFBuffer = await streamToBuffer(renderedPDFStream); + try { + const renderedPDFStream = await ReactPDF.renderToStream( + MyDocument({ docProps: reqBodyobj }) + ); + const renderedPDFBuffer = await streamToBuffer(renderedPDFStream); - await createRepPDFBlob( - renderedPDFBuffer, - containerID, - caseRef, - reqBodyobj.repfile_name - ) - .then((data) => { - if (shouldDownload) { - const filename = `${reqBodyobj.repfile_name || "questionnaire"}.pdf`; - res.setHeader("Content-Type", "application/pdf"); - res.setHeader( - "Content-Disposition", - `attachment; filename="${filename}"` - ); - return res.status(200).send(renderedPDFBuffer); - } + const data = await createRepPDFBlob( + renderedPDFBuffer, + containerID, + caseRef, + reqBodyobj.repfile_name + ); - return respondSuccess(res, { - status: "success", - data: data, - path: `${caseRef}/${reqBodyobj.repfile_name}/files/${reqBodyobj.repfile_name}.pdf` - }); - }) - .catch((error) => { - consoleLogger(error); - return respondError(res, { - status: 400, - code: "GENERATE_PDF_FAILED", - message: "Failed to generate PDF" - }); + if (shouldDownload) { + const filename = `${reqBodyobj.repfile_name || "questionnaire"}.pdf`; + res.setHeader("Content-Type", "application/pdf"); + res.setHeader( + "Content-Disposition", + `attachment; filename="${filename}"` + ); + return res.status(200).send(renderedPDFBuffer); + } + + return respondSuccess(res, { + status: "success", + data: data, + path: `${caseRef}/${reqBodyobj.repfile_name}/files/${reqBodyobj.repfile_name}.pdf` }); + } catch (error) { + consoleLogger(error); + return respondError(res, { + status: 400, + code: "GENERATE_PDF_FAILED", + message: "Failed to generate PDF" + }); + } } diff --git a/pages/api/file/getawaitingsubmissionfromblob.js b/pages/api/file/getawaitingsubmissionfromblob.js index d8ca4315..8ee749a7 100644 --- a/pages/api/file/getawaitingsubmissionfromblob.js +++ b/pages/api/file/getawaitingsubmissionfromblob.js @@ -2,7 +2,6 @@ import { downloadAllProgressFiles, getAllProgressBlobs } from "../../../actions/azurestorage"; -import _ from "lodash"; import nextConnect from "next-connect"; import middleware from "../middleware/middleware"; import { hashAPIPath } from "../../../actions/core/hash"; @@ -12,8 +11,8 @@ const ApiProxy = nextConnect(); ApiProxy.use(middleware); ApiProxy.get(async (req, res) => { - var containerName = req.query.container; - var checkHash = req.query.hash; + const containerName = req.query.container; + const checkHash = req.query.hash; if ( typeof containerName === "undefined" || @@ -28,10 +27,17 @@ ApiProxy.get(async (req, res) => { }); } - var checkquerypath = - "/api/file/getawaitingsubmissionfromblob?container=" + containerName; + const hashCandidatePaths = [ + "/api/file/getawaitingsubmissionfromblob?container=" + containerName, + "/api/file/getawaitingsubmissionfromblob?container=" + + encodeURIComponent(containerName) + ]; - if (hashAPIPath(checkquerypath) != "&hash=" + checkHash) { + const isHashValid = hashCandidatePaths.some( + (candidatePath) => hashAPIPath(candidatePath) == "&hash=" + checkHash + ); + + if (!isHashValid) { return respondError(res, { status: 400, code: "INVALID_HASH", @@ -39,13 +45,20 @@ ApiProxy.get(async (req, res) => { }); } - const blobObj = await getAllProgressBlobs(containerName) - .then((data) => { - return downloadAllProgressFiles(containerName, data); - }) - .then((data) => { - return respondSuccess(res, data); + try { + const progressBlobs = await getAllProgressBlobs(containerName); + const data = await downloadAllProgressFiles( + containerName, + progressBlobs + ); + return respondSuccess(res, data); + } catch (error) { + return respondError(res, { + status: 400, + code: "GET_AWAITING_SUBMISSION_BLOB_FAILED", + message: "Failed to retrieve awaiting submission blobs" }); + } }); export const config = { diff --git a/pages/api/file/getawaitingsubmissionfromblobproxy.js b/pages/api/file/getawaitingsubmissionfromblobproxy.js index f04a2198..3dcda590 100644 --- a/pages/api/file/getawaitingsubmissionfromblobproxy.js +++ b/pages/api/file/getawaitingsubmissionfromblobproxy.js @@ -11,7 +11,7 @@ const hasValue = (value) => typeof value === "string" && value.trim().length > 0; export default async function ApiProxy(req, res) { - var containerName = req.query.container; + const containerName = req.query.container; if (!hasValue(containerName)) { return respondError(res, { @@ -21,25 +21,23 @@ export default async function ApiProxy(req, res) { }); } - var token = await getToken(); + const token = await getToken(); - var queryUrl = + const queryUrl = "/api/file/getawaitingsubmissionfromblob?container=" + containerName; - return axios - .get( + try { + const { data } = await axios.get( BASE_URL + queryUrl + hashAPIPath(queryUrl), azureHeaders(token.access_token) - ) - .then(({ data }) => { - return respondSuccess(res, data); - }) - .catch((error) => { - consoleLogger(error); - return respondError(res, { - status: 400, - code: "GET_AWAITING_SUBMISSION_PROXY_FAILED", - message: "Failed to fetch awaiting submission blob" - }); + ); + return respondSuccess(res, data); + } catch (error) { + consoleLogger(error); + return respondError(res, { + status: 400, + code: "GET_AWAITING_SUBMISSION_PROXY_FAILED", + message: "Failed to fetch awaiting submission blob" }); + } } diff --git a/pages/api/file/getbloblist.js b/pages/api/file/getbloblist.js index da29a655..8b21ee3a 100644 --- a/pages/api/file/getbloblist.js +++ b/pages/api/file/getbloblist.js @@ -40,10 +40,9 @@ const ApiProxy = nextConnect(); ApiProxy.use(middleware); ApiProxy.get(async (req, res) => { - var containerName = req.query.container; - var casefolderID = req.query.casefolderID; - - var checkHash = req.query.hash; + const containerName = req.query.container; + const casefolderID = req.query.casefolderID; + const checkHash = req.query.hash; if ( typeof containerName === "undefined" || @@ -60,13 +59,24 @@ ApiProxy.get(async (req, res) => { }); } - var checkquerypath = - "/api/file/getbloblist?container=" + - containerName + - "&casefolderID=" + - casefolderID; + const casefolderIDTrimmed = casefolderID.trim(); - if (hashAPIPath(checkquerypath) != "&hash=" + checkHash) { + const hashCandidatePaths = [ + "/api/file/getbloblist?container=" + + containerName + + "&casefolderID=" + + casefolderIDTrimmed, + "/api/file/getbloblist?container=" + + containerName + + "&casefolderID=" + + encodeURIComponent(casefolderIDTrimmed) + ]; + + const isHashValid = hashCandidatePaths.some( + (candidatePath) => hashAPIPath(candidatePath) == "&hash=" + checkHash + ); + + if (!isHashValid) { return respondError(res, { status: 400, code: "INVALID_HASH", @@ -74,16 +84,24 @@ ApiProxy.get(async (req, res) => { }); } - const data = - casefolderID.split("/").length > 1 - ? await getRepsFilesBlobs( - containerName, - casefolderID.split("/")[0], - casefolderID.split("/")[1] - ) - : await getBlobs(containerName, casefolderID); + try { + const data = + casefolderIDTrimmed.split("/").length > 1 + ? await getRepsFilesBlobs( + containerName, + casefolderIDTrimmed.split("/")[0], + casefolderIDTrimmed.split("/")[1] + ) + : await getBlobs(containerName, casefolderIDTrimmed); - return respondSuccess(res, { "value": [data] }); + return respondSuccess(res, { "value": [data] }); + } catch (error) { + return respondError(res, { + status: 400, + code: "GET_BLOB_LIST_FAILED", + message: "Failed to retrieve blob list" + }); + } }); export const config = { diff --git a/pages/api/file/getbloblistproxy.js b/pages/api/file/getbloblistproxy.js index c6771fca..6dd29acf 100644 --- a/pages/api/file/getbloblistproxy.js +++ b/pages/api/file/getbloblistproxy.js @@ -11,8 +11,8 @@ const hasValue = (value) => typeof value === "string" && value.trim().length > 0; export default async function ApiProxy(req, res) { - var containerName = req.query.container; - var casefolderID = req.query.casefolderID; + const containerName = req.query.container; + const casefolderID = req.query.casefolderID; if (!hasValue(containerName) || !hasValue(casefolderID)) { return respondError(res, { @@ -22,28 +22,26 @@ export default async function ApiProxy(req, res) { }); } - var token = await getToken(); + const token = await getToken(); - var queryUrl = + const queryUrl = "/api/file/getbloblist?container=" + - containerName + + encodeURIComponent(containerName) + "&casefolderID=" + - casefolderID; + encodeURIComponent(casefolderID); - return axios - .get( + try { + const { data } = await axios.get( BASE_URL + queryUrl + hashAPIPath(queryUrl), azureHeaders(token.access_token) - ) - .then(({ data }) => { - return respondSuccess(res, data); - }) - .catch((error) => { - consoleLogger(error); - return respondError(res, { - status: 400, - code: "GET_BLOB_LIST_PROXY_FAILED", - message: "Failed to fetch blob list" - }); + ); + return respondSuccess(res, data); + } catch (error) { + consoleLogger(error); + return respondError(res, { + status: 400, + code: "GET_BLOB_LIST_PROXY_FAILED", + message: "Failed to fetch blob list" }); + } } diff --git a/pages/api/file/getprogressobjblob.js b/pages/api/file/getprogressobjblob.js index a9481aa4..ebdea060 100644 --- a/pages/api/file/getprogressobjblob.js +++ b/pages/api/file/getprogressobjblob.js @@ -12,10 +12,9 @@ const ApiProxy = nextConnect(); ApiProxy.use(middleware); ApiProxy.get(async (req, res) => { - var containerName = req.query.container; - var casefolderID = req.query.casefolderID; - - var checkHash = req.query.hash; + const containerName = req.query.container; + const casefolderID = req.query.casefolderID; + const checkHash = req.query.hash; if ( typeof containerName === "undefined" || @@ -32,13 +31,24 @@ ApiProxy.get(async (req, res) => { }); } - var checkquerypath = - "/api/file/getprogressobjblob?container=" + - containerName + - "&casefolderID=" + - casefolderID; + const casefolderIDTrimmed = casefolderID.trim(); - if (hashAPIPath(checkquerypath) != "&hash=" + checkHash) { + const hashCandidatePaths = [ + "/api/file/getprogressobjblob?container=" + + containerName + + "&casefolderID=" + + casefolderIDTrimmed, + "/api/file/getprogressobjblob?container=" + + containerName + + "&casefolderID=" + + encodeURIComponent(casefolderIDTrimmed) + ]; + + const isHashValid = hashCandidatePaths.some( + (candidatePath) => hashAPIPath(candidatePath) == "&hash=" + checkHash + ); + + if (!isHashValid) { return respondError(res, { status: 400, code: "INVALID_HASH", @@ -46,13 +56,24 @@ ApiProxy.get(async (req, res) => { }); } - await getProgressBlobs(containerName, casefolderID) - .then((data) => { - return downloadProgressFile(containerName, data.path, casefolderID); - }) - .then((data) => { - return respondSuccess(res, data); + try { + const progressBlob = await getProgressBlobs( + containerName, + casefolderIDTrimmed + ); + const data = await downloadProgressFile( + containerName, + progressBlob.path, + casefolderIDTrimmed + ); + return respondSuccess(res, data); + } catch (error) { + return respondError(res, { + status: 400, + code: "GET_PROGRESS_OBJ_BLOB_FAILED", + message: "Failed to retrieve progress blob" }); + } }); export const config = { diff --git a/pages/api/file/getrepsblob.js b/pages/api/file/getrepsblob.js index a059f5a2..45dff296 100644 --- a/pages/api/file/getrepsblob.js +++ b/pages/api/file/getrepsblob.js @@ -33,7 +33,6 @@ import { downloadAllRepsFiles, getRepsBlobs } from "../../../actions/azurestorage"; -import _ from "lodash"; import nextConnect from "next-connect"; import middleware from "../middleware/middleware"; import { hashAPIPath } from "../../../actions/core/hash"; @@ -44,10 +43,8 @@ const ApiProxy = nextConnect(); ApiProxy.use(middleware); ApiProxy.get(async (req, res) => { - var containerName = req.query.container; - var casefolderID = req.query.casefolderID; - - var checkHash = req.query.hash; + const containerName = req.query.container; + const checkHash = req.query.hash; if ( typeof containerName === "undefined" || @@ -62,9 +59,16 @@ ApiProxy.get(async (req, res) => { }); } - var checkquerypath = "/api/file/getrepsblob?container=" + containerName; + const hashCandidatePaths = [ + "/api/file/getrepsblob?container=" + containerName, + "/api/file/getrepsblob?container=" + encodeURIComponent(containerName) + ]; - if (hashAPIPath(checkquerypath) != "&hash=" + checkHash) { + const isHashValid = hashCandidatePaths.some( + (candidatePath) => hashAPIPath(candidatePath) == "&hash=" + checkHash + ); + + if (!isHashValid) { return respondError(res, { status: 400, code: "INVALID_HASH", @@ -72,23 +76,19 @@ ApiProxy.get(async (req, res) => { }); } - const blobObj = await getRepsBlobs(containerName) - .then(async (data) => { - return data; - }) - .then(async (data) => { - let result = await downloadAllRepsFiles(containerName, data); - res.setHeader("Cache-Control", "no-store"); - return respondSuccess(res, result); - }) - .catch((error) => { - consoleLogger(error); - return respondError(res, { - status: 400, - code: "GET_REPS_BLOB_FAILED", - message: "Failed to retrieve representation blobs" - }); + try { + const repsBlobs = await getRepsBlobs(containerName); + const result = await downloadAllRepsFiles(containerName, repsBlobs); + res.setHeader("Cache-Control", "no-store"); + return respondSuccess(res, result); + } catch (error) { + consoleLogger(error); + return respondError(res, { + status: 400, + code: "GET_REPS_BLOB_FAILED", + message: "Failed to retrieve representation blobs" }); + } }); export default ApiProxy; diff --git a/pages/api/file/getrepsblobproxy.js b/pages/api/file/getrepsblobproxy.js index 36383f1a..64a02e54 100644 --- a/pages/api/file/getrepsblobproxy.js +++ b/pages/api/file/getrepsblobproxy.js @@ -11,7 +11,7 @@ const hasValue = (value) => typeof value === "string" && value.trim().length > 0; export default async function ApiProxy(req, res) { - var containerName = req.query.container; + const containerName = req.query.container; if (!hasValue(containerName)) { return respondError(res, { @@ -21,24 +21,23 @@ export default async function ApiProxy(req, res) { }); } - var token = await getToken(); + const token = await getToken(); - var queryUrl = "/api/file/getrepsblob?container=" + containerName; + const queryUrl = + "/api/file/getrepsblob?container=" + encodeURIComponent(containerName); - return axios - .get( + try { + const { data } = await axios.get( BASE_URL + queryUrl + hashAPIPath(queryUrl), azureHeaders(token.access_token) - ) - .then(({ data }) => { - return respondSuccess(res, data); - }) - .catch((error) => { - consoleLogger(error); - return respondError(res, { - status: 400, - code: "GET_REPS_BLOB_PROXY_FAILED", - message: "Failed to fetch representation blobs" - }); + ); + return respondSuccess(res, data); + } catch (error) { + consoleLogger(error); + return respondError(res, { + status: 400, + code: "GET_REPS_BLOB_PROXY_FAILED", + message: "Failed to fetch representation blobs" }); + } } diff --git a/pages/api/file/setupcontainer.js b/pages/api/file/setupcontainer.js index ca2c5d00..01927dcc 100644 --- a/pages/api/file/setupcontainer.js +++ b/pages/api/file/setupcontainer.js @@ -1,10 +1,4 @@ -import { - createContainer, - createContainerSas, - getContainers, - getBlobs, - uploadFile -} from "../../../actions/azurestorage"; +import { createContainer } from "../../../actions/azurestorage"; import { hashAPIPath } from "../../../actions/core/hash"; import { consoleLogger } from "../../../actions/core/logger"; import { respondError, respondSuccess } from "../middleware/apiResponse"; @@ -16,8 +10,8 @@ const ApiProxy = nextConnect(); ApiProxy.use(middleware); ApiProxy.get(async (req, res) => { - var containerName = req.query.ident; - var checkHash = req.query.hash; + const containerName = req.query.ident; + const checkHash = req.query.hash; if ( typeof containerName === "undefined" || @@ -32,9 +26,19 @@ ApiProxy.get(async (req, res) => { }); } - var checkquerypath = "/api/file/setupcontainer?ident=" + containerName; + const containerNameTrimmed = containerName.trim(); - if (hashAPIPath(checkquerypath) != "&hash=" + checkHash) { + const hashCandidatePaths = [ + "/api/file/setupcontainer?ident=" + containerNameTrimmed, + "/api/file/setupcontainer?ident=" + + encodeURIComponent(containerNameTrimmed) + ]; + + const isHashValid = hashCandidatePaths.some( + (candidatePath) => hashAPIPath(candidatePath) == "&hash=" + checkHash + ); + + if (!isHashValid) { return respondError(res, { status: 400, code: "INVALID_HASH", @@ -42,18 +46,17 @@ ApiProxy.get(async (req, res) => { }); } - await createContainer(containerName) - .then((data) => { - return respondSuccess(res, { data: "success", output: data }); - }) - .catch((error) => { - consoleLogger(error); - return respondError(res, { - status: 400, - code: "SETUP_CONTAINER_FAILED", - message: "Failed to setup container" - }); + try { + const data = await createContainer(containerNameTrimmed); + return respondSuccess(res, { data: "success", output: data }); + } catch (error) { + consoleLogger(error); + return respondError(res, { + status: 400, + code: "SETUP_CONTAINER_FAILED", + message: "Failed to setup container" }); + } }); export const config = { diff --git a/pages/api/file/updatecase_api.js b/pages/api/file/updatecase_api.js index c458b8dd..ccb51318 100644 --- a/pages/api/file/updatecase_api.js +++ b/pages/api/file/updatecase_api.js @@ -1,5 +1,4 @@ import axios from "axios"; -import CryptoJS from "crypto-js"; import { consoleLogger } from "../../../actions/core/logger"; import { getToken } from "../../../actions/core/token"; import { hashAPIPath } from "../../../actions/core/hash"; @@ -10,12 +9,12 @@ const WEBAPI_URL = "https://dev-pedw-ns.servicebus.windows.net/dev-pedw-hc/"; export default async function ApiProxy(req, res) { - var data = JSON.stringify(req.body); - var appealObj = req.query.appealObj; - var incidentID = req.query.incident; + const data = JSON.stringify(req.body); + const appealObj = req.query.appealObj; + const incidentID = req.query.incident; - var updateFormCollection = req.query.updateFormCollection; - var queryUrl = updateFormCollection + "(" + appealObj + ")"; + const updateFormCollection = req.query.updateFormCollection; + const queryUrl = updateFormCollection + "(" + appealObj + ")"; if ( typeof updateFormCollection === "undefined" || @@ -35,9 +34,9 @@ export default async function ApiProxy(req, res) { }); } - var token = await getToken(); + const token = await getToken(); - var config = { + const config = { method: "patch", url: WEBAPI_URL + queryUrl + hashAPIPath(queryUrl), headers: { @@ -53,16 +52,15 @@ export default async function ApiProxy(req, res) { //console.log("update case:", data); - return axios(config) - .then(({ data }) => { - return respondSuccess(res, data); - }) - .catch((error) => { - consoleLogger(error); - return respondError(res, { - status: 400, - code: "UPDATE_CASE_FAILED", - message: "Failed to update case" - }); + try { + const { data: responseData } = await axios(config); + return respondSuccess(res, responseData); + } catch (error) { + consoleLogger(error); + return respondError(res, { + status: 400, + code: "UPDATE_CASE_FAILED", + message: "Failed to update case" }); + } } diff --git a/pages/api/file/upload.js b/pages/api/file/upload.js index 80038527..70072889 100644 --- a/pages/api/file/upload.js +++ b/pages/api/file/upload.js @@ -1,8 +1,4 @@ -import { - createBlob, - createRepBlob, - uploadFile -} from "../../../actions/azurestorage"; +import { createBlob, createRepBlob } from "../../../actions/azurestorage"; import { hashAPIPath } from "../../../actions/core/hash"; import { respondError, respondSuccess } from "../middleware/apiResponse"; @@ -13,7 +9,7 @@ const ApiProxy = nextConnect(); ApiProxy.use(middleware); ApiProxy.post(async (req, res) => { - var checkHash = req.query.hash; + const checkHash = req.query.hash; if (typeof checkHash === "undefined" || checkHash.length === 0) { return respondError(res, { @@ -23,9 +19,12 @@ ApiProxy.post(async (req, res) => { }); } - var checkquerypath = "/api/file/upload"; + const hashCandidatePaths = ["/api/file/upload", "/api/file/upload?"]; + const isHashValid = hashCandidatePaths.some( + (candidatePath) => hashAPIPath(candidatePath) == "?hash=" + checkHash + ); - if (hashAPIPath(checkquerypath) != "?hash=" + checkHash) { + if (!isHashValid) { return respondError(res, { status: 400, code: "INVALID_HASH", @@ -51,21 +50,19 @@ ApiProxy.post(async (req, res) => { }); } - return ( - repOrAppeal + try { + const data = await (repOrAppeal ? createRepBlob(appealData, containerID, casefolderID) - : createBlob(appealData, containerID, casefolderID) - ) - .then((data) => { - return respondSuccess(res, { data }); - }) - .catch(() => { - return respondError(res, { - status: 400, - code: "UPLOAD_FAILED", - message: "Failed to upload blob" - }); + : createBlob(appealData, containerID, casefolderID)); + + return respondSuccess(res, { data }); + } catch { + return respondError(res, { + status: 400, + code: "UPLOAD_FAILED", + message: "Failed to upload blob" }); + } }); export const config = { diff --git a/pages/api/file/uploadsinglefile.js b/pages/api/file/uploadsinglefile.js index d5c0a99c..bef3de5a 100644 --- a/pages/api/file/uploadsinglefile.js +++ b/pages/api/file/uploadsinglefile.js @@ -1,8 +1,4 @@ -import { - createBlob, - createRepBlob, - uploadSingleFile -} from "../../../actions/azurestorage"; +import { uploadSingleFile } from "../../../actions/azurestorage"; import nextConnect from "next-connect"; import middleware from "../middleware/middleware"; @@ -37,7 +33,7 @@ const ApiProxy = nextConnect(); ApiProxy.use(middleware); ApiProxy.post(async (req, res) => { - var checkHash = req.query.hash; + const checkHash = req.query.hash; if (typeof checkHash === "undefined" || checkHash.length === 0) { return respondError(res, { @@ -47,9 +43,15 @@ ApiProxy.post(async (req, res) => { }); } - var checkquerypath = "/api/file/uploadsinglefile"; + const hashCandidatePaths = [ + "/api/file/uploadsinglefile", + "/api/file/uploadsinglefile?" + ]; + const isHashValid = hashCandidatePaths.some( + (candidatePath) => hashAPIPath(candidatePath) == "?hash=" + checkHash + ); - if (hashAPIPath(checkquerypath) != "?hash=" + checkHash) { + if (!isHashValid) { return respondError(res, { status: 400, code: "INVALID_HASH", @@ -87,8 +89,8 @@ ApiProxy.post(async (req, res) => { "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" ]; - var allowedFilesFormData = {}; - var invalidFiles = []; // To store the names of invalid files + const allowedFilesFormData = {}; + const invalidFiles = []; // To store the names of invalid files for (const [fileName, fileDetails] of Object.entries(uploadedFiles)) { const file = fileDetails[0]; diff --git a/tests/phase21/api-contract-slice1.test.cjs b/tests/phase21/api-contract-slice1.test.cjs index 2f89aaf8..f055de07 100644 --- a/tests/phase21/api-contract-slice1.test.cjs +++ b/tests/phase21/api-contract-slice1.test.cjs @@ -2,12 +2,14 @@ const runHelperTests = require("./api-response-helper.test.cjs"); const runHandlerTests = require("./file-handler-contract.test.cjs"); const runEmailHandlerTests = require("./email-handler-contract.test.cjs"); const runEndpointHandlerTests = require("./endpoint-handler-contract.test.cjs"); +const runDocumentsHandlerTests = require("./documents-handler-contract.test.cjs"); const run = async () => { await runHelperTests(); await runHandlerTests(); await runEmailHandlerTests(); await runEndpointHandlerTests(); + await runDocumentsHandlerTests(); console.log("Phase 21 combined suite passed."); }; diff --git a/tests/phase21/documents-handler-contract.test.cjs b/tests/phase21/documents-handler-contract.test.cjs new file mode 100644 index 00000000..e1035b3e --- /dev/null +++ b/tests/phase21/documents-handler-contract.test.cjs @@ -0,0 +1,134 @@ +const assert = require("assert"); +const { Readable } = require("stream"); +const { loadModule } = require("./_shared.cjs"); + +const tests = []; +const test = (name, fn) => tests.push({ name, fn }); + +const createRedirectRes = () => { + const state = { + redirectedTo: null, + headers: {}, + piped: false, + headersSent: false + }; + + return { + state, + get headersSent() { + return state.headersSent; + }, + setHeader(key, value) { + state.headers[key.toLowerCase()] = value; + }, + redirect(path) { + state.redirectedTo = path; + state.headersSent = true; + return path; + } + }; +}; + +test("documents download redirects when id/hash query is missing", async () => { + const mod = loadModule("pages/api/documents/download/[id].js", { + getToken: async () => ({ access_token: "tok" }), + consoleLogger: () => {}, + axios: { get: async () => ({}) } + }); + + const req = { query: { id: "DOC-1" } }; + const res = createRedirectRes(); + + await mod.default(req, res); + + assert.strictEqual(res.state.redirectedTo, "/filenotavailable"); +}); + +test("documents download success sets attachment headers and pipes stream", async () => { + const stream = new Readable({ + read() {} + }); + + const mod = loadModule("pages/api/documents/download/[id].js", { + setTimeout: (fn) => { + fn(); + return 0; + }, + getToken: async () => ({ access_token: "tok" }), + consoleLogger: () => {}, + axios: { + get: async () => ({ + headers: { + "content-disposition": "attachment; filename=test-file.pdf" + }, + data: stream + }) + } + }); + + const req = { query: { id: "DOC-1", hash: "h1" } }; + const res = createRedirectRes(); + + stream.pipe = (target) => { + target.state.piped = true; + target.state.headersSent = true; + return target; + }; + + await mod.default(req, res); + + assert.strictEqual( + res.state.headers["content-disposition"], + "attachment; filename=test-file.pdf" + ); + assert.strictEqual( + res.state.headers["content-type"], + "application/octet-stream" + ); + assert.strictEqual(res.state.piped, true); + assert.strictEqual(res.state.redirectedTo, null); +}); + +test("documents download redirects when relay request fails", async () => { + const mod = loadModule("pages/api/documents/download/[id].js", { + setTimeout: (fn) => { + fn(); + return 0; + }, + getToken: async () => ({ access_token: "tok" }), + consoleLogger: () => {}, + axios: { + get: async () => { + throw new Error("relay failed"); + } + } + }); + + const req = { query: { id: "DOC-1", hash: "h1" } }; + const res = createRedirectRes(); + + await mod.default(req, res); + + assert.strictEqual(res.state.redirectedTo, "/filenotavailable"); +}); + +const run = async () => { + let passed = 0; + for (const currentTest of tests) { + await currentTest.fn(); + passed += 1; + } + + console.log( + `Phase 21 documents-handler contract tests passed (${passed}/${tests.length}).` + ); +}; + +module.exports = run; + +if (require.main === module) { + run().catch((error) => { + console.error(error); + process.exit(1); + }); +} diff --git a/tests/phase21/endpoint-handler-contract.test.cjs b/tests/phase21/endpoint-handler-contract.test.cjs index 0b3e09e1..8e165eba 100644 --- a/tests/phase21/endpoint-handler-contract.test.cjs +++ b/tests/phase21/endpoint-handler-contract.test.cjs @@ -323,6 +323,93 @@ test("getportallogin catch path returns PORTAL_LOGIN_FETCH_FAILED", async () => ); }); +test("getportallogin accepts encoded email hash variant", async () => { + const mod = loadModule("pages/api/endpoint/getportallogin_api.js", { + respondError: respondErrorMock, + respondSuccess: respondSuccessMock, + getToken: async () => ({ access_token: "token" }), + hashAPIPath: (input) => + input && input.includes("emailAddress=user%2Btest%40local") + ? "&hash=expected" + : "&hash=other", + azureHeadersPaged: () => ({}), + axios: { + get: async () => ({ data: { value: [{ contactid: "c1" }] } }) + }, + consoleLogger: () => {} + }); + + const req = { + query: { emailAddress: "user+test@local", hash: "expected" } + }; + const res = createRes(); + await mod.default(req, res); + + assert.strictEqual(res.state.statusCode, 200); + assert.deepStrictEqual(JSON.parse(JSON.stringify(res.state.jsonBody)), { + value: [{ contactid: "c1" }] + }); +}); + +test("getnewappeals catch path returns ADMIN_NEW_APPEALS_FETCH_FAILED", async () => { + const mod = loadModule("pages/api/admin/getnewappeals_api.js", { + respondError: respondErrorMock, + respondSuccess: respondSuccessMock, + getToken: async () => ({ access_token: "token" }), + hashAPIPath: () => "&hash=expected", + azureHeadersPagedCustom: () => ({}), + axios: { + get: async () => { + throw new Error("admin new appeals failed"); + } + }, + consoleLogger: () => {}, + _: { has: () => false } + }); + + const req = { query: {} }; + const res = createRes(); + await mod.default(req, res); + + assert.strictEqual(res.state.statusCode, 400); + assert.strictEqual( + res.state.jsonBody.error.code, + "ADMIN_NEW_APPEALS_FETCH_FAILED" + ); +}); + +test("getlatestdocuments catch path returns ADMIN_LATEST_DOCS_FETCH_FAILED", async () => { + const mod = loadModule("pages/api/admin/getlatestdocuments_api.js", { + respondError: respondErrorMock, + respondSuccess: respondSuccessMock, + getToken: async () => ({ access_token: "token" }), + hashAPIPath: () => "&hash=expected", + azureHeadersPagedCustom: () => ({}), + axios: { + get: async () => { + throw new Error("latest docs failed"); + } + }, + consoleLogger: () => {}, + process: { env: { HASHKEY: "abc" } }, + CryptoJS: { + HmacSHA256: () => ({ toString: () => "hash" }), + enc: { Hex: { parse: () => "parsed" } } + }, + _: { has: () => false } + }); + + const req = { query: {} }; + const res = createRes(); + await mod.default(req, res); + + assert.strictEqual(res.state.statusCode, 400); + assert.strictEqual( + res.state.jsonBody.error.code, + "ADMIN_LATEST_DOCS_FETCH_FAILED" + ); +}); + test("getportalloginproxy catch path returns PORTAL_LOGIN_PROXY_FETCH_FAILED", async () => { const mod = loadModule("pages/api/endpoint/getportalloginproxy_api.js", { respondError: respondErrorMock, @@ -3688,6 +3775,37 @@ test("getsipsmedia returns success contract", async () => { assert.strictEqual(res.state.jsonBody["@odata.count"], 4); }); +test("getsipsmedia returns METHOD_NOT_ALLOWED for non-GET methods", async () => { + const mod = loadModule("pages/api/endpoint/getsipsmedia_api.js", { + respondSuccess: respondSuccessMock, + respondError: respondErrorMock + }); + + const req = { method: "POST", query: {} }; + const res = createRes(); + await mod.default(req, res); + + assert.strictEqual(res.state.statusCode, 405); + assert.strictEqual(res.state.jsonBody.error.code, "METHOD_NOT_ALLOWED"); +}); + +test("getappealtypesfornewappeal returns METHOD_NOT_ALLOWED for non-GET methods", async () => { + const mod = loadModule( + "pages/api/endpoint/getappealtypesfornewappeal_api.js", + { + respondSuccess: respondSuccessMock, + respondError: respondErrorMock + } + ); + + const req = { method: "POST", query: {} }; + const res = createRes(); + await mod.default(req, res); + + assert.strictEqual(res.state.statusCode, 405); + assert.strictEqual(res.state.jsonBody.error.code, "METHOD_NOT_ALLOWED"); +}); + const run = async () => { let passed = 0; for (const currentTest of tests) { diff --git a/tests/phase21/file-handler-contract.test.cjs b/tests/phase21/file-handler-contract.test.cjs index cc0f9ee9..3d976800 100644 --- a/tests/phase21/file-handler-contract.test.cjs +++ b/tests/phase21/file-handler-contract.test.cjs @@ -51,6 +51,38 @@ test("upload handler returns INVALID_HASH for wrong hash", async () => { assert.strictEqual(res.state.jsonBody.error.code, "INVALID_HASH"); }); +test("upload handler accepts alternate canonical hash candidate", async () => { + const mod = loadModule("pages/api/file/upload.js", { + nextConnect: createNextConnectMock(), + middleware: () => {}, + hashAPIPath: (queryPath) => + queryPath === "/api/file/upload?" + ? "?hash=expected" + : "?hash=other", + respondError: respondErrorMock, + respondSuccess: respondSuccessMock, + createBlob: async () => ({ id: "blob-alt" }), + createRepBlob: async () => ({ id: "rep-alt" }) + }); + + const req = { + query: { hash: "expected" }, + body: { + appealData: { key: "value" }, + containerID: ["c1"], + casefolderID: ["f1"], + repOrAppeal: false + } + }; + 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: { id: "blob-alt" } + }); +}); + test("deleteblob handler returns MISSING_REQUIRED_QUERY when blobname missing", async () => { const mod = loadModule("pages/api/file/deleteblob.js", { nextConnect: createNextConnectMock(), @@ -71,6 +103,827 @@ test("deleteblob handler returns MISSING_REQUIRED_QUERY when blobname missing", assert.strictEqual(res.state.jsonBody.error.code, "MISSING_REQUIRED_QUERY"); }); +test("deleteblob handler accepts encoded hash variant and normalizes prefixed path", async () => { + let deletedPath = null; + const mod = loadModule("pages/api/file/deleteblob.js", { + nextConnect: createNextConnectMock(), + middleware: () => {}, + hashAPIPath: (queryPath) => + queryPath.includes("/api/file/deleteblob?container=c1") + ? "&hash=expected" + : "&hash=other", + respondError: respondErrorMock, + respondSuccess: respondSuccessMock, + deleteBlob: async (_container, blobPath) => { + deletedPath = blobPath; + return { deleted: true }; + } + }); + + const req = { + query: { + container: "c1", + casefolderID: "TMP-LFE1C", + blobname: + "TMP-LFE1C/files/2026-03-18_-_Site_Location_Plan_-_test document 021.xlsx", + hash: "expected" + } + }; + const res = createRes(); + await mod.default.handler(req, res); + + assert.strictEqual(res.state.statusCode, 200); + assert.strictEqual( + deletedPath, + "TMP-LFE1C/files/2026-03-18_-_Site_Location_Plan_-_test document 021.xlsx" + ); +}); + +test("deleteblob handler dependency failure returns DELETE_BLOB_FAILED", async () => { + const mod = loadModule("pages/api/file/deleteblob.js", { + nextConnect: createNextConnectMock(), + middleware: () => {}, + hashAPIPath: () => "&hash=expected", + respondError: respondErrorMock, + respondSuccess: respondSuccessMock, + deleteBlob: async () => { + throw new Error("delete failed"); + } + }); + + const req = { + query: { + container: "c1", + casefolderID: "f1", + blobname: "doc.pdf", + hash: "expected" + } + }; + const res = createRes(); + await mod.default.handler(req, res); + + assert.strictEqual(res.state.statusCode, 400); + assert.strictEqual(res.state.jsonBody.error.code, "DELETE_BLOB_FAILED"); +}); + +test("deleteblobcase handler accepts encoded hash variant", async () => { + let deletedCaseFolder = null; + const mod = loadModule("pages/api/file/deleteblobcase.js", { + nextConnect: createNextConnectMock(), + middleware: () => {}, + hashAPIPath: (queryPath) => + queryPath.includes("casefolderID=CASE%2FSUB") + ? "&hash=expected" + : "&hash=other", + respondError: respondErrorMock, + respondSuccess: respondSuccessMock, + deleteBlobCase: async (_container, casefolder) => { + deletedCaseFolder = casefolder; + return { deleted: true }; + } + }); + + const req = { + query: { + container: "c1", + casefolderID: "CASE/SUB", + hash: "expected" + } + }; + const res = createRes(); + await mod.default.handler(req, res); + + assert.strictEqual(res.state.statusCode, 200); + assert.strictEqual(deletedCaseFolder, "CASE/SUB"); +}); + +test("deleteblobcase handler dependency failure returns DELETE_BLOB_CASE_FAILED", async () => { + const mod = loadModule("pages/api/file/deleteblobcase.js", { + nextConnect: createNextConnectMock(), + middleware: () => {}, + hashAPIPath: () => "&hash=expected", + respondError: respondErrorMock, + respondSuccess: respondSuccessMock, + deleteBlobCase: async () => { + throw new Error("delete case failed"); + } + }); + + const req = { + query: { + container: "c1", + casefolderID: "f1", + hash: "expected" + } + }; + const res = createRes(); + await mod.default.handler(req, res); + + assert.strictEqual(res.state.statusCode, 400); + assert.strictEqual( + res.state.jsonBody.error.code, + "DELETE_BLOB_CASE_FAILED" + ); +}); + +test("deleteblobrep handler accepts encoded hash variant", async () => { + let deletedRepPath = null; + const mod = loadModule("pages/api/file/deleteblobrep.js", { + nextConnect: createNextConnectMock(), + middleware: () => {}, + hashAPIPath: (queryPath) => + queryPath.includes("repfile=Rep%20Name") + ? "&hash=expected" + : "&hash=other", + respondError: respondErrorMock, + respondSuccess: respondSuccessMock, + deleteBlobRep: async (_container, repPath) => { + deletedRepPath = repPath; + return { deleted: true }; + } + }); + + const req = { + query: { + container: "c1", + casefolderID: "CASE-1", + repfile: "Rep Name", + hash: "expected" + } + }; + const res = createRes(); + await mod.default.handler(req, res); + + assert.strictEqual(res.state.statusCode, 200); + assert.strictEqual(deletedRepPath, "CASE-1/Rep Name"); +}); + +test("deleteblobrep handler dependency failure returns DELETE_BLOB_REP_FAILED", async () => { + const mod = loadModule("pages/api/file/deleteblobrep.js", { + nextConnect: createNextConnectMock(), + middleware: () => {}, + hashAPIPath: () => "&hash=expected", + respondError: respondErrorMock, + respondSuccess: respondSuccessMock, + deleteBlobRep: async () => { + throw new Error("delete rep failed"); + } + }); + + const req = { + query: { + container: "c1", + casefolderID: "f1", + repfile: "rep1", + hash: "expected" + } + }; + const res = createRes(); + await mod.default.handler(req, res); + + assert.strictEqual(res.state.statusCode, 400); + assert.strictEqual(res.state.jsonBody.error.code, "DELETE_BLOB_REP_FAILED"); +}); + +test("deleteawaitingsubmissionfromblob accepts legacy deleteblob hash variant", async () => { + let deletedPath = null; + const mod = loadModule( + "pages/api/file/deleteawaitingsubmissionfromblob.js", + { + nextConnect: createNextConnectMock(), + middleware: () => {}, + hashAPIPath: (queryPath) => + queryPath.includes("/api/file/deleteblob?container=c1") + ? "&hash=expected" + : "&hash=other", + respondError: respondErrorMock, + respondSuccess: respondSuccessMock, + deleteBlob: async (_container, blobPath) => { + deletedPath = blobPath; + return { deleted: true }; + } + } + ); + + const req = { + query: { + container: "c1", + casefolderID: "CASE-1", + blobname: "CASE-1/files/doc.pdf", + hash: "expected" + } + }; + const res = createRes(); + await mod.default.handler(req, res); + + assert.strictEqual(res.state.statusCode, 200); + assert.strictEqual(deletedPath, "CASE-1/files/doc.pdf"); +}); + +test("deleteawaitingsubmissionfromblob dependency failure returns DELETE_AWAITING_SUBMISSION_BLOB_FAILED", async () => { + const mod = loadModule( + "pages/api/file/deleteawaitingsubmissionfromblob.js", + { + nextConnect: createNextConnectMock(), + middleware: () => {}, + hashAPIPath: () => "&hash=expected", + respondError: respondErrorMock, + respondSuccess: respondSuccessMock, + deleteBlob: async () => { + throw new Error("delete awaiting failed"); + } + } + ); + + const req = { + query: { + container: "c1", + casefolderID: "CASE-1", + blobname: "doc.pdf", + hash: "expected" + } + }; + const res = createRes(); + await mod.default.handler(req, res); + + assert.strictEqual(res.state.statusCode, 400); + assert.strictEqual( + res.state.jsonBody.error.code, + "DELETE_AWAITING_SUBMISSION_BLOB_FAILED" + ); +}); + +test("getrepsblob handler success returns representations payload", async () => { + const mod = loadModule("pages/api/file/getrepsblob.js", { + nextConnect: createNextConnectMock(), + middleware: () => {}, + hashAPIPath: () => "&hash=expected", + respondError: respondErrorMock, + respondSuccess: respondSuccessMock, + consoleLogger: () => {}, + getRepsBlobs: async () => [ + { path: "c/ref_rep.json", name: "ref_rep.json" } + ], + downloadAllRepsFiles: async () => ({ + "@odata.count": 1, + value: [{ id: "r1" }] + }) + }); + + const req = { + query: { container: "c1", 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)), { + "@odata.count": 1, + value: [{ id: "r1" }] + }); +}); + +test("getrepsblob handler dependency failure returns GET_REPS_BLOB_FAILED", async () => { + const mod = loadModule("pages/api/file/getrepsblob.js", { + nextConnect: createNextConnectMock(), + middleware: () => {}, + hashAPIPath: () => "&hash=expected", + respondError: respondErrorMock, + respondSuccess: respondSuccessMock, + consoleLogger: () => {}, + getRepsBlobs: async () => { + throw new Error("storage failure"); + }, + downloadAllRepsFiles: async () => ({}) + }); + + const req = { + query: { container: "c1", hash: "expected" } + }; + const res = createRes(); + await mod.default.handler(req, res); + + assert.strictEqual(res.state.statusCode, 400); + assert.strictEqual(res.state.jsonBody.error.code, "GET_REPS_BLOB_FAILED"); +}); + +test("getrepsblob handler accepts encoded container hash variant", async () => { + const mod = loadModule("pages/api/file/getrepsblob.js", { + nextConnect: createNextConnectMock(), + middleware: () => {}, + hashAPIPath: (queryPath) => + queryPath.includes("container=cont%2F1") + ? "&hash=expected" + : "&hash=other", + respondError: respondErrorMock, + respondSuccess: respondSuccessMock, + consoleLogger: () => {}, + getRepsBlobs: async () => [{ path: "cont/rep1.json" }], + downloadAllRepsFiles: async () => ({ value: [{ id: "r-encoded" }] }) + }); + + const req = { + query: { container: "cont/1", hash: "expected" } + }; + const res = createRes(); + await mod.default.handler(req, res); + + assert.strictEqual(res.state.statusCode, 200); + assert.deepStrictEqual(JSON.parse(JSON.stringify(res.state.jsonBody)), { + value: [{ id: "r-encoded" }] + }); +}); + +test("uploadsinglefile handler returns HASH_REQUIRED when hash missing", async () => { + const mod = loadModule("pages/api/file/uploadsinglefile.js", { + nextConnect: createNextConnectMock(), + middleware: () => {}, + hashAPIPath: () => "?hash=expected", + respondError: respondErrorMock, + respondSuccess: respondSuccessMock, + consoleLogger: () => {}, + uploadSingleFile: async () => ({ uploaded: 0 }), + fileTypeFromBuffer: async () => ({ mime: "application/pdf" }), + fs: { readFileSync: () => Buffer.from("x") } + }); + + const req = { query: {}, body: {}, files: {} }; + const res = createRes(); + await mod.default.handler(req, res); + + assert.strictEqual(res.state.statusCode, 400); + assert.strictEqual(res.state.jsonBody.error.code, "HASH_REQUIRED"); +}); + +test("uploadsinglefile handler dependency failure returns UPLOAD_SINGLE_FILE_FAILED", async () => { + const mod = loadModule("pages/api/file/uploadsinglefile.js", { + nextConnect: createNextConnectMock(), + middleware: () => {}, + hashAPIPath: () => "?hash=expected", + respondError: respondErrorMock, + respondSuccess: respondSuccessMock, + consoleLogger: () => {}, + uploadSingleFile: async () => { + throw new Error("upload failed"); + }, + fileTypeFromBuffer: async () => ({ mime: "application/pdf" }), + fs: { readFileSync: () => Buffer.from("x") } + }); + + const req = { + query: { hash: "expected" }, + body: { containerID: ["c1"], casefolderID: ["f1"] }, + files: {} + }; + const res = createRes(); + await mod.default.handler(req, res); + + assert.strictEqual(res.state.statusCode, 500); + assert.strictEqual( + res.state.jsonBody.error.code, + "UPLOAD_SINGLE_FILE_FAILED" + ); +}); + +test("createrepcompletemessage handler accepts encoded hash variant", async () => { + const mod = loadModule("pages/api/file/createrepcompletemessage_api.js", { + nextConnect: createNextConnectMock(), + middleware: () => {}, + hashAPIPath: (queryPath) => + queryPath.includes("repid=rep%201") + ? "&hash=expected" + : "&hash=other", + respondError: respondErrorMock, + respondSuccess: respondSuccessMock, + consoleLogger: () => {}, + createRepCompleteMessage: async () => ({ ok: true }) + }); + + const req = { + query: { + container: "c1", + tempcaseref: "TMP/1", + repid: "rep 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)), { + ok: true + }); +}); + +test("createrepcompletemessage handler dependency failure returns CREATE_REP_COMPLETE_MESSAGE_FAILED", async () => { + const mod = loadModule("pages/api/file/createrepcompletemessage_api.js", { + nextConnect: createNextConnectMock(), + middleware: () => {}, + hashAPIPath: () => "&hash=expected", + respondError: respondErrorMock, + respondSuccess: respondSuccessMock, + consoleLogger: () => {}, + createRepCompleteMessage: async () => { + throw new Error("rep message failed"); + } + }); + + const req = { + query: { + container: "c1", + tempcaseref: "TMP1", + repid: "rep1", + hash: "expected" + } + }; + const res = createRes(); + await mod.default.handler(req, res); + + assert.strictEqual(res.state.statusCode, 400); + assert.strictEqual( + res.state.jsonBody.error.code, + "CREATE_REP_COMPLETE_MESSAGE_FAILED" + ); +}); + +test("getbloblistproxy success returns proxied payload", async () => { + const mod = loadModule("pages/api/file/getbloblistproxy.js", { + respondError: respondErrorMock, + respondSuccess: respondSuccessMock, + getToken: async () => ({ access_token: "tok" }), + azureHeaders: () => ({ headers: { Authorization: "Bearer tok" } }), + hashAPIPath: () => "&hash=abc", + consoleLogger: () => {}, + process: { env: { API_ROOT: "http://example.com" } }, + axios: { + get: async () => ({ data: { value: [{ id: "b1" }] } }) + } + }); + + const req = { + query: { container: "cont/1", casefolderID: "CASE/1" } + }; + const res = createRes(); + await mod.default(req, res); + + assert.strictEqual(res.state.statusCode, 200); + assert.deepStrictEqual(JSON.parse(JSON.stringify(res.state.jsonBody)), { + value: [{ id: "b1" }] + }); +}); + +test("getbloblistproxy dependency failure returns GET_BLOB_LIST_PROXY_FAILED", async () => { + const mod = loadModule("pages/api/file/getbloblistproxy.js", { + respondError: respondErrorMock, + respondSuccess: respondSuccessMock, + getToken: async () => ({ access_token: "tok" }), + azureHeaders: () => ({ headers: {} }), + hashAPIPath: () => "&hash=abc", + consoleLogger: () => {}, + process: { env: { API_ROOT: "http://example.com" } }, + axios: { + get: async () => { + throw new Error("proxy failed"); + } + } + }); + + const req = { + query: { container: "c1", casefolderID: "f1" } + }; + const res = createRes(); + await mod.default(req, res); + + assert.strictEqual(res.state.statusCode, 400); + assert.strictEqual( + res.state.jsonBody.error.code, + "GET_BLOB_LIST_PROXY_FAILED" + ); +}); + +test("getrepsblobproxy success returns proxied payload", async () => { + const mod = loadModule("pages/api/file/getrepsblobproxy.js", { + respondError: respondErrorMock, + respondSuccess: respondSuccessMock, + getToken: async () => ({ access_token: "tok" }), + azureHeaders: () => ({ headers: {} }), + hashAPIPath: () => "&hash=abc", + consoleLogger: () => {}, + process: { env: { API_ROOT: "http://example.com" } }, + axios: { + get: async () => ({ data: { value: [{ id: "r1" }] } }) + } + }); + + const req = { + query: { container: "c1" } + }; + const res = createRes(); + await mod.default(req, res); + + assert.strictEqual(res.state.statusCode, 200); + assert.deepStrictEqual(JSON.parse(JSON.stringify(res.state.jsonBody)), { + value: [{ id: "r1" }] + }); +}); + +test("getawaitingsubmissionfromblobproxy dependency failure returns GET_AWAITING_SUBMISSION_PROXY_FAILED", async () => { + const mod = loadModule( + "pages/api/file/getawaitingsubmissionfromblobproxy.js", + { + respondError: respondErrorMock, + respondSuccess: respondSuccessMock, + getToken: async () => ({ access_token: "tok" }), + azureHeaders: () => ({ headers: {} }), + hashAPIPath: () => "&hash=abc", + consoleLogger: () => {}, + process: { env: { API_ROOT: "http://example.com" } }, + axios: { + get: async () => { + throw new Error("awaiting failed"); + } + } + } + ); + + const req = { + query: { container: "c1" } + }; + const res = createRes(); + await mod.default(req, res); + + assert.strictEqual(res.state.statusCode, 400); + assert.strictEqual( + res.state.jsonBody.error.code, + "GET_AWAITING_SUBMISSION_PROXY_FAILED" + ); +}); + +test("createappealcompletemessageproxy dependency failure returns CREATE_APPEAL_COMPLETE_MESSAGE_PROXY_FAILED", async () => { + const mod = loadModule( + "pages/api/file/createappealcompletemessageproxy_api.js", + { + nextConnect: createNextConnectMock(), + middleware: () => {}, + respondError: respondErrorMock, + respondSuccess: respondSuccessMock, + getToken: async () => ({ access_token: "tok" }), + azureHeaders: () => ({ headers: {} }), + hashAPIPath: () => "&hash=abc", + consoleLogger: () => {}, + process: { env: { API_ROOT: "http://example.com" } }, + axios: { + get: async () => { + throw new Error("create appeal proxy failed"); + } + } + } + ); + + const req = { + query: { container: "c1", tempcaseref: "TMP1" } + }; + const res = createRes(); + await mod.default.handler(req, res); + + assert.strictEqual(res.state.statusCode, 400); + assert.strictEqual( + res.state.jsonBody.error.code, + "CREATE_APPEAL_COMPLETE_MESSAGE_PROXY_FAILED" + ); +}); + +test("createappealcompletemessage_api accepts encoded hash variant and returns success", async () => { + const mod = loadModule( + "pages/api/file/createappealcompletemessage_api.js", + { + nextConnect: createNextConnectMock(), + middleware: () => {}, + respondError: respondErrorMock, + respondSuccess: respondSuccessMock, + hashAPIPath: (queryPath) => + queryPath.includes("tempcaseref=TMP%2F1") + ? "&hash=expected" + : "&hash=other", + getProgressBlobs: async () => ({ path: "TMP/1/progress.json" }), + downloadProgressFile: async (_container, path) => { + if (path.includes("_case.json")) { + return { + "customerid_contact@odata.bind": + "/contacts(00000000-0000-0000-0000-000000000001)" + }; + } + + return { + pinswg_siteaddressline1: "Line 1", + pinswg_siteaddressline2: "", + pinswg_siteaddresstown: "Town", + pinswg_siteaddresscounty: "County", + pinswg_siteaddresspostcode: "CF1 1AA", + pinswg_lpaapplicationreference: "LPA-1", + pinswg_developmentdescription: "Desc", + pinswg_name: "tmp" + }; + }, + createBlob: async () => ({}), + getCaseBlob: async () => ({}), + createCaseCompleteMessage: async () => ({}), + updateAccount: async () => ({}), + consoleLogger: () => {}, + _: { isEmpty: (v) => v === undefined || v === null || v === "" } + } + ); + + const req = { + query: { + container: "cont/1", + tempcaseref: "TMP/1", + inv: "846040001", + 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)), { + status: "success" + }); +}); + +test("createappealcompletemessage_api dependency failure returns CREATE_APPEAL_COMPLETE_MESSAGE_FAILED", async () => { + const mod = loadModule( + "pages/api/file/createappealcompletemessage_api.js", + { + nextConnect: createNextConnectMock(), + middleware: () => {}, + respondError: respondErrorMock, + respondSuccess: respondSuccessMock, + hashAPIPath: () => "&hash=expected", + getProgressBlobs: async () => { + throw new Error("progress failed"); + }, + consoleLogger: () => {}, + _: { isEmpty: () => true } + } + ); + + const req = { + query: { + container: "c1", + tempcaseref: "TMP1", + hash: "expected" + } + }; + const res = createRes(); + await mod.default.handler(req, res); + + assert.strictEqual(res.state.statusCode, 400); + assert.strictEqual( + res.state.jsonBody.error.code, + "CREATE_APPEAL_COMPLETE_MESSAGE_FAILED" + ); +}); + +test("createcaseinvolvement returns record exists on 412 conflict", async () => { + const mod = loadModule("pages/api/file/createcaseinvolvement_api.js", { + respondError: respondErrorMock, + respondSuccess: respondSuccessMock, + getToken: async () => ({ access_token: "tok" }), + hashAPIPath: () => "?hash=abc", + consoleLogger: () => {}, + process: { + env: { CRMURL: "crm.example", RELAY_ROOT: "https://relay/" } + }, + axios: async () => { + const error = new Error("exists"); + error.status = 412; + throw error; + } + }); + + const req = { + body: { contactid: "c1", incidentid: "i1" } + }; + const res = createRes(); + await mod.default(req, res); + + assert.strictEqual(res.state.statusCode, 200); + assert.deepStrictEqual(JSON.parse(JSON.stringify(res.state.jsonBody)), { + record: "exists" + }); +}); + +test("createrepinvolvement dependency failure returns CREATE_REP_INVOLVEMENT_FAILED", async () => { + const mod = loadModule("pages/api/file/createrepinvolvement_api.js", { + respondError: respondErrorMock, + respondSuccess: respondSuccessMock, + getToken: async () => ({ access_token: "tok" }), + hashAPIPath: () => "?hash=abc", + consoleLogger: () => {}, + process: { + env: { + CRMURL: "crm.example", + CRMURL_VERSION: "v9.2", + RELAY_ROOT: "https://relay/" + } + }, + axios: async () => { + throw new Error("rep involvement failed"); + } + }); + + const req = { + body: { contactid: "c1", incidentid: "i1" } + }; + const res = createRes(); + await mod.default(req, res); + + assert.strictEqual(res.state.statusCode, 400); + assert.strictEqual( + res.state.jsonBody.error.code, + "CREATE_REP_INVOLVEMENT_FAILED" + ); +}); + +test("updatecase dependency failure returns UPDATE_CASE_FAILED", async () => { + const mod = loadModule("pages/api/file/updatecase_api.js", { + respondError: respondErrorMock, + respondSuccess: respondSuccessMock, + getToken: async () => ({ access_token: "tok" }), + hashAPIPath: () => "?hash=abc", + consoleLogger: () => {}, + process: { env: { RELAY_ROOT: "https://relay/" } }, + axios: async () => { + throw new Error("update failed"); + } + }); + + const req = { + query: { + updateFormCollection: "pinswg_forms", + appealObj: "obj1", + incident: "inc1" + }, + body: { field: "value" } + }; + const res = createRes(); + await mod.default(req, res); + + assert.strictEqual(res.state.statusCode, 400); + assert.strictEqual(res.state.jsonBody.error.code, "UPDATE_CASE_FAILED"); +}); + +test("getawaitingsubmissionfromblob handler success returns payload", async () => { + const mod = loadModule("pages/api/file/getawaitingsubmissionfromblob.js", { + nextConnect: createNextConnectMock(), + middleware: () => {}, + hashAPIPath: () => "&hash=expected", + respondError: respondErrorMock, + respondSuccess: respondSuccessMock, + getAllProgressBlobs: async () => [{ path: "c1/case1_appeal.json" }], + downloadAllProgressFiles: async () => ({ value: [{ id: "a1" }] }) + }); + + const req = { + query: { container: "c1", hash: "expected" } + }; + const res = createRes(); + await mod.default.handler(req, res); + + assert.strictEqual(res.state.statusCode, 200); + assert.deepStrictEqual(JSON.parse(JSON.stringify(res.state.jsonBody)), { + value: [{ id: "a1" }] + }); +}); + +test("getawaitingsubmissionfromblob handler dependency failure returns GET_AWAITING_SUBMISSION_BLOB_FAILED", async () => { + const mod = loadModule("pages/api/file/getawaitingsubmissionfromblob.js", { + nextConnect: createNextConnectMock(), + middleware: () => {}, + hashAPIPath: () => "&hash=expected", + respondError: respondErrorMock, + respondSuccess: respondSuccessMock, + getAllProgressBlobs: async () => { + throw new Error("progress fetch failed"); + }, + downloadAllProgressFiles: async () => ({}) + }); + + const req = { + query: { container: "c1", hash: "expected" } + }; + const res = createRes(); + await mod.default.handler(req, res); + + assert.strictEqual(res.state.statusCode, 400); + assert.strictEqual( + res.state.jsonBody.error.code, + "GET_AWAITING_SUBMISSION_BLOB_FAILED" + ); +}); + test("getbloblist handler returns INVALID_HASH for mismatch", async () => { const mod = loadModule("pages/api/file/getbloblist.js", { nextConnect: createNextConnectMock(), @@ -92,6 +945,138 @@ test("getbloblist handler returns INVALID_HASH for mismatch", async () => { assert.strictEqual(res.state.jsonBody.error.code, "INVALID_HASH"); }); +test("getbloblist handler accepts encoded casefolder hash variant", async () => { + const mod = loadModule("pages/api/file/getbloblist.js", { + nextConnect: createNextConnectMock(), + middleware: () => {}, + hashAPIPath: (queryPath) => + queryPath.includes("casefolderID=CASE%2FSUB") + ? "&hash=expected" + : "&hash=other", + respondError: respondErrorMock, + respondSuccess: respondSuccessMock, + getBlobs: async () => ({ file: "a.pdf" }), + getRepsFilesBlobs: async () => ({ file: "rep.pdf" }) + }); + + const req = { + query: { + container: "c1", + casefolderID: "CASE/SUB", + hash: "expected" + } + }; + const res = createRes(); + await mod.default.handler(req, res); + + assert.strictEqual(res.state.statusCode, 200); + assert.deepStrictEqual(JSON.parse(JSON.stringify(res.state.jsonBody)), { + value: [{ file: "rep.pdf" }] + }); +}); + +test("getbloblist handler dependency failure returns GET_BLOB_LIST_FAILED", async () => { + const mod = loadModule("pages/api/file/getbloblist.js", { + nextConnect: createNextConnectMock(), + middleware: () => {}, + hashAPIPath: () => "&hash=expected", + respondError: respondErrorMock, + respondSuccess: respondSuccessMock, + getBlobs: async () => { + throw new Error("blob lookup failed"); + }, + getRepsFilesBlobs: async () => ({ file: "rep.pdf" }) + }); + + const req = { + query: { container: "c1", casefolderID: "f1", hash: "expected" } + }; + const res = createRes(); + await mod.default.handler(req, res); + + assert.strictEqual(res.state.statusCode, 400); + assert.strictEqual(res.state.jsonBody.error.code, "GET_BLOB_LIST_FAILED"); +}); + +test("getprogressobjblob handler success returns payload", async () => { + const mod = loadModule("pages/api/file/getprogressobjblob.js", { + nextConnect: createNextConnectMock(), + middleware: () => {}, + hashAPIPath: () => "&hash=expected", + respondError: respondErrorMock, + respondSuccess: respondSuccessMock, + getProgressBlobs: async () => ({ path: "CASE1/CASE1_appeal.json" }), + downloadProgressFile: async () => ({ id: "progress-1" }) + }); + + const req = { + query: { container: "c1", casefolderID: "CASE1", 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)), { + id: "progress-1" + }); +}); + +test("getprogressobjblob handler accepts encoded casefolder hash variant", async () => { + const mod = loadModule("pages/api/file/getprogressobjblob.js", { + nextConnect: createNextConnectMock(), + middleware: () => {}, + hashAPIPath: (queryPath) => + queryPath.includes("casefolderID=CASE%2FSUB") + ? "&hash=expected" + : "&hash=other", + respondError: respondErrorMock, + respondSuccess: respondSuccessMock, + getProgressBlobs: async () => ({ path: "CASE/SUB/CASE_appeal.json" }), + downloadProgressFile: async () => ({ id: "progress-2" }) + }); + + const req = { + query: { + container: "c1", + casefolderID: "CASE/SUB", + 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)), { + id: "progress-2" + }); +}); + +test("getprogressobjblob handler dependency failure returns GET_PROGRESS_OBJ_BLOB_FAILED", async () => { + const mod = loadModule("pages/api/file/getprogressobjblob.js", { + nextConnect: createNextConnectMock(), + middleware: () => {}, + hashAPIPath: () => "&hash=expected", + respondError: respondErrorMock, + respondSuccess: respondSuccessMock, + getProgressBlobs: async () => { + throw new Error("progress lookup failed"); + }, + downloadProgressFile: async () => ({}) + }); + + const req = { + query: { container: "c1", casefolderID: "CASE1", hash: "expected" } + }; + const res = createRes(); + await mod.default.handler(req, res); + + assert.strictEqual(res.state.statusCode, 400); + assert.strictEqual( + res.state.jsonBody.error.code, + "GET_PROGRESS_OBJ_BLOB_FAILED" + ); +}); + test("setupcontainer handler returns MISSING_REQUIRED_QUERY when ident missing", async () => { const mod = loadModule("pages/api/file/setupcontainer.js", { nextConnect: createNextConnectMock(), @@ -111,6 +1096,31 @@ test("setupcontainer handler returns MISSING_REQUIRED_QUERY when ident missing", assert.strictEqual(res.state.jsonBody.error.code, "MISSING_REQUIRED_QUERY"); }); +test("setupcontainer handler accepts encoded ident hash variant", async () => { + const mod = loadModule("pages/api/file/setupcontainer.js", { + nextConnect: createNextConnectMock(), + middleware: () => {}, + hashAPIPath: (queryPath) => + queryPath.includes("ident=container%2Fname") + ? "&hash=expected" + : "&hash=other", + consoleLogger: () => {}, + respondError: respondErrorMock, + respondSuccess: respondSuccessMock, + createContainer: async () => ({ created: true }) + }); + + const req = { query: { ident: "container/name", 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: { created: true } + }); +}); + test("upload handler success returns data payload with 200", async () => { const mod = loadModule("pages/api/file/upload.js", { nextConnect: createNextConnectMock(), @@ -263,6 +1273,248 @@ test("setupcontainer dependency failure returns SETUP_CONTAINER_FAILED", async ( assert.strictEqual(res.state.jsonBody.error.code, "SETUP_CONTAINER_FAILED"); }); +test("downloadblob dependency failure returns DOWNLOAD_BLOB_FAILED", async () => { + const mod = loadModule("pages/api/file/downloadblob.js", { + nextConnect: createNextConnectMock(), + middleware: () => {}, + hashAPIPath: () => "&hash=expected", + respondError: respondErrorMock, + downloadFile: async () => { + throw new Error("download failed"); + } + }); + + const req = { + query: { + container: "c1", + casefolderID: "f1", + blobname: "doc.pdf", + hash: "expected" + } + }; + const res = createRes(); + await mod.default.handler(req, res); + + assert.strictEqual(res.state.statusCode, 400); + assert.strictEqual(res.state.jsonBody.error.code, "DOWNLOAD_BLOB_FAILED"); +}); + +test("generateappealpdfcopy returns INCIDENT_ID_REQUIRED when docProps missing", async () => { + const mod = loadModule("pages/api/file/generateappealpdfcopy.js", { + respondError: respondErrorMock, + getCaseByID: async () => [], + getPortalModuleDetails: async () => ({ value: [{}] }), + getAppealPDFDocs: async () => [], + getFormCollectionByID: () => ({ UrlName: "planningappeals78" }), + getPickLists: async () => ({}), + planningappeals78_pdf: () => ({}), + other_pdf: () => ({}), + pdf: () => ({ toBuffer: async () => Buffer.from("pdf") }) + }); + + const req = { body: {} }; + const res = createRes(); + await mod.default(req, res); + + assert.strictEqual(res.state.statusCode, 400); + assert.strictEqual(res.state.jsonBody.error.code, "INCIDENT_ID_REQUIRED"); +}); + +test("generateappealpdfcopy returns CASE_NOT_FOUND when case lookup is empty", async () => { + const mod = loadModule("pages/api/file/generateappealpdfcopy.js", { + respondError: respondErrorMock, + getCaseByID: async () => [], + getPortalModuleDetails: async () => ({ value: [{}] }), + getAppealPDFDocs: async () => [], + getFormCollectionByID: () => ({ UrlName: "planningappeals78" }), + getPickLists: async () => ({}), + planningappeals78_pdf: () => ({}), + other_pdf: () => ({}), + pdf: () => ({ toBuffer: async () => Buffer.from("pdf") }) + }); + + const req = { body: { docProps: "i1" } }; + const res = createRes(); + await mod.default(req, res); + + assert.strictEqual(res.state.statusCode, 404); + assert.strictEqual(res.state.jsonBody.error.code, "CASE_NOT_FOUND"); +}); + +test("generateappealpdfcopy returns FORM_COLLECTION_NOT_FOUND when collection is unresolved", async () => { + const mod = loadModule("pages/api/file/generateappealpdfcopy.js", { + respondError: respondErrorMock, + getCaseByID: async () => [ + { + pinswg_appealcasetype: 846040000, + ticketnumber: "CAS-1", + description: "desc" + } + ], + getPortalModuleDetails: async () => ({ value: [{}] }), + getAppealPDFDocs: async () => [], + getFormCollectionByID: () => null, + getPickLists: async () => ({}), + planningappeals78_pdf: () => ({}), + other_pdf: () => ({}), + pdf: () => ({ toBuffer: async () => Buffer.from("pdf") }) + }); + + const req = { body: { docProps: "i1" } }; + const res = createRes(); + await mod.default(req, res); + + assert.strictEqual(res.state.statusCode, 400); + assert.strictEqual( + res.state.jsonBody.error.code, + "FORM_COLLECTION_NOT_FOUND" + ); +}); + +test("generateappealpdfcopy success returns pdf buffer and headers", async () => { + const mod = loadModule("pages/api/file/generateappealpdfcopy.js", { + respondError: respondErrorMock, + getCaseByID: async () => [ + { + pinswg_appealcasetype: 846040000, + ticketnumber: "CAS-1", + description: "desc" + } + ], + getPortalModuleDetails: async () => ({ value: [{}] }), + getAppealPDFDocs: async () => [{ id: "d1" }], + getFormCollectionByID: () => ({ + UrlName: "planningappeals78", + LogicalCollectionName: "pinswg_planningappeals78s" + }), + getPickLists: async () => ({ a: 1 }), + planningappeals78_pdf: () => ({ type: "pdfComponent" }), + other_pdf: () => ({ type: "otherComponent" }), + pdf: () => ({ toBuffer: async () => Buffer.from("pdf-content") }) + }); + + const req = { body: { docProps: "i1" } }; + const res = createRes(); + await mod.default(req, res); + + assert.strictEqual(res.state.headers["content-type"], "application/pdf"); + assert.strictEqual( + typeof res.state.headers["content-disposition"], + "string" + ); + assert.strictEqual(res.state.sentBody.toString(), "pdf-content"); +}); + +test("generateappealpdfcopy catch path returns APPEAL_PDF_COPY_GENERATION_FAILED", async () => { + const mod = loadModule("pages/api/file/generateappealpdfcopy.js", { + respondError: respondErrorMock, + getCaseByID: async () => [ + { + pinswg_appealcasetype: 846040000, + ticketnumber: "CAS-1", + description: "desc" + } + ], + getPortalModuleDetails: async () => ({ value: [{}] }), + getAppealPDFDocs: async () => [{ id: "d1" }], + getFormCollectionByID: () => ({ + UrlName: "planningappeals78", + LogicalCollectionName: "pinswg_planningappeals78s" + }), + getPickLists: async () => ({ a: 1 }), + planningappeals78_pdf: () => ({ type: "pdfComponent" }), + other_pdf: () => ({ type: "otherComponent" }), + pdf: () => ({ + toBuffer: async () => { + throw new Error("pdf failure"); + } + }) + }); + + const req = { body: { docProps: "i1" } }; + const res = createRes(); + await mod.default(req, res); + + assert.strictEqual(res.state.statusCode, 400); + assert.strictEqual( + res.state.jsonBody.error.code, + "APPEAL_PDF_COPY_GENERATION_FAILED" + ); +}); + +test("generatepdf catch path returns GENERATE_PDF_FAILED", async () => { + const { Readable } = require("stream"); + const mod = loadModule("pages/api/file/generatepdf.js", { + Buffer, + respondError: respondErrorMock, + respondSuccess: respondSuccessMock, + hashAPIPath: () => "&hash=expected", + consoleLogger: () => {}, + ReactPDF: { + renderToStream: async () => Readable.from(["pdf"]) + }, + createRepPDFBlob: async () => { + throw new Error("pdf write failed"); + }, + other_pdf: () => ({ type: "other" }) + }); + + const req = { + query: { hash: "expected", download: "true" }, + body: { + representationType: "Other", + repfile_name: "rep-file", + containerID: "c1", + casefolderID: "f1", + caseRef: "CAS-1" + } + }; + const res = createRes(); + await mod.default(req, res); + + assert.strictEqual(res.state.statusCode, 400); + assert.strictEqual(res.state.jsonBody.error.code, "GENERATE_PDF_FAILED"); +}); + +test("generateappealpdf catch path returns GENERATE_APPEAL_PDF_FAILED", async () => { + const mod = loadModule("pages/api/file/generateappealpdf.js", { + respondError: respondErrorMock, + respondSuccess: respondSuccessMock, + hashAPIPath: () => "&hash=expected", + getTempCaseBlob: async () => ({ ticketnumber: "CAS-1" }), + getProgressBlobs: async () => ({ path: "CAS-1/progress.json" }), + downloadProgressFile: async () => { + throw new Error("progress failed"); + }, + getPickLists: async () => ({}), + planningappeals78_pdf: () => ({ type: "planning" }), + other_pdf: () => ({ type: "other" }), + ReactPDF: { + renderToStream: async () => { + throw new Error("render failed"); + } + } + }); + + const req = { + query: { hash: "expected", appealType: "846040000" }, + body: { + containerID: "c1", + casefolderID: "f1", + caseRef: "CAS-1", + filesList: [] + } + }; + const res = createRes(); + await mod.default(req, res); + + assert.strictEqual(res.state.statusCode, 400); + assert.strictEqual( + res.state.jsonBody.error.code, + "GENERATE_APPEAL_PDF_FAILED" + ); +}); + const run = async () => { let passed = 0; for (const currentTest of tests) {