From 39448f7fc13703724a347cc0f73f0e361ee8ec46 Mon Sep 17 00:00:00 2001 From: Tomas Pajurek Date: Fri, 11 Oct 2024 14:07:09 +0200 Subject: [PATCH] Rework and enrich FluentAssertions for Storage Blobs (#22) --- .../BlobBaseClientAssertions.cs | 198 ++++++++- .../Blobs/BlobFluentAssertionsTests.cs | 393 ++++++++++++++++++ .../Storage/Blobs/FluentAssertionsTests.cs | 38 -- 3 files changed, 581 insertions(+), 48 deletions(-) create mode 100644 tests/Tests/Storage/Blobs/BlobFluentAssertionsTests.cs delete mode 100644 tests/Tests/Storage/Blobs/FluentAssertionsTests.cs diff --git a/src/Spotflow.InMemory.Azure.Storage.FluentAssertions/BlobBaseClientAssertions.cs b/src/Spotflow.InMemory.Azure.Storage.FluentAssertions/BlobBaseClientAssertions.cs index 16eb0fa..4fc4e3c 100644 --- a/src/Spotflow.InMemory.Azure.Storage.FluentAssertions/BlobBaseClientAssertions.cs +++ b/src/Spotflow.InMemory.Azure.Storage.FluentAssertions/BlobBaseClientAssertions.cs @@ -1,8 +1,10 @@ using System.Diagnostics; using Azure; +using Azure.Storage.Blobs.Models; using Azure.Storage.Blobs.Specialized; +using FluentAssertions; using FluentAssertions.Execution; using FluentAssertions.Primitives; @@ -13,29 +15,205 @@ public class BlobBaseClientAssertions(BlobBaseClient subject) { protected override string Identifier => nameof(BlobBaseClient); - public async Task ExistAsync(TimeSpan? maxWaitTime = null, string? because = null, params object[] becauseArgs) + /// + /// Checks that the blob exists. If is provided, it will wait up the the specified time for the blob to exist before failing. + /// + [CustomAssertion] + public AndConstraint Exist(TimeSpan? waitTime = null, string? because = null, params object[] becauseArgs) { - maxWaitTime ??= TimeSpan.FromSeconds(8); + using var scope = StartScope(because, becauseArgs); + + if (waitTime is null) + { + if (!Subject.Exists()) + { + scope.FailWith("Expected blob to exist{reason} but it does not."); + } + + return new(this); + } var startTime = Stopwatch.GetTimestamp(); - while (Stopwatch.GetElapsedTime(startTime) < maxWaitTime) + while (Stopwatch.GetElapsedTime(startTime) < waitTime) { try { - if (await Subject.ExistsAsync()) + if (Subject.Exists()) { - return; + return new(this); } } catch (RequestFailedException) { } - await Task.Delay(10); + Thread.Sleep(10); + } + + scope.FailWith("Expected blob to exist{reason} eventually but it does not exist event after {0} seconds.", waitTime.Value.TotalSeconds); + + return new(this); + + } + + [CustomAssertion] + public AndConstraint MatchName(string expectedNamePattern, string? because = null, params object[] becauseArgs) + { + using var scope = StartScope($"name of the blob", because, becauseArgs); + + var actualBlobName = Subject.Name; + + actualBlobName.Should().Match(expectedNamePattern); + + return new(this); + } + + #region Content + + [CustomAssertion] + public AndConstraint HaveSize(long expectedSize, string? because = null, params object[] becauseArgs) + { + using var scope = StartScope($"size of the blob", because, becauseArgs); + + var actualSize = Subject.GetProperties().Value.ContentLength; + + actualSize.Should().Be(expectedSize); + + return new(this); + } + + [CustomAssertion] + public AndConstraint BeEmpty(string? because = null, params object[] becauseArgs) + { + return HaveSize(0, because: because, becauseArgs: becauseArgs); + } + + [CustomAssertion] + public AndWhichConstraint HaveContent(string expectedContent, string? because = null, params object[] becauseArgs) + { + using var scope = StartScope($"content of the blob", because, becauseArgs); + + var actualContent = Subject.DownloadContent().Value.Content.ToString(); + + actualContent.Should().Be(expectedContent, because: because, becauseArgs: becauseArgs); + + return new(this, actualContent); + } + + #endregion + + #region Blocks + + [CustomAssertion] + public AndConstraint HaveCommittedBlocks(int expectedCount, string? because = null, params object[] becauseArgs) + { + using var scope = StartScope($"number of committed blocks in the blob '{Subject.Uri}'", because, becauseArgs); + + var actualBlocks = GetBlockList().CommittedBlocks.ToList(); + + actualBlocks.Should().HaveCount(expectedCount); + + return new(this); + } + + [CustomAssertion] + public AndConstraint HaveNoCommittedBlocks(string? because = null, params object[] becauseArgs) + { + return HaveCommittedBlocks(0, because: because, becauseArgs: becauseArgs); + } + + [CustomAssertion] + public AndConstraint HaveUncommittedBlocks(int expectedCount, string? because = null, params object[] becauseArgs) + { + using var scope = StartScope($"number of uncommitted blocks in the blob '{Subject.Uri}'", because, becauseArgs); + + var actualBlocks = GetBlockList().UncommittedBlocks.ToList(); + + actualBlocks.Should().HaveCount(expectedCount); + + return new(this); + } + + [CustomAssertion] + public AndConstraint HaveNoUncommittedBlocks(string? because = null, params object[] becauseArgs) + { + return HaveUncommittedBlocks(0, because: because, becauseArgs: becauseArgs); + } + + [CustomAssertion] + public AndConstraint HaveCommittedBlocksWithSizes(int?[] expectedBlockSizes, string? because = null, params object[] becauseArgs) + { + var blocks = GetBlockList().CommittedBlocks.ToList(); + + HaveCommittedBlocks(expectedBlockSizes.Length, because: because, becauseArgs: becauseArgs); + + using (var scope = StartScope(because, becauseArgs)) + { + for (var i = 0; i < expectedBlockSizes.Length; i++) + { + var expectedBlockSize = expectedBlockSizes[i]; + if (expectedBlockSize.HasValue) + { + scope.Context = new($"block #{i} size"); + + var actualBlockSize = blocks[i].Size; + + actualBlockSize.Should().Be(expectedBlockSize.Value); + } + } } - Execute - .Assertion - .BecauseOf(because, becauseArgs) - .FailWith("Blob {0} should exist{reason} but it does not exist event after {1} seconds.", Subject.Uri, maxWaitTime.Value.TotalSeconds); + return new(this); + } + + [CustomAssertion] + public AndConstraint HaveCommittedBlock(int blockOrdinal, Action blockAssertion, string? because = null, params object[] becauseArgs) + { + using var scope = StartScope(because, becauseArgs); + + var blocks = GetBlockList().CommittedBlocks.ToList(); + + var actualBlockCount = blocks.Count; + var minimalExpectedBlockCount = blockOrdinal + 1; + + scope.AddReportable("Block", $"#{blockOrdinal}"); + + actualBlockCount.Should().BeGreaterOrEqualTo(minimalExpectedBlockCount); + + var actualBlock = blocks[blockOrdinal]; + + blockAssertion(actualBlock); + + return new(this); + } + + #endregion + + private AssertionScope StartScope(string? because, object[] becauseArgs) + { + var scope = new AssertionScope(); + return StartScopeCore(because, becauseArgs, scope); + } + + private AssertionScope StartScope(string context, string? because, object[] becauseArgs) + { + var scope = new AssertionScope(context); + return StartScopeCore(because, becauseArgs, scope); + } + + private AssertionScope StartScopeCore(string? because, object[] becauseArgs, AssertionScope scope) + { + scope.BecauseOf(because, becauseArgs); + scope.AddReportable("Blob", Subject.Uri.ToString()); + return scope; + } + + private BlockList GetBlockList() + { + var container = Subject.GetParentBlobContainerClient(); + + var client = container.GetBlockBlobClient(Subject.Name); + + return client.GetBlockList(BlockListTypes.All).Value; + } } diff --git a/tests/Tests/Storage/Blobs/BlobFluentAssertionsTests.cs b/tests/Tests/Storage/Blobs/BlobFluentAssertionsTests.cs new file mode 100644 index 0000000..4378143 --- /dev/null +++ b/tests/Tests/Storage/Blobs/BlobFluentAssertionsTests.cs @@ -0,0 +1,393 @@ +using Azure.Storage.Blobs.Specialized; + +using Spotflow.InMemory.Azure.Storage; +using Spotflow.InMemory.Azure.Storage.Blobs; +using Spotflow.InMemory.Azure.Storage.FluentAssertions; + +namespace Tests.Storage.Blobs; + +[TestClass] +public class BlobFluentAssertionsTests +{ + [TestMethod] + public void Exist_With_Existing_Blob_Should_Succeed() + { + var account = new InMemoryStorageProvider().AddAccount(); + + var blob = InMemoryBlobClient.FromAccount(account, "test-container", "test-blob"); + + blob.GetParentBlobContainerClient().Create(); + + blob.Upload(BinaryData.FromString("test-data")); + + blob.Should().Exist(); + } + + [TestMethod] + public void Exist_With_Missing_Blob_Should_Throw() + { + var account = new InMemoryStorageProvider().AddAccount(); + + var blob = InMemoryBlobClient.FromAccount(account, "test-container", "test-blob"); + + blob.GetParentBlobContainerClient().Create(); + + var act = () => blob.Should().Exist(); + + act.Should() + .Throw() + .WithMessage("" + + "Expected blob to exist but it does not." + + "With Blob:https://*/test-container/test-blob"); + } + + + [TestMethod] + public async Task Exist_With_Wait_For_Eventually_Created_Blob_Should_Succeed() + { + var account = new InMemoryStorageProvider().AddAccount(); + + var blob = InMemoryBlobClient.FromAccount(account, "test-container", "test-blob"); + + blob.GetParentBlobContainerClient().Create(); + + var existTask = Task.Run(() => blob.Should().Exist(waitTime: TimeSpan.FromSeconds(8))); + + existTask.IsCompleted.Should().BeFalse(); + + await Task.Delay(100); + + existTask.IsCompleted.Should().BeFalse(); + + await Task.Delay(100); + + blob.Upload(BinaryData.FromString("test-data")); + + await existTask; + + existTask.IsCompletedSuccessfully.Should().BeTrue(); + + } + + [TestMethod] + public async Task Exist_With_Wait_For_Missing_Blob_Should_Throw() + { + var account = new InMemoryStorageProvider().AddAccount(); + + var blob = InMemoryBlobClient.FromAccount(account, "test-container", "test-blob"); + + blob.GetParentBlobContainerClient().Create(); + + var existTask = Task.Run(() => blob.Should().Exist(waitTime: TimeSpan.FromSeconds(1))); + + existTask.IsCompleted.Should().BeFalse(); + + await Task.Delay(1000); + + var act = () => existTask; + + await act.Should() + .ThrowAsync() + .WithMessage("" + + "Expected blob to exist eventually but it does not exist event after 1.0 seconds.*" + + "With Blob:*https://*/test-container/test-blob"); + } + + [TestMethod] + public void MatchName_Should_Succeed() + { + var account = new InMemoryStorageProvider().AddAccount(); + + var blob = InMemoryBlobClient.FromAccount(account, "test-container", "test-blob"); + + blob.Should().MatchName("test-blob*"); + } + + [TestMethod] + public void MatchName_Should_Throw() + { + var account = new InMemoryStorageProvider().AddAccount(); + + var blob = InMemoryBlobClient.FromAccount(account, "test-container", "test-blob"); + + var act = () => blob.Should().MatchName("test-bloc*"); + + act.Should() + .Throw() + .WithMessage("" + + "Expected name of the blob to match \"test-bloc*\", but \"test-blob\" does not." + + "With Blob:https://*/test-container/test-blob"); + } + + + [TestMethod] + public void HaveSize_Should_Succeed() + { + var account = new InMemoryStorageProvider().AddAccount(); + + var blob = InMemoryBlobClient.FromAccount(account, "test-container", "test-blob"); + + blob.GetParentBlobContainerClient().Create(); + + blob.Upload(BinaryData.FromString("test-data")); + + blob.Should().HaveSize(9); + } + + [TestMethod] + public void HaveSize_Should_Throw() + { + var account = new InMemoryStorageProvider().AddAccount(); + + var blob = InMemoryBlobClient.FromAccount(account, "test-container", "test-blob"); + + blob.GetParentBlobContainerClient().Create(); + + blob.Upload(BinaryData.FromString("test-datax")); + + var act = () => blob.Should().HaveSize(9); + + act.Should() + .Throw() + .WithMessage("" + + "Expected size of the blob to be 9L, but found 10L (difference of 1)." + + "With Blob:https://*/test-container/test-blob"); + } + + + [TestMethod] + public void BeEmpty_Should_Succeed() + { + var account = new InMemoryStorageProvider().AddAccount(); + + var blob = InMemoryBlobClient.FromAccount(account, "test-container", "test-blob"); + + blob.GetParentBlobContainerClient().Create(); + + blob.Upload(BinaryData.FromString(string.Empty)); + + blob.Should().BeEmpty(); + } + + [TestMethod] + public void BeEmpty_Should_Throw() + { + var account = new InMemoryStorageProvider().AddAccount(); + + var blob = InMemoryBlobClient.FromAccount(account, "test-container", "test-blob"); + + blob.GetParentBlobContainerClient().Create(); + + blob.Upload(BinaryData.FromString("test-data")); + + var act = () => blob.Should().BeEmpty(); + + act.Should() + .Throw() + .WithMessage("" + + "Expected size of the blob to be 0L, but found 9L (difference of 9)." + + "With Blob:*https://*/test-container/test-blob"); + } + + [TestMethod] + public void HaveContent_Should_Succeed() + { + var account = new InMemoryStorageProvider().AddAccount(); + + var blob = InMemoryBlobClient.FromAccount(account, "test-container", "test-blob"); + + blob.GetParentBlobContainerClient().Create(); + + blob.Upload(BinaryData.FromString("test-data")); + + blob.Should().HaveContent("test-data"); + } + + [TestMethod] + public void HaveContent_Should_Throw() + { + var account = new InMemoryStorageProvider().AddAccount(); + + var blob = InMemoryBlobClient.FromAccount(account, "test-container", "test-blob"); + + blob.GetParentBlobContainerClient().Create(); + + blob.Upload(BinaryData.FromString("test-data")); + + var act = () => blob.Should().HaveContent("test-data-x"); + + act.Should() + .Throw() + .WithMessage("" + + "Expected content of the blob to be \"test-data-x\" with a length of 11, but \"test-data\" has a length of 9, differs near \"a\" (index 8)." + + "With Blob:*https://*/test-container/test-blob"); + } + + [TestMethod] + public void Block_Count_Assertions_Should_Succeed() + { + var account = new InMemoryStorageProvider().AddAccount(); + + var blob = InMemoryBlockBlobClient.FromAccount(account, "test-container", "test-blob"); + + blob.GetParentBlobContainerClient().Create(); + + blob.StageBlock("dGVzdC1ibG9jay0x", BinaryData.FromString("test-data-1").ToStream()); + + blob.Should().HaveNoCommittedBlocks(); + blob.Should().HaveUncommittedBlocks(1); + + blob.StageBlock("dGVzdC1ibG9jay0y", BinaryData.FromString("test-data-2").ToStream()); + + blob.Should().HaveNoCommittedBlocks(); + blob.Should().HaveUncommittedBlocks(2); + + blob.CommitBlockList(["dGVzdC1ibG9jay0x"]); + + blob.Should().HaveCommittedBlocks(1); + blob.Should().HaveNoUncommittedBlocks(); + } + + [TestMethod] + public void Block_Count_Assertions_Should_Throw() + { + var account = new InMemoryStorageProvider().AddAccount(); + + var blob = InMemoryBlockBlobClient.FromAccount(account, "test-container", "test-blob"); + + blob.GetParentBlobContainerClient().Create(); + + blob.StageBlock("dGVzdC1ibG9jay0x", BinaryData.FromString("test-data-1").ToStream()); + + var actNoUncommittedBlocks = () => blob.Should().HaveNoUncommittedBlocks(); + actNoUncommittedBlocks.Should() + .Throw() + .WithMessage("Expected number of uncommitted blocks in the blob '*/test-container/test-blob' to contain 0 item(s), but found 1:*"); + + var actUncommittedBlocks = () => blob.Should().HaveUncommittedBlocks(2); + actUncommittedBlocks.Should() + .Throw() + .WithMessage("Expected number of uncommitted blocks in the blob '*/test-container/test-blob' to contain 2 item(s), but found 1:*"); + + var actCommittedBlocks = () => blob.Should().HaveCommittedBlocks(1); + actCommittedBlocks.Should() + .Throw() + .WithMessage("Expected number of committed blocks in the blob '*/test-container/test-blob' to contain 1 item(s), but found 0:*"); + + blob.CommitBlockList(["dGVzdC1ibG9jay0x"]); + + var actNoCommittedBlocks = () => blob.Should().HaveNoCommittedBlocks(); + actNoCommittedBlocks.Should() + .Throw() + .WithMessage("Expected number of committed blocks in the blob '*/test-container/test-blob' to contain 0 item(s), but found 1:*"); + + } + + [TestMethod] + public void HaveCommittedBlockSizes_Should_Succeeed() + { + var account = new InMemoryStorageProvider().AddAccount(); + + var blob = InMemoryBlockBlobClient.FromAccount(account, "test-container", "test-blob"); + + blob.GetParentBlobContainerClient().Create(); + + blob.StageBlock("dGVzdC1ibG9jay0x", BinaryData.FromString("test-data-a").ToStream()); + blob.StageBlock("dGVzdC1ibG9jay0y", BinaryData.FromString("test-data-ab").ToStream()); + blob.CommitBlockList(["dGVzdC1ibG9jay0x", "dGVzdC1ibG9jay0y"]); + + blob.Should().HaveCommittedBlocksWithSizes([11, 12]); + } + + [TestMethod] + public void HaveCommittedBlockSizes_With_Invalid_Number_Of_Blocks_Should_Throw() + { + var account = new InMemoryStorageProvider().AddAccount(); + + var blob = InMemoryBlockBlobClient.FromAccount(account, "test-container", "test-blob"); + + blob.GetParentBlobContainerClient().Create(); + + blob.StageBlock("dGVzdC1ibG9jay0x", BinaryData.FromString("test-data-a").ToStream()); + blob.StageBlock("dGVzdC1ibG9jay0y", BinaryData.FromString("test-data-ab").ToStream()); + blob.CommitBlockList(["dGVzdC1ibG9jay0x", "dGVzdC1ibG9jay0y"]); + + var actOneLess = () => blob.Should().HaveCommittedBlocksWithSizes([11]); + + actOneLess.Should() + .Throw() + .WithMessage("Expected number of committed blocks in the blob '*/test-container/test-blob' to contain 1 item(s), but found 2:*"); + + var actOneMore = () => blob.Should().HaveCommittedBlocksWithSizes([11, 12, 13]); + actOneMore.Should() + .Throw() + .WithMessage("Expected number of committed blocks in the blob '*/test-container/test-blob' to contain 3 item(s), but found 2:*"); + + } + + [TestMethod] + public void HaveCommittedBlockSizes_With_Invalid_Sizes_Should_Throw() + { + var account = new InMemoryStorageProvider().AddAccount(); + + var blob = InMemoryBlockBlobClient.FromAccount(account, "test-container", "test-blob"); + + blob.GetParentBlobContainerClient().Create(); + + blob.StageBlock("dGVzdC1ibG9jay0x", BinaryData.FromString("test-data-a").ToStream()); + blob.StageBlock("dGVzdC1ibG9jay0y", BinaryData.FromString("test-data-ab").ToStream()); + blob.CommitBlockList(["dGVzdC1ibG9jay0x", "dGVzdC1ibG9jay0y"]); + + var act = () => blob.Should().HaveCommittedBlocksWithSizes([24, 41]); + + act.Should() + .Throw() + .WithMessage("" + + "Expected block #0 size to be 24, but found 11 (difference of -13).\n" + + "Expected block #1 size to be 41, but found 12 (difference of -29).*" + + "With Blob:*https://*/test-container/test-blob"); + + } + + + [TestMethod] + public void HaveCommittedBlock_Should_Succeed() + { + var account = new InMemoryStorageProvider().AddAccount(); + + var blob = InMemoryBlockBlobClient.FromAccount(account, "test-container", "test-blob"); + + blob.GetParentBlobContainerClient().Create(); + + blob.StageBlock("dGVzdC1ibG9jay0x", BinaryData.FromString("test-data-a").ToStream()); + blob.CommitBlockList(["dGVzdC1ibG9jay0x"]); + + blob.Should().HaveCommittedBlock(0, block => block.Name.Should().Be("dGVzdC1ibG9jay0x")); + + } + + [TestMethod] + public void HaveCommittedBlock_Should_Throw() + { + var account = new InMemoryStorageProvider().AddAccount(); + + var blob = InMemoryBlockBlobClient.FromAccount(account, "test-container", "test-blob"); + + blob.GetParentBlobContainerClient().Create(); + + blob.StageBlock("dGVzdC1ibG9jay0x", BinaryData.FromString("test-data-a").ToStream()); + blob.CommitBlockList(["dGVzdC1ibG9jay0x"]); + + var act = () => blob.Should().HaveCommittedBlock(0, block => block.Name.Should().Be("XdGVzdC1ibG9jay0x")); + + act.Should() + .Throw() + .WithMessage("" + + "Expected * to be \"XdGVzdC1ibG9jay0x\" with a length of 17, but \"dGVzdC1ibG9jay0x\" has a length of 16, differs near \"dGV\" (index 0).*" + + "With Blob:*https://*/test-container/test-blob*" + + "With Block:*#0"); + + } + + +} diff --git a/tests/Tests/Storage/Blobs/FluentAssertionsTests.cs b/tests/Tests/Storage/Blobs/FluentAssertionsTests.cs deleted file mode 100644 index 45e3f11..0000000 --- a/tests/Tests/Storage/Blobs/FluentAssertionsTests.cs +++ /dev/null @@ -1,38 +0,0 @@ -using Azure.Storage.Blobs.Specialized; - -using Spotflow.InMemory.Azure.Storage; -using Spotflow.InMemory.Azure.Storage.Blobs; -using Spotflow.InMemory.Azure.Storage.FluentAssertions; - -namespace Tests.Storage.Blobs; - -[TestClass] -public class FluentAssertionsTests -{ - [TestMethod] - public async Task ExistAsync_Should_Wait_For_Blob_To_Be_Created() - { - var account = new InMemoryStorageProvider().AddAccount(); - - var blob = InMemoryBlobClient.FromAccount(account, "test-container", "test-blob"); - - blob.GetParentBlobContainerClient().Create(); - - var existTask = blob.Should().ExistAsync(); - - existTask.IsCompleted.Should().BeFalse(); - - await Task.Delay(100); - - existTask.IsCompleted.Should().BeFalse(); - - await Task.Delay(100); - - blob.Upload(BinaryData.FromString("test-data")); - - await existTask; - - existTask.IsCompletedSuccessfully.Should().BeTrue(); - - } -}