From 25ba88ac34b6857fcf318ac5972f11e4a2e96bbc Mon Sep 17 00:00:00 2001 From: robbond Date: Tue, 17 Mar 2026 13:49:05 +0000 Subject: [PATCH] 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); +});