Files
pedwfrontend/tests/phase22/azurestorage-helper-behaviour.test.cjs
T

357 lines
11 KiB
JavaScript

const fs = require("fs");
const path = require("path");
const vm = require("vm");
const assert = require("assert");
const rootDir = path.resolve(__dirname, "..", "..");
const loadAzureStorageHelperModule = (injected = {}) => {
const filePath = path.join(rootDir, "actions", "azurestorage.js");
const source = fs.readFileSync(filePath, "utf8");
const start = source.indexOf("const buildDownloadBlobQueryPath =");
const end = source.indexOf("export const createContainerSas =");
if (start < 0 || end < 0 || end <= start) {
throw new Error("Unable to isolate azurestorage helper function block");
}
let helperSource = source.slice(start, end);
helperSource +=
"\nmodule.exports = { buildDownloadBlobQueryPath, buildDeleteBlobQueryPath, buildGetBlobListQueryPath, buildHashMetadataPaths };\n";
const context = {
module: { exports: {} },
exports: {},
require,
encodeURIComponent,
hashAPIPath: (route) => `HASH(${route})`,
...injected
};
vm.runInNewContext(helperSource, context, { filename: filePath });
return context.module.exports;
};
const loadRecursiveDeletionHelpers = ({
blobNames = [],
deleteBlob = () => Promise.resolve()
} = {}) => {
const filePath = path.join(rootDir, "actions", "azurestorage.js");
const source = fs.readFileSync(filePath, "utf8");
const start = source.indexOf("const isSafeDeletionPathSegment =");
const end = source.indexOf("export const uploadFile =");
if (start < 0 || end < 0 || end <= start) {
throw new Error("Unable to isolate recursive deletion helper block");
}
const listedPrefixes = [];
const deletedBlobNames = [];
const containerClient = {
async *listBlobsFlat({ prefix }) {
listedPrefixes.push(prefix);
for (const name of blobNames.filter((blobName) =>
blobName.startsWith(prefix)
)) {
yield { name };
}
},
deleteBlob(name) {
deletedBlobNames.push(name);
return deleteBlob(name);
},
getBlockBlobClient() {
return { exists: async () => true };
}
};
let helperSource = source
.slice(start, end)
.replace(/export const /g, "const ");
helperSource +=
"\nmodule.exports = { deleteBlobCase, deleteBlobRep };\n";
const context = {
module: { exports: {} },
exports: {},
console: { log: () => {} },
DefaultAzureCredential: class {},
createContainerSas: async () => "sas-token",
STORAGE_PATH: "https://storage.example.test",
ContainerClient: class {
constructor() {
return containerClient;
}
}
};
vm.runInNewContext(helperSource, context, { filename: filePath });
return {
...context.module.exports,
listedPrefixes,
deletedBlobNames
};
};
const tests = [];
const test = (name, fn) => tests.push({ name, fn });
const createDeferred = () => {
let resolve;
let reject;
const promise = new Promise((resolvePromise, rejectPromise) => {
resolve = resolvePromise;
reject = rejectPromise;
});
return { promise, resolve, reject };
};
const flushMicrotasks = async () => {
await Promise.resolve();
await Promise.resolve();
};
test("azurestorage helper builds download path with default encoding", () => {
const mod = loadAzureStorageHelperModule();
const pathResult = mod.buildDownloadBlobQueryPath({
containerName: "alpha",
casefolderID: "A/B",
blobname: "my doc.pdf"
});
assert.strictEqual(
pathResult,
"/api/file/downloadblob?container=alpha&casefolderID=A%2FB&blobname=my%20doc.pdf"
);
});
test("azurestorage helper preserves raw values when encoding disabled", () => {
const mod = loadAzureStorageHelperModule();
const deletePath = mod.buildDeleteBlobQueryPath({
containerName: "alpha",
casefolderID: "A/B",
blobname: "my doc.pdf",
encodeCasefolderID: false,
encodeBlobname: false
});
assert.strictEqual(
deletePath,
"/api/file/deleteblob?container=alpha&casefolderID=A/B&blobname=my doc.pdf"
);
});
test("azurestorage helper builds getbloblist path without field mutation", () => {
const mod = loadAzureStorageHelperModule();
const listPath = mod.buildGetBlobListQueryPath({
containerName: "alpha",
casefolderID: "A/B"
});
assert.strictEqual(
listPath,
"/api/file/getbloblist?container=alpha&casefolderID=A/B"
);
});
test("azurestorage helper builds hash metadata map with stable keys", () => {
const mod = loadAzureStorageHelperModule({
hashAPIPath: (route) => `signed:${route}`
});
const metadata = mod.buildHashMetadataPaths({
containerName: "alpha",
casefolderID: "A/B",
blobname: "my doc.pdf"
});
assert.deepStrictEqual(JSON.parse(JSON.stringify(metadata)), {
hashedfilepath:
"signed:/api/file/downloadblob?container=alpha&casefolderID=A%2FB&blobname=my%20doc.pdf",
hasheddeletepath:
"signed:/api/file/deleteblob?container=alpha&casefolderID=A%2FB&blobname=my%20doc.pdf",
hashgetblobs:
"signed:/api/file/getbloblist?container=alpha&casefolderID=A/B"
});
});
test("case deletion uses a complete folder prefix and excludes lexical siblings", async () => {
const loaded = loadRecursiveDeletionHelpers({
blobNames: [
"TMP-ABC/TMP-ABC_appeal.json",
"TMP-ABC/files/evidence.pdf",
"TMP-ABC1/TMP-ABC1_appeal.json",
"TMP-ABCD/files/sibling.pdf"
]
});
await loaded.deleteBlobCase("container-1", "TMP-ABC");
assert.deepStrictEqual(loaded.listedPrefixes, ["TMP-ABC/"]);
assert.deepStrictEqual(loaded.deletedBlobNames, [
"TMP-ABC/TMP-ABC_appeal.json",
"TMP-ABC/files/evidence.pdf"
]);
});
test("representation deletion uses a complete folder prefix and preserves parent and siblings", async () => {
const loaded = loadRecursiveDeletionHelpers({
blobNames: [
"CASE-1/CASE-1_case.json",
"CASE-1/REP-1/REP-1_rep.json",
"CASE-1/REP-1/files/evidence.pdf",
"CASE-1/REP-10/REP-10_rep.json",
"CASE-1/REP-2/REP-2_rep.json"
]
});
await loaded.deleteBlobRep("container-1", "CASE-1/REP-1");
assert.deepStrictEqual(loaded.listedPrefixes, ["CASE-1/REP-1/"]);
assert.deepStrictEqual(loaded.deletedBlobNames, [
"CASE-1/REP-1/REP-1_rep.json",
"CASE-1/REP-1/files/evidence.pdf"
]);
});
test("recursive deletion helpers reject empty and unsafe resource paths before listing", async () => {
for (const [helperName, unsafePath] of [
["deleteBlobCase", ""],
["deleteBlobCase", "CASE-1/REP-1"],
["deleteBlobRep", "CASE-1"],
["deleteBlobRep", "CASE-1/REP-1/FILES"],
["deleteBlobRep", "CASE-1/../REP-1"]
]) {
const loaded = loadRecursiveDeletionHelpers();
await assert.rejects(
() => loaded[helperName]("container-1", unsafePath),
/Invalid deletion resource path/
);
assert.deepStrictEqual(loaded.listedPrefixes, []);
}
});
for (const deletionCase of [
{
helperName: "deleteBlobCase",
resourcePath: "TMP-ABC",
blobName: "TMP-ABC/files/evidence.pdf"
},
{
helperName: "deleteBlobRep",
resourcePath: "CASE-1/REP-1",
blobName: "CASE-1/REP-1/files/evidence.pdf"
}
]) {
test(`${deletionCase.helperName} remains pending until Azure deletion completes`, async () => {
const deletion = createDeferred();
const deletionStarted = createDeferred();
const loaded = loadRecursiveDeletionHelpers({
blobNames: [deletionCase.blobName],
deleteBlob: () => {
deletionStarted.resolve();
return deletion.promise;
}
});
let settled = false;
const helperPromise = loaded[
deletionCase.helperName
]("container-1", deletionCase.resourcePath).then((result) => {
settled = true;
return result;
});
await deletionStarted.promise;
await flushMicrotasks();
assert.strictEqual(settled, false);
deletion.resolve();
assert.deepStrictEqual(
JSON.parse(JSON.stringify(await helperPromise)),
{ deleted: deletionCase.resourcePath }
);
});
test(`${deletionCase.helperName} propagates Azure deletion rejection`, async () => {
const deletionError = new Error("controlled Azure deletion failure");
const rejectedDeletion = Promise.reject(deletionError);
rejectedDeletion.catch(() => {});
const loaded = loadRecursiveDeletionHelpers({
blobNames: [deletionCase.blobName],
deleteBlob: () => rejectedDeletion
});
await assert.rejects(
() =>
loaded[deletionCase.helperName](
"container-1",
deletionCase.resourcePath
),
/controlled Azure deletion failure/
);
});
}
test("recursive deletion does not complete until all selected blob deletions complete", async () => {
const firstDeletion = createDeferred();
const secondDeletion = createDeferred();
const deletionPromises = new Map([
["TMP-ABC/files/first.pdf", firstDeletion.promise],
["TMP-ABC/files/second.pdf", secondDeletion.promise]
]);
const loaded = loadRecursiveDeletionHelpers({
blobNames: [...deletionPromises.keys()],
deleteBlob: (name) => deletionPromises.get(name)
});
let settled = false;
const helperPromise = loaded
.deleteBlobCase("container-1", "TMP-ABC")
.then((result) => {
settled = true;
return result;
});
await flushMicrotasks();
assert.strictEqual(settled, false);
firstDeletion.resolve();
await flushMicrotasks();
assert.strictEqual(settled, false);
secondDeletion.resolve();
assert.deepStrictEqual(
JSON.parse(JSON.stringify(await helperPromise)),
{ deleted: "TMP-ABC" }
);
});
const run = async () => {
let passed = 0;
for (const currentTest of tests) {
await currentTest.fn();
passed += 1;
}
console.log(
`Phase 22 azurestorage-helper tests passed (${passed}/${tests.length}).`
);
};
module.exports = run;
if (require.main === module) {
run().catch((error) => {
console.error(error);
process.exit(1);
});
}