From 6e02ffe26d4d223473ed515eb2e65848239e0e02 Mon Sep 17 00:00:00 2001 From: robbond Date: Mon, 23 Mar 2026 17:56:14 +0000 Subject: [PATCH] TASK22224: harden deleteawaitingsubmission route parity --- .../file/deleteawaitingsubmissionfromblob.js | 73 ++++++++++++++----- tests/phase21/file-handler-contract.test.cjs | 68 +++++++++++++++++ 2 files changed, 124 insertions(+), 17 deletions(-) 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/tests/phase21/file-handler-contract.test.cjs b/tests/phase21/file-handler-contract.test.cjs index adf03867..6b55f036 100644 --- a/tests/phase21/file-handler-contract.test.cjs +++ b/tests/phase21/file-handler-contract.test.cjs @@ -253,6 +253,74 @@ test("deleteblobrep handler dependency failure returns DELETE_BLOB_REP_FAILED", 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(),