const assert = require("assert"); const fs = require("fs"); const path = require("path"); const vm = require("vm"); const { loadModule, createRes, createNextConnectMock, respondSuccessMock, respondErrorMock } = require("./_shared.cjs"); const tests = []; const test = (name, fn) => tests.push({ name, fn }); const TRUSTED_EN_ORIGIN = "https://portal.example.test"; const TRUSTED_CY_ORIGIN = "https://porth.example.test"; const deletionTestProcess = { env: { NEXTAUTH_URL: TRUSTED_EN_ORIGIN, CY_API_ROOT: TRUSTED_CY_ORIGIN } }; const validCaseDeletionRequest = (overrides = {}) => ({ method: "POST", headers: { origin: TRUSTED_EN_ORIGIN }, query: { container: "c1", casefolderID: "CASE-SUB", hash: "expected" }, ...overrides }); const validRepDeletionRequest = (overrides = {}) => ({ method: "POST", headers: { origin: TRUSTED_EN_ORIGIN }, query: { container: "c1", casefolderID: "CASE-1", repfile: "representation.json", hash: "expected" }, ...overrides }); const loadDocumentDirectService = (injected = {}) => { const sourcePath = path.resolve( __dirname, "..", "..", "actions/services/documentDirectService.js" ); let source = fs.readFileSync(sourcePath, "utf8"); source = source.replace(/import[\s\S]*?from\s+"[^"]+";\n?/g, ""); source = source.replace(/export const\s+/g, "const "); source += ` module.exports = { deleteAwaitingSubmissionsFromBlob, deleteMyRepresentationsFromBlob };`; const context = { module: { exports: {} }, exports: {}, ...injected }; vm.runInNewContext(source, context, { filename: sourcePath }); return context.module.exports; }; test("upload handler returns HASH_REQUIRED for missing hash", async () => { const mod = loadModule("pages/api/file/upload.js", { nextConnect: createNextConnectMock(), middleware: () => {}, getServerSession: async () => ({ user: { id: "c1" } }), authOptions: () => ({}), hashAPIPath: () => "?hash=expected", respondError: respondErrorMock, respondSuccess: respondSuccessMock, createBlob: async () => ({ ok: true }), createRepBlob: async () => ({ ok: true }) }); const req = { query: {}, body: {} }; const res = createRes(); await mod.default.handler(req, res); assert.strictEqual(res.state.statusCode, 400); assert.strictEqual(res.state.jsonBody.error.code, "HASH_REQUIRED"); }); test("upload handler returns INVALID_HASH for wrong hash", async () => { const mod = loadModule("pages/api/file/upload.js", { nextConnect: createNextConnectMock(), middleware: () => {}, getServerSession: async () => ({ user: { id: "c1" } }), authOptions: () => ({}), hashAPIPath: () => "?hash=expected", respondError: respondErrorMock, respondSuccess: respondSuccessMock, createBlob: async () => ({ ok: true }), createRepBlob: async () => ({ ok: true }) }); const req = { query: { hash: "wrong" }, body: { containerID: ["c1"], casefolderID: ["f1"] } }; const res = createRes(); await mod.default.handler(req, res); assert.strictEqual(res.state.statusCode, 400); assert.strictEqual(res.state.jsonBody.error.code, "INVALID_HASH"); }); test("upload handler accepts alternate canonical hash candidate", async () => { const mod = loadModule("pages/api/file/upload.js", { nextConnect: createNextConnectMock(), middleware: () => {}, getServerSession: async () => ({ user: { id: "c1" } }), authOptions: () => ({}), hashAPIPath: (queryPath) => queryPath === "/api/file/upload?" ? "?hash=expected" : "?hash=other", respondError: respondErrorMock, respondSuccess: respondSuccessMock, createBlob: async () => ({ id: "blob-alt" }), createRepBlob: async () => ({ id: "rep-alt" }) }); const req = { query: { hash: "expected" }, body: { appealData: { key: "value" }, containerID: ["c1"], casefolderID: ["f1"], repOrAppeal: false } }; const res = createRes(); await mod.default.handler(req, res); assert.strictEqual(res.state.statusCode, 200); assert.deepStrictEqual(JSON.parse(JSON.stringify(res.state.jsonBody)), { data: { id: "blob-alt" } }); }); const loadUploadAuthorization = (overrides = {}) => { const calls = { session: [], options: [], createBlob: [], createRepBlob: [], hash: [] }; const options = { id: "pedw-upload-options" }; const mod = loadModule("pages/api/file/upload.js", { nextConnect: createNextConnectMock(), middleware: () => {}, authOptions: (req, res) => { calls.options.push([req, res]); return options; }, getServerSession: async (...args) => { calls.session.push(args); return { user: { id: "user-container" } }; }, hashAPIPath: (queryPath) => { calls.hash.push(queryPath); return "?hash=expected"; }, respondError: respondErrorMock, respondSuccess: respondSuccessMock, createBlob: async (...args) => { calls.createBlob.push(args); return { id: "appeal-blob" }; }, createRepBlob: async (...args) => { calls.createRepBlob.push(args); return { id: "representation-blob" }; }, ...overrides }); return { mod, calls, options }; }; const uploadAuthorizationRequest = (overrides = {}) => ({ query: { hash: "expected" }, body: { appealData: [JSON.stringify({ key: "value" })], containerID: ["user-container"], casefolderID: ["CASE-1"], repOrAppeal: ["false"] }, files: {}, ...overrides }); const assertNoUploadWrites = (calls) => { assert.strictEqual(calls.createBlob.length, 0); assert.strictEqual(calls.createRepBlob.length, 0); }; test("upload rejects a missing NextAuth session before HMAC or Azure write", async () => { const loaded = loadUploadAuthorization({ getServerSession: async () => null }); const res = createRes(); await loaded.mod.default.handler(uploadAuthorizationRequest(), res); assert.strictEqual(res.state.statusCode, 401); assert.strictEqual(res.state.jsonBody.error.code, "UNAUTHENTICATED"); assert.strictEqual(loaded.calls.hash.length, 0); assertNoUploadWrites(loaded.calls); }); test("upload rejects an unusable NextAuth session before HMAC or Azure write", async () => { const loaded = loadUploadAuthorization({ getServerSession: async () => ({ user: {} }) }); const res = createRes(); await loaded.mod.default.handler(uploadAuthorizationRequest(), res); assert.strictEqual(res.state.statusCode, 401); assert.strictEqual(res.state.jsonBody.error.code, "UNAUTHENTICATED"); assert.strictEqual(loaded.calls.hash.length, 0); assertNoUploadWrites(loaded.calls); }); test("upload passes the request, response and PEDW options to getServerSession", async () => { const loaded = loadUploadAuthorization(); const req = uploadAuthorizationRequest(); const res = createRes(); await loaded.mod.default.handler(req, res); assert.deepStrictEqual(loaded.calls.options, [[req, res]]); assert.strictEqual(loaded.calls.session.length, 1); assert.strictEqual(loaded.calls.session[0][0], req); assert.strictEqual(loaded.calls.session[0][1], res); assert.strictEqual(loaded.calls.session[0][2], loaded.options); }); test("upload forbids another user's container before HMAC or Azure write", async () => { const loaded = loadUploadAuthorization(); const req = uploadAuthorizationRequest(); req.body.containerID = ["other-container"]; const res = createRes(); await loaded.mod.default.handler(req, res); assert.strictEqual(res.state.statusCode, 403); assert.strictEqual(res.state.jsonBody.error.code, "FORBIDDEN"); assert.strictEqual(loaded.calls.hash.length, 0); assertNoUploadWrites(loaded.calls); }); test("upload rejects invalid HMAC for the owning user without Azure write", async () => { const loaded = loadUploadAuthorization({ hashAPIPath: () => "?hash=different" }); const res = createRes(); await loaded.mod.default.handler(uploadAuthorizationRequest(), res); assert.strictEqual(res.state.statusCode, 400); assert.strictEqual(res.state.jsonBody.error.code, "INVALID_HASH"); assertNoUploadWrites(loaded.calls); }); test("upload requires HMAC for the authenticated owning user", async () => { const loaded = loadUploadAuthorization(); const req = uploadAuthorizationRequest({ query: {} }); const res = createRes(); await loaded.mod.default.handler(req, res); assert.strictEqual(res.state.statusCode, 400); assert.strictEqual(res.state.jsonBody.error.code, "HASH_REQUIRED"); assertNoUploadWrites(loaded.calls); }); test("upload preserves the authorised owner success contract and helper arguments", async () => { const loaded = loadUploadAuthorization(); const res = createRes(); await loaded.mod.default.handler(uploadAuthorizationRequest(), res); assert.strictEqual(res.state.statusCode, 200); assert.deepStrictEqual(JSON.parse(JSON.stringify(res.state.jsonBody)), { data: { id: "appeal-blob" } }); assert.deepStrictEqual(loaded.calls.createBlob, [ [JSON.stringify({ key: "value" }), "user-container", "CASE-1"] ]); assert.strictEqual(loaded.calls.createRepBlob.length, 0); }); test("deleteblob handler returns MISSING_REQUIRED_QUERY when blobname missing", async () => { const mod = loadModule("pages/api/file/deleteblob.js", { nextConnect: createNextConnectMock(), middleware: () => {}, getServerSession: async () => ({ user: { id: "container-1" } }), authOptions: () => ({}), hashAPIPath: () => "&hash=expected", respondError: respondErrorMock, respondSuccess: respondSuccessMock, deleteBlob: async () => ({ ok: true }) }); const req = { query: { container: "container-1", 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, "MISSING_REQUIRED_QUERY"); }); test("deleteblob handler accepts encoded hash variant and normalizes prefixed path", async () => { let deletedPath = null; const mod = loadModule("pages/api/file/deleteblob.js", { nextConnect: createNextConnectMock(), middleware: () => {}, getServerSession: async () => ({ user: { id: "container-1" } }), authOptions: () => ({}), hashAPIPath: (queryPath) => queryPath.includes("/api/file/deleteblob?container=container-1") ? "&hash=expected" : "&hash=other", respondError: respondErrorMock, respondSuccess: respondSuccessMock, deleteBlob: async (_container, blobPath) => { deletedPath = blobPath; return { deleted: true }; } }); const req = { query: { container: "container-1", casefolderID: "TMP-LFE1C", blobname: "TMP-LFE1C/files/2026-03-18_-_Site_Location_Plan_-_test document 021.xlsx", hash: "expected" } }; const res = createRes(); await mod.default.handler(req, res); assert.strictEqual(res.state.statusCode, 200); assert.strictEqual( deletedPath, "TMP-LFE1C/files/2026-03-18_-_Site_Location_Plan_-_test document 021.xlsx" ); }); test("deleteblob handler dependency failure returns DELETE_BLOB_FAILED", async () => { const mod = loadModule("pages/api/file/deleteblob.js", { nextConnect: createNextConnectMock(), middleware: () => {}, getServerSession: async () => ({ user: { id: "container-1" } }), authOptions: () => ({}), hashAPIPath: () => "&hash=expected", respondError: respondErrorMock, respondSuccess: respondSuccessMock, deleteBlob: async () => { throw new Error("delete failed"); } }); const req = { query: { container: "container-1", casefolderID: "f1", 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_BLOB_FAILED"); }); test("deleteblobcase handler accepts a valid case folder", async () => { let deletedCaseFolder = null; const mod = loadModule("pages/api/file/deleteblobcase.js", { nextConnect: createNextConnectMock(), middleware: () => {}, getServerSession: async () => ({ user: { id: "c1" } }), authOptions: () => ({}), hashAPIPath: (queryPath) => queryPath.includes("casefolderID=CASE-SUB") ? "&hash=expected" : "&hash=other", respondError: respondErrorMock, respondSuccess: respondSuccessMock, process: deletionTestProcess, deleteBlobCase: async (_container, casefolder) => { deletedCaseFolder = casefolder; return { deleted: true }; } }); const req = validCaseDeletionRequest(); const res = createRes(); await mod.default.handler(req, res); assert.strictEqual(res.state.statusCode, 200); assert.strictEqual(deletedCaseFolder, "CASE-SUB"); }); test("deleteblobcase rejects unsafe normalized folder identifiers before deletion", async () => { for (const casefolderID of [" ", "CASE-1/REP-1", "..", "CASE\\REP"]) { let deletionCalls = 0; const mod = loadModule("pages/api/file/deleteblobcase.js", { nextConnect: createNextConnectMock(), middleware: () => {}, getServerSession: async () => ({ user: { id: "c1" } }), authOptions: () => ({}), hashAPIPath: () => "&hash=expected", respondError: respondErrorMock, respondSuccess: respondSuccessMock, process: deletionTestProcess, deleteBlobCase: async () => { deletionCalls += 1; } }); const res = createRes(); await mod.default.handler( { method: "POST", headers: { origin: TRUSTED_EN_ORIGIN }, query: { container: "c1", casefolderID, hash: "expected" } }, res ); assert.strictEqual(res.state.statusCode, 400); assert.strictEqual(res.state.jsonBody.error.code, "INVALID_REQUEST"); assert.strictEqual(deletionCalls, 0); } }); test("deleteblobcase handler dependency failure returns DELETE_BLOB_CASE_FAILED", async () => { const mod = loadModule("pages/api/file/deleteblobcase.js", { nextConnect: createNextConnectMock(), middleware: () => {}, getServerSession: async () => ({ user: { id: "c1" } }), authOptions: () => ({}), hashAPIPath: () => "&hash=expected", respondError: respondErrorMock, respondSuccess: respondSuccessMock, process: deletionTestProcess, deleteBlobCase: async () => { throw new Error("delete case failed"); } }); const req = { method: "POST", headers: { origin: TRUSTED_EN_ORIGIN }, 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, "DELETE_BLOB_CASE_FAILED" ); }); const loadCaseDeletionSecurity = (overrides = {}) => { let deletionCalls = 0; let sessionCalls = 0; const mod = loadModule("pages/api/file/deleteblobcase.js", { nextConnect: createNextConnectMock(), getServerSession: async () => { sessionCalls += 1; return { user: { id: "c1" } }; }, authOptions: () => ({}), hashAPIPath: () => "&hash=expected", respondError: respondErrorMock, respondSuccess: respondSuccessMock, process: deletionTestProcess, deleteBlobCase: async () => { deletionCalls += 1; return { deleted: true }; }, ...overrides }); return { mod, getDeletionCalls: () => deletionCalls, getSessionCalls: () => sessionCalls }; }; for (const method of ["GET", "DELETE"]) { test(`deleteblobcase rejects ${method} with 405 before deletion`, async () => { const loaded = loadCaseDeletionSecurity(); const res = createRes(); await loaded.mod.default.handler( validCaseDeletionRequest({ method }), res ); assert.strictEqual(res.state.statusCode, 405); assert.strictEqual(res.state.jsonBody.error.code, "METHOD_NOT_ALLOWED"); assert.strictEqual(res.state.headers.allow, "POST"); assert.strictEqual(loaded.getSessionCalls(), 0); assert.strictEqual(loaded.getDeletionCalls(), 0); }); } for (const [name, origin] of [ ["cross-origin", "https://attacker.example.test"], ["missing Origin", undefined], ["malformed Origin", "not a valid origin"], ["Origin containing a path", `${TRUSTED_EN_ORIGIN}/delete`], ["null Origin", "null"], ["subdomain spoof", "https://portal.example.test.attacker.test"], ["hostname suffix spoof", "https://attacker-portal.example.test"] ]) { test(`deleteblobcase rejects ${name} before session or deletion`, async () => { const loaded = loadCaseDeletionSecurity(); const headers = origin === undefined ? {} : { origin }; const res = createRes(); await loaded.mod.default.handler( validCaseDeletionRequest({ headers }), res ); assert.strictEqual(res.state.statusCode, 403); assert.strictEqual(res.state.jsonBody.error.code, "FORBIDDEN"); assert.strictEqual(loaded.getSessionCalls(), 0); assert.strictEqual(loaded.getDeletionCalls(), 0); }); } test("deleteblobcase accepts the configured Welsh portal Origin", async () => { const loaded = loadCaseDeletionSecurity(); const res = createRes(); await loaded.mod.default.handler( validCaseDeletionRequest({ headers: { origin: TRUSTED_CY_ORIGIN } }), res ); assert.strictEqual(res.state.statusCode, 200); assert.strictEqual(loaded.getSessionCalls(), 1); assert.strictEqual(loaded.getDeletionCalls(), 1); }); test("deleteblobrep handler accepts encoded hash variant", async () => { let deletedRepPath = null; const mod = loadModule("pages/api/file/deleteblobrep.js", { nextConnect: createNextConnectMock(), middleware: () => {}, getServerSession: async () => ({ user: { id: "c1" } }), authOptions: () => ({}), hashAPIPath: (queryPath) => queryPath.includes("repfile=Rep%20Name") ? "&hash=expected" : "&hash=other", respondError: respondErrorMock, respondSuccess: respondSuccessMock, process: deletionTestProcess, deleteBlobRep: async (_container, repPath) => { deletedRepPath = repPath; return { deleted: true }; } }); const req = { method: "POST", headers: { origin: TRUSTED_EN_ORIGIN }, query: { container: "c1", casefolderID: "CASE-1", repfile: "Rep Name", hash: "expected" } }; const res = createRes(); await mod.default.handler(req, res); assert.strictEqual(res.state.statusCode, 200); assert.strictEqual(deletedRepPath, "CASE-1/Rep Name"); }); test("deleteblobrep rejects missing or unsafe folder segments before deletion", async () => { for (const invalidQuery of [ { casefolderID: " ", repfile: "REP-1" }, { casefolderID: "CASE-1", repfile: " " }, { casefolderID: "CASE-1/REP-1", repfile: "REP-1" }, { casefolderID: "CASE-1", repfile: "REP-1/FILES" }, { casefolderID: "CASE-1", repfile: ".." } ]) { let deletionCalls = 0; const mod = loadModule("pages/api/file/deleteblobrep.js", { nextConnect: createNextConnectMock(), middleware: () => {}, getServerSession: async () => ({ user: { id: "c1" } }), authOptions: () => ({}), hashAPIPath: () => "&hash=expected", respondError: respondErrorMock, respondSuccess: respondSuccessMock, process: deletionTestProcess, deleteBlobRep: async () => { deletionCalls += 1; } }); const res = createRes(); await mod.default.handler( { method: "POST", headers: { origin: TRUSTED_EN_ORIGIN }, query: { container: "c1", hash: "expected", ...invalidQuery } }, res ); assert.strictEqual(res.state.statusCode, 400); assert.strictEqual(res.state.jsonBody.error.code, "INVALID_REQUEST"); assert.strictEqual(deletionCalls, 0); } }); test("deleteblobrep handler dependency failure returns DELETE_BLOB_REP_FAILED", async () => { const mod = loadModule("pages/api/file/deleteblobrep.js", { nextConnect: createNextConnectMock(), middleware: () => {}, getServerSession: async () => ({ user: { id: "c1" } }), authOptions: () => ({}), hashAPIPath: () => "&hash=expected", respondError: respondErrorMock, respondSuccess: respondSuccessMock, process: deletionTestProcess, deleteBlobRep: async () => { throw new Error("delete rep failed"); } }); const req = { method: "POST", headers: { origin: TRUSTED_EN_ORIGIN }, query: { container: "c1", casefolderID: "f1", repfile: "rep1", 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_BLOB_REP_FAILED"); }); const loadRepDeletionSecurity = (overrides = {}) => { let deletionCalls = 0; let sessionCalls = 0; const mod = loadModule("pages/api/file/deleteblobrep.js", { nextConnect: createNextConnectMock(), getServerSession: async () => { sessionCalls += 1; return { user: { id: "c1" } }; }, authOptions: () => ({}), hashAPIPath: () => "&hash=expected", respondError: respondErrorMock, respondSuccess: respondSuccessMock, process: deletionTestProcess, deleteBlobRep: async () => { deletionCalls += 1; return { deleted: true }; }, ...overrides }); return { mod, getDeletionCalls: () => deletionCalls, getSessionCalls: () => sessionCalls }; }; for (const method of ["GET", "DELETE"]) { test(`deleteblobrep rejects ${method} with 405 before deletion`, async () => { const loaded = loadRepDeletionSecurity(); const res = createRes(); await loaded.mod.default.handler( validRepDeletionRequest({ method }), res ); assert.strictEqual(res.state.statusCode, 405); assert.strictEqual(res.state.jsonBody.error.code, "METHOD_NOT_ALLOWED"); assert.strictEqual(res.state.headers.allow, "POST"); assert.strictEqual(loaded.getSessionCalls(), 0); assert.strictEqual(loaded.getDeletionCalls(), 0); }); } for (const [name, origin] of [ ["cross-origin", "https://attacker.example.test"], ["missing Origin", undefined], ["malformed Origin", "not a valid origin"], ["Origin containing a path", `${TRUSTED_EN_ORIGIN}/delete`], ["null Origin", "null"], ["subdomain spoof", "https://portal.example.test.attacker.test"], ["hostname suffix spoof", "https://attacker-portal.example.test"] ]) { test(`deleteblobrep rejects ${name} before session or deletion`, async () => { const loaded = loadRepDeletionSecurity(); const headers = origin === undefined ? {} : { origin }; const res = createRes(); await loaded.mod.default.handler( validRepDeletionRequest({ headers }), res ); assert.strictEqual(res.state.statusCode, 403); assert.strictEqual(res.state.jsonBody.error.code, "FORBIDDEN"); assert.strictEqual(loaded.getSessionCalls(), 0); assert.strictEqual(loaded.getDeletionCalls(), 0); }); } test("deleteblobrep accepts the configured Welsh portal Origin", async () => { const loaded = loadRepDeletionSecurity(); const res = createRes(); await loaded.mod.default.handler( validRepDeletionRequest({ headers: { origin: TRUSTED_CY_ORIGIN } }), res ); assert.strictEqual(res.state.statusCode, 200); assert.strictEqual(loaded.getSessionCalls(), 1); assert.strictEqual(loaded.getDeletionCalls(), 1); }); const ordinaryDeletionCases = [ { name: "deleteblobcase", route: "pages/api/file/deleteblobcase.js", request: { method: "POST", headers: { origin: TRUSTED_EN_ORIGIN } }, query: { container: "user-container", casefolderID: "CASE-1", hash: "expected" }, dependency: "deleteBlobCase" }, { name: "deleteblobrep", route: "pages/api/file/deleteblobrep.js", request: { method: "POST", headers: { origin: TRUSTED_EN_ORIGIN } }, query: { container: "user-container", casefolderID: "CASE-1", repfile: "representation.json", hash: "expected" }, dependency: "deleteBlobRep" } ]; for (const deletionCase of ordinaryDeletionCases) { test(`${deletionCase.name} forbids a missing NextAuth session before deletion`, async () => { let deletionCalls = 0; const mod = loadModule(deletionCase.route, { nextConnect: createNextConnectMock(), middleware: () => {}, getServerSession: async () => null, authOptions: () => ({}), hashAPIPath: () => "&hash=expected", respondError: respondErrorMock, respondSuccess: respondSuccessMock, process: deletionTestProcess, [deletionCase.dependency]: async () => { deletionCalls += 1; } }); const res = createRes(); await mod.default.handler( { ...deletionCase.request, query: deletionCase.query }, res ); assert.strictEqual(res.state.statusCode, 403); assert.strictEqual(res.state.jsonBody.error.code, "FORBIDDEN"); assert.strictEqual(deletionCalls, 0); }); test(`${deletionCase.name} forbids a session without user id before deletion`, async () => { let deletionCalls = 0; const mod = loadModule(deletionCase.route, { nextConnect: createNextConnectMock(), middleware: () => {}, getServerSession: async () => ({ user: {} }), authOptions: () => ({}), hashAPIPath: () => "&hash=expected", respondError: respondErrorMock, respondSuccess: respondSuccessMock, process: deletionTestProcess, [deletionCase.dependency]: async () => { deletionCalls += 1; } }); const res = createRes(); await mod.default.handler( { ...deletionCase.request, query: deletionCase.query }, res ); assert.strictEqual(res.state.statusCode, 403); assert.strictEqual(deletionCalls, 0); }); test(`${deletionCase.name} forbids cross-container deletion before HMAC or Azure deletion`, async () => { let hashCalls = 0; let deletionCalls = 0; const mod = loadModule(deletionCase.route, { nextConnect: createNextConnectMock(), middleware: () => {}, getServerSession: async () => ({ user: { id: "different-user" } }), authOptions: () => ({}), hashAPIPath: () => { hashCalls += 1; return "&hash=expected"; }, respondError: respondErrorMock, respondSuccess: respondSuccessMock, process: deletionTestProcess, [deletionCase.dependency]: async () => { deletionCalls += 1; } }); const res = createRes(); await mod.default.handler( { ...deletionCase.request, query: deletionCase.query }, res ); assert.strictEqual(res.state.statusCode, 403); assert.strictEqual(hashCalls, 0); assert.strictEqual(deletionCalls, 0); }); test(`${deletionCase.name} passes the request, response and PEDW options to getServerSession`, async () => { const sessionCalls = []; const optionCalls = []; const options = { id: `${deletionCase.name}-options` }; const mod = loadModule(deletionCase.route, { nextConnect: createNextConnectMock(), middleware: () => {}, authOptions: (req, res) => { optionCalls.push([req, res]); return options; }, getServerSession: async (...args) => { sessionCalls.push(args); return { user: { id: "user-container" } }; }, hashAPIPath: () => "&hash=expected", respondError: respondErrorMock, respondSuccess: respondSuccessMock, process: deletionTestProcess, [deletionCase.dependency]: async () => ({ deleted: true }) }); const req = { ...deletionCase.request, query: deletionCase.query, body: { parsed: ["multipart"] } }; const res = createRes(); await mod.default.handler(req, res); assert.deepStrictEqual(optionCalls, [[req, res]]); assert.strictEqual(sessionCalls.length, 1); assert.strictEqual(sessionCalls[0][0], req); assert.strictEqual(sessionCalls[0][1], res); assert.strictEqual(sessionCalls[0][2], options); assert.strictEqual(res.state.statusCode, 200); }); test(`${deletionCase.name} rejects invalid HMAC for the owning user without deletion`, async () => { let deletionCalls = 0; const mod = loadModule(deletionCase.route, { nextConnect: createNextConnectMock(), middleware: () => {}, getServerSession: async () => ({ user: { id: "user-container" } }), authOptions: () => ({}), hashAPIPath: () => "&hash=different", respondError: respondErrorMock, respondSuccess: respondSuccessMock, process: deletionTestProcess, [deletionCase.dependency]: async () => { deletionCalls += 1; } }); const res = createRes(); await mod.default.handler( { ...deletionCase.request, query: deletionCase.query }, res ); assert.strictEqual(res.state.statusCode, 400); assert.strictEqual(res.state.jsonBody.error.code, "INVALID_HASH"); assert.strictEqual(deletionCalls, 0); }); test(`${deletionCase.name} preserves valid same-user deletion`, async () => { const deletionCalls = []; const mod = loadModule(deletionCase.route, { nextConnect: createNextConnectMock(), middleware: () => {}, getServerSession: async () => ({ user: { id: "user-container" } }), authOptions: () => ({}), hashAPIPath: () => "&hash=expected", respondError: respondErrorMock, respondSuccess: respondSuccessMock, process: deletionTestProcess, [deletionCase.dependency]: async (...args) => { deletionCalls.push(args); return { deleted: true }; } }); const res = createRes(); await mod.default.handler( { ...deletionCase.request, query: deletionCase.query }, res ); assert.strictEqual(res.state.statusCode, 200); assert.strictEqual(deletionCalls.length, 1); assert.strictEqual(deletionCalls[0][0], "user-container"); }); } const loadAdminDeletion = (overrides = {}) => { let deletionCalls = 0; const mod = loadModule("pages/api/admin/deleteblobrep.js", { deleteBlobRep: async () => { deletionCalls += 1; return { deleted: true }; }, readAdminSessionCookie: () => "admin-token", validateAdminSession: () => true, hasTrustedSameOrigin: () => true, setAdminAuthNoStore: () => {}, respondError: respondErrorMock, respondSuccess: respondSuccessMock, ...overrides }); return { mod, getDeletionCalls: () => deletionCalls }; }; const validAdminDeletionRequest = () => ({ method: "POST", headers: { origin: "https://portal.example.test" }, body: { container: "ck-user-container", casefolderID: "CASE-1", repfile: "representation.json" } }); test("admin representation deletion forbids a missing administrative session", async () => { const loaded = loadAdminDeletion({ readAdminSessionCookie: () => null, validateAdminSession: () => false }); const res = createRes(); await loaded.mod.default(validAdminDeletionRequest(), res); assert.strictEqual(res.state.statusCode, 403); assert.strictEqual(res.state.jsonBody.error.code, "FORBIDDEN"); assert.strictEqual(loaded.getDeletionCalls(), 0); }); test("admin representation deletion forbids an invalid or expired administrative session", async () => { const loaded = loadAdminDeletion({ validateAdminSession: () => false }); const res = createRes(); await loaded.mod.default(validAdminDeletionRequest(), res); assert.strictEqual(res.state.statusCode, 403); assert.strictEqual(loaded.getDeletionCalls(), 0); }); for (const originCase of ["missing", "cross-origin"]) { test(`admin representation deletion forbids ${originCase} Origin`, async () => { const loaded = loadAdminDeletion({ hasTrustedSameOrigin: () => false }); const req = validAdminDeletionRequest(); if (originCase === "missing") delete req.headers.origin; if (originCase === "cross-origin") { req.headers.origin = "https://attacker.example"; } const res = createRes(); await loaded.mod.default(req, res); assert.strictEqual(res.state.statusCode, 403); assert.strictEqual(loaded.getDeletionCalls(), 0); }); } test("ordinary user HMAC cannot authorize administrative representation deletion", async () => { const loaded = loadAdminDeletion({ readAdminSessionCookie: () => null, validateAdminSession: () => false }); const req = validAdminDeletionRequest(); req.body.hash = "ordinary-user-hmac"; const res = createRes(); await loaded.mod.default(req, res); assert.strictEqual(res.state.statusCode, 403); assert.strictEqual(loaded.getDeletionCalls(), 0); }); test("admin representation deletion rejects malformed paths without deletion", async () => { for (const malformedBody of [ { casefolderID: "../CASE-1" }, { repfile: "folder/representation.json" }, { repfile: ".." } ]) { const loaded = loadAdminDeletion(); const req = validAdminDeletionRequest(); req.body = { ...req.body, ...malformedBody }; const res = createRes(); await loaded.mod.default(req, res); assert.strictEqual(res.state.statusCode, 400); assert.strictEqual(res.state.jsonBody.error.code, "INVALID_REQUEST"); assert.strictEqual(loaded.getDeletionCalls(), 0); } }); test("valid administrative session and origin authorize cross-container representation deletion", async () => { const deletionCalls = []; const loaded = loadAdminDeletion({ deleteBlobRep: async (...args) => { deletionCalls.push(args); return { deleted: args[1] }; } }); const res = createRes(); await loaded.mod.default(validAdminDeletionRequest(), res); assert.strictEqual(res.state.statusCode, 200); assert.deepStrictEqual(JSON.parse(JSON.stringify(deletionCalls)), [ ["ck-user-container", "CASE-1/representation.json"] ]); }); test("admin storage caller uses dedicated POST deletion while ordinary caller remains signed GET", () => { const rootDir = path.resolve(__dirname, "..", ".."); const adminSource = fs.readFileSync( path.join(rootDir, "components/admin/tabs/storage.js"), "utf8" ); const ordinarySource = fs.readFileSync( path.join(rootDir, "actions/services/documentDirectService.js"), "utf8" ); assert.match(adminSource, /fetch\("\/api\/admin\/deleteblobrep"/); assert.match(adminSource, /method:\s*"POST"/); assert.doesNotMatch(adminSource, /deleteMyRepresentationsFromBlob/); assert.match( ordinarySource, /buildFileQuery\("\/api\/file\/deleteblobrep"[\s\S]*postSignedFileJson\(queryUrl\)/ ); }); const loadAdminCaseDeletion = (overrides = {}) => { const calls = { noStore: 0, readSession: 0, validateSession: 0, origin: 0, deletion: [] }; const payload = { deleted: "TMP-CASE-1" }; const mod = loadModule("pages/api/admin/deleteblobcase.js", { deleteBlobCase: async (...args) => { calls.deletion.push(args); return payload; }, readAdminSessionCookie: () => { calls.readSession += 1; return "admin-token"; }, validateAdminSession: () => { calls.validateSession += 1; return true; }, hasTrustedSameOrigin: () => { calls.origin += 1; return true; }, setAdminAuthNoStore: () => { calls.noStore += 1; }, respondError: respondErrorMock, respondSuccess: respondSuccessMock, ...overrides }); return { mod, calls, payload }; }; const validAdminCaseDeletionRequest = (overrides = {}) => ({ method: "POST", headers: { origin: "https://portal.example.test", cookie: "pedw_admin_session=admin-token" }, body: { container: "selected-user-container", casefolderID: "TMP-CASE-1" }, ...overrides }); for (const method of ["GET", "DELETE"]) { test(`admin case deletion rejects ${method} with 405 before authorization or deletion`, async () => { const loaded = loadAdminCaseDeletion(); const res = createRes(); await loaded.mod.default( validAdminCaseDeletionRequest({ method }), res ); assert.strictEqual(res.state.statusCode, 405); assert.strictEqual(res.state.headers.allow, "POST"); assert.strictEqual(res.state.jsonBody.error.code, "METHOD_NOT_ALLOWED"); assert.strictEqual(loaded.calls.noStore, 1); assert.strictEqual(loaded.calls.readSession, 0); assert.strictEqual(loaded.calls.deletion.length, 0); }); } for (const sessionCase of [ { name: "missing", token: null }, { name: "invalid", token: "invalid-token" }, { name: "expired", token: "expired-token" } ]) { test(`admin case deletion rejects ${sessionCase.name} administrative session before deletion`, async () => { const loaded = loadAdminCaseDeletion({ readAdminSessionCookie: () => sessionCase.token, validateAdminSession: () => false }); const res = createRes(); await loaded.mod.default(validAdminCaseDeletionRequest(), res); assert.strictEqual(res.state.statusCode, 403); assert.strictEqual(res.state.jsonBody.error.code, "FORBIDDEN"); assert.strictEqual(loaded.calls.origin, 0); assert.strictEqual(loaded.calls.deletion.length, 0); }); } for (const originCase of [ { name: "missing", headers: { cookie: "pedw_admin_session=admin-token" } }, { name: "cross-origin", headers: { origin: "https://attacker.example.test", cookie: "pedw_admin_session=admin-token" } } ]) { test(`admin case deletion rejects ${originCase.name} Origin before deletion`, async () => { const loaded = loadAdminCaseDeletion({ hasTrustedSameOrigin: () => false }); const res = createRes(); await loaded.mod.default( validAdminCaseDeletionRequest({ headers: originCase.headers }), res ); assert.strictEqual(res.state.statusCode, 403); assert.strictEqual(loaded.calls.deletion.length, 0); }); } for (const inputCase of [ { name: "missing container", body: { container: undefined } }, { name: "malformed container", body: { container: "Invalid_Container" } }, { name: "missing case identifier", body: { casefolderID: undefined } }, { name: "case identifier path separator", body: { casefolderID: "TMP/CASE-1" } }, { name: "case identifier traversal segment", body: { casefolderID: ".." } }, { name: "case identifier control character", body: { casefolderID: "TMP\u0000CASE" } } ]) { test(`admin case deletion rejects ${inputCase.name} without deletion`, async () => { const loaded = loadAdminCaseDeletion(); const request = validAdminCaseDeletionRequest(); const res = createRes(); request.body = { ...request.body, ...inputCase.body }; Object.keys(request.body).forEach((key) => { if (typeof request.body[key] === "undefined") delete request.body[key]; }); await loaded.mod.default(request, res); assert.strictEqual(res.state.statusCode, 400); assert.strictEqual(res.state.jsonBody.error.code, "INVALID_REQUEST"); assert.strictEqual(loaded.calls.deletion.length, 0); }); } test("valid administrator can delete a cross-container case prefix with the existing helper", async () => { const loaded = loadAdminCaseDeletion(); const request = validAdminCaseDeletionRequest(); const res = createRes(); await loaded.mod.default(request, res); assert.strictEqual(res.state.statusCode, 200); assert.deepStrictEqual(loaded.calls.deletion, [ [request.body.container, request.body.casefolderID] ]); assert.deepStrictEqual(JSON.parse(JSON.stringify(res.state.jsonBody)), { data: loaded.payload }); }); test("admin case deletion dependency failure returns DELETE_BLOB_CASE_FAILED", async () => { const loaded = loadAdminCaseDeletion({ deleteBlobCase: async (...args) => { loaded.calls.deletion.push(args); throw new Error("delete case failed"); } }); const res = createRes(); await loaded.mod.default(validAdminCaseDeletionRequest(), res); assert.strictEqual(res.state.statusCode, 400); assert.strictEqual( res.state.jsonBody.error.code, "DELETE_BLOB_CASE_FAILED" ); assert.strictEqual(loaded.calls.deletion.length, 1); }); test("case and representation deletion use signed POST", async () => { const calls = []; const mod = loadDocumentDirectService({ buildFileQuery: (route, params) => { const query = Object.entries(params) .map(([key, value]) => `${key}=${value}`) .join("&"); return `${route}?${query}`; }, postSignedFileJson: async (...args) => { calls.push(["post", ...args]); return { deleted: true }; }, getSignedFileJson: async (...args) => { calls.push(["get", ...args]); return { deleted: true }; }, consoleLogger: () => {} }); await mod.deleteAwaitingSubmissionsFromBlob("c1", "CASE-1"); await mod.deleteMyRepresentationsFromBlob("c1", "CASE-1", "REP-1"); assert.deepStrictEqual(JSON.parse(JSON.stringify(calls)), [ ["post", "/api/file/deleteblobcase?container=c1&casefolderID=CASE-1"], [ "post", "/api/file/deleteblobrep?container=c1&casefolderID=CASE-1&repfile=REP-1" ] ]); }); const loadAdminRepJsonEdit = (overrides = {}) => { let azureClientConstructions = 0; const storageCalls = []; const sourceJson = { containerID: "ck-user-container", ticketnumber: "CASE-1", repComplete: true, representationType: "Support", nested: { retained: true } }; const uploaded = {}; class DefaultAzureCredentialMock {} class BlobServiceClientMock { constructor(url, credential) { azureClientConstructions += 1; storageCalls.push(["construct", url, credential]); } getContainerClient(container) { storageCalls.push(["container", container]); return { getBlockBlobClient(blobName) { storageCalls.push(["blob", blobName]); return { async download(offset) { storageCalls.push(["download", offset]); const { Readable } = require("stream"); return { readableStreamBody: Readable.from([ JSON.stringify(sourceJson) ]) }; }, async upload(content, length, options) { uploaded.content = content; uploaded.length = length; uploaded.options = options; storageCalls.push(["upload"]); }, async setTags(tags) { uploaded.tags = tags; storageCalls.push(["tags"]); }, async setMetadata(metadata) { uploaded.metadata = metadata; storageCalls.push(["metadata"]); } }; } }; } } const mod = loadModule("pages/api/admin/editRepJson.js", { Buffer, DefaultAzureCredential: DefaultAzureCredentialMock, BlobServiceClient: BlobServiceClientMock, consoleLogger: () => {}, readAdminSessionCookie: () => "admin-token", validateAdminSession: () => true, hasTrustedSameOrigin: () => true, setAdminAuthNoStore: () => {}, respondError: respondErrorMock, respondSuccess: respondSuccessMock, ...overrides }); return { mod, getAzureClientConstructions: () => azureClientConstructions, storageCalls, sourceJson, uploaded }; }; const validAdminRepJsonEditRequest = () => ({ method: "POST", headers: { origin: "https://portal.example.test" }, body: { container: "ck-user-container", blobName: "CASE-1/representation-1/representation_rep.json" } }); test("admin representation JSON edit forbids a missing administrative session before Azure access", async () => { const loaded = loadAdminRepJsonEdit({ readAdminSessionCookie: () => null, validateAdminSession: () => false }); const res = createRes(); await loaded.mod.default(validAdminRepJsonEditRequest(), res); assert.strictEqual(res.state.statusCode, 403); assert.strictEqual(res.state.jsonBody.error.code, "FORBIDDEN"); assert.strictEqual(loaded.getAzureClientConstructions(), 0); }); test("admin representation JSON edit forbids an invalid administrative session before Azure access", async () => { const loaded = loadAdminRepJsonEdit({ validateAdminSession: () => false }); const res = createRes(); await loaded.mod.default(validAdminRepJsonEditRequest(), res); assert.strictEqual(res.state.statusCode, 403); assert.strictEqual(loaded.getAzureClientConstructions(), 0); }); test("admin representation JSON edit forbids cross-origin requests before Azure access", async () => { const loaded = loadAdminRepJsonEdit({ hasTrustedSameOrigin: () => false }); const req = validAdminRepJsonEditRequest(); req.headers.origin = "https://attacker.example"; const res = createRes(); await loaded.mod.default(req, res); assert.strictEqual(res.state.statusCode, 403); assert.strictEqual(loaded.getAzureClientConstructions(), 0); }); test("admin representation JSON edit rejects unsupported methods before Azure access", async () => { const loaded = loadAdminRepJsonEdit(); const req = validAdminRepJsonEditRequest(); req.method = "GET"; const res = createRes(); await loaded.mod.default(req, res); assert.strictEqual(res.state.statusCode, 405); assert.strictEqual(res.state.jsonBody.error.code, "METHOD_NOT_ALLOWED"); assert.strictEqual(res.state.headers.allow, "POST"); assert.strictEqual(loaded.getAzureClientConstructions(), 0); }); test("admin representation JSON edit rejects malformed identifiers before Azure access", async () => { for (const malformedBody of [ { container: "UPPERCASE" }, { container: "ab" }, { blobName: "../representation_rep.json" }, { blobName: "CASE-1//representation_rep.json" }, { blobName: "CASE-1\\representation_rep.json" }, { blobName: " representation_rep.json" } ]) { const loaded = loadAdminRepJsonEdit(); const req = validAdminRepJsonEditRequest(); req.body = { ...req.body, ...malformedBody }; const res = createRes(); await loaded.mod.default(req, res); assert.strictEqual(res.state.statusCode, 400); assert.strictEqual(res.state.jsonBody.error.code, "INVALID_REQUEST"); assert.strictEqual(loaded.getAzureClientConstructions(), 0); } }); test("valid administrative representation JSON edit preserves the existing mutation contract", async () => { const loaded = loadAdminRepJsonEdit(); const res = createRes(); await loaded.mod.default(validAdminRepJsonEditRequest(), res); assert.strictEqual(res.state.statusCode, 200); assert.deepStrictEqual(JSON.parse(JSON.stringify(res.state.jsonBody)), { message: "repComplete removed successfully" }); assert.strictEqual(loaded.getAzureClientConstructions(), 1); assert.deepStrictEqual(loaded.storageCalls.slice(1, 4), [ ["container", "ck-user-container"], ["blob", "CASE-1/representation-1/representation_rep.json"], ["download", 0] ]); const updatedJson = JSON.parse(loaded.uploaded.content); assert.strictEqual(Object.hasOwn(updatedJson, "repComplete"), false); assert.strictEqual(updatedJson.containerID, loaded.sourceJson.containerID); assert.strictEqual( updatedJson.ticketnumber, loaded.sourceJson.ticketnumber ); assert.strictEqual( updatedJson.representationType, loaded.sourceJson.representationType ); assert.deepStrictEqual( JSON.parse(JSON.stringify(updatedJson.nested)), loaded.sourceJson.nested ); assert.strictEqual( loaded.uploaded.length, Buffer.byteLength(loaded.uploaded.content) ); assert.deepStrictEqual( JSON.parse(JSON.stringify(loaded.uploaded.options)), { blobHTTPHeaders: { blobContentType: "application/json" } } ); assert.deepStrictEqual(JSON.parse(JSON.stringify(loaded.uploaded.tags)), { containerid: "ck-user-container", caseID: "CASE-1", blobType: "Representation" }); assert.deepStrictEqual( JSON.parse(JSON.stringify(loaded.uploaded.metadata)), JSON.parse(JSON.stringify(loaded.uploaded.tags)) ); }); test("retired file representation JSON endpoint cannot mutate storage", async () => { let storageCalls = 0; const mod = loadModule("pages/api/file/editRepJson.js", { BlobServiceClient: class { constructor() { storageCalls += 1; } }, respondError: respondErrorMock }); const res = createRes(); await mod.default(validAdminRepJsonEditRequest(), res); assert.strictEqual(res.state.statusCode, 410); assert.strictEqual(res.state.jsonBody.error.code, "ENDPOINT_RETIRED"); assert.strictEqual(storageCalls, 0); }); test("admin storage caller uses the protected representation JSON edit endpoint", () => { const rootDir = path.resolve(__dirname, "..", ".."); const adminSource = fs.readFileSync( path.join(rootDir, "components/admin/tabs/storage.js"), "utf8" ); assert.match(adminSource, /fetch\(\s*"\/api\/admin\/editRepJson"/); assert.doesNotMatch(adminSource, /fetch\(\s*"\/api\/file\/editRepJson"/); }); test("deleteawaitingsubmissionfromblob accepts legacy deleteblob hash variant", async () => { let deletedPath = null; const mod = loadModule( "pages/api/file/deleteawaitingsubmissionfromblob.js", { nextConnect: createNextConnectMock(), middleware: () => {}, getServerSession: async () => ({ user: { id: "container-1" } }), authOptions: () => ({}), hashAPIPath: (queryPath) => queryPath.includes("/api/file/deleteblob?container=container-1") ? "&hash=expected" : "&hash=other", respondError: respondErrorMock, respondSuccess: respondSuccessMock, deleteBlob: async (_container, blobPath) => { deletedPath = blobPath; return { deleted: true }; } } ); const req = { query: { container: "container-1", 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: () => {}, getServerSession: async () => ({ user: { id: "container-1" } }), authOptions: () => ({}), hashAPIPath: () => "&hash=expected", respondError: respondErrorMock, respondSuccess: respondSuccessMock, deleteBlob: async () => { throw new Error("delete awaiting failed"); } } ); const req = { query: { container: "container-1", 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" ); }); const ownerBoundBlobDeletionCases = [ { name: "deleteblob", route: "pages/api/file/deleteblob.js" }, { name: "deleteawaitingsubmissionfromblob", route: "pages/api/file/deleteawaitingsubmissionfromblob.js" } ]; const loadOwnerBoundBlobDeletion = ( deletionCase, { session, ...overrides } = {} ) => { const calls = { options: [], session: [], hash: [], deletion: [] }; const options = { id: `${deletionCase.name}-options` }; const resolvedSession = typeof session === "undefined" ? { user: { id: "owner-container" } } : session; const mod = loadModule(deletionCase.route, { nextConnect: createNextConnectMock(), middleware: () => {}, authOptions: (req, res) => { calls.options.push([req, res]); return options; }, getServerSession: async (...args) => { calls.session.push(args); return resolvedSession; }, hashAPIPath: (queryPath) => { calls.hash.push(queryPath); return "&hash=expected"; }, respondError: respondErrorMock, respondSuccess: respondSuccessMock, deleteBlob: async (...args) => { calls.deletion.push(args); return { deleted: true }; }, ...overrides }); return { mod, calls, options }; }; const ownerBoundBlobDeletionRequest = (overrides = {}) => ({ query: { container: "owner-container", casefolderID: "CASE-1", blobname: "document.pdf", hash: "expected" }, ...overrides }); const assertNoOwnerBoundBlobDeletion = (calls) => { assert.strictEqual(calls.hash.length, 0); assert.strictEqual(calls.deletion.length, 0); }; for (const deletionCase of ownerBoundBlobDeletionCases) { for (const sessionCase of [ { name: "missing session", session: null }, { name: "session without user id", session: { user: {} } }, { name: "session with empty user id", session: { user: { id: "" } } }, { name: "session with non-string user id", session: { user: { id: 123 } } } ]) { test(`${deletionCase.name} rejects ${sessionCase.name} before HMAC or Azure deletion`, async () => { const loaded = loadOwnerBoundBlobDeletion(deletionCase, { session: sessionCase.session }); const res = createRes(); await loaded.mod.default.handler( ownerBoundBlobDeletionRequest(), res ); assert.strictEqual(res.state.statusCode, 403); assert.strictEqual(res.state.jsonBody.error.code, "FORBIDDEN"); assertNoOwnerBoundBlobDeletion(loaded.calls); }); } test(`${deletionCase.name} rejects cross-container deletion before HMAC or Azure deletion`, async () => { const loaded = loadOwnerBoundBlobDeletion(deletionCase, { session: { user: { id: "different-container" } } }); const res = createRes(); await loaded.mod.default.handler(ownerBoundBlobDeletionRequest(), res); assert.strictEqual(res.state.statusCode, 403); assert.strictEqual(res.state.jsonBody.error.code, "FORBIDDEN"); assertNoOwnerBoundBlobDeletion(loaded.calls); }); test(`${deletionCase.name} rejects an invalid container before authentication or deletion`, async () => { const loaded = loadOwnerBoundBlobDeletion(deletionCase); const res = createRes(); await loaded.mod.default.handler( ownerBoundBlobDeletionRequest({ query: { container: "Invalid_Container", casefolderID: "CASE-1", blobname: "document.pdf", hash: "expected" } }), res ); assert.strictEqual(res.state.statusCode, 400); assert.strictEqual(res.state.jsonBody.error.code, "INVALID_REQUEST"); assert.strictEqual(loaded.calls.options.length, 0); assert.strictEqual(loaded.calls.session.length, 0); assertNoOwnerBoundBlobDeletion(loaded.calls); }); test(`${deletionCase.name} rejects a missing blob name before authentication or deletion`, async () => { const loaded = loadOwnerBoundBlobDeletion(deletionCase); const res = createRes(); await loaded.mod.default.handler( ownerBoundBlobDeletionRequest({ query: { container: "owner-container", casefolderID: "CASE-1", hash: "expected" } }), res ); assert.strictEqual(res.state.statusCode, 400); assert.strictEqual( res.state.jsonBody.error.code, "MISSING_REQUIRED_QUERY" ); assert.strictEqual(loaded.calls.options.length, 0); assert.strictEqual(loaded.calls.session.length, 0); assertNoOwnerBoundBlobDeletion(loaded.calls); }); test(`${deletionCase.name} rejects malformed folder and blob parameters before authentication or deletion`, async () => { const loaded = loadOwnerBoundBlobDeletion(deletionCase); for (const query of [ { container: "owner-container", casefolderID: ["CASE-1"], blobname: "document.pdf", hash: "expected" }, { container: "owner-container", casefolderID: "CASE-1", blobname: ["document.pdf"], hash: "expected" }, { container: "owner-container", casefolderID: " ", blobname: "document.pdf", hash: "expected" }, { container: "owner-container", casefolderID: "CASE-1", blobname: " ", hash: "expected" } ]) { const res = createRes(); await loaded.mod.default.handler( ownerBoundBlobDeletionRequest({ query }), res ); assert.strictEqual(res.state.statusCode, 400); assert.strictEqual( res.state.jsonBody.error.code, "MISSING_REQUIRED_QUERY" ); } assert.strictEqual(loaded.calls.options.length, 0); assert.strictEqual(loaded.calls.session.length, 0); assertNoOwnerBoundBlobDeletion(loaded.calls); }); test(`${deletionCase.name} rejects an invalid HMAC after owner authentication and before deletion`, async () => { const loaded = loadOwnerBoundBlobDeletion(deletionCase, { hashAPIPath: () => "&hash=different" }); const res = createRes(); await loaded.mod.default.handler(ownerBoundBlobDeletionRequest(), res); assert.strictEqual(res.state.statusCode, 400); assert.strictEqual(res.state.jsonBody.error.code, "INVALID_HASH"); assert.strictEqual(loaded.calls.deletion.length, 0); }); test(`${deletionCase.name} preserves owner deletion and request-aware session arguments`, async () => { const loaded = loadOwnerBoundBlobDeletion(deletionCase); const req = ownerBoundBlobDeletionRequest(); const res = createRes(); await loaded.mod.default.handler(req, res); assert.strictEqual(res.state.statusCode, 200); assert.deepStrictEqual(loaded.calls.options, [[req, res]]); assert.deepStrictEqual(loaded.calls.session, [ [req, res, loaded.options] ]); assert.strictEqual(loaded.calls.hash.length, 1); assert.deepStrictEqual(loaded.calls.deletion, [ ["owner-container", "CASE-1/files/document.pdf"] ]); assert.deepStrictEqual(JSON.parse(JSON.stringify(res.state.jsonBody)), { data: { deleted: true } }); }); } test("getrepsblob handler success returns representations payload", async () => { const mod = loadModule("pages/api/file/getrepsblob.js", { nextConnect: createNextConnectMock(), middleware: () => {}, hashAPIPath: () => "&hash=expected", respondError: respondErrorMock, respondSuccess: respondSuccessMock, consoleLogger: () => {}, getServerSession: async () => ({ user: { id: "c1" } }), authOptions: () => ({}), readAdminSessionCookie: () => null, validateAdminSession: () => false, getRepsBlobs: async () => [ { path: "c/ref_rep.json", name: "ref_rep.json" } ], downloadAllRepsFiles: async () => ({ "@odata.count": 1, value: [{ id: "r1" }] }) }); const req = { query: { container: "c1", 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)), { "@odata.count": 1, value: [{ id: "r1" }] }); }); test("getrepsblob handler rejects missing HMAC before Azure access", async () => { let azureCalls = 0; const mod = loadModule("pages/api/file/getrepsblob.js", { nextConnect: createNextConnectMock(), middleware: () => {}, hashAPIPath: () => "&hash=expected", respondError: respondErrorMock, respondSuccess: respondSuccessMock, consoleLogger: () => {}, getRepsBlobs: async () => { azureCalls += 1; return []; }, downloadAllRepsFiles: async () => { azureCalls += 1; return {}; } }); const res = createRes(); await mod.default.handler({ query: { container: "c1" } }, res); assert.strictEqual(res.state.statusCode, 400); assert.strictEqual(res.state.jsonBody.error.code, "MISSING_REQUIRED_QUERY"); assert.strictEqual(azureCalls, 0); }); test("getrepsblob handler rejects invalid HMAC before Azure access", async () => { let azureCalls = 0; const mod = loadModule("pages/api/file/getrepsblob.js", { nextConnect: createNextConnectMock(), middleware: () => {}, hashAPIPath: () => "&hash=expected", respondError: respondErrorMock, respondSuccess: respondSuccessMock, consoleLogger: () => {}, getServerSession: async () => ({ user: { id: "c1" } }), authOptions: () => ({}), readAdminSessionCookie: () => null, validateAdminSession: () => false, getRepsBlobs: async () => { azureCalls += 1; return []; }, downloadAllRepsFiles: async () => { azureCalls += 1; return {}; } }); const res = createRes(); await mod.default.handler( { query: { container: "c1", hash: "wrong" } }, res ); assert.strictEqual(res.state.statusCode, 400); assert.strictEqual(res.state.jsonBody.error.code, "INVALID_HASH"); assert.strictEqual(azureCalls, 0); }); test("general hash endpoint does not authorize getrepsblob signing", async () => { let hashCalls = 0; const mod = loadModule("pages/api/endpoint/gethash_api.js", { nextConnect: createNextConnectMock(), getServerSession: async () => ({ user: { id: "user-container" } }), authOptions: () => ({}), hashAPIPath: () => { hashCalls += 1; return "&hash=unexpected"; }, respondError: respondErrorMock, respondSuccess: respondSuccessMock }); const res = createRes(); await mod.default.handler( { query: { path: "/api/file/getrepsblob?container=user-container" } }, res ); assert.strictEqual(res.state.statusCode, 400); assert.strictEqual(res.state.jsonBody.error.code, "INVALID_HASH_PATH"); assert.strictEqual(hashCalls, 0); }); const loadAppealCompletionSigning = ({ session, ...overrides } = {}) => { const calls = { options: [], session: [], hash: [] }; const options = { id: "appeal-completion-signing-options" }; const resolvedSession = typeof session === "undefined" ? { user: { id: "owner-container" } } : session; const mod = loadModule("pages/api/endpoint/gethash_api.js", { nextConnect: createNextConnectMock(), authOptions: (req, res) => { calls.options.push([req, res]); return options; }, getServerSession: async (...args) => { calls.session.push(args); return resolvedSession; }, hashAPIPath: (queryPath) => { calls.hash.push(queryPath); return "&hash=completion-capability"; }, respondError: respondErrorMock, respondSuccess: respondSuccessMock, URL, ...overrides }); return { mod, calls, options }; }; const appealCompletionSigningRequest = (pathValue) => ({ query: { path: pathValue || "/api/file/createappealcompletemessage_api?container=owner-container&tempcaseref=TMP-CASE-1" } }); for (const sessionCase of [ { name: "missing session", session: null, status: 401, code: "UNAUTHENTICATED" }, { name: "session without user id", session: { user: {} } }, { name: "session with empty user id", session: { user: { id: "" } } }, { name: "session with non-string user id", session: { user: { id: 123 } } }, { name: "foreign-container session", session: { user: { id: "foreign-container" } } } ]) { test(`gethash rejects appeal completion signing for ${sessionCase.name} before HMAC generation`, async () => { const loaded = loadAppealCompletionSigning({ session: sessionCase.session }); const res = createRes(); await loaded.mod.default.handler(appealCompletionSigningRequest(), res); assert.strictEqual(res.state.statusCode, sessionCase.status || 403); assert.strictEqual( res.state.jsonBody.error.code, sessionCase.code || "FORBIDDEN" ); assert.strictEqual(loaded.calls.hash.length, 0); }); } for (const pathCase of [ { name: "missing container", path: "/api/file/createappealcompletemessage_api?tempcaseref=TMP-CASE-1" }, { name: "invalid container", path: "/api/file/createappealcompletemessage_api?container=Invalid_Container&tempcaseref=TMP-CASE-1" }, { name: "missing draft reference", path: "/api/file/createappealcompletemessage_api?container=owner-container" }, { name: "malformed draft reference", path: "/api/file/createappealcompletemessage_api?container=owner-container&tempcaseref=..%2Fforeign" }, { name: "duplicate container binding", path: "/api/file/createappealcompletemessage_api?container=owner-container&container=foreign-container&tempcaseref=TMP-CASE-1" }, { name: "duplicate draft binding", path: "/api/file/createappealcompletemessage_api?container=owner-container&tempcaseref=TMP-CASE-1&tempcaseref=TMP-CASE-2" }, { name: "completion route lookalike", path: "/api/file/createappealcompletemessage_api-foreign?container=owner-container&tempcaseref=TMP-CASE-1" } ]) { test(`gethash rejects appeal completion signing with ${pathCase.name} before HMAC generation`, async () => { const loaded = loadAppealCompletionSigning(); const res = createRes(); await loaded.mod.default.handler( appealCompletionSigningRequest(pathCase.path), res ); assert.strictEqual(res.state.statusCode, 400); assert.strictEqual(res.state.jsonBody.error.code, "INVALID_HASH_PATH"); assert.strictEqual(loaded.calls.hash.length, 0); }); } test("gethash issues the existing appeal completion capability only to the owning session", async () => { const loaded = loadAppealCompletionSigning(); const req = appealCompletionSigningRequest(); const res = createRes(); await loaded.mod.default.handler(req, res); assert.strictEqual(res.state.statusCode, 200); assert.deepStrictEqual(loaded.calls.options, [[req, res]]); assert.deepStrictEqual(loaded.calls.session, [[req, res, loaded.options]]); assert.deepStrictEqual(loaded.calls.hash, [req.query.path]); assert.deepStrictEqual(JSON.parse(JSON.stringify(res.state.jsonBody)), { hash: "&hash=completion-capability" }); }); test("gethash preserves unrelated allow-listed signing without applying completion ownership", async () => { const loaded = loadAppealCompletionSigning({ session: { user: { email: "user@test.local" } } }); const req = appealCompletionSigningRequest("/api/file/upload"); const res = createRes(); await loaded.mod.default.handler(req, res); assert.strictEqual(res.state.statusCode, 200); assert.deepStrictEqual(loaded.calls.hash, [req.query.path]); }); test("getrepsblob handler dependency failure returns GET_REPS_BLOB_FAILED", async () => { const mod = loadModule("pages/api/file/getrepsblob.js", { nextConnect: createNextConnectMock(), middleware: () => {}, hashAPIPath: () => "&hash=expected", respondError: respondErrorMock, respondSuccess: respondSuccessMock, consoleLogger: () => {}, getServerSession: async () => ({ user: { id: "c1" } }), authOptions: () => ({}), readAdminSessionCookie: () => null, validateAdminSession: () => false, getRepsBlobs: async () => { throw new Error("storage failure"); }, downloadAllRepsFiles: async () => ({}) }); const req = { query: { container: "c1", 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_REPS_BLOB_FAILED"); }); test("getrepsblob handler accepts encoded container hash variant", async () => { const mod = loadModule("pages/api/file/getrepsblob.js", { nextConnect: createNextConnectMock(), middleware: () => {}, hashAPIPath: (queryPath) => queryPath.includes("container=cont%2F1") ? "&hash=expected" : "&hash=other", respondError: respondErrorMock, respondSuccess: respondSuccessMock, consoleLogger: () => {}, getServerSession: async () => ({ user: { id: "cont/1" } }), authOptions: () => ({}), readAdminSessionCookie: () => null, validateAdminSession: () => false, getRepsBlobs: async () => [{ path: "cont/rep1.json" }], downloadAllRepsFiles: async () => ({ value: [{ id: "r-encoded" }] }) }); const req = { query: { container: "cont/1", 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: [{ id: "r-encoded" }] }); }); test("uploadsinglefile handler returns HASH_REQUIRED when hash missing", async () => { const mod = loadModule("pages/api/file/uploadsinglefile.js", { nextConnect: createNextConnectMock(), middleware: () => {}, getServerSession: async () => ({ user: { id: "c1" } }), authOptions: () => ({}), hashAPIPath: () => "?hash=expected", respondError: respondErrorMock, respondSuccess: respondSuccessMock, consoleLogger: () => {}, uploadSingleFile: async () => ({ uploaded: 0 }), fileTypeFromBuffer: async () => ({ mime: "application/pdf" }), fs: { readFileSync: () => Buffer.from("x") } }); const req = { query: {}, body: {}, files: {} }; const res = createRes(); await mod.default.handler(req, res); assert.strictEqual(res.state.statusCode, 400); assert.strictEqual(res.state.jsonBody.error.code, "HASH_REQUIRED"); }); test("uploadsinglefile handler dependency failure returns UPLOAD_SINGLE_FILE_FAILED", async () => { const mod = loadModule("pages/api/file/uploadsinglefile.js", { nextConnect: createNextConnectMock(), middleware: () => {}, getServerSession: async () => ({ user: { id: "c1" } }), authOptions: () => ({}), hashAPIPath: () => "?hash=expected", respondError: respondErrorMock, respondSuccess: respondSuccessMock, consoleLogger: () => {}, uploadSingleFile: async () => { throw new Error("upload failed"); }, fileTypeFromBuffer: async () => ({ mime: "application/pdf" }), fs: { readFileSync: () => Buffer.from("x") }, path: { basename: (value) => value } }); const req = { query: { hash: "expected" }, body: { containerID: ["c1"], casefolderID: ["f1"] }, files: { document: [ { fieldName: "document.pdf", originalFilename: "document.pdf", path: "/tmp/document.pdf", size: 1, headers: { "content-type": "application/pdf" } } ] } }; const res = createRes(); await mod.default.handler(req, res); assert.strictEqual(res.state.statusCode, 500); assert.strictEqual( res.state.jsonBody.error.code, "UPLOAD_SINGLE_FILE_FAILED" ); }); const loadSingleFileUploadAuthorization = (overrides = {}) => { const calls = { session: [], options: [], upload: [], hash: [], fileReads: 0, typeChecks: 0 }; const options = { id: "pedw-request-options" }; const mod = loadModule("pages/api/file/uploadsinglefile.js", { nextConnect: createNextConnectMock(), middleware: () => {}, authOptions: (req, res) => { calls.options.push([req, res]); return options; }, getServerSession: async (...args) => { calls.session.push(args); return { user: { id: "user-container" } }; }, hashAPIPath: (queryPath) => { calls.hash.push(queryPath); return "?hash=expected"; }, respondError: respondErrorMock, respondSuccess: respondSuccessMock, consoleLogger: () => {}, uploadSingleFile: async (...args) => { calls.upload.push(args); return [{ file: "document.pdf" }]; }, fileTypeFromBuffer: async () => { calls.typeChecks += 1; return { mime: "application/pdf" }; }, fs: { readFileSync: () => { calls.fileReads += 1; return Buffer.from("synthetic-pdf"); } }, path: { basename: (value) => value }, ...overrides }); return { mod, calls, options }; }; const singleFileUploadAuthorizationRequest = (overrides = {}) => ({ query: { hash: "expected" }, body: { containerID: ["user-container"], casefolderID: ["CASE-1"] }, files: { document: [ { fieldName: "document.pdf", originalFilename: "document.pdf", path: "/tmp/document.pdf", size: 1, headers: { "content-type": "application/pdf" } } ] }, ...overrides }); const assertNoSingleFileWork = (calls, { noHash = false } = {}) => { assert.strictEqual(calls.upload.length, 0); assert.strictEqual(calls.fileReads, 0); assert.strictEqual(calls.typeChecks, 0); if (noHash) assert.strictEqual(calls.hash.length, 0); }; test("uploadsinglefile rejects a missing NextAuth session before HMAC, file processing or Azure write", async () => { const loaded = loadSingleFileUploadAuthorization({ getServerSession: async () => null }); const res = createRes(); await loaded.mod.default.handler( singleFileUploadAuthorizationRequest(), res ); assert.strictEqual(res.state.statusCode, 401); assert.strictEqual(res.state.jsonBody.error.code, "UNAUTHENTICATED"); assertNoSingleFileWork(loaded.calls, { noHash: true }); }); test("uploadsinglefile rejects an unusable NextAuth session before HMAC, file processing or Azure write", async () => { const loaded = loadSingleFileUploadAuthorization({ getServerSession: async () => ({ user: { id: "" } }) }); const res = createRes(); await loaded.mod.default.handler( singleFileUploadAuthorizationRequest(), res ); assert.strictEqual(res.state.statusCode, 401); assert.strictEqual(res.state.jsonBody.error.code, "UNAUTHENTICATED"); assertNoSingleFileWork(loaded.calls, { noHash: true }); }); test("uploadsinglefile passes the multipart request, response and PEDW request-scoped options to getServerSession", async () => { const loaded = loadSingleFileUploadAuthorization(); const req = singleFileUploadAuthorizationRequest(); const res = createRes(); await loaded.mod.default.handler(req, res); assert.strictEqual(loaded.calls.options.length, 1); assert.strictEqual(loaded.calls.options[0][0], req); assert.strictEqual(loaded.calls.options[0][1], res); assert.strictEqual(loaded.calls.session.length, 1); assert.strictEqual(loaded.calls.session[0][0], req); assert.strictEqual(loaded.calls.session[0][1], res); assert.strictEqual(loaded.calls.session[0][2], loaded.options); }); test("installed getServerSession resolves a database session cookie without treating multipart body as a session update", async () => { const { getServerSession } = require("next-auth/next"); const adapterCalls = []; const callbackCalls = []; const expires = new Date(Date.now() + 30 * 60 * 1000); const req = { method: "POST", body: { containerID: ["user-container"], casefolderID: ["CASE-1"] }, headers: { host: "localhost:3000" }, cookies: { "next-auth.session-token": "session-token" } }; const responseHeaders = {}; const res = { getHeader(name) { return responseHeaders[name.toLowerCase()]; }, setHeader(name, value) { responseHeaders[name.toLowerCase()] = value; } }; const user = { id: "user-container", name: "Owner", email: "owner@example.test", image: null }; const session = { sessionToken: "session-token", userId: user.id, expires }; const options = { secret: "test-secret", session: { strategy: "database", maxAge: 30 * 60, updateAge: 5 * 60 }, cookies: { sessionToken: { name: "next-auth.session-token", options: { httpOnly: true, sameSite: "lax", path: "/", secure: false } } }, adapter: { async getSessionAndUser(sessionToken) { adapterCalls.push(["getSessionAndUser", sessionToken]); return { session, user }; }, async updateSession(updatedSession) { adapterCalls.push(["updateSession", updatedSession]); return updatedSession; }, async deleteSession(sessionToken) { adapterCalls.push(["deleteSession", sessionToken]); } }, callbacks: { async session(callbackArguments) { callbackCalls.push(callbackArguments); return callbackArguments; } } }; const resolvedSession = await getServerSession(req, res, options); assert.deepStrictEqual(adapterCalls[0], [ "getSessionAndUser", "session-token" ]); assert.strictEqual(callbackCalls.length, 1); assert.strictEqual(callbackCalls[0].user.id, "user-container"); assert.strictEqual(callbackCalls[0].newSession, undefined); assert.strictEqual(resolvedSession.user.id, "user-container"); assert.deepStrictEqual(req.body, { containerID: ["user-container"], casefolderID: ["CASE-1"] }); }); test("uploadsinglefile forbids another user's container before HMAC, file processing or Azure write", async () => { const loaded = loadSingleFileUploadAuthorization(); const req = singleFileUploadAuthorizationRequest(); req.body.containerID = ["other-container"]; const res = createRes(); await loaded.mod.default.handler(req, res); assert.strictEqual(res.state.statusCode, 403); assert.strictEqual(res.state.jsonBody.error.code, "FORBIDDEN"); assertNoSingleFileWork(loaded.calls, { noHash: true }); }); test("uploadsinglefile rejects invalid HMAC for the owning user before file processing or Azure write", async () => { const loaded = loadSingleFileUploadAuthorization({ hashAPIPath: () => "?hash=different" }); const res = createRes(); await loaded.mod.default.handler( singleFileUploadAuthorizationRequest(), res ); assert.strictEqual(res.state.statusCode, 400); assert.strictEqual(res.state.jsonBody.error.code, "INVALID_HASH"); assertNoSingleFileWork(loaded.calls); }); test("uploadsinglefile requires HMAC for the authenticated owning user", async () => { const loaded = loadSingleFileUploadAuthorization(); const req = singleFileUploadAuthorizationRequest({ query: {} }); const res = createRes(); await loaded.mod.default.handler(req, res); assert.strictEqual(res.state.statusCode, 400); assert.strictEqual(res.state.jsonBody.error.code, "HASH_REQUIRED"); assertNoSingleFileWork(loaded.calls); }); test("uploadsinglefile preserves multipart owner success response and exact Azure helper arguments", async () => { const loaded = loadSingleFileUploadAuthorization(); const res = createRes(); await loaded.mod.default.handler( singleFileUploadAuthorizationRequest(), res ); assert.strictEqual(res.state.statusCode, 200); assert.deepStrictEqual(JSON.parse(JSON.stringify(res.state.jsonBody)), { data: [{ file: "document.pdf" }], invalidFiles: [] }); assert.strictEqual(loaded.calls.upload.length, 1); assert.strictEqual(loaded.calls.upload[0][1], "user-container"); assert.strictEqual(loaded.calls.upload[0][2], "CASE-1"); assert.deepStrictEqual( JSON.parse(JSON.stringify(loaded.calls.upload[0][0])), { document: [ { fieldName: "document.pdf", originalFilename: "document.pdf", path: "/tmp/document.pdf", size: 1, headers: { "content-type": "application/pdf" }, contentType: "application/pdf" } ] } ); assert.strictEqual(loaded.calls.fileReads, 1); assert.strictEqual(loaded.calls.typeChecks, 1); }); const loadOrdinaryRepCompletion = (overrides = {}) => { const calls = { hash: [], queue: [] }; const payload = { messageId: "ordinary-message" }; const mod = loadModule("pages/api/file/createrepcompletemessage_api.js", { respondError: respondErrorMock, respondSuccess: respondSuccessMock, getSession: async () => ({ user: { id: "user-container" } }), hashAPIPath: (queryPath) => { calls.hash.push(queryPath); return "&hash=expected"; }, createRepCompleteMessage: async (...args) => { calls.queue.push(args); return payload; }, consoleLogger: () => {}, ...overrides }); return { mod, calls, payload }; }; const ordinaryRepCompletionRequest = (overrides = {}) => ({ method: "GET", headers: {}, query: { container: "user-container", tempcaseref: "CAS-12345-A1B2C3", repid: "2026-03-18-10:20:30_-_APP_-_Statement_-_Jones_Rob", hash: "expected" }, ...overrides }); const assertNoCompletionWork = (calls, { noHash = false } = {}) => { assert.strictEqual(calls.queue.length, 0); if (noHash) assert.strictEqual(calls.hash.length, 0); }; test("createrepcompletemessage rejects unsupported methods before authorization work", async () => { const loaded = loadOrdinaryRepCompletion(); const res = createRes(); await loaded.mod.default( ordinaryRepCompletionRequest({ method: "POST" }), res ); assert.strictEqual(res.state.statusCode, 405); assert.strictEqual(res.state.headers.allow, "GET"); assert.strictEqual(res.state.jsonBody.error.code, "METHOD_NOT_ALLOWED"); assertNoCompletionWork(loaded.calls, { noHash: true }); }); for (const inputCase of [ { name: "missing container", query: { container: undefined } }, { name: "invalid container type", query: { container: ["user-container"] } }, { name: "malformed container", query: { container: "Invalid_Container" } }, { name: "missing case reference", query: { tempcaseref: undefined } }, { name: "unsafe case-reference path separator", query: { tempcaseref: "CAS/123" } }, { name: "unsafe case-reference traversal segment", query: { tempcaseref: ".." } }, { name: "missing representation identifier", query: { repid: undefined } }, { name: "unsafe representation path separator", query: { repid: "rep\\file" } }, { name: "unsafe representation control character", query: { repid: "rep\u0000file" } } ]) { test(`createrepcompletemessage rejects ${inputCase.name} before authorization work`, async () => { const loaded = loadOrdinaryRepCompletion(); const request = ordinaryRepCompletionRequest(); const res = createRes(); request.query = { ...request.query, ...inputCase.query }; Object.keys(request.query).forEach((key) => { if (typeof request.query[key] === "undefined") delete request.query[key]; }); await loaded.mod.default(request, res); assert.strictEqual(res.state.statusCode, 400); assertNoCompletionWork(loaded.calls, { noHash: true }); }); } for (const sessionCase of [ { name: "missing NextAuth session", session: null }, { name: "session without user id", session: { user: {} } }, { name: "session with empty user id", session: { user: { id: "" } } }, { name: "session with non-string user id", session: { user: { id: 123 } } } ]) { test(`createrepcompletemessage rejects ${sessionCase.name} before HMAC processing`, async () => { const loaded = loadOrdinaryRepCompletion({ getSession: async () => sessionCase.session }); const res = createRes(); await loaded.mod.default(ordinaryRepCompletionRequest(), res); assert.strictEqual(res.state.statusCode, 403); assertNoCompletionWork(loaded.calls, { noHash: true }); }); } test("createrepcompletemessage rejects cross-container ownership before HMAC processing", async () => { const loaded = loadOrdinaryRepCompletion({ getSession: async () => ({ user: { id: "different-container" } }) }); const res = createRes(); await loaded.mod.default(ordinaryRepCompletionRequest(), res); assert.strictEqual(res.state.statusCode, 403); assertNoCompletionWork(loaded.calls, { noHash: true }); }); test("administrative cookie alone cannot authorize ordinary representation completion", async () => { const loaded = loadOrdinaryRepCompletion({ getSession: async () => null }); const res = createRes(); await loaded.mod.default( ordinaryRepCompletionRequest({ headers: { cookie: "pedw_admin_session=valid-admin-token" } }), res ); assert.strictEqual(res.state.statusCode, 403); assertNoCompletionWork(loaded.calls, { noHash: true }); }); test("previously valid HMAC cannot bypass a missing current ordinary session", async () => { const loaded = loadOrdinaryRepCompletion({ getSession: async () => null }); const res = createRes(); await loaded.mod.default(ordinaryRepCompletionRequest(), res); assert.strictEqual(res.state.statusCode, 403); assertNoCompletionWork(loaded.calls, { noHash: true }); }); for (const hashCase of [ { name: "missing HMAC", hash: undefined }, { name: "invalid HMAC", hash: "wrong" } ]) { test(`createrepcompletemessage rejects ${hashCase.name} without queue work`, async () => { const loaded = loadOrdinaryRepCompletion(); const request = ordinaryRepCompletionRequest(); const res = createRes(); if (typeof hashCase.hash === "undefined") delete request.query.hash; else request.query.hash = hashCase.hash; await loaded.mod.default(request, res); assert.strictEqual(res.state.statusCode, 400); assertNoCompletionWork(loaded.calls); }); } test("createrepcompletemessage accepts punctuation and preserves exact identifiers and response", async () => { const loaded = loadOrdinaryRepCompletion(); const request = ordinaryRepCompletionRequest(); const res = createRes(); await loaded.mod.default(request, res); assert.strictEqual(res.state.statusCode, 200); assert.deepStrictEqual(loaded.calls.queue, [ [ request.query.container, request.query.tempcaseref, request.query.repid ] ]); assert.deepStrictEqual( JSON.parse(JSON.stringify(res.state.jsonBody)), loaded.payload ); }); test("createrepcompletemessage preserves encoded HMAC compatibility", async () => { const loaded = loadOrdinaryRepCompletion({ hashAPIPath: (queryPath) => { loaded.calls.hash.push(queryPath); return queryPath.includes("repid=rep%20name%20%26%20notes.json") ? "&hash=expected" : "&hash=other"; } }); const request = ordinaryRepCompletionRequest(); request.query.repid = "rep name & notes.json"; const res = createRes(); await loaded.mod.default(request, res); assert.strictEqual(res.state.statusCode, 200); assert.strictEqual(loaded.calls.queue.length, 1); assert.strictEqual(loaded.calls.hash.length, 2); }); test("createrepcompletemessage dependency failure returns CREATE_REP_COMPLETE_MESSAGE_FAILED", async () => { const loaded = loadOrdinaryRepCompletion({ createRepCompleteMessage: async (...args) => { loaded.calls.queue.push(args); throw new Error("rep message failed"); } }); const res = createRes(); await loaded.mod.default(ordinaryRepCompletionRequest(), res); assert.strictEqual(res.state.statusCode, 400); assert.strictEqual( res.state.jsonBody.error.code, "CREATE_REP_COMPLETE_MESSAGE_FAILED" ); assert.strictEqual(loaded.calls.queue.length, 1); }); const loadAdminRepCompletion = (overrides = {}) => { const calls = { noStore: 0, readSession: 0, validate: 0, origin: 0, queue: [] }; const payload = { messageId: "admin-message" }; const mod = loadModule("pages/api/admin/createrepcompletemessage.js", { respondError: respondErrorMock, respondSuccess: respondSuccessMock, setAdminAuthNoStore: () => { calls.noStore += 1; }, readAdminSessionCookie: () => { calls.readSession += 1; return "admin-token"; }, validateAdminSession: () => { calls.validate += 1; return true; }, hasTrustedSameOrigin: () => { calls.origin += 1; return true; }, createRepCompleteMessage: async (...args) => { calls.queue.push(args); return payload; }, consoleLogger: () => {}, ...overrides }); return { mod, calls, payload }; }; const adminRepCompletionRequest = (overrides = {}) => ({ method: "POST", headers: { origin: "https://portal.example.test", cookie: "pedw_admin_session=admin-token" }, body: { container: "selected-user-container", tempcaseref: "CAS-12345-A1B2C3", repid: "2026-03-18-10:20:30_-_IP_-_Statement_-_Smith_Alex_rep.json" }, ...overrides }); test("admin representation completion rejects unsupported methods before authentication", async () => { const loaded = loadAdminRepCompletion(); const res = createRes(); await loaded.mod.default(adminRepCompletionRequest({ method: "GET" }), res); assert.strictEqual(res.state.statusCode, 405); assert.strictEqual(res.state.headers.allow, "POST"); assert.strictEqual(loaded.calls.noStore, 1); assert.strictEqual(loaded.calls.readSession, 0); assertNoCompletionWork(loaded.calls); }); for (const sessionCase of [ { name: "missing administrative session", token: null }, { name: "invalid administrative session", token: "invalid-token" }, { name: "expired administrative session", token: "expired-token" } ]) { test(`admin representation completion rejects ${sessionCase.name}`, async () => { const loaded = loadAdminRepCompletion({ readAdminSessionCookie: () => sessionCase.token, validateAdminSession: () => false }); const res = createRes(); await loaded.mod.default(adminRepCompletionRequest(), res); assert.strictEqual(res.state.statusCode, 403); assert.strictEqual(loaded.calls.origin, 0); assertNoCompletionWork(loaded.calls); }); } for (const originCase of [ { name: "missing Origin", headers: { cookie: "pedw_admin_session=admin-token" } }, { name: "cross-origin request", headers: { origin: "https://attacker.example.test", cookie: "pedw_admin_session=admin-token" } } ]) { test(`admin representation completion rejects ${originCase.name}`, async () => { const loaded = loadAdminRepCompletion({ hasTrustedSameOrigin: () => false }); const res = createRes(); await loaded.mod.default( adminRepCompletionRequest({ headers: originCase.headers }), res ); assert.strictEqual(res.state.statusCode, 403); assertNoCompletionWork(loaded.calls); }); } for (const inputCase of [ { name: "missing container", body: { container: undefined } }, { name: "malformed container", body: { container: "Invalid_Container" } }, { name: "missing case reference", body: { tempcaseref: undefined } }, { name: "unsafe case-reference segment", body: { tempcaseref: "CAS/123" } }, { name: "missing representation identifier", body: { repid: undefined } }, { name: "unsafe representation segment", body: { repid: "../rep.json" } } ]) { test(`admin representation completion rejects ${inputCase.name} without queue work`, async () => { const loaded = loadAdminRepCompletion(); const request = adminRepCompletionRequest(); const res = createRes(); request.body = { ...request.body, ...inputCase.body }; Object.keys(request.body).forEach((key) => { if (typeof request.body[key] === "undefined") delete request.body[key]; }); await loaded.mod.default(request, res); assert.strictEqual(res.state.statusCode, 400); assertNoCompletionWork(loaded.calls); }); } test("ordinary NextAuth session alone cannot authorize admin representation completion", async () => { const loaded = loadAdminRepCompletion({ readAdminSessionCookie: () => null, validateAdminSession: () => false, getSession: async () => ({ user: { id: "selected-user-container" } }) }); const res = createRes(); await loaded.mod.default(adminRepCompletionRequest(), res); assert.strictEqual(res.state.statusCode, 403); assertNoCompletionWork(loaded.calls); }); test("admin representation completion preserves cross-container identifiers and response", async () => { const loaded = loadAdminRepCompletion(); const request = adminRepCompletionRequest(); const res = createRes(); await loaded.mod.default(request, res); assert.strictEqual(res.state.statusCode, 200); assert.deepStrictEqual(loaded.calls.queue, [ [request.body.container, request.body.tempcaseref, request.body.repid] ]); assert.deepStrictEqual( JSON.parse(JSON.stringify(res.state.jsonBody)), loaded.payload ); }); test("admin representation completion accepts legitimate filename punctuation", async () => { const loaded = loadAdminRepCompletion(); const request = adminRepCompletionRequest(); request.body.repid = "rep name_-_notes.v2 (final)!.json"; const res = createRes(); await loaded.mod.default(request, res); assert.strictEqual(res.state.statusCode, 200); assert.strictEqual(loaded.calls.queue.length, 1); }); const loadBlobListProxy = ({ session, ...overrides } = {}) => { const calls = { session: [], options: [], token: 0, hash: [], headers: [], axios: [] }; const options = { id: "pedw-blob-list-options" }; const payload = { value: [{ id: "blob-1" }] }; const resolvedSession = typeof session === "undefined" ? { user: { id: "user-container" } } : session; const mod = loadModule("pages/api/file/getbloblistproxy.js", { respondError: respondErrorMock, respondSuccess: respondSuccessMock, authOptions: (req, res) => { calls.options.push([req, res]); return options; }, getServerSession: async (...args) => { calls.session.push(args); return resolvedSession; }, getToken: async () => { calls.token += 1; return { access_token: "synthetic-token" }; }, azureHeaders: (accessToken) => { calls.headers.push(accessToken); return { headers: { Authorization: "Bearer synthetic-token" } }; }, hashAPIPath: (queryPath) => { calls.hash.push(queryPath); return "&hash=synthetic-hash"; }, consoleLogger: () => {}, process: { env: { API_ROOT: "http://example.com" } }, axios: { get: async (...args) => { calls.axios.push(args); return { data: payload }; } }, ...overrides }); return { mod, calls, options, payload }; }; const blobListProxyRequest = (overrides = {}) => ({ method: "GET", query: { container: "user-container", casefolderID: "CASE/1" }, headers: {}, ...overrides }); const assertNoBlobListProxyWork = (calls) => { assert.strictEqual(calls.token, 0); assert.strictEqual(calls.hash.length, 0); assert.strictEqual(calls.headers.length, 0); assert.strictEqual(calls.axios.length, 0); }; test("getbloblistproxy rejects unsupported methods before authentication or proxy work", async () => { const loaded = loadBlobListProxy(); const res = createRes(); await loaded.mod.default(blobListProxyRequest({ method: "POST" }), res); assert.strictEqual(res.state.statusCode, 405); assert.strictEqual(res.state.jsonBody.error.code, "METHOD_NOT_ALLOWED"); assert.strictEqual(loaded.calls.options.length, 0); assert.strictEqual(loaded.calls.session.length, 0); assertNoBlobListProxyWork(loaded.calls); }); test("getbloblistproxy rejects a missing container before authentication or proxy work", async () => { const loaded = loadBlobListProxy(); const res = createRes(); await loaded.mod.default( blobListProxyRequest({ query: { casefolderID: "CASE/1" } }), res ); assert.strictEqual(res.state.statusCode, 400); assert.strictEqual(res.state.jsonBody.error.code, "MISSING_REQUIRED_QUERY"); assert.strictEqual(loaded.calls.options.length, 0); assert.strictEqual(loaded.calls.session.length, 0); assertNoBlobListProxyWork(loaded.calls); }); test("getbloblistproxy rejects a malformed container before authentication or proxy work", async () => { const loaded = loadBlobListProxy(); const res = createRes(); await loaded.mod.default( blobListProxyRequest({ query: { container: "Invalid_Container", casefolderID: "CASE/1" } }), res ); assert.strictEqual(res.state.statusCode, 400); assert.strictEqual(res.state.jsonBody.error.code, "INVALID_REQUEST"); assert.strictEqual(loaded.calls.options.length, 0); assert.strictEqual(loaded.calls.session.length, 0); assertNoBlobListProxyWork(loaded.calls); }); test("getbloblistproxy rejects a missing casefolderID before authentication or proxy work", async () => { const loaded = loadBlobListProxy(); const res = createRes(); await loaded.mod.default( blobListProxyRequest({ query: { container: "user-container" } }), res ); assert.strictEqual(res.state.statusCode, 400); assert.strictEqual(res.state.jsonBody.error.code, "MISSING_REQUIRED_QUERY"); assert.strictEqual(loaded.calls.options.length, 0); assert.strictEqual(loaded.calls.session.length, 0); assertNoBlobListProxyWork(loaded.calls); }); for (const sessionCase of [ { name: "missing session", session: null }, { name: "session without user id", session: { user: {} } }, { name: "session with empty user id", session: { user: { id: "" } } }, { name: "session with non-string user id", session: { user: { id: 123 } } } ]) { test(`getbloblistproxy rejects ${sessionCase.name} before proxy work`, async () => { const loaded = loadBlobListProxy({ session: sessionCase.session }); const res = createRes(); await loaded.mod.default(blobListProxyRequest(), res); assert.strictEqual(res.state.statusCode, 403); assert.strictEqual(res.state.jsonBody.error.code, "FORBIDDEN"); assertNoBlobListProxyWork(loaded.calls); }); } test("getbloblistproxy rejects cross-container access before proxy work", async () => { const loaded = loadBlobListProxy({ session: { user: { id: "different-container" } } }); const res = createRes(); await loaded.mod.default(blobListProxyRequest(), res); assert.strictEqual(res.state.statusCode, 403); assert.strictEqual(res.state.jsonBody.error.code, "FORBIDDEN"); assertNoBlobListProxyWork(loaded.calls); }); test("getbloblistproxy preserves signed path, bearer header and response for owning user", async () => { const loaded = loadBlobListProxy(); const req = blobListProxyRequest(); const res = createRes(); await loaded.mod.default(req, res); const expectedPath = "/api/file/getbloblist?container=user-container&casefolderID=CASE%2F1"; assert.strictEqual(res.state.statusCode, 200); assert.deepStrictEqual(loaded.calls.options, [[req, res]]); assert.deepStrictEqual(loaded.calls.session, [[req, res, loaded.options]]); assert.strictEqual(loaded.calls.token, 1); assert.deepStrictEqual(loaded.calls.hash, [expectedPath]); assert.deepStrictEqual(loaded.calls.headers, ["synthetic-token"]); assert.strictEqual(loaded.calls.axios.length, 1); assert.strictEqual( loaded.calls.axios[0][0], `http://example.com${expectedPath}&hash=synthetic-hash` ); assert.deepStrictEqual(loaded.calls.axios[0][1], { headers: { Authorization: "Bearer synthetic-token" } }); assert.deepStrictEqual( JSON.parse(JSON.stringify(res.state.jsonBody)), loaded.payload ); }); test("getbloblistproxy dependency failure returns GET_BLOB_LIST_PROXY_FAILED", async () => { const loaded = loadBlobListProxy({ axios: { get: async (...args) => { loaded.calls.axios.push(args); throw new Error("proxy failed"); } } }); const res = createRes(); await loaded.mod.default(blobListProxyRequest(), res); assert.strictEqual(res.state.statusCode, 400); assert.strictEqual( res.state.jsonBody.error.code, "GET_BLOB_LIST_PROXY_FAILED" ); assert.strictEqual(loaded.calls.token, 1); assert.strictEqual(loaded.calls.hash.length, 1); assert.strictEqual(loaded.calls.headers.length, 1); assert.strictEqual(loaded.calls.axios.length, 1); }); const loadOrdinaryRepsProxy = (overrides = {}) => { const calls = { session: [], options: [], token: 0, hash: [], axios: [] }; const options = { id: "pedw-representation-options" }; const payload = { "@odata.count": 1, value: [{ id: "r1" }] }; const mod = loadModule("pages/api/file/getrepsblobproxy.js", { respondError: respondErrorMock, respondSuccess: respondSuccessMock, authOptions: (req, res) => { calls.options.push([req, res]); return options; }, getServerSession: async (...args) => { calls.session.push(args); return { user: { id: "user-container" } }; }, getToken: async () => { calls.token += 1; return { access_token: "tok" }; }, azureHeaders: () => ({ headers: { Authorization: "Bearer tok" } }), hashAPIPath: (queryPath) => { calls.hash.push(queryPath); return "&hash=abc"; }, consoleLogger: () => {}, BASE_URL: "http://example.com", process: { env: { API_ROOT: "http://example.com", NEXTAUTH_URL: "http://example.com" } }, axios: { get: async (...args) => { calls.axios.push(args); return { data: payload }; } }, ...overrides }); return { mod, calls, options, payload }; }; const ordinaryRepsRequest = (overrides = {}) => ({ method: "GET", query: { container: "user-container" }, headers: {}, ...overrides }); const assertNoProxyWork = (calls) => { assert.strictEqual(calls.token, 0); assert.strictEqual(calls.hash.length, 0); assert.strictEqual(calls.axios.length, 0); }; for (const sessionCase of [ { name: "missing session", session: null }, { name: "session without user id", session: { user: {} } }, { name: "session with empty user id", session: { user: { id: "" } } }, { name: "session with non-string user id", session: { user: { id: 123 } } } ]) { test(`getrepsblobproxy rejects ${sessionCase.name} before proxy work`, async () => { const loaded = loadOrdinaryRepsProxy({ getServerSession: async () => sessionCase.session }); const res = createRes(); await loaded.mod.default(ordinaryRepsRequest(), res); assert.strictEqual(res.state.statusCode, 403); assert.strictEqual(res.state.jsonBody.error.code, "FORBIDDEN"); assertNoProxyWork(loaded.calls); }); } test("getrepsblobproxy rejects cross-container access before proxy work", async () => { const loaded = loadOrdinaryRepsProxy({ getServerSession: async () => ({ user: { id: "different-container" } }) }); const res = createRes(); await loaded.mod.default(ordinaryRepsRequest(), res); assert.strictEqual(res.state.statusCode, 403); assert.strictEqual(res.state.jsonBody.error.code, "FORBIDDEN"); assertNoProxyWork(loaded.calls); }); test("administrative cookie alone cannot authorize getrepsblobproxy", async () => { const loaded = loadOrdinaryRepsProxy({ getServerSession: async () => null }); const res = createRes(); await loaded.mod.default( ordinaryRepsRequest({ headers: { cookie: "pedw_admin_session=valid-admin-token" } }), res ); assert.strictEqual(res.state.statusCode, 403); assertNoProxyWork(loaded.calls); }); test("getrepsblobproxy rejects unsupported methods before proxy work", async () => { const loaded = loadOrdinaryRepsProxy(); const res = createRes(); await loaded.mod.default(ordinaryRepsRequest({ method: "POST" }), res); assert.strictEqual(res.state.statusCode, 405); assert.strictEqual(res.state.jsonBody.error.code, "METHOD_NOT_ALLOWED"); assertNoProxyWork(loaded.calls); }); for (const containerCase of [ { name: "missing container", query: {} }, { name: "empty container", query: { container: "" } }, { name: "null container", query: { container: null } }, { name: "non-string container", query: { container: ["user-container"] } }, { name: "malformed container", query: { container: "Invalid_Container" } } ]) { test(`getrepsblobproxy rejects ${containerCase.name} before proxy work`, async () => { const loaded = loadOrdinaryRepsProxy(); const res = createRes(); await loaded.mod.default( ordinaryRepsRequest({ query: containerCase.query }), res ); assert.strictEqual(res.state.statusCode, 400); assertNoProxyWork(loaded.calls); }); } test("getrepsblobproxy preserves signed path and response for owning user", async () => { const loaded = loadOrdinaryRepsProxy(); const req = ordinaryRepsRequest({ headers: { cookie: "next-auth.session-token=test-session" } }); const res = createRes(); await loaded.mod.default(req, res); assert.strictEqual(res.state.statusCode, 200); assert.deepStrictEqual(loaded.calls.options, [[req, res]]); assert.deepStrictEqual(loaded.calls.session, [[req, res, loaded.options]]); assert.strictEqual(loaded.calls.token, 1); assert.deepStrictEqual(loaded.calls.hash, [ "/api/file/getrepsblob?container=user-container" ]); assert.strictEqual( loaded.calls.axios[0][0], "http://example.com/api/file/getrepsblob?container=user-container&hash=abc" ); assert.deepStrictEqual( JSON.parse(JSON.stringify(loaded.calls.axios[0][1])), { headers: { Authorization: "Bearer tok", Cookie: "next-auth.session-token=test-session" }, maxRedirects: 0 } ); assert.deepStrictEqual( JSON.parse(JSON.stringify(res.state.jsonBody)), loaded.payload ); }); test("getrepsblobproxy dependency failure returns GET_REPS_BLOB_PROXY_FAILED", async () => { const loaded = loadOrdinaryRepsProxy({ axios: { get: async (...args) => { loaded.calls.axios.push(args); throw new Error("representation proxy failed"); } } }); const res = createRes(); await loaded.mod.default(ordinaryRepsRequest(), res); assert.strictEqual(res.state.statusCode, 400); assert.strictEqual( res.state.jsonBody.error.code, "GET_REPS_BLOB_PROXY_FAILED" ); assert.strictEqual(loaded.calls.token, 1); assert.strictEqual(loaded.calls.hash.length, 1); assert.strictEqual(loaded.calls.axios.length, 1); }); const loadAdminRepsProxy = (overrides = {}) => { const calls = { token: 0, hash: [], axios: [] }; const payload = { "@odata.count": 1, value: [{ id: "admin-r1" }] }; const mod = loadModule("pages/api/admin/getrepsblobproxy.js", { respondError: respondErrorMock, respondSuccess: respondSuccessMock, ADMIN_SESSION_COOKIE: "pedw_admin_session", readAdminSessionCookie: () => "admin-token", validateAdminSession: () => true, hasTrustedSameOrigin: () => true, setAdminAuthNoStore: () => {}, getToken: async () => { calls.token += 1; return { access_token: "tok" }; }, azureHeaders: () => ({ headers: { Authorization: "Bearer tok" } }), hashAPIPath: (queryPath) => { calls.hash.push(queryPath); return "&hash=abc"; }, consoleLogger: () => {}, BASE_URL: "http://example.com", process: { env: { API_ROOT: "http://example.com", NEXTAUTH_URL: "http://example.com" } }, axios: { get: async (...args) => { calls.axios.push(args); return { data: payload }; } }, ...overrides }); return { mod, calls, payload }; }; const adminRepsRequest = (overrides = {}) => ({ method: "POST", headers: { origin: "https://portal.example.test" }, body: { container: "selected-user-container" }, ...overrides }); for (const adminSessionCase of [ { name: "missing administrative session", overrides: { readAdminSessionCookie: () => null, validateAdminSession: () => false } }, { name: "invalid administrative session", overrides: { validateAdminSession: () => false } }, { name: "expired administrative session", overrides: { validateAdminSession: () => false } } ]) { test(`admin getrepsblobproxy rejects ${adminSessionCase.name} before proxy work`, async () => { const loaded = loadAdminRepsProxy(adminSessionCase.overrides); const res = createRes(); await loaded.mod.default(adminRepsRequest(), res); assert.strictEqual(res.state.statusCode, 403); assert.strictEqual(res.state.jsonBody.error.code, "FORBIDDEN"); assertNoProxyWork(loaded.calls); }); } for (const originCase of ["missing Origin", "cross-origin request"]) { test(`admin getrepsblobproxy rejects ${originCase} before proxy work`, async () => { const loaded = loadAdminRepsProxy({ hasTrustedSameOrigin: () => false }); const req = adminRepsRequest(); if (originCase === "missing Origin") delete req.headers.origin; if (originCase === "cross-origin request") { req.headers.origin = "https://attacker.example"; } const res = createRes(); await loaded.mod.default(req, res); assert.strictEqual(res.state.statusCode, 403); assertNoProxyWork(loaded.calls); }); } for (const containerCase of [ { name: "missing container", body: {} }, { name: "malformed container", body: { container: "Invalid_Container" } } ]) { test(`admin getrepsblobproxy rejects ${containerCase.name} before proxy work`, async () => { const loaded = loadAdminRepsProxy(); const res = createRes(); await loaded.mod.default( adminRepsRequest({ body: containerCase.body }), res ); assert.strictEqual(res.state.statusCode, 400); assert.strictEqual(res.state.jsonBody.error.code, "INVALID_REQUEST"); assertNoProxyWork(loaded.calls); }); } test("admin getrepsblobproxy rejects unsupported methods before proxy work", async () => { const loaded = loadAdminRepsProxy(); const res = createRes(); await loaded.mod.default(adminRepsRequest({ method: "GET" }), res); assert.strictEqual(res.state.statusCode, 405); assert.strictEqual(res.state.jsonBody.error.code, "METHOD_NOT_ALLOWED"); assertNoProxyWork(loaded.calls); }); test("ordinary NextAuth session alone cannot authorize admin getrepsblobproxy", async () => { const loaded = loadAdminRepsProxy({ readAdminSessionCookie: () => null, validateAdminSession: () => false, getSession: async () => ({ user: { id: "selected-user-container" } }) }); const res = createRes(); await loaded.mod.default(adminRepsRequest(), res); assert.strictEqual(res.state.statusCode, 403); assertNoProxyWork(loaded.calls); }); test("admin getrepsblobproxy preserves cross-container signed path and response", async () => { const loaded = loadAdminRepsProxy(); const res = createRes(); await loaded.mod.default(adminRepsRequest(), res); assert.strictEqual(res.state.statusCode, 200); assert.strictEqual(loaded.calls.token, 1); assert.deepStrictEqual(loaded.calls.hash, [ "/api/file/getrepsblob?container=selected-user-container" ]); assert.strictEqual( loaded.calls.axios[0][0], "http://example.com/api/file/getrepsblob?container=selected-user-container&hash=abc" ); assert.deepStrictEqual( JSON.parse(JSON.stringify(loaded.calls.axios[0][1])), { headers: { Authorization: "Bearer tok", Cookie: "pedw_admin_session=admin-token" }, maxRedirects: 0 } ); assert.deepStrictEqual( JSON.parse(JSON.stringify(res.state.jsonBody)), loaded.payload ); }); test("admin storage refresh uses dedicated POST proxy while ordinary callers remain unchanged", () => { const rootDir = path.resolve(__dirname, "..", ".."); const adminSource = fs.readFileSync( path.join(rootDir, "components/admin/tabs/storage.js"), "utf8" ); const topThreeSource = fs.readFileSync( path.join(rootDir, "components/myportal/topthree_reps.js"), "utf8" ); const viewAllSource = fs.readFileSync( path.join(rootDir, "components/myportal/viewall.js"), "utf8" ); assert.match(adminSource, /fetch\("\/api\/admin\/getrepsblobproxy"/); assert.match( adminSource, /fetch\("\/api\/admin\/getrepsblobproxy"[\s\S]*method:\s*"POST"[\s\S]*JSON\.stringify\(\{ container: containerID \}\)/ ); assert.doesNotMatch(adminSource, /getRepsFromBlobProxy/); assert.match(topThreeSource, /getRepsFromBlobProxy\(containerID\)/); assert.match(viewAllSource, /getRepsFromBlobProxy\(containerID\)/); }); test("admin getrepsblobproxy rejects untrusted internal destination before proxy work", async () => { const loaded = loadAdminRepsProxy({ BASE_URL: "https://attacker.example" }); const res = createRes(); await loaded.mod.default(adminRepsRequest(), res); assert.strictEqual(res.state.statusCode, 403); assert.strictEqual(res.state.jsonBody.error.code, "FORBIDDEN"); assertNoProxyWork(loaded.calls); }); test("admin storage completion uses dedicated POST route while ordinary completion remains unchanged", () => { const rootDir = path.resolve(__dirname, "..", ".."); const adminSource = fs.readFileSync( path.join(rootDir, "components/admin/tabs/storage.js"), "utf8" ); const ordinarySource = fs.readFileSync( path.join( rootDir, "components/case/representation/representationComplete.js" ), "utf8" ); assert.match( adminSource, /fetch\(\s*"\/api\/admin\/createrepcompletemessage",\s*\{\s*method:\s*"POST"/ ); assert.match(adminSource, /container:\s*c\.container/); assert.match( adminSource, /tempcaseref:\s*folder\.repJsonBlob\.name\.split\(\s*"\/"\s*\)\[0\]/ ); assert.match( adminSource, /repid:\s*folder\.repJsonBlob\.name\.split\(\s*"\/"\s*\)\[1\]/ ); assert.match(adminSource, /if \(!response\.ok\)/); assert.doesNotMatch(adminSource, /sendRepCompleteMessage/); assert.match(ordinarySource, /sendRepCompleteMessage\(/); }); const loadAwaitingSubmissionProxy = (overrides = {}) => { const calls = { session: [], options: [], token: 0, hash: [], axios: [] }; const options = { id: "pedw-awaiting-submission-options" }; const payload = { "@odata.count": 1, value: [{ pinswg_name: "TMP-SYNTHETIC" }], case: [{ ticketnumber: "TMP-SYNTHETIC" }] }; const mod = loadModule( "pages/api/file/getawaitingsubmissionfromblobproxy.js", { respondError: respondErrorMock, respondSuccess: respondSuccessMock, authOptions: (req, res) => { calls.options.push([req, res]); return options; }, getServerSession: async (...args) => { calls.session.push(args); return { user: { id: "user-container" } }; }, getToken: async () => { calls.token += 1; return { access_token: "tok" }; }, azureHeaders: () => ({ headers: { Authorization: "Bearer tok" } }), hashAPIPath: (queryPath) => { calls.hash.push(queryPath); return "&hash=abc"; }, consoleLogger: () => {}, process: { env: { API_ROOT: "http://example.com" } }, axios: { get: async (...args) => { calls.axios.push(args); return { data: payload }; } }, ...overrides } ); return { mod, calls, options, payload }; }; const awaitingSubmissionRequest = (overrides = {}) => ({ method: "GET", query: { container: "user-container" }, headers: {}, ...overrides }); for (const sessionCase of [ { name: "missing session", session: null }, { name: "session without user id", session: { user: {} } }, { name: "session with empty user id", session: { user: { id: "" } } }, { name: "session with non-string user id", session: { user: { id: 123 } } } ]) { test(`getawaitingsubmissionfromblobproxy rejects ${sessionCase.name} before proxy work`, async () => { const loaded = loadAwaitingSubmissionProxy({ getServerSession: async () => sessionCase.session }); const res = createRes(); await loaded.mod.default(awaitingSubmissionRequest(), res); assert.strictEqual(res.state.statusCode, 403); assert.strictEqual(res.state.jsonBody.error.code, "FORBIDDEN"); assertNoProxyWork(loaded.calls); }); } test("getawaitingsubmissionfromblobproxy rejects cross-container access before proxy work", async () => { const loaded = loadAwaitingSubmissionProxy({ getServerSession: async () => ({ user: { id: "different-container" } }) }); const res = createRes(); await loaded.mod.default(awaitingSubmissionRequest(), res); assert.strictEqual(res.state.statusCode, 403); assert.strictEqual(res.state.jsonBody.error.code, "FORBIDDEN"); assertNoProxyWork(loaded.calls); }); test("getawaitingsubmissionfromblobproxy rejects unsupported methods before proxy work", async () => { const loaded = loadAwaitingSubmissionProxy(); const res = createRes(); await loaded.mod.default( awaitingSubmissionRequest({ method: "POST" }), res ); assert.strictEqual(res.state.statusCode, 405); assert.strictEqual(res.state.jsonBody.error.code, "METHOD_NOT_ALLOWED"); assertNoProxyWork(loaded.calls); }); for (const containerCase of [ { name: "missing container", query: {} }, { name: "empty container", query: { container: "" } }, { name: "null container", query: { container: null } }, { name: "non-string container", query: { container: ["user-container"] } }, { name: "malformed container", query: { container: "Invalid_Container" } } ]) { test(`getawaitingsubmissionfromblobproxy rejects ${containerCase.name} before proxy work`, async () => { const loaded = loadAwaitingSubmissionProxy(); const res = createRes(); await loaded.mod.default( awaitingSubmissionRequest({ query: containerCase.query }), res ); assert.strictEqual(res.state.statusCode, 400); assertNoProxyWork(loaded.calls); }); } test("getawaitingsubmissionfromblobproxy preserves signed path and response for owning user", async () => { const loaded = loadAwaitingSubmissionProxy(); const req = awaitingSubmissionRequest(); const res = createRes(); await loaded.mod.default(req, res); assert.strictEqual(res.state.statusCode, 200); assert.deepStrictEqual(loaded.calls.options, [[req, res]]); assert.deepStrictEqual(loaded.calls.session, [[req, res, loaded.options]]); assert.strictEqual(loaded.calls.token, 1); assert.deepStrictEqual(loaded.calls.hash, [ "/api/file/getawaitingsubmissionfromblob?container=user-container" ]); assert.strictEqual( loaded.calls.axios[0][0], "http://example.com/api/file/getawaitingsubmissionfromblob?container=user-container&hash=abc" ); assert.deepStrictEqual( JSON.parse(JSON.stringify(res.state.jsonBody)), loaded.payload ); }); test("getawaitingsubmissionfromblobproxy dependency failure returns GET_AWAITING_SUBMISSION_PROXY_FAILED", async () => { const loaded = loadAwaitingSubmissionProxy({ axios: { get: async (...args) => { loaded.calls.axios.push(args); throw new Error("awaiting failed"); } } }); const res = createRes(); await loaded.mod.default(awaitingSubmissionRequest(), res); assert.strictEqual(res.state.statusCode, 400); assert.strictEqual( res.state.jsonBody.error.code, "GET_AWAITING_SUBMISSION_PROXY_FAILED" ); assert.strictEqual(loaded.calls.token, 1); assert.strictEqual(loaded.calls.hash.length, 1); assert.strictEqual(loaded.calls.axios.length, 1); }); const loadAppealCompletionProxy = ({ session, ...overrides } = {}) => { const calls = { options: [], session: [], token: 0, hash: [], headers: [], axios: [] }; const options = { id: "appeal-completion-proxy-options" }; const payload = { status: "success" }; const resolvedSession = typeof session === "undefined" ? { user: { id: "owner-container" } } : session; const mod = loadModule( "pages/api/file/createappealcompletemessageproxy_api.js", { nextConnect: createNextConnectMock(), middleware: () => {}, respondError: respondErrorMock, respondSuccess: respondSuccessMock, authOptions: (req, res) => { calls.options.push([req, res]); return options; }, getServerSession: async (...args) => { calls.session.push(args); return resolvedSession; }, getToken: async () => { calls.token += 1; return { access_token: "synthetic-token" }; }, azureHeaders: (token) => { calls.headers.push(token); return { headers: { Authorization: `Bearer ${token}` } }; }, hashAPIPath: (queryPath) => { calls.hash.push(queryPath); return "&hash=synthetic-hash"; }, consoleLogger: () => {}, process: { env: { API_ROOT: "http://example.com" } }, axios: { get: async (...args) => { calls.axios.push(args); return { data: payload }; } }, ...overrides } ); return { mod, calls, options, payload }; }; const appealCompletionProxyRequest = (overrides = {}) => ({ query: { container: "owner-container", tempcaseref: "TMP-CASE-1" }, headers: { cookie: "next-auth.session-token=synthetic-session" }, ...overrides }); const assertNoAppealCompletionProxyWork = (calls) => { assert.strictEqual(calls.token, 0); assert.strictEqual(calls.hash.length, 0); assert.strictEqual(calls.headers.length, 0); assert.strictEqual(calls.axios.length, 0); }; for (const sessionCase of [ { name: "missing session", session: null }, { name: "session without user id", session: { user: {} } }, { name: "session with empty user id", session: { user: { id: "" } } }, { name: "session with non-string user id", session: { user: { id: 123 } } }, { name: "cross-container session", session: { user: { id: "foreign-container" } } } ]) { test(`createappealcompletemessageproxy rejects ${sessionCase.name} before token, signing or submission`, async () => { const loaded = loadAppealCompletionProxy({ session: sessionCase.session }); const res = createRes(); await loaded.mod.default.handler(appealCompletionProxyRequest(), res); assert.strictEqual(res.state.statusCode, 403); assert.strictEqual(res.state.jsonBody.error.code, "FORBIDDEN"); assertNoAppealCompletionProxyWork(loaded.calls); }); } for (const inputCase of [ { name: "invalid container", query: { container: "Invalid_Container", tempcaseref: "TMP-CASE-1" } }, { name: "missing temporary case reference", query: { container: "owner-container" }, code: "MISSING_REQUIRED_QUERY" }, { name: "malformed temporary case reference", query: { container: "owner-container", tempcaseref: "../foreign" } }, { name: "array temporary case reference", query: { container: "owner-container", tempcaseref: ["TMP-CASE-1"] }, code: "MISSING_REQUIRED_QUERY" } ]) { test(`createappealcompletemessageproxy rejects ${inputCase.name} before authentication or submission`, async () => { const loaded = loadAppealCompletionProxy(); const res = createRes(); await loaded.mod.default.handler( appealCompletionProxyRequest({ query: inputCase.query }), res ); assert.strictEqual(res.state.statusCode, 400); assert.strictEqual( res.state.jsonBody.error.code, inputCase.code || "INVALID_REQUEST" ); assert.strictEqual(loaded.calls.options.length, 0); assert.strictEqual(loaded.calls.session.length, 0); assertNoAppealCompletionProxyWork(loaded.calls); }); } test("createappealcompletemessageproxy preserves owner submission and forwards the authenticated cookie", async () => { const loaded = loadAppealCompletionProxy(); const req = appealCompletionProxyRequest(); const res = createRes(); await loaded.mod.default.handler(req, res); const expectedPath = "/api/file/createappealcompletemessage_api?container=owner-container&tempcaseref=TMP-CASE-1"; assert.strictEqual(res.state.statusCode, 200); assert.deepStrictEqual(loaded.calls.options, [[req, res]]); assert.deepStrictEqual(loaded.calls.session, [[req, res, loaded.options]]); assert.strictEqual(loaded.calls.token, 1); assert.deepStrictEqual(loaded.calls.hash, [expectedPath]); assert.deepStrictEqual(loaded.calls.headers, ["synthetic-token"]); assert.strictEqual(loaded.calls.axios.length, 1); assert.strictEqual( loaded.calls.axios[0][0], `http://example.com${expectedPath}&hash=synthetic-hash` ); assert.deepStrictEqual( JSON.parse(JSON.stringify(loaded.calls.axios[0][1])), { headers: { Authorization: "Bearer synthetic-token", cookie: "next-auth.session-token=synthetic-session" } } ); assert.deepStrictEqual( JSON.parse(JSON.stringify(res.state.jsonBody)), loaded.payload ); }); const loadAppealCompletionDirect = ({ session, ...overrides } = {}) => { const calls = { options: [], session: [], hash: [], progress: [], download: [], createBlob: [], caseBlob: [], account: [], queue: [] }; const options = { id: "appeal-completion-direct-options" }; const resolvedSession = typeof session === "undefined" ? { user: { id: "owner-container" } } : session; const progress = { pinswg_siteaddressline1: "Line 1", pinswg_siteaddressline2: "", pinswg_siteaddresstown: "Town", pinswg_siteaddresscounty: "County", pinswg_siteaddresspostcode: "CF1 1AA", pinswg_lpaapplicationreference: "LPA-1", pinswg_developmentdescription: "Description", pinswg_name: "draft-name" }; const caseObject = { "customerid_contact@odata.bind": "/contacts(00000000-0000-0000-0000-000000000001)" }; const mod = loadModule( "pages/api/file/createappealcompletemessage_api.js", { nextConnect: createNextConnectMock(), middleware: () => {}, respondError: respondErrorMock, respondSuccess: respondSuccessMock, authOptions: (req, res) => { calls.options.push([req, res]); return options; }, getServerSession: async (...args) => { calls.session.push(args); return resolvedSession; }, hashAPIPath: (queryPath) => { calls.hash.push(queryPath); return "&hash=expected"; }, getProgressBlobs: async (...args) => { calls.progress.push(args); return { path: "TMP-CASE-1/progress.json" }; }, downloadProgressFile: async (...args) => { calls.download.push(args); return args[1].endsWith("_case.json") ? { ...caseObject } : { ...progress }; }, createBlob: async (...args) => { calls.createBlob.push(args); return {}; }, getCaseBlob: async (...args) => { calls.caseBlob.push(args); return {}; }, updateCrmContact: async (...args) => { calls.account.push(args); return {}; }, createCaseCompleteMessage: async (...args) => { calls.queue.push(args); return {}; }, consoleLogger: () => {}, _: { isEmpty: (value) => value === undefined || value === null || value === "" }, ...overrides } ); return { mod, calls, options }; }; const appealCompletionDirectRequest = (overrides = {}) => ({ query: { container: "owner-container", tempcaseref: "TMP-CASE-1", inv: "846040001", hash: "expected" }, ...overrides }); const assertNoAppealCompletionEffects = (calls) => { assert.strictEqual(calls.progress.length, 0); assert.strictEqual(calls.download.length, 0); assert.strictEqual(calls.createBlob.length, 0); assert.strictEqual(calls.caseBlob.length, 0); assert.strictEqual(calls.account.length, 0); assert.strictEqual(calls.queue.length, 0); }; for (const sessionCase of [ { name: "missing session", session: null }, { name: "session without user id", session: { user: {} } }, { name: "session with empty user id", session: { user: { id: "" } } }, { name: "session with non-string user id", session: { user: { id: 123 } } }, { name: "cross-container session", session: { user: { id: "foreign-container" } } } ]) { test(`createappealcompletemessage_api blocks direct bypass with ${sessionCase.name} before HMAC or side effects`, async () => { const loaded = loadAppealCompletionDirect({ session: sessionCase.session }); const res = createRes(); await loaded.mod.default.handler(appealCompletionDirectRequest(), res); assert.strictEqual(res.state.statusCode, 403); assert.strictEqual(res.state.jsonBody.error.code, "FORBIDDEN"); assert.strictEqual(loaded.calls.hash.length, 0); assertNoAppealCompletionEffects(loaded.calls); }); } for (const inputCase of [ { name: "invalid container", query: { container: "Invalid_Container", tempcaseref: "TMP-CASE-1", hash: "expected" } }, { name: "missing temporary case reference", query: { container: "owner-container", hash: "expected" }, code: "MISSING_REQUIRED_QUERY" }, { name: "malformed temporary case reference", query: { container: "owner-container", tempcaseref: "../foreign", hash: "expected" } } ]) { test(`createappealcompletemessage_api rejects ${inputCase.name} before authentication or side effects`, async () => { const loaded = loadAppealCompletionDirect(); const res = createRes(); await loaded.mod.default.handler( appealCompletionDirectRequest({ query: inputCase.query }), res ); assert.strictEqual(res.state.statusCode, 400); assert.strictEqual( res.state.jsonBody.error.code, inputCase.code || "INVALID_REQUEST" ); assert.strictEqual(loaded.calls.options.length, 0); assert.strictEqual(loaded.calls.session.length, 0); assert.strictEqual(loaded.calls.hash.length, 0); assertNoAppealCompletionEffects(loaded.calls); }); } for (const hashCase of [ { name: "missing HMAC", hash: undefined }, { name: "invalid HMAC", hash: "wrong" } ]) { test(`createappealcompletemessage_api rejects ${hashCase.name} after owner authentication and before side effects`, async () => { const loaded = loadAppealCompletionDirect(); const request = appealCompletionDirectRequest(); const res = createRes(); if (typeof hashCase.hash === "undefined") delete request.query.hash; else request.query.hash = hashCase.hash; await loaded.mod.default.handler(request, res); assert.strictEqual(res.state.statusCode, 400); assertNoAppealCompletionEffects(loaded.calls); }); } for (const resourceCase of [ { name: "another container", query: { container: "other-container", tempcaseref: "TMP-CASE-1", inv: "846040001", hash: "expected" }, session: { user: { id: "other-container" } } }, { name: "another draft folder", query: { container: "owner-container", tempcaseref: "TMP-CASE-2", inv: "846040001", hash: "expected" } } ]) { test(`createappealcompletemessage_api rejects an HMAC issued for ${resourceCase.name}`, async () => { const loaded = loadAppealCompletionDirect({ session: resourceCase.session, hashAPIPath: (queryPath) => { loaded.calls.hash.push(queryPath); return queryPath === "/api/file/createappealcompletemessage_api?container=owner-container&tempcaseref=TMP-CASE-1" ? "&hash=expected" : "&hash=other"; } }); const res = createRes(); await loaded.mod.default.handler( appealCompletionDirectRequest({ query: resourceCase.query }), res ); assert.strictEqual(res.state.statusCode, 400); assert.strictEqual(res.state.jsonBody.error.code, "INVALID_HASH"); assertNoAppealCompletionEffects(loaded.calls); }); } test("createappealcompletemessage_api rejects a capability for another operation", async () => { const loaded = loadAppealCompletionDirect({ hashAPIPath: (queryPath) => { loaded.calls.hash.push(queryPath); return queryPath.startsWith("/api/file/deleteblob?") ? "&hash=expected" : "&hash=other"; } }); const res = createRes(); await loaded.mod.default.handler(appealCompletionDirectRequest(), res); assert.strictEqual(res.state.statusCode, 400); assert.strictEqual(res.state.jsonBody.error.code, "INVALID_HASH"); assertNoAppealCompletionEffects(loaded.calls); }); test("createappealcompletemessage_api preserves owner payloads and downstream completion calls", async () => { const loaded = loadAppealCompletionDirect(); const req = appealCompletionDirectRequest(); const res = createRes(); await loaded.mod.default.handler(req, res); assert.strictEqual(res.state.statusCode, 200); assert.deepStrictEqual(loaded.calls.options, [[req, res]]); assert.deepStrictEqual(loaded.calls.session, [[req, res, loaded.options]]); assert.deepStrictEqual(loaded.calls.progress, [ ["owner-container", "TMP-CASE-1"] ]); assert.deepStrictEqual(loaded.calls.download, [ ["owner-container", "TMP-CASE-1/progress.json", "TMP-CASE-1"], ["owner-container", "TMP-CASE-1/TMP-CASE-1_case.json", "TMP-CASE-1"] ]); const savedProgress = JSON.parse(loaded.calls.createBlob[0][0]); assert.strictEqual(savedProgress.appealComplete, "TMP-CASE-1"); assert.strictEqual(Object.hasOwn(savedProgress, "pinswg_name"), false); assert.deepStrictEqual(loaded.calls.createBlob[0].slice(1), [ "owner-container", "TMP-CASE-1" ]); assert.strictEqual(loaded.calls.caseBlob.length, 1); assert.deepStrictEqual(JSON.parse(JSON.stringify(loaded.calls.account)), [ [ "00000000-0000-0000-0000-000000000001", { pinswg_typeofinvolvement: 846040001 } ] ]); assert.deepStrictEqual(loaded.calls.queue, [ ["owner-container", "TMP-CASE-1"] ]); assert.deepStrictEqual(JSON.parse(JSON.stringify(res.state.jsonBody)), { status: "success" }); }); test("createappealcompletemessageproxy dependency failure returns CREATE_APPEAL_COMPLETE_MESSAGE_PROXY_FAILED", async () => { const mod = loadModule( "pages/api/file/createappealcompletemessageproxy_api.js", { nextConnect: createNextConnectMock(), middleware: () => {}, getServerSession: async () => ({ user: { id: "container-1" } }), authOptions: () => ({}), respondError: respondErrorMock, respondSuccess: respondSuccessMock, getToken: async () => ({ access_token: "tok" }), azureHeaders: () => ({ headers: {} }), hashAPIPath: () => "&hash=abc", consoleLogger: () => {}, process: { env: { API_ROOT: "http://example.com" } }, axios: { get: async () => { throw new Error("create appeal proxy failed"); } } } ); const req = { query: { container: "container-1", tempcaseref: "TMP1" }, headers: { cookie: "next-auth.session-token=synthetic" } }; const res = createRes(); await mod.default.handler(req, res); assert.strictEqual(res.state.statusCode, 400); assert.strictEqual( res.state.jsonBody.error.code, "CREATE_APPEAL_COMPLETE_MESSAGE_PROXY_FAILED" ); }); test("createappealcompletemessage_api accepts encoded hash variant and returns success", async () => { const mod = loadModule( "pages/api/file/createappealcompletemessage_api.js", { nextConnect: createNextConnectMock(), middleware: () => {}, getServerSession: async () => ({ user: { id: "container-1" } }), authOptions: () => ({}), respondError: respondErrorMock, respondSuccess: respondSuccessMock, hashAPIPath: (queryPath) => queryPath.includes("tempcaseref=TMP%2F1") ? "&hash=expected" : "&hash=other", getProgressBlobs: async () => ({ path: "TMP/1/progress.json" }), downloadProgressFile: async (_container, path) => { if (path.includes("_case.json")) { return { "customerid_contact@odata.bind": "/contacts(00000000-0000-0000-0000-000000000001)" }; } return { pinswg_siteaddressline1: "Line 1", pinswg_siteaddressline2: "", pinswg_siteaddresstown: "Town", pinswg_siteaddresscounty: "County", pinswg_siteaddresspostcode: "CF1 1AA", pinswg_lpaapplicationreference: "LPA-1", pinswg_developmentdescription: "Desc", pinswg_name: "tmp" }; }, createBlob: async () => ({}), getCaseBlob: async () => ({}), createCaseCompleteMessage: async () => ({}), updateCrmContact: async () => ({}), consoleLogger: () => {}, _: { isEmpty: (v) => v === undefined || v === null || v === "" } } ); const req = { query: { container: "container-1", tempcaseref: "TMP/1", inv: "846040001", 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)), { status: "success" }); }); test("createappealcompletemessage_api dependency failure returns CREATE_APPEAL_COMPLETE_MESSAGE_FAILED", async () => { const mod = loadModule( "pages/api/file/createappealcompletemessage_api.js", { nextConnect: createNextConnectMock(), middleware: () => {}, getServerSession: async () => ({ user: { id: "owner-container" } }), authOptions: () => ({}), respondError: respondErrorMock, respondSuccess: respondSuccessMock, hashAPIPath: () => "&hash=expected", getProgressBlobs: async () => { throw new Error("progress failed"); }, consoleLogger: () => {}, _: { isEmpty: () => true } } ); const req = { query: { container: "owner-container", tempcaseref: "TMP1", hash: "expected" } }; const res = createRes(); await mod.default.handler(req, res); assert.strictEqual(res.state.statusCode, 400); assert.strictEqual( res.state.jsonBody.error.code, "CREATE_APPEAL_COMPLETE_MESSAGE_FAILED" ); }); test("createcaseinvolvement returns record exists on 412 conflict", async () => { const mod = loadModule("pages/api/file/createcaseinvolvement_api.js", { respondError: respondErrorMock, respondSuccess: respondSuccessMock, getToken: async () => ({ access_token: "tok" }), hashAPIPath: () => "?hash=abc", consoleLogger: () => {}, process: { env: { CRMURL: "crm.example", RELAY_ROOT: "https://relay/" } }, axios: async () => { const error = new Error("exists"); error.status = 412; throw error; } }); const req = { body: { contactid: "c1", incidentid: "i1" } }; const res = createRes(); await mod.default(req, res); assert.strictEqual(res.state.statusCode, 200); assert.deepStrictEqual(JSON.parse(JSON.stringify(res.state.jsonBody)), { record: "exists" }); }); test("createrepinvolvement dependency failure returns CREATE_REP_INVOLVEMENT_FAILED", async () => { const mod = loadModule("pages/api/file/createrepinvolvement_api.js", { respondError: respondErrorMock, respondSuccess: respondSuccessMock, getToken: async () => ({ access_token: "tok" }), getPersonalAccount: async () => ({}), hashAPIPath: () => "?hash=abc", consoleLogger: () => {}, process: { env: { CRMURL: "crm.example", CRMURL_VERSION: "v9.2", RELAY_ROOT: "https://relay/" } }, axios: async () => { throw new Error("rep involvement failed"); } }); const req = { body: { contactid: "c1", incidentid: "i1" } }; const res = createRes(); await mod.default(req, res); assert.strictEqual(res.state.statusCode, 400); assert.strictEqual( res.state.jsonBody.error.code, "CREATE_REP_INVOLVEMENT_FAILED" ); }); test("updatecase dependency failure returns UPDATE_CASE_FAILED", async () => { const mod = loadModule("pages/api/file/updatecase_api.js", { respondError: respondErrorMock, respondSuccess: respondSuccessMock, getToken: async () => ({ access_token: "tok" }), hashAPIPath: () => "?hash=abc", consoleLogger: () => {}, process: { env: { RELAY_ROOT: "https://relay/" } }, axios: async () => { throw new Error("update failed"); } }); const req = { query: { updateFormCollection: "pinswg_forms", appealObj: "obj1", incident: "inc1" }, body: { field: "value" } }; const res = createRes(); await mod.default(req, res); assert.strictEqual(res.state.statusCode, 400); assert.strictEqual(res.state.jsonBody.error.code, "UPDATE_CASE_FAILED"); }); test("getawaitingsubmissionfromblob handler success returns payload", async () => { const mod = loadModule("pages/api/file/getawaitingsubmissionfromblob.js", { nextConnect: createNextConnectMock(), middleware: () => {}, hashAPIPath: () => "&hash=expected", respondError: respondErrorMock, respondSuccess: respondSuccessMock, getAllProgressBlobs: async () => [{ path: "c1/case1_appeal.json" }], downloadAllProgressFiles: async () => ({ value: [{ id: "a1" }] }) }); const req = { query: { container: "c1", 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: [{ id: "a1" }] }); }); test("getawaitingsubmissionfromblob handler dependency failure returns GET_AWAITING_SUBMISSION_BLOB_FAILED", async () => { const mod = loadModule("pages/api/file/getawaitingsubmissionfromblob.js", { nextConnect: createNextConnectMock(), middleware: () => {}, hashAPIPath: () => "&hash=expected", respondError: respondErrorMock, respondSuccess: respondSuccessMock, getAllProgressBlobs: async () => { throw new Error("progress fetch failed"); }, downloadAllProgressFiles: async () => ({}) }); const req = { query: { container: "c1", 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_AWAITING_SUBMISSION_BLOB_FAILED" ); }); test("getbloblist handler returns INVALID_HASH for mismatch", async () => { const mod = loadModule("pages/api/file/getbloblist.js", { nextConnect: createNextConnectMock(), middleware: () => {}, hashAPIPath: () => "&hash=expected", respondError: respondErrorMock, respondSuccess: respondSuccessMock, getBlobs: async () => ({ id: "x" }), getRepsFilesBlobs: async () => ({ id: "x" }) }); const req = { query: { container: "c1", casefolderID: "f1", hash: "wrong" } }; const res = createRes(); await mod.default.handler(req, res); assert.strictEqual(res.state.statusCode, 400); 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(), middleware: () => {}, hashAPIPath: () => "&hash=expected", consoleLogger: () => {}, respondError: respondErrorMock, respondSuccess: respondSuccessMock, createContainer: async () => ({ ok: true }) }); const req = { query: { hash: "expected" } }; const res = createRes(); await mod.default.handler(req, res); assert.strictEqual(res.state.statusCode, 400); assert.strictEqual(res.state.jsonBody.error.code, "MISSING_REQUIRED_QUERY"); }); test("setupcontainer handler accepts encoded ident hash variant", async () => { const mod = loadModule("pages/api/file/setupcontainer.js", { nextConnect: createNextConnectMock(), middleware: () => {}, hashAPIPath: (queryPath) => queryPath.includes("ident=container%2Fname") ? "&hash=expected" : "&hash=other", consoleLogger: () => {}, respondError: respondErrorMock, respondSuccess: respondSuccessMock, createContainer: async () => ({ created: true }) }); const req = { query: { ident: "container/name", 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)), { data: "success", output: { created: true } }); }); test("upload handler success returns data payload with 200", async () => { const mod = loadModule("pages/api/file/upload.js", { nextConnect: createNextConnectMock(), middleware: () => {}, getServerSession: async () => ({ user: { id: "c1" } }), authOptions: () => ({}), hashAPIPath: () => "?hash=expected", respondError: respondErrorMock, respondSuccess: respondSuccessMock, createBlob: async () => ({ id: "blob-1" }), createRepBlob: async () => ({ id: "rep-1" }) }); const req = { query: { hash: "expected" }, body: { appealData: { key: "value" }, containerID: ["c1"], casefolderID: ["f1"], repOrAppeal: false } }; const res = createRes(); await mod.default.handler(req, res); assert.strictEqual(res.state.statusCode, 200); assert.deepStrictEqual(JSON.parse(JSON.stringify(res.state.jsonBody)), { data: { id: "blob-1" } }); }); test("setupcontainer handler success returns expected contract", async () => { const mod = loadModule("pages/api/file/setupcontainer.js", { nextConnect: createNextConnectMock(), middleware: () => {}, hashAPIPath: () => "&hash=expected", consoleLogger: () => {}, respondError: respondErrorMock, respondSuccess: respondSuccessMock, createContainer: async () => ({ created: true }) }); const req = { query: { ident: "cont-1", 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)), { data: "success", output: { created: true } }); }); test("getbloblist handler success returns value array contract", async () => { const mod = loadModule("pages/api/file/getbloblist.js", { nextConnect: createNextConnectMock(), middleware: () => {}, hashAPIPath: () => "&hash=expected", respondError: respondErrorMock, respondSuccess: respondSuccessMock, getBlobs: async () => ({ file: "a.pdf" }), 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, 200); assert.deepStrictEqual(JSON.parse(JSON.stringify(res.state.jsonBody)), { value: [{ file: "a.pdf" }] }); }); test("downloadblob handler success sets attachment header and returns body", async () => { const mod = loadModule("pages/api/file/downloadblob.js", { nextConnect: createNextConnectMock(), middleware: () => {}, hashAPIPath: () => "&hash=expected", respondError: respondErrorMock, downloadFile: async () => Buffer.from("file-content") }); const req = { query: { container: "c1", casefolderID: "f1", blobname: "doc.pdf", hash: "expected" } }; const res = createRes(); await mod.default.handler(req, res); assert.strictEqual(res.state.statusCode, 200); assert.strictEqual( res.state.headers["content-disposition"], "attachment; filename=doc.pdf" ); assert.strictEqual(res.state.sentBody.toString(), "file-content"); }); const loadDownloadBlobCapability = (overrides = {}) => { const calls = { hash: [], download: [] }; const mod = loadModule("pages/api/file/downloadblob.js", { nextConnect: createNextConnectMock(), middleware: () => {}, hashAPIPath: (queryPath) => { calls.hash.push(queryPath); return queryPath === "/api/file/downloadblob?container=selected-container&casefolderID=CASE%2F1&blobname=document.pdf" ? "&hash=valid-capability" : "&hash=other-capability"; }, respondError: respondErrorMock, downloadFile: async (...args) => { calls.download.push(args); return Buffer.from("private-draft-content"); }, ...overrides }); return { mod, calls }; }; const downloadBlobCapabilityRequest = (overrides = {}) => ({ query: { container: "selected-container", casefolderID: "CASE/1", blobname: "document.pdf", hash: "valid-capability" }, ...overrides }); test("downloadblob preserves anonymous access with a valid exact-resource capability", async () => { const loaded = loadDownloadBlobCapability(); const res = createRes(); await loaded.mod.default.handler(downloadBlobCapabilityRequest(), res); assert.strictEqual(res.state.statusCode, 200); assert.deepStrictEqual(loaded.calls.download, [ ["selected-container", "CASE/1/files/document.pdf"] ]); assert.strictEqual( res.state.headers["content-disposition"], "attachment; filename=document.pdf" ); assert.strictEqual(res.state.sentBody.toString(), "private-draft-content"); }); test("downloadblob rejects a missing capability before Azure download", async () => { const loaded = loadDownloadBlobCapability(); const res = createRes(); await loaded.mod.default.handler( downloadBlobCapabilityRequest({ query: { container: "selected-container", casefolderID: "CASE/1", blobname: "document.pdf" } }), res ); assert.strictEqual(res.state.statusCode, 400); assert.strictEqual(res.state.jsonBody.error.code, "MISSING_REQUIRED_QUERY"); assert.strictEqual(loaded.calls.hash.length, 0); assert.strictEqual(loaded.calls.download.length, 0); }); test("downloadblob rejects an invalid capability before Azure download", async () => { const loaded = loadDownloadBlobCapability(); const res = createRes(); await loaded.mod.default.handler( downloadBlobCapabilityRequest({ query: { container: "selected-container", casefolderID: "CASE/1", blobname: "document.pdf", hash: "invalid-capability" } }), res ); assert.strictEqual(res.state.statusCode, 400); assert.strictEqual(res.state.jsonBody.error.code, "INVALID_HASH"); assert.strictEqual(loaded.calls.download.length, 0); }); for (const capabilityCase of [ { name: "different blob", query: { container: "selected-container", casefolderID: "CASE/1", blobname: "other-document.pdf", hash: "valid-capability" } }, { name: "different folder", query: { container: "selected-container", casefolderID: "CASE/2", blobname: "document.pdf", hash: "valid-capability" } }, { name: "different container", query: { container: "other-container", casefolderID: "CASE/1", blobname: "document.pdf", hash: "valid-capability" } } ]) { test(`downloadblob rejects a capability issued for a ${capabilityCase.name}`, async () => { const loaded = loadDownloadBlobCapability(); const res = createRes(); await loaded.mod.default.handler( downloadBlobCapabilityRequest({ query: capabilityCase.query }), res ); assert.strictEqual(res.state.statusCode, 400); assert.strictEqual(res.state.jsonBody.error.code, "INVALID_HASH"); assert.strictEqual(loaded.calls.download.length, 0); }); } test("downloadblob rejects a delete-operation capability before Azure download", async () => { const loaded = loadDownloadBlobCapability({ hashAPIPath: (queryPath) => { loaded.calls.hash.push(queryPath); return queryPath.startsWith("/api/file/deleteblob?") ? "&hash=valid-capability" : "&hash=other-capability"; } }); const res = createRes(); await loaded.mod.default.handler(downloadBlobCapabilityRequest(), res); assert.strictEqual(res.state.statusCode, 400); assert.strictEqual(res.state.jsonBody.error.code, "INVALID_HASH"); assert.strictEqual(loaded.calls.download.length, 0); }); test("downloadblob preserves dependency failure contract for a valid capability", async () => { const loaded = loadDownloadBlobCapability({ downloadFile: async (...args) => { loaded.calls.download.push(args); throw new Error("download failed"); } }); const res = createRes(); await loaded.mod.default.handler(downloadBlobCapabilityRequest(), res); assert.strictEqual(res.state.statusCode, 400); assert.strictEqual(res.state.jsonBody.error.code, "DOWNLOAD_BLOB_FAILED"); assert.strictEqual(loaded.calls.download.length, 1); }); test("upload handler dependency failure returns UPLOAD_FAILED", async () => { const mod = loadModule("pages/api/file/upload.js", { nextConnect: createNextConnectMock(), middleware: () => {}, getServerSession: async () => ({ user: { id: "c1" } }), authOptions: () => ({}), hashAPIPath: () => "?hash=expected", respondError: respondErrorMock, respondSuccess: respondSuccessMock, createBlob: async () => { throw new Error("storage down"); }, createRepBlob: async () => ({ id: "rep-1" }) }); const req = { query: { hash: "expected" }, body: { appealData: { key: "value" }, containerID: ["c1"], casefolderID: ["f1"], repOrAppeal: false } }; const res = createRes(); await mod.default.handler(req, res); assert.strictEqual(res.state.statusCode, 400); assert.strictEqual(res.state.jsonBody.error.code, "UPLOAD_FAILED"); }); test("setupcontainer dependency failure returns SETUP_CONTAINER_FAILED", async () => { const mod = loadModule("pages/api/file/setupcontainer.js", { nextConnect: createNextConnectMock(), middleware: () => {}, hashAPIPath: () => "&hash=expected", consoleLogger: () => {}, respondError: respondErrorMock, respondSuccess: respondSuccessMock, createContainer: async () => { throw new Error("container failed"); } }); const req = { query: { ident: "cont-1", hash: "expected" } }; const res = createRes(); await mod.default.handler(req, res); assert.strictEqual(res.state.statusCode, 400); assert.strictEqual(res.state.jsonBody.error.code, "SETUP_CONTAINER_FAILED"); }); test("downloadblob dependency failure returns DOWNLOAD_BLOB_FAILED", async () => { const mod = loadModule("pages/api/file/downloadblob.js", { nextConnect: createNextConnectMock(), middleware: () => {}, hashAPIPath: () => "&hash=expected", respondError: respondErrorMock, downloadFile: async () => { throw new Error("download failed"); } }); const req = { query: { container: "c1", casefolderID: "f1", 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, "DOWNLOAD_BLOB_FAILED"); }); test("generateappealpdfcopy returns INCIDENT_ID_REQUIRED when docProps missing", async () => { const mod = loadModule("pages/api/file/generateappealpdfcopy.js", { respondError: respondErrorMock, getCaseByID: async () => [], getPortalModuleDetails: async () => ({ value: [{}] }), getAppealPDFDocs: async () => [], getFormCollectionByID: () => ({ UrlName: "planningappeals78" }), getPickLists: async () => ({}), planningappeals78_pdf: () => ({}), other_pdf: () => ({}), pdf: () => ({ toBuffer: async () => Buffer.from("pdf") }) }); const req = { body: {} }; const res = createRes(); await mod.default(req, res); assert.strictEqual(res.state.statusCode, 400); assert.strictEqual(res.state.jsonBody.error.code, "INCIDENT_ID_REQUIRED"); }); test("generateappealpdfcopy returns CASE_NOT_FOUND when case lookup is empty", async () => { const mod = loadModule("pages/api/file/generateappealpdfcopy.js", { respondError: respondErrorMock, getCaseByID: async () => [], getPortalModuleDetails: async () => ({ value: [{}] }), getAppealPDFDocs: async () => [], getFormCollectionByID: () => ({ UrlName: "planningappeals78" }), getPickLists: async () => ({}), planningappeals78_pdf: () => ({}), other_pdf: () => ({}), pdf: () => ({ toBuffer: async () => Buffer.from("pdf") }) }); const req = { body: { docProps: "i1" } }; const res = createRes(); await mod.default(req, res); assert.strictEqual(res.state.statusCode, 404); assert.strictEqual(res.state.jsonBody.error.code, "CASE_NOT_FOUND"); }); test("generateappealpdfcopy returns FORM_COLLECTION_NOT_FOUND when collection is unresolved", async () => { const mod = loadModule("pages/api/file/generateappealpdfcopy.js", { respondError: respondErrorMock, getCaseByID: async () => [ { pinswg_appealcasetype: 846040000, ticketnumber: "CAS-1", description: "desc" } ], getPortalModuleDetails: async () => ({ value: [{}] }), getAppealPDFDocs: async () => [], getFormCollectionByID: () => null, getPickLists: async () => ({}), planningappeals78_pdf: () => ({}), other_pdf: () => ({}), pdf: () => ({ toBuffer: async () => Buffer.from("pdf") }) }); const req = { body: { docProps: "i1" } }; const res = createRes(); await mod.default(req, res); assert.strictEqual(res.state.statusCode, 400); assert.strictEqual( res.state.jsonBody.error.code, "FORM_COLLECTION_NOT_FOUND" ); }); test("generateappealpdfcopy success returns pdf buffer and headers", async () => { const mod = loadModule("pages/api/file/generateappealpdfcopy.js", { respondError: respondErrorMock, getCaseByID: async () => [ { pinswg_appealcasetype: 846040000, ticketnumber: "CAS-1", description: "desc" } ], getPortalModuleDetails: async () => ({ value: [{}] }), getAppealPDFDocs: async () => [{ id: "d1" }], getFormCollectionByID: () => ({ UrlName: "planningappeals78", LogicalCollectionName: "pinswg_planningappeals78s" }), getPickLists: async () => ({ a: 1 }), planningappeals78_pdf: () => ({ type: "pdfComponent" }), other_pdf: () => ({ type: "otherComponent" }), pdf: () => ({ toBuffer: async () => Buffer.from("pdf-content") }) }); const req = { body: { docProps: "i1" } }; const res = createRes(); await mod.default(req, res); assert.strictEqual(res.state.headers["content-type"], "application/pdf"); assert.strictEqual( typeof res.state.headers["content-disposition"], "string" ); assert.strictEqual(res.state.sentBody.toString(), "pdf-content"); }); test("generateappealpdfcopy catch path returns APPEAL_PDF_COPY_GENERATION_FAILED", async () => { const mod = loadModule("pages/api/file/generateappealpdfcopy.js", { respondError: respondErrorMock, getCaseByID: async () => [ { pinswg_appealcasetype: 846040000, ticketnumber: "CAS-1", description: "desc" } ], getPortalModuleDetails: async () => ({ value: [{}] }), getAppealPDFDocs: async () => [{ id: "d1" }], getFormCollectionByID: () => ({ UrlName: "planningappeals78", LogicalCollectionName: "pinswg_planningappeals78s" }), getPickLists: async () => ({ a: 1 }), planningappeals78_pdf: () => ({ type: "pdfComponent" }), other_pdf: () => ({ type: "otherComponent" }), pdf: () => ({ toBuffer: async () => { throw new Error("pdf failure"); } }) }); const req = { body: { docProps: "i1" } }; const res = createRes(); await mod.default(req, res); assert.strictEqual(res.state.statusCode, 400); assert.strictEqual( res.state.jsonBody.error.code, "APPEAL_PDF_COPY_GENERATION_FAILED" ); }); const PDF_OWNER_CONTAINER = "owner-container"; const PDF_CASE_FOLDER = "CASE-1"; const loadGeneratePdf = (overrides = {}) => { const { Readable } = require("stream"); const calls = { session: [], options: [], hash: [], render: [], create: [] }; const options = { id: "generate-pdf-options" }; const mod = loadModule("pages/api/file/generatepdf.js", { Buffer, respondError: respondErrorMock, respondSuccess: respondSuccessMock, authOptions: (req, res) => { calls.options.push([req, res]); return options; }, getServerSession: async (...args) => { calls.session.push(args); return { user: { id: PDF_OWNER_CONTAINER } }; }, hashAPIPath: (queryPath) => { calls.hash.push(queryPath); return queryPath.includes("?") ? "&hash=expected" : "?hash=expected"; }, consoleLogger: () => {}, ReactPDF: { renderToStream: async (...args) => { calls.render.push(args); return Readable.from(["pdf"]); } }, createRepPDFBlob: async (...args) => { calls.create.push(args); return { id: "representation-pdf" }; }, other_pdf: () => ({ type: "other" }), ...overrides }); return { mod, calls, options }; }; const generatePdfRequest = (overrides = {}) => ({ method: "POST", query: { hash: "expected" }, body: { representationType: "Other", repfile_name: "rep-file", containerID: PDF_OWNER_CONTAINER, caseRef: PDF_CASE_FOLDER }, ...overrides }); const loadGenerateAppealPdf = (overrides = {}) => { const { Readable } = require("stream"); const calls = { session: [], options: [], hash: [], temp: [], progress: [], download: [], pickLists: [], render: [], create: [] }; const options = { id: "generate-appeal-pdf-options" }; const mod = loadModule("pages/api/file/generateappealpdf.js", { Buffer, respondError: respondErrorMock, respondSuccess: respondSuccessMock, authOptions: (req, res) => { calls.options.push([req, res]); return options; }, getServerSession: async (...args) => { calls.session.push(args); return { user: { id: PDF_OWNER_CONTAINER } }; }, hashAPIPath: (queryPath) => { calls.hash.push(queryPath); return "&hash=expected"; }, getTempCaseBlob: async (...args) => { calls.temp.push(args); return { ticketnumber: PDF_CASE_FOLDER }; }, getProgressBlobs: async (...args) => { calls.progress.push(args); return { path: `${PDF_CASE_FOLDER}/progress.json` }; }, downloadProgressFile: async (...args) => { calls.download.push(args); return { pinswg_name: PDF_CASE_FOLDER, pinswg_appealcasetype: "846040000" }; }, getPickLists: async (...args) => { calls.pickLists.push(args); return {}; }, planningappeals78_pdf: () => ({ type: "planning" }), other_pdf: () => ({ type: "other" }), ReactPDF: { renderToStream: async (...args) => { calls.render.push(args); return Readable.from(["pdf"]); } }, createAppealPDFBlob: async (...args) => { calls.create.push(args); return { id: "appeal-pdf" }; }, ...overrides }); return { mod, calls, options }; }; const generateAppealPdfRequest = (overrides = {}) => ({ method: "POST", query: { hash: "expected", appealType: "846040000" }, body: { containerID: PDF_OWNER_CONTAINER, casefolderID: PDF_CASE_FOLDER, filesList: [] }, ...overrides }); const assertNoGeneratePdfWork = (calls) => { assert.strictEqual(calls.render.length, 0); assert.strictEqual(calls.create.length, 0); }; const assertNoGenerateAppealPdfWork = (calls) => { assert.strictEqual(calls.temp.length, 0); assert.strictEqual(calls.progress.length, 0); assert.strictEqual(calls.download.length, 0); assert.strictEqual(calls.pickLists.length, 0); assert.strictEqual(calls.render.length, 0); assert.strictEqual(calls.create.length, 0); }; for (const endpoint of [ { name: "generatepdf", load: loadGeneratePdf, request: generatePdfRequest, assertNoWork: assertNoGeneratePdfWork }, { name: "generateappealpdf", load: loadGenerateAppealPdf, request: generateAppealPdfRequest, assertNoWork: assertNoGenerateAppealPdfWork } ]) { for (const sessionCase of [ { name: "missing session", session: null }, { name: "missing session user id", session: { user: {} } }, { name: "empty session user id", session: { user: { id: "" } } }, { name: "non-string session user id", session: { user: { id: 123 } } } ]) { test(`${endpoint.name} rejects ${sessionCase.name} before privileged work`, async () => { const loaded = endpoint.load({ getServerSession: async () => sessionCase.session }); const res = createRes(); await loaded.mod.default(endpoint.request(), res); assert.strictEqual(res.state.statusCode, 403); assert.strictEqual(res.state.jsonBody.error.code, "FORBIDDEN"); endpoint.assertNoWork(loaded.calls); }); } test(`${endpoint.name} rejects another user's container before privileged work`, async () => { const loaded = endpoint.load({ getServerSession: async () => ({ user: { id: "different-container" } }) }); const res = createRes(); await loaded.mod.default(endpoint.request(), res); assert.strictEqual(res.state.statusCode, 403); assert.strictEqual(res.state.jsonBody.error.code, "FORBIDDEN"); endpoint.assertNoWork(loaded.calls); }); test(`${endpoint.name} ignores a client-provided user identity`, async () => { const loaded = endpoint.load({ getServerSession: async () => ({ user: { id: "different-container" } }) }); const req = endpoint.request(); req.body.userID = PDF_OWNER_CONTAINER; const res = createRes(); await loaded.mod.default(req, res); assert.strictEqual(res.state.statusCode, 403); endpoint.assertNoWork(loaded.calls); }); test(`${endpoint.name} passes req, res and request-scoped auth options to getServerSession`, async () => { const loaded = endpoint.load(); const req = endpoint.request(); const res = createRes(); await loaded.mod.default(req, res); assert.deepStrictEqual(loaded.calls.options, [[req, res]]); assert.deepStrictEqual(loaded.calls.session, [ [req, res, loaded.options] ]); }); } for (const containerCase of [ { name: "missing", value: undefined }, { name: "invalid", value: "Invalid_Container" }, { name: "ambiguous", value: [PDF_OWNER_CONTAINER] } ]) { test(`generatepdf rejects ${containerCase.name} container before privileged work`, async () => { const loaded = loadGeneratePdf(); const req = generatePdfRequest(); if (typeof containerCase.value === "undefined") { delete req.body.containerID; } else { req.body.containerID = containerCase.value; } const res = createRes(); await loaded.mod.default(req, res); assert.strictEqual(res.state.statusCode, 400); assertNoGeneratePdfWork(loaded.calls); }); test(`generateappealpdf rejects ${containerCase.name} container before privileged work`, async () => { const loaded = loadGenerateAppealPdf(); const req = generateAppealPdfRequest(); if (typeof containerCase.value === "undefined") { delete req.body.containerID; } else { req.body.containerID = containerCase.value; } const res = createRes(); await loaded.mod.default(req, res); assert.strictEqual(res.state.statusCode, 400); assertNoGenerateAppealPdfWork(loaded.calls); }); } test("generatepdf rejects missing draft-folder identifiers", async () => { const loaded = loadGeneratePdf(); const res = createRes(); await loaded.mod.default( generatePdfRequest({ body: { representationType: "Other", repfile_name: "rep-file", containerID: PDF_OWNER_CONTAINER } }), res ); assert.strictEqual(res.state.statusCode, 400); assertNoGeneratePdfWork(loaded.calls); }); test("generateappealpdf rejects missing or malformed draft-folder identifiers", async () => { for (const value of [undefined, "../CASE-1", [PDF_CASE_FOLDER]]) { const loaded = loadGenerateAppealPdf(); const req = generateAppealPdfRequest(); if (typeof value === "undefined") { delete req.body.casefolderID; } else { req.body.casefolderID = value; } const res = createRes(); await loaded.mod.default(req, res); assert.strictEqual(res.state.statusCode, 400); assertNoGenerateAppealPdfWork(loaded.calls); } }); for (const endpoint of [ { name: "generatepdf", load: loadGeneratePdf, request: generatePdfRequest, assertNoWork: assertNoGeneratePdfWork }, { name: "generateappealpdf", load: loadGenerateAppealPdf, request: generateAppealPdfRequest, assertNoWork: assertNoGenerateAppealPdfWork } ]) { test(`${endpoint.name} rejects a query/body container ambiguity`, async () => { const loaded = endpoint.load(); const req = endpoint.request(); req.query.containerID = PDF_OWNER_CONTAINER; const res = createRes(); await loaded.mod.default(req, res); assert.strictEqual(res.state.statusCode, 400); endpoint.assertNoWork(loaded.calls); }); test(`${endpoint.name} rejects missing HMAC before privileged work`, async () => { const loaded = endpoint.load(); const req = endpoint.request(); delete req.query.hash; const res = createRes(); await loaded.mod.default(req, res); assert.strictEqual(res.state.statusCode, 400); endpoint.assertNoWork(loaded.calls); }); test(`${endpoint.name} rejects invalid HMAC before privileged work`, async () => { const loaded = endpoint.load(); const req = endpoint.request(); req.query.hash = "wrong"; const res = createRes(); await loaded.mod.default(req, res); assert.strictEqual(res.state.statusCode, 400); assert.strictEqual(res.state.jsonBody.error.code, "INVALID_HASH"); endpoint.assertNoWork(loaded.calls); }); test(`${endpoint.name} rejects a valid HMAC for an unauthorised container`, async () => { const loaded = endpoint.load({ getServerSession: async () => ({ user: { id: "different-container" } }) }); const res = createRes(); await loaded.mod.default(endpoint.request(), res); assert.strictEqual(res.state.statusCode, 403); endpoint.assertNoWork(loaded.calls); }); } test("generatepdf owner success writes the PDF to the authorised container", async () => { const loaded = loadGeneratePdf(); const res = createRes(); await loaded.mod.default(generatePdfRequest(), res); assert.strictEqual(res.state.statusCode, 200); assert.strictEqual(loaded.calls.render.length, 1); assert.strictEqual(loaded.calls.create.length, 1); assert.strictEqual(loaded.calls.create[0][1], PDF_OWNER_CONTAINER); assert.strictEqual(loaded.calls.create[0][2], PDF_CASE_FOLDER); assert.strictEqual(loaded.calls.create[0][3], "rep-file"); assert.strictEqual(res.state.jsonBody.status, "success"); }); test("generatepdf preserves ticketnumber precedence when references differ", async () => { const loaded = loadGeneratePdf(); const res = createRes(); await loaded.mod.default( generatePdfRequest({ body: { representationType: "Other", repfile_name: "rep-file", containerID: PDF_OWNER_CONTAINER, ticketnumber: PDF_CASE_FOLDER, caseRef: "CASE-2" } }), res ); assert.strictEqual(res.state.statusCode, 200); assert.strictEqual(loaded.calls.create.length, 1); assert.strictEqual(loaded.calls.create[0][1], PDF_OWNER_CONTAINER); assert.strictEqual(loaded.calls.create[0][2], PDF_CASE_FOLDER); assert.strictEqual(res.state.jsonBody.status, "success"); }); test("generatepdf rejects malformed secondary case references", async () => { const loaded = loadGeneratePdf(); const res = createRes(); await loaded.mod.default( generatePdfRequest({ body: { representationType: "Other", repfile_name: "rep-file", containerID: PDF_OWNER_CONTAINER, ticketnumber: PDF_CASE_FOLDER, caseRef: "../other-case" } }), res ); assert.strictEqual(res.state.statusCode, 400); assertNoGeneratePdfWork(loaded.calls); }); test("generateappealpdf owner success reads and writes only the authorised container", async () => { const loaded = loadGenerateAppealPdf(); const res = createRes(); await loaded.mod.default(generateAppealPdfRequest(), res); assert.strictEqual(res.state.statusCode, 200); assert.strictEqual(loaded.calls.temp[0][0], PDF_OWNER_CONTAINER); assert.strictEqual(loaded.calls.progress[0][0], PDF_OWNER_CONTAINER); assert.strictEqual(loaded.calls.download[0][0], PDF_OWNER_CONTAINER); assert.strictEqual(loaded.calls.create[0][1], PDF_OWNER_CONTAINER); assert.strictEqual(loaded.calls.create[0][2], PDF_CASE_FOLDER); assert.strictEqual(res.state.jsonBody.status, "success"); }); test("generatepdf catch path returns GENERATE_PDF_FAILED for an authorised owner", async () => { const loaded = loadGeneratePdf({ createRepPDFBlob: async (...args) => { loaded.calls.create.push(args); throw new Error("pdf write failed"); } }); const req = generatePdfRequest({ query: { hash: "expected", download: "true" }, body: { representationType: "Other", repfile_name: "rep-file", containerID: PDF_OWNER_CONTAINER, caseRef: PDF_CASE_FOLDER } }); const res = createRes(); await loaded.mod.default(req, res); assert.strictEqual(res.state.statusCode, 400); assert.strictEqual(res.state.jsonBody.error.code, "GENERATE_PDF_FAILED"); assert.strictEqual(loaded.calls.render.length, 1); assert.strictEqual(loaded.calls.create.length, 1); }); test("generateappealpdf catch path returns GENERATE_APPEAL_PDF_FAILED for an authorised owner", async () => { const loaded = loadGenerateAppealPdf({ downloadProgressFile: async () => { loaded.calls.download.push([ PDF_OWNER_CONTAINER, `${PDF_CASE_FOLDER}/progress.json`, PDF_CASE_FOLDER ]); throw new Error("progress failed"); } }); const res = createRes(); await loaded.mod.default(generateAppealPdfRequest(), res); assert.strictEqual(res.state.statusCode, 400); assert.strictEqual( res.state.jsonBody.error.code, "GENERATE_APPEAL_PDF_FAILED" ); assert.strictEqual(loaded.calls.temp.length, 1); assert.strictEqual(loaded.calls.progress.length, 1); assert.strictEqual(loaded.calls.download.length, 1); assert.strictEqual(loaded.calls.render.length, 0); assert.strictEqual(loaded.calls.create.length, 0); }); const CASE_OWNER_CONTAINER = "case-owner-container"; const GENERATED_CASE_REFERENCE = "TMP-CASE-04E"; const loadCreateCase = (overrides = {}) => { const calls = { session: [], options: [], token: 0, reference: 0, caseBlob: [], appealBlob: [] }; const options = { id: "create-case-options" }; const mod = loadModule("pages/api/file/createcase_api.js", { respondError: respondErrorMock, respondSuccess: respondSuccessMock, authOptions: (req, res) => { calls.options.push([req, res]); return options; }, getServerSession: async (...args) => { calls.session.push(args); return { user: { id: CASE_OWNER_CONTAINER } }; }, getToken: async () => { calls.token += 1; return { access_token: "synthetic-token" }; }, getTempCaseRef: () => { calls.reference += 1; return GENERATED_CASE_REFERENCE; }, uuidv4: () => "00000000-0000-4000-8000-00000000004e", getCaseBlob: async (...args) => { calls.caseBlob.push(args); return `${GENERATED_CASE_REFERENCE}/${GENERATED_CASE_REFERENCE}_case.json`; }, createBlob: async (...args) => { calls.appealBlob.push(args); return GENERATED_CASE_REFERENCE; }, _: { isEmpty: (value) => !value || Object.keys(value).length === 0 }, ...overrides }); return { mod, calls, options }; }; const createCaseRequest = (overrides = {}) => ({ method: "POST", query: { contactid: "00000000-0000-4000-8000-000000000001", appealTypeId: "846040000", containername: CASE_OWNER_CONTAINER, lpaID: "00000000-0000-4000-8000-000000000002" }, body: { pinswg_lpaname: "Synthetic LPA", createdon: "2026-11-04T00:00:00.000Z" }, ...overrides }); const assertNoCreateCaseSideEffects = (calls) => { assert.strictEqual(calls.token, 0); assert.strictEqual(calls.reference, 0); assert.strictEqual(calls.caseBlob.length, 0); assert.strictEqual(calls.appealBlob.length, 0); }; for (const sessionCase of [ { name: "missing session", session: null }, { name: "missing session user ID", session: { user: {} } }, { name: "empty session user ID", session: { user: { id: "" } } }, { name: "non-string session user ID", session: { user: { id: 123 } } } ]) { test(`createcase_api rejects ${sessionCase.name} before private draft creation`, async () => { const loaded = loadCreateCase({ getServerSession: async () => sessionCase.session }); const res = createRes(); await loaded.mod.default(createCaseRequest(), res); assert.strictEqual(res.state.statusCode, 403); assert.strictEqual(res.state.jsonBody.error.code, "FORBIDDEN"); assertNoCreateCaseSideEffects(loaded.calls); }); } test("createcase_api rejects a foreign destination container before private draft creation", async () => { const loaded = loadCreateCase({ getServerSession: async () => ({ user: { id: "different-container" } }) }); const res = createRes(); await loaded.mod.default(createCaseRequest(), res); assert.strictEqual(res.state.statusCode, 403); assert.strictEqual(res.state.jsonBody.error.code, "FORBIDDEN"); assertNoCreateCaseSideEffects(loaded.calls); }); test("createcase_api does not trust a caller-supplied identity", async () => { const loaded = loadCreateCase({ getServerSession: async () => ({ user: { id: "different-container" } }) }); const req = createCaseRequest(); req.body.userID = CASE_OWNER_CONTAINER; const res = createRes(); await loaded.mod.default(req, res); assert.strictEqual(res.state.statusCode, 403); assertNoCreateCaseSideEffects(loaded.calls); }); test("createcase_api passes req, res and request-scoped options to getServerSession", async () => { const loaded = loadCreateCase(); const req = createCaseRequest(); const res = createRes(); await loaded.mod.default(req, res); assert.deepStrictEqual(loaded.calls.options, [[req, res]]); assert.deepStrictEqual(loaded.calls.session, [[req, res, loaded.options]]); }); for (const containerCase of [ { name: "missing", value: undefined }, { name: "invalid", value: "Invalid_Container" }, { name: "array-valued", value: [CASE_OWNER_CONTAINER] } ]) { test(`createcase_api rejects a ${containerCase.name} container before private draft creation`, async () => { const loaded = loadCreateCase(); const req = createCaseRequest(); if (typeof containerCase.value === "undefined") { delete req.query.containername; } else { req.query.containername = containerCase.value; } const res = createRes(); await loaded.mod.default(req, res); assert.strictEqual(res.state.statusCode, 400); assertNoCreateCaseSideEffects(loaded.calls); }); } test("createcase_api rejects malformed JSON before private draft creation", async () => { const loaded = loadCreateCase(); const res = createRes(); await loaded.mod.default(createCaseRequest({ body: "{" }), res); assert.strictEqual(res.state.statusCode, 400); assert.strictEqual(res.state.jsonBody.error.code, "INVALID_REQUEST"); assertNoCreateCaseSideEffects(loaded.calls); }); test("createcase_api rejects ambiguous destination identifiers before private draft creation", async () => { for (const request of [ createCaseRequest({ query: { ...createCaseRequest().query, containerID: CASE_OWNER_CONTAINER } }), createCaseRequest({ body: { ...createCaseRequest().body, containername: CASE_OWNER_CONTAINER } }) ]) { const loaded = loadCreateCase(); const res = createRes(); await loaded.mod.default(request, res); assert.strictEqual(res.state.statusCode, 400); assert.strictEqual(res.state.jsonBody.error.code, "INVALID_REQUEST"); assertNoCreateCaseSideEffects(loaded.calls); } }); test("createcase_api rejects an invalid generated folder before token acquisition and Blob writes", async () => { const loaded = loadCreateCase({ getTempCaseRef: () => "../foreign-case" }); const res = createRes(); await loaded.mod.default(createCaseRequest(), res); assert.strictEqual(res.state.statusCode, 400); assert.strictEqual(res.state.jsonBody.error.code, "INVALID_REQUEST"); assert.strictEqual(loaded.calls.token, 0); assert.strictEqual(loaded.calls.caseBlob.length, 0); assert.strictEqual(loaded.calls.appealBlob.length, 0); }); test("createcase_api creates both draft JSON blobs in the authenticated owner's container", async () => { const loaded = loadCreateCase(); const req = createCaseRequest(); const res = createRes(); await loaded.mod.default(req, res); assert.strictEqual(res.state.statusCode, 200); assert.strictEqual(loaded.calls.token, 1); assert.strictEqual(loaded.calls.reference, 1); assert.strictEqual(loaded.calls.caseBlob.length, 1); assert.strictEqual(loaded.calls.appealBlob.length, 1); assert.strictEqual(loaded.calls.caseBlob[0][0], CASE_OWNER_CONTAINER); assert.strictEqual(loaded.calls.caseBlob[0][1], GENERATED_CASE_REFERENCE); assert.strictEqual(loaded.calls.appealBlob[0][1], CASE_OWNER_CONTAINER); assert.deepStrictEqual( JSON.parse(JSON.stringify(loaded.calls.caseBlob[0][2])), JSON.parse(JSON.stringify(res.state.jsonBody)) ); assert.deepStrictEqual(JSON.parse(loaded.calls.appealBlob[0][0]), { pinswg_name: GENERATED_CASE_REFERENCE, pinswg_appealcasetype: "846040000", _pinswg_associatedlpa_value: "00000000-0000-4000-8000-000000000002" }); assert.strictEqual( res.state.jsonBody.ticketnumber, GENERATED_CASE_REFERENCE ); assert.strictEqual(res.state.jsonBody.title, GENERATED_CASE_REFERENCE); assert.strictEqual( res.state.jsonBody.incidentid, "00000000-0000-4000-8000-00000000004e" ); }); test("createcase_api preserves the existing propagated token dependency failure for an authorised owner", async () => { const loaded = loadCreateCase({ getToken: async () => { loaded.calls.token += 1; throw new Error("token failure"); } }); const res = createRes(); await assert.rejects( loaded.mod.default(createCaseRequest(), res), /token failure/ ); assert.strictEqual(loaded.calls.token, 1); assert.strictEqual(loaded.calls.caseBlob.length, 0); assert.strictEqual(loaded.calls.appealBlob.length, 0); }); const loadRepresentationDeletion = (route, operationName, session) => { const calls = []; const mod = loadModule(route, { nextConnect: createNextConnectMock(), middleware: () => {}, getServerSession: async () => session, authOptions: () => ({}), hashAPIPath: () => "&hash=expected", respondError: respondErrorMock, respondSuccess: respondSuccessMock, [operationName]: async (...args) => { calls.push(args); return { deleted: true }; } }); return { mod, calls }; }; test("whole-representation deletion remains cross-user protected with zero Azure operations", async () => { const loaded = loadRepresentationDeletion( "pages/api/file/deleteblobrep.js", "deleteBlobRep", { user: { id: "different-container" } } ); const res = createRes(); await loaded.mod.default.handler( { method: "POST", headers: { origin: "http://localhost:3000" }, query: { container: "organisation-container", casefolderID: "CASE-1", repfile: "REP-1", hash: "expected" } }, res ); assert.strictEqual(res.state.statusCode, 403); assert.strictEqual(res.state.jsonBody.error.code, "FORBIDDEN"); assert.strictEqual(loaded.calls.length, 0); }); test("representation supporting-file deletion remains cross-user protected with zero Azure operations", async () => { const loaded = loadRepresentationDeletion( "pages/api/file/deleteblob.js", "deleteBlob", { user: { id: "different-container" } } ); const res = createRes(); await loaded.mod.default.handler( { query: { container: "organisation-container", casefolderID: "CASE-1/REP-1", blobname: "evidence.pdf", hash: "expected" } }, res ); assert.strictEqual(res.state.statusCode, 403); assert.strictEqual(res.state.jsonBody.error.code, "FORBIDDEN"); assert.strictEqual(loaded.calls.length, 0); }); const run = async () => { let passed = 0; for (const currentTest of tests) { await currentTest.fn(); passed += 1; } console.log( `Phase 21 file-handler contract tests passed (${passed}/${tests.length}).` ); }; module.exports = run; if (require.main === module) { run().catch((error) => { console.error(error); process.exit(1); }); }