fix(storage): await recursive blob deletions and propagate failures

This commit is contained in:
2026-11-07 11:17:48 +00:00
parent 57e0c62940
commit 12b0e5eff0
2 changed files with 119 additions and 4 deletions
@@ -33,7 +33,10 @@ const loadAzureStorageHelperModule = (injected = {}) => {
return context.module.exports;
};
const loadRecursiveDeletionHelpers = ({ blobNames = [] } = {}) => {
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 =");
@@ -56,7 +59,7 @@ const loadRecursiveDeletionHelpers = ({ blobNames = [] } = {}) => {
},
deleteBlob(name) {
deletedBlobNames.push(name);
return Promise.resolve();
return deleteBlob(name);
},
getBlockBlobClient() {
return { exists: async () => true };
@@ -95,6 +98,22 @@ const loadRecursiveDeletionHelpers = ({ blobNames = [] } = {}) => {
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();
@@ -219,6 +238,102 @@ test("recursive deletion helpers reject empty and unsafe resource paths before l
}
});
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) {