From ab50430493233718a69ed7e5fb67a481f7451765 Mon Sep 17 00:00:00 2001 From: robbond Date: Mon, 23 Mar 2026 17:45:47 +0000 Subject: [PATCH] TASK22224: harden getbloblist and getprogressobjblob parity --- pages/api/file/getbloblist.js | 56 +++++--- pages/api/file/getprogressobjblob.js | 53 +++++--- tests/phase21/file-handler-contract.test.cjs | 132 +++++++++++++++++++ 3 files changed, 206 insertions(+), 35 deletions(-) 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/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/tests/phase21/file-handler-contract.test.cjs b/tests/phase21/file-handler-contract.test.cjs index bae7431b..adf03867 100644 --- a/tests/phase21/file-handler-contract.test.cjs +++ b/tests/phase21/file-handler-contract.test.cjs @@ -377,6 +377,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(),