From 1cf4641353cd970c4216f5007693c2fb46ff3efe Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 24 Sep 2026 08:38:05 +0000 Subject: [PATCH 1/7] Initial plan From d90c6cfd3f2cb9e854ae265540be585836a0144b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 24 Sep 2026 08:40:24 +0000 Subject: [PATCH 2/7] Validate proposed lease IDs Co-authored-by: jainakanksha-msft <181211853+jainakanksha-msft@users.noreply.github.com> --- src/blob/handlers/BlobHandler.ts | 7 ++++++- src/blob/handlers/ContainerHandler.ts | 10 +++++++++- src/blob/utils/utils.ts | 25 +++++++++++++++++++++++++ 3 files changed, 40 insertions(+), 2 deletions(-) diff --git a/src/blob/handlers/BlobHandler.ts b/src/blob/handlers/BlobHandler.ts index ca5206566..897c5fb9a 100644 --- a/src/blob/handlers/BlobHandler.ts +++ b/src/blob/handlers/BlobHandler.ts @@ -29,7 +29,8 @@ import { deserializePageBlobRangeHeader, deserializeRangeHeader, getBlobTagsCount, - validateBlobTag + validateBlobTag, + validateProposedLeaseId } from "../utils/utils"; import BaseHandler from "./BaseHandler"; import IPageBlobRangesManager from "./IPageBlobRangesManager"; @@ -371,6 +372,8 @@ export default class BlobHandler extends BaseHandler implements IBlobHandler { options: Models.BlobAcquireLeaseOptionalParams, context: Context ): Promise { + validateProposedLeaseId(options.proposedLeaseId, context.contextId); + const blobCtx = new BlobStorageContext(context); const account = blobCtx.account!; const container = blobCtx.container!; @@ -505,6 +508,8 @@ export default class BlobHandler extends BaseHandler implements IBlobHandler { options: Models.BlobChangeLeaseOptionalParams, context: Context ): Promise { + validateProposedLeaseId(proposedLeaseId, context.contextId); + const blobCtx = new BlobStorageContext(context); const account = blobCtx.account!; const container = blobCtx.container!; diff --git a/src/blob/handlers/ContainerHandler.ts b/src/blob/handlers/ContainerHandler.ts index 7563261b2..0db5d7f3f 100644 --- a/src/blob/handlers/ContainerHandler.ts +++ b/src/blob/handlers/ContainerHandler.ts @@ -16,7 +16,11 @@ import { EMULATOR_ACCOUNT_SKUNAME } from "../utils/constants"; import { DEFAULT_LIST_BLOBS_MAX_RESULTS } from "../utils/constants"; -import { getBlobTagsCount, removeQuotationFromListBlobEtag } from "../utils/utils"; +import { + getBlobTagsCount, + removeQuotationFromListBlobEtag, + validateProposedLeaseId +} from "../utils/utils"; import BaseHandler from "./BaseHandler"; import { BlobBatchHandler } from "./BlobBatchHandler"; @@ -423,6 +427,8 @@ export default class ContainerHandler extends BaseHandler options: Models.ContainerAcquireLeaseOptionalParams, context: Context ): Promise { + validateProposedLeaseId(options.proposedLeaseId, context.contextId); + const blobCtx = new BlobStorageContext(context); const accountName = blobCtx.account!; const containerName = blobCtx.container!; @@ -589,6 +595,8 @@ export default class ContainerHandler extends BaseHandler options: Models.ContainerChangeLeaseOptionalParams, context: Context ): Promise { + validateProposedLeaseId(proposedLeaseId, context.contextId); + const blobCtx = new BlobStorageContext(context); const accountName = blobCtx.account!; const containerName = blobCtx.container!; diff --git a/src/blob/utils/utils.ts b/src/blob/utils/utils.ts index eb7a1c4a9..897bb6738 100644 --- a/src/blob/utils/utils.ts +++ b/src/blob/utils/utils.ts @@ -6,6 +6,16 @@ import { BlobTag, BlobTags } from "@azure/storage-blob"; import { TagContent } from "../persistence/QueryInterpreter/QueryNodes/IQueryNode"; import { computeTransactionalChecksums } from "../../common/utils/utils"; +const AZURE_GUID_REGEX = new RegExp( + "^(" + + "[0-9a-fA-F]{32}" + + "|[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}" + + "|\\{[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}\\}" + + "|\\([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}\\)" + + "|\\{0x[0-9a-fA-F]{8},0x[0-9a-fA-F]{4},0x[0-9a-fA-F]{4},\\{0x[0-9a-fA-F]{2}(,0x[0-9a-fA-F]{2}){7}\\}\\}" + + ")$" +); + function decodeBase64HeaderValue(value: string): Buffer | undefined { if (value.length === 0) { return Buffer.alloc(0); @@ -100,6 +110,21 @@ export function validateTransactionalChecksumHeaders( return { md5, crc64 }; } +export function validateProposedLeaseId( + proposedLeaseId: string | undefined, + contextId: string | undefined +): void { + if ( + proposedLeaseId !== undefined && + !AZURE_GUID_REGEX.test(proposedLeaseId) + ) { + throw StorageErrorFactory.getInvalidHeaderValue(contextId, { + HeaderName: "x-ms-proposed-lease-id", + HeaderValue: proposedLeaseId + }); + } +} + /** * Computes MD5 and/or CRC-64/NVME from a stream in a single pass and validates * against the request-supplied values. Throws Md5Mismatch / Crc64Mismatch From bba980e2de60c328335faedc08a021a6887c3fa8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 24 Sep 2026 08:41:46 +0000 Subject: [PATCH 3/7] Add lease ID validation tests Co-authored-by: jainakanksha-msft <181211853+jainakanksha-msft@users.noreply.github.com> --- ChangeLog.md | 1 + tests/blob/apis/blob.test.ts | 55 +++++++++++++++++++++++++++++++ tests/blob/apis/container.test.ts | 55 +++++++++++++++++++++++++++++++ 3 files changed, 111 insertions(+) diff --git a/ChangeLog.md b/ChangeLog.md index 71f381dd4..c5ceb9d93 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -14,6 +14,7 @@ General: Blob: - Fixed block blob uploads with `If-None-Match: *` returning `BlobAlreadyExists` before validating an active lease, matching Azure Storage's `LeaseIdMissing` and lease mismatch error precedence. (issue #2637) +- Validate proposed blob and container lease IDs during acquire and change lease operations, returning `InvalidHeaderValue` for malformed GUID values to match Azure Storage. (issue #2367) - Fixed service- and container-level Filter Blobs requests failing when the optional `where` query parameter is omitted. - Fixed blob operations hanging when a client disconnects before the operation queue processes the request. (issue #2575) - Implement `PutBlobFromUrl` (`Put Blob From URL`), which previously returned 501. The source is fetched over loopback, as `PutBlockFromURL` already does, so that SAS authentication and the `x-ms-source-if-*` conditions are enforced by the existing download path. Standard blob properties are copied from the source unless `x-ms-copy-source-blob-properties` is false, request blob content headers override them either way, request metadata replaces the source's rather than adding to it, and `x-ms-copy-source-tag-option: COPY` reads the source's tags over that same authorized path. An `x-ms-source-content-md5`, `x-ms-blob-content-md5`, `Content-MD5`, or `x-ms-content-crc64` header is checked against the copied content, and the response reports the MD5 and CRC64 of that content. A SAS needs Create or Write to create the blob, Write to overwrite it, and Tag as well when the request sets tags with `x-ms-tags` or copies the source's. As with `CopyBlobFromURL`, only sources on the same Azurite instance are supported. diff --git a/tests/blob/apis/blob.test.ts b/tests/blob/apis/blob.test.ts index fcc52a712..7c99173d2 100644 --- a/tests/blob/apis/blob.test.ts +++ b/tests/blob/apis/blob.test.ts @@ -53,6 +53,23 @@ describe("BlobAPIs", () => { let blockBlobClient = blobClient.getBlockBlobClient(); let blobLeaseClient = blobClient.getBlobLeaseClient(); const content = "Hello World"; + const xFormatGuid = + "{0xca761232,0xed42,0x11ce,{0xba,0xcd,0x00,0xaa,0x00,0x57,0xb2,0x23}}"; + const xFormatGuidExtraClosingBrace = `${xFormatGuid}}`; + + function assertInvalidProposedLeaseId(error: any, headerValue: string): void { + assert.deepStrictEqual(error.statusCode, 400); + assert.deepStrictEqual(error.code, "InvalidHeaderValue"); + assert.deepStrictEqual(error.details.errorCode, "InvalidHeaderValue"); + assert.deepStrictEqual( + /([^<]*)([^<]*) { await server.start(); @@ -744,6 +761,28 @@ describe("BlobAPIs", () => { ); }); + it("acquireLease_available_proposedLeaseId_xFormat @loki @sql", async () => { + const duration = 30; + blobLeaseClient = await blobClient.getBlobLeaseClient(xFormatGuid); + const result = await blobLeaseClient.acquireLease(duration); + assert.equal(result.leaseId, xFormatGuid); + + await blobLeaseClient.releaseLease(); + }); + + it("acquireLease rejects malformed proposedLeaseId @loki @sql", async () => { + blobLeaseClient = await blobClient.getBlobLeaseClient( + xFormatGuidExtraClosingBrace + ); + + try { + await blobLeaseClient.acquireLease(30); + assert.fail("Should not reach here"); + } catch (error) { + assertInvalidProposedLeaseId(error, xFormatGuidExtraClosingBrace); + } + }); + it("acquireLease_available_NoproposedLeaseId_infinite @loki @sql", async () => { const leaseResult = await blobLeaseClient.acquireLease(-1); const leaseId = leaseResult.leaseId; @@ -916,6 +955,22 @@ describe("BlobAPIs", () => { await blobLeaseClient.releaseLease(); }); + it("changeLease rejects malformed proposedLeaseId @loki @sql", async () => { + const guid = "ca761232ed4211cebacd00aa0057b223"; + const invalidGuid = "not-a-guid"; + blobLeaseClient = blobClient.getBlobLeaseClient(guid); + await blobLeaseClient.acquireLease(30); + + try { + await blobLeaseClient.changeLease(invalidGuid); + assert.fail("Should not reach here"); + } catch (error) { + assertInvalidProposedLeaseId(error, invalidGuid); + } finally { + await blobLeaseClient.releaseLease(); + } + }); + it("breakLease @loki @sql", async () => { const guid = "ca761232ed4211cebacd00aa0057b223"; const duration = 15; diff --git a/tests/blob/apis/container.test.ts b/tests/blob/apis/container.test.ts index 5900269ba..eba51df83 100644 --- a/tests/blob/apis/container.test.ts +++ b/tests/blob/apis/container.test.ts @@ -51,6 +51,23 @@ describe("ContainerAPIs", () => { let containerName: string = getUniqueName("container"); let containerClient = serviceClient.getContainerClient(containerName); let blobLeaseClient = containerClient.getBlobLeaseClient(); + const xFormatGuid = + "{0xca761232,0xed42,0x11ce,{0xba,0xcd,0x00,0xaa,0x00,0x57,0xb2,0x23}}"; + const xFormatGuidExtraClosingBrace = `${xFormatGuid}}`; + + function assertInvalidProposedLeaseId(error: any, headerValue: string): void { + assert.deepStrictEqual(error.statusCode, 400); + assert.deepStrictEqual(error.code, "InvalidHeaderValue"); + assert.deepStrictEqual(error.details.errorCode, "InvalidHeaderValue"); + assert.deepStrictEqual( + /([^<]*)([^<]*) { await server.start(); @@ -446,6 +463,28 @@ describe("ContainerAPIs", () => { ); }); + it("acquireLease_available_proposedLeaseId_xFormat @loki @sql", async () => { + const duration = 30; + blobLeaseClient = containerClient.getBlobLeaseClient(xFormatGuid); + const result = await blobLeaseClient.acquireLease(duration); + assert.equal(result.leaseId, xFormatGuid); + + await blobLeaseClient.releaseLease(); + }); + + it("acquireLease rejects malformed proposedLeaseId @loki @sql", async () => { + blobLeaseClient = containerClient.getBlobLeaseClient( + xFormatGuidExtraClosingBrace + ); + + try { + await blobLeaseClient.acquireLease(30); + assert.fail("Should not reach here"); + } catch (error) { + assertInvalidProposedLeaseId(error, xFormatGuidExtraClosingBrace); + } + }); + it("acquireLease_available_NoproposedLeaseId_infinite @loki @sql", async () => { const leaseResult = await blobLeaseClient.acquireLease(-1); const leaseId = leaseResult.leaseId; @@ -525,6 +564,22 @@ describe("ContainerAPIs", () => { await blobLeaseClient.releaseLease(); }); + it("changeLease rejects malformed proposedLeaseId @loki @sql", async () => { + const guid = "ca761232ed4211cebacd00aa0057b223"; + const invalidGuid = "not-a-guid"; + blobLeaseClient = containerClient.getBlobLeaseClient(guid); + await blobLeaseClient.acquireLease(30); + + try { + await blobLeaseClient.changeLease(invalidGuid); + assert.fail("Should not reach here"); + } catch (error) { + assertInvalidProposedLeaseId(error, invalidGuid); + } finally { + await blobLeaseClient.releaseLease(); + } + }); + it("breakLease @loki @sql", async () => { const guid = "ca761232ed4211cebacd00aa0057b223"; const duration = 15; From a38b8a51b2c4cd4488193eb862738d0cde9f0cc1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 24 Sep 2026 08:46:29 +0000 Subject: [PATCH 4/7] Address lease validation review feedback Co-authored-by: jainakanksha-msft <181211853+jainakanksha-msft@users.noreply.github.com> --- src/blob/utils/utils.ts | 19 ++++++++++++++----- tests/blob/apis/blob.test.ts | 8 ++++---- tests/blob/apis/container.test.ts | 4 ++-- 3 files changed, 20 insertions(+), 11 deletions(-) diff --git a/src/blob/utils/utils.ts b/src/blob/utils/utils.ts index 897bb6738..9ebc429bf 100644 --- a/src/blob/utils/utils.ts +++ b/src/blob/utils/utils.ts @@ -6,13 +6,16 @@ import { BlobTag, BlobTags } from "@azure/storage-blob"; import { TagContent } from "../persistence/QueryInterpreter/QueryNodes/IQueryNode"; import { computeTransactionalChecksums } from "../../common/utils/utils"; +const GUID_HEX = "[0-9a-fA-F]"; +const GUID_DASHED = `${GUID_HEX}{8}-${GUID_HEX}{4}-${GUID_HEX}{4}-${GUID_HEX}{4}-${GUID_HEX}{12}`; +const GUID_X_FORMAT = `\\{0x${GUID_HEX}{8},0x${GUID_HEX}{4},0x${GUID_HEX}{4},\\{0x${GUID_HEX}{2}(,0x${GUID_HEX}{2}){7}\\}\\}`; const AZURE_GUID_REGEX = new RegExp( "^(" + - "[0-9a-fA-F]{32}" + - "|[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}" + - "|\\{[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}\\}" + - "|\\([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}\\)" + - "|\\{0x[0-9a-fA-F]{8},0x[0-9a-fA-F]{4},0x[0-9a-fA-F]{4},\\{0x[0-9a-fA-F]{2}(,0x[0-9a-fA-F]{2}){7}\\}\\}" + + `${GUID_HEX}{32}` + + `|${GUID_DASHED}` + + `|\\{${GUID_DASHED}\\}` + + `|\\(${GUID_DASHED}\\)` + + `|${GUID_X_FORMAT}` + ")$" ); @@ -110,6 +113,12 @@ export function validateTransactionalChecksumHeaders( return { md5, crc64 }; } +/** + * Validates x-ms-proposed-lease-id against the Azure accepted GUID string + * forms: 32 hex digits, dashed GUID, braced dashed GUID, parenthesized dashed + * GUID, and X-format GUID. Throws InvalidHeaderValue with header details when + * the supplied value is malformed. + */ export function validateProposedLeaseId( proposedLeaseId: string | undefined, contextId: string | undefined diff --git a/tests/blob/apis/blob.test.ts b/tests/blob/apis/blob.test.ts index 7c99173d2..9b7202db6 100644 --- a/tests/blob/apis/blob.test.ts +++ b/tests/blob/apis/blob.test.ts @@ -763,15 +763,15 @@ describe("BlobAPIs", () => { it("acquireLease_available_proposedLeaseId_xFormat @loki @sql", async () => { const duration = 30; - blobLeaseClient = await blobClient.getBlobLeaseClient(xFormatGuid); + blobLeaseClient = blobClient.getBlobLeaseClient(xFormatGuid); const result = await blobLeaseClient.acquireLease(duration); assert.equal(result.leaseId, xFormatGuid); await blobLeaseClient.releaseLease(); }); - it("acquireLease rejects malformed proposedLeaseId @loki @sql", async () => { - blobLeaseClient = await blobClient.getBlobLeaseClient( + it("acquireLease_malformed_proposedLeaseId @loki @sql", async () => { + blobLeaseClient = blobClient.getBlobLeaseClient( xFormatGuidExtraClosingBrace ); @@ -955,7 +955,7 @@ describe("BlobAPIs", () => { await blobLeaseClient.releaseLease(); }); - it("changeLease rejects malformed proposedLeaseId @loki @sql", async () => { + it("changeLease_malformed_proposedLeaseId @loki @sql", async () => { const guid = "ca761232ed4211cebacd00aa0057b223"; const invalidGuid = "not-a-guid"; blobLeaseClient = blobClient.getBlobLeaseClient(guid); diff --git a/tests/blob/apis/container.test.ts b/tests/blob/apis/container.test.ts index eba51df83..2a69d07f8 100644 --- a/tests/blob/apis/container.test.ts +++ b/tests/blob/apis/container.test.ts @@ -472,7 +472,7 @@ describe("ContainerAPIs", () => { await blobLeaseClient.releaseLease(); }); - it("acquireLease rejects malformed proposedLeaseId @loki @sql", async () => { + it("acquireLease_malformed_proposedLeaseId @loki @sql", async () => { blobLeaseClient = containerClient.getBlobLeaseClient( xFormatGuidExtraClosingBrace ); @@ -564,7 +564,7 @@ describe("ContainerAPIs", () => { await blobLeaseClient.releaseLease(); }); - it("changeLease rejects malformed proposedLeaseId @loki @sql", async () => { + it("changeLease_malformed_proposedLeaseId @loki @sql", async () => { const guid = "ca761232ed4211cebacd00aa0057b223"; const invalidGuid = "not-a-guid"; blobLeaseClient = containerClient.getBlobLeaseClient(guid); From 97bce408dcfbd4e1cfd744a9be7ef33dffcad95a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 24 Sep 2026 08:48:51 +0000 Subject: [PATCH 5/7] Share lease validation test helpers Co-authored-by: jainakanksha-msft <181211853+jainakanksha-msft@users.noreply.github.com> --- tests/blob/apis/blob.test.ts | 22 +++++----------------- tests/blob/apis/container.test.ts | 22 +++++----------------- tests/blob/apis/leaseTestUtils.ts | 22 ++++++++++++++++++++++ 3 files changed, 32 insertions(+), 34 deletions(-) create mode 100644 tests/blob/apis/leaseTestUtils.ts diff --git a/tests/blob/apis/blob.test.ts b/tests/blob/apis/blob.test.ts index 9b7202db6..8d81c6cba 100644 --- a/tests/blob/apis/blob.test.ts +++ b/tests/blob/apis/blob.test.ts @@ -22,6 +22,11 @@ import { } from "../../testutils"; import CustomHeaderPolicyFactory from "../RequestPolicy/CustomHeaderPolicyFactory"; import RangePolicyFactory from "../RequestPolicy/RangePolicyFactory"; +import { + assertInvalidProposedLeaseId, + xFormatGuid, + xFormatGuidExtraClosingBrace +} from "./leaseTestUtils"; // Set true to enable debug log configLogger(false); @@ -53,23 +58,6 @@ describe("BlobAPIs", () => { let blockBlobClient = blobClient.getBlockBlobClient(); let blobLeaseClient = blobClient.getBlobLeaseClient(); const content = "Hello World"; - const xFormatGuid = - "{0xca761232,0xed42,0x11ce,{0xba,0xcd,0x00,0xaa,0x00,0x57,0xb2,0x23}}"; - const xFormatGuidExtraClosingBrace = `${xFormatGuid}}`; - - function assertInvalidProposedLeaseId(error: any, headerValue: string): void { - assert.deepStrictEqual(error.statusCode, 400); - assert.deepStrictEqual(error.code, "InvalidHeaderValue"); - assert.deepStrictEqual(error.details.errorCode, "InvalidHeaderValue"); - assert.deepStrictEqual( - /([^<]*)([^<]*) { await server.start(); diff --git a/tests/blob/apis/container.test.ts b/tests/blob/apis/container.test.ts index 2a69d07f8..7834b5a29 100644 --- a/tests/blob/apis/container.test.ts +++ b/tests/blob/apis/container.test.ts @@ -24,6 +24,11 @@ import { sleep } from "../../testutils"; import QueryRequestPolicyFactory from "../RequestPolicy/QueryRequestPolicyFactory"; +import { + assertInvalidProposedLeaseId, + xFormatGuid, + xFormatGuidExtraClosingBrace +} from "./leaseTestUtils"; // Set to true enable debug log configLogger(false); @@ -51,23 +56,6 @@ describe("ContainerAPIs", () => { let containerName: string = getUniqueName("container"); let containerClient = serviceClient.getContainerClient(containerName); let blobLeaseClient = containerClient.getBlobLeaseClient(); - const xFormatGuid = - "{0xca761232,0xed42,0x11ce,{0xba,0xcd,0x00,0xaa,0x00,0x57,0xb2,0x23}}"; - const xFormatGuidExtraClosingBrace = `${xFormatGuid}}`; - - function assertInvalidProposedLeaseId(error: any, headerValue: string): void { - assert.deepStrictEqual(error.statusCode, 400); - assert.deepStrictEqual(error.code, "InvalidHeaderValue"); - assert.deepStrictEqual(error.details.errorCode, "InvalidHeaderValue"); - assert.deepStrictEqual( - /([^<]*)([^<]*) { await server.start(); diff --git a/tests/blob/apis/leaseTestUtils.ts b/tests/blob/apis/leaseTestUtils.ts new file mode 100644 index 000000000..ecdb32d9d --- /dev/null +++ b/tests/blob/apis/leaseTestUtils.ts @@ -0,0 +1,22 @@ +import * as assert from "assert"; + +export const xFormatGuid = + "{0xca761232,0xed42,0x11ce,{0xba,0xcd,0x00,0xaa,0x00,0x57,0xb2,0x23}}"; +export const xFormatGuidExtraClosingBrace = `${xFormatGuid}}`; + +export function assertInvalidProposedLeaseId( + error: any, + headerValue: string +): void { + assert.deepStrictEqual(error.statusCode, 400); + assert.deepStrictEqual(error.code, "InvalidHeaderValue"); + assert.deepStrictEqual(error.details.errorCode, "InvalidHeaderValue"); + assert.deepStrictEqual( + /([^<]*)([^<]*) Date: Thu, 24 Sep 2026 10:32:39 +0000 Subject: [PATCH 6/7] Address lease validation review feedback Co-authored-by: jainakanksha-msft <181211853+jainakanksha-msft@users.noreply.github.com> --- src/blob/utils/utils.ts | 3 ++- tests/blob/apis/appendblob.test.ts | 4 +-- tests/blob/apis/blob.test.ts | 42 ++++++++++++++++++++++++++---- tests/blob/apis/blockblob.test.ts | 2 +- tests/blob/apis/container.test.ts | 42 ++++++++++++++++++++++++++---- tests/blob/apis/leaseTestUtils.ts | 4 +++ 6 files changed, 83 insertions(+), 14 deletions(-) diff --git a/src/blob/utils/utils.ts b/src/blob/utils/utils.ts index 9ebc429bf..128117543 100644 --- a/src/blob/utils/utils.ts +++ b/src/blob/utils/utils.ts @@ -8,7 +8,8 @@ import { computeTransactionalChecksums } from "../../common/utils/utils"; const GUID_HEX = "[0-9a-fA-F]"; const GUID_DASHED = `${GUID_HEX}{8}-${GUID_HEX}{4}-${GUID_HEX}{4}-${GUID_HEX}{4}-${GUID_HEX}{12}`; -const GUID_X_FORMAT = `\\{0x${GUID_HEX}{8},0x${GUID_HEX}{4},0x${GUID_HEX}{4},\\{0x${GUID_HEX}{2}(,0x${GUID_HEX}{2}){7}\\}\\}`; +const GUID_X_PREFIX = "0[xX]"; +const GUID_X_FORMAT = `\\{${GUID_X_PREFIX}${GUID_HEX}{8},${GUID_X_PREFIX}${GUID_HEX}{4},${GUID_X_PREFIX}${GUID_HEX}{4},\\{${GUID_X_PREFIX}${GUID_HEX}{2}(,${GUID_X_PREFIX}${GUID_HEX}{2}){7}\\}\\}`; const AZURE_GUID_REGEX = new RegExp( "^(" + `${GUID_HEX}{32}` + diff --git a/tests/blob/apis/appendblob.test.ts b/tests/blob/apis/appendblob.test.ts index dbe72e78d..da665fed5 100644 --- a/tests/blob/apis/appendblob.test.ts +++ b/tests/blob/apis/appendblob.test.ts @@ -774,7 +774,7 @@ describe("AppendBlobAPIs", () => { it("Append block lease condition should work @loki", async () => { await appendBlobClient.create(); - const leaseId = "abcdefg"; + const leaseId = "ca761232-ed42-11ce-bacd-00aa0057b223"; const blobLeaseClient = await appendBlobClient.getBlobLeaseClient(leaseId); await blobLeaseClient.acquireLease(20); @@ -802,7 +802,7 @@ describe("AppendBlobAPIs", () => { it("Append block should refresh lease state @loki", async () => { await appendBlobClient.create(); - const leaseId = "abcdefg"; + const leaseId = "3c7e72eb-b430-4526-bc53-d8ecef03798f"; const blobLeaseClient = await appendBlobClient.getBlobLeaseClient(leaseId); await blobLeaseClient.acquireLease(20); diff --git a/tests/blob/apis/blob.test.ts b/tests/blob/apis/blob.test.ts index 8d81c6cba..4361f6a35 100644 --- a/tests/blob/apis/blob.test.ts +++ b/tests/blob/apis/blob.test.ts @@ -24,7 +24,10 @@ import CustomHeaderPolicyFactory from "../RequestPolicy/CustomHeaderPolicyFactor import RangePolicyFactory from "../RequestPolicy/RangePolicyFactory"; import { assertInvalidProposedLeaseId, + bracedGuid, + parenthesizedGuid, xFormatGuid, + xFormatGuidUppercasePrefix, xFormatGuidExtraClosingBrace } from "./leaseTestUtils"; @@ -749,13 +752,20 @@ describe("BlobAPIs", () => { ); }); - it("acquireLease_available_proposedLeaseId_xFormat @loki @sql", async () => { + it("acquireLease_available_proposedLeaseId_guidFormats @loki @sql", async () => { const duration = 30; - blobLeaseClient = blobClient.getBlobLeaseClient(xFormatGuid); - const result = await blobLeaseClient.acquireLease(duration); - assert.equal(result.leaseId, xFormatGuid); + for (const guid of [ + bracedGuid, + parenthesizedGuid, + xFormatGuid, + xFormatGuidUppercasePrefix + ]) { + blobLeaseClient = blobClient.getBlobLeaseClient(guid); + const result = await blobLeaseClient.acquireLease(duration); + assert.equal(result.leaseId, guid); - await blobLeaseClient.releaseLease(); + await blobLeaseClient.releaseLease(); + } }); it("acquireLease_malformed_proposedLeaseId @loki @sql", async () => { @@ -959,6 +969,28 @@ describe("BlobAPIs", () => { } }); + it("changeLease_available_proposedLeaseId_guidFormats @loki @sql", async () => { + let guid = "ca761232ed4211cebacd00aa0057b223"; + blobLeaseClient = blobClient.getBlobLeaseClient(guid); + await blobLeaseClient.acquireLease(30); + + try { + for (const proposedGuid of [ + bracedGuid, + parenthesizedGuid, + xFormatGuid, + xFormatGuidUppercasePrefix + ]) { + const result = await blobLeaseClient.changeLease(proposedGuid); + assert.equal(result.leaseId, proposedGuid); + guid = proposedGuid; + blobLeaseClient = blobClient.getBlobLeaseClient(guid); + } + } finally { + await blobLeaseClient.releaseLease(); + } + }); + it("breakLease @loki @sql", async () => { const guid = "ca761232ed4211cebacd00aa0057b223"; const duration = 15; diff --git a/tests/blob/apis/blockblob.test.ts b/tests/blob/apis/blockblob.test.ts index b9b04ee18..0dfd0072c 100644 --- a/tests/blob/apis/blockblob.test.ts +++ b/tests/blob/apis/blockblob.test.ts @@ -110,7 +110,7 @@ describe("BlockBlobAPIs", () => { it("Block blob upload should refresh lease state @loki @sql", async () => { await blockBlobClient.upload('a', 1); - const leaseId = "abcdefg"; + const leaseId = "ca761232-ed42-11ce-bacd-00aa0057b223"; const blobLeaseClient = await blockBlobClient.getBlobLeaseClient(leaseId); await blobLeaseClient.acquireLease(20); diff --git a/tests/blob/apis/container.test.ts b/tests/blob/apis/container.test.ts index 7834b5a29..d77218e1d 100644 --- a/tests/blob/apis/container.test.ts +++ b/tests/blob/apis/container.test.ts @@ -26,7 +26,10 @@ import { import QueryRequestPolicyFactory from "../RequestPolicy/QueryRequestPolicyFactory"; import { assertInvalidProposedLeaseId, + bracedGuid, + parenthesizedGuid, xFormatGuid, + xFormatGuidUppercasePrefix, xFormatGuidExtraClosingBrace } from "./leaseTestUtils"; @@ -451,13 +454,20 @@ describe("ContainerAPIs", () => { ); }); - it("acquireLease_available_proposedLeaseId_xFormat @loki @sql", async () => { + it("acquireLease_available_proposedLeaseId_guidFormats @loki @sql", async () => { const duration = 30; - blobLeaseClient = containerClient.getBlobLeaseClient(xFormatGuid); - const result = await blobLeaseClient.acquireLease(duration); - assert.equal(result.leaseId, xFormatGuid); + for (const guid of [ + bracedGuid, + parenthesizedGuid, + xFormatGuid, + xFormatGuidUppercasePrefix + ]) { + blobLeaseClient = containerClient.getBlobLeaseClient(guid); + const result = await blobLeaseClient.acquireLease(duration); + assert.equal(result.leaseId, guid); - await blobLeaseClient.releaseLease(); + await blobLeaseClient.releaseLease(); + } }); it("acquireLease_malformed_proposedLeaseId @loki @sql", async () => { @@ -568,6 +578,28 @@ describe("ContainerAPIs", () => { } }); + it("changeLease_available_proposedLeaseId_guidFormats @loki @sql", async () => { + let guid = "ca761232ed4211cebacd00aa0057b223"; + blobLeaseClient = containerClient.getBlobLeaseClient(guid); + await blobLeaseClient.acquireLease(30); + + try { + for (const proposedGuid of [ + bracedGuid, + parenthesizedGuid, + xFormatGuid, + xFormatGuidUppercasePrefix + ]) { + const result = await blobLeaseClient.changeLease(proposedGuid); + assert.equal(result.leaseId, proposedGuid); + guid = proposedGuid; + blobLeaseClient = containerClient.getBlobLeaseClient(guid); + } + } finally { + await blobLeaseClient.releaseLease(); + } + }); + it("breakLease @loki @sql", async () => { const guid = "ca761232ed4211cebacd00aa0057b223"; const duration = 15; diff --git a/tests/blob/apis/leaseTestUtils.ts b/tests/blob/apis/leaseTestUtils.ts index ecdb32d9d..154659069 100644 --- a/tests/blob/apis/leaseTestUtils.ts +++ b/tests/blob/apis/leaseTestUtils.ts @@ -1,7 +1,11 @@ import * as assert from "assert"; +export const bracedGuid = "{ca761232-ed42-11ce-bacd-00aa0057b223}"; +export const parenthesizedGuid = "(ca761232-ed42-11ce-bacd-00aa0057b223)"; export const xFormatGuid = "{0xca761232,0xed42,0x11ce,{0xba,0xcd,0x00,0xaa,0x00,0x57,0xb2,0x23}}"; +export const xFormatGuidUppercasePrefix = + "{0Xca761232,0Xed42,0X11ce,{0Xba,0Xcd,0X00,0Xaa,0X00,0X57,0Xb2,0X23}}"; export const xFormatGuidExtraClosingBrace = `${xFormatGuid}}`; export function assertInvalidProposedLeaseId( From 785fc8ef5fd6f8eac43924283aa6f3ce3d14c5be Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 24 Sep 2026 10:41:51 +0000 Subject: [PATCH 7/7] Clean up lease format tests Co-authored-by: jainakanksha-msft <181211853+jainakanksha-msft@users.noreply.github.com> --- tests/blob/apis/blob.test.ts | 5 ++--- tests/blob/apis/container.test.ts | 5 ++--- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/tests/blob/apis/blob.test.ts b/tests/blob/apis/blob.test.ts index 4361f6a35..9ea5834b9 100644 --- a/tests/blob/apis/blob.test.ts +++ b/tests/blob/apis/blob.test.ts @@ -970,7 +970,7 @@ describe("BlobAPIs", () => { }); it("changeLease_available_proposedLeaseId_guidFormats @loki @sql", async () => { - let guid = "ca761232ed4211cebacd00aa0057b223"; + const guid = "ca761232ed4211cebacd00aa0057b223"; blobLeaseClient = blobClient.getBlobLeaseClient(guid); await blobLeaseClient.acquireLease(30); @@ -983,8 +983,7 @@ describe("BlobAPIs", () => { ]) { const result = await blobLeaseClient.changeLease(proposedGuid); assert.equal(result.leaseId, proposedGuid); - guid = proposedGuid; - blobLeaseClient = blobClient.getBlobLeaseClient(guid); + blobLeaseClient = blobClient.getBlobLeaseClient(proposedGuid); } } finally { await blobLeaseClient.releaseLease(); diff --git a/tests/blob/apis/container.test.ts b/tests/blob/apis/container.test.ts index d77218e1d..e93679605 100644 --- a/tests/blob/apis/container.test.ts +++ b/tests/blob/apis/container.test.ts @@ -579,7 +579,7 @@ describe("ContainerAPIs", () => { }); it("changeLease_available_proposedLeaseId_guidFormats @loki @sql", async () => { - let guid = "ca761232ed4211cebacd00aa0057b223"; + const guid = "ca761232ed4211cebacd00aa0057b223"; blobLeaseClient = containerClient.getBlobLeaseClient(guid); await blobLeaseClient.acquireLease(30); @@ -592,8 +592,7 @@ describe("ContainerAPIs", () => { ]) { const result = await blobLeaseClient.changeLease(proposedGuid); assert.equal(result.leaseId, proposedGuid); - guid = proposedGuid; - blobLeaseClient = containerClient.getBlobLeaseClient(guid); + blobLeaseClient = containerClient.getBlobLeaseClient(proposedGuid); } } finally { await blobLeaseClient.releaseLease();