3310 lines
112 KiB
JavaScript
3310 lines
112 KiB
JavaScript
const assert = require("assert");
|
|
const fs = require("fs");
|
|
const path = require("path");
|
|
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: () => {},
|
|
getSession: async () => ({ user: { id: "c1" } }),
|
|
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: () => {},
|
|
getSession: async () => ({ user: { id: "c1" } }),
|
|
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: () => {},
|
|
getSession: async () => ({ user: { id: "c1" } }),
|
|
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 = { createBlob: [], createRepBlob: [], hash: [] };
|
|
const mod = loadModule("pages/api/file/upload.js", {
|
|
nextConnect: createNextConnectMock(),
|
|
middleware: () => {},
|
|
getSession: async () => ({ 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 };
|
|
};
|
|
|
|
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({ getSession: 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({
|
|
getSession: 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 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: () => {},
|
|
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("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: () => {},
|
|
hashAPIPath: (queryPath) =>
|
|
queryPath.includes("/api/file/deleteblob?container=c1")
|
|
? "&hash=expected"
|
|
: "&hash=other",
|
|
respondError: respondErrorMock,
|
|
respondSuccess: respondSuccessMock,
|
|
deleteBlob: async (_container, blobPath) => {
|
|
deletedPath = blobPath;
|
|
return { deleted: true };
|
|
}
|
|
});
|
|
|
|
const req = {
|
|
query: {
|
|
container: "c1",
|
|
casefolderID: "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: () => {},
|
|
hashAPIPath: () => "&hash=expected",
|
|
respondError: respondErrorMock,
|
|
respondSuccess: respondSuccessMock,
|
|
deleteBlob: async () => {
|
|
throw new Error("delete 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, "DELETE_BLOB_FAILED");
|
|
});
|
|
|
|
test("deleteblobcase handler accepts encoded hash variant", async () => {
|
|
let deletedCaseFolder = null;
|
|
const mod = loadModule("pages/api/file/deleteblobcase.js", {
|
|
nextConnect: createNextConnectMock(),
|
|
middleware: () => {},
|
|
getSession: async () => ({ user: { id: "c1" } }),
|
|
hashAPIPath: (queryPath) =>
|
|
queryPath.includes("casefolderID=CASE%2FSUB")
|
|
? "&hash=expected"
|
|
: "&hash=other",
|
|
respondError: respondErrorMock,
|
|
respondSuccess: respondSuccessMock,
|
|
deleteBlobCase: async (_container, casefolder) => {
|
|
deletedCaseFolder = casefolder;
|
|
return { deleted: true };
|
|
}
|
|
});
|
|
|
|
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.strictEqual(deletedCaseFolder, "CASE/SUB");
|
|
});
|
|
|
|
test("deleteblobcase handler dependency failure returns DELETE_BLOB_CASE_FAILED", async () => {
|
|
const mod = loadModule("pages/api/file/deleteblobcase.js", {
|
|
nextConnect: createNextConnectMock(),
|
|
middleware: () => {},
|
|
getSession: async () => ({ user: { id: "c1" } }),
|
|
hashAPIPath: () => "&hash=expected",
|
|
respondError: respondErrorMock,
|
|
respondSuccess: respondSuccessMock,
|
|
deleteBlobCase: async () => {
|
|
throw new Error("delete case failed");
|
|
}
|
|
});
|
|
|
|
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,
|
|
"DELETE_BLOB_CASE_FAILED"
|
|
);
|
|
});
|
|
|
|
test("deleteblobrep handler accepts encoded hash variant", async () => {
|
|
let deletedRepPath = null;
|
|
const mod = loadModule("pages/api/file/deleteblobrep.js", {
|
|
nextConnect: createNextConnectMock(),
|
|
middleware: () => {},
|
|
getSession: async () => ({ user: { id: "c1" } }),
|
|
hashAPIPath: (queryPath) =>
|
|
queryPath.includes("repfile=Rep%20Name")
|
|
? "&hash=expected"
|
|
: "&hash=other",
|
|
respondError: respondErrorMock,
|
|
respondSuccess: respondSuccessMock,
|
|
deleteBlobRep: async (_container, repPath) => {
|
|
deletedRepPath = repPath;
|
|
return { deleted: true };
|
|
}
|
|
});
|
|
|
|
const req = {
|
|
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 handler dependency failure returns DELETE_BLOB_REP_FAILED", async () => {
|
|
const mod = loadModule("pages/api/file/deleteblobrep.js", {
|
|
nextConnect: createNextConnectMock(),
|
|
middleware: () => {},
|
|
getSession: async () => ({ user: { id: "c1" } }),
|
|
hashAPIPath: () => "&hash=expected",
|
|
respondError: respondErrorMock,
|
|
respondSuccess: respondSuccessMock,
|
|
deleteBlobRep: async () => {
|
|
throw new Error("delete rep failed");
|
|
}
|
|
});
|
|
|
|
const req = {
|
|
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 ordinaryDeletionCases = [
|
|
{
|
|
name: "deleteblobcase",
|
|
route: "pages/api/file/deleteblobcase.js",
|
|
query: {
|
|
container: "user-container",
|
|
casefolderID: "CASE-1",
|
|
hash: "expected"
|
|
},
|
|
dependency: "deleteBlobCase"
|
|
},
|
|
{
|
|
name: "deleteblobrep",
|
|
route: "pages/api/file/deleteblobrep.js",
|
|
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: () => {},
|
|
getSession: async () => null,
|
|
hashAPIPath: () => "&hash=expected",
|
|
respondError: respondErrorMock,
|
|
respondSuccess: respondSuccessMock,
|
|
[deletionCase.dependency]: async () => {
|
|
deletionCalls += 1;
|
|
}
|
|
});
|
|
const res = createRes();
|
|
|
|
await mod.default.handler({ 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: () => {},
|
|
getSession: async () => ({ user: {} }),
|
|
hashAPIPath: () => "&hash=expected",
|
|
respondError: respondErrorMock,
|
|
respondSuccess: respondSuccessMock,
|
|
[deletionCase.dependency]: async () => {
|
|
deletionCalls += 1;
|
|
}
|
|
});
|
|
const res = createRes();
|
|
|
|
await mod.default.handler({ 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: () => {},
|
|
getSession: async () => ({ user: { id: "different-user" } }),
|
|
hashAPIPath: () => {
|
|
hashCalls += 1;
|
|
return "&hash=expected";
|
|
},
|
|
respondError: respondErrorMock,
|
|
respondSuccess: respondSuccessMock,
|
|
[deletionCase.dependency]: async () => {
|
|
deletionCalls += 1;
|
|
}
|
|
});
|
|
const res = createRes();
|
|
|
|
await mod.default.handler({ query: deletionCase.query }, res);
|
|
|
|
assert.strictEqual(res.state.statusCode, 403);
|
|
assert.strictEqual(hashCalls, 0);
|
|
assert.strictEqual(deletionCalls, 0);
|
|
});
|
|
|
|
test(`${deletionCase.name} rejects invalid HMAC for the owning user without deletion`, async () => {
|
|
let deletionCalls = 0;
|
|
const mod = loadModule(deletionCase.route, {
|
|
nextConnect: createNextConnectMock(),
|
|
middleware: () => {},
|
|
getSession: async () => ({ user: { id: "user-container" } }),
|
|
hashAPIPath: () => "&hash=different",
|
|
respondError: respondErrorMock,
|
|
respondSuccess: respondSuccessMock,
|
|
[deletionCase.dependency]: async () => {
|
|
deletionCalls += 1;
|
|
}
|
|
});
|
|
const res = createRes();
|
|
|
|
await mod.default.handler({ 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: () => {},
|
|
getSession: async () => ({ user: { id: "user-container" } }),
|
|
hashAPIPath: () => "&hash=expected",
|
|
respondError: respondErrorMock,
|
|
respondSuccess: respondSuccessMock,
|
|
[deletionCase.dependency]: async (...args) => {
|
|
deletionCalls.push(args);
|
|
return { deleted: true };
|
|
}
|
|
});
|
|
const res = createRes();
|
|
|
|
await mod.default.handler({ 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]*getSignedFileJson\(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("admin storage case deletion uses dedicated POST route while ordinary callers remain 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\(\s*"\/api\/admin\/deleteblobcase"/);
|
|
assert.match(
|
|
adminSource,
|
|
/"\/api\/admin\/deleteblobcase"[\s\S]*method:\s*"POST"[\s\S]*container:\s*c\.container[\s\S]*casefolderID:\s*folder\.folderPath/
|
|
);
|
|
assert.match(adminSource, /if \(!response\.ok\)/);
|
|
assert.doesNotMatch(adminSource, /deleteAwaitingSubmissionsFromBlob/);
|
|
assert.match(
|
|
ordinarySource,
|
|
/buildFileQuery\("\/api\/file\/deleteblobcase"[\s\S]*getSignedFileJson\(queryUrl\)/
|
|
);
|
|
});
|
|
|
|
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: () => {},
|
|
hashAPIPath: (queryPath) =>
|
|
queryPath.includes("/api/file/deleteblob?container=c1")
|
|
? "&hash=expected"
|
|
: "&hash=other",
|
|
respondError: respondErrorMock,
|
|
respondSuccess: respondSuccessMock,
|
|
deleteBlob: async (_container, blobPath) => {
|
|
deletedPath = blobPath;
|
|
return { deleted: true };
|
|
}
|
|
}
|
|
);
|
|
|
|
const req = {
|
|
query: {
|
|
container: "c1",
|
|
casefolderID: "CASE-1",
|
|
blobname: "CASE-1/files/doc.pdf",
|
|
hash: "expected"
|
|
}
|
|
};
|
|
const res = createRes();
|
|
await mod.default.handler(req, res);
|
|
|
|
assert.strictEqual(res.state.statusCode, 200);
|
|
assert.strictEqual(deletedPath, "CASE-1/files/doc.pdf");
|
|
});
|
|
|
|
test("deleteawaitingsubmissionfromblob dependency failure returns DELETE_AWAITING_SUBMISSION_BLOB_FAILED", async () => {
|
|
const mod = loadModule(
|
|
"pages/api/file/deleteawaitingsubmissionfromblob.js",
|
|
{
|
|
nextConnect: createNextConnectMock(),
|
|
middleware: () => {},
|
|
hashAPIPath: () => "&hash=expected",
|
|
respondError: respondErrorMock,
|
|
respondSuccess: respondSuccessMock,
|
|
deleteBlob: async () => {
|
|
throw new Error("delete awaiting failed");
|
|
}
|
|
}
|
|
);
|
|
|
|
const req = {
|
|
query: {
|
|
container: "c1",
|
|
casefolderID: "CASE-1",
|
|
blobname: "doc.pdf",
|
|
hash: "expected"
|
|
}
|
|
};
|
|
const res = createRes();
|
|
await mod.default.handler(req, res);
|
|
|
|
assert.strictEqual(res.state.statusCode, 400);
|
|
assert.strictEqual(
|
|
res.state.jsonBody.error.code,
|
|
"DELETE_AWAITING_SUBMISSION_BLOB_FAILED"
|
|
);
|
|
});
|
|
|
|
test("getrepsblob handler success returns representations payload", async () => {
|
|
const mod = loadModule("pages/api/file/getrepsblob.js", {
|
|
nextConnect: createNextConnectMock(),
|
|
middleware: () => {},
|
|
hashAPIPath: () => "&hash=expected",
|
|
respondError: respondErrorMock,
|
|
respondSuccess: respondSuccessMock,
|
|
consoleLogger: () => {},
|
|
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: () => {},
|
|
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(),
|
|
getSession: async () => ({ user: { id: "user-container" } }),
|
|
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);
|
|
});
|
|
|
|
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: () => {},
|
|
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: () => {},
|
|
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: () => {},
|
|
getSession: async () => ({ user: { id: "c1" } }),
|
|
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: () => {},
|
|
getSession: async () => ({ user: { id: "c1" } }),
|
|
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 = { upload: [], hash: [], fileReads: 0, typeChecks: 0 };
|
|
const mod = loadModule("pages/api/file/uploadsinglefile.js", {
|
|
nextConnect: createNextConnectMock(),
|
|
middleware: () => {},
|
|
getSession: async () => ({ 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 };
|
|
};
|
|
|
|
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({
|
|
getSession: 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({
|
|
getSession: 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 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);
|
|
});
|
|
|
|
test("getbloblistproxy success returns proxied payload", async () => {
|
|
const mod = loadModule("pages/api/file/getbloblistproxy.js", {
|
|
respondError: respondErrorMock,
|
|
respondSuccess: respondSuccessMock,
|
|
getToken: async () => ({ access_token: "tok" }),
|
|
azureHeaders: () => ({ headers: { Authorization: "Bearer tok" } }),
|
|
hashAPIPath: () => "&hash=abc",
|
|
consoleLogger: () => {},
|
|
process: { env: { API_ROOT: "http://example.com" } },
|
|
axios: {
|
|
get: async () => ({ data: { value: [{ id: "b1" }] } })
|
|
}
|
|
});
|
|
|
|
const req = {
|
|
query: { container: "cont/1", casefolderID: "CASE/1" }
|
|
};
|
|
const res = createRes();
|
|
await mod.default(req, res);
|
|
|
|
assert.strictEqual(res.state.statusCode, 200);
|
|
assert.deepStrictEqual(JSON.parse(JSON.stringify(res.state.jsonBody)), {
|
|
value: [{ id: "b1" }]
|
|
});
|
|
});
|
|
|
|
test("getbloblistproxy dependency failure returns GET_BLOB_LIST_PROXY_FAILED", async () => {
|
|
const mod = loadModule("pages/api/file/getbloblistproxy.js", {
|
|
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("proxy failed");
|
|
}
|
|
}
|
|
});
|
|
|
|
const req = {
|
|
query: { container: "c1", casefolderID: "f1" }
|
|
};
|
|
const res = createRes();
|
|
await mod.default(req, res);
|
|
|
|
assert.strictEqual(res.state.statusCode, 400);
|
|
assert.strictEqual(
|
|
res.state.jsonBody.error.code,
|
|
"GET_BLOB_LIST_PROXY_FAILED"
|
|
);
|
|
});
|
|
|
|
const loadOrdinaryRepsProxy = (overrides = {}) => {
|
|
const calls = { token: 0, hash: [], axios: [] };
|
|
const payload = { "@odata.count": 1, value: [{ id: "r1" }] };
|
|
const mod = loadModule("pages/api/file/getrepsblobproxy.js", {
|
|
respondError: respondErrorMock,
|
|
respondSuccess: respondSuccessMock,
|
|
getSession: async () => ({ 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, 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({
|
|
getSession: 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({
|
|
getSession: 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({ getSession: 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 res = createRes();
|
|
|
|
await loaded.mod.default(ordinaryRepsRequest(), res);
|
|
|
|
assert.strictEqual(res.state.statusCode, 200);
|
|
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(res.state.jsonBody)),
|
|
loaded.payload
|
|
);
|
|
});
|
|
|
|
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,
|
|
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: () => {},
|
|
process: { env: { API_ROOT: "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(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 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\(/);
|
|
});
|
|
|
|
test("getawaitingsubmissionfromblobproxy dependency failure returns GET_AWAITING_SUBMISSION_PROXY_FAILED", async () => {
|
|
const mod = loadModule(
|
|
"pages/api/file/getawaitingsubmissionfromblobproxy.js",
|
|
{
|
|
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("awaiting failed");
|
|
}
|
|
}
|
|
}
|
|
);
|
|
|
|
const req = {
|
|
query: { container: "c1" }
|
|
};
|
|
const res = createRes();
|
|
await mod.default(req, res);
|
|
|
|
assert.strictEqual(res.state.statusCode, 400);
|
|
assert.strictEqual(
|
|
res.state.jsonBody.error.code,
|
|
"GET_AWAITING_SUBMISSION_PROXY_FAILED"
|
|
);
|
|
});
|
|
|
|
test("createappealcompletemessageproxy dependency failure returns CREATE_APPEAL_COMPLETE_MESSAGE_PROXY_FAILED", async () => {
|
|
const mod = loadModule(
|
|
"pages/api/file/createappealcompletemessageproxy_api.js",
|
|
{
|
|
nextConnect: createNextConnectMock(),
|
|
middleware: () => {},
|
|
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: "c1", tempcaseref: "TMP1" }
|
|
};
|
|
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: () => {},
|
|
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 () => ({}),
|
|
updateAccount: async () => ({}),
|
|
consoleLogger: () => {},
|
|
_: { isEmpty: (v) => v === undefined || v === null || v === "" }
|
|
}
|
|
);
|
|
|
|
const req = {
|
|
query: {
|
|
container: "cont/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: () => {},
|
|
respondError: respondErrorMock,
|
|
respondSuccess: respondSuccessMock,
|
|
hashAPIPath: () => "&hash=expected",
|
|
getProgressBlobs: async () => {
|
|
throw new Error("progress failed");
|
|
},
|
|
consoleLogger: () => {},
|
|
_: { isEmpty: () => true }
|
|
}
|
|
);
|
|
|
|
const req = {
|
|
query: {
|
|
container: "c1",
|
|
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: () => {},
|
|
getSession: async () => ({ user: { id: "c1" } }),
|
|
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: () => {},
|
|
getSession: async () => ({ user: { id: "c1" } }),
|
|
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"
|
|
);
|
|
});
|
|
|
|
test("generatepdf catch path returns GENERATE_PDF_FAILED", async () => {
|
|
const { Readable } = require("stream");
|
|
const mod = loadModule("pages/api/file/generatepdf.js", {
|
|
Buffer,
|
|
respondError: respondErrorMock,
|
|
respondSuccess: respondSuccessMock,
|
|
hashAPIPath: () => "&hash=expected",
|
|
consoleLogger: () => {},
|
|
ReactPDF: {
|
|
renderToStream: async () => Readable.from(["pdf"])
|
|
},
|
|
createRepPDFBlob: async () => {
|
|
throw new Error("pdf write failed");
|
|
},
|
|
other_pdf: () => ({ type: "other" })
|
|
});
|
|
|
|
const req = {
|
|
query: { hash: "expected", download: "true" },
|
|
body: {
|
|
representationType: "Other",
|
|
repfile_name: "rep-file",
|
|
containerID: "c1",
|
|
casefolderID: "f1",
|
|
caseRef: "CAS-1"
|
|
}
|
|
};
|
|
const res = createRes();
|
|
await mod.default(req, res);
|
|
|
|
assert.strictEqual(res.state.statusCode, 400);
|
|
assert.strictEqual(res.state.jsonBody.error.code, "GENERATE_PDF_FAILED");
|
|
});
|
|
|
|
test("generateappealpdf catch path returns GENERATE_APPEAL_PDF_FAILED", async () => {
|
|
const mod = loadModule("pages/api/file/generateappealpdf.js", {
|
|
respondError: respondErrorMock,
|
|
respondSuccess: respondSuccessMock,
|
|
hashAPIPath: () => "&hash=expected",
|
|
getTempCaseBlob: async () => ({ ticketnumber: "CAS-1" }),
|
|
getProgressBlobs: async () => ({ path: "CAS-1/progress.json" }),
|
|
downloadProgressFile: async () => {
|
|
throw new Error("progress failed");
|
|
},
|
|
getPickLists: async () => ({}),
|
|
planningappeals78_pdf: () => ({ type: "planning" }),
|
|
other_pdf: () => ({ type: "other" }),
|
|
ReactPDF: {
|
|
renderToStream: async () => {
|
|
throw new Error("render failed");
|
|
}
|
|
}
|
|
});
|
|
|
|
const req = {
|
|
query: { hash: "expected", appealType: "846040000" },
|
|
body: {
|
|
containerID: "c1",
|
|
casefolderID: "f1",
|
|
caseRef: "CAS-1",
|
|
filesList: []
|
|
}
|
|
};
|
|
const res = createRes();
|
|
await mod.default(req, res);
|
|
|
|
assert.strictEqual(res.state.statusCode, 400);
|
|
assert.strictEqual(
|
|
res.state.jsonBody.error.code,
|
|
"GENERATE_APPEAL_PDF_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);
|
|
});
|
|
}
|