From 598e4a6077bffcb2d3def6a24351309d17b0f371 Mon Sep 17 00:00:00 2001 From: robbond Date: Sat, 14 Mar 2026 07:06:57 +0000 Subject: [PATCH] TASK22057: phase19 endpoint consistency hardening slice --- memory-bank/activeContext.md | 15 ++ memory-bank/change-log.md | 44 ++++ memory-bank/progress.md | 34 ++++ memory-bank/refactor-plan-actions-index.md | 27 +++ pages/api/endpoint/createwatchedcases_api.js | 26 ++- pages/api/endpoint/getappealtypes_api.js | 2 - pages/api/endpoint/getdnslist_api.js | 2 - tests/phase19/service-behaviour.test.cjs | 203 +++++++++++++++++++ 8 files changed, 345 insertions(+), 8 deletions(-) create mode 100644 tests/phase19/service-behaviour.test.cjs diff --git a/memory-bank/activeContext.md b/memory-bank/activeContext.md index d3127d98..b76346dd 100644 --- a/memory-bank/activeContext.md +++ b/memory-bank/activeContext.md @@ -595,6 +595,21 @@ - Added focused test pack: - `tests/phase18/service-behaviour.test.cjs` (8/8 pass) +## Phase 19 endpoint consistency slice (2026-03-14) + +- Continued on branch: + - `TASK22057-phase17-hardening-closeout` +- Applied a small consistency-only slice across: + - `pages/api/endpoint/createwatchedcases_api.js` + - `pages/api/endpoint/getappealtypes_api.js` + - `pages/api/endpoint/getdnslist_api.js` +- Scope: + - standardized missing/malformed required `@odata.bind` handling in `createwatchedcases_api` with early 400 + - removed noisy query logging in appeal-types and dns-list endpoint handlers + - preserved response shapes/signatures +- Added focused test file: + - `tests/phase19/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 30d0ec7c..83ff0d49 100644 --- a/memory-bank/change-log.md +++ b/memory-bank/change-log.md @@ -1214,3 +1214,47 @@ Validation: Follow-ups: - Optional: execute integrated manual HTTP matrix for phase-18 touched handlers in an environment with full relay/storage dependencies for non-mocked happy-path verification. + +--- + +### CL-033: Phase 19 endpoint consistency slice — createwatchedcases guard hardening + noisy log cleanup + +date: 2026-03-14 +author: Cline +scope: `pages/api/endpoint/{createwatchedcases_api,getappealtypes_api,getdnslist_api}.js`, `tests/phase19/service-behaviour.test.cjs`, `memory-bank/*` +type: change +rationale: Deliver a small reversible consistency-only slice for selected sensitive endpoint handlers by standardizing invalid required input handling and removing noisy logging without changing contracts. +impact: Improves negative-path consistency and logging hygiene for selected handlers; response signatures/shapes preserved. +status: completed + +Summary: + +- `createwatchedcases_api` now consistently rejects missing/malformed required `@odata.bind` values with early 400. +- Removed noisy query URL logging in: + - `getappealtypes_api` + - `getdnslist_api` +- Added focused behavioural test pack: + - `tests/phase19/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) +- `npm run lint` -> warnings only (pre-existing hook dependency warnings) + +Follow-ups: + +- Continue same small-batch consistency pattern for remaining sensitive endpoint handlers only where clear guard/logging drift exists. diff --git a/memory-bank/progress.md b/memory-bank/progress.md index 6075ffef..50785909 100644 --- a/memory-bank/progress.md +++ b/memory-bank/progress.md @@ -623,6 +623,40 @@ - `node tests/phase15/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 19 endpoint consistency slice) + +- Continued on branch: + - `TASK22057-phase17-hardening-closeout` +- Delivered a small additional consistency-only endpoint slice across: + - `pages/api/endpoint/createwatchedcases_api.js` + - `pages/api/endpoint/getappealtypes_api.js` + - `pages/api/endpoint/getdnslist_api.js` +- Scope delivered: + - added required early 400 handling for malformed/missing `@odata.bind` values in `createwatchedcases_api` + - removed noisy query logging in `getappealtypes_api` and `getdnslist_api` + - preserved response-shape/signature contracts +- Added focused tests: + - `tests/phase19/service-behaviour.test.cjs` (5/5 passing) + +### Validation snapshot (Phase 19) + +- `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) +- `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 0f6f1e06..30839465 100644 --- a/memory-bank/refactor-plan-actions-index.md +++ b/memory-bank/refactor-plan-actions-index.md @@ -656,6 +656,33 @@ Status key: `[x] done`, `[ ] pending` - `tests/phase18/service-behaviour.test.cjs` - Re-run phase6–phase18 baseline tests and lint after rollback to confirm parity. +### Phase 19 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/createwatchedcases_api.js` + - `pages/api/endpoint/getappealtypes_api.js` + - `pages/api/endpoint/getdnslist_api.js` +2. `[x]` Standardize negative-path required input handling + - early 400 on missing/malformed required `@odata.bind` values in `createwatchedcases_api` +3. `[x]` Remove noisy sensitive-path logging + - removed direct query logging in appeal-types and dns-list handlers +4. `[x]` Add focused tests + - `tests/phase19/service-behaviour.test.cjs` (5/5) +5. `[x]` Execute validation bundle + - phase6 parity + phase6–phase19 behavioural suites pass + - lint warnings only (pre-existing) + +### Phase 19 rollback notes + +- Revert files in this slice: + - `pages/api/endpoint/createwatchedcases_api.js` + - `pages/api/endpoint/getappealtypes_api.js` + - `pages/api/endpoint/getdnslist_api.js` + - `tests/phase19/service-behaviour.test.cjs` +- Re-run phase6–phase19 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/createwatchedcases_api.js b/pages/api/endpoint/createwatchedcases_api.js index 56f05b14..17941ff0 100644 --- a/pages/api/endpoint/createwatchedcases_api.js +++ b/pages/api/endpoint/createwatchedcases_api.js @@ -86,12 +86,30 @@ const recordExists = async (incidentId, contactId, token) => { }; export default async function ApiProxy(req, res) { - const token = await getToken(); const data = req.body; + const watchedCaseBind = data?.["pinswg_WatchedCase@odata.bind"]; + const contactBind = data?.["pinswg_Contact@odata.bind"]; - const incidentId = - data["pinswg_WatchedCase@odata.bind"].match(/\(([^)]+)\)/)[1]; - const contactId = data["pinswg_Contact@odata.bind"].match(/\(([^)]+)\)/)[1]; + if ( + typeof watchedCaseBind !== "string" || + watchedCaseBind.length === 0 || + typeof contactBind !== "string" || + contactBind.length === 0 + ) { + return res.status(400).json(); + } + + const watchedCaseMatch = watchedCaseBind.match(/\(([^)]+)\)/); + const contactMatch = contactBind.match(/\(([^)]+)\)/); + + if (!watchedCaseMatch || !contactMatch) { + return res.status(400).json(); + } + + const token = await getToken(); + + const incidentId = watchedCaseMatch[1]; + const contactId = contactMatch[1]; const existingRecord = await recordExists(incidentId, contactId, token); diff --git a/pages/api/endpoint/getappealtypes_api.js b/pages/api/endpoint/getappealtypes_api.js index 24a81c7d..53e63d30 100644 --- a/pages/api/endpoint/getappealtypes_api.js +++ b/pages/api/endpoint/getappealtypes_api.js @@ -49,8 +49,6 @@ export default async function ApiProxy(req, res) { // "stringmaps?$filter=objecttypecode eq 'incident' and attributename eq 'pinswg_appealcasetype' and value ne 'Misc Casework'&$count=true&$select=value,stringmapid,organizationid,attributevalue&$orderby=value asc"; //"stringmaps?$filter=objecttypecode eq 'incident' and attributename eq 'pinswg_appealcasetype' and value ne 'Developments of National Significance' and value ne 'LDP' and value ne 'Misc Casework'&$count=true&$select=value,stringmapid,organizationid,attributevalue&$orderby=value asc"; - console.log(queryUrl); - return axios .get( WEBAPI_URL + queryUrl + hashAPIPath(queryUrl), diff --git a/pages/api/endpoint/getdnslist_api.js b/pages/api/endpoint/getdnslist_api.js index b9735928..d543da96 100644 --- a/pages/api/endpoint/getdnslist_api.js +++ b/pages/api/endpoint/getdnslist_api.js @@ -29,8 +29,6 @@ export default async function ApiProxy(req, res) { var queryUrl = "incidents?$select=pinswg_environmentalstatementlocation,pinswg_appealcasetype,_customerid_value,_pinswg_associatedlpa_value,_ownerid_value,statuscode,ticketnumber,title &$filter=(pinswg_appealcasetype eq 846040011 or pinswg_appealcasetype eq 846040002) and pinswg_publishtoweb eq true&$orderby=createdon desc&$count=true"; - console.log(queryUrl); - function renameJsonKey(jsonObj, oldKey, newKey) { if (jsonObj.hasOwnProperty(oldKey)) { jsonObj[newKey] = jsonObj[oldKey]; // Add new key with the same value diff --git a/tests/phase19/service-behaviour.test.cjs b/tests/phase19/service-behaviour.test.cjs new file mode 100644 index 00000000..184a2ca9 --- /dev/null +++ b/tests/phase19/service-behaviour.test.cjs @@ -0,0 +1,203 @@ +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("createwatchedcases_api rejects missing required @odata binds with 400", async () => { + let axiosCalls = 0; + const axiosMock = async () => { + axiosCalls += 1; + return { data: {} }; + }; + axiosMock.get = async () => ({ data: { value: [] } }); + + const mod = loadModule("pages/api/endpoint/createwatchedcases_api.js", { + axios: axiosMock, + getToken: async () => ({ access_token: "token" }) + }); + + const req = { body: {} }; + const res = createRes(); + await mod.default(req, res); + + assert.strictEqual(res.state.statusCode, 400); + assert.strictEqual(axiosCalls, 0); +}); + +test("createwatchedcases_api rejects malformed @odata bind values with 400", async () => { + let axiosCalls = 0; + const axiosMock = async () => { + axiosCalls += 1; + return { data: {} }; + }; + axiosMock.get = async () => ({ data: { value: [] } }); + + const mod = loadModule("pages/api/endpoint/createwatchedcases_api.js", { + axios: axiosMock, + getToken: async () => ({ access_token: "token" }) + }); + + const req = { + body: { + "pinswg_WatchedCase@odata.bind": "/incidents/no-brackets", + "pinswg_Contact@odata.bind": "/contacts/no-brackets" + } + }; + const res = createRes(); + await mod.default(req, res); + + assert.strictEqual(res.state.statusCode, 400); + assert.strictEqual(axiosCalls, 0); +}); + +test("createwatchedcases_api valid body still returns 200", async () => { + const axiosConfigCalls = []; + const axiosMock = async (config) => { + axiosConfigCalls.push(config); + return { data: { ok: true } }; + }; + axiosMock.get = async () => ({ data: { value: [] } }); + + const mod = loadModule("pages/api/endpoint/createwatchedcases_api.js", { + axios: axiosMock, + getToken: async () => ({ access_token: "token" }) + }); + + const req = { + body: { + "pinswg_WatchedCase@odata.bind": "/incidents(inc-1)", + "pinswg_Contact@odata.bind": "/contacts(con-1)" + } + }; + const res = createRes(); + await mod.default(req, res); + + assert.strictEqual(res.state.statusCode, 200); + assert.strictEqual(axiosConfigCalls.length, 1); +}); + +test("getappealtypes_api still returns 200 on successful relay call", async () => { + const mod = loadModule("pages/api/endpoint/getappealtypes_api.js", { + axios: { get: async () => ({ data: { value: [{ id: 1 }] } }) }, + getToken: async () => ({ access_token: "token" }), + azureHeaders: () => ({}) + }); + + const req = { query: {} }; + 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: 1 }] + }); +}); + +test("getdnslist_api still returns 200 and preserves nextLink transform", async () => { + const mod = loadModule("pages/api/endpoint/getdnslist_api.js", { + axios: { + get: async () => ({ + data: { + value: [{ id: 1 }], + "@odata.nextLink": + "https://example.test/v8.2/incidents?$skiptoken=abc" + } + }) + }, + getToken: async () => ({ access_token: "token" }), + azureHeaders: () => ({}), + _: { 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=abc"' + ); +}); + +const run = async () => { + let passed = 0; + + for (const currentTest of tests) { + await currentTest.fn(); + passed += 1; + } + + console.log( + `Phase 19 behavioural tests passed (${passed}/${tests.length}).` + ); +}; + +run().catch((error) => { + console.error(error); + process.exit(1); +});