diff --git a/memory-bank/activeContext.md b/memory-bank/activeContext.md index b76346dd..6b28e99d 100644 --- a/memory-bank/activeContext.md +++ b/memory-bank/activeContext.md @@ -610,6 +610,22 @@ - Added focused test file: - `tests/phase19/service-behaviour.test.cjs` (5/5 pass) +## Phase 20 endpoint consistency slice (2026-03-14) + +- Continued on branch: + - `TASK22057-phase17-hardening-closeout` +- Applied a small consistency-only slice across: + - `pages/api/endpoint/getbasicdnssearch_api.js` + - `pages/api/endpoint/getbasicdnsurlsearch_api.js` + - `pages/api/endpoint/getbasicsearchdetailspaged_api.js` +- Scope: + - removed noisy query logging in `getbasicdnssearch_api` + - added early 400 for missing/empty `searchString` in `getbasicdnsurlsearch_api` + - added early 400 for missing required params in `getbasicsearchdetailspaged_api` + - preserved response shapes/signatures +- Added focused test file: + - `tests/phase20/service-behaviour.test.cjs` (5/5 pass) + ## Likely next steps 1. Stabilize and simplify breadcrumb/back-link decision logic with focused regression checks. diff --git a/memory-bank/change-log.md b/memory-bank/change-log.md index 83ff0d49..2246687d 100644 --- a/memory-bank/change-log.md +++ b/memory-bank/change-log.md @@ -1258,3 +1258,47 @@ Validation: Follow-ups: - Continue same small-batch consistency pattern for remaining sensitive endpoint handlers only where clear guard/logging drift exists. + +--- + +### CL-034: Phase 20 endpoint consistency slice — dns/basic-search guard + logging cleanup + +date: 2026-03-14 +author: Cline +scope: `pages/api/endpoint/{getbasicdnssearch_api,getbasicdnsurlsearch_api,getbasicsearchdetailspaged_api}.js`, `tests/phase20/service-behaviour.test.cjs`, `memory-bank/*` +type: change +rationale: Deliver a small reversible consistency-only slice by removing noisy logging and standardizing required-input negative-path handling in additional sensitive endpoint handlers. +impact: Improves endpoint negative-path consistency and logging hygiene while preserving response signatures/shapes. +status: completed + +Summary: + +- Removed noisy `console.log` query logging in `getbasicdnssearch_api`. +- Added early 400 handling for missing/empty `searchString` in `getbasicdnsurlsearch_api`. +- Added early 400 handling for missing required params (`appealTypeName`, `primaryIdAttribute`, `incidentID`) in `getbasicsearchdetailspaged_api`. +- Added focused behavioural test pack: + - `tests/phase20/service-behaviour.test.cjs` (5 tests) + +Validation: + +- `node tests/phase6/service-parity.test.cjs` -> pass +- `node tests/phase6/service-behaviour.test.cjs` -> pass (8/8) +- `node tests/phase7/service-behaviour.test.cjs` -> pass (12/12) +- `node tests/phase8/service-behaviour.test.cjs` -> pass (5/5) +- `node tests/phase9/service-behaviour.test.cjs` -> pass (5/5) +- `node tests/phase10/service-behaviour.test.cjs` -> pass (5/5) +- `node tests/phase11/service-behaviour.test.cjs` -> pass (4/4) +- `node tests/phase12/service-behaviour.test.cjs` -> pass (4/4) +- `node tests/phase13/service-behaviour.test.cjs` -> pass (7/7) +- `node tests/phase14/service-behaviour.test.cjs` -> pass (9/9) +- `node tests/phase15/service-behaviour.test.cjs` -> pass (5/5) +- `node tests/phase16/service-behaviour.test.cjs` -> pass (4/4) +- `node tests/phase17/service-behaviour.test.cjs` -> pass (6/6) +- `node tests/phase18/service-behaviour.test.cjs` -> pass (8/8) +- `node tests/phase19/service-behaviour.test.cjs` -> pass (5/5) +- `node tests/phase20/service-behaviour.test.cjs` -> pass (5/5) +- `npm run lint` -> warnings only (pre-existing hook dependency warnings) + +Follow-ups: + +- Continue same small-batch consistency pattern for remaining endpoint handlers where required-input/logging drift is clear. diff --git a/memory-bank/progress.md b/memory-bank/progress.md index 50785909..4d330a1d 100644 --- a/memory-bank/progress.md +++ b/memory-bank/progress.md @@ -657,6 +657,42 @@ - `node tests/phase19/service-behaviour.test.cjs` -> **pass** (5/5) - `npm run lint` -> **warnings only** (pre-existing `react-hooks/exhaustive-deps` warnings) +## Latest update (2026-03-14 — Phase 20 endpoint consistency slice) + +- Continued on branch: + - `TASK22057-phase17-hardening-closeout` +- Delivered a small additional consistency-only endpoint slice across: + - `pages/api/endpoint/getbasicdnssearch_api.js` + - `pages/api/endpoint/getbasicdnsurlsearch_api.js` + - `pages/api/endpoint/getbasicsearchdetailspaged_api.js` +- Scope delivered: + - removed noisy query logging in `getbasicdnssearch_api` + - standardized missing/empty `searchString` handling in `getbasicdnsurlsearch_api` with early 400 + - standardized missing required input handling in `getbasicsearchdetailspaged_api` (`appealTypeName`, `primaryIdAttribute`, `incidentID`) with early 400 + - preserved response-shape/signature contracts +- Added focused tests: + - `tests/phase20/service-behaviour.test.cjs` (5/5 passing) + +### Validation snapshot (Phase 20) + +- `node tests/phase6/service-parity.test.cjs` -> **pass** +- `node tests/phase6/service-behaviour.test.cjs` -> **pass** (8/8) +- `node tests/phase7/service-behaviour.test.cjs` -> **pass** (12/12) +- `node tests/phase8/service-behaviour.test.cjs` -> **pass** (5/5) +- `node tests/phase9/service-behaviour.test.cjs` -> **pass** (5/5) +- `node tests/phase10/service-behaviour.test.cjs` -> **pass** (5/5) +- `node tests/phase11/service-behaviour.test.cjs` -> **pass** (4/4) +- `node tests/phase12/service-behaviour.test.cjs` -> **pass** (4/4) +- `node tests/phase13/service-behaviour.test.cjs` -> **pass** (7/7) +- `node tests/phase14/service-behaviour.test.cjs` -> **pass** (9/9) +- `node tests/phase15/service-behaviour.test.cjs` -> **pass** (5/5) +- `node tests/phase16/service-behaviour.test.cjs` -> **pass** (4/4) +- `node tests/phase17/service-behaviour.test.cjs` -> **pass** (6/6) +- `node tests/phase18/service-behaviour.test.cjs` -> **pass** (8/8) +- `node tests/phase19/service-behaviour.test.cjs` -> **pass** (5/5) +- `node tests/phase20/service-behaviour.test.cjs` -> **pass** (5/5) +- `npm run lint` -> **warnings only** (pre-existing `react-hooks/exhaustive-deps` warnings) + ### Manual HTTP snapshot (Phase 15) - Dev server run on `http://localhost:3002`. diff --git a/memory-bank/refactor-plan-actions-index.md b/memory-bank/refactor-plan-actions-index.md index 30839465..babd2760 100644 --- a/memory-bank/refactor-plan-actions-index.md +++ b/memory-bank/refactor-plan-actions-index.md @@ -683,6 +683,34 @@ Status key: `[x] done`, `[ ] pending` - `tests/phase19/service-behaviour.test.cjs` - Re-run phase6–phase19 baseline tests and lint after rollback. +### Phase 20 endpoint consistency slice snapshot (2026-03-14) + +Status key: `[x] done`, `[ ] pending` + +1. `[x]` Apply small consistency-only endpoint hardening in selected handlers + - `pages/api/endpoint/getbasicdnssearch_api.js` + - `pages/api/endpoint/getbasicdnsurlsearch_api.js` + - `pages/api/endpoint/getbasicsearchdetailspaged_api.js` +2. `[x]` Standardize negative-path required input handling + - early 400 for missing/empty `searchString` in `getbasicdnsurlsearch_api` + - early 400 for missing required params in `getbasicsearchdetailspaged_api` +3. `[x]` Remove noisy sensitive-path logging + - removed direct query logging in `getbasicdnssearch_api` +4. `[x]` Add focused tests + - `tests/phase20/service-behaviour.test.cjs` (5/5) +5. `[x]` Execute validation bundle + - phase6 parity + phase6–phase20 behavioural suites pass + - lint warnings only (pre-existing) + +### Phase 20 rollback notes + +- Revert files in this slice: + - `pages/api/endpoint/getbasicdnssearch_api.js` + - `pages/api/endpoint/getbasicdnsurlsearch_api.js` + - `pages/api/endpoint/getbasicsearchdetailspaged_api.js` + - `tests/phase20/service-behaviour.test.cjs` +- Re-run phase6–phase20 baseline tests and lint after rollback. + ## Safe execution mode for migration chunks (required) To reduce terminal hangs during bulk migration work, run refactor chunks in **safe stepwise mode** instead of long chained commands. diff --git a/pages/api/endpoint/getbasicdnssearch_api.js b/pages/api/endpoint/getbasicdnssearch_api.js index f947ebf7..2269e0b3 100644 --- a/pages/api/endpoint/getbasicdnssearch_api.js +++ b/pages/api/endpoint/getbasicdnssearch_api.js @@ -47,8 +47,6 @@ export default async function ApiProxy(req, res) { 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&$expand=primarycontactid($select=fullname)&$filter=(pinswg_appealcasetype eq 846040011 or pinswg_appealcasetype eq 846040002)and pinswg_publishtoweb eq true&$orderby=createdon desc&$count=true"; - console.log(queryUrl); - return axios .get( WEBAPI_URL + queryUrl + hashAPIPath(queryUrl), diff --git a/pages/api/endpoint/getbasicdnsurlsearch_api.js b/pages/api/endpoint/getbasicdnsurlsearch_api.js index 1e0e2383..5f445e07 100644 --- a/pages/api/endpoint/getbasicdnsurlsearch_api.js +++ b/pages/api/endpoint/getbasicdnsurlsearch_api.js @@ -43,6 +43,11 @@ const hashAPIPath = (queryPath) => { export default async function ApiProxy(req, res) { var searchString = req.query.searchString; + + if (!searchString || String(searchString).trim().length === 0) { + return res.status(400).json(); + } + var token = await getToken(); searchString = searchString.replace(/\'/g, "''"); @@ -54,28 +59,26 @@ export default async function ApiProxy(req, res) { searchString + "')) and (pinswg_appealcasetype eq 846040011 or pinswg_appealcasetype eq 846040002 ) and pinswg_publishtoweb eq true&$orderby=createdon desc&$count=true"; - console.log("basic search ", queryUrl); - var apiResponse = _.isEmpty(req.query) ? res.status(400).json() : searchString.length > 0 - ? axios - .get( - WEBAPI_URL + queryUrl + hashAPIPath(queryUrl), - azureHeadersPaged(token.access_token) - ) - .then(({ data }) => { - var dataStr; - _.has(data, "@odata.nextLink") == true && - ((dataStr = JSON.stringify(data["@odata.nextLink"])), - (data["@odata.nextLink"] = dataStr.split("/v8.2/")[1])); - res.status(200).json(data); - }) - .catch((error) => { - consoleLogger(error); - res.status(400).json(error); - }) - : res.status(400).json(); + ? axios + .get( + WEBAPI_URL + queryUrl + hashAPIPath(queryUrl), + azureHeadersPaged(token.access_token) + ) + .then(({ data }) => { + var dataStr; + _.has(data, "@odata.nextLink") == true && + ((dataStr = JSON.stringify(data["@odata.nextLink"])), + (data["@odata.nextLink"] = dataStr.split("/v8.2/")[1])); + res.status(200).json(data); + }) + .catch((error) => { + consoleLogger(error); + res.status(400).json(error); + }) + : res.status(400).json(); return apiResponse; } diff --git a/pages/api/endpoint/getbasicsearchdetailspaged_api.js b/pages/api/endpoint/getbasicsearchdetailspaged_api.js index 22023e32..d17fee80 100644 --- a/pages/api/endpoint/getbasicsearchdetailspaged_api.js +++ b/pages/api/endpoint/getbasicsearchdetailspaged_api.js @@ -59,6 +59,18 @@ export default async function ApiProxy(req, res) { var appealTypeName = req.query.appealTypeName; var primaryIdAttribute = req.query.primaryIdAttribute; var incidentID = req.query.incidentID; + + if ( + !appealTypeName || + !primaryIdAttribute || + !incidentID || + String(appealTypeName).trim().length === 0 || + String(primaryIdAttribute).trim().length === 0 || + String(incidentID).trim().length === 0 + ) { + return res.status(400).json(); + } + var token = await getToken(); let navigationProperty = @@ -92,8 +104,6 @@ export default async function ApiProxy(req, res) { ? "pinswg_sipscase_value" : primaryIdAttribute + "s_value"); - console.log("query: ", queryUrl, "<<< { let flattened = data.value.map((r) => ({ ...r, - ticketnumber: r[navigationProperty]?.ticketnumber || null, + ticketnumber: r[navigationProperty]?.ticketnumber || null })); data.value = flattened; diff --git a/tests/phase20/service-behaviour.test.cjs b/tests/phase20/service-behaviour.test.cjs new file mode 100644 index 00000000..5352e909 --- /dev/null +++ b/tests/phase20/service-behaviour.test.cjs @@ -0,0 +1,219 @@ +const fs = require("fs"); +const path = require("path"); +const vm = require("vm"); +const assert = require("assert"); + +const rootDir = path.resolve(__dirname, "..", ".."); + +const loadModule = (relativePath, injected = {}) => { + const filePath = path.join(rootDir, relativePath); + let source = fs.readFileSync(filePath, "utf8"); + + source = source.replace(/import[\s\S]*?from\s+"[^"]+";\n?/g, ""); + source = source.replace( + /export default async function\s+(\w+)\s*\(/g, + "async function $1(" + ); + source = source.replace(/export const\s+/g, "const "); + source = source.replace( + /export default\s+(\w+);/g, + "module.exports.default = $1;" + ); + + source += + '\nif (typeof ApiProxy !== "undefined" && !module.exports.default) module.exports.default = ApiProxy;\n'; + + const context = { + module: { exports: {} }, + exports: {}, + require, + process, + CryptoJS: { + HmacSHA256: () => ({ toString: () => "hashed" }), + enc: { Hex: { parse: () => "" } } + }, + console: { + log: () => {}, + info: () => {}, + warn: () => {}, + error: () => {} + }, + ...injected + }; + + vm.runInNewContext(source, context, { filename: filePath }); + return context.module.exports; +}; + +const createRes = () => { + const state = { + statusCode: null, + jsonBody: undefined + }; + + return { + state, + status(code) { + state.statusCode = code; + return this; + }, + json(payload) { + state.jsonBody = payload; + return payload; + } + }; +}; + +const tests = []; +const test = (name, fn) => tests.push({ name, fn }); + +test("getbasicdnssearch_api still returns 200 and preserves nextLink transform", async () => { + const mod = loadModule("pages/api/endpoint/getbasicdnssearch_api.js", { + axios: { + get: async () => ({ + data: { + value: [{ id: 1 }], + "@odata.nextLink": + "https://example.test/v8.2/incidents?$skiptoken=dns" + } + }) + }, + getToken: async () => ({ access_token: "token" }), + azureHeadersPaged: () => ({}), + _: { has: (obj, key) => Object.prototype.hasOwnProperty.call(obj, key) } + }); + + const req = { query: {} }; + const res = createRes(); + await mod.default(req, res); + + assert.strictEqual(res.state.statusCode, 200); + assert.strictEqual( + res.state.jsonBody["@odata.nextLink"], + 'incidents?$skiptoken=dns"' + ); +}); + +test("getbasicdnsurlsearch_api rejects missing searchString with 400", async () => { + const mod = loadModule("pages/api/endpoint/getbasicdnsurlsearch_api.js", { + axios: { get: async () => ({ data: { value: [] } }) }, + getToken: async () => ({ access_token: "token" }), + azureHeadersPaged: () => ({}), + _: { has: () => false, isEmpty: () => true } + }); + + const req = { query: {} }; + const res = createRes(); + await mod.default(req, res); + + assert.strictEqual(res.state.statusCode, 400); +}); + +test("getbasicdnsurlsearch_api valid searchString still returns 200", async () => { + const mod = loadModule("pages/api/endpoint/getbasicdnsurlsearch_api.js", { + axios: { + get: async () => ({ + data: { + value: [{ id: 1 }], + "@odata.nextLink": + "https://example.test/v8.2/incidents?$skiptoken=url" + } + }) + }, + getToken: async () => ({ access_token: "token" }), + azureHeadersPaged: () => ({}), + _: { + has: (obj, key) => Object.prototype.hasOwnProperty.call(obj, key), + isEmpty: () => false + } + }); + + const req = { query: { searchString: "farm" } }; + const res = createRes(); + await mod.default(req, res); + + assert.strictEqual(res.state.statusCode, 200); + assert.strictEqual( + res.state.jsonBody["@odata.nextLink"], + 'incidents?$skiptoken=url"' + ); +}); + +test("getbasicsearchdetailspaged_api rejects missing required params with 400", async () => { + const mod = loadModule( + "pages/api/endpoint/getbasicsearchdetailspaged_api.js", + { + axios: { get: async () => ({ data: { value: [] } }) }, + getToken: async () => ({ access_token: "token" }), + azureHeadersPaged: () => ({}), + getSelectQuery: () => "", + getNavigationPropertyByPrimaryAttribute: () => ({ + NavigationProperty: "incident" + }), + _: { has: () => false } + } + ); + + const req = { + query: { + appealTypeName: "", + primaryIdAttribute: "pinswg_case", + incidentID: "" + } + }; + const res = createRes(); + await mod.default(req, res); + + assert.strictEqual(res.state.statusCode, 400); +}); + +test("getbasicsearchdetailspaged_api valid params still return 200", async () => { + const mod = loadModule( + "pages/api/endpoint/getbasicsearchdetailspaged_api.js", + { + axios: { + get: async () => ({ + data: { value: [{ incident: { ticketnumber: "CAS-1" } }] } + }) + }, + getToken: async () => ({ access_token: "token" }), + azureHeadersPaged: () => ({}), + getSelectQuery: () => "", + getNavigationPropertyByPrimaryAttribute: () => ({ + NavigationProperty: "incident" + }), + _: { has: () => false } + } + ); + + const req = { + query: { + appealTypeName: "pinswg_cases", + primaryIdAttribute: "pinswg_case", + incidentID: "incidentid eq guid'1'" + } + }; + const res = createRes(); + await mod.default(req, res); + + assert.strictEqual(res.state.statusCode, 200); + assert.strictEqual(res.state.jsonBody.value[0].ticketnumber, "CAS-1"); +}); + +const run = async () => { + let passed = 0; + + for (const currentTest of tests) { + await currentTest.fn(); + passed += 1; + } + + console.log( + `Phase 20 behavioural tests passed (${passed}/${tests.length}).` + ); +}; + +run().catch((error) => { + console.error(error); + process.exit(1); +});