From 12b0e5eff0a167f67a0cc5acaa1dbb886d8bb988 Mon Sep 17 00:00:00 2001 From: robbond Date: Sat, 7 Nov 2026 11:17:48 +0000 Subject: [PATCH] fix(storage): await recursive blob deletions and propagate failures --- actions/azurestorage.js | 4 +- .../azurestorage-helper-behaviour.test.cjs | 119 +++++++++++++++++- 2 files changed, 119 insertions(+), 4 deletions(-) diff --git a/actions/azurestorage.js b/actions/azurestorage.js index 341db6fb..cc10eed4 100644 --- a/actions/azurestorage.js +++ b/actions/azurestorage.js @@ -571,7 +571,7 @@ export const deleteBlobCase = async (containerName, blobName) => { prefix: folderPrefix })) { console.log(" ------ :", blob.name); - containerClient.deleteBlob(blob.name); + await containerClient.deleteBlob(blob.name); } //containerClient.deleteBlob(blobName); @@ -605,7 +605,7 @@ export const deleteBlobRep = async (containerName, blobName) => { console.log("=======:", await blockBlobClient.exists()); (await blockBlobClient.exists()) && - containerClient.deleteBlob(blob.name); + (await containerClient.deleteBlob(blob.name)); //containerClient.deleteIfExists(); //containerClient.deleteBlob(blob.name); diff --git a/tests/phase22/azurestorage-helper-behaviour.test.cjs b/tests/phase22/azurestorage-helper-behaviour.test.cjs index 3e32cf73..1ead7376 100644 --- a/tests/phase22/azurestorage-helper-behaviour.test.cjs +++ b/tests/phase22/azurestorage-helper-behaviour.test.cjs @@ -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) {