From 25ba88ac34b6857fcf318ac5972f11e4a2e96bbc Mon Sep 17 00:00:00 2001 From: robbond Date: Tue, 17 Mar 2026 13:49:05 +0000 Subject: [PATCH 1/4] TASK22104: slice1 add api response contract tests --- tests/phase21/api-contract-slice1.test.cjs | 277 +++++++++++++++++++++ 1 file changed, 277 insertions(+) create mode 100644 tests/phase21/api-contract-slice1.test.cjs diff --git a/tests/phase21/api-contract-slice1.test.cjs b/tests/phase21/api-contract-slice1.test.cjs new file mode 100644 index 00000000..755cf87f --- /dev/null +++ b/tests/phase21/api-contract-slice1.test.cjs @@ -0,0 +1,277 @@ +const fs = require("fs"); +const path = require("path"); +const vm = require("vm"); +const assert = require("assert"); + +const rootDir = path.resolve(__dirname, "..", ".."); + +const loadModule = (relativePath, injected = {}) => { + const filePath = path.join(rootDir, relativePath); + let source = fs.readFileSync(filePath, "utf8"); + + source = source.replace(/import[\s\S]*?from\s+"[^"]+";\n?/g, ""); + source = source.replace(/export const\s+/g, "const "); + source = source.replace( + /export default\s+(\w+);/g, + "module.exports.default = $1;" + ); + + source += + '\nif (typeof respondSuccess !== "undefined") module.exports.respondSuccess = respondSuccess;\n'; + source += + '\nif (typeof respondError !== "undefined") module.exports.respondError = respondError;\n'; + source += + '\nif (typeof ApiProxy !== "undefined" && !module.exports.default) module.exports.default = ApiProxy;\n'; + + const context = { + module: { exports: {} }, + exports: {}, + require, + process, + console: { + log: () => {}, + info: () => {}, + warn: () => {}, + error: () => {} + }, + ...injected + }; + + vm.runInNewContext(source, context, { filename: filePath }); + return context.module.exports; +}; + +const createRes = () => { + const state = { + statusCode: null, + jsonBody: undefined + }; + + return { + state, + status(code) { + state.statusCode = code; + return this; + }, + json(payload) { + state.jsonBody = payload; + return payload; + } + }; +}; + +const createNextConnectMock = () => { + const router = { + handler: null, + use: () => {}, + get(fn) { + this.handler = fn; + }, + post(fn) { + this.handler = fn; + } + }; + + return () => router; +}; + +const respondSuccessMock = (res, data, status = 200) => { + return res.status(status).json(data); +}; + +const respondErrorMock = ( + res, + { + status = 500, + code = "INTERNAL_SERVER_ERROR", + message = "Request failed" + } = {} +) => { + return res.status(status).json({ + success: false, + error: { code, message } + }); +}; + +const tests = []; +const test = (name, fn) => tests.push({ name, fn }); + +test("apiResponse.respondSuccess returns data with default 200", () => { + const mod = loadModule("pages/api/middleware/apiResponse.js"); + const res = createRes(); + + mod.respondSuccess(res, { ok: true }); + + assert.strictEqual(res.state.statusCode, 200); + assert.deepStrictEqual(JSON.parse(JSON.stringify(res.state.jsonBody)), { + ok: true + }); +}); + +test("apiResponse.respondError includes details outside production", () => { + const original = process.env.NODE_ENV; + process.env.NODE_ENV = "development"; + + const mod = loadModule("pages/api/middleware/apiResponse.js"); + const res = createRes(); + mod.respondError(res, { + status: 400, + code: "INVALID_HASH", + message: "Invalid hash", + details: { expected: "x", actual: "y" } + }); + + assert.strictEqual(res.state.statusCode, 400); + assert.deepStrictEqual(JSON.parse(JSON.stringify(res.state.jsonBody)), { + success: false, + error: { + code: "INVALID_HASH", + message: "Invalid hash", + details: { expected: "x", actual: "y" } + } + }); + + process.env.NODE_ENV = original; +}); + +test("apiResponse.respondError suppresses details in production", () => { + const original = process.env.NODE_ENV; + process.env.NODE_ENV = "production"; + + const mod = loadModule("pages/api/middleware/apiResponse.js"); + const res = createRes(); + mod.respondError(res, { + status: 500, + code: "X", + message: "Y", + details: { shouldNot: "appear" } + }); + + assert.strictEqual(res.state.statusCode, 500); + assert.deepStrictEqual(JSON.parse(JSON.stringify(res.state.jsonBody)), { + success: false, + error: { + code: "X", + message: "Y" + } + }); + + process.env.NODE_ENV = original; +}); + +test("upload handler returns HASH_REQUIRED for missing hash", async () => { + const mod = loadModule("pages/api/file/upload.js", { + nextConnect: createNextConnectMock(), + middleware: () => {}, + 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: () => {}, + 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("deleteblob handler returns MISSING_REQUIRED_QUERY when blobname missing", async () => { + const mod = loadModule("pages/api/file/deleteblob.js", { + nextConnect: createNextConnectMock(), + middleware: () => {}, + hashAPIPath: () => "&hash=expected", + respondError: respondErrorMock, + respondSuccess: respondSuccessMock, + deleteBlob: async () => ({ ok: true }) + }); + + 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, "MISSING_REQUIRED_QUERY"); +}); + +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("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"); +}); + +const run = async () => { + let passed = 0; + + for (const currentTest of tests) { + await currentTest.fn(); + passed += 1; + } + + console.log(`Phase 21 Slice 1 tests passed (${passed}/${tests.length}).`); +}; + +run().catch((error) => { + console.error(error); + process.exit(1); +}); From 991cbf73ba2a7d7fe0f8e26c0edb83bfe7e3a0aa Mon Sep 17 00:00:00 2001 From: robbond Date: Tue, 17 Mar 2026 13:57:48 +0000 Subject: [PATCH 2/4] TASK22104: slice2 extend api contract success and download tests --- tests/phase21/api-contract-slice1.test.cjs | 113 ++++++++++++++++++++- 1 file changed, 112 insertions(+), 1 deletion(-) diff --git a/tests/phase21/api-contract-slice1.test.cjs b/tests/phase21/api-contract-slice1.test.cjs index 755cf87f..4eea123c 100644 --- a/tests/phase21/api-contract-slice1.test.cjs +++ b/tests/phase21/api-contract-slice1.test.cjs @@ -44,7 +44,9 @@ const loadModule = (relativePath, injected = {}) => { const createRes = () => { const state = { statusCode: null, - jsonBody: undefined + jsonBody: undefined, + sentBody: undefined, + headers: {} }; return { @@ -56,6 +58,13 @@ const createRes = () => { json(payload) { state.jsonBody = payload; return payload; + }, + send(payload) { + state.sentBody = payload; + return payload; + }, + setHeader(key, value) { + state.headers[key.toLowerCase()] = value; } }; }; @@ -260,6 +269,108 @@ test("setupcontainer handler returns MISSING_REQUIRED_QUERY when ident missing", assert.strictEqual(res.state.jsonBody.error.code, "MISSING_REQUIRED_QUERY"); }); +test("upload handler success returns data payload with 200", async () => { + const mod = loadModule("pages/api/file/upload.js", { + nextConnect: createNextConnectMock(), + middleware: () => {}, + 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 run = async () => { let passed = 0; From 0e99d9548bcf69f92227515766ce3f7aa1d3357d Mon Sep 17 00:00:00 2001 From: robbond Date: Tue, 17 Mar 2026 14:00:09 +0000 Subject: [PATCH 3/4] TASK22104: slice3 add failure-path api contract tests --- tests/phase21/api-contract-slice1.test.cjs | 66 ++++++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/tests/phase21/api-contract-slice1.test.cjs b/tests/phase21/api-contract-slice1.test.cjs index 4eea123c..406c8187 100644 --- a/tests/phase21/api-contract-slice1.test.cjs +++ b/tests/phase21/api-contract-slice1.test.cjs @@ -371,6 +371,72 @@ test("downloadblob handler success sets attachment header and returns body", asy assert.strictEqual(res.state.sentBody.toString(), "file-content"); }); +test("apiResponse.respondError default contract returns 500 envelope", () => { + const mod = loadModule("pages/api/middleware/apiResponse.js"); + const res = createRes(); + + mod.respondError(res); + + assert.strictEqual(res.state.statusCode, 500); + assert.deepStrictEqual(JSON.parse(JSON.stringify(res.state.jsonBody)), { + success: false, + error: { + code: "INTERNAL_SERVER_ERROR", + message: "Request failed" + } + }); +}); + +test("upload handler dependency failure returns UPLOAD_FAILED", async () => { + const mod = loadModule("pages/api/file/upload.js", { + nextConnect: createNextConnectMock(), + middleware: () => {}, + 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"); +}); + const run = async () => { let passed = 0; From 16a54931097ca48d14404d8b7ca8e511ebe68609 Mon Sep 17 00:00:00 2001 From: robbond Date: Tue, 17 Mar 2026 14:06:57 +0000 Subject: [PATCH 4/4] TASK22104: slice4 split phase21 helper and handler contract tests --- tests/phase21/_shared.cjs | 110 +++++ tests/phase21/api-contract-slice1.test.cjs | 451 +------------------ tests/phase21/api-response-helper.test.cjs | 102 +++++ tests/phase21/file-handler-contract.test.cjs | 284 ++++++++++++ 4 files changed, 501 insertions(+), 446 deletions(-) create mode 100644 tests/phase21/_shared.cjs create mode 100644 tests/phase21/api-response-helper.test.cjs create mode 100644 tests/phase21/file-handler-contract.test.cjs diff --git a/tests/phase21/_shared.cjs b/tests/phase21/_shared.cjs new file mode 100644 index 00000000..9cf1aaab --- /dev/null +++ b/tests/phase21/_shared.cjs @@ -0,0 +1,110 @@ +const fs = require("fs"); +const path = require("path"); +const vm = require("vm"); + +const rootDir = path.resolve(__dirname, "..", ".."); + +const loadModule = (relativePath, injected = {}) => { + const filePath = path.join(rootDir, relativePath); + let source = fs.readFileSync(filePath, "utf8"); + + source = source.replace(/import[\s\S]*?from\s+"[^"]+";\n?/g, ""); + source = source.replace(/export const\s+/g, "const "); + source = source.replace( + /export default\s+(\w+);/g, + "module.exports.default = $1;" + ); + + source += + '\nif (typeof respondSuccess !== "undefined") module.exports.respondSuccess = respondSuccess;\n'; + source += + '\nif (typeof respondError !== "undefined") module.exports.respondError = respondError;\n'; + source += + '\nif (typeof ApiProxy !== "undefined" && !module.exports.default) module.exports.default = ApiProxy;\n'; + + const context = { + module: { exports: {} }, + exports: {}, + require, + process, + console: { + log: () => {}, + info: () => {}, + warn: () => {}, + error: () => {} + }, + ...injected + }; + + vm.runInNewContext(source, context, { filename: filePath }); + return context.module.exports; +}; + +const createRes = () => { + const state = { + statusCode: null, + jsonBody: undefined, + sentBody: undefined, + headers: {} + }; + + return { + state, + status(code) { + state.statusCode = code; + return this; + }, + json(payload) { + state.jsonBody = payload; + return payload; + }, + send(payload) { + state.sentBody = payload; + return payload; + }, + setHeader(key, value) { + state.headers[key.toLowerCase()] = value; + } + }; +}; + +const createNextConnectMock = () => { + const router = { + handler: null, + use: () => {}, + get(fn) { + this.handler = fn; + }, + post(fn) { + this.handler = fn; + } + }; + + return () => router; +}; + +const respondSuccessMock = (res, data, status = 200) => { + return res.status(status).json(data); +}; + +const respondErrorMock = ( + res, + { + status = 500, + code = "INTERNAL_SERVER_ERROR", + message = "Request failed" + } = {} +) => { + return res.status(status).json({ + success: false, + error: { code, message } + }); +}; + +module.exports = { + loadModule, + createRes, + createNextConnectMock, + respondSuccessMock, + respondErrorMock +}; diff --git a/tests/phase21/api-contract-slice1.test.cjs b/tests/phase21/api-contract-slice1.test.cjs index 406c8187..5d493fb0 100644 --- a/tests/phase21/api-contract-slice1.test.cjs +++ b/tests/phase21/api-contract-slice1.test.cjs @@ -1,451 +1,10 @@ -const fs = require("fs"); -const path = require("path"); -const vm = require("vm"); -const assert = require("assert"); - -const rootDir = path.resolve(__dirname, "..", ".."); - -const loadModule = (relativePath, injected = {}) => { - const filePath = path.join(rootDir, relativePath); - let source = fs.readFileSync(filePath, "utf8"); - - source = source.replace(/import[\s\S]*?from\s+"[^"]+";\n?/g, ""); - source = source.replace(/export const\s+/g, "const "); - source = source.replace( - /export default\s+(\w+);/g, - "module.exports.default = $1;" - ); - - source += - '\nif (typeof respondSuccess !== "undefined") module.exports.respondSuccess = respondSuccess;\n'; - source += - '\nif (typeof respondError !== "undefined") module.exports.respondError = respondError;\n'; - source += - '\nif (typeof ApiProxy !== "undefined" && !module.exports.default) module.exports.default = ApiProxy;\n'; - - const context = { - module: { exports: {} }, - exports: {}, - require, - process, - console: { - log: () => {}, - info: () => {}, - warn: () => {}, - error: () => {} - }, - ...injected - }; - - vm.runInNewContext(source, context, { filename: filePath }); - return context.module.exports; -}; - -const createRes = () => { - const state = { - statusCode: null, - jsonBody: undefined, - sentBody: undefined, - headers: {} - }; - - return { - state, - status(code) { - state.statusCode = code; - return this; - }, - json(payload) { - state.jsonBody = payload; - return payload; - }, - send(payload) { - state.sentBody = payload; - return payload; - }, - setHeader(key, value) { - state.headers[key.toLowerCase()] = value; - } - }; -}; - -const createNextConnectMock = () => { - const router = { - handler: null, - use: () => {}, - get(fn) { - this.handler = fn; - }, - post(fn) { - this.handler = fn; - } - }; - - return () => router; -}; - -const respondSuccessMock = (res, data, status = 200) => { - return res.status(status).json(data); -}; - -const respondErrorMock = ( - res, - { - status = 500, - code = "INTERNAL_SERVER_ERROR", - message = "Request failed" - } = {} -) => { - return res.status(status).json({ - success: false, - error: { code, message } - }); -}; - -const tests = []; -const test = (name, fn) => tests.push({ name, fn }); - -test("apiResponse.respondSuccess returns data with default 200", () => { - const mod = loadModule("pages/api/middleware/apiResponse.js"); - const res = createRes(); - - mod.respondSuccess(res, { ok: true }); - - assert.strictEqual(res.state.statusCode, 200); - assert.deepStrictEqual(JSON.parse(JSON.stringify(res.state.jsonBody)), { - ok: true - }); -}); - -test("apiResponse.respondError includes details outside production", () => { - const original = process.env.NODE_ENV; - process.env.NODE_ENV = "development"; - - const mod = loadModule("pages/api/middleware/apiResponse.js"); - const res = createRes(); - mod.respondError(res, { - status: 400, - code: "INVALID_HASH", - message: "Invalid hash", - details: { expected: "x", actual: "y" } - }); - - assert.strictEqual(res.state.statusCode, 400); - assert.deepStrictEqual(JSON.parse(JSON.stringify(res.state.jsonBody)), { - success: false, - error: { - code: "INVALID_HASH", - message: "Invalid hash", - details: { expected: "x", actual: "y" } - } - }); - - process.env.NODE_ENV = original; -}); - -test("apiResponse.respondError suppresses details in production", () => { - const original = process.env.NODE_ENV; - process.env.NODE_ENV = "production"; - - const mod = loadModule("pages/api/middleware/apiResponse.js"); - const res = createRes(); - mod.respondError(res, { - status: 500, - code: "X", - message: "Y", - details: { shouldNot: "appear" } - }); - - assert.strictEqual(res.state.statusCode, 500); - assert.deepStrictEqual(JSON.parse(JSON.stringify(res.state.jsonBody)), { - success: false, - error: { - code: "X", - message: "Y" - } - }); - - process.env.NODE_ENV = original; -}); - -test("upload handler returns HASH_REQUIRED for missing hash", async () => { - const mod = loadModule("pages/api/file/upload.js", { - nextConnect: createNextConnectMock(), - middleware: () => {}, - 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: () => {}, - 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("deleteblob handler returns MISSING_REQUIRED_QUERY when blobname missing", async () => { - const mod = loadModule("pages/api/file/deleteblob.js", { - nextConnect: createNextConnectMock(), - middleware: () => {}, - hashAPIPath: () => "&hash=expected", - respondError: respondErrorMock, - respondSuccess: respondSuccessMock, - deleteBlob: async () => ({ ok: true }) - }); - - 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, "MISSING_REQUIRED_QUERY"); -}); - -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("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("upload handler success returns data payload with 200", async () => { - const mod = loadModule("pages/api/file/upload.js", { - nextConnect: createNextConnectMock(), - middleware: () => {}, - 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"); -}); - -test("apiResponse.respondError default contract returns 500 envelope", () => { - const mod = loadModule("pages/api/middleware/apiResponse.js"); - const res = createRes(); - - mod.respondError(res); - - assert.strictEqual(res.state.statusCode, 500); - assert.deepStrictEqual(JSON.parse(JSON.stringify(res.state.jsonBody)), { - success: false, - error: { - code: "INTERNAL_SERVER_ERROR", - message: "Request failed" - } - }); -}); - -test("upload handler dependency failure returns UPLOAD_FAILED", async () => { - const mod = loadModule("pages/api/file/upload.js", { - nextConnect: createNextConnectMock(), - middleware: () => {}, - 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"); -}); +const runHelperTests = require("./api-response-helper.test.cjs"); +const runHandlerTests = require("./file-handler-contract.test.cjs"); const run = async () => { - let passed = 0; - - for (const currentTest of tests) { - await currentTest.fn(); - passed += 1; - } - - console.log(`Phase 21 Slice 1 tests passed (${passed}/${tests.length}).`); + await runHelperTests(); + await runHandlerTests(); + console.log("Phase 21 combined suite passed."); }; run().catch((error) => { diff --git a/tests/phase21/api-response-helper.test.cjs b/tests/phase21/api-response-helper.test.cjs new file mode 100644 index 00000000..2ea205da --- /dev/null +++ b/tests/phase21/api-response-helper.test.cjs @@ -0,0 +1,102 @@ +const assert = require("assert"); +const { loadModule, createRes } = require("./_shared.cjs"); + +const tests = []; +const test = (name, fn) => tests.push({ name, fn }); + +test("apiResponse.respondSuccess returns data with default 200", () => { + const mod = loadModule("pages/api/middleware/apiResponse.js"); + const res = createRes(); + + mod.respondSuccess(res, { ok: true }); + + assert.strictEqual(res.state.statusCode, 200); + assert.deepStrictEqual(JSON.parse(JSON.stringify(res.state.jsonBody)), { + ok: true + }); +}); + +test("apiResponse.respondError includes details outside production", () => { + const original = process.env.NODE_ENV; + process.env.NODE_ENV = "development"; + + const mod = loadModule("pages/api/middleware/apiResponse.js"); + const res = createRes(); + mod.respondError(res, { + status: 400, + code: "INVALID_HASH", + message: "Invalid hash", + details: { expected: "x", actual: "y" } + }); + + assert.strictEqual(res.state.statusCode, 400); + assert.deepStrictEqual(JSON.parse(JSON.stringify(res.state.jsonBody)), { + success: false, + error: { + code: "INVALID_HASH", + message: "Invalid hash", + details: { expected: "x", actual: "y" } + } + }); + + process.env.NODE_ENV = original; +}); + +test("apiResponse.respondError suppresses details in production", () => { + const original = process.env.NODE_ENV; + process.env.NODE_ENV = "production"; + + const mod = loadModule("pages/api/middleware/apiResponse.js"); + const res = createRes(); + mod.respondError(res, { + status: 500, + code: "X", + message: "Y", + details: { shouldNot: "appear" } + }); + + assert.strictEqual(res.state.statusCode, 500); + assert.deepStrictEqual(JSON.parse(JSON.stringify(res.state.jsonBody)), { + success: false, + error: { + code: "X", + message: "Y" + } + }); + + process.env.NODE_ENV = original; +}); + +test("apiResponse.respondError default contract returns 500 envelope", () => { + const mod = loadModule("pages/api/middleware/apiResponse.js"); + const res = createRes(); + + mod.respondError(res); + + assert.strictEqual(res.state.statusCode, 500); + assert.deepStrictEqual(JSON.parse(JSON.stringify(res.state.jsonBody)), { + success: false, + error: { + code: "INTERNAL_SERVER_ERROR", + message: "Request failed" + } + }); +}); + +const run = async () => { + let passed = 0; + for (const currentTest of tests) { + await currentTest.fn(); + passed += 1; + } + console.log(`Phase 21 helper tests passed (${passed}/${tests.length}).`); +}; + +module.exports = run; + +if (require.main === module) { + run().catch((error) => { + console.error(error); + process.exit(1); + }); +} diff --git a/tests/phase21/file-handler-contract.test.cjs b/tests/phase21/file-handler-contract.test.cjs new file mode 100644 index 00000000..cc0f9ee9 --- /dev/null +++ b/tests/phase21/file-handler-contract.test.cjs @@ -0,0 +1,284 @@ +const assert = require("assert"); +const { + loadModule, + createRes, + createNextConnectMock, + respondSuccessMock, + respondErrorMock +} = require("./_shared.cjs"); + +const tests = []; +const test = (name, fn) => tests.push({ name, fn }); + +test("upload handler returns HASH_REQUIRED for missing hash", async () => { + const mod = loadModule("pages/api/file/upload.js", { + nextConnect: createNextConnectMock(), + middleware: () => {}, + 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: () => {}, + 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("deleteblob handler returns MISSING_REQUIRED_QUERY when blobname missing", async () => { + const mod = loadModule("pages/api/file/deleteblob.js", { + nextConnect: createNextConnectMock(), + middleware: () => {}, + hashAPIPath: () => "&hash=expected", + respondError: respondErrorMock, + respondSuccess: respondSuccessMock, + deleteBlob: async () => ({ ok: true }) + }); + + 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, "MISSING_REQUIRED_QUERY"); +}); + +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("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("upload handler success returns data payload with 200", async () => { + const mod = loadModule("pages/api/file/upload.js", { + nextConnect: createNextConnectMock(), + middleware: () => {}, + 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"); +}); + +test("upload handler dependency failure returns UPLOAD_FAILED", async () => { + const mod = loadModule("pages/api/file/upload.js", { + nextConnect: createNextConnectMock(), + middleware: () => {}, + 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"); +}); + +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); + }); +}