From 846cba164589bb9ba2c8c51a057c50402f227246 Mon Sep 17 00:00:00 2001 From: robbond Date: Fri, 13 Mar 2026 13:02:49 +0000 Subject: [PATCH] TASK22019: phase 13 harden delete and involvement guards --- pages/api/file/createcaseinvolvement_api.js | 12 +- pages/api/file/createrepinvolvement_api.js | 12 +- pages/api/file/deleteblobcase.js | 10 +- pages/api/file/deleteblobrep.js | 10 +- tests/phase13/service-behaviour.test.cjs | 197 ++++++++++++++++++++ 5 files changed, 231 insertions(+), 10 deletions(-) create mode 100644 tests/phase13/service-behaviour.test.cjs diff --git a/pages/api/file/createcaseinvolvement_api.js b/pages/api/file/createcaseinvolvement_api.js index 2fcc597b..f3553eb0 100644 --- a/pages/api/file/createcaseinvolvement_api.js +++ b/pages/api/file/createcaseinvolvement_api.js @@ -34,6 +34,16 @@ const hashAPIPath = (queryPath) => { }; export default async function ApiProxy(req, res) { + if ( + !req.body || + typeof req.body.contactid === "undefined" || + req.body.contactid.length === 0 || + typeof req.body.incidentid === "undefined" || + req.body.incidentid.length === 0 + ) { + return res.status(400).json(); + } + var token = await getToken(); var contactid = req.body.contactid; var queryUrl = @@ -41,8 +51,6 @@ export default async function ApiProxy(req, res) { req.body.incidentid + ")/pinswg_incident_contact_case_involvement/$ref"; - console.log(req.body, queryUrl); - var crmUrl = "https://" + process.env.CRMURL; var data = { diff --git a/pages/api/file/createrepinvolvement_api.js b/pages/api/file/createrepinvolvement_api.js index 3fe378d9..90732604 100644 --- a/pages/api/file/createrepinvolvement_api.js +++ b/pages/api/file/createrepinvolvement_api.js @@ -34,6 +34,16 @@ const hashAPIPath = (queryPath) => { }; export default async function ApiProxy(req, res) { + if ( + !req.body || + typeof req.body.contactid === "undefined" || + req.body.contactid.length === 0 || + typeof req.body.incidentid === "undefined" || + req.body.incidentid.length === 0 + ) { + return res.status(400).json(); + } + var token = await getToken(); var contactid = req.body.contactid; var queryUrl = @@ -41,8 +51,6 @@ export default async function ApiProxy(req, res) { req.body.incidentid + ")/pinswg_incident_contact_case_involvement/$ref"; - console.log(req.body, queryUrl); - var crmUrl = "https://" + process.env.CRMURL; var crmVersion = process.env.CRMURL_VERSION; diff --git a/pages/api/file/deleteblobcase.js b/pages/api/file/deleteblobcase.js index bedd0fb0..39e2cfde 100644 --- a/pages/api/file/deleteblobcase.js +++ b/pages/api/file/deleteblobcase.js @@ -16,7 +16,9 @@ ApiProxy.get(async (req, res) => { typeof containerName === "undefined" || containerName.length === 0 || typeof casefolderID === "undefined" || - casefolderID.length === 0 + casefolderID.length === 0 || + typeof checkHash === "undefined" || + checkHash.length === 0 ) { return res.status(400).json(); } @@ -27,12 +29,14 @@ ApiProxy.get(async (req, res) => { "&casefolderID=" + casefolderID; + if (hashAPIPath(checkquerypath) != "&hash=" + checkHash) { + return res.status(400).json(); + } + if (hashAPIPath(checkquerypath) == "&hash=" + checkHash) { await deleteBlobCase(containerName, casefolderID).then((data) => { return res.status(200).json({ data: data }); }); - } else { - return res.status(400).json(); } }); diff --git a/pages/api/file/deleteblobrep.js b/pages/api/file/deleteblobrep.js index 271cc21d..5d55a259 100644 --- a/pages/api/file/deleteblobrep.js +++ b/pages/api/file/deleteblobrep.js @@ -19,7 +19,9 @@ ApiProxy.get(async (req, res) => { typeof casefolderID === "undefined" || casefolderID.length === 0 || typeof repfile === "undefined" || - repfile.length === 0 + repfile.length === 0 || + typeof checkHash === "undefined" || + checkHash.length === 0 ) { return res.status(400).json(); } @@ -32,14 +34,16 @@ ApiProxy.get(async (req, res) => { "&repfile=" + repfile; + if (hashAPIPath(checkquerypath) != "&hash=" + checkHash) { + return res.status(400).json(); + } + casefolderID = casefolderID + "/" + repfile; if (hashAPIPath(checkquerypath) == "&hash=" + checkHash) { await deleteBlobRep(containerName, casefolderID).then((data) => { return res.status(200).json({ data: data }); }); - } else { - return res.status(400).json(); } }); diff --git a/tests/phase13/service-behaviour.test.cjs b/tests/phase13/service-behaviour.test.cjs new file mode 100644 index 00000000..4c0164da --- /dev/null +++ b/tests/phase13/service-behaviour.test.cjs @@ -0,0 +1,197 @@ +const fs = require("fs"); +const path = require("path"); +const vm = require("vm"); +const assert = require("assert"); + +const rootDir = path.resolve(__dirname, "..", ".."); + +const loadModule = (relativePath, injected = {}) => { + const filePath = path.join(rootDir, relativePath); + let source = fs.readFileSync(filePath, "utf8"); + + source = source.replace(/import[\s\S]*?from\s+"[^"]+";\n?/g, ""); + source = source.replace( + /export default async function\s+(\w+)\s*\(/, + "async function $1(" + ); + source = source.replace(/export const\s+/g, "const "); + source = source.replace( + /export default\s+(\w+);/g, + "module.exports.default = $1;" + ); + source += + '\nif (typeof ApiProxy !== "undefined" && !module.exports.default) module.exports.default = ApiProxy;\n'; + + const context = { + module: { exports: {} }, + exports: {}, + require, + process, + console: { log: () => {}, error: () => {} }, + ...injected + }; + + vm.runInNewContext(source, context, { filename: filePath }); + return context.module.exports; +}; + +const createNextConnectMock = () => { + const router = { + handler: null, + use: () => {}, + get(fn) { + this.handler = fn; + } + }; + return () => router; +}; + +const createRes = () => { + const state = { statusCode: null, jsonBody: undefined }; + return { + state, + status(code) { + state.statusCode = code; + return this; + }, + json(payload) { + state.jsonBody = payload; + return payload; + } + }; +}; + +const tests = []; +const test = (name, fn) => tests.push({ name, fn }); + +test("deleteblobcase rejects missing hash with 400", async () => { + const calls = []; + const mod = loadModule("pages/api/file/deleteblobcase.js", { + hashAPIPath: () => "&hash=expected", + deleteBlobCase: async (...args) => { + calls.push(args); + return true; + }, + nextConnect: createNextConnectMock(), + middleware: () => {} + }); + + const req = { query: { container: "c1", casefolderID: "case-1" } }; + const res = createRes(); + await mod.default.handler(req, res); + assert.strictEqual(res.state.statusCode, 400); + assert.strictEqual(calls.length, 0); +}); + +test("deleteblobrep rejects missing hash with 400", async () => { + const calls = []; + const mod = loadModule("pages/api/file/deleteblobrep.js", { + hashAPIPath: () => "&hash=expected", + deleteBlobRep: async (...args) => { + calls.push(args); + return true; + }, + nextConnect: createNextConnectMock(), + middleware: () => {} + }); + + const req = { + query: { container: "c1", casefolderID: "case-1", repfile: "r.pdf" } + }; + const res = createRes(); + await mod.default.handler(req, res); + assert.strictEqual(res.state.statusCode, 400); + assert.strictEqual(calls.length, 0); +}); + +test("createcaseinvolvement_api rejects missing required body values with 400", async () => { + const mod = loadModule("pages/api/file/createcaseinvolvement_api.js", { + getToken: async () => ({ access_token: "token" }), + axios: async () => ({ data: { ok: true } }), + consoleLogger: () => {} + }); + + const req = { body: {} }; + const res = createRes(); + await mod.default(req, res); + assert.strictEqual(res.state.statusCode, 400); +}); + +test("createrepinvolvement_api rejects missing required body values with 400", async () => { + const mod = loadModule("pages/api/file/createrepinvolvement_api.js", { + getToken: async () => ({ access_token: "token" }), + axios: async () => ({ data: { ok: true } }), + consoleLogger: () => {} + }); + + const req = { body: {} }; + const res = createRes(); + await mod.default(req, res); + assert.strictEqual(res.state.statusCode, 400); +}); + +test("deleteblobcase valid hash returns 200", async () => { + const mod = loadModule("pages/api/file/deleteblobcase.js", { + hashAPIPath: () => "&hash=expected", + deleteBlobCase: async () => ({ ok: true }), + nextConnect: createNextConnectMock(), + middleware: () => {} + }); + + const req = { + query: { container: "c1", casefolderID: "case-1", hash: "expected" } + }; + const res = createRes(); + await mod.default.handler(req, res); + assert.strictEqual(res.state.statusCode, 200); +}); + +test("createcaseinvolvement_api with valid body returns 200", async () => { + const mod = loadModule("pages/api/file/createcaseinvolvement_api.js", { + getToken: async () => ({ access_token: "token" }), + CryptoJS: { + HmacSHA256: () => ({ toString: () => "signed" }), + enc: { Hex: { parse: () => "" } } + }, + axios: async () => ({ data: { ok: true } }), + consoleLogger: () => {} + }); + + const req = { body: { contactid: "c1", incidentid: "i1" } }; + const res = createRes(); + await mod.default(req, res); + assert.strictEqual(res.state.statusCode, 200); +}); + +test("createrepinvolvement_api with valid body returns 200", async () => { + const mod = loadModule("pages/api/file/createrepinvolvement_api.js", { + getToken: async () => ({ access_token: "token" }), + CryptoJS: { + HmacSHA256: () => ({ toString: () => "signed" }), + enc: { Hex: { parse: () => "" } } + }, + axios: async () => ({ data: { ok: true } }), + consoleLogger: () => {} + }); + + const req = { body: { contactid: "c1", incidentid: "i1" } }; + const res = createRes(); + await mod.default(req, res); + assert.strictEqual(res.state.statusCode, 200); +}); + +const run = async () => { + let passed = 0; + for (const t of tests) { + await t.fn(); + passed += 1; + } + console.log( + `Phase 13 behavioural tests passed (${passed}/${tests.length}).` + ); +}; + +run().catch((error) => { + console.error(error); + process.exit(1); +});