diff --git a/CosmosTestHelpers.IntegrationTests/ContainerMockTests.cs b/CosmosTestHelpers.IntegrationTests/ContainerMockTests.cs deleted file mode 100644 index f255d27..0000000 --- a/CosmosTestHelpers.IntegrationTests/ContainerMockTests.cs +++ /dev/null @@ -1,75 +0,0 @@ -using System.Diagnostics.CodeAnalysis; -using System.Text; -using CosmosTestHelpers.IntegrationTests.TestModels; -using FluentAssertions; -using Microsoft.Azure.Cosmos; -using Newtonsoft.Json; -using Xunit; - -namespace CosmosTestHelpers.IntegrationTests -{ - public class ContainerMockTests - { - [Theory] - [InlineData("/")] - [InlineData(@"\\")] - [InlineData("?")] - [InlineData("#")] - public async Task When_inserting_an_item_with_a_forward_slash_in_the_id__Then_an_error_occurs(string invalidChars) - { - var containerMock = new ContainerMock(); - - var model = new TestModel { Id = "url" + invalidChars + "WillBreak", PartitionKey = "pk" }; - Func act = () => containerMock.CreateItemAsync(model, new PartitionKey(model.PartitionKey)); - await act.Should().ThrowAsync(); - } - - [Theory] - [InlineData("/")] - [InlineData(@"\")] - [InlineData("?")] - [InlineData("#")] - [SuppressMessage("Reliability", "CA2000", Justification = "Calling AsyncDispose")] - public async Task When_inserting_an_item_by_stream_with_a_forward_slash_in_the_id__Then_an_error_occurs(string invalidChars) - { - var containerMock = new ContainerMock(); - - var model = new TestModel { Id = "url" + invalidChars + "WillBreak", PartitionKey = "pk" }; - var bytes = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(model)); - await using var ms = new MemoryStream(bytes); - Func act = () => containerMock.CreateItemStreamAsync(ms, new PartitionKey(model.PartitionKey)); - await act.Should().ThrowAsync(); - } - - [Theory] - [InlineData("/")] - [InlineData(@"\")] - [InlineData("?")] - [InlineData("#")] - public async Task When_upserting_an_item__with_a_forward_slash_in_the_id__Then_an_error_occurs(string invalidChars) - { - var containerMock = new ContainerMock(); - - var model = new TestModel { Id = "url" + invalidChars + "WillBreak", PartitionKey = "pk" }; - Func act = () => containerMock.UpsertItemAsync(model, new PartitionKey(model.PartitionKey)); - await act.Should().ThrowAsync(); - } - - [Theory] - [InlineData("/")] - [InlineData(@"\\")] - [InlineData("?")] - [InlineData("#")] - [SuppressMessage("Reliability", "CA2000", Justification = "Calling AsyncDispose")] - public async Task When_upserting_an_item_by_stream__with_a_forward_slash_in_the_id__Then_an_error_occurs(string invalidChars) - { - var containerMock = new ContainerMock(); - - var model = new TestModel { Id = "url" + invalidChars + "WillBreak", PartitionKey = "pk"}; - var bytes = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(model)); - await using var ms = new MemoryStream(bytes); - Func act = () => containerMock.UpsertItemStreamAsync(ms, new PartitionKey(model.PartitionKey)); - await act.Should().ThrowAsync(); - } - } -} \ No newline at end of file diff --git a/CosmosTestHelpers.IntegrationTests/CosmosCreateContainerWithUniqueKeyIncludingId.cs b/CosmosTestHelpers.IntegrationTests/CosmosCreateContainerWithUniqueKeyIncludingId.cs index f0b5aad..bfc6000 100644 --- a/CosmosTestHelpers.IntegrationTests/CosmosCreateContainerWithUniqueKeyIncludingId.cs +++ b/CosmosTestHelpers.IntegrationTests/CosmosCreateContainerWithUniqueKeyIncludingId.cs @@ -2,38 +2,45 @@ using Microsoft.Azure.Cosmos; using Xunit; -namespace CosmosTestHelpers.IntegrationTests +namespace CosmosTestHelpers.IntegrationTests; + +[Collection("Integration Tests")] +public sealed class CosmosCreateContainerWithUniqueKeyIncludingId : IAsyncLifetime, IDisposable { - [Collection("Integration Tests")] - public sealed class CosmosCreateContainerWithUniqueKeyIncludingId : IAsyncLifetime, IDisposable - { - private TestCosmos _testCosmos; + private TestCosmos _testCosmos; - [Fact] - public async Task CreateUniqueKeyViolationIsEquivalent() + [Fact] + public async Task CreateUniqueKeyViolationIsEquivalent() + { + var uniqueKeyPolicy = new UniqueKeyPolicy { - var (realException, testException) = await _testCosmos.SetupAsyncProducesExceptions("/partitionKey", new UniqueKeyPolicy { UniqueKeys = { new UniqueKey { Paths = { "/id", "/ItemId", "/Type" } } } }); + UniqueKeys = { new UniqueKey { Paths = { "/id", "/ItemId", "/Type" } } } + }; + + var (realException, testException) = await _testCosmos.SetupAsyncProducesExceptions( + "/partitionKey", + uniqueKeyPolicy + ); - realException.Should().NotBeNull(); - testException.Should().NotBeNull(); - realException.StatusCode.Should().Be(testException.StatusCode); - realException.Should().BeOfType(testException.GetType()); - } + realException.Should().NotBeNull(); + testException.Should().NotBeNull(); + realException.StatusCode.Should().Be(testException.StatusCode); + realException.Should().BeOfType(testException.GetType()); + } - public Task InitializeAsync() - { - _testCosmos = new TestCosmos(); - return Task.CompletedTask; - } + public Task InitializeAsync() + { + _testCosmos = new TestCosmos(); + return Task.CompletedTask; + } - public async Task DisposeAsync() - { - await _testCosmos.CleanupAsync(); - } + public async Task DisposeAsync() + { + await _testCosmos.CleanupAsync(); + } - public void Dispose() - { - _testCosmos?.Dispose(); - } + public void Dispose() + { + _testCosmos?.Dispose(); } } \ No newline at end of file diff --git a/CosmosTestHelpers.IntegrationTests/CosmosCreateStreamTests.cs b/CosmosTestHelpers.IntegrationTests/CosmosCreateStreamTests.cs index b5d5450..f43f146 100644 --- a/CosmosTestHelpers.IntegrationTests/CosmosCreateStreamTests.cs +++ b/CosmosTestHelpers.IntegrationTests/CosmosCreateStreamTests.cs @@ -1,129 +1,124 @@ -using System.Diagnostics.CodeAnalysis; -using System.Text; +using System.Text; using CosmosTestHelpers.IntegrationTests.TestModels; using FluentAssertions; using Microsoft.Azure.Cosmos; using Newtonsoft.Json; using Xunit; -namespace CosmosTestHelpers.IntegrationTests +namespace CosmosTestHelpers.IntegrationTests; + +[Collection("Integration Tests")] +public sealed class CosmosCreateStreamTests : IAsyncLifetime, IDisposable { - [Collection("Integration Tests")] - public sealed class CosmosCreateStreamTests : IAsyncLifetime, IDisposable - { - private TestCosmos _testCosmos; + private TestCosmos _testCosmos; - [Fact] - [SuppressMessage("", "CA2000", Justification = "AsyncDispose....")] - public async Task CreateStreamNonExistingIsEquivalent() - { - var bytes = GetItemBytes( - new TestModel - { - Id = "RECORD2", - Name = "Bob Bobertson", - EnumValue = TestEnum.Value1, - PartitionKey = "test-cst1" - }); - - await using var ms = new MemoryStream(bytes); - var (real, test) = await _testCosmos.WhenCreatingStream(ms, new PartitionKey("test-cst1")); - - real.StatusCode.Should().Be(test.StatusCode); - } - - [Fact] - [SuppressMessage("", "CA2000", Justification = "AsyncDispose....")] - public async Task CreateStreamExistingIsEquivalent() + [Fact] + public async Task CreateStreamNonExistingIsEquivalent() + { + var bytes = GetItemBytes(new TestModel { - await _testCosmos.GivenAnExistingItem(new TestModel - { - Id = "RECORD1", - Name = "Bob Bobertson", - EnumValue = TestEnum.Value2, - PartitionKey = "test-cst2" - }); - - var bytes = GetItemBytes( - new TestModel - { - Id = "RECORD1", - Name = "Bob Bobertson", - EnumValue = TestEnum.Value1, - PartitionKey = "test-cst2" - }); - - await using var ms = new MemoryStream(bytes); - var (real, test) = await _testCosmos.WhenCreatingStream(ms, new PartitionKey("test-cst2")); - - real.StatusCode.Should().Be(test.StatusCode); - } - - [Fact] - [SuppressMessage("", "CA2000", Justification = "AsyncDispose....")] - public async Task CreateStreamWithEmptyIdIsEquivalent() + Id = "RECORD2", + Name = "Bob Bobertson", + EnumValue = TestEnum.Value1, + PartitionKey = "test-cst1" + }); + + await using var ms = new MemoryStream(bytes); + var (real, test) = await _testCosmos.WhenCreatingStream(ms, new PartitionKey("test-cst1")); + + real.StatusCode.Should().Be(test.StatusCode); + } + + [Fact] + public async Task CreateStreamExistingIsEquivalent() + { + await _testCosmos.GivenAnExistingItem(new TestModel { - var bytes = GetItemBytes( - new TestModel - { - Id = string.Empty, - Name = "Bob Bobertson", - EnumValue = TestEnum.Value1, - PartitionKey = "test-cst2" - }); - - await using var ms = new MemoryStream(bytes); - var (real, test) = await _testCosmos.WhenCreatingStream(ms, new PartitionKey("test-cst2")); - - real.StatusCode.Should().Be(test.StatusCode); - } - - [Fact] - [SuppressMessage("", "CA2000", Justification = "AsyncDispose....")] - public async Task CreateStreamUniqueKeyViolationIsEquivalent() + Id = "RECORD1", + Name = "Bob Bobertson", + EnumValue = TestEnum.Value2, + PartitionKey = "test-cst2" + }); + + var bytes = GetItemBytes(new TestModel { - await _testCosmos.GivenAnExistingItem(new TestModel - { - Id = "RECORD1", - Name = "Bob Bobertson", - EnumValue = TestEnum.Value2, - PartitionKey = "test-cst3" - }); - - var bytes = GetItemBytes( - new TestModel - { - Id = "RECORD2", - Name = "Bob Bobertson", - EnumValue = TestEnum.Value1, - PartitionKey = "test-cst3" - }); - - await using var ms = new MemoryStream(bytes); - var (real, test) = await _testCosmos.WhenCreatingStream(ms, new PartitionKey("test-cst3")); - - real.StatusCode.Should().Be(test.StatusCode); - } - - public Task InitializeAsync() + Id = "RECORD1", + Name = "Bob Bobertson", + EnumValue = TestEnum.Value1, + PartitionKey = "test-cst2" + }); + + await using var ms = new MemoryStream(bytes); + var (real, test) = await _testCosmos.WhenCreatingStream(ms, new PartitionKey("test-cst2")); + + real.StatusCode.Should().Be(test.StatusCode); + } + + [Fact] + public async Task CreateStreamWithEmptyIdIsEquivalent() + { + var bytes = GetItemBytes(new TestModel { - _testCosmos = new TestCosmos(); - return _testCosmos.SetupAsync("/partitionKey", new UniqueKeyPolicy { UniqueKeys = { new UniqueKey { Paths = {"/Name"}}}}); - } + Id = string.Empty, + Name = "Bob Bobertson", + EnumValue = TestEnum.Value1, + PartitionKey = "test-cst2" + }); - public async Task DisposeAsync() + await using var ms = new MemoryStream(bytes); + var (real, test) = await _testCosmos.WhenCreatingStream(ms, new PartitionKey("test-cst2")); + + real.StatusCode.Should().Be(test.StatusCode); + } + + [Fact] + public async Task CreateStreamUniqueKeyViolationIsEquivalent() + { + await _testCosmos.GivenAnExistingItem(new TestModel { - await _testCosmos.CleanupAsync(); - } + Id = "RECORD1", + Name = "Bob Bobertson", + EnumValue = TestEnum.Value2, + PartitionKey = "test-cst3" + }); - public void Dispose() + var bytes = GetItemBytes(new TestModel { - _testCosmos?.Dispose(); - } + Id = "RECORD2", + Name = "Bob Bobertson", + EnumValue = TestEnum.Value1, + PartitionKey = "test-cst3" + }); + + await using var ms = new MemoryStream(bytes); + var (real, test) = await _testCosmos.WhenCreatingStream(ms, new PartitionKey("test-cst3")); + + real.StatusCode.Should().Be(test.StatusCode); + } - private static byte[] GetItemBytes(TestModel model) + public async Task InitializeAsync() + { + var uniqueKeyPolicy = new UniqueKeyPolicy { - return Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(model)); - } + UniqueKeys = { new UniqueKey { Paths = { "/Name" } } } + }; + + _testCosmos = new TestCosmos(); + await _testCosmos.SetupAsync("/partitionKey", uniqueKeyPolicy); + } + + public async Task DisposeAsync() + { + await _testCosmos.CleanupAsync(); + } + + public void Dispose() + { + _testCosmos?.Dispose(); + } + + private static byte[] GetItemBytes(TestModel model) + { + return Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(model)); } } \ No newline at end of file diff --git a/CosmosTestHelpers.IntegrationTests/CosmosCreateTests.cs b/CosmosTestHelpers.IntegrationTests/CosmosCreateTests.cs index 5c87bfa..df8a739 100644 --- a/CosmosTestHelpers.IntegrationTests/CosmosCreateTests.cs +++ b/CosmosTestHelpers.IntegrationTests/CosmosCreateTests.cs @@ -3,107 +3,112 @@ using Microsoft.Azure.Cosmos; using Xunit; -namespace CosmosTestHelpers.IntegrationTests +namespace CosmosTestHelpers.IntegrationTests; + +[Collection("Integration Tests")] +public sealed class CosmosCreateTests : IAsyncLifetime, IDisposable { - [Collection("Integration Tests")] - public sealed class CosmosCreateTests : IAsyncLifetime, IDisposable - { - private TestCosmos _testCosmos; + private TestCosmos _testCosmos; - [Fact] - public async Task CreateNonExistingIsEquivalent() - { - var testModel = new TestModel - { - Id = "RECORD1", - Name = "Fred Blogs", - EnumValue = TestEnum.Value1 - }; - var (realResult, testResult) = await _testCosmos.WhenCreating( - testModel, new PartitionKey(testModel.PartitionKey)); - - realResult.StatusCode.Should().Be(testResult.StatusCode); - realResult.Resource.Should().BeEquivalentTo(testResult.Resource); - } - - [Fact] - public async Task CreateEmptyIdIsEquivalent() + [Fact] + public async Task CreateNonExistingIsEquivalent() + { + var testModel = new TestModel { - var testModel = new TestModel - { - Id = string.Empty, - Name = "Bob Bobertson", - EnumValue = TestEnum.Value1 - }; - - var (realException, testException) = await _testCosmos.WhenCreatingProducesException( - testModel, new PartitionKey(testModel.PartitionKey)); - - realException.Should().NotBeNull(); - testException.Should().NotBeNull(); - realException.StatusCode.Should().Be(testException.StatusCode); - realException.Should().BeOfType(testException.GetType()); - } - - [Fact] - public async Task CreateExistingIsEquivalent() + Id = "RECORD1", + Name = "Fred Blogs", + EnumValue = TestEnum.Value1 + }; + + var (realResult, testResult) = await _testCosmos.WhenCreating( + testModel, new PartitionKey(testModel.PartitionKey)); + + realResult.StatusCode.Should().Be(testResult.StatusCode); + realResult.Resource.Should().BeEquivalentTo(testResult.Resource); + } + + [Fact] + public async Task CreateEmptyIdIsEquivalent() + { + var testModel = new TestModel { - await _testCosmos.GivenAnExistingItem(new TestModel - { - Id = "RECORD1", - Name = "Bob Bobertson", - EnumValue = TestEnum.Value2 - }); - - var testModel = new TestModel - { - Id = "RECORD1", - Name = "Bob Bobertson", - EnumValue = TestEnum.Value1 - }; - - var (realException, testException) = await _testCosmos.WhenCreatingProducesException( - testModel, new PartitionKey(testModel.PartitionKey)); - - realException.Should().NotBeNull(); - testException.Should().NotBeNull(); - realException.StatusCode.Should().Be(testException.StatusCode); - realException.Should().BeOfType(testException.GetType()); - } - - [Fact] - public async Task CreateWithMismatchedPartitionIsEquivalent() + Id = string.Empty, + Name = "Bob Bobertson", + EnumValue = TestEnum.Value1 + }; + + var (realException, testException) = await _testCosmos.WhenCreatingProducesException( + testModel, new PartitionKey(testModel.PartitionKey)); + + realException.Should().NotBeNull(); + testException.Should().NotBeNull(); + realException.StatusCode.Should().Be(testException.StatusCode); + realException.Should().BeOfType(testException.GetType()); + } + + [Fact] + public async Task CreateExistingIsEquivalent() + { + await _testCosmos.GivenAnExistingItem(new TestModel { - var testModel = new TestModel - { - Id = "RECORD1", - Name = "Bob Bobertson", - EnumValue = TestEnum.Value1 - }; - - var (realException, testException) = await _testCosmos.WhenCreatingProducesException( - testModel, PartitionKey.None); - - realException.Should().NotBeNull(); - testException.Should().NotBeNull(); - realException.StatusCode.Should().Be(testException.StatusCode); - realException.Should().BeOfType(testException.GetType()); - } - - public Task InitializeAsync() + Id = "RECORD1", + Name = "Bob Bobertson", + EnumValue = TestEnum.Value2 + }); + + var testModel = new TestModel { - _testCosmos = new TestCosmos(); - return _testCosmos.SetupAsync("/partitionKey", new UniqueKeyPolicy { UniqueKeys = { new UniqueKey { Paths = {"/Name"}}}}); - } + Id = "RECORD1", + Name = "Bob Bobertson", + EnumValue = TestEnum.Value1 + }; - public async Task DisposeAsync() + var (realException, testException) = await _testCosmos.WhenCreatingProducesException( + testModel, new PartitionKey(testModel.PartitionKey)); + + realException.Should().NotBeNull(); + testException.Should().NotBeNull(); + realException.StatusCode.Should().Be(testException.StatusCode); + realException.Should().BeOfType(testException.GetType()); + } + + [Fact] + public async Task CreateWithMismatchedPartitionIsEquivalent() + { + var testModel = new TestModel { - await _testCosmos.CleanupAsync(); - } + Id = "RECORD1", + Name = "Bob Bobertson", + EnumValue = TestEnum.Value1 + }; - public void Dispose() + var (realException, testException) = await _testCosmos.WhenCreatingProducesException( + testModel, PartitionKey.None); + + realException.Should().NotBeNull(); + testException.Should().NotBeNull(); + realException.StatusCode.Should().Be(testException.StatusCode); + realException.Should().BeOfType(testException.GetType()); + } + + public Task InitializeAsync() + { + var uniqueKeyPolicy = new UniqueKeyPolicy { - _testCosmos?.Dispose(); - } + UniqueKeys = { new UniqueKey { Paths = { "/Name" } } } + }; + + _testCosmos = new TestCosmos(); + return _testCosmos.SetupAsync("/partitionKey", uniqueKeyPolicy); + } + + public async Task DisposeAsync() + { + await _testCosmos.CleanupAsync(); + } + + public void Dispose() + { + _testCosmos?.Dispose(); } } \ No newline at end of file diff --git a/CosmosTestHelpers.IntegrationTests/CosmosDeleteTests.cs b/CosmosTestHelpers.IntegrationTests/CosmosDeleteTests.cs index e838c0a..75ed950 100644 --- a/CosmosTestHelpers.IntegrationTests/CosmosDeleteTests.cs +++ b/CosmosTestHelpers.IntegrationTests/CosmosDeleteTests.cs @@ -3,83 +3,87 @@ using Microsoft.Azure.Cosmos; using Xunit; -namespace CosmosTestHelpers.IntegrationTests +namespace CosmosTestHelpers.IntegrationTests; + +[Collection("Integration Tests")] +public sealed class CosmosDeleteTests : IAsyncLifetime, IDisposable { - [Collection("Integration Tests")] - public sealed class CosmosDeleteTests : IAsyncLifetime, IDisposable + private TestCosmos _testCosmos; + + [Fact] + public async Task DeleteExistingWithWrongETagIsEquivalent() { - private TestCosmos _testCosmos; - - [Fact] - public async Task DeleteExistingWithWrongETagIsEquivalent() + await _testCosmos.GivenAnExistingItem(new TestModel { - await _testCosmos.GivenAnExistingItem(new TestModel + Id = "RECORD1", + Name = "Bob Bobertson", + EnumValue = TestEnum.Value2, + PartitionKey = "test-dt1" + }); + + var (realException, testException) = await _testCosmos.WhenDeletingProducesException( + "RECORD1", + new PartitionKey("test-dt1"), + new ItemRequestOptions { - Id = "RECORD1", - Name = "Bob Bobertson", - EnumValue = TestEnum.Value2, - PartitionKey = "test-dt1" + IfMatchEtag = Guid.NewGuid().ToString() + }, + new ItemRequestOptions + { + IfMatchEtag = Guid.NewGuid().ToString() }); - var (realException, testException) = await _testCosmos.WhenDeletingProducesException( - "RECORD1", - new PartitionKey("test-dt1"), - new ItemRequestOptions - { - IfMatchEtag = Guid.NewGuid().ToString() - }, - new ItemRequestOptions - { - IfMatchEtag = Guid.NewGuid().ToString() - }); + realException.Should().NotBeNull(); + testException.Should().NotBeNull(); + realException.StatusCode.Should().Be(testException.StatusCode); + realException.Should().BeOfType(testException.GetType()); + } - realException.Should().NotBeNull(); - testException.Should().NotBeNull(); - realException.StatusCode.Should().Be(testException.StatusCode); - realException.Should().BeOfType(testException.GetType()); - } - - [Fact] - public async Task DeleteExistingWithCorrectETagIsEquivalent() + [Fact] + public async Task DeleteExistingWithCorrectETagIsEquivalent() + { + var (testETag, realETag) = await _testCosmos.GivenAnExistingItem(new TestModel { - var (testETag, realETag) = await _testCosmos.GivenAnExistingItem(new TestModel + Id = "RECORD1", + Name = "Bob Bobertson", + EnumValue = TestEnum.Value2, + PartitionKey = "test-dt2" + }); + + var (realResult, testResult) = await _testCosmos.WhenDeleting( + "RECORD1", + new PartitionKey("test-dt2"), + new ItemRequestOptions { - Id = "RECORD1", - Name = "Bob Bobertson", - EnumValue = TestEnum.Value2, - PartitionKey = "test-dt2" + IfMatchEtag = testETag + }, + new ItemRequestOptions + { + IfMatchEtag = realETag }); - var (realResult, testResult) = await _testCosmos.WhenDeleting( - "RECORD1", - new PartitionKey("test-dt2"), - new ItemRequestOptions - { - IfMatchEtag = testETag - }, - new ItemRequestOptions - { - IfMatchEtag = realETag - }); + realResult.StatusCode.Should().Be(testResult.StatusCode); + realResult.Resource.Should().BeEquivalentTo(testResult.Resource); + } - realResult.StatusCode.Should().Be(testResult.StatusCode); - realResult.Resource.Should().BeEquivalentTo(testResult.Resource); - } - - public Task InitializeAsync() + public Task InitializeAsync() + { + var uniqueKeyPolicy = new UniqueKeyPolicy { - _testCosmos = new TestCosmos(); - return _testCosmos.SetupAsync("/partitionKey", new UniqueKeyPolicy { UniqueKeys = { new UniqueKey { Paths = {"/Name"}}}}); - } + UniqueKeys = { new UniqueKey { Paths = { "/Name" } } } + }; - public async Task DisposeAsync() - { - await _testCosmos.CleanupAsync(); - } + _testCosmos = new TestCosmos(); + return _testCosmos.SetupAsync("/partitionKey", uniqueKeyPolicy); + } - public void Dispose() - { - _testCosmos?.Dispose(); - } + public async Task DisposeAsync() + { + await _testCosmos.CleanupAsync(); + } + + public void Dispose() + { + _testCosmos?.Dispose(); } } \ No newline at end of file diff --git a/CosmosTestHelpers.IntegrationTests/CosmosEnumTests.cs b/CosmosTestHelpers.IntegrationTests/CosmosEnumTests.cs index e681bff..543a8dc 100644 --- a/CosmosTestHelpers.IntegrationTests/CosmosEnumTests.cs +++ b/CosmosTestHelpers.IntegrationTests/CosmosEnumTests.cs @@ -1,206 +1,229 @@ -using System.Diagnostics.CodeAnalysis; -using CosmosTestHelpers.IntegrationTests.TestModels; +using CosmosTestHelpers.IntegrationTests.TestModels; using FluentAssertions; using Xunit; // Deliberately testing this #pragma warning disable 472 -namespace CosmosTestHelpers.IntegrationTests -{ - [SuppressMessage("", "SA1131", Justification = "I want this code to work regardless of the ordering of the equals")] - [Collection("Integration Tests")] - public sealed class CosmosEnumTests : IAsyncLifetime, IDisposable - { - private TestCosmos _testCosmos; - [Fact] - public async Task GivenAQueryUsingAValueForNullableEnumWhenExecutingThenTheResultsShouldMatch() - { - await _testCosmos.GivenAnExistingItem(new TestModel - { - Id = "RECORD1", - Name = "Bob Bobertson", - Value = false, - PartitionKey = "partition-et1", - NullableEnum = TestEnum.Value1 - }); +namespace CosmosTestHelpers.IntegrationTests; - var (realResults, testResults) = await _testCosmos.WhenExecutingAQuery("partition-et1", q => q.Where(tm => tm.NullableEnum.Value != null && tm.NullableEnum == TestEnum.Value1)); +[Collection("Integration Tests")] +public sealed class CosmosEnumTests : IAsyncLifetime, IDisposable +{ + private TestCosmos _testCosmos; - realResults.Count.Should().Be(1); - testResults.Should().BeEquivalentTo(realResults); - } - - [Fact] - public async Task GivenAQueryUsingAValueForNullableOnSubModelEnumWhenExecutingThenTheResultsShouldMatch() + [Fact] + public async Task GivenAQueryUsingAValueForNullableEnumWhenExecutingThenTheResultsShouldMatch() + { + await _testCosmos.GivenAnExistingItem(new TestModel { - await _testCosmos.GivenAnExistingItem(new TestModel - { - Id = "RECORD1", - Name = "Bob Bobertson", - PartitionKey = "partition-et2", - OnlyChild = new SubModel - { - NullableEnum = TestEnum.Value1 - } - }); - - var (realResults, testResults) = await _testCosmos.WhenExecutingAQuery("partition-et2", q => q.Where(tm => tm.OnlyChild != null && tm.OnlyChild.NullableEnum.Value != null && tm.OnlyChild.NullableEnum == TestEnum.Value1)); - - realResults.Count.Should().Be(1); - testResults.Should().BeEquivalentTo(realResults); - } - - [Fact] - public async Task GivenAQueryUsingAValueForNullableEnumWhenExecutingThenTheResultsShouldMatchReversedEquality() + Id = "RECORD1", + Name = "Bob Bobertson", + Value = false, + PartitionKey = "partition-et1", + NullableEnum = TestEnum.Value1 + }); + + var (realResults, testResults) = await _testCosmos.WhenExecutingAQuery( + "partition-et1", + q => q.Where(tm => tm.NullableEnum.Value != null && tm.NullableEnum == TestEnum.Value1) + ); + + realResults.Count.Should().Be(1); + testResults.Should().BeEquivalentTo(realResults); + } + + [Fact] + public async Task GivenAQueryUsingAValueForNullableOnSubModelEnumWhenExecutingThenTheResultsShouldMatch() + { + await _testCosmos.GivenAnExistingItem(new TestModel { - await _testCosmos.GivenAnExistingItem(new TestModel + Id = "RECORD1", + Name = "Bob Bobertson", + PartitionKey = "partition-et2", + OnlyChild = new SubModel { - Id = "RECORD1", - Name = "Bob Bobertson", - Value = false, - PartitionKey = "partition-et3", NullableEnum = TestEnum.Value1 - }); - - var (realResults, testResults) = await _testCosmos.WhenExecutingAQuery("partition-et3", q => q.Where(tm => null != tm.NullableEnum.Value && TestEnum.Value1 == tm.NullableEnum)); - - realResults.Count.Should().Be(1); - testResults.Should().BeEquivalentTo(realResults); - } + } + }); + + var (realResults, testResults) = await _testCosmos.WhenExecutingAQuery( + "partition-et2", + q => q.Where(tm => + tm.OnlyChild != null && tm.OnlyChild.NullableEnum.Value != null && + tm.OnlyChild.NullableEnum == TestEnum.Value1) + ); + + realResults.Count.Should().Be(1); + testResults.Should().BeEquivalentTo(realResults); + } - [Fact] - public async Task GivenAQueryUsingNullableEnumWhenExecutingThenTheResultsShouldMatch() + [Fact] + public async Task GivenAQueryUsingAValueForNullableEnumWhenExecutingThenTheResultsShouldMatchReversedEquality() + { + await _testCosmos.GivenAnExistingItem(new TestModel { - await _testCosmos.GivenAnExistingItem(new TestModel - { - Id = "RECORD1", - Name = "Bob Bobertson", - Value = false, - PartitionKey = "partition-et4", - NullableEnum = null - }); - - var (realResults, testResults) = await _testCosmos.WhenExecutingAQuery("partition-et4", q => q.Where(tm => tm.NullableEnum.Value == null)); + Id = "RECORD1", + Name = "Bob Bobertson", + Value = false, + PartitionKey = "partition-et3", + NullableEnum = TestEnum.Value1 + }); + + var (realResults, testResults) = await _testCosmos.WhenExecutingAQuery( + "partition-et3", + q => q.Where(tm => null != tm.NullableEnum.Value && TestEnum.Value1 == tm.NullableEnum) + ); + + realResults.Count.Should().Be(1); + testResults.Should().BeEquivalentTo(realResults); + } - realResults.Count.Should().Be(1); - testResults.Should().BeEquivalentTo(realResults); - } - - [Fact] - public async Task GivenAQueryUsingNullableEnumOnSubmodelWhenExecutingThenTheResultsShouldMatch() - { - await _testCosmos.GivenAnExistingItem(new TestModel - { - Id = "RECORD1", - Name = "Bob Bobertson", - Value = false, - PartitionKey = "partition-et5", - OnlyChild = new SubModel - { - NullableEnum = null - } - }); - - var (realResults, testResults) = await _testCosmos.WhenExecutingAQuery("partition-et5", q => q.Where(tm => tm.OnlyChild.NullableEnum.Value == null)); - - realResults.Count.Should().Be(1); - testResults.Should().BeEquivalentTo(realResults); - } - - [Fact] - public async Task GivenAQueryUsingNotNullEnumOnSubmodelWhenExecutingThenTheResultsShouldMatch() + [Fact] + public async Task GivenAQueryUsingNullableEnumWhenExecutingThenTheResultsShouldMatch() + { + await _testCosmos.GivenAnExistingItem(new TestModel { - await _testCosmos.GivenAnExistingItem(new TestModel - { - Id = "RECORD1", - Name = "Bob Bobertson", - Value = false, - PartitionKey = "partition-et6", - OnlyChild = new SubModel - { - NullableEnum = null - } - }); - - var (realResults, testResults) = await _testCosmos.WhenExecutingAQuery("partition-et6", q => q.Where(tm => tm.OnlyChild.NullableEnum.Value != null)); - - realResults.Count.Should().Be(0); - testResults.Should().BeEquivalentTo(realResults); - } - - [Fact] - public async Task GivenAQueryUsingNullableEnumWhenExecutingThenTheResultsShouldMatchReversedEquality() + Id = "RECORD1", + Name = "Bob Bobertson", + Value = false, + PartitionKey = "partition-et4", + NullableEnum = null + }); + + var (realResults, testResults) = await _testCosmos.WhenExecutingAQuery( + "partition-et4", + q => q.Where(tm => tm.NullableEnum.Value == null) + ); + + realResults.Count.Should().Be(1); + testResults.Should().BeEquivalentTo(realResults); + } + + [Fact] + public async Task GivenAQueryUsingNullableEnumOnSubmodelWhenExecutingThenTheResultsShouldMatch() + { + await _testCosmos.GivenAnExistingItem(new TestModel { - await _testCosmos.GivenAnExistingItem(new TestModel + Id = "RECORD1", + Name = "Bob Bobertson", + Value = false, + PartitionKey = "partition-et5", + OnlyChild = new SubModel { - Id = "RECORD1", - Name = "Bob Bobertson", - Value = false, - PartitionKey = "partition-et7", NullableEnum = null - }); + } + }); - var (realResults, testResults) = await _testCosmos.WhenExecutingAQuery("partition-et7", q => q.Where(tm => null == tm.NullableEnum.Value)); + var (realResults, testResults) = await _testCosmos.WhenExecutingAQuery( + "partition-et5", + q => q.Where(tm => tm.OnlyChild.NullableEnum.Value == null) + ); - realResults.Count.Should().Be(1); - testResults.Should().BeEquivalentTo(realResults); - } + realResults.Count.Should().Be(1); + testResults.Should().BeEquivalentTo(realResults); + } - [Fact] - public async Task GivenAQueryUsingAValueForNullableNonStringEnumWhenExecutingThenTheResultsShouldMatch() + [Fact] + public async Task GivenAQueryUsingNotNullEnumOnSubmodelWhenExecutingThenTheResultsShouldMatch() + { + await _testCosmos.GivenAnExistingItem(new TestModel { - await _testCosmos.GivenAnExistingItem(new TestModel + Id = "RECORD1", + Name = "Bob Bobertson", + Value = false, + PartitionKey = "partition-et6", + OnlyChild = new SubModel { - Id = "RECORD2", - Name = "Bob Bobertson", - Value = false, - PartitionKey = "partition-et8", - NullableEnumNotString = TestEnum.Value1 - }); + NullableEnum = null + } + }); - var (realResults, testResults) = await _testCosmos.WhenExecutingAQuery("partition-et8", q => q.Where(tm => tm.NullableEnumNotString.Value != null && tm.NullableEnumNotString == TestEnum.Value1)); + var (realResults, testResults) = await _testCosmos.WhenExecutingAQuery( + "partition-et6", + q => q.Where(tm => tm.OnlyChild.NullableEnum.Value != null) + ); - realResults.Count.Should().Be(1); - testResults.Should().BeEquivalentTo(realResults); - } + realResults.Count.Should().Be(0); + testResults.Should().BeEquivalentTo(realResults); + } - [Fact] - public async Task GivenAQueryUsingNullableNonStringEnumWhenExecutingThenTheResultsShouldMatch() + [Fact] + public async Task GivenAQueryUsingNullableEnumWhenExecutingThenTheResultsShouldMatchReversedEquality() + { + await _testCosmos.GivenAnExistingItem(new TestModel { - await _testCosmos.GivenAnExistingItem(new TestModel - { - Id = "RECORD1", - Name = "Bob Bobertson", - Value = false, - PartitionKey = "partition-et9", - NullableEnumNotString = null - }); - - var (realResults, testResults) = await _testCosmos.WhenExecutingAQuery("partition-et9", q => q.Where(tm => tm.NullableEnumNotString.Value == null)); - - realResults.Count.Should().Be(1); - testResults.Should().BeEquivalentTo(realResults); - } + Id = "RECORD1", + Name = "Bob Bobertson", + Value = false, + PartitionKey = "partition-et7", + NullableEnum = null + }); + + var (realResults, testResults) = await _testCosmos.WhenExecutingAQuery( + "partition-et7", + q => q.Where(tm => null == tm.NullableEnum.Value) + ); + + realResults.Count.Should().Be(1); + testResults.Should().BeEquivalentTo(realResults); + } - public Task InitializeAsync() + [Fact] + public async Task GivenAQueryUsingAValueForNullableNonStringEnumWhenExecutingThenTheResultsShouldMatch() + { + await _testCosmos.GivenAnExistingItem(new TestModel { - _testCosmos = new TestCosmos(); - return _testCosmos.SetupAsync("/partitionKey"); - } + Id = "RECORD2", + Name = "Bob Bobertson", + Value = false, + PartitionKey = "partition-et8", + NullableEnumNotString = TestEnum.Value1 + }); + + var (realResults, testResults) = await _testCosmos.WhenExecutingAQuery( + "partition-et8", + q => q.Where(tm => tm.NullableEnumNotString.Value != null && tm.NullableEnumNotString == TestEnum.Value1) + ); + + realResults.Count.Should().Be(1); + testResults.Should().BeEquivalentTo(realResults); + } - public async Task DisposeAsync() + [Fact] + public async Task GivenAQueryUsingNullableNonStringEnumWhenExecutingThenTheResultsShouldMatch() + { + await _testCosmos.GivenAnExistingItem(new TestModel { - await _testCosmos.CleanupAsync(); - } + Id = "RECORD1", + Name = "Bob Bobertson", + Value = false, + PartitionKey = "partition-et9", + NullableEnumNotString = null + }); + + var (realResults, testResults) = await _testCosmos.WhenExecutingAQuery( + "partition-et9", + q => q.Where(tm => tm.NullableEnumNotString.Value == null) + ); + + realResults.Count.Should().Be(1); + testResults.Should().BeEquivalentTo(realResults); + } - public void Dispose() - { - _testCosmos?.Dispose(); - } + public Task InitializeAsync() + { + _testCosmos = new TestCosmos(); + return _testCosmos.SetupAsync("/partitionKey"); + } - private bool GetTrue() => true; + public async Task DisposeAsync() + { + await _testCosmos.CleanupAsync(); + } - private bool GetBool(bool b) => b; + public void Dispose() + { + _testCosmos?.Dispose(); } } \ No newline at end of file diff --git a/CosmosTestHelpers.IntegrationTests/CosmosEquivalencyException.cs b/CosmosTestHelpers.IntegrationTests/CosmosEquivalencyException.cs deleted file mode 100644 index 949935c..0000000 --- a/CosmosTestHelpers.IntegrationTests/CosmosEquivalencyException.cs +++ /dev/null @@ -1,66 +0,0 @@ -using System.Diagnostics.CodeAnalysis; -using System.Text.Json; - -namespace CosmosTestHelpers.IntegrationTests -{ - [SuppressMessage("", "CA1032", Justification = "Unnecessary")] - public sealed class CosmosEquivalencyException : Exception - { - public Exception RealException { get; } - - public Exception TestException { get; } - - public object RealValue { get; } - - public object TestValue { get; } - - public CosmosEquivalencyException(Exception realException, Exception testException, object realValue, object testValue) - : base(FormatMessage(realException, testException, realValue, testValue)) - { - RealException = realException; - TestException = testException; - RealValue = realValue; - TestValue = testValue; - } - - private static string FormatMessage(Exception realException, Exception testException, object realValue, object testValue) - { - var message = "An error occurred executing query on"; - - if (realException != null && testException != null) - { - message += " both Test and Real CosmosDb\n"; - } - else if (realException != null) - { - message += " only Real CosmosDb\n"; - } - else if (testException != null) - { - message += " only Test CosmosDb\n"; - } - - if (realException != null) - { - message += "the real exception's message is: " + realException.Message + "\n"; - } - - if (testException != null) - { - message += "the test exception's message is: " + testException.Message + "\n"; - } - - if (realValue != null) - { - message += "the real value is: " + JsonSerializer.Serialize(realValue) + "\n"; - } - - if (testValue != null) - { - message += "the test value is: " + JsonSerializer.Serialize(testValue) + "\n"; - } - - return message; - } - } -} \ No newline at end of file diff --git a/CosmosTestHelpers.IntegrationTests/CosmosPartitionTests.cs b/CosmosTestHelpers.IntegrationTests/CosmosPartitionTests.cs index ab1dbed..94f4f0e 100644 --- a/CosmosTestHelpers.IntegrationTests/CosmosPartitionTests.cs +++ b/CosmosTestHelpers.IntegrationTests/CosmosPartitionTests.cs @@ -2,112 +2,114 @@ using FluentAssertions; using Xunit; -namespace CosmosTestHelpers.IntegrationTests +namespace CosmosTestHelpers.IntegrationTests; + +[Collection("Integration Tests")] +public sealed class CosmosPartitionTests : IAsyncLifetime, IDisposable { - [Collection("Integration Tests")] - public sealed class CosmosPartitionTests : IAsyncLifetime, IDisposable - { - private TestCosmos _testCosmos; + private TestCosmos _testCosmos; - [Fact] - public async Task GivenDataInTwoPartitionsWhenReadingAPartitionDoesNotRetrieveTheOther() + [Fact] + public async Task GivenDataInTwoPartitionsWhenReadingAPartitionDoesNotRetrieveTheOther() + { + await _testCosmos.GivenAnExistingItem(new TestModel { - await _testCosmos.GivenAnExistingItem(new TestModel - { - Id = "RECORD1", - Name = "Bob Bobertson", - PartitionKey = "partition1" - }); - - await _testCosmos.GivenAnExistingItem(new TestModel - { - Id = "RECORD2", - Name = "Bobetta Bobertson", - PartitionKey = "partition2" - }); - - var (realResults, testResults) = await _testCosmos.WhenExecutingAQuery("partition1", q => q); - - realResults.Count.Should().Be(1); - realResults.Single().Name.Should().Be("Bob Bobertson"); - testResults.Should().BeEquivalentTo(realResults); - } - - [Fact] - public async Task GivenDataInTwoPartitionsWhenReadingAPartitionDoesNotCountTheOther() + Id = "RECORD1", + Name = "Bob Bobertson", + PartitionKey = "partition1" + }); + + await _testCosmos.GivenAnExistingItem(new TestModel { - await _testCosmos.GivenAnExistingItem(new TestModel - { - Id = "RECORD1", - Name = "Bob Bobertson", - PartitionKey = "partition1" - }); - - await _testCosmos.GivenAnExistingItem(new TestModel - { - Id = "RECORD2", - Name = "Bobetta Bobertson", - PartitionKey = "partition2" - }); - - var (realResults, testResults) = await _testCosmos.WhenCountingAQuery("partition1", q => q); - - realResults.Should().Be(1); - testResults.Should().Be(1); - } - - [Fact] - public async Task GivenACrossPartitionQueryUsingEqualsWhenExecutingThenTheResultsShouldMatch() + Id = "RECORD2", + Name = "Bobetta Bobertson", + PartitionKey = "partition2" + }); + + var (realResults, testResults) = await _testCosmos.WhenExecutingAQuery("partition1", q => q); + + realResults.Count.Should().Be(1); + realResults.Single().Name.Should().Be("Bob Bobertson"); + testResults.Should().BeEquivalentTo(realResults); + } + + [Fact] + public async Task GivenDataInTwoPartitionsWhenReadingAPartitionDoesNotCountTheOther() + { + await _testCosmos.GivenAnExistingItem(new TestModel { - await _testCosmos.GivenAnExistingItem(new TestModel - { - Id = "RECORD1", - Name = "Bob Bobertson", - Value = false, - PartitionKey = "partition" - }); - - var (realResults, testResults) = await _testCosmos.WhenExecutingAQuery(null, q => q.Where(tm => tm.Name == "Bob Bobertson")); - - realResults.Count.Should().Be(1); - testResults.Should().BeEquivalentTo(realResults); - } - - [Fact] - public async Task GivenACrossPartitionCountUsingEqualsWhenExecutingThenTheResultsShouldMatch() + Id = "RECORD1", + Name = "Bob Bobertson", + PartitionKey = "partition1" + }); + + await _testCosmos.GivenAnExistingItem(new TestModel { - await _testCosmos.GivenAnExistingItem(new TestModel - { - Id = "RECORD1", - Name = "Bob Bobertson", - Value = false, - PartitionKey = "partition" - }); + Id = "RECORD2", + Name = "Bobetta Bobertson", + PartitionKey = "partition2" + }); - var (realResults, testResults) = await _testCosmos.WhenCountingAQuery(null, q => q.Where(tm => tm.Name == "Bob Bobertson")); + var (realResults, testResults) = await _testCosmos.WhenCountingAQuery("partition1", q => q); - realResults.Should().Be(1); - testResults.Should().Be(1); - } + realResults.Should().Be(1); + testResults.Should().Be(1); + } - public Task InitializeAsync() + [Fact] + public async Task GivenACrossPartitionQueryUsingEqualsWhenExecutingThenTheResultsShouldMatch() + { + await _testCosmos.GivenAnExistingItem(new TestModel { - _testCosmos = new TestCosmos(); - return _testCosmos.SetupAsync("/partitionKey"); - } + Id = "RECORD1", + Name = "Bob Bobertson", + Value = false, + PartitionKey = "partition" + }); + + var (realResults, testResults) = await _testCosmos.WhenExecutingAQuery( + null, + q => q.Where(tm => tm.Name == "Bob Bobertson") + ); + + realResults.Count.Should().Be(1); + testResults.Should().BeEquivalentTo(realResults); + } - public async Task DisposeAsync() + [Fact] + public async Task GivenACrossPartitionCountUsingEqualsWhenExecutingThenTheResultsShouldMatch() + { + await _testCosmos.GivenAnExistingItem(new TestModel { - await _testCosmos.CleanupAsync(); - } + Id = "RECORD1", + Name = "Bob Bobertson", + Value = false, + PartitionKey = "partition" + }); + + var (realResults, testResults) = + await _testCosmos.WhenCountingAQuery( + null, + q => q.Where(tm => tm.Name == "Bob Bobertson") + ); + + realResults.Should().Be(1); + testResults.Should().Be(1); + } - public void Dispose() - { - _testCosmos?.Dispose(); - } + public Task InitializeAsync() + { + _testCosmos = new TestCosmos(); + return _testCosmos.SetupAsync("/partitionKey"); + } - private bool GetTrue() => true; + public async Task DisposeAsync() + { + await _testCosmos.CleanupAsync(); + } - private bool GetBool(bool b) => b; + public void Dispose() + { + _testCosmos?.Dispose(); } } \ No newline at end of file diff --git a/CosmosTestHelpers.IntegrationTests/CosmosQueryEquivalencyTests.cs b/CosmosTestHelpers.IntegrationTests/CosmosQueryEquivalencyTests.cs index d91aa28..23f4316 100644 --- a/CosmosTestHelpers.IntegrationTests/CosmosQueryEquivalencyTests.cs +++ b/CosmosTestHelpers.IntegrationTests/CosmosQueryEquivalencyTests.cs @@ -1,408 +1,461 @@ -using System.Diagnostics.CodeAnalysis; -using CosmosTestHelpers.IntegrationTests.TestModels; +using CosmosTestHelpers.IntegrationTests.TestModels; using FluentAssertions; using Microsoft.Azure.Cosmos.Linq; using Xunit; -namespace CosmosTestHelpers.IntegrationTests +namespace CosmosTestHelpers.IntegrationTests; + +[Collection("Integration Tests")] +public sealed class CosmosQueryEquivalencyTests : IAsyncLifetime, IDisposable { - [Collection("Integration Tests")] - public sealed class CosmosQueryEquivalencyTests : IAsyncLifetime, IDisposable - { - private TestCosmos _testCosmos; + private TestCosmos _testCosmos; - [Fact] - public async Task GivenAQueryUsingEqualsWhenExecutingThenTheResultsShouldMatch() - { - await _testCosmos.GivenAnExistingItem(new TestModel - { - Id = "RECORD1", - Name = "Bob Bobertson", - Value = false, - PartitionKey = "partition" - }); - - var (realResults, testResults) = await _testCosmos.WhenExecutingAQuery("partition", q => q.Where(tm => tm.Name == "Bob Bobertson")); - - realResults.Count.Should().Be(1); - testResults.Should().BeEquivalentTo(realResults); - } - - [Fact] - public async Task IsNullWorks() - { - await _testCosmos.GivenAnExistingItem(new TestModel - { - Id = "RECORD1", - Name = null, - Value = false, - PartitionKey = "partition" - }); - - var (realResults, testResults) = await _testCosmos.WhenExecutingAQuery("partition", q => q.Where(tm => tm.Name.IsNull())); - - realResults.Count.Should().Be(1); - testResults.Should().BeEquivalentTo(realResults); - } - - [Fact] - public async Task IsDefinedWorksOnNull() + [Fact] + public async Task GivenAQueryUsingEqualsWhenExecutingThenTheResultsShouldMatch() + { + await _testCosmos.GivenAnExistingItem(new TestModel { - await _testCosmos.GivenAnExistingItem(new TestModel - { - Id = "RECORD1", - Name = null, - Value = false, - PartitionKey = "partition" - }); - - var (realResults, testResults) = await _testCosmos.WhenExecutingAQuery("partition", q => q.Where(tm => tm.Name.IsDefined())); - - realResults.Count.Should().Be(1); - testResults.Should().BeEquivalentTo(realResults); - } + Id = "RECORD1", + Name = "Bob Bobertson", + Value = false, + PartitionKey = "partition" + }); + + var (realResults, testResults) = await _testCosmos.WhenExecutingAQuery( + "partition", + q => q.Where(tm => tm.Name == "Bob Bobertson") + ); + + realResults.Count.Should().Be(1); + testResults.Should().BeEquivalentTo(realResults); + } - [Fact] - public async Task GivenACountUsingEqualsWhenExecutingThenTheResultsShouldMatch() + [Fact] + public async Task IsNullWorks() + { + await _testCosmos.GivenAnExistingItem(new TestModel { - await _testCosmos.GivenAnExistingItem(new TestModel - { - Id = "RECORD1", - Name = "Bob Bobertson", - Value = false, - PartitionKey = "partition" - }); - - await _testCosmos.GivenAnExistingItem(new TestModel - { - Id = "RECORD2", - Name = "Bobetta Bobertson", - Value = false, - PartitionKey = "partition" - }); - - var (realResults, testResults) = await _testCosmos.WhenCountingAQuery("partition", q => q.Where(tm => tm.Name == "Bob Bobertson")); - - realResults.Should().Be(1); - testResults.Should().Be(1); - } + Id = "RECORD1", + Name = null, + Value = false, + PartitionKey = "partition" + }); + + var (realResults, testResults) = await _testCosmos.WhenExecutingAQuery( + "partition", + q => q.Where(tm => tm.Name.IsNull()) + ); + + realResults.Count.Should().Be(1); + testResults.Should().BeEquivalentTo(realResults); + } - [Fact] - public async Task GivenAQueryForAllItemsInAPartitionUsingEqualsWhenExecutingThenTheResultsShouldMatch() + [Fact] + public async Task IsDefinedWorksOnNull() + { + await _testCosmos.GivenAnExistingItem(new TestModel { - await _testCosmos.GivenAnExistingItem(new TestModel - { - Id = "RECORD1", - Name = "Bob Bobertson", - Value = false, - PartitionKey = "partition" - }); - - var (realResults, testResults) = await _testCosmos.WhenExecutingAQuery("partition"); - - realResults.Count.Should().Be(1); - testResults.Should().BeEquivalentTo(realResults); - } + Id = "RECORD1", + Name = null, + Value = false, + PartitionKey = "partition" + }); + + var (realResults, testResults) = await _testCosmos.WhenExecutingAQuery( + "partition", + q => q.Where(tm => tm.Name.IsDefined()) + ); + + realResults.Count.Should().Be(1); + testResults.Should().BeEquivalentTo(realResults); + } - [Fact] - public async Task GivenAQueryUsingContainsOnAnEnumWithAToStringWhenExecutingThenTheResultsShouldMatch() + [Fact] + public async Task GivenACountUsingEqualsWhenExecutingThenTheResultsShouldMatch() + { + await _testCosmos.GivenAnExistingItem(new TestModel { - await _testCosmos.GivenAnExistingItem(new TestModel - { - Id = "RECORD1", - Name = "Bob Bobertson", - EnumValue = TestEnum.Value2, - PartitionKey = "partition" - }); + Id = "RECORD1", + Name = "Bob Bobertson", + Value = false, + PartitionKey = "partition" + }); - var (realResults, testResults) = await _testCosmos.WhenExecutingAQuery("partition", q => q.Where(tm => new[] { TestEnum.Value2.ToString() }.Contains(tm.EnumValue.ToString()))); - - realResults.Count.Should().Be(1); - testResults.Should().BeEquivalentTo(realResults); - } - - [Fact] - public async Task GivenAQueryUsingAValueFromAMethodWhenExecutingThenTheResultsShouldMatch() + await _testCosmos.GivenAnExistingItem(new TestModel { - await _testCosmos.GivenAnExistingItem(new TestModel - { - Id = "RECORD1", - Name = "Bob Bobertson", - Value = true, - PartitionKey = "partition" - }); - - var (realResults, testResults) = await _testCosmos.WhenExecutingAQuery("partition", q => q.Where(tm => tm.Value == GetTrue())); - - realResults.Count.Should().Be(1); - testResults.Should().BeEquivalentTo(realResults); - } + Id = "RECORD2", + Name = "Bobetta Bobertson", + Value = false, + PartitionKey = "partition" + }); + + var (realResults, testResults) = await _testCosmos.WhenCountingAQuery( + "partition", + q => q.Where(tm => tm.Name == "Bob Bobertson") + ); + + realResults.Should().Be(1); + testResults.Should().Be(1); + } - [Fact] - public async Task GivenAQueryUsingAValueFromAMethodWithArgsWhenExecutingThenTheResultsShouldMatch() + [Fact] + public async Task GivenAQueryForAllItemsInAPartitionUsingEqualsWhenExecutingThenTheResultsShouldMatch() + { + await _testCosmos.GivenAnExistingItem(new TestModel { - await _testCosmos.GivenAnExistingItem(new TestModel - { - Id = "RECORD1", - Name = "Bob Bobertson", - Value = true, - PartitionKey = "partition" - }); + Id = "RECORD1", + Name = "Bob Bobertson", + Value = false, + PartitionKey = "partition" + }); - var (realResults, testResults) = await _testCosmos.WhenExecutingAQuery("partition", q => q.Where(tm => tm.Value == GetBool(true))); + var (realResults, testResults) = await _testCosmos.WhenExecutingAQuery("partition"); - realResults.Count.Should().Be(1); - testResults.Should().BeEquivalentTo(realResults); - } + realResults.Count.Should().Be(1); + testResults.Should().BeEquivalentTo(realResults); + } - [Fact] - public async Task GivenAQueryUsingAValueFromAMethodAgainstModelWhenExecutingThenTheResultsShouldMatch() + [Fact] + public async Task GivenAQueryUsingContainsOnAnEnumWithAToStringWhenExecutingThenTheResultsShouldMatch() + { + await _testCosmos.GivenAnExistingItem(new TestModel { - await _testCosmos.GivenAnExistingItem(new TestModel - { - Id = "RECORD1", - Name = "Bob Bobertson", - Value = true, - PartitionKey = "partition" - }); - - Func action = () => _testCosmos.WhenExecutingAQuery("partition", q => q.Where(tm => tm.GetBoolValue() == true)); - - var exceptionAssertions = await action.Should().ThrowAsync(); - exceptionAssertions.Which.RealException.Should().NotBeNull(); - exceptionAssertions.Which.TestException.Should().NotBeNull(); - } + Id = "RECORD1", + Name = "Bob Bobertson", + EnumValue = TestEnum.Value2, + PartitionKey = "partition" + }); + + var (realResults, testResults) = await _testCosmos.WhenExecutingAQuery( + "partition", + q => q.Where(tm => new[] { TestEnum.Value2.ToString() }.Contains(tm.EnumValue.ToString())) + ); + + realResults.Count.Should().Be(1); + testResults.Should().BeEquivalentTo(realResults); + } - [Fact] - public async Task GivenAQueryUsingProjectionWhenExecutingThenTheResultsShouldMatch() + [Fact] + public async Task GivenAQueryUsingAValueFromAMethodWhenExecutingThenTheResultsShouldMatch() + { + await _testCosmos.GivenAnExistingItem(new TestModel { - await _testCosmos.GivenAnExistingItem(new TestModel - { - Id = "RECORD1", - Name = "Bob Bobertson", - Value = false, - PartitionKey = "partition" - }); - - var (realResults, testResults) = await _testCosmos.WhenExecutingAQuery("partition", q => q.Select(tm => new TestModel { Name = "Projected" })); - - realResults.Count.Should().Be(1); - testResults.Should().BeEquivalentTo(realResults); - } + Id = "RECORD1", + Name = "Bob Bobertson", + Value = true, + PartitionKey = "partition" + }); + + var (realResults, testResults) = await _testCosmos.WhenExecutingAQuery( + "partition", + q => q.Where(tm => tm.Value == GetTrue()) + ); + + realResults.Count.Should().Be(1); + testResults.Should().BeEquivalentTo(realResults); + } - [Fact] - public async Task GivenAQueryUsingNotEqualsWhenExecutingThenTheResultsShouldMatch() + [Fact] + public async Task GivenAQueryUsingAValueFromAMethodWithArgsWhenExecutingThenTheResultsShouldMatch() + { + await _testCosmos.GivenAnExistingItem(new TestModel { - await _testCosmos.GivenAnExistingItem(new TestModel - { - Id = "RECORD1", - Name = "Bob Bobertson", - Value = false, - PartitionKey = "partition" - }); - - var (realResults, testResults) = await _testCosmos.WhenExecutingAQuery("partition", q => q.Where(tm => tm.Name != "Bobbeta Bobertson")); - - realResults.Count.Should().Be(1); - testResults.Should().BeEquivalentTo(realResults); - } + Id = "RECORD1", + Name = "Bob Bobertson", + Value = true, + PartitionKey = "partition" + }); + + var (realResults, testResults) = await _testCosmos.WhenExecutingAQuery( + "partition", + q => q.Where(tm => tm.Value == GetBool(true)) + ); + + realResults.Count.Should().Be(1); + testResults.Should().BeEquivalentTo(realResults); + } - [Fact] - public async Task GivenAQueryUsingNotXorWhenExecutingThenTheResultsShouldMatch() + [Fact] + public async Task GivenAQueryUsingAValueFromAMethodAgainstModelWhenExecutingThenTheResultsShouldMatch() + { + await _testCosmos.GivenAnExistingItem(new TestModel { - await _testCosmos.GivenAnExistingItem(new TestModel - { - Id = "RECORD1", - Name = "Bob Bobertson", - Value = false, - PartitionKey = "partition" - }); - - var (realResults, testResults) = await _testCosmos.WhenExecutingAQuery("partition", q => q.Where(tm => !(tm.Value ^ false))); - - realResults.Count.Should().Be(0); - testResults.Should().BeEquivalentTo(realResults); - } + Id = "RECORD1", + Name = "Bob Bobertson", + Value = true, + PartitionKey = "partition" + }); + + Func action = () => _testCosmos.WhenExecutingAQuery( + "partition", + q => q.Where(tm => tm.GetBoolValue() == true) + ); + + var exceptionAssertions = await action.Should().ThrowAsync(); + exceptionAssertions.Which.RealException.Should().NotBeNull(); + exceptionAssertions.Which.TestException.Should().NotBeNull(); + } - [Fact] - public async Task GivenAQueryUsingXorWhenExecutingThenTheResultsShouldMatch() + [Fact] + public async Task GivenAQueryUsingProjectionWhenExecutingThenTheResultsShouldMatch() + { + await _testCosmos.GivenAnExistingItem(new TestModel { - await _testCosmos.GivenAnExistingItem(new TestModel - { - Id = "RECORD1", - Name = "Bob Bobertson", - Value = false, - PartitionKey = "partition" - }); - - var (realResults, testResults) = await _testCosmos.WhenExecutingAQuery("partition", q => q.Where(tm => tm.Value ^ true)); - - realResults.Count.Should().Be(0); - testResults.Should().BeEquivalentTo(realResults); - } + Id = "RECORD1", + Name = "Bob Bobertson", + Value = false, + PartitionKey = "partition" + }); + + var (realResults, testResults) = await _testCosmos.WhenExecutingAQuery( + "partition", + q => q.Select(tm => new TestModel { Name = "Projected" }) + ); + + realResults.Count.Should().Be(1); + testResults.Should().BeEquivalentTo(realResults); + } - [Fact] - [SuppressMessage("", "CA1304", Justification = "Culture Not Supported")] - public async Task GivenAQueryUsingToUpperWhenExecutingThenTheResultsShouldMatch() + [Fact] + public async Task GivenAQueryUsingNotEqualsWhenExecutingThenTheResultsShouldMatch() + { + await _testCosmos.GivenAnExistingItem(new TestModel { - await _testCosmos.GivenAnExistingItem(new TestModel - { - Id = "RECORD1", - Name = "Bob Bobertson", - Value = false, - PartitionKey = "partition" - }); - - var (realResults, testResults) = await _testCosmos.WhenExecutingAQuery("partition", q => q.Where(tm => tm.Name.ToUpper() == "BOB BOBERTSON")); - - realResults.Count.Should().Be(1); - testResults.Should().BeEquivalentTo(realResults); - } + Id = "RECORD1", + Name = "Bob Bobertson", + Value = false, + PartitionKey = "partition" + }); + + var (realResults, testResults) = await _testCosmos.WhenExecutingAQuery( + "partition", + q => q.Where(tm => tm.Name != "Bobbeta Bobertson") + ); + + realResults.Count.Should().Be(1); + testResults.Should().BeEquivalentTo(realResults); + } - [Fact] - [SuppressMessage("", "CA1304", Justification = "Culture Not Supported")] - public async Task GivenAQueryUsingToLowerWhenExecutingThenTheResultsShouldMatch() + [Fact] + public async Task GivenAQueryUsingNotXorWhenExecutingThenTheResultsShouldMatch() + { + await _testCosmos.GivenAnExistingItem(new TestModel { - await _testCosmos.GivenAnExistingItem(new TestModel - { - Id = "RECORD1", - Name = "Bob Bobertson", - Value = false, - PartitionKey = "partition" - }); - - var (realResults, testResults) = await _testCosmos.WhenExecutingAQuery("partition", q => q.Where(tm => tm.Name.ToLower() == "bob bobertson")); - - realResults.Count.Should().Be(1); - testResults.Should().BeEquivalentTo(realResults); - } + Id = "RECORD1", + Name = "Bob Bobertson", + Value = false, + PartitionKey = "partition" + }); + + var (realResults, testResults) = await _testCosmos.WhenExecutingAQuery( + "partition", + q => q.Where(tm => !(tm.Value ^ false)) + ); + + realResults.Count.Should().Be(0); + testResults.Should().BeEquivalentTo(realResults); + } - [Fact] - [SuppressMessage("", "CA1304", Justification = "Culture Not Supported")] - public async Task GivenAQueryUsingAnyInASubQueryWhenExecutingThenTheResultsShouldMatch() - { - await _testCosmos.GivenAnExistingItem(new TestModel - { - Id = "RECORD1", - Children = new[] - { - new SubModel { Value = "bob bobertson" } - }, - PartitionKey = "partition" - }); - - var (realResults, testResults) = await _testCosmos.WhenExecutingAQuery("partition", q => q.Where(tm => tm.Children.Any(c => c.Value == "bob bobertson"))); - - realResults.Count.Should().Be(1); - testResults.Should().BeEquivalentTo(realResults); - } - - [Fact] - public async Task GivenAQueryUsingToUpperInvariantWhenExecutingThenBothShouldError() - { - await _testCosmos.GivenAnExistingItem(new TestModel - { - Id = "RECORD1", - Name = "Bob Bobertson", - Value = false, - PartitionKey = "partition" - }); - - Func action = () => _testCosmos.WhenExecutingAQuery("partition", q => q.Where(tm => tm.Name.ToUpperInvariant() == "BOB BOBERTSON")); - - var exceptionAssertions = await action.Should().ThrowAsync(); - exceptionAssertions.Which.RealException.Should().NotBeNull(); - exceptionAssertions.Which.TestException.Should().NotBeNull(); - } - - [Fact] - [SuppressMessage("", "CA1308", Justification = "Specifically testing")] - public async Task GivenAQueryUsingToLowerInvariantWhenExecutingThenBothShouldError() - { - await _testCosmos.GivenAnExistingItem(new TestModel - { - Id = "RECORD1", - Name = "Bob Bobertson", - Value = false, - PartitionKey = "partition" - }); - - Func action = () => _testCosmos.WhenExecutingAQuery("partition", q => q.Where(tm => tm.Name.ToLowerInvariant() == "bob bobertson")); - - var exceptionAssertions = await action.Should().ThrowAsync(); - exceptionAssertions.Which.RealException.Should().NotBeNull(); - exceptionAssertions.Which.TestException.Should().NotBeNull(); - } - - [Fact] - [SuppressMessage("", "CA1308", Justification = "Specifically testing")] - public async Task GivenAQueryUsingToAnyWhenExecutingThenBothShouldWork() + [Fact] + public async Task GivenAQueryUsingXorWhenExecutingThenTheResultsShouldMatch() + { + await _testCosmos.GivenAnExistingItem(new TestModel { - await _testCosmos.GivenAnExistingItem(new TestModel - { - Id = "RECORD1", - Name = "Bob Bobertson", - Value = false, - PartitionKey = "partition" - }); - - var (realResults, testResults) = await _testCosmos.WhenExecutingAQuery("partition", q => q.Where(tm => tm.Name == "bob bobertson")); - - testResults.Any().Should().Be(realResults.Any()); - } + Id = "RECORD1", + Name = "Bob Bobertson", + Value = false, + PartitionKey = "partition" + }); + + var (realResults, testResults) = await _testCosmos.WhenExecutingAQuery( + "partition", + q => q.Where(tm => tm.Value ^ true) + ); + + realResults.Count.Should().Be(0); + testResults.Should().BeEquivalentTo(realResults); + } - [Fact] - public async Task GivenAQueryUsingFirstOrDefaultWhenExecutingThenBothShouldWork() + [Fact] + public async Task GivenAQueryUsingToUpperWhenExecutingThenTheResultsShouldMatch() + { + await _testCosmos.GivenAnExistingItem(new TestModel { - await _testCosmos.GivenAnExistingItem(new TestModel - { - Id = "RECORD1", - Name = "Bob Bobertson", - Value = false, - PartitionKey = "partition" - }); - - var (realResults, testResults) = await _testCosmos.WhenExecutingAQuery("partition", q => q.Where(tm => tm.Name == "Bob Bobertson")); + Id = "RECORD1", + Name = "Bob Bobertson", + Value = false, + PartitionKey = "partition" + }); + + var (realResults, testResults) = await _testCosmos.WhenExecutingAQuery( + "partition", + q => q.Where(tm => tm.Name.ToUpper() == "BOB BOBERTSON") + ); + + realResults.Count.Should().Be(1); + testResults.Should().BeEquivalentTo(realResults); + } - realResults.FirstOrDefault().Should().NotBeNull(); - testResults.FirstOrDefault().Should().BeEquivalentTo(realResults.FirstOrDefault()); - } + [Fact] + public async Task GivenAQueryUsingToLowerWhenExecutingThenTheResultsShouldMatch() + { + await _testCosmos.GivenAnExistingItem(new TestModel + { + Id = "RECORD1", + Name = "Bob Bobertson", + Value = false, + PartitionKey = "partition" + }); + + var (realResults, testResults) = await _testCosmos.WhenExecutingAQuery( + "partition", + q => q.Where(tm => tm.Name.ToLower() == "bob bobertson") + ); + + realResults.Count.Should().Be(1); + testResults.Should().BeEquivalentTo(realResults); + } - [Fact] - public async Task GivenAQueryUsingSingleOrDefaultWhenExecutingThenBothShouldWork() + [Fact] + public async Task GivenAQueryUsingAnyInASubQueryWhenExecutingThenTheResultsShouldMatch() + { + await _testCosmos.GivenAnExistingItem(new TestModel { - await _testCosmos.GivenAnExistingItem(new TestModel + Id = "RECORD1", + Children = new[] { - Id = "RECORD1", - Name = "Bob Bobertson", - Value = false, - PartitionKey = "partition" - }); + new SubModel { Value = "bob bobertson" } + }, + PartitionKey = "partition" + }); + + var (realResults, testResults) = await _testCosmos.WhenExecutingAQuery( + "partition", + q => q.Where(tm => tm.Children.Any(c => c.Value == "bob bobertson")) + ); + + realResults.Count.Should().Be(1); + testResults.Should().BeEquivalentTo(realResults); + } - var (realResults, testResults) = await _testCosmos.WhenExecutingAQuery("partition", q => q.Where(tm => tm.Name == "Bob Bobertson")); + [Fact] + public async Task GivenAQueryUsingToUpperInvariantWhenExecutingThenBothShouldError() + { + await _testCosmos.GivenAnExistingItem(new TestModel + { + Id = "RECORD1", + Name = "Bob Bobertson", + Value = false, + PartitionKey = "partition" + }); + + Func action = () => _testCosmos.WhenExecutingAQuery( + "partition", + q => q.Where(tm => tm.Name.ToUpperInvariant() == "BOB BOBERTSON") + ); + + var exceptionAssertions = await action.Should().ThrowAsync(); + exceptionAssertions.Which.RealException.Should().NotBeNull(); + exceptionAssertions.Which.TestException.Should().NotBeNull(); + } - realResults.SingleOrDefault().Should().NotBeNull(); - testResults.SingleOrDefault().Should().BeEquivalentTo(realResults.SingleOrDefault()); - } + [Fact] + public async Task GivenAQueryUsingToLowerInvariantWhenExecutingThenBothShouldError() + { + await _testCosmos.GivenAnExistingItem(new TestModel + { + Id = "RECORD1", + Name = "Bob Bobertson", + Value = false, + PartitionKey = "partition" + }); + + Func action = () => _testCosmos.WhenExecutingAQuery( + "partition", + q => q.Where(tm => tm.Name.ToLowerInvariant() == "bob bobertson") + ); + + var exceptionAssertions = await action.Should().ThrowAsync(); + exceptionAssertions.Which.RealException.Should().NotBeNull(); + exceptionAssertions.Which.TestException.Should().NotBeNull(); + } - public Task InitializeAsync() + [Fact] + public async Task GivenAQueryUsingToAnyWhenExecutingThenBothShouldWork() + { + await _testCosmos.GivenAnExistingItem(new TestModel { - _testCosmos = new TestCosmos(); - return _testCosmos.SetupAsync("/partitionKey"); - } + Id = "RECORD1", + Name = "Bob Bobertson", + Value = false, + PartitionKey = "partition" + }); + + var (realResults, testResults) = await _testCosmos.WhenExecutingAQuery( + "partition", + q => q.Where(tm => tm.Name == "bob bobertson") + ); + + testResults.Any().Should().Be(realResults.Any()); + } - public async Task DisposeAsync() + [Fact] + public async Task GivenAQueryUsingFirstOrDefaultWhenExecutingThenBothShouldWork() + { + await _testCosmos.GivenAnExistingItem(new TestModel { - await _testCosmos.CleanupAsync(); - } + Id = "RECORD1", + Name = "Bob Bobertson", + Value = false, + PartitionKey = "partition" + }); + + var (realResults, testResults) = await _testCosmos.WhenExecutingAQuery( + "partition", + q => q.Where(tm => tm.Name == "Bob Bobertson") + ); + + realResults.FirstOrDefault().Should().NotBeNull(); + testResults.FirstOrDefault().Should().BeEquivalentTo(realResults.FirstOrDefault()); + } - public void Dispose() + [Fact] + public async Task GivenAQueryUsingSingleOrDefaultWhenExecutingThenBothShouldWork() + { + await _testCosmos.GivenAnExistingItem(new TestModel { - _testCosmos?.Dispose(); - } + Id = "RECORD1", + Name = "Bob Bobertson", + Value = false, + PartitionKey = "partition" + }); + + var (realResults, testResults) = await _testCosmos.WhenExecutingAQuery( + "partition", + q => q.Where(tm => tm.Name == "Bob Bobertson") + ); + + realResults.SingleOrDefault().Should().NotBeNull(); + testResults.SingleOrDefault().Should().BeEquivalentTo(realResults.SingleOrDefault()); + } - private bool GetTrue() => true; + public Task InitializeAsync() + { + _testCosmos = new TestCosmos(); + return _testCosmos.SetupAsync("/partitionKey"); + } - private bool GetBool(bool b) => b; + public async Task DisposeAsync() + { + await _testCosmos.CleanupAsync(); + } + + public void Dispose() + { + _testCosmos?.Dispose(); } + + private bool GetTrue() => true; + + private bool GetBool(bool b) => b; } \ No newline at end of file diff --git a/CosmosTestHelpers.IntegrationTests/CosmosQueryEquivalencyTestsLegacyQueries.cs b/CosmosTestHelpers.IntegrationTests/CosmosQueryEquivalencyTestsLegacyQueries.cs index 141fd43..3bfff07 100644 --- a/CosmosTestHelpers.IntegrationTests/CosmosQueryEquivalencyTestsLegacyQueries.cs +++ b/CosmosTestHelpers.IntegrationTests/CosmosQueryEquivalencyTestsLegacyQueries.cs @@ -1,349 +1,377 @@ -using System.Diagnostics.CodeAnalysis; -using CosmosTestHelpers.IntegrationTests.TestModels; +using CosmosTestHelpers.IntegrationTests.TestModels; using FluentAssertions; using Xunit; -namespace CosmosTestHelpers.IntegrationTests +namespace CosmosTestHelpers.IntegrationTests; + +[Obsolete("Kept to test legacy querying until the CreateItemLinqQueryable method is removed")] +[Collection("Integration Tests")] +public sealed class CosmosQueryEquivalencyTestsLegacyQueries : IAsyncLifetime, IDisposable { - [Obsolete("Kept to test legacy querying until the CreateItemLinqQueryable method is removed")] - [Collection("Integration Tests")] - public sealed class CosmosQueryEquivalencyTestsLegacyQueries : IAsyncLifetime, IDisposable - { - private TestCosmos _testCosmos; + private TestCosmos _testCosmos; - [Fact] - public async Task GivenAQueryUsingEqualsWhenExecutingThenTheResultsShouldMatch() + [Fact] + public async Task GivenAQueryUsingEqualsWhenExecutingThenTheResultsShouldMatch() + { + await _testCosmos.GivenAnExistingItem(new TestModel { - await _testCosmos.GivenAnExistingItem(new TestModel - { - Id = "RECORD1", - Name = "Bob Bobertson", - Value = false - }); + Id = "RECORD1", + Name = "Bob Bobertson", + Value = false + }); - var (realResults, testResults) = _testCosmos.WhenExecutingAQuery(q => q.Where(tm => tm.Name == "Bob Bobertson")); + var (realResults, testResults) = _testCosmos.WhenExecutingAQuery( + q => q.Where(tm => tm.Name == "Bob Bobertson") + ); - realResults.Count().Should().Be(1); - realResults.Should().BeEquivalentTo(testResults); - } + realResults.Count().Should().Be(1); + realResults.Should().BeEquivalentTo(testResults); + } - [Fact] - public async Task GivenAQueryUsingContainsOnAnEnumWithAToStringWhenExecutingThenTheResultsShouldMatch() + [Fact] + public async Task GivenAQueryUsingContainsOnAnEnumWithAToStringWhenExecutingThenTheResultsShouldMatch() + { + await _testCosmos.GivenAnExistingItem(new TestModel { - await _testCosmos.GivenAnExistingItem(new TestModel - { - Id = "RECORD1", - Name = "Bob Bobertson", - EnumValue = TestEnum.Value2 - }); + Id = "RECORD1", + Name = "Bob Bobertson", + EnumValue = TestEnum.Value2 + }); - var (realResults, testResults) = _testCosmos.WhenExecutingAQuery(q => q.Where(tm => new[] { TestEnum.Value2.ToString() }.Contains(tm.EnumValue.ToString()))); + var (realResults, testResults) = _testCosmos.WhenExecutingAQuery( + q => q.Where(tm => new[] { TestEnum.Value2.ToString() }.Contains(tm.EnumValue.ToString())) + ); - realResults.Count().Should().Be(1); - realResults.Should().BeEquivalentTo(testResults); - } + realResults.Count().Should().Be(1); + realResults.Should().BeEquivalentTo(testResults); + } - [Fact] - public async Task GivenAQueryUsingAValueFromAMethodWhenExecutingThenTheResultsShouldMatch() + [Fact] + public async Task GivenAQueryUsingAValueFromAMethodWhenExecutingThenTheResultsShouldMatch() + { + await _testCosmos.GivenAnExistingItem(new TestModel { - await _testCosmos.GivenAnExistingItem(new TestModel - { - Id = "RECORD1", - Name = "Bob Bobertson", - Value = true - }); + Id = "RECORD1", + Name = "Bob Bobertson", + Value = true + }); - var (realResults, testResults) = _testCosmos.WhenExecutingAQuery(q => q.Where(tm => tm.Value == GetTrue())); + var (realResults, testResults) = _testCosmos.WhenExecutingAQuery( + q => q.Where(tm => tm.Value == GetTrue()) + ); - realResults.Count().Should().Be(1); - realResults.Should().BeEquivalentTo(testResults); - } + realResults.Count().Should().Be(1); + realResults.Should().BeEquivalentTo(testResults); + } - [Fact] - public async Task GivenAQueryUsingAValueFromAMethodAgainstModelWhenExecutingThenTheResultsShouldMatch() - { - await _testCosmos.GivenAnExistingItem(new TestModel - { - Id = "RECORD1", - Name = "Bob Bobertson", - Value = true, - PartitionKey = "partition" - }); - - var (realResults, testResults) = _testCosmos.WhenExecutingAQuery(q => q.Where(tm => tm.GetBoolValue() == true)); - - var realResultsAction = new Func(() => realResults.SingleOrDefault()); - var testResultsAction = new Func(() => testResults.SingleOrDefault()); - - realResultsAction.Should().Throw(); - testResultsAction.Should().Throw(); - } - - [Fact] - public async Task GivenAQueryUsingAValueFromAMethodWithArgsWhenExecutingThenTheResultsShouldMatch() + [Fact] + public async Task GivenAQueryUsingAValueFromAMethodAgainstModelWhenExecutingThenTheResultsShouldMatch() + { + await _testCosmos.GivenAnExistingItem(new TestModel { - await _testCosmos.GivenAnExistingItem(new TestModel - { - Id = "RECORD1", - Name = "Bob Bobertson", - Value = true - }); + Id = "RECORD1", + Name = "Bob Bobertson", + Value = true, + PartitionKey = "partition" + }); + + var (realResults, testResults) = _testCosmos.WhenExecutingAQuery( + q => q.Where(tm => tm.GetBoolValue() == true) + ); - var (realResults, testResults) = _testCosmos.WhenExecutingAQuery(q => q.Where(tm => tm.Value == GetBool(true))); + var realResultsAction = new Func(() => realResults.SingleOrDefault()); + var testResultsAction = new Func(() => testResults.SingleOrDefault()); - realResults.Count().Should().Be(1); - realResults.Should().BeEquivalentTo(testResults); - } + realResultsAction.Should().Throw(); + testResultsAction.Should().Throw(); + } - [Fact] - public async Task GivenAQueryUsingProjectionWhenExecutingThenTheResultsShouldMatch() + [Fact] + public async Task GivenAQueryUsingAValueFromAMethodWithArgsWhenExecutingThenTheResultsShouldMatch() + { + await _testCosmos.GivenAnExistingItem(new TestModel { - await _testCosmos.GivenAnExistingItem(new TestModel - { - Id = "RECORD1", - Name = "Bob Bobertson", - Value = false - }); + Id = "RECORD1", + Name = "Bob Bobertson", + Value = true + }); - var (realResults, testResults) = _testCosmos.WhenExecutingAQuery(q => q.Select(tm => new TestModel { Name = "Projected" })); + var (realResults, testResults) = _testCosmos.WhenExecutingAQuery( + q => q.Where(tm => tm.Value == GetBool(true)) + ); - realResults.Count().Should().Be(1); - realResults.Should().BeEquivalentTo(testResults); - } + realResults.Count().Should().Be(1); + realResults.Should().BeEquivalentTo(testResults); + } - [Fact] - public async Task GivenAQueryUsingNotEqualsWhenExecutingThenTheResultsShouldMatch() + [Fact] + public async Task GivenAQueryUsingProjectionWhenExecutingThenTheResultsShouldMatch() + { + await _testCosmos.GivenAnExistingItem(new TestModel { - await _testCosmos.GivenAnExistingItem(new TestModel - { - Id = "RECORD1", - Name = "Bob Bobertson", - Value = false - }); + Id = "RECORD1", + Name = "Bob Bobertson", + Value = false + }); - var (realResults, testResults) = _testCosmos.WhenExecutingAQuery(q => q.Where(tm => tm.Name != "Bobbeta Bobertson")); + var (realResults, testResults) = _testCosmos.WhenExecutingAQuery( + q => q.Select(tm => new TestModel { Name = "Projected" }) + ); - realResults.Count().Should().Be(1); - realResults.Should().BeEquivalentTo(testResults); - } + realResults.Count().Should().Be(1); + realResults.Should().BeEquivalentTo(testResults); + } - [Fact] - public async Task GivenAQueryUsingNotXorWhenExecutingThenTheResultsShouldMatch() + [Fact] + public async Task GivenAQueryUsingNotEqualsWhenExecutingThenTheResultsShouldMatch() + { + await _testCosmos.GivenAnExistingItem(new TestModel { - await _testCosmos.GivenAnExistingItem(new TestModel - { - Id = "RECORD1", - Name = "Bob Bobertson", - Value = false - }); + Id = "RECORD1", + Name = "Bob Bobertson", + Value = false + }); - var (realResults, testResults) = _testCosmos.WhenExecutingAQuery(q => q.Where(tm => !(tm.Value ^ false))); + var (realResults, testResults) = _testCosmos.WhenExecutingAQuery( + q => q.Where(tm => tm.Name != "Bobbeta Bobertson") + ); - realResults.Count().Should().Be(0); - realResults.Should().BeEquivalentTo(testResults); - } + realResults.Count().Should().Be(1); + realResults.Should().BeEquivalentTo(testResults); + } - [Fact] - public async Task GivenAQueryUsingXorWhenExecutingThenTheResultsShouldMatch() + [Fact] + public async Task GivenAQueryUsingNotXorWhenExecutingThenTheResultsShouldMatch() + { + await _testCosmos.GivenAnExistingItem(new TestModel { - await _testCosmos.GivenAnExistingItem(new TestModel - { - Id = "RECORD1", - Name = "Bob Bobertson", - Value = false - }); + Id = "RECORD1", + Name = "Bob Bobertson", + Value = false + }); - var (realResults, testResults) = _testCosmos.WhenExecutingAQuery(q => q.Where(tm => tm.Value ^ true)); + var (realResults, testResults) = _testCosmos.WhenExecutingAQuery( + q => q.Where(tm => !(tm.Value ^ false)) + ); - realResults.Count().Should().Be(0); - realResults.Should().BeEquivalentTo(testResults); - } + realResults.Count().Should().Be(0); + realResults.Should().BeEquivalentTo(testResults); + } - [Fact] - [SuppressMessage("", "CA1304", Justification = "Culture Not Supported")] - public async Task GivenAQueryUsingToUpperWhenExecutingThenTheResultsShouldMatch() + [Fact] + public async Task GivenAQueryUsingXorWhenExecutingThenTheResultsShouldMatch() + { + await _testCosmos.GivenAnExistingItem(new TestModel { - await _testCosmos.GivenAnExistingItem(new TestModel - { - Id = "RECORD1", - Name = "Bob Bobertson", - Value = false - }); + Id = "RECORD1", + Name = "Bob Bobertson", + Value = false + }); - var (realResults, testResults) = _testCosmos.WhenExecutingAQuery(q => q.Where(tm => tm.Name.ToUpper() == "BOB BOBERTSON")); + var (realResults, testResults) = _testCosmos.WhenExecutingAQuery( + q => q.Where(tm => tm.Value ^ true) + ); - realResults.Count().Should().Be(1); - realResults.Should().BeEquivalentTo(testResults); - } + realResults.Count().Should().Be(0); + realResults.Should().BeEquivalentTo(testResults); + } - [Fact] - [SuppressMessage("", "CA1304", Justification = "Culture Not Supported")] - public async Task GivenAQueryUsingToLowerWhenExecutingThenTheResultsShouldMatch() + [Fact] + public async Task GivenAQueryUsingToUpperWhenExecutingThenTheResultsShouldMatch() + { + await _testCosmos.GivenAnExistingItem(new TestModel { - await _testCosmos.GivenAnExistingItem(new TestModel - { - Id = "RECORD1", - Name = "Bob Bobertson", - Value = false - }); + Id = "RECORD1", + Name = "Bob Bobertson", + Value = false + }); - var (realResults, testResults) = _testCosmos.WhenExecutingAQuery(q => q.Where(tm => tm.Name.ToLower() == "bob bobertson")); + var (realResults, testResults) = _testCosmos.WhenExecutingAQuery( + q => q.Where(tm => tm.Name.ToUpper() == "BOB BOBERTSON") + ); - realResults.Count().Should().Be(1); - realResults.Should().BeEquivalentTo(testResults); - } + realResults.Count().Should().Be(1); + realResults.Should().BeEquivalentTo(testResults); + } - [Fact] - [SuppressMessage("", "CA1304", Justification = "Culture Not Supported")] - public async Task GivenAQueryUsingAnyInASubQueryWhenExecutingThenTheResultsShouldMatch() + [Fact] + public async Task GivenAQueryUsingToLowerWhenExecutingThenTheResultsShouldMatch() + { + await _testCosmos.GivenAnExistingItem(new TestModel { - await _testCosmos.GivenAnExistingItem(new TestModel - { - Id = "RECORD1", - Children = new[] - { - new SubModel { Value = "bob bobertson" } - } - }); + Id = "RECORD1", + Name = "Bob Bobertson", + Value = false + }); - var (realResults, testResults) = _testCosmos.WhenExecutingAQuery(q => q.Where(tm => tm.Children.Any(c => c.Value == "bob bobertson"))); + var (realResults, testResults) = _testCosmos.WhenExecutingAQuery( + q => q.Where(tm => tm.Name.ToLower() == "bob bobertson") + ); - realResults.Count().Should().Be(1); - realResults.Should().BeEquivalentTo(testResults); - } + realResults.Count().Should().Be(1); + realResults.Should().BeEquivalentTo(testResults); + } - [Fact] - public async Task GivenAQueryUsingToUpperInvariantWhenExecutingThenBothShouldError() - { - await _testCosmos.GivenAnExistingItem(new TestModel - { - Id = "RECORD1", - Name = "Bob Bobertson", - Value = false - }); - - var (realResults, testResults) = _testCosmos.WhenExecutingAQuery(q => q.Where(tm => tm.Name.ToUpperInvariant() == "BOB BOBERTSON")); - - var realResultsAction = new Func>(() => realResults.ToList()); - var testResultsAction = new Func>(() => testResults.ToList()); - - realResultsAction.Should().Throw(); - testResultsAction.Should().Throw(); - } - - [Fact] - [SuppressMessage("", "CA1308", Justification = "Specifically testing")] - public async Task GivenAQueryUsingToLowerInvariantWhenExecutingThenBothShouldError() - { - await _testCosmos.GivenAnExistingItem(new TestModel - { - Id = "RECORD1", - Name = "Bob Bobertson", - Value = false - }); - - var (realResults, testResults) = _testCosmos.WhenExecutingAQuery(q => q.Where(tm => tm.Name.ToLowerInvariant() == "bob bobertson")); - - var realResultsAction = new Func>(() => realResults.ToList()); - var testResultsAction = new Func>(() => testResults.ToList()); - - realResultsAction.Should().Throw(); - testResultsAction.Should().Throw(); - } - - [Fact] - [SuppressMessage("", "CA1308", Justification = "Specifically testing")] - public async Task GivenAQueryUsingToAnyWhenExecutingThenBothShouldError() + [Fact] + public async Task GivenAQueryUsingAnyInASubQueryWhenExecutingThenTheResultsShouldMatch() + { + await _testCosmos.GivenAnExistingItem(new TestModel { - await _testCosmos.GivenAnExistingItem(new TestModel + Id = "RECORD1", + Children = new[] { - Id = "RECORD1", - Name = "Bob Bobertson", - Value = false - }); - - var (realResults, testResults) = _testCosmos.WhenExecutingAQuery(q => q.Where(tm => tm.Name == "bob bobertson")); - - var realResultsAction = new Func(() => realResults.Any()); - var testResultsAction = new Func(() => testResults.Any()); - - realResultsAction.Should().Throw(); - testResultsAction.Should().Throw(); - } - - [Fact] - public async Task GivenAQueryUsingFirstOrDefaultWhenExecutingThenBothShouldError() + new SubModel { Value = "bob bobertson" } + } + }); + + var (realResults, testResults) = _testCosmos.WhenExecutingAQuery( + q => q.Where(tm => tm.Children.Any(c => c.Value == "bob bobertson")) + ); + + realResults.Count().Should().Be(1); + realResults.Should().BeEquivalentTo(testResults); + } + + [Fact] + public async Task GivenAQueryUsingToUpperInvariantWhenExecutingThenBothShouldError() + { + await _testCosmos.GivenAnExistingItem(new TestModel { - await _testCosmos.GivenAnExistingItem(new TestModel - { - Id = "RECORD1", - Name = "Bob Bobertson", - Value = false - }); - - var (realResults, testResults) = _testCosmos.WhenExecutingAQuery(q => q.Where(tm => tm.Name == "Bob Bobertson")); - - var realResultsAction = new Func(() => realResults.FirstOrDefault()); - var testResultsAction = new Func(() => testResults.FirstOrDefault()); - - realResultsAction.Should().Throw(); - testResultsAction.Should().Throw(); - } - - [Fact] - public async Task GivenAQueryUsingSingleOrDefaultWhenExecutingThenBothShouldError() + Id = "RECORD1", + Name = "Bob Bobertson", + Value = false + }); + + var (realResults, testResults) = _testCosmos.WhenExecutingAQuery( + q => q.Where(tm => tm.Name.ToUpperInvariant() == "BOB BOBERTSON") + ); + + var realResultsAction = new Func>(() => realResults.ToList()); + var testResultsAction = new Func>(() => testResults.ToList()); + + realResultsAction.Should().Throw(); + testResultsAction.Should().Throw(); + } + + [Fact] + public async Task GivenAQueryUsingToLowerInvariantWhenExecutingThenBothShouldError() + { + await _testCosmos.GivenAnExistingItem(new TestModel { - await _testCosmos.GivenAnExistingItem(new TestModel - { - Id = "RECORD1", - Name = "Bob Bobertson", - Value = false - }); - - var (realResults, testResults) = _testCosmos.WhenExecutingAQuery(q => q.Where(tm => tm.Name == "Bob Bobertson")); - - var realResultsAction = new Func(() => realResults.SingleOrDefault()); - var testResultsAction = new Func(() => testResults.SingleOrDefault()); - - realResultsAction.Should().Throw(); - testResultsAction.Should().Throw(); - } - - [Fact] - [SuppressMessage("", "CA1304", Justification = "Culture Not Supported")] - public async Task GivenAQueryUsingAnyWhenExecutingThenTheResultsShouldMatch() + Id = "RECORD1", + Name = "Bob Bobertson", + Value = false + }); + + var (realResults, testResults) = _testCosmos.WhenExecutingAQuery( + q => q.Where(tm => tm.Name.ToLowerInvariant() == "bob bobertson") + ); + + var realResultsAction = new Func>(() => realResults.ToList()); + var testResultsAction = new Func>(() => testResults.ToList()); + + realResultsAction.Should().Throw(); + testResultsAction.Should().Throw(); + } + + [Fact] + public async Task GivenAQueryUsingToAnyWhenExecutingThenBothShouldError() + { + await _testCosmos.GivenAnExistingItem(new TestModel { - await _testCosmos.GivenAnExistingItem(new TestModel - { - Id = "RECORD1", - Name = "Bob Bobertson", - Value = false - }); + Id = "RECORD1", + Name = "Bob Bobertson", + Value = false + }); - var (_, realException, _, testException) = _testCosmos.WhenExecutingAQuery(q => q.Any()); + var (realResults, testResults) = _testCosmos.WhenExecutingAQuery( + q => q.Where(tm => tm.Name == "bob bobertson") + ); - realException.Should().NotBeNull(); - testException.Should().NotBeNull(); - } + var realResultsAction = new Func(() => realResults.Any()); + var testResultsAction = new Func(() => testResults.Any()); - public Task InitializeAsync() + realResultsAction.Should().Throw(); + testResultsAction.Should().Throw(); + } + + [Fact] + public async Task GivenAQueryUsingFirstOrDefaultWhenExecutingThenBothShouldError() + { + await _testCosmos.GivenAnExistingItem(new TestModel { - _testCosmos = new TestCosmos(); - return _testCosmos.SetupAsync("/partitionKey"); - } + Id = "RECORD1", + Name = "Bob Bobertson", + Value = false + }); - public async Task DisposeAsync() + var (realResults, testResults) = _testCosmos.WhenExecutingAQuery( + q => q.Where(tm => tm.Name == "Bob Bobertson") + ); + + var realResultsAction = new Func(() => realResults.FirstOrDefault()); + var testResultsAction = new Func(() => testResults.FirstOrDefault()); + + realResultsAction.Should().Throw(); + testResultsAction.Should().Throw(); + } + + [Fact] + public async Task GivenAQueryUsingSingleOrDefaultWhenExecutingThenBothShouldError() + { + await _testCosmos.GivenAnExistingItem(new TestModel { - await _testCosmos.CleanupAsync(); - } + Id = "RECORD1", + Name = "Bob Bobertson", + Value = false + }); + + var (realResults, testResults) = _testCosmos.WhenExecutingAQuery( + q => q.Where(tm => tm.Name == "Bob Bobertson") + ); - public void Dispose() + var realResultsAction = new Func(() => realResults.SingleOrDefault()); + var testResultsAction = new Func(() => testResults.SingleOrDefault()); + + realResultsAction.Should().Throw(); + testResultsAction.Should().Throw(); + } + + [Fact] + public async Task GivenAQueryUsingAnyWhenExecutingThenTheResultsShouldMatch() + { + await _testCosmos.GivenAnExistingItem(new TestModel { - _testCosmos?.Dispose(); - } + Id = "RECORD1", + Name = "Bob Bobertson", + Value = false + }); - private bool GetTrue() => true; + var (_, realException, _, testException) = _testCosmos.WhenExecutingAQuery( + q => q.Any() + ); - private bool GetBool(bool b) => b; + realException.Should().NotBeNull(); + testException.Should().NotBeNull(); } + + public Task InitializeAsync() + { + _testCosmos = new TestCosmos(); + return _testCosmos.SetupAsync("/partitionKey"); + } + + public async Task DisposeAsync() + { + await _testCosmos.CleanupAsync(); + } + + public void Dispose() + { + _testCosmos?.Dispose(); + } + + private bool GetTrue() => true; + + private bool GetBool(bool b) => b; } \ No newline at end of file diff --git a/CosmosTestHelpers.IntegrationTests/CosmosReadItemStreamTests.cs b/CosmosTestHelpers.IntegrationTests/CosmosReadItemStreamTests.cs index 6d33e96..5f1ee31 100644 --- a/CosmosTestHelpers.IntegrationTests/CosmosReadItemStreamTests.cs +++ b/CosmosTestHelpers.IntegrationTests/CosmosReadItemStreamTests.cs @@ -2,52 +2,51 @@ using Microsoft.Azure.Cosmos; using Xunit; -namespace CosmosTestHelpers.IntegrationTests +namespace CosmosTestHelpers.IntegrationTests; + +[Collection("Integration Tests")] +public sealed class CosmosReadItemStreamTests : IAsyncLifetime, IDisposable { - [Collection("Integration Tests")] - public sealed class CosmosReadItemStreamTests : IAsyncLifetime, IDisposable - { - private TestCosmos _testCosmos; + private TestCosmos _testCosmos; - [Fact] - public async Task ReadWithEmptyIdIsEquivalent() - { - var (realResponse, testResponse) = await _testCosmos.WhenReadItemStream(string.Empty); + [Fact] + public async Task ReadWithEmptyIdIsEquivalent() + { + var (realResponse, testResponse) = await _testCosmos.WhenReadItemStream(string.Empty); - realResponse.Should().NotBeNull(); - testResponse.Should().NotBeNull(); - realResponse.StatusCode.Should().Be(testResponse.StatusCode); - } + realResponse.Should().NotBeNull(); + testResponse.Should().NotBeNull(); + realResponse.StatusCode.Should().Be(testResponse.StatusCode); + } - [Fact] - public async Task ReadWithNullIdIsEquivalent() - { - var (realException, testException) = await _testCosmos.WhenReadItemStreamProducesException(null); + [Fact] + public async Task ReadWithNullIdIsEquivalent() + { + var (realException, testException) = await _testCosmos.WhenReadItemStreamProducesException(null); - realException.Should().NotBeNull(); - testException.Should().NotBeNull(); - realException.Should().BeOfType(testException.GetType()); + realException.Should().NotBeNull(); + testException.Should().NotBeNull(); + realException.Should().BeOfType(testException.GetType()); - if (realException is CosmosException realCosmosException && testException is CosmosException testCosmosException) - { - realCosmosException.StatusCode.Should().Be(testCosmosException.StatusCode); - } - } - - public Task InitializeAsync() + if (realException is CosmosException realCosmosException && testException is CosmosException testCosmosException) { - _testCosmos = new TestCosmos(); - return _testCosmos.SetupAsync("/partitionKey"); + realCosmosException.StatusCode.Should().Be(testCosmosException.StatusCode); } + } + + public Task InitializeAsync() + { + _testCosmos = new TestCosmos(); + return _testCosmos.SetupAsync("/partitionKey"); + } - public async Task DisposeAsync() - { - await _testCosmos.CleanupAsync(); - } + public async Task DisposeAsync() + { + await _testCosmos.CleanupAsync(); + } - public void Dispose() - { - _testCosmos?.Dispose(); - } + public void Dispose() + { + _testCosmos?.Dispose(); } } \ No newline at end of file diff --git a/CosmosTestHelpers.IntegrationTests/CosmosReadTests.cs b/CosmosTestHelpers.IntegrationTests/CosmosReadTests.cs index 764a003..81804b5 100644 --- a/CosmosTestHelpers.IntegrationTests/CosmosReadTests.cs +++ b/CosmosTestHelpers.IntegrationTests/CosmosReadTests.cs @@ -1,73 +1,73 @@ -using FluentAssertions; +using CosmosTestHelpers.IntegrationTests.TestModels; +using FluentAssertions; using Microsoft.Azure.Cosmos; using Xunit; -namespace CosmosTestHelpers.IntegrationTests +namespace CosmosTestHelpers.IntegrationTests; + +[Collection("Integration Tests")] +public sealed class CosmosReadTests : IAsyncLifetime, IDisposable { - [Collection("Integration Tests")] - public sealed class CosmosReadTests : IAsyncLifetime, IDisposable - { - private TestCosmos _testCosmos; + private TestCosmos _testCosmos; - [Fact] - public async Task ReadWithEmptyIdIsEquivalent() - { - var (realException, testException) = await _testCosmos.WhenReadItemProducesException(string.Empty); + [Fact] + public async Task ReadWithEmptyIdIsEquivalent() + { + var (realException, testException) = await _testCosmos.WhenReadItemProducesException(string.Empty); - realException.Should().NotBeNull(); - testException.Should().NotBeNull(); - realException.Should().BeOfType(testException.GetType()); + realException.Should().NotBeNull(); + testException.Should().NotBeNull(); + realException.Should().BeOfType(testException.GetType()); - if (realException is CosmosException realCosmosException && testException is CosmosException testCosmosException) - { - realCosmosException.StatusCode.Should().Be(testCosmosException.StatusCode); - } - } - - [Fact] - public async Task ReadWithInvalidIdIsEquivalent() + if (realException is CosmosException realCosmosException && testException is CosmosException testCosmosException) { - var (realException, testException) = await _testCosmos.WhenReadItemProducesException("#"); + realCosmosException.StatusCode.Should().Be(testCosmosException.StatusCode); + } + } + + [Fact] + public async Task ReadWithInvalidIdIsEquivalent() + { + var (realException, testException) = await _testCosmos.WhenReadItemProducesException("#"); - realException.Should().NotBeNull(); - testException.Should().NotBeNull(); - realException.Should().BeOfType(testException.GetType()); + realException.Should().NotBeNull(); + testException.Should().NotBeNull(); + realException.Should().BeOfType(testException.GetType()); - if (realException is CosmosException realCosmosException && testException is CosmosException testCosmosException) - { - realCosmosException.StatusCode.Should().Be(testCosmosException.StatusCode); - } - } - - [Fact] - public async Task ReadWithNullIdIsEquivalent() + if (realException is CosmosException realCosmosException && testException is CosmosException testCosmosException) { - var (realException, testException) = await _testCosmos.WhenReadItemProducesException(null); + realCosmosException.StatusCode.Should().Be(testCosmosException.StatusCode); + } + } + + [Fact] + public async Task ReadWithNullIdIsEquivalent() + { + var (realException, testException) = await _testCosmos.WhenReadItemProducesException(null); - realException.Should().NotBeNull(); - testException.Should().NotBeNull(); - realException.Should().BeOfType(testException.GetType()); + realException.Should().NotBeNull(); + testException.Should().NotBeNull(); + realException.Should().BeOfType(testException.GetType()); - if (realException is CosmosException realCosmosException && testException is CosmosException testCosmosException) - { - realCosmosException.StatusCode.Should().Be(testCosmosException.StatusCode); - } - } - - public Task InitializeAsync() + if (realException is CosmosException realCosmosException && testException is CosmosException testCosmosException) { - _testCosmos = new TestCosmos(); - return _testCosmos.SetupAsync("/partitionKey"); + realCosmosException.StatusCode.Should().Be(testCosmosException.StatusCode); } + } + + public Task InitializeAsync() + { + _testCosmos = new TestCosmos(); + return _testCosmos.SetupAsync("/partitionKey"); + } - public async Task DisposeAsync() - { - await _testCosmos.CleanupAsync(); - } + public async Task DisposeAsync() + { + await _testCosmos.CleanupAsync(); + } - public void Dispose() - { - _testCosmos?.Dispose(); - } + public void Dispose() + { + _testCosmos?.Dispose(); } } \ No newline at end of file diff --git a/CosmosTestHelpers.IntegrationTests/CosmosReplaceTests.cs b/CosmosTestHelpers.IntegrationTests/CosmosReplaceTests.cs index e1ec341..58a1f6f 100644 --- a/CosmosTestHelpers.IntegrationTests/CosmosReplaceTests.cs +++ b/CosmosTestHelpers.IntegrationTests/CosmosReplaceTests.cs @@ -3,109 +3,114 @@ using Microsoft.Azure.Cosmos; using Xunit; -namespace CosmosTestHelpers.IntegrationTests +namespace CosmosTestHelpers.IntegrationTests; + +[Collection("Integration Tests")] +public sealed class CosmosReplaceTests : IAsyncLifetime, IDisposable { - [Collection("Integration Tests")] - public sealed class CosmosReplaceTests : IAsyncLifetime, IDisposable - { - private TestCosmos _testCosmos; + private TestCosmos _testCosmos; - [Fact] - public async Task ReplaceExistingWithWrongETagIsEquivalent() + [Fact] + public async Task ReplaceExistingWithWrongETagIsEquivalent() + { + await _testCosmos.GivenAnExistingItem(new TestModel { - await _testCosmos.GivenAnExistingItem(new TestModel + Id = "RECORD1", + Name = "Bob Bobertson", + EnumValue = TestEnum.Value2 + }); + + var (realException, testException) = await _testCosmos.WhenReplacingProducesException( + new TestModel { Id = "RECORD1", Name = "Bob Bobertson", - EnumValue = TestEnum.Value2 + EnumValue = TestEnum.Value1 + }, + "RECORD1", + new ItemRequestOptions + { + IfMatchEtag = Guid.NewGuid().ToString() + }, + new ItemRequestOptions + { + IfMatchEtag = Guid.NewGuid().ToString() }); - var (realException, testException) = await _testCosmos.WhenReplacingProducesException( - new TestModel - { - Id = "RECORD1", - Name = "Bob Bobertson", - EnumValue = TestEnum.Value1 - }, - "RECORD1", - new ItemRequestOptions - { - IfMatchEtag = Guid.NewGuid().ToString() - }, - new ItemRequestOptions - { - IfMatchEtag = Guid.NewGuid().ToString() - }); + realException.Should().NotBeNull(); + testException.Should().NotBeNull(); + realException.StatusCode.Should().Be(testException.StatusCode); + realException.Should().BeOfType(testException.GetType()); + } - realException.Should().NotBeNull(); - testException.Should().NotBeNull(); - realException.StatusCode.Should().Be(testException.StatusCode); - realException.Should().BeOfType(testException.GetType()); - } - - [Fact] - public async Task ReplaceExistingWithCorrectETagIsEquivalent() + [Fact] + public async Task ReplaceExistingWithCorrectETagIsEquivalent() + { + var (testETag, realETag) = await _testCosmos.GivenAnExistingItem(new TestModel { - var (testETag, realETag) = await _testCosmos.GivenAnExistingItem(new TestModel + Id = "RECORD1", + Name = "Bob Bobertson", + EnumValue = TestEnum.Value2 + }); + + var (realResult, testResult) = await _testCosmos.WhenReplacing( + new TestModel { Id = "RECORD1", Name = "Bob Bobertson", - EnumValue = TestEnum.Value2 + EnumValue = TestEnum.Value1 + }, + "RECORD1", + new ItemRequestOptions + { + IfMatchEtag = testETag + }, + new ItemRequestOptions + { + IfMatchEtag = realETag }); - var (realResult, testResult) = await _testCosmos.WhenReplacing( - new TestModel - { - Id = "RECORD1", - Name = "Bob Bobertson", - EnumValue = TestEnum.Value1 - }, - "RECORD1", - new ItemRequestOptions - { - IfMatchEtag = testETag - }, - new ItemRequestOptions - { - IfMatchEtag = realETag - }); + realResult.StatusCode.Should().Be(testResult.StatusCode); + realResult.Resource.Should().BeEquivalentTo(testResult.Resource); + } - realResult.StatusCode.Should().Be(testResult.StatusCode); - realResult.Resource.Should().BeEquivalentTo(testResult.Resource); - } - - [Fact] - public async Task ReplaceNonExistentItemThrowsTheSameException() - { - var (realException, testException) = await _testCosmos.WhenReplacingProducesException( - new TestModel - { - Id = "RECORD1", - Name = "Bob Bobertson", - EnumValue = TestEnum.Value1 - }, - "RECORD1"); + [Fact] + public async Task ReplaceNonExistentItemThrowsTheSameException() + { + var (realException, testException) = await _testCosmos.WhenReplacingProducesException( + new TestModel + { + Id = "RECORD1", + Name = "Bob Bobertson", + EnumValue = TestEnum.Value1 + }, + "RECORD1" + ); - realException.Should().NotBeNull(); - testException.Should().NotBeNull(); - realException.StatusCode.Should().Be(testException.StatusCode); - realException.Should().BeOfType(testException.GetType()); - } + realException.Should().NotBeNull(); + testException.Should().NotBeNull(); + realException.StatusCode.Should().Be(testException.StatusCode); + realException.Should().BeOfType(testException.GetType()); + } - public Task InitializeAsync() + public async Task InitializeAsync() + { + var uniqueKeyPolicy = new UniqueKeyPolicy { - _testCosmos = new TestCosmos(); - return _testCosmos.SetupAsync("/partitionKey", new UniqueKeyPolicy { UniqueKeys = { new UniqueKey { Paths = {"/Name"}}}}); - } + UniqueKeys = { new UniqueKey { Paths = { "/Name" } } } + }; - public async Task DisposeAsync() - { - await _testCosmos.CleanupAsync(); - } + _testCosmos = new TestCosmos(); + await _testCosmos.SetupAsync("/partitionKey", uniqueKeyPolicy); + } - public void Dispose() - { - _testCosmos?.Dispose(); - } + public async Task DisposeAsync() + { + await _testCosmos.CleanupAsync(); + } + + public void Dispose() + { + _testCosmos?.Dispose(); } } \ No newline at end of file diff --git a/CosmosTestHelpers.IntegrationTests/CosmosTestHelpers.IntegrationTests.csproj.DotSettings b/CosmosTestHelpers.IntegrationTests/CosmosTestHelpers.IntegrationTests.csproj.DotSettings new file mode 100644 index 0000000..8b01856 --- /dev/null +++ b/CosmosTestHelpers.IntegrationTests/CosmosTestHelpers.IntegrationTests.csproj.DotSettings @@ -0,0 +1,2 @@ + + True \ No newline at end of file diff --git a/CosmosTestHelpers.IntegrationTests/CosmosUpsertStreamTests.cs b/CosmosTestHelpers.IntegrationTests/CosmosUpsertStreamTests.cs index a1b6e45..c0d4d53 100644 --- a/CosmosTestHelpers.IntegrationTests/CosmosUpsertStreamTests.cs +++ b/CosmosTestHelpers.IntegrationTests/CosmosUpsertStreamTests.cs @@ -1,184 +1,177 @@ -using System.Diagnostics.CodeAnalysis; -using System.Text; +using System.Text; using CosmosTestHelpers.IntegrationTests.TestModels; using FluentAssertions; using Microsoft.Azure.Cosmos; using Newtonsoft.Json; using Xunit; -namespace CosmosTestHelpers.IntegrationTests +namespace CosmosTestHelpers.IntegrationTests; + +[Collection("Integration Tests")] +public sealed class CosmosUpsertStreamTests : IAsyncLifetime, IDisposable { - [Collection("Integration Tests")] - public sealed class CosmosUpsertStreamTests : IAsyncLifetime, IDisposable + private TestCosmos _testCosmos; + + [Fact] + public async Task UpsertStreamNonExistingIsEquivalent() { - private TestCosmos _testCosmos; + var bytes = GetItemBytes(new TestModel + { + Id = "RECORD2", + Name = "Bob Bobertson", + EnumValue = TestEnum.Value1, + PartitionKey = "test" + }); + + await using var ms = new MemoryStream(bytes); + var (real, test) = await _testCosmos.WhenUpsertingStream(ms, new PartitionKey("test")); + + real.StatusCode.Should().Be(test.StatusCode); + } - [Fact] - [SuppressMessage("", "CA2000", Justification = "AsyncDispose....")] - public async Task UpsertStreamNonExistingIsEquivalent() + [Fact] + public async Task UpsertStreamExistingIsEquivalent() + { + await _testCosmos.GivenAnExistingItem(new TestModel { - var bytes = GetItemBytes( - new TestModel - { - Id = "RECORD2", - Name = "Bob Bobertson", - EnumValue = TestEnum.Value1, - PartitionKey = "test" - }); - - await using var ms = new MemoryStream(bytes); - var (real, test) = await _testCosmos.WhenUpsertingStream(ms, new PartitionKey("test")); - - real.StatusCode.Should().Be(test.StatusCode); - } - - [Fact] - [SuppressMessage("", "CA2000", Justification = "AsyncDispose....")] - public async Task UpsertStreamExistingIsEquivalent() + Id = "RECORD1", + Name = "Bob Bobertson", + EnumValue = TestEnum.Value2, + PartitionKey = "test" + }); + + var bytes = GetItemBytes(new TestModel { - await _testCosmos.GivenAnExistingItem(new TestModel - { - Id = "RECORD1", - Name = "Bob Bobertson", - EnumValue = TestEnum.Value2, - PartitionKey = "test" - }); + Id = "RECORD1", + Name = "Bob Bobertson", + EnumValue = TestEnum.Value1, + PartitionKey = "test" + }); - var bytes = GetItemBytes( - new TestModel - { - Id = "RECORD1", - Name = "Bob Bobertson", - EnumValue = TestEnum.Value1, - PartitionKey = "test" - }); - - await using var ms = new MemoryStream(bytes); - var (real, test) = await _testCosmos.WhenUpsertingStream(ms, new PartitionKey("test")); - - real.StatusCode.Should().Be(test.StatusCode); - } - - [Fact] - [SuppressMessage("", "CA2000", Justification = "AsyncDispose....")] - public async Task UpsertStreamExistingWithCorrectETagIsEquivalent() + await using var ms = new MemoryStream(bytes); + var (real, test) = await _testCosmos.WhenUpsertingStream(ms, new PartitionKey("test")); + + real.StatusCode.Should().Be(test.StatusCode); + } + + [Fact] + public async Task UpsertStreamExistingWithCorrectETagIsEquivalent() + { + var (testETag, realETag) = await _testCosmos.GivenAnExistingItem(new TestModel { - var (testETag, realETag) = await _testCosmos.GivenAnExistingItem(new TestModel - { - Id = "RECORD1", - Name = "Bob Bobertson", - EnumValue = TestEnum.Value2, - PartitionKey = "test" - }); + Id = "RECORD1", + Name = "Bob Bobertson", + EnumValue = TestEnum.Value2, + PartitionKey = "test" + }); - var bytes = GetItemBytes( - new TestModel - { - Id = "RECORD1", - Name = "Bob Bobertson", - EnumValue = TestEnum.Value1, - PartitionKey = "test" - }); - - await using var ms = new MemoryStream(bytes); - var (real, test) = await _testCosmos.WhenUpsertingStream( - ms, - new PartitionKey("test"), - new ItemRequestOptions - { - IfMatchEtag = testETag - }, - new ItemRequestOptions - { - IfMatchEtag = realETag - }); - - real.StatusCode.Should().Be(test.StatusCode); - } - - [Fact] - [SuppressMessage("", "CA2000", Justification = "AsyncDispose....")] - public async Task UpsertStreamExistingWithWrongETagIsEquivalent() + var bytes = GetItemBytes(new TestModel { - await _testCosmos.GivenAnExistingItem(new TestModel + Id = "RECORD1", + Name = "Bob Bobertson", + EnumValue = TestEnum.Value1, + PartitionKey = "test" + }); + + await using var ms = new MemoryStream(bytes); + var (real, test) = await _testCosmos.WhenUpsertingStream( + ms, + new PartitionKey("test"), + new ItemRequestOptions + { + IfMatchEtag = testETag + }, + new ItemRequestOptions { - Id = "RECORD1", - Name = "Bob Bobertson", - EnumValue = TestEnum.Value2, - PartitionKey = "test" + IfMatchEtag = realETag }); - var bytes = GetItemBytes( - new TestModel - { - Id = "RECORD1", - Name = "Bob Bobertson", - EnumValue = TestEnum.Value1, - PartitionKey = "test" - }); - - await using var ms = new MemoryStream(bytes); - var (real, test) = await _testCosmos.WhenUpsertingStream( - ms, - new PartitionKey("test"), - new ItemRequestOptions - { - IfMatchEtag = Guid.NewGuid().ToString() - }, - new ItemRequestOptions - { - IfMatchEtag = Guid.NewGuid().ToString() - }); - - real.StatusCode.Should().Be(test.StatusCode); - } - - [Fact] - [SuppressMessage("", "CA2000", Justification = "AsyncDispose....")] - public async Task UpsertStreamUniqueKeyViolationIsEquivalent() + real.StatusCode.Should().Be(test.StatusCode); + } + + [Fact] + public async Task UpsertStreamExistingWithWrongETagIsEquivalent() + { + await _testCosmos.GivenAnExistingItem(new TestModel + { + Id = "RECORD1", + Name = "Bob Bobertson", + EnumValue = TestEnum.Value2, + PartitionKey = "test" + }); + + var bytes = GetItemBytes(new TestModel { - await _testCosmos.GivenAnExistingItem(new TestModel + Id = "RECORD1", + Name = "Bob Bobertson", + EnumValue = TestEnum.Value1, + PartitionKey = "test" + }); + + await using var ms = new MemoryStream(bytes); + var (real, test) = await _testCosmos.WhenUpsertingStream( + ms, + new PartitionKey("test"), + new ItemRequestOptions + { + IfMatchEtag = Guid.NewGuid().ToString() + }, + new ItemRequestOptions { - Id = "RECORD1", - Name = "Bob Bobertson", - EnumValue = TestEnum.Value2, - PartitionKey = "test" + IfMatchEtag = Guid.NewGuid().ToString() }); - var bytes = GetItemBytes( - new TestModel - { - Id = "RECORD2", - Name = "Bob Bobertson", - EnumValue = TestEnum.Value1, - PartitionKey = "test" - }); - - await using var ms = new MemoryStream(bytes); - var (real, test) = await _testCosmos.WhenUpsertingStream(ms, new PartitionKey("test")); - - real.StatusCode.Should().Be(test.StatusCode); - } - - public Task InitializeAsync() - { - _testCosmos = new TestCosmos(); - return _testCosmos.SetupAsync("/partitionKey", new UniqueKeyPolicy { UniqueKeys = { new UniqueKey { Paths = {"/Name"}}}}); - } + real.StatusCode.Should().Be(test.StatusCode); + } - public async Task DisposeAsync() + [Fact] + public async Task UpsertStreamUniqueKeyViolationIsEquivalent() + { + await _testCosmos.GivenAnExistingItem(new TestModel { - await _testCosmos.CleanupAsync(); - } + Id = "RECORD1", + Name = "Bob Bobertson", + EnumValue = TestEnum.Value2, + PartitionKey = "test" + }); - public void Dispose() + var bytes = GetItemBytes(new TestModel { - _testCosmos?.Dispose(); - } + Id = "RECORD2", + Name = "Bob Bobertson", + EnumValue = TestEnum.Value1, + PartitionKey = "test" + }); + + await using var ms = new MemoryStream(bytes); + var (real, test) = await _testCosmos.WhenUpsertingStream(ms, new PartitionKey("test")); - private static byte[] GetItemBytes(TestModel model) + real.StatusCode.Should().Be(test.StatusCode); + } + + public Task InitializeAsync() + { + var uniqueKeyPolicy = new UniqueKeyPolicy { - return Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(model)); - } + UniqueKeys = { new UniqueKey { Paths = { "/Name" } } } + }; + + _testCosmos = new TestCosmos(); + return _testCosmos.SetupAsync("/partitionKey", uniqueKeyPolicy); + } + + public async Task DisposeAsync() + { + await _testCosmos.CleanupAsync(); + } + + public void Dispose() + { + _testCosmos?.Dispose(); + } + + private static byte[] GetItemBytes(TestModel model) + { + return Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(model)); } } \ No newline at end of file diff --git a/CosmosTestHelpers.IntegrationTests/CosmosUpsertTests.cs b/CosmosTestHelpers.IntegrationTests/CosmosUpsertTests.cs index d57c5eb..bf39e3a 100644 --- a/CosmosTestHelpers.IntegrationTests/CosmosUpsertTests.cs +++ b/CosmosTestHelpers.IntegrationTests/CosmosUpsertTests.cs @@ -3,150 +3,151 @@ using Microsoft.Azure.Cosmos; using Xunit; -namespace CosmosTestHelpers.IntegrationTests +namespace CosmosTestHelpers.IntegrationTests; + +[Collection("Integration Tests")] +public sealed class CosmosUpsertTests : IAsyncLifetime, IDisposable { - [Collection("Integration Tests")] - public sealed class CosmosUpsertTests : IAsyncLifetime, IDisposable + private TestCosmos _testCosmos; + + [Fact] + public async Task UpsertNonExistingIsEquivalent() { - private TestCosmos _testCosmos; + var (realResult, testResult) = await _testCosmos.WhenUpserting( + new TestModel + { + Id = "RECORD1", + Name = "Fred Blogs", + EnumValue = TestEnum.Value1 + }); - [Fact] - public async Task UpsertNonExistingIsEquivalent() - { - var (realResult, testResult) = await _testCosmos.WhenUpserting( - new TestModel - { - Id = "RECORD1", - Name = "Fred Blogs", - EnumValue = TestEnum.Value1 - }); - - realResult.StatusCode.Should().Be(testResult.StatusCode); - realResult.Resource.Should().BeEquivalentTo(testResult.Resource); - } - - [Fact] - public async Task UpsertExistingIsEquivalent() + realResult.StatusCode.Should().Be(testResult.StatusCode); + realResult.Resource.Should().BeEquivalentTo(testResult.Resource); + } + + [Fact] + public async Task UpsertExistingIsEquivalent() + { + await _testCosmos.GivenAnExistingItem(new TestModel { - await _testCosmos.GivenAnExistingItem(new TestModel + Id = "RECORD1", + Name = "Bob Bobertson", + EnumValue = TestEnum.Value2 + }); + + var (realResult, testResult) = await _testCosmos.WhenUpserting( + new TestModel { Id = "RECORD1", Name = "Bob Bobertson", - EnumValue = TestEnum.Value2 + EnumValue = TestEnum.Value1 }); - var (realResult, testResult) = await _testCosmos.WhenUpserting( - new TestModel - { - Id = "RECORD1", - Name = "Bob Bobertson", - EnumValue = TestEnum.Value1 - }); - - realResult.StatusCode.Should().Be(testResult.StatusCode); - realResult.Resource.Should().BeEquivalentTo(testResult.Resource); - } - - [Fact] - public async Task UpsertExistingWithWrongETagIsEquivalent() + realResult.StatusCode.Should().Be(testResult.StatusCode); + realResult.Resource.Should().BeEquivalentTo(testResult.Resource); + } + + [Fact] + public async Task UpsertExistingWithWrongETagIsEquivalent() + { + await _testCosmos.GivenAnExistingItem(new TestModel { - await _testCosmos.GivenAnExistingItem(new TestModel + Id = "RECORD1", + Name = "Bob Bobertson", + EnumValue = TestEnum.Value2 + }); + + var (realException, testException) = await _testCosmos.WhenUpsertingProducesException( + new TestModel { Id = "RECORD1", Name = "Bob Bobertson", - EnumValue = TestEnum.Value2 + EnumValue = TestEnum.Value1 + }, + new ItemRequestOptions + { + IfMatchEtag = Guid.NewGuid().ToString() + }, + new ItemRequestOptions + { + IfMatchEtag = Guid.NewGuid().ToString() }); - var (realException, testException) = await _testCosmos.WhenUpsertingProducesException( - new TestModel - { - Id = "RECORD1", - Name = "Bob Bobertson", - EnumValue = TestEnum.Value1 - }, - new ItemRequestOptions - { - IfMatchEtag = Guid.NewGuid().ToString() - }, - new ItemRequestOptions - { - IfMatchEtag = Guid.NewGuid().ToString() - }); - - realException.Should().NotBeNull(); - testException.Should().NotBeNull(); - realException.StatusCode.Should().Be(testException.StatusCode); - realException.Should().BeOfType(testException.GetType()); - } - - [Fact] - public async Task UpsertExistingWithCorrectETagIsEquivalent() + realException.Should().NotBeNull(); + testException.Should().NotBeNull(); + realException.StatusCode.Should().Be(testException.StatusCode); + realException.Should().BeOfType(testException.GetType()); + } + + [Fact] + public async Task UpsertExistingWithCorrectETagIsEquivalent() + { + var (testETag, realETag) = await _testCosmos.GivenAnExistingItem(new TestModel { - var (testETag, realETag) = await _testCosmos.GivenAnExistingItem(new TestModel + Id = "RECORD1", + Name = "Bob Bobertson", + EnumValue = TestEnum.Value2 + }); + + var (realResult, testResult) = await _testCosmos.WhenUpserting( + new TestModel { Id = "RECORD1", Name = "Bob Bobertson", - EnumValue = TestEnum.Value2 + EnumValue = TestEnum.Value1 + }, + new ItemRequestOptions + { + IfMatchEtag = testETag + }, + new ItemRequestOptions + { + IfMatchEtag = realETag }); - var (realResult, testResult) = await _testCosmos.WhenUpserting( - new TestModel - { - Id = "RECORD1", - Name = "Bob Bobertson", - EnumValue = TestEnum.Value1 - }, - new ItemRequestOptions - { - IfMatchEtag = testETag - }, - new ItemRequestOptions - { - IfMatchEtag = realETag - }); - - realResult.StatusCode.Should().Be(testResult.StatusCode); - realResult.Resource.Should().BeEquivalentTo(testResult.Resource); - } - - [Fact] - public async Task UpsertUniqueKeyViolationIsEquivalent() + realResult.StatusCode.Should().Be(testResult.StatusCode); + realResult.Resource.Should().BeEquivalentTo(testResult.Resource); + } + + [Fact] + public async Task UpsertUniqueKeyViolationIsEquivalent() + { + await _testCosmos.GivenAnExistingItem(new TestModel { - await _testCosmos.GivenAnExistingItem(new TestModel + Id = "RECORD1", + Name = "Bob Bobertson", + EnumValue = TestEnum.Value2 + }); + + var (realException, testException) = await _testCosmos.WhenUpsertingProducesException( + new TestModel { - Id = "RECORD1", + Id = "RECORD2", Name = "Bob Bobertson", - EnumValue = TestEnum.Value2 + EnumValue = TestEnum.Value1 }); - var (realException, testException) = await _testCosmos.WhenUpsertingProducesException( - new TestModel - { - Id = "RECORD2", - Name = "Bob Bobertson", - EnumValue = TestEnum.Value1 - }); - - realException.Should().NotBeNull(); - testException.Should().NotBeNull(); - realException.StatusCode.Should().Be(testException.StatusCode); - realException.Should().BeOfType(testException.GetType()); - } - - public Task InitializeAsync() - { - _testCosmos = new TestCosmos(); - return _testCosmos.SetupAsync("/partitionKey", new UniqueKeyPolicy { UniqueKeys = { new UniqueKey { Paths = {"/Name"}}}}); - } + realException.Should().NotBeNull(); + testException.Should().NotBeNull(); + realException.StatusCode.Should().Be(testException.StatusCode); + realException.Should().BeOfType(testException.GetType()); + } - public async Task DisposeAsync() - { - await _testCosmos.CleanupAsync(); - } + public Task InitializeAsync() + { + var uniqueKeyPolicy = new UniqueKeyPolicy { UniqueKeys = { new UniqueKey { Paths = { "/Name" } } } }; - public void Dispose() - { - _testCosmos?.Dispose(); - } + _testCosmos = new TestCosmos(); + return _testCosmos.SetupAsync("/partitionKey", uniqueKeyPolicy); + } + + public async Task DisposeAsync() + { + await _testCosmos.CleanupAsync(); + } + + public void Dispose() + { + _testCosmos?.Dispose(); } } \ No newline at end of file diff --git a/CosmosTestHelpers.IntegrationTests/CosmosUpsertTestsOnTripleUniqueKey.cs b/CosmosTestHelpers.IntegrationTests/CosmosUpsertTestsOnTripleUniqueKey.cs index 811c0cb..8161f1e 100644 --- a/CosmosTestHelpers.IntegrationTests/CosmosUpsertTestsOnTripleUniqueKey.cs +++ b/CosmosTestHelpers.IntegrationTests/CosmosUpsertTestsOnTripleUniqueKey.cs @@ -3,34 +3,44 @@ using Microsoft.Azure.Cosmos; using Xunit; -namespace CosmosTestHelpers.IntegrationTests +namespace CosmosTestHelpers.IntegrationTests; + +[Collection("Integration Tests")] +public sealed class CosmosUpsertTestsOnTripleUniqueKey : IAsyncLifetime, IDisposable { - [Collection("Integration Tests")] - public sealed class CosmosUpsertTestsOnTripleUniqueKey : IAsyncLifetime, IDisposable + private TestCosmos _testCosmos; + + [Fact] + public async Task UpsertNonExistingIsEquivalent() { - private TestCosmos _testCosmos; + var (realResult, testResult) = await _testCosmos.WhenUpserting( + new TripleUniqueKeyModel + { + Id = Guid.NewGuid(), + CustomerId = "Fred Blogs", + ItemId = "MT12342", + Type = TestEnum.Value1 + }); - [Fact] - public async Task UpsertNonExistingIsEquivalent() - { - var (realResult, testResult) = await _testCosmos.WhenUpsertingTripleKeyModel( - new TripleUniqueKeyModel - { - Id = Guid.NewGuid(), - CustomerId = "Fred Blogs", - ItemId = "MT12342", - Type = TestEnum.Value1 - }); - - realResult.StatusCode.Should().Be(testResult.StatusCode); - realResult.Resource.Should().BeEquivalentTo(testResult.Resource); - } - - [Fact] - public async Task UpsertExistingIsEquivalent() + realResult.StatusCode.Should().Be(testResult.StatusCode); + realResult.Resource.Should().BeEquivalentTo(testResult.Resource); + } + + [Fact] + public async Task UpsertExistingIsEquivalent() + { + var id = Guid.NewGuid(); + + await _testCosmos.GivenAnExistingItem(new TripleUniqueKeyModel { - var id = Guid.NewGuid(); - await _testCosmos.GivenAnExistingItem(new TripleUniqueKeyModel + Id = id, + CustomerId = "Fred Blogs", + ItemId = "MT12342", + Type = TestEnum.Value1 + }); + + var (realResult, testResult) = await _testCosmos.WhenUpserting( + new TripleUniqueKeyModel { Id = id, CustomerId = "Fred Blogs", @@ -38,103 +48,100 @@ await _testCosmos.GivenAnExistingItem(new TripleUniqueKeyModel Type = TestEnum.Value1 }); - var (realResult, testResult) = await _testCosmos.WhenUpsertingTripleKeyModel( - new TripleUniqueKeyModel - { - Id = id, - CustomerId = "Fred Blogs", - ItemId = "MT12342", - Type = TestEnum.Value1 - }); - - realResult.StatusCode.Should().Be(testResult.StatusCode); - realResult.Resource.Should().BeEquivalentTo(testResult.Resource); - } - - [Fact] - public async Task UpsertUniqueKeyViolationIsEquivalent() + realResult.StatusCode.Should().Be(testResult.StatusCode); + realResult.Resource.Should().BeEquivalentTo(testResult.Resource); + } + + [Fact] + public async Task UpsertUniqueKeyViolationIsEquivalent() + { + var id = Guid.NewGuid(); + + await _testCosmos.GivenAnExistingItem(new TripleUniqueKeyModel { - var id = Guid.NewGuid(); - await _testCosmos.GivenAnExistingItem(new TripleUniqueKeyModel + Id = id, + CustomerId = "Fred Blogs", + ItemId = "MT12342", + Type = TestEnum.Value1 + }); + + var (realException, testException) = await _testCosmos.WhenUpsertingProducesException( + new TripleUniqueKeyModel { - Id = id, + Id = Guid.NewGuid(), CustomerId = "Fred Blogs", ItemId = "MT12342", Type = TestEnum.Value1 }); - var (realException, testException) = await _testCosmos.WhenUpsertingTripleKeyModelProducesException( - new TripleUniqueKeyModel - { - Id = Guid.NewGuid(), - CustomerId = "Fred Blogs", - ItemId = "MT12342", - Type = TestEnum.Value1 - }); - - realException.Should().NotBeNull(); - testException.Should().NotBeNull(); - realException.StatusCode.Should().Be(testException.StatusCode); - realException.Should().BeOfType(testException.GetType()); - } - - [Fact] - public async Task CreateNonExistingIsEquivalent() - { - var (realResult, testResult) = await _testCosmos.WhenCreatingTripleKeyModel( - new TripleUniqueKeyModel - { - Id = Guid.NewGuid(), - CustomerId = "Fred Blogs", - ItemId = "MT12342", - Type = TestEnum.Value1 - }); - - realResult.StatusCode.Should().Be(testResult.StatusCode); - realResult.Resource.Should().BeEquivalentTo(testResult.Resource); - } - - [Fact] - public async Task CreateUniqueKeyViolationIsEquivalent() - { - var id = Guid.NewGuid(); - await _testCosmos.GivenAnExistingItem(new TripleUniqueKeyModel + realException.Should().NotBeNull(); + testException.Should().NotBeNull(); + realException.StatusCode.Should().Be(testException.StatusCode); + realException.Should().BeOfType(testException.GetType()); + } + + [Fact] + public async Task CreateNonExistingIsEquivalent() + { + var (realResult, testResult) = await _testCosmos.WhenCreating( + new TripleUniqueKeyModel { - Id = id, + Id = Guid.NewGuid(), CustomerId = "Fred Blogs", ItemId = "MT12342", Type = TestEnum.Value1 }); - var (realException, testException) = await _testCosmos.WhenCreatingTripleKeyModelProducesException( - new TripleUniqueKeyModel - { - Id = Guid.NewGuid(), - CustomerId = "Fred Blogs", - ItemId = "MT12342", - Type = TestEnum.Value1 - }); - - realException.Should().NotBeNull(); - testException.Should().NotBeNull(); - realException.StatusCode.Should().Be(testException.StatusCode); - realException.Should().BeOfType(testException.GetType()); - } - - public Task InitializeAsync() - { - _testCosmos = new TestCosmos(); - return _testCosmos.SetupAsync("/partitionKey", new UniqueKeyPolicy { UniqueKeys = { new UniqueKey { Paths = { "/CustomerId", "/ItemId", "/Type" } } } }); - } + realResult.StatusCode.Should().Be(testResult.StatusCode); + realResult.Resource.Should().BeEquivalentTo(testResult.Resource); + } + + [Fact] + public async Task CreateUniqueKeyViolationIsEquivalent() + { + var id = Guid.NewGuid(); - public async Task DisposeAsync() + await _testCosmos.GivenAnExistingItem(new TripleUniqueKeyModel { - await _testCosmos.CleanupAsync(); - } + Id = id, + CustomerId = "Fred Blogs", + ItemId = "MT12342", + Type = TestEnum.Value1 + }); + + var (realException, testException) = await _testCosmos.WhenCreatingProducesException( + new TripleUniqueKeyModel + { + Id = Guid.NewGuid(), + CustomerId = "Fred Blogs", + ItemId = "MT12342", + Type = TestEnum.Value1 + }); - public void Dispose() + realException.Should().NotBeNull(); + testException.Should().NotBeNull(); + realException.StatusCode.Should().Be(testException.StatusCode); + realException.Should().BeOfType(testException.GetType()); + } + + public Task InitializeAsync() + { + var uniqueKeyPolicy = new UniqueKeyPolicy { - _testCosmos?.Dispose(); - } + UniqueKeys = { new UniqueKey { Paths = { "/CustomerId", "/ItemId", "/Type" } } } + }; + + _testCosmos = new TestCosmos(); + return _testCosmos.SetupAsync("/partitionKey", uniqueKeyPolicy); + } + + public async Task DisposeAsync() + { + await _testCosmos.CleanupAsync(); + } + + public void Dispose() + { + _testCosmos?.Dispose(); } } \ No newline at end of file diff --git a/CosmosTestHelpers.IntegrationTests/Helpers/CosmosEquivalencyException.cs b/CosmosTestHelpers.IntegrationTests/Helpers/CosmosEquivalencyException.cs new file mode 100644 index 0000000..c846fad --- /dev/null +++ b/CosmosTestHelpers.IntegrationTests/Helpers/CosmosEquivalencyException.cs @@ -0,0 +1,63 @@ +using System.Text.Json; + +namespace CosmosTestHelpers.IntegrationTests; + +public sealed class CosmosEquivalencyException : Exception +{ + public Exception RealException { get; } + + public Exception TestException { get; } + + public object RealValue { get; } + + public object TestValue { get; } + + public CosmosEquivalencyException(Exception realException, Exception testException, object realValue, object testValue) + : base(FormatMessage(realException, testException, realValue, testValue)) + { + RealException = realException; + TestException = testException; + RealValue = realValue; + TestValue = testValue; + } + + private static string FormatMessage(Exception realException, Exception testException, object realValue, object testValue) + { + var message = "An error occurred executing query on"; + + if (realException != null && testException != null) + { + message += " both Test and Real CosmosDb\n"; + } + else if (realException != null) + { + message += " only Real CosmosDb\n"; + } + else if (testException != null) + { + message += " only Test CosmosDb\n"; + } + + if (realException != null) + { + message += "the real exception's message is: " + realException.Message + "\n"; + } + + if (testException != null) + { + message += "the test exception's message is: " + testException.Message + "\n"; + } + + if (realValue != null) + { + message += "the real value is: " + JsonSerializer.Serialize(realValue) + "\n"; + } + + if (testValue != null) + { + message += "the test value is: " + JsonSerializer.Serialize(testValue) + "\n"; + } + + return message; + } +} \ No newline at end of file diff --git a/CosmosTestHelpers.IntegrationTests/Helpers/IAsyncEnumerableExtensions.cs b/CosmosTestHelpers.IntegrationTests/Helpers/IAsyncEnumerableExtensions.cs new file mode 100644 index 0000000..6aabc09 --- /dev/null +++ b/CosmosTestHelpers.IntegrationTests/Helpers/IAsyncEnumerableExtensions.cs @@ -0,0 +1,15 @@ +namespace CosmosTestHelpers.IntegrationTests; + +internal static class AsyncEnumerableExtensions +{ + public static async Task> ToListAsync(this IAsyncEnumerable enumerable) + { + var result = new List(); + await foreach (var item in enumerable) + { + result.Add(item); + } + + return result; + } +} \ No newline at end of file diff --git a/CosmosTestHelpers.IntegrationTests/Helpers/RetryPolicy.cs b/CosmosTestHelpers.IntegrationTests/Helpers/RetryPolicy.cs new file mode 100644 index 0000000..4bb3afe --- /dev/null +++ b/CosmosTestHelpers.IntegrationTests/Helpers/RetryPolicy.cs @@ -0,0 +1,71 @@ +using System.Net; +using Microsoft.Azure.Cosmos; +using Microsoft.Extensions.Logging; +using Polly; +using Polly.Contrib.WaitAndRetry; + +namespace CosmosTestHelpers.IntegrationTests; + +public class RetryPolicy //: ICosmosRetryPolicy +{ + private static readonly IEnumerable BackOffPolicy = Backoff.ExponentialBackoff(TimeSpan.FromMilliseconds(500), retryCount: 5, fastFirst: true); + + private readonly ILogger _logger; + + public RetryPolicy(ILogger logger) + { + _logger = logger; + } + + public async Task CreateAndExecutePolicyAsync(string actionName, Func> action) + { + var policy = Polly.Policy + .Handle( + exception => exception.StatusCode == HttpStatusCode.ServiceUnavailable || + exception.StatusCode == HttpStatusCode.RequestTimeout + ) + .WaitAndRetryAsync(BackOffPolicy, (exception, timeSpan, retryCount, context) => + { + if (!(exception is CosmosException)) return; + + var cosmosException = (CosmosException)exception; + _logger.LogWarning( + "Failed to {actionName} on retry {retryCount} due to {exceptionType} {statusCode} - {exceptionMessage}. Attempting again.", actionName, retryCount, nameof(CosmosException), cosmosException.StatusCode, cosmosException.Message); + }); + + var result = await policy.ExecuteAndCaptureAsync(action); + + if (result.Outcome == OutcomeType.Successful) + { + return result.Result; + } + + throw result.FinalException; + } + + public T CreateAndExecutePolicy(string actionName, Func action) + { + var policy = Polly.Policy + .Handle( + exception => exception.StatusCode == HttpStatusCode.ServiceUnavailable || + exception.StatusCode == HttpStatusCode.RequestTimeout + ) + .WaitAndRetry(5, i => TimeSpan.FromSeconds(5), (exception, timeSpan, retryCount, context) => + { + if (!(exception is CosmosException)) return; + + var cosmosException = (CosmosException)exception; + _logger.LogWarning( + "Failed to {actionName} on retry {retryCount} due to {exceptionType} {statusCode} - {exceptionMessage}. Attempting again.", actionName, retryCount, nameof(CosmosException), cosmosException.StatusCode, cosmosException.Message); + }); + + var result = policy.ExecuteAndCapture(action); + + if (result.Outcome == OutcomeType.Successful) + { + return result.Result; + } + + throw result.FinalException; + } +} \ No newline at end of file diff --git a/CosmosTestHelpers.IntegrationTests/ShimExtensions.cs b/CosmosTestHelpers.IntegrationTests/Helpers/ShimExtensions.cs similarity index 100% rename from CosmosTestHelpers.IntegrationTests/ShimExtensions.cs rename to CosmosTestHelpers.IntegrationTests/Helpers/ShimExtensions.cs diff --git a/CosmosTestHelpers.IntegrationTests/Helpers/TestCosmos.cs b/CosmosTestHelpers.IntegrationTests/Helpers/TestCosmos.cs new file mode 100644 index 0000000..6002563 --- /dev/null +++ b/CosmosTestHelpers.IntegrationTests/Helpers/TestCosmos.cs @@ -0,0 +1,509 @@ +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.Net; +using Microsoft.Azure.Cosmos; + +namespace CosmosTestHelpers.IntegrationTests; + +public sealed class TestCosmos : IDisposable +{ + private readonly CosmosClient _client; + + private Container _testContainer; + + private Container _realContainer; + + public TestCosmos() + { + var testConnectionString = Environment.GetEnvironmentVariable("TEST_COSMOS_CONNECTION_STRING"); + if (string.IsNullOrWhiteSpace(testConnectionString)) + { + throw new Exception("TEST_COSMOS_CONNECTION_STRING environment variable must be set"); + } + _client = new CosmosClient(testConnectionString); + } + + public async Task SetupAsync(string partitionKeyPath, UniqueKeyPolicy uniqueKeyPolicy = null) + { + var response = await CreateCosmosContainer(partitionKeyPath, uniqueKeyPolicy); + _realContainer = response.Container; + _testContainer = new ContainerMock(partitionKeyPath, uniqueKeyPolicy); + } + + public async Task<(CosmosException realException, CosmosException testException)> SetupAsyncProducesExceptions(string partitionKeyPath, UniqueKeyPolicy uniqueKeyPolicy = null) + { + CosmosException realException = null, testException = null; + + try + { + var response = await CreateCosmosContainer(partitionKeyPath, uniqueKeyPolicy); + _realContainer = response.Container; + } + catch (CosmosException ex) + { + realException = ex; + } + + try + { + _testContainer = new ContainerMock(partitionKeyPath, uniqueKeyPolicy); + } + catch (CosmosException ex) + { + testException = ex; + } + + return (realException, testException); + } + + public async Task<(string testETag, string realETag)> GivenAnExistingItem(T item) + { + var realItem = await _realContainer.UpsertItemAsync(item); + var testItem = await _testContainer.UpsertItemAsync(item); + + return (testItem.ETag, realItem.ETag); + } + + [SuppressMessage("", "CA1031", Justification = "Need to catch exceptions")] + public async Task<(IList realResults, IList testResults)> WhenExecutingAQuery(string partitionKey, Func, IQueryable> query = null) + { + Exception realException = null, testException = null; + + IList realQuery = null, inMemoryQuery = null; + + try + { + if (query != null) + { + realQuery = await _realContainer.QueryAsync(partitionKey, query).ToListAsync(); + } + else + { + realQuery = await _realContainer.QueryAsync(partitionKey).ToListAsync(); + } + } + catch (Exception ex) + { + realException = ex; + } + + try + { + if (query != null) + { + inMemoryQuery = await _testContainer.QueryAsync(partitionKey, query).ToListAsync(); + } + else + { + inMemoryQuery = await _testContainer.QueryAsync(partitionKey).ToListAsync(); + } + } + catch (Exception ex) + { + testException = ex; + } + + if (realException != null || testException != null) + { + throw new CosmosEquivalencyException(realException, testException, realQuery, inMemoryQuery); + } + + return (realQuery, inMemoryQuery); + } + + [SuppressMessage("", "CA1031", Justification = "Need to catch exceptions")] + public async Task<(int? realResults, int? testResults)> WhenCountingAQuery(string partitionKey, Func, IQueryable> query = null) + { + Exception realException = null, testException = null; + + int? realQuery = null, inMemoryQuery = null; + + try + { + if (query != null) + { + realQuery = await _realContainer.CountAsync(partitionKey, query); + } + else + { + realQuery = await _realContainer.CountAsync(partitionKey); + } + } + catch (Exception ex) + { + realException = ex; + } + + try + { + if (query != null) + { + inMemoryQuery = await _testContainer.CountAsync(partitionKey, query); + } + else + { + inMemoryQuery = await _testContainer.CountAsync(partitionKey); + } + } + catch (Exception ex) + { + testException = ex; + } + + if (realException != null || testException != null) + { + throw new CosmosEquivalencyException(realException, testException, realQuery, inMemoryQuery); + } + + return (realQuery, inMemoryQuery); + } + + [Obsolete("Testing old query method, until the CreateItemLinqQueryable method is removed")] + public (IQueryable realResults, IQueryable testResults) WhenExecutingAQuery(Func, IQueryable> query) + { + if (query == null) + { + throw new ArgumentNullException(nameof(query)); + } + + var realQueryable = _realContainer.GetItemLinqQueryable(true); + var realQuery = query(realQueryable); + + var inMemoryQueryable = _testContainer.GetItemLinqQueryable(); + var inMemoryQuery = query(inMemoryQueryable); + + return (realQuery, inMemoryQuery); + } + + [Obsolete("Testing old query method, until the CreateItemLinqQueryable method is removed")] + [SuppressMessage(null, "CA1031", Justification = "Need to know if any exception occurs")] + public (TResult realResults, Exception realException, TResult testResults, Exception testException) WhenExecutingAQuery(Func, TResult> query) + { + if (query == null) + { + throw new ArgumentNullException(nameof(query)); + } + + Exception realException = null, testException = null; + TResult realQuery = default, inMemoryQuery = default; + + try + { + var realQueryable = _realContainer.GetItemLinqQueryable(true); + realQuery = query(realQueryable); + } + catch (Exception ex) + { + realException = ex; + } + + try + { + var inMemoryQueryable = _testContainer.GetItemLinqQueryable(); + inMemoryQuery = query(inMemoryQueryable); + } + catch (Exception ex) + { + testException = ex; + } + + return (realQuery, realException, inMemoryQuery, testException); + } + + public async Task<(ItemResponse realResult, ItemResponse testResult)> WhenCreating( + T testModel, + PartitionKey? partitionKey = null, + ItemRequestOptions testRequestOptions = null, + ItemRequestOptions realRequestOptions = null) + { + var realResult = await _realContainer.CreateItemAsync(testModel, partitionKey, realRequestOptions); + var testResult = await _testContainer.CreateItemAsync(testModel, partitionKey, testRequestOptions); + return (realResult, testResult); + } + + public async Task<(CosmosException realException, CosmosException testException)> WhenCreatingProducesException( + T testModel, + PartitionKey? partitionKey = null, + ItemRequestOptions testRequestOptions = null, + ItemRequestOptions realRequestOptions = null) + { + CosmosException real = null; + CosmosException test = null; + + try + { + await _realContainer.CreateItemAsync(testModel, partitionKey, realRequestOptions); + } + catch (CosmosException exc) + { + real = exc; + } + + try + { + await _testContainer.CreateItemAsync(testModel, partitionKey, testRequestOptions); + } + catch (CosmosException exc) + { + test = exc; + } + + return (real, test); + } + + public async Task<(ItemResponse realResult, ItemResponse testResult)> WhenUpserting( + T testModel, + ItemRequestOptions testRequestOptions = null, + ItemRequestOptions realRequestOptions = null) + { + var realResult = await _realContainer.UpsertItemAsync(testModel, requestOptions: realRequestOptions); + var testResult = await _testContainer.UpsertItemAsync(testModel, requestOptions: testRequestOptions); + return (realResult, testResult); + } + + public async Task<(ItemResponse realResult, ItemResponse testResult)> WhenReplacing( + T testModel, + string id, + ItemRequestOptions testRequestOptions = null, + ItemRequestOptions realRequestOptions = null) + { + var realResult = await _realContainer.ReplaceItemAsync(testModel, id, requestOptions: realRequestOptions); + var testResult = await _testContainer.ReplaceItemAsync(testModel, id, requestOptions: testRequestOptions); + return (realResult, testResult); + } + + public async Task<(ItemResponse realResult, ItemResponse testResult)> WhenDeleting( + string id, + PartitionKey partitionKey, + ItemRequestOptions testRequestOptions = null, + ItemRequestOptions realRequestOptions = null) + { + var realResult = await _realContainer.DeleteItemAsync(id, partitionKey, realRequestOptions); + var testResult = await _testContainer.DeleteItemAsync(id, partitionKey, testRequestOptions); + return (realResult, testResult); + } + + public async Task<(ResponseMessage realResult, ResponseMessage testResult)> WhenUpsertingStream( + Stream stream, + PartitionKey partitionKey, + ItemRequestOptions testRequestOptions = null, + ItemRequestOptions realRequestOptions = null) + { + var realResult = await _realContainer.UpsertItemStreamAsync(stream, partitionKey, realRequestOptions); + var testResult = await _testContainer.UpsertItemStreamAsync(stream, partitionKey, testRequestOptions); + return (realResult, testResult); + } + + public async Task<(ResponseMessage realResult, ResponseMessage testResult)> WhenCreatingStream(MemoryStream stream, PartitionKey partitionKey) + { + var realResult = await _realContainer.CreateItemStreamAsync(stream, partitionKey); + var testResult = await _testContainer.CreateItemStreamAsync(stream, partitionKey); + return (realResult, testResult); + } + + public async Task<(CosmosException realException, CosmosException testException)> WhenUpsertingProducesException( + T testModel, + ItemRequestOptions testRequestOptions = null, + ItemRequestOptions realRequestOptions = null) + { + CosmosException real = null; + CosmosException test = null; + + try + { + await _realContainer.UpsertItemAsync(testModel, requestOptions: realRequestOptions); + } + catch (CosmosException exc) + { + real = exc; + } + + try + { + await _testContainer.UpsertItemAsync(testModel, requestOptions: testRequestOptions); + } + catch (CosmosException exc) + { + test = exc; + } + + return (real, test); + } + + public async Task<(CosmosException realException, CosmosException testException)> WhenReplacingProducesException( + T testModel, + string id, + ItemRequestOptions testRequestOptions = null, + ItemRequestOptions realRequestOptions = null) + { + CosmosException real = null; + CosmosException test = null; + + try + { + await _realContainer.ReplaceItemAsync(testModel, id, requestOptions: realRequestOptions); + } + catch (CosmosException exc) + { + real = exc; + } + + try + { + await _testContainer.ReplaceItemAsync(testModel, id, requestOptions: testRequestOptions); + } + catch (CosmosException exc) + { + test = exc; + } + + return (real, test); + } + + public async Task<(CosmosException realException, CosmosException testException)> WhenDeletingProducesException( + string id, + PartitionKey partitionKey, + ItemRequestOptions testRequestOptions = null, + ItemRequestOptions realRequestOptions = null) + { + CosmosException real = null; + CosmosException test = null; + + try + { + await _realContainer.DeleteItemAsync(id, partitionKey, realRequestOptions); + } + catch (CosmosException exc) + { + real = exc; + } + + try + { + await _testContainer.DeleteItemAsync(id, partitionKey, testRequestOptions); + } + catch (CosmosException exc) + { + test = exc; + } + + return (real, test); + } + + [SuppressMessage("", "CA1031", Justification = "I want to catch all exceptions.")] + public async Task<(Exception realException, Exception testException)> WhenReadItemProducesException(string id) + { + Exception real = null; + Exception test = null; + + try + { + await _realContainer.ReadItemAsync(id, PartitionKey.None); + } + catch (Exception exc) + { + real = exc; + } + + try + { + await _testContainer.ReadItemAsync(id, PartitionKey.None); + } + catch (Exception exc) + { + test = exc; + } + + return (real, test); + } + + [SuppressMessage("", "CA1031", Justification = "I want to catch all exceptions.")] + public async Task<(Exception realException, Exception testException)> WhenReadItemStreamProducesException(string id) + { + Exception real = null; + Exception test = null; + + try + { + await _realContainer.ReadItemStreamAsync(id, PartitionKey.None); + } + catch (Exception exc) + { + real = exc; + } + + try + { + await _testContainer.ReadItemStreamAsync(id, PartitionKey.None); + } + catch (Exception exc) + { + test = exc; + } + + return (real, test); + } + + public async Task<(ResponseMessage realException, ResponseMessage testException)> WhenReadItemStream(string id) + { + var real = await _realContainer.ReadItemStreamAsync(id, PartitionKey.None); + var test = await _testContainer.ReadItemStreamAsync(id, PartitionKey.None); + + return (real, test); + } + + public async Task CleanupAsync() + { + if (_realContainer == null) + { + return; + } + + await _realContainer.DeleteContainerAsync(); + } + + public void Dispose() + { + _client?.Dispose(); + } + + private async Task CreateCosmosContainer(string partitionKeyPath, UniqueKeyPolicy uniqueKeyPolicy) + { + var dbName = typeof(TestCosmos).Assembly.GetName().Name; + var database = (await _client.CreateDatabaseIfNotExistsAsync(dbName, throughput: null)).Database; + + var iterator = database.GetContainerQueryIterator(); + do + { + foreach (var container in await iterator.ReadNextAsync()) + { + if (container.LastModified < DateTime.UtcNow.Subtract(TimeSpan.FromMinutes(30))) + { + try + { + await _client.GetContainer(dbName, container.Id).DeleteContainerAsync(); + } + catch (CosmosException cex) when (cex.StatusCode == HttpStatusCode.NotFound) + { + // Another test setup already did the delete in the time it took us to get it, so we don't need to do anything more + } + } + } + } + while (iterator.HasMoreResults); + + var containerProperties = new ContainerProperties + { + Id = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture), + PartitionKeyPath = partitionKeyPath, + }; + + if (uniqueKeyPolicy != null) + { + containerProperties.UniqueKeyPolicy = uniqueKeyPolicy; + } + + var response = await database.CreateContainerAsync(containerProperties); + return response; + } +} \ No newline at end of file diff --git a/CosmosTestHelpers.IntegrationTests/IAsyncEnumerableExtensions.cs b/CosmosTestHelpers.IntegrationTests/IAsyncEnumerableExtensions.cs deleted file mode 100644 index 9a00595..0000000 --- a/CosmosTestHelpers.IntegrationTests/IAsyncEnumerableExtensions.cs +++ /dev/null @@ -1,88 +0,0 @@ -namespace CosmosTestHelpers.IntegrationTests -{ - internal static class AsyncEnumerableExtensions - { - public static async Task FirstAsync(this IAsyncEnumerable enumerable) - { - await foreach (var item in enumerable) - { - return item; - } - - throw new InvalidOperationException("FirstAsync was called on a collection with zero elements"); - } - - public static async Task AnyAsync(this IAsyncEnumerable enumerable) - { - await foreach (var unused in enumerable) - { - return true; - } - - return false; - } - - public static async Task FirstOrDefaultAsync(this IAsyncEnumerable enumerable) - { - await foreach (var item in enumerable) - { - return item; - } - - return default(T); - } - - public static async Task SingleAsync(this IAsyncEnumerable enumerable) - { - var result = default(T); - var found = false; - await foreach (var item in enumerable) - { - if (found) - { - throw new InvalidOperationException("SingleSync called on a collection with more than one element"); - } - - found = true; - result = item; - } - - if (!found) - { - - throw new InvalidOperationException("SingleAsync was called on a collection with zero elements"); - } - - return result; - } - - public static async Task SingleOrDefaultAsync(this IAsyncEnumerable enumerable) - { - var result = default(T); - var found = false; - await foreach (var item in enumerable) - { - if (found) - { - throw new InvalidOperationException("SingleSync called on a collection with more than one element"); - } - - found = true; - result = item; - } - - return result; - } - - public static async Task> ToListAsync(this IAsyncEnumerable enumerable) - { - var result = new List(); - await foreach (var item in enumerable) - { - result.Add(item); - } - - return result; - } - } -} \ No newline at end of file diff --git a/CosmosTestHelpers.IntegrationTests/RetryPolicy.cs b/CosmosTestHelpers.IntegrationTests/RetryPolicy.cs deleted file mode 100644 index 06e1e06..0000000 --- a/CosmosTestHelpers.IntegrationTests/RetryPolicy.cs +++ /dev/null @@ -1,72 +0,0 @@ -using System.Net; -using Microsoft.Azure.Cosmos; -using Microsoft.Extensions.Logging; -using Polly; -using Polly.Contrib.WaitAndRetry; - -namespace CosmosTestHelpers.IntegrationTests -{ - public class RetryPolicy //: ICosmosRetryPolicy - { - private static readonly IEnumerable BackOffPolicy = Backoff.ExponentialBackoff(TimeSpan.FromMilliseconds(500), retryCount: 5, fastFirst: true); - - private readonly ILogger _logger; - - public RetryPolicy(ILogger logger) - { - _logger = logger; - } - - public async Task CreateAndExecutePolicyAsync(string actionName, Func> action) - { - var policy = Polly.Policy - .Handle( - exception => exception.StatusCode == HttpStatusCode.ServiceUnavailable || - exception.StatusCode == HttpStatusCode.RequestTimeout - ) - .WaitAndRetryAsync(BackOffPolicy, (exception, timeSpan, retryCount, context) => - { - if (!(exception is CosmosException)) return; - - var cosmosException = (CosmosException)exception; - _logger.LogWarning( - "Failed to {actionName} on retry {retryCount} due to {exceptionType} {statusCode} - {exceptionMessage}. Attempting again.", actionName, retryCount, nameof(CosmosException), cosmosException.StatusCode, cosmosException.Message); - }); - - var result = await policy.ExecuteAndCaptureAsync(action); - - if (result.Outcome == OutcomeType.Successful) - { - return result.Result; - } - - throw result.FinalException; - } - - public T CreateAndExecutePolicy(string actionName, Func action) - { - var policy = Polly.Policy - .Handle( - exception => exception.StatusCode == HttpStatusCode.ServiceUnavailable || - exception.StatusCode == HttpStatusCode.RequestTimeout - ) - .WaitAndRetry(5, i => TimeSpan.FromSeconds(5), (exception, timeSpan, retryCount, context) => - { - if (!(exception is CosmosException)) return; - - var cosmosException = (CosmosException)exception; - _logger.LogWarning( - "Failed to {actionName} on retry {retryCount} due to {exceptionType} {statusCode} - {exceptionMessage}. Attempting again.", actionName, retryCount, nameof(CosmosException), cosmosException.StatusCode, cosmosException.Message); - }); - - var result = policy.ExecuteAndCapture(action); - - if (result.Outcome == OutcomeType.Successful) - { - return result.Result; - } - - throw result.FinalException; - } - } -} diff --git a/CosmosTestHelpers.IntegrationTests/TestCosmos.cs b/CosmosTestHelpers.IntegrationTests/TestCosmos.cs deleted file mode 100644 index 6c91444..0000000 --- a/CosmosTestHelpers.IntegrationTests/TestCosmos.cs +++ /dev/null @@ -1,589 +0,0 @@ -using System.Diagnostics.CodeAnalysis; -using System.Globalization; -using System.Net; -using CosmosTestHelpers.IntegrationTests.TestModels; -using Microsoft.Azure.Cosmos; - -namespace CosmosTestHelpers.IntegrationTests -{ - public sealed class TestCosmos : IDisposable - { - private readonly CosmosClient _client; - - private Container _testContainer; - - private Container _container; - - public TestCosmos() - { - var testConnectionString = Environment.GetEnvironmentVariable("TEST_COSMOS_CONNECTION_STRING"); - if (string.IsNullOrWhiteSpace(testConnectionString)) - { - throw new Exception("TEST_COSMOS_CONNECTION_STRING environment variable must be set"); - } - _client = new CosmosClient(testConnectionString); - } - - public async Task SetupAsync(string partitionKeyPath, UniqueKeyPolicy uniqueKeyPolicy = null) - { - var response = await CreateCosmosContainer(partitionKeyPath, uniqueKeyPolicy); - _container = response.Container; - _testContainer = new ContainerMock(partitionKeyPath, uniqueKeyPolicy); - } - - public async Task<(CosmosException realException, CosmosException testException)> SetupAsyncProducesExceptions(string partitionKeyPath, UniqueKeyPolicy uniqueKeyPolicy = null) - { - CosmosException realException = null, testException = null; - - try - { - var response = await CreateCosmosContainer(partitionKeyPath, uniqueKeyPolicy); - _container = response.Container; - } - catch (CosmosException ex) - { - realException = ex; - } - - try - { - _testContainer = new ContainerMock(partitionKeyPath, uniqueKeyPolicy); - } - catch (CosmosException ex) - { - testException = ex; - } - - return (realException, testException); - } - - public async Task<(string testETag, string realETag)> GivenAnExistingItem(T item) - { - var realItem = await _container.UpsertItemAsync(item); - var testItem = await _testContainer.UpsertItemAsync(item); - - return (testItem.ETag, realItem.ETag); - } - - [SuppressMessage("", "CA1031", Justification = "Need to catch exceptions")] - public async Task<(IList realResults, IList testResults)> WhenExecutingAQuery(string partitionKey, Func, IQueryable> query = null) - { - Exception realException = null, testException = null; - - IList realQuery = null, inMemoryQuery = null; - - try - { - if (query != null) - { - realQuery = await _container.QueryAsync(partitionKey, query).ToListAsync(); - } - else - { - realQuery = await _container.QueryAsync(partitionKey).ToListAsync(); - } - } - catch (Exception ex) - { - realException = ex; - } - - try - { - if (query != null) - { - inMemoryQuery = await _testContainer.QueryAsync(partitionKey, query).ToListAsync(); - } - else - { - inMemoryQuery = await _testContainer.QueryAsync(partitionKey).ToListAsync(); - } - } - catch (Exception ex) - { - testException = ex; - } - - if (realException != null || testException != null) - { - throw new CosmosEquivalencyException(realException, testException, realQuery, inMemoryQuery); - } - - return (realQuery, inMemoryQuery); - } - - [SuppressMessage("", "CA1031", Justification = "Need to catch exceptions")] - public async Task<(int? realResults, int? testResults)> WhenCountingAQuery(string partitionKey, Func, IQueryable> query = null) - { - Exception realException = null, testException = null; - - int? realQuery = null, inMemoryQuery = null; - - try - { - if (query != null) - { - realQuery = await _container.CountAsync(partitionKey, query); - } - else - { - realQuery = await _container.CountAsync(partitionKey); - } - } - catch (Exception ex) - { - realException = ex; - } - - try - { - if (query != null) - { - inMemoryQuery = await _testContainer.CountAsync(partitionKey, query); - } - else - { - inMemoryQuery = await _testContainer.CountAsync(partitionKey); - } - } - catch (Exception ex) - { - testException = ex; - } - - if (realException != null || testException != null) - { - throw new CosmosEquivalencyException(realException, testException, realQuery, inMemoryQuery); - } - - return (realQuery, inMemoryQuery); - } - - [Obsolete("Testing old query method, until the CreateItemLinqQueryable method is removed")] - public (IQueryable realResults, IQueryable testResults) WhenExecutingAQuery(Func, IQueryable> query) - { - if (query == null) - { - throw new ArgumentNullException(nameof(query)); - } - - var realQueryable = _container.GetItemLinqQueryable(true); - var realQuery = query(realQueryable); - - var inMemoryQueryable = _testContainer.GetItemLinqQueryable(); - var inMemoryQuery = query(inMemoryQueryable); - - return (realQuery, inMemoryQuery); - } - - [Obsolete("Testing old query method, until the CreateItemLinqQueryable method is removed")] - [SuppressMessage(null, "CA1031", Justification = "Need to know if any exception occurs")] - public (TResult realResults, Exception realException, TResult testResults, Exception testException) WhenExecutingAQuery(Func, TResult> query) - { - if (query == null) - { - throw new ArgumentNullException(nameof(query)); - } - - Exception realException = null, testException = null; - TResult realQuery = default, inMemoryQuery = default; - - try - { - var realQueryable = _container.GetItemLinqQueryable(true); - realQuery = query(realQueryable); - } - catch (Exception ex) - { - realException = ex; - } - - try - { - var inMemoryQueryable = _testContainer.GetItemLinqQueryable(); - inMemoryQuery = query(inMemoryQueryable); - } - catch (Exception ex) - { - testException = ex; - } - - return (realQuery, realException, inMemoryQuery, testException); - } - - public async Task<(ItemResponse realResult, ItemResponse testResult)> WhenCreating( - TestModel testModel, - PartitionKey partitionKey, - ItemRequestOptions testRequestOptions = null, - ItemRequestOptions realRequestOptions = null) - { - var realResult = await _container.CreateItemAsync(testModel, partitionKey, realRequestOptions); - var testResult = await _testContainer.CreateItemAsync(testModel, partitionKey, testRequestOptions); - return (realResult, testResult); - } - - public async Task<(ItemResponse realResult, ItemResponse testResult)> WhenCreatingTripleKeyModel( - TripleUniqueKeyModel testModel, - ItemRequestOptions testRequestOptions = null, - ItemRequestOptions realRequestOptions = null) - { - var realResult = await _container.CreateItemAsync(testModel, requestOptions: realRequestOptions); - var testResult = await _testContainer.CreateItemAsync(testModel, requestOptions: testRequestOptions); - return (realResult, testResult); - } - - public async Task<(CosmosException realException, CosmosException testException)> WhenCreatingProducesException( - TestModel testModel, - PartitionKey partitionKey, - ItemRequestOptions testRequestOptions = null, - ItemRequestOptions realRequestOptions = null) - { - CosmosException real = null; - CosmosException test = null; - - try - { - await _container.CreateItemAsync(testModel, partitionKey, realRequestOptions); - } - catch (CosmosException exc) - { - real = exc; - } - - try - { - await _testContainer.CreateItemAsync(testModel, partitionKey, testRequestOptions); - } - catch (CosmosException exc) - { - test = exc; - } - - return (real, test); - } - - public async Task<(CosmosException realException, CosmosException testException)> WhenCreatingTripleKeyModelProducesException( - TripleUniqueKeyModel testModel, - ItemRequestOptions testRequestOptions = null, - ItemRequestOptions realRequestOptions = null) - { - CosmosException real = null; - CosmosException test = null; - - try - { - await _container.CreateItemAsync(testModel, requestOptions: realRequestOptions); - } - catch (CosmosException exc) - { - real = exc; - } - - try - { - await _testContainer.CreateItemAsync(testModel, requestOptions: testRequestOptions); - } - catch (CosmosException exc) - { - test = exc; - } - - return (real, test); - } - - public async Task<(ItemResponse realResult, ItemResponse testResult)> WhenUpserting( - TestModel testModel, - ItemRequestOptions testRequestOptions = null, - ItemRequestOptions realRequestOptions = null) - { - var realResult = await _container.UpsertItemAsync(testModel, requestOptions: realRequestOptions); - var testResult = await _testContainer.UpsertItemAsync(testModel, requestOptions: testRequestOptions); - return (realResult, testResult); - } - - public async Task<(ItemResponse realResult, ItemResponse testResult)> WhenUpsertingTripleKeyModel( - TripleUniqueKeyModel testModel, - ItemRequestOptions testRequestOptions = null, - ItemRequestOptions realRequestOptions = null) - { - var realResult = await _container.UpsertItemAsync(testModel, requestOptions: realRequestOptions); - var testResult = await _testContainer.UpsertItemAsync(testModel, requestOptions: testRequestOptions); - return (realResult, testResult); - } - - public async Task<(ItemResponse realResult, ItemResponse testResult)> WhenReplacing( - TestModel testModel, - string id, - ItemRequestOptions testRequestOptions = null, - ItemRequestOptions realRequestOptions = null) - { - var realResult = await _container.ReplaceItemAsync(testModel, id, requestOptions: realRequestOptions); - var testResult = await _testContainer.ReplaceItemAsync(testModel, id, requestOptions: testRequestOptions); - return (realResult, testResult); - } - - public async Task<(ItemResponse realResult, ItemResponse testResult)> WhenDeleting( - string id, - PartitionKey partitionKey, - ItemRequestOptions testRequestOptions = null, - ItemRequestOptions realRequestOptions = null) - { - var realResult = await _container.DeleteItemAsync(id, partitionKey, realRequestOptions); - var testResult = await _testContainer.DeleteItemAsync(id, partitionKey, testRequestOptions); - return (realResult, testResult); - } - - public async Task<(ResponseMessage realResult, ResponseMessage testResult)> WhenUpsertingStream( - Stream stream, - PartitionKey partitionKey, - ItemRequestOptions testRequestOptions = null, - ItemRequestOptions realRequestOptions = null) - { - var realResult = await _container.UpsertItemStreamAsync(stream, partitionKey, realRequestOptions); - var testResult = await _testContainer.UpsertItemStreamAsync(stream, partitionKey, testRequestOptions); - return (realResult, testResult); - } - - public async Task<(ResponseMessage realResult, ResponseMessage testResult)> WhenCreatingStream(MemoryStream stream, PartitionKey partitionKey) - { - var realResult = await _container.CreateItemStreamAsync(stream, partitionKey); - var testResult = await _testContainer.CreateItemStreamAsync(stream, partitionKey); - return (realResult, testResult); - } - - public async Task<(CosmosException realException, CosmosException testException)> WhenUpsertingProducesException( - TestModel testModel, - ItemRequestOptions testRequestOptions = null, - ItemRequestOptions realRequestOptions = null) - { - CosmosException real = null; - CosmosException test = null; - - try - { - await _container.UpsertItemAsync(testModel, requestOptions: realRequestOptions); - } - catch (CosmosException exc) - { - real = exc; - } - - try - { - await _testContainer.UpsertItemAsync(testModel, requestOptions: testRequestOptions); - } - catch (CosmosException exc) - { - test = exc; - } - - return (real, test); - } - - public async Task<(CosmosException realException, CosmosException testException)> WhenUpsertingTripleKeyModelProducesException( - TripleUniqueKeyModel testModel, - ItemRequestOptions testRequestOptions = null, - ItemRequestOptions realRequestOptions = null) - { - CosmosException real = null; - CosmosException test = null; - - try - { - await _container.UpsertItemAsync(testModel, requestOptions: realRequestOptions); - } - catch (CosmosException exc) - { - real = exc; - } - - try - { - await _testContainer.UpsertItemAsync(testModel, requestOptions: testRequestOptions); - } - catch (CosmosException exc) - { - test = exc; - } - - return (real, test); - } - - public async Task<(CosmosException realException, CosmosException testException)> WhenReplacingProducesException( - TestModel testModel, - string id, - ItemRequestOptions testRequestOptions = null, - ItemRequestOptions realRequestOptions = null) - { - CosmosException real = null; - CosmosException test = null; - - try - { - await _container.ReplaceItemAsync(testModel, id, requestOptions: realRequestOptions); - } - catch (CosmosException exc) - { - real = exc; - } - - try - { - await _testContainer.ReplaceItemAsync(testModel, id, requestOptions: testRequestOptions); - } - catch (CosmosException exc) - { - test = exc; - } - - return (real, test); - } - - public async Task<(CosmosException realException, CosmosException testException)> WhenDeletingProducesException( - string id, - PartitionKey partitionKey, - ItemRequestOptions testRequestOptions = null, - ItemRequestOptions realRequestOptions = null) - { - CosmosException real = null; - CosmosException test = null; - - try - { - await _container.DeleteItemAsync(id, partitionKey, realRequestOptions); - } - catch (CosmosException exc) - { - real = exc; - } - - try - { - await _testContainer.DeleteItemAsync(id, partitionKey, testRequestOptions); - } - catch (CosmosException exc) - { - test = exc; - } - - return (real, test); - } - - [SuppressMessage("", "CA1031", Justification = "I want to catch all exceptions.")] - public async Task<(Exception realException, Exception testException)> WhenReadItemProducesException(string id) - { - Exception real = null; - Exception test = null; - - try - { - await _container.ReadItemAsync(id, PartitionKey.None); - } - catch (Exception exc) - { - real = exc; - } - - try - { - await _testContainer.ReadItemAsync(id, PartitionKey.None); - } - catch (Exception exc) - { - test = exc; - } - - return (real, test); - } - - [SuppressMessage("", "CA1031", Justification = "I want to catch all exceptions.")] - public async Task<(Exception realException, Exception testException)> WhenReadItemStreamProducesException(string id) - { - Exception real = null; - Exception test = null; - - try - { - await _container.ReadItemStreamAsync(id, PartitionKey.None); - } - catch (Exception exc) - { - real = exc; - } - - try - { - await _testContainer.ReadItemStreamAsync(id, PartitionKey.None); - } - catch (Exception exc) - { - test = exc; - } - - return (real, test); - } - - public async Task<(ResponseMessage realException, ResponseMessage testException)> WhenReadItemStream(string id) - { - var real = await _container.ReadItemStreamAsync(id, PartitionKey.None); - var test = await _testContainer.ReadItemStreamAsync(id, PartitionKey.None); - - return (real, test); - } - - public async Task CleanupAsync() - { - if (_container == null) - { - return; - } - - await _container.DeleteContainerAsync(); - } - - public void Dispose() - { - _client?.Dispose(); - } - - private async Task CreateCosmosContainer(string partitionKeyPath, UniqueKeyPolicy uniqueKeyPolicy) - { - var dbName = typeof(TestCosmos).Assembly.GetName().Name; - var database = (await _client.CreateDatabaseIfNotExistsAsync(dbName, throughput: null)).Database; - - var iterator = database.GetContainerQueryIterator(); - do - { - foreach (var container in await iterator.ReadNextAsync()) - { - if (container.LastModified < DateTime.UtcNow.Subtract(TimeSpan.FromMinutes(30))) - { - try - { - await _client.GetContainer(dbName, container.Id).DeleteContainerAsync(); - } - catch (CosmosException cex) when (cex.StatusCode == HttpStatusCode.NotFound) - { - // Another test setup already did the delete in the time it took us to get it, so we don't need to do anything more - } - } - } - } - while (iterator.HasMoreResults); - - var containerProperties = new ContainerProperties - { - Id = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture), - PartitionKeyPath = partitionKeyPath, - }; - - if (uniqueKeyPolicy != null) - { - containerProperties.UniqueKeyPolicy = uniqueKeyPolicy; - } - - var response = await database.CreateContainerAsync(containerProperties); - return response; - } - } -} \ No newline at end of file diff --git a/CosmosTestHelpers.IntegrationTests/TestModels/SubModel.cs b/CosmosTestHelpers.IntegrationTests/TestModels/SubModel.cs index c1957fb..872d981 100644 --- a/CosmosTestHelpers.IntegrationTests/TestModels/SubModel.cs +++ b/CosmosTestHelpers.IntegrationTests/TestModels/SubModel.cs @@ -1,13 +1,12 @@ using Newtonsoft.Json; using Newtonsoft.Json.Converters; -namespace CosmosTestHelpers.IntegrationTests.TestModels +namespace CosmosTestHelpers.IntegrationTests.TestModels; + +public class SubModel { - public class SubModel - { - public string Value { get; set; } + public string Value { get; set; } - [JsonConverter(typeof(StringEnumConverter))] - public TestEnum? NullableEnum { get; set; } - } + [JsonConverter(typeof(StringEnumConverter))] + public TestEnum? NullableEnum { get; set; } } \ No newline at end of file diff --git a/CosmosTestHelpers.IntegrationTests/TestModels/TestEnum.cs b/CosmosTestHelpers.IntegrationTests/TestModels/TestEnum.cs index 752a36e..7d8e99f 100644 --- a/CosmosTestHelpers.IntegrationTests/TestModels/TestEnum.cs +++ b/CosmosTestHelpers.IntegrationTests/TestModels/TestEnum.cs @@ -1,9 +1,8 @@ -namespace CosmosTestHelpers.IntegrationTests.TestModels +namespace CosmosTestHelpers.IntegrationTests.TestModels; + +public enum TestEnum { - public enum TestEnum - { - Value1, + Value1, - Value2 - } + Value2 } \ No newline at end of file diff --git a/CosmosTestHelpers.IntegrationTests/TestModels/TestModel.cs b/CosmosTestHelpers.IntegrationTests/TestModels/TestModel.cs index 9b19d6f..035f0da 100644 --- a/CosmosTestHelpers.IntegrationTests/TestModels/TestModel.cs +++ b/CosmosTestHelpers.IntegrationTests/TestModels/TestModel.cs @@ -1,35 +1,34 @@ using Newtonsoft.Json; using Newtonsoft.Json.Converters; -namespace CosmosTestHelpers.IntegrationTests.TestModels +namespace CosmosTestHelpers.IntegrationTests.TestModels; + +public class TestModel { - public class TestModel - { - [JsonProperty("id")] - public string Id { get; set; } + [JsonProperty("id")] + public string Id { get; set; } - public string Name { get; set; } + public string Name { get; set; } - public bool Value { get; set; } + public bool Value { get; set; } - [JsonConverter(typeof(StringEnumConverter))] - public TestEnum EnumValue { get; set; } + [JsonConverter(typeof(StringEnumConverter))] + public TestEnum EnumValue { get; set; } - [JsonConverter(typeof(StringEnumConverter))] - public TestEnum? NullableEnum { get; set; } + [JsonConverter(typeof(StringEnumConverter))] + public TestEnum? NullableEnum { get; set; } - public TestEnum? NullableEnumNotString { get; set; } + public TestEnum? NullableEnumNotString { get; set; } - [JsonProperty("partitionKey")] - public string PartitionKey { get; set; } + [JsonProperty("partitionKey")] + public string PartitionKey { get; set; } - public IEnumerable Children { get; set; } + public IEnumerable Children { get; set; } - public SubModel OnlyChild { get; set; } + public SubModel OnlyChild { get; set; } - public bool GetBoolValue() - { - return Value; - } + public bool GetBoolValue() + { + return Value; } } \ No newline at end of file diff --git a/CosmosTestHelpers.IntegrationTests/TestModels/TripleUniqueKeyModel.cs b/CosmosTestHelpers.IntegrationTests/TestModels/TripleUniqueKeyModel.cs index 94dff01..dbe3cb6 100644 --- a/CosmosTestHelpers.IntegrationTests/TestModels/TripleUniqueKeyModel.cs +++ b/CosmosTestHelpers.IntegrationTests/TestModels/TripleUniqueKeyModel.cs @@ -1,23 +1,22 @@ using Newtonsoft.Json; using Newtonsoft.Json.Converters; -namespace CosmosTestHelpers.IntegrationTests.TestModels +namespace CosmosTestHelpers.IntegrationTests.TestModels; + +public class TripleUniqueKeyModel { - public class TripleUniqueKeyModel - { - [JsonProperty("id")] - public Guid Id { get; set; } + [JsonProperty("id")] + public Guid Id { get; set; } - [JsonProperty("partitionKey")] - public string PartitionKey => CustomerId; + [JsonProperty("partitionKey")] + public string PartitionKey => CustomerId; - public string CustomerId { get; set; } + public string CustomerId { get; set; } - public string ItemId { get; set; } + public string ItemId { get; set; } - public string Value { get; set; } + public string Value { get; set; } - [JsonConverter(typeof(StringEnumConverter))] - public TestEnum Type { get; set; } - } -} + [JsonConverter(typeof(StringEnumConverter))] + public TestEnum Type { get; set; } +} \ No newline at end of file diff --git a/CosmosTestHelpers.Tests/ConcurrencyTests.cs b/CosmosTestHelpers.Tests/ConcurrencyTests.cs new file mode 100644 index 0000000..4cbf964 --- /dev/null +++ b/CosmosTestHelpers.Tests/ConcurrencyTests.cs @@ -0,0 +1,46 @@ +using System.Net; +using FluentAssertions; +using Microsoft.Azure.Cosmos; +using Newtonsoft.Json; +using Xunit; + +namespace CosmosTestHelpers.Tests; + +public class ConcurrencyTests +{ + [Fact] + public async Task CanSimulateAConcurrencyException() + { + var sut = new ContainerMock(partitionKeyPath: "/partitionKey"); + + await sut.UpsertItemAsync(new TestClass { Id = "MyId", PartitionKey = "APartition", MyValue = "Value1" }, new PartitionKey("APartition")); + sut.TheNextWriteToDocumentRequiresEtagAndWillRaiseAConcurrencyException(new PartitionKey("APartition"), "MyId"); + + var documentInDb = await sut.ReadItemAsync("MyId", new PartitionKey("APartition")); + + Func mustPassETag = () => sut.UpsertItemAsync(new TestClass { Id = "MyId", PartitionKey = "APartition", MyValue = "Value2" }, new PartitionKey("APartition")); + (await mustPassETag.Should().ThrowAsync()) + .Which.Message.Should().Be("An eTag must be provided as a concurrency exception is queued"); + + // First update should fail as if the document has been modified by another process + Func shouldPreConditionFail = () => sut.UpsertItemAsync(new TestClass { Id = "MyId", PartitionKey = "APartition", MyValue = "Value2" }, new PartitionKey("APartition"), requestOptions: new ItemRequestOptions { IfMatchEtag = documentInDb.ETag }); + (await shouldPreConditionFail.Should().ThrowAsync()) + .Which.StatusCode.Should().Be(HttpStatusCode.PreconditionFailed); + + // Second update should succeed if it has the correct etag + documentInDb = await sut.ReadItemAsync("MyId", new PartitionKey("APartition")); + var success = await sut.UpsertItemAsync(new TestClass { Id = "MyId", PartitionKey = "APartition", MyValue = "Value2" }, new PartitionKey("APartition"), requestOptions: new ItemRequestOptions { IfMatchEtag = documentInDb.ETag }); + success.StatusCode.Should().Be(HttpStatusCode.OK); + } + + private class TestClass + { + [JsonProperty("id")] + public string Id { get; set; } = null!; + + [JsonProperty("partitionKey")] + public string PartitionKey { get; set; } = null!; + + public string? MyValue { get; set; } + } +} \ No newline at end of file diff --git a/CosmosTestHelpers.Tests/ContainerMockTests.cs b/CosmosTestHelpers.Tests/ContainerMockTests.cs deleted file mode 100644 index aa2108e..0000000 --- a/CosmosTestHelpers.Tests/ContainerMockTests.cs +++ /dev/null @@ -1,47 +0,0 @@ -using System.Net; -using FluentAssertions; -using Microsoft.Azure.Cosmos; -using Newtonsoft.Json; -using Xunit; - -namespace CosmosTestHelpers.Tests -{ - public class ContainerMockTests - { - [Fact] - public async Task CanSimulateAConcurrencyException() - { - var sut = new ContainerMock(partitionKeyPath: "/partitionKey"); - - await sut.UpsertItemAsync(new TestClass { Id = "MyId", PartitionKey = "APartition", MyValue = "Value1" }, new PartitionKey("APartition")); - sut.TheNextWriteToDocumentRequiresEtagAndWillRaiseAConcurrencyException(new PartitionKey("APartition"), "MyId"); - - var documentInDb = await sut.ReadItemAsync("MyId", new PartitionKey("APartition")); - - Func mustPassETag = () => sut.UpsertItemAsync(new TestClass { Id = "MyId", PartitionKey = "APartition", MyValue = "Value2" }, new PartitionKey("APartition")); - (await mustPassETag.Should().ThrowAsync()) - .Which.Message.Should().Be("An eTag must be provided as a concurrency exception is queued"); - - // First update should fail as if the document has been modified by another process - Func shouldPreConditionFail = () => sut.UpsertItemAsync(new TestClass { Id = "MyId", PartitionKey = "APartition", MyValue = "Value2" }, new PartitionKey("APartition"), requestOptions: new ItemRequestOptions { IfMatchEtag = documentInDb.ETag }); - (await shouldPreConditionFail.Should().ThrowAsync()) - .Which.StatusCode.Should().Be(HttpStatusCode.PreconditionFailed); - - // Second update should succeed if it has the correct etag - documentInDb = await sut.ReadItemAsync("MyId", new PartitionKey("APartition")); - var success = await sut.UpsertItemAsync(new TestClass { Id = "MyId", PartitionKey = "APartition", MyValue = "Value2" }, new PartitionKey("APartition"), requestOptions: new ItemRequestOptions { IfMatchEtag = documentInDb.ETag }); - success.StatusCode.Should().Be(HttpStatusCode.OK); - } - - private class TestClass - { - [JsonProperty("id")] - public string Id { get; set; } - - [JsonProperty("partitionKey")] - public string PartitionKey { get; set; } - - public string MyValue { get; set; } - } - } -} \ No newline at end of file diff --git a/CosmosTestHelpers.Tests/IAsyncEnumerableExtensions.cs b/CosmosTestHelpers.Tests/IAsyncEnumerableExtensions.cs index 7614d15..13a0f55 100644 --- a/CosmosTestHelpers.Tests/IAsyncEnumerableExtensions.cs +++ b/CosmosTestHelpers.Tests/IAsyncEnumerableExtensions.cs @@ -1,88 +1,16 @@ -namespace CosmosTestHelpers.Tests +namespace CosmosTestHelpers.Tests; + +internal static class AsyncEnumerableExtensions { - internal static class AsyncEnumerableExtensions + public static async Task> ToListAsync(this IAsyncEnumerable enumerable) { - public static async Task FirstAsync(this IAsyncEnumerable enumerable) - { - await foreach (var item in enumerable) - { - return item; - } - - throw new InvalidOperationException("FirstAsync was called on a collection with zero elements"); - } - - public static async Task AnyAsync(this IAsyncEnumerable enumerable) - { - await foreach (var unused in enumerable) - { - return true; - } - - return false; - } - - public static async Task FirstOrDefaultAsync(this IAsyncEnumerable enumerable) - { - await foreach (var item in enumerable) - { - return item; - } - - return default(T); - } + var result = new List(); - public static async Task SingleAsync(this IAsyncEnumerable enumerable) + await foreach (var item in enumerable) { - var result = default(T); - var found = false; - await foreach (var item in enumerable) - { - if (found) - { - throw new InvalidOperationException("SingleSync called on a collection with more than one element"); - } - - found = true; - result = item; - } - - if (!found) - { - - throw new InvalidOperationException("SingleAsync was called on a collection with zero elements"); - } - - return result; - } - - public static async Task SingleOrDefaultAsync(this IAsyncEnumerable enumerable) - { - var result = default(T); - var found = false; - await foreach (var item in enumerable) - { - if (found) - { - throw new InvalidOperationException("SingleSync called on a collection with more than one element"); - } - - found = true; - result = item; - } - - return result; + result.Add(item); } - - public static async Task> ToListAsync(this IAsyncEnumerable enumerable) - { - var result = new List(); - await foreach (var item in enumerable) - { - result.Add(item); - } - return result; - } - } + return result; + } } \ No newline at end of file diff --git a/CosmosTestHelpers.Tests/InvalidIdTests.cs b/CosmosTestHelpers.Tests/InvalidIdTests.cs new file mode 100644 index 0000000..177129f --- /dev/null +++ b/CosmosTestHelpers.Tests/InvalidIdTests.cs @@ -0,0 +1,81 @@ +using System.Diagnostics.CodeAnalysis; +using System.Text; +using FluentAssertions; +using Microsoft.Azure.Cosmos; +using Newtonsoft.Json; +using Xunit; + +namespace CosmosTestHelpers.Tests; + +[SuppressMessage("ReSharper", "AccessToDisposedClosure")] +public class InvalidIdTests +{ + public static IEnumerable InvalidIdTestCases() + { + yield return new object[] { "/" }; + yield return new object[] { @"\" }; + yield return new object[] { "?" }; + yield return new object[] { "#" }; + } + + [Theory] + [MemberData(nameof(InvalidIdTestCases))] + public async Task When_inserting_an_item_with_a_forward_slash_in_the_id__Then_an_error_occurs(string invalidChars) + { + var containerMock = new ContainerMock(); + + var model = new TestModel { Id = "url" + invalidChars + "WillBreak", PartitionKey = "pk" }; + Func act = () => containerMock.CreateItemAsync(model, new PartitionKey(model.PartitionKey)); + + await act.Should().ThrowAsync(); + } + + [Theory] + [MemberData(nameof(InvalidIdTestCases))] + public async Task When_inserting_an_item_by_stream_with_a_forward_slash_in_the_id__Then_an_error_occurs(string invalidChars) + { + var containerMock = new ContainerMock(); + + var model = new TestModel { Id = "url" + invalidChars + "WillBreak", PartitionKey = "pk" }; + var bytes = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(model)); + await using var ms = new MemoryStream(bytes); + Func act = () => containerMock.CreateItemStreamAsync(ms, new PartitionKey(model.PartitionKey)); + + await act.Should().ThrowAsync(); + } + + [Theory] + [MemberData(nameof(InvalidIdTestCases))] + public async Task When_upserting_an_item__with_a_forward_slash_in_the_id__Then_an_error_occurs(string invalidChars) + { + var containerMock = new ContainerMock(); + + var model = new TestModel { Id = "url" + invalidChars + "WillBreak", PartitionKey = "pk" }; + Func act = () => containerMock.UpsertItemAsync(model, new PartitionKey(model.PartitionKey)); + + await act.Should().ThrowAsync(); + } + + [Theory] + [MemberData(nameof(InvalidIdTestCases))] + public async Task When_upserting_an_item_by_stream__with_a_forward_slash_in_the_id__Then_an_error_occurs(string invalidChars) + { + var containerMock = new ContainerMock(); + + var model = new TestModel { Id = "url" + invalidChars + "WillBreak", PartitionKey = "pk"}; + var bytes = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(model)); + await using var ms = new MemoryStream(bytes); + Func act = () => containerMock.UpsertItemStreamAsync(ms, new PartitionKey(model.PartitionKey)); + + await act.Should().ThrowAsync(); + } + + private class TestModel + { + [JsonProperty("id")] + public string Id { get; set; } = null!; + + [JsonProperty("partitionKey")] + public string PartitionKey { get; set; } = null!; + } +} \ No newline at end of file diff --git a/CosmosTestHelpers.Tests/TtlTests.cs b/CosmosTestHelpers.Tests/TtlTests.cs index 9c043ba..13723a7 100644 --- a/CosmosTestHelpers.Tests/TtlTests.cs +++ b/CosmosTestHelpers.Tests/TtlTests.cs @@ -5,271 +5,272 @@ using Newtonsoft.Json; using Xunit; -namespace CosmosTestHelpers.Tests +namespace CosmosTestHelpers.Tests; + +public class TtlTests { - public class TtlTests + public static IEnumerable CreateAndRetrieveTestCases() { - public static IEnumerable CreateAndRetrieveTestCases() - { - yield return new object[] { -1, 30, false }; - yield return new object[] { 30, null, false }; - yield return new object[] { 30, 30, false }; - yield return new object[] { 30, -1, true }; - yield return new object[] { 31, null, true }; - yield return new object[] { 31, 1, false }; - yield return new object[] { -1, 31, true }; - yield return new object[] { 1, 31, true }; - } + yield return new object?[] { -1, 30, false }; + yield return new object?[] { 30, null, false }; + yield return new object?[] { 30, 30, false }; + yield return new object?[] { 30, -1, true }; + yield return new object?[] { 31, null, true }; + yield return new object?[] { 31, 1, false }; + yield return new object?[] { -1, 31, true }; + yield return new object?[] { 1, 31, true }; + } - public static IEnumerable UpdateItemExtendExpiryTestCases() - { - yield return new object[] { -1, 30, true }; - yield return new object[] { 30, null, true }; - yield return new object[] { 30, 30, true }; - yield return new object[] { -1, 19, false }; - yield return new object[] { 19, null, false }; - } + public static IEnumerable UpdateItemExtendExpiryTestCases() + { + yield return new object?[] { -1, 30, true }; + yield return new object?[] { 30, null, true }; + yield return new object?[] { 30, 30, true }; + yield return new object?[] { -1, 19, false }; + yield return new object?[] { 19, null, false }; + } - public static IEnumerable ReplaceItemExtendExpiryTestCases() - { - yield return new object[] { -1, 30, true }; - yield return new object[] { 30, null, true }; - yield return new object[] { 30, 30, true }; - } + public static IEnumerable ReplaceItemExtendExpiryTestCases() + { + yield return new object?[] { -1, 30, true }; + yield return new object?[] { 30, null, true }; + yield return new object?[] { 30, 30, true }; + } - [Theory] - [MemberData(nameof(CreateAndRetrieveTestCases))] - public async Task ItemRemovedAfterTtlExpires_GetAllItems(int containerTtl, int? itemTtl, bool expectedItemExists) - { - var container = new ContainerMock(partitionKeyPath: "/partitionKey", defaultDocumentTimeToLive: containerTtl); + [Theory] + [MemberData(nameof(CreateAndRetrieveTestCases))] + public async Task ItemRemovedAfterTtlExpires_GetAllItems(int containerTtl, int? itemTtl, bool expectedItemExists) + { + var container = new ContainerMock(partitionKeyPath: "/partitionKey", defaultDocumentTimeToLive: containerTtl); - var document = new TestDocumentWithTtl { Id = "MyId", PartitionKey = "MyPartitionKey", TimeToLive = itemTtl }; + var document = new TestDocumentWithTtl { Id = "MyId", PartitionKey = "MyPartitionKey", TimeToLive = itemTtl }; - await container.CreateItemAsync(document); + await container.CreateItemAsync(document); - container.AdvanceTime(30); + container.AdvanceTime(30); - var results = container - .GetAllItems() - .ToList(); + var results = container + .GetAllItems() + .ToList(); - var expectedResultsCount = expectedItemExists ? 1 : 0; - results.Should().HaveCount(expectedResultsCount); - } + var expectedResultsCount = expectedItemExists ? 1 : 0; + results.Should().HaveCount(expectedResultsCount); + } - [Theory] - [MemberData(nameof(CreateAndRetrieveTestCases))] - public async Task ItemRemovedAfterTtlExpires_ReadItemAsync(int containerTtl, int? itemTtl, bool expectedItemExists) - { - var container = new ContainerMock(partitionKeyPath: "/partitionKey", defaultDocumentTimeToLive: containerTtl); + [Theory] + [MemberData(nameof(CreateAndRetrieveTestCases))] + public async Task ItemRemovedAfterTtlExpires_ReadItemAsync(int containerTtl, int? itemTtl, bool expectedItemExists) + { + var container = new ContainerMock(partitionKeyPath: "/partitionKey", defaultDocumentTimeToLive: containerTtl); - var document = new TestDocumentWithTtl { Id = "MyId", PartitionKey = "MyPartitionKey", TimeToLive = itemTtl }; + var document = new TestDocumentWithTtl { Id = "MyId", PartitionKey = "MyPartitionKey", TimeToLive = itemTtl }; - await container.CreateItemAsync(document); + await container.CreateItemAsync(document); - container.AdvanceTime(30); + container.AdvanceTime(30); - Func act = async () => { await container.ReadItemAsync(document.Id, new PartitionKey(document.PartitionKey)); }; + Func act = async () => { await container.ReadItemAsync(document.Id, new PartitionKey(document.PartitionKey)); }; - if (expectedItemExists) - { - await act.Should().NotThrowAsync(); - } - else - { - (await act.Should().ThrowAsync()).Where(e => e.StatusCode == HttpStatusCode.NotFound); - } + if (expectedItemExists) + { + await act.Should().NotThrowAsync(); } - - [Theory] - [MemberData(nameof(CreateAndRetrieveTestCases))] - public async Task ItemRemovedAfterTtlExpires_ReadItemStreamAsync(int containerTtl, int? itemTtl, bool expectedItemExists) + else { - var container = new ContainerMock(partitionKeyPath: "/partitionKey", defaultDocumentTimeToLive: containerTtl); + (await act.Should().ThrowAsync()).Where(e => e.StatusCode == HttpStatusCode.NotFound); + } + } - var document = new TestDocumentWithTtl { Id = "MyId", PartitionKey = "MyPartitionKey", TimeToLive = itemTtl }; + [Theory] + [MemberData(nameof(CreateAndRetrieveTestCases))] + public async Task ItemRemovedAfterTtlExpires_ReadItemStreamAsync(int containerTtl, int? itemTtl, bool expectedItemExists) + { + var container = new ContainerMock(partitionKeyPath: "/partitionKey", defaultDocumentTimeToLive: containerTtl); - await container.CreateItemAsync(document); + var document = new TestDocumentWithTtl { Id = "MyId", PartitionKey = "MyPartitionKey", TimeToLive = itemTtl }; - container.AdvanceTime(30); + await container.CreateItemAsync(document); - var response = await container.ReadItemStreamAsync(document.Id, new PartitionKey(document.PartitionKey)); + container.AdvanceTime(30); - var expectedStatusCode = expectedItemExists - ? HttpStatusCode.OK - : HttpStatusCode.NotFound; + var response = await container.ReadItemStreamAsync(document.Id, new PartitionKey(document.PartitionKey)); - response.StatusCode.Should().Be(expectedStatusCode); - } + var expectedStatusCode = expectedItemExists + ? HttpStatusCode.OK + : HttpStatusCode.NotFound; - [Theory] - [MemberData(nameof(CreateAndRetrieveTestCases))] - public async Task ItemRemovedAfterTtlExpires_GetItemLinqQueryable(int containerTtl, int? itemTtl, bool expectedItemExists) - { - var container = new ContainerMock(partitionKeyPath: "/partitionKey", defaultDocumentTimeToLive: containerTtl); + response.StatusCode.Should().Be(expectedStatusCode); + } - var document = new TestDocumentWithTtl { Id = "MyId", PartitionKey = "MyPartitionKey", TimeToLive = itemTtl }; + [Theory] + [MemberData(nameof(CreateAndRetrieveTestCases))] + public async Task ItemRemovedAfterTtlExpires_GetItemLinqQueryable(int containerTtl, int? itemTtl, bool expectedItemExists) + { + var container = new ContainerMock(partitionKeyPath: "/partitionKey", defaultDocumentTimeToLive: containerTtl); - await container.CreateItemAsync(document); + var document = new TestDocumentWithTtl { Id = "MyId", PartitionKey = "MyPartitionKey", TimeToLive = itemTtl }; - container.AdvanceTime(30); + await container.CreateItemAsync(document); - var results = container - .GetItemLinqQueryable() - .Where(d => d.Id == document.Id) - .ToList(); + container.AdvanceTime(30); - var expectedResultsCount = expectedItemExists ? 1 : 0; + var results = container + .GetItemLinqQueryable() + .Where(d => d.Id == document.Id) + .ToList(); - results.Should().HaveCount(expectedResultsCount); - } + var expectedResultsCount = expectedItemExists ? 1 : 0; - [Theory] - [MemberData(nameof(CreateAndRetrieveTestCases))] - public async Task ItemRemovedAfterTtlExpires_CountAsync(int containerTtl, int? itemTtl, bool expectedItemExists) - { - var container = new ContainerMock(partitionKeyPath: "/partitionKey", defaultDocumentTimeToLive: containerTtl); + results.Should().HaveCount(expectedResultsCount); + } - var document = new TestDocumentWithTtl { Id = "MyId", PartitionKey = "MyPartitionKey", TimeToLive = itemTtl }; + [Theory] + [MemberData(nameof(CreateAndRetrieveTestCases))] + public async Task ItemRemovedAfterTtlExpires_CountAsync(int containerTtl, int? itemTtl, bool expectedItemExists) + { + var container = new ContainerMock(partitionKeyPath: "/partitionKey", defaultDocumentTimeToLive: containerTtl); - await container.CreateItemAsync(document); + var document = new TestDocumentWithTtl { Id = "MyId", PartitionKey = "MyPartitionKey", TimeToLive = itemTtl }; - container.AdvanceTime(30); + await container.CreateItemAsync(document); - var count = await container.CountAsync(document.PartitionKey, query => query); + container.AdvanceTime(30); - var expectedCount = expectedItemExists ? 1 : 0; + var count = await container.CountAsync(document.PartitionKey, query => query); - count.Should().Be(expectedCount); - } + var expectedCount = expectedItemExists ? 1 : 0; - [Theory] - [MemberData(nameof(CreateAndRetrieveTestCases))] - public async Task ItemRemovedAfterTtlExpires_QueryAsync(int containerTtl, int? itemTtl, bool expectedItemExists) - { - var container = new ContainerMock(partitionKeyPath: "/partitionKey", defaultDocumentTimeToLive: containerTtl); + count.Should().Be(expectedCount); + } - var document = new TestDocumentWithTtl { Id = "MyId", PartitionKey = "MyPartitionKey", TimeToLive = itemTtl }; + [Theory] + [MemberData(nameof(CreateAndRetrieveTestCases))] + public async Task ItemRemovedAfterTtlExpires_QueryAsync(int containerTtl, int? itemTtl, bool expectedItemExists) + { + var container = new ContainerMock(partitionKeyPath: "/partitionKey", defaultDocumentTimeToLive: containerTtl); - await container.CreateItemAsync(document); + var document = new TestDocumentWithTtl { Id = "MyId", PartitionKey = "MyPartitionKey", TimeToLive = itemTtl }; - container.AdvanceTime(30); + await container.CreateItemAsync(document); - var results = container.QueryAsync(document.PartitionKey, query => query); - var items = await results.ToListAsync(); + container.AdvanceTime(30); - var expectedResultsCount = expectedItemExists ? 1 : 0; + var results = container.QueryAsync(document.PartitionKey, query => query); + var items = await results.ToListAsync(); - items.Should().HaveCount(expectedResultsCount); - } + var expectedResultsCount = expectedItemExists ? 1 : 0; - [Theory] - [MemberData(nameof(UpdateItemExtendExpiryTestCases))] - public async Task ItemExistsAfterItemUpdated_UpsertItemAsync(int containerTtl, int? itemTtl, bool expectedItemExists) - { - var container = new ContainerMock(partitionKeyPath: "/partitionKey", defaultDocumentTimeToLive: containerTtl); + items.Should().HaveCount(expectedResultsCount); + } - var document = new TestDocumentWithTtl { Id = "MyId", PartitionKey = "MyPartitionKey", TimeToLive = itemTtl }; + [Theory] + [MemberData(nameof(UpdateItemExtendExpiryTestCases))] + public async Task ItemExistsAfterItemUpdated_UpsertItemAsync(int containerTtl, int? itemTtl, bool expectedItemExists) + { + var container = new ContainerMock(partitionKeyPath: "/partitionKey", defaultDocumentTimeToLive: containerTtl); - await container.CreateItemAsync(document); + var document = new TestDocumentWithTtl { Id = "MyId", PartitionKey = "MyPartitionKey", TimeToLive = itemTtl }; - container.AdvanceTime(20); + await container.CreateItemAsync(document); - await container.UpsertItemAsync(document); + container.AdvanceTime(20); - container.AdvanceTime(20); + await container.UpsertItemAsync(document); - var response = await container.ReadItemStreamAsync(document.Id, new PartitionKey(document.PartitionKey)); + container.AdvanceTime(20); - var expectedStatusCode = expectedItemExists - ? HttpStatusCode.OK - : HttpStatusCode.NotFound; + var response = await container.ReadItemStreamAsync(document.Id, new PartitionKey(document.PartitionKey)); - response.StatusCode.Should().Be(expectedStatusCode); - } + var expectedStatusCode = expectedItemExists + ? HttpStatusCode.OK + : HttpStatusCode.NotFound; - [Theory] - [MemberData(nameof(UpdateItemExtendExpiryTestCases))] - public async Task ItemExistsAfterItemUpdated_UpsertItemStreamAsync(int containerTtl, int? itemTtl, bool expectedItemExists) - { - var container = new ContainerMock(partitionKeyPath: "/partitionKey", defaultDocumentTimeToLive: containerTtl); + response.StatusCode.Should().Be(expectedStatusCode); + } + + [Theory] + [MemberData(nameof(UpdateItemExtendExpiryTestCases))] + public async Task ItemExistsAfterItemUpdated_UpsertItemStreamAsync(int containerTtl, int? itemTtl, bool expectedItemExists) + { + var container = new ContainerMock(partitionKeyPath: "/partitionKey", defaultDocumentTimeToLive: containerTtl); - var document = new TestDocumentWithTtl { Id = "MyId", PartitionKey = "MyPartitionKey", TimeToLive = itemTtl }; + var document = new TestDocumentWithTtl { Id = "MyId", PartitionKey = "MyPartitionKey", TimeToLive = itemTtl }; - await container.CreateItemAsync(document); + await container.CreateItemAsync(document); - container.AdvanceTime(20); + container.AdvanceTime(20); - var bytes = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(document)); - await using var ms = new MemoryStream(bytes); - await container.UpsertItemStreamAsync(ms, new PartitionKey(document.PartitionKey)); + var bytes = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(document)); + await using var ms = new MemoryStream(bytes); + await container.UpsertItemStreamAsync(ms, new PartitionKey(document.PartitionKey)); - container.AdvanceTime(20); + container.AdvanceTime(20); - var response = await container.ReadItemStreamAsync(document.Id, new PartitionKey(document.PartitionKey)); + var response = await container.ReadItemStreamAsync(document.Id, new PartitionKey(document.PartitionKey)); - var expectedStatusCode = expectedItemExists - ? HttpStatusCode.OK - : HttpStatusCode.NotFound; + var expectedStatusCode = expectedItemExists + ? HttpStatusCode.OK + : HttpStatusCode.NotFound; - response.StatusCode.Should().Be(expectedStatusCode); - } + response.StatusCode.Should().Be(expectedStatusCode); + } - [Theory] - [MemberData(nameof(ReplaceItemExtendExpiryTestCases))] - public async Task ItemExistsAfterItemUpdated_ReplaceItemAsync(int containerTtl, int? itemTtl, bool expectedItemExists) - { - var container = new ContainerMock(partitionKeyPath: "/partitionKey", defaultDocumentTimeToLive: containerTtl); + [Theory] + [MemberData(nameof(ReplaceItemExtendExpiryTestCases))] + public async Task ItemExistsAfterItemUpdated_ReplaceItemAsync(int containerTtl, int? itemTtl, bool expectedItemExists) + { + var container = new ContainerMock(partitionKeyPath: "/partitionKey", defaultDocumentTimeToLive: containerTtl); - var document = new TestDocumentWithTtl { Id = "MyId", PartitionKey = "MyPartitionKey", TimeToLive = itemTtl }; + var document = new TestDocumentWithTtl { Id = "MyId", PartitionKey = "MyPartitionKey", TimeToLive = itemTtl }; - await container.CreateItemAsync(document); + await container.CreateItemAsync(document); - container.AdvanceTime(20); + container.AdvanceTime(20); - await container.ReplaceItemAsync(document, document.Id, new PartitionKey(document.PartitionKey)); + await container.ReplaceItemAsync(document, document.Id, new PartitionKey(document.PartitionKey)); - container.AdvanceTime(20); + container.AdvanceTime(20); - var response = await container.ReadItemStreamAsync(document.Id, new PartitionKey(document.PartitionKey)); + var response = await container.ReadItemStreamAsync(document.Id, new PartitionKey(document.PartitionKey)); - var expectedStatusCode = expectedItemExists - ? HttpStatusCode.OK - : HttpStatusCode.NotFound; + var expectedStatusCode = expectedItemExists + ? HttpStatusCode.OK + : HttpStatusCode.NotFound; - response.StatusCode.Should().Be(expectedStatusCode); - } + response.StatusCode.Should().Be(expectedStatusCode); + } - [Theory] - [MemberData(nameof(CreateAndRetrieveTestCases))] - public async Task ItemRemovedAfterTtlExpires_CreateItemStreamAsync_GetAllItems(int containerTtl, int? itemTtl, bool expectedItemExists) - { - var container = new ContainerMock(partitionKeyPath: "/partitionKey", defaultDocumentTimeToLive: containerTtl); + [Theory] + [MemberData(nameof(CreateAndRetrieveTestCases))] + public async Task ItemRemovedAfterTtlExpires_CreateItemStreamAsync_GetAllItems(int containerTtl, int? itemTtl, bool expectedItemExists) + { + var container = new ContainerMock(partitionKeyPath: "/partitionKey", defaultDocumentTimeToLive: containerTtl); - var document = new TestDocumentWithTtl { Id = "MyId", PartitionKey = "MyPartitionKey", TimeToLive = itemTtl }; + var document = new TestDocumentWithTtl { Id = "MyId", PartitionKey = "MyPartitionKey", TimeToLive = itemTtl }; - var bytes = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(document)); - await using var ms = new MemoryStream(bytes); - await container.CreateItemStreamAsync(ms, new PartitionKey(document.PartitionKey)); + var bytes = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(document)); + await using var ms = new MemoryStream(bytes); + await container.CreateItemStreamAsync(ms, new PartitionKey(document.PartitionKey)); - container.AdvanceTime(30); + container.AdvanceTime(30); - var results = container - .GetAllItems() - .ToList(); + var results = container + .GetAllItems() + .ToList(); - var expectedResultsCount = expectedItemExists ? 1 : 0; - results.Should().HaveCount(expectedResultsCount); - } + var expectedResultsCount = expectedItemExists ? 1 : 0; + results.Should().HaveCount(expectedResultsCount); + } - private class TestDocumentWithTtl - { - [JsonProperty("id")] public string Id { get; set; } + private class TestDocumentWithTtl + { + [JsonProperty("id")] + public string Id { get; set; } = null!; - [JsonProperty("partitionKey")] public string PartitionKey { get; set; } + [JsonProperty("partitionKey")] + public string PartitionKey { get; set; } = null!; - [JsonProperty("ttl", NullValueHandling = NullValueHandling.Ignore)] - public int? TimeToLive { get; set; } - } + [JsonProperty("ttl", NullValueHandling = NullValueHandling.Ignore)] + public int? TimeToLive { get; set; } } } \ No newline at end of file diff --git a/CosmosTestHelpers/ContainerMock.cs b/CosmosTestHelpers/ContainerMock.cs index 7103bde..b335969 100644 --- a/CosmosTestHelpers/ContainerMock.cs +++ b/CosmosTestHelpers/ContainerMock.cs @@ -8,539 +8,540 @@ #pragma warning disable CS1998 -namespace CosmosTestHelpers +namespace CosmosTestHelpers; + +public class ContainerMock : Container { - public class ContainerMock : Container - { - private readonly string _partitionKeyPath; + private readonly string _partitionKeyPath; - private readonly ContainerData _containerData; + private readonly ContainerData _containerData; - private readonly ConcurrentQueue<(CosmosException exception, Func condition)> _exceptionsToThrow; + private readonly ConcurrentQueue<(CosmosException exception, Func condition)> _exceptionsToThrow; - public override string Id { get; } - public override Conflicts Conflicts => null; - public override Scripts Scripts => null; - public override Database Database => null; + public override string Id { get; } + public override Conflicts Conflicts => null; + public override Scripts Scripts => null; + public override Database Database => null; - public override ChangeFeedProcessorBuilder GetChangeFeedProcessorBuilderWithManualCheckpoint(string processorName, ChangeFeedStreamHandlerWithManualCheckpoint onChangesDelegate) - { - throw new NotImplementedException(); - } + public override ChangeFeedProcessorBuilder GetChangeFeedProcessorBuilderWithManualCheckpoint(string processorName, ChangeFeedStreamHandlerWithManualCheckpoint onChangesDelegate) + { + throw new NotImplementedException(); + } - public delegate Task DocumentChangedEvent(IReadOnlyCollection changes, CancellationToken token); + public delegate Task DocumentChangedEvent(IReadOnlyCollection changes, CancellationToken token); - public event DocumentChangedEvent DocumentChanged - { - add => _containerData.DocumentChanged += value; - remove => _containerData.DocumentChanged -= value; - } + public event DocumentChangedEvent DocumentChanged + { + add => _containerData.DocumentChanged += value; + remove => _containerData.DocumentChanged -= value; + } - public ContainerMock(string partitionKeyPath = null, UniqueKeyPolicy uniqueKeyPolicy = null, string containerName = "TestContainer", int defaultDocumentTimeToLive = -1) - { - _containerData = new ContainerData(uniqueKeyPolicy, defaultDocumentTimeToLive); - _exceptionsToThrow = new ConcurrentQueue<(CosmosException, Func condition)>(); + public ContainerMock(string partitionKeyPath = null, UniqueKeyPolicy uniqueKeyPolicy = null, string containerName = "TestContainer", int defaultDocumentTimeToLive = -1) + { + _containerData = new ContainerData(uniqueKeyPolicy, defaultDocumentTimeToLive); + _exceptionsToThrow = new ConcurrentQueue<(CosmosException, Func condition)>(); - _partitionKeyPath = partitionKeyPath; + _partitionKeyPath = partitionKeyPath; - Id = containerName; - } - - public IEnumerable> GetAllItems() - { - var allItems = _containerData.GetAllItems(); + Id = containerName; + } - foreach (var containerItem in allItems) - { - var deserialized = containerItem.Deserialize(); - yield return new TestContainerItem(containerItem.PartitionKey.ToString(), containerItem.Id, deserialized); - } - } + public IEnumerable> GetAllItems() + { + var allItems = _containerData.GetAllItems(); - public override Task ReadContainerAsync(ContainerRequestOptions requestOptions = null, CancellationToken cancellationToken = new CancellationToken()) + foreach (var containerItem in allItems) { - throw new NotImplementedException(); + var deserialized = containerItem.Deserialize(); + yield return new TestContainerItem(containerItem.PartitionKey.ToString(), containerItem.Id, deserialized); } + } - public override Task ReadContainerStreamAsync(ContainerRequestOptions requestOptions = null, CancellationToken cancellationToken = new CancellationToken()) - { - throw new NotImplementedException(); - } + public override async Task CreateItemStreamAsync( + Stream streamPayload, + PartitionKey partitionKey, + ItemRequestOptions requestOptions = null, + CancellationToken cancellationToken = default + ) + { + ThrowNextExceptionIfPresent(new InvocationInformation(nameof(CreateItemStreamAsync))); - public override Task ReplaceContainerAsync(ContainerProperties containerProperties, ContainerRequestOptions requestOptions = null, CancellationToken cancellationToken = new CancellationToken()) + streamPayload.Position = 0; + var streamReader = new StreamReader(streamPayload); + var json = await streamReader.ReadToEndAsync(); + + if (JsonHelpers.GetIdFromJson(json) == string.Empty) { - throw new NotImplementedException(); + return new ResponseMessage(HttpStatusCode.BadRequest); } - - public override Task ReplaceContainerStreamAsync(ContainerProperties containerProperties, ContainerRequestOptions requestOptions = null, CancellationToken cancellationToken = new CancellationToken()) + + try { - throw new NotImplementedException(); + var response = await _containerData.AddItem(json, partitionKey, requestOptions, cancellationToken); + + return ToCosmosResponseMessage(response, streamPayload); } - - public override Task DeleteContainerAsync(ContainerRequestOptions requestOptions = null, CancellationToken cancellationToken = new CancellationToken()) + catch (ContainerMockException ex) { - throw new NotImplementedException(); + return new ResponseMessage(ex.StatusCode); } + } - public override Task DeleteContainerStreamAsync(ContainerRequestOptions requestOptions = null, CancellationToken cancellationToken = new CancellationToken()) - { - throw new NotImplementedException(); - } + public override async Task> CreateItemAsync( + T item, + PartitionKey? partitionKey = default, + ItemRequestOptions requestOptions = null, + CancellationToken cancellationToken = default + ) + { + ThrowNextExceptionIfPresent(new InvocationInformation(nameof(CreateItemAsync))); - public override Task ReadThroughputAsync(CancellationToken cancellationToken = new CancellationToken()) + if (partitionKey != null && partitionKey == PartitionKey.None && _partitionKeyPath != null) { - throw new NotImplementedException(); + throw new CosmosException( + "PartitionKey extracted from document doesn't match the one specified in the header. Learn more: https://aka.ms/CosmosDB/sql/errors/wrong-pk-value", + HttpStatusCode.BadRequest, + 0, + String.Empty, + 0); } - public override Task ReadThroughputAsync(RequestOptions requestOptions, CancellationToken cancellationToken = new CancellationToken()) + var json = JsonConvert.SerializeObject(item); + + if (JsonHelpers.GetIdFromJson(json) == string.Empty) { - throw new NotImplementedException(); + throw new CosmosException("Response status code does not indicate success: BadRequest (400);", HttpStatusCode.BadRequest, 400, Guid.NewGuid().ToString(), 0); } - - public override Task ReplaceThroughputAsync(int throughput, RequestOptions requestOptions = null, CancellationToken cancellationToken = new CancellationToken()) + + try { - throw new NotImplementedException(); + var response = await _containerData.AddItem(json, GetPartitionKey(json, partitionKey), requestOptions, cancellationToken); + + return ToCosmosItemResponse(response); } - - public override Task ReplaceThroughputAsync(ThroughputProperties throughputProperties, RequestOptions requestOptions = null, CancellationToken cancellationToken = new CancellationToken()) + catch (ContainerMockException ex) { - throw new NotImplementedException(); + throw ex.ToCosmosException(); } + } - public override async Task CreateItemStreamAsync( - Stream streamPayload, - PartitionKey partitionKey, - ItemRequestOptions requestOptions = null, - CancellationToken cancellationToken = default - ) - { - ThrowNextExceptionIfPresent(new InvocationInformation(nameof(CreateItemAsync))); + private PartitionKey GetPartitionKey(string json, PartitionKey? partitionKey) + { + return partitionKey ?? new PartitionKey(JsonHelpers.GetValueFromJson(json, _partitionKeyPath)); + } - streamPayload.Position = 0; - var streamReader = new StreamReader(streamPayload); - var json = await streamReader.ReadToEndAsync(); - - if (JsonHelpers.GetIdFromJson(json) == string.Empty) - { - return new ResponseMessage(HttpStatusCode.BadRequest); - } + public override Task ReadItemStreamAsync(string id, PartitionKey partitionKey, ItemRequestOptions requestOptions = null, CancellationToken cancellationToken = default) + { + ThrowNextExceptionIfPresent(new InvocationInformation(nameof(ReadItemStreamAsync))); - try - { - var response = await _containerData.AddItem(json, partitionKey, requestOptions, cancellationToken); - - return ToCosmosResponseMessage(response, streamPayload); - } - catch (ContainerMockException ex) - { - return new ResponseMessage(ex.StatusCode); - } - } - - public override async Task> CreateItemAsync( - T item, - PartitionKey? partitionKey = default, - ItemRequestOptions requestOptions = null, - CancellationToken cancellationToken = default - ) + if (id == string.Empty) { - ThrowNextExceptionIfPresent(new InvocationInformation(nameof(CreateItemAsync))); - - if (partitionKey != null && partitionKey == PartitionKey.None && _partitionKeyPath != null) - { - throw new CosmosException( - "PartitionKey extracted from document doesn't match the one specified in the header. Learn more: https://aka.ms/CosmosDB/sql/errors/wrong-pk-value", - HttpStatusCode.BadRequest, - 0, - String.Empty, - 0); - } - - var json = JsonConvert.SerializeObject(item); - - if (JsonHelpers.GetIdFromJson(json) == string.Empty) - { - throw new CosmosException("Response status code does not indicate success: BadRequest (400);", HttpStatusCode.BadRequest, 400, Guid.NewGuid().ToString(), 0); - } - - try - { - var response = await _containerData.AddItem(json, GetPartitionKey(json, partitionKey), requestOptions, cancellationToken); - - return ToCosmosItemResponse(response); - } - catch (ContainerMockException ex) - { - throw ex.ToCosmosException(); - } + return Task.FromResult(new ResponseMessage(HttpStatusCode.BadRequest)); } - private PartitionKey GetPartitionKey(string json, PartitionKey? partitionKey) - { - return partitionKey ?? new PartitionKey(JsonHelpers.GetValueFromJson(json, _partitionKeyPath)); - } + var item = _containerData.GetItem(id, partitionKey); - public override Task ReadItemStreamAsync(string id, PartitionKey partitionKey, ItemRequestOptions requestOptions = null, CancellationToken cancellationToken = default) + if (item == null) { - ThrowNextExceptionIfPresent(new InvocationInformation(nameof(ReadItemAsync))); - - if (id == string.Empty) - { - return Task.FromResult(new ResponseMessage(HttpStatusCode.BadRequest)); - } + return Task.FromResult(new ResponseMessage(HttpStatusCode.NotFound)); + } - var item = _containerData.GetItem(id, partitionKey); + var responseStream = new MemoryStream(Encoding.UTF8.GetBytes(item.Json)); + responseStream.Position = 0; - if (item == null) - { - return Task.FromResult(new ResponseMessage(HttpStatusCode.NotFound)); - } + var response = new ResponseMessage(HttpStatusCode.OK) { Content = responseStream }; + response.Headers.Add("etag", item.ETag); + return Task.FromResult(response); + } - var responseStream = new MemoryStream(Encoding.UTF8.GetBytes(item.Json)); - responseStream.Position = 0; + public override Task> ReadItemAsync(string id, PartitionKey partitionKey, ItemRequestOptions requestOptions = null, CancellationToken cancellationToken = default) + { + ThrowNextExceptionIfPresent(new InvocationInformation(nameof(ReadItemAsync))); - var response = new ResponseMessage(HttpStatusCode.OK) { Content = responseStream }; - response.Headers.Add("etag", item.ETag); - return Task.FromResult(response); + if (id == string.Empty) + { + throw new CosmosException("Response status code does not indicate success: BadRequest (400);", HttpStatusCode.BadRequest, 400, Guid.NewGuid().ToString(), 0); } - public override Task> ReadItemAsync(string id, PartitionKey partitionKey, ItemRequestOptions requestOptions = null, CancellationToken cancellationToken = default) - { - ThrowNextExceptionIfPresent(new InvocationInformation(nameof(ReadItemAsync))); + var item = _containerData.GetItem(id, partitionKey); - if (id == string.Empty) - { - throw new CosmosException("Response status code does not indicate success: BadRequest (400);", HttpStatusCode.BadRequest, 400, Guid.NewGuid().ToString(), 0); - } + if (item == null) + { + throw new NotFoundException().ToCosmosException(); + } - var item = _containerData.GetItem(id, partitionKey); + var itemResponse = new MockItemResponse(item.Deserialize(), item.ETag); + return Task.FromResult>(itemResponse); + } - if (item == null) - { - throw new NotFoundException().ToCosmosException(); - } + public override async Task UpsertItemStreamAsync(Stream streamPayload, PartitionKey partitionKey, ItemRequestOptions requestOptions = null, CancellationToken cancellationToken = default) + { + ThrowNextExceptionIfPresent(new InvocationInformation(nameof(UpsertItemStreamAsync))); - var itemResponse = new MockItemResponse(item.Deserialize(), item.ETag); - return Task.FromResult>(itemResponse); - } + streamPayload.Position = 0; + var streamReader = new StreamReader(streamPayload); + var json = await streamReader.ReadToEndAsync(); - public override async Task UpsertItemStreamAsync(Stream streamPayload, PartitionKey partitionKey, ItemRequestOptions requestOptions = null, CancellationToken cancellationToken = default) + try { - ThrowNextExceptionIfPresent(new InvocationInformation(nameof(UpsertItemAsync))); - - streamPayload.Position = 0; - var streamReader = new StreamReader(streamPayload); - var json = await streamReader.ReadToEndAsync(); - - try - { - var response = await _containerData.UpsertItem(json, partitionKey, requestOptions, cancellationToken); - return ToCosmosResponseMessage(response, streamPayload); - } - catch (ContainerMockException ex) - { - return new ResponseMessage(ex.StatusCode); - } + var response = await _containerData.UpsertItem(json, partitionKey, requestOptions, cancellationToken); + return ToCosmosResponseMessage(response, streamPayload); } - - public override Task ReplaceItemStreamAsync(Stream streamPayload, string id, PartitionKey partitionKey, ItemRequestOptions requestOptions = null, CancellationToken cancellationToken = new CancellationToken()) + catch (ContainerMockException ex) { - throw new NotImplementedException(); + return new ResponseMessage(ex.StatusCode); } + } - public override Task ReadManyItemsStreamAsync(IReadOnlyList<(string id, PartitionKey partitionKey)> items, ReadManyRequestOptions readManyRequestOptions = null, CancellationToken cancellationToken = new CancellationToken()) - { - throw new NotImplementedException(); - } + public override async Task> UpsertItemAsync(T item, PartitionKey? partitionKey = default, ItemRequestOptions requestOptions = null, CancellationToken cancellationToken = default) + { + ThrowNextExceptionIfPresent(new InvocationInformation(nameof(UpsertItemAsync))); - public override Task> ReadManyItemsAsync(IReadOnlyList<(string id, PartitionKey partitionKey)> items, ReadManyRequestOptions readManyRequestOptions = null, CancellationToken cancellationToken = new CancellationToken()) - { - throw new NotImplementedException(); - } + var json = JsonConvert.SerializeObject(item); - public override Task> PatchItemAsync(string id, PartitionKey partitionKey, IReadOnlyList patchOperations, PatchItemRequestOptions requestOptions = null, CancellationToken cancellationToken = new CancellationToken()) + try { - throw new NotImplementedException(); + var response = await _containerData.UpsertItem(json, GetPartitionKey(json, partitionKey), requestOptions, cancellationToken); + return ToCosmosItemResponse(response); } - - public override Task PatchItemStreamAsync(string id, PartitionKey partitionKey, IReadOnlyList patchOperations, PatchItemRequestOptions requestOptions = null, CancellationToken cancellationToken = new CancellationToken()) + catch (ContainerMockException ex) { - throw new NotImplementedException(); + throw ex.ToCosmosException(); } + } - public override Task DeleteItemStreamAsync(string id, PartitionKey partitionKey, ItemRequestOptions requestOptions = null, CancellationToken cancellationToken = new CancellationToken()) - { - throw new NotImplementedException(); - } + public override async Task> ReplaceItemAsync(T item, string id, PartitionKey? partitionKey = default, ItemRequestOptions requestOptions = null, CancellationToken cancellationToken = default) + { + ThrowNextExceptionIfPresent(new InvocationInformation(nameof(ReplaceItemAsync))); - public override FeedIterator GetItemQueryStreamIterator(QueryDefinition queryDefinition, string continuationToken = null, QueryRequestOptions requestOptions = null) - { - throw new NotImplementedException(); - } + var json = JsonConvert.SerializeObject(item); - public override FeedIterator GetItemQueryIterator(QueryDefinition queryDefinition, string continuationToken = null, QueryRequestOptions requestOptions = null) + try { - throw new NotImplementedException(); + var response = await _containerData.ReplaceItem(id, json, GetPartitionKey(json, partitionKey), requestOptions, cancellationToken); + return ToCosmosItemResponse(response); } - - public override FeedIterator GetItemQueryStreamIterator(string queryText = null, string continuationToken = null, QueryRequestOptions requestOptions = null) + catch (ContainerMockException ex) { - throw new NotImplementedException(); + throw ex.ToCosmosException(); } - - public override FeedIterator GetItemQueryIterator(string queryText = null, string continuationToken = null, QueryRequestOptions requestOptions = null) + } + + public Task CountAsync(string partitionKey, Func, IQueryable> applyQuery) + { + if (applyQuery == null) { - throw new NotImplementedException(); + throw new ArgumentNullException(nameof(applyQuery)); } - public override FeedIterator GetItemQueryStreamIterator(FeedRange feedRange, QueryDefinition queryDefinition, string continuationToken, QueryRequestOptions requestOptions = null) - { - throw new NotImplementedException(); - } + ThrowNextExceptionIfPresent(new InvocationInformation(nameof(CountAsync))); + + var partition = partitionKey == null + ? (PartitionKey?)null + : new PartitionKey(partitionKey); + + var items = _containerData.GetItemsInPartition(partition); + + var itemLinqQueryable = new CosmosQueryableMock( + items + .OrderBy(i => i.Id) + .Select(i => i.Deserialize()) + .AsQueryable() + ); + + var queryable = applyQuery(itemLinqQueryable); + return Task.FromResult(queryable.Count()); + } - public override FeedIterator GetItemQueryIterator(FeedRange feedRange, QueryDefinition queryDefinition, string continuationToken = null, QueryRequestOptions requestOptions = null) + public override Task> DeleteItemAsync(string id, PartitionKey partitionKey, ItemRequestOptions requestOptions = null, CancellationToken cancellationToken = default) + { + ThrowNextExceptionIfPresent(new InvocationInformation(nameof(DeleteItemAsync))); + + try { - throw new NotImplementedException(); + _containerData.RemoveItem(id, partitionKey, requestOptions); + + var itemResponse = new MockItemResponse(HttpStatusCode.NoContent); + return Task.FromResult>(itemResponse); } - - public override ChangeFeedProcessorBuilder GetChangeFeedProcessorBuilder(string processorName, ChangesHandler onChangesDelegate) + catch (ContainerMockException ex) { - throw new NotImplementedException(); + throw ex.ToCosmosException(); } + } - public override ChangeFeedProcessorBuilder GetChangeFeedEstimatorBuilder(string processorName, ChangesEstimationHandler estimationDelegate, TimeSpan? estimationPeriod = null) - { - throw new NotImplementedException(); - } + public override IOrderedQueryable GetItemLinqQueryable(bool allowSynchronousQueryExecution = false, string continuationToken = null, QueryRequestOptions requestOptions = null, CosmosLinqSerializerOptions linqSerializerOptions = null) + { + ThrowNextExceptionIfPresent(new InvocationInformation(nameof(GetItemLinqQueryable))); - public override ChangeFeedEstimator GetChangeFeedEstimator(string processorName, Container leaseContainer) - { - throw new NotImplementedException(); - } + var items = _containerData.GetItemsInPartition(requestOptions?.PartitionKey); - public override TransactionalBatch CreateTransactionalBatch(PartitionKey partitionKey) - { - throw new NotImplementedException(); - } + var itemLinqQueryable = new CosmosQueryableMock( + items + .OrderBy(i => i.Id) + .Select(i => i.Deserialize()) + .AsQueryable() + ); - public override Task> GetFeedRangesAsync(CancellationToken cancellationToken = new CancellationToken()) - { - throw new NotImplementedException(); - } + return itemLinqQueryable; + } - public override FeedIterator GetChangeFeedStreamIterator(ChangeFeedStartFrom changeFeedStartFrom, ChangeFeedMode changeFeedMode, ChangeFeedRequestOptions changeFeedRequestOptions = null) + public async IAsyncEnumerable QueryAsync(string partitionKey, Func, IQueryable> applyQuery) + { + if (applyQuery == null) { - throw new NotImplementedException(); + throw new ArgumentNullException(nameof(applyQuery)); } - public override FeedIterator GetChangeFeedIterator(ChangeFeedStartFrom changeFeedStartFrom, ChangeFeedMode changeFeedMode, ChangeFeedRequestOptions changeFeedRequestOptions = null) - { - throw new NotImplementedException(); - } + ThrowNextExceptionIfPresent(new InvocationInformation(nameof(QueryAsync))); - public override ChangeFeedProcessorBuilder GetChangeFeedProcessorBuilder(string processorName, ChangeFeedHandler onChangesDelegate) - { - throw new NotImplementedException(); - } + var partition = partitionKey == null + ? (PartitionKey?)null + : new PartitionKey(partitionKey); + + var items = _containerData.GetItemsInPartition(partition); - public override ChangeFeedProcessorBuilder GetChangeFeedProcessorBuilderWithManualCheckpoint(string processorName, ChangeFeedHandlerWithManualCheckpoint onChangesDelegate) - { - throw new NotImplementedException(); - } + var itemLinqQueryable = new CosmosQueryableMock( + items + .OrderBy(i => i.Id) + .Select(i => i.Deserialize()) + .AsQueryable() + ); - public override ChangeFeedProcessorBuilder GetChangeFeedProcessorBuilder(string processorName, ChangeFeedStreamHandler onChangesDelegate) - { - throw new NotImplementedException(); - } + var results = applyQuery(itemLinqQueryable).ToList(); - public override async Task> UpsertItemAsync(T item, PartitionKey? partitionKey = default, ItemRequestOptions requestOptions = null, CancellationToken cancellationToken = default) + foreach (var result in results) { - ThrowNextExceptionIfPresent(new InvocationInformation(nameof(UpsertItemAsync))); - - var json = JsonConvert.SerializeObject(item); - - try - { - var response = await _containerData.UpsertItem(json, GetPartitionKey(json, partitionKey), requestOptions, cancellationToken); - return ToCosmosItemResponse(response); - } - catch (ContainerMockException ex) - { - throw ex.ToCosmosException(); - } + yield return result; } + } - public override async Task> ReplaceItemAsync(T item, string id, PartitionKey? partitionKey = default, ItemRequestOptions requestOptions = null, CancellationToken cancellationToken = default) + public void Reset() + { + _containerData.Clear(); + } + + public void QueueExceptionToBeThrown(CosmosException exceptionToThrow, Func condition = null) + { + condition ??= (_ => true); + _exceptionsToThrow.Enqueue((exceptionToThrow, condition)); + } + + public void TheNextWriteToDocumentRequiresEtagAndWillRaiseAConcurrencyException(PartitionKey partitionKey, string id) + { + var item = _containerData.GetItem(id, partitionKey); + + if (item == null) { - ThrowNextExceptionIfPresent(new InvocationInformation(nameof(ReplaceItemAsync))); - - var json = JsonConvert.SerializeObject(item); - - try - { - var response = await _containerData.ReplaceItem(id, json, GetPartitionKey(json, partitionKey), requestOptions, cancellationToken); - return ToCosmosItemResponse(response); - } - catch (ContainerMockException ex) - { - throw ex.ToCosmosException(); - } + throw new InvalidOperationException($"Could not find item '{id}' in partition '{partitionKey}'"); } - - public Task CountAsync(string partitionKey, Func, IQueryable> applyQuery) - { - if (applyQuery == null) - { - throw new ArgumentNullException(nameof(applyQuery)); - } + + item.ScheduleMismatchETagOnNextUpdate(); + } - ThrowNextExceptionIfPresent(new InvocationInformation(nameof(QueryAsync))); + public void AdvanceTime(int seconds) + { + _containerData.AdvanceTime(seconds); + } + + #region Not Implemented - var partition = partitionKey == null - ? (PartitionKey?)null - : new PartitionKey(partitionKey); + public override Task ReadContainerAsync(ContainerRequestOptions requestOptions = null, CancellationToken cancellationToken = new CancellationToken()) + { + throw new NotImplementedException(); + } - var items = _containerData.GetItemsInPartition(partition); + public override Task ReadContainerStreamAsync(ContainerRequestOptions requestOptions = null, CancellationToken cancellationToken = new CancellationToken()) + { + throw new NotImplementedException(); + } - var itemLinqQueryable = new CosmosQueryableMock( - items - .OrderBy(i => i.Id) - .Select(i => i.Deserialize()) - .AsQueryable() - ); + public override Task ReplaceContainerAsync(ContainerProperties containerProperties, ContainerRequestOptions requestOptions = null, CancellationToken cancellationToken = new CancellationToken()) + { + throw new NotImplementedException(); + } - var queryable = applyQuery(itemLinqQueryable); - return Task.FromResult(queryable.Count()); - } + public override Task ReplaceContainerStreamAsync(ContainerProperties containerProperties, ContainerRequestOptions requestOptions = null, CancellationToken cancellationToken = new CancellationToken()) + { + throw new NotImplementedException(); + } - public override Task> DeleteItemAsync(string id, PartitionKey partitionKey, ItemRequestOptions requestOptions = null, CancellationToken cancellationToken = default) - { - ThrowNextExceptionIfPresent(new InvocationInformation(nameof(DeleteItemAsync))); - - try - { - _containerData.RemoveItem(id, partitionKey, requestOptions); - - var itemResponse = new MockItemResponse(HttpStatusCode.NoContent); - return Task.FromResult>(itemResponse); - } - catch (ContainerMockException ex) - { - throw ex.ToCosmosException(); - } - } + public override Task DeleteContainerAsync(ContainerRequestOptions requestOptions = null, CancellationToken cancellationToken = new CancellationToken()) + { + throw new NotImplementedException(); + } - public override IOrderedQueryable GetItemLinqQueryable(bool allowSynchronousQueryExecution = false, string continuationToken = null, QueryRequestOptions requestOptions = null, CosmosLinqSerializerOptions linqSerializerOptions = null) - { - ThrowNextExceptionIfPresent(new InvocationInformation(nameof(GetItemLinqQueryable))); + public override Task DeleteContainerStreamAsync(ContainerRequestOptions requestOptions = null, CancellationToken cancellationToken = new CancellationToken()) + { + throw new NotImplementedException(); + } - var items = _containerData.GetItemsInPartition(requestOptions?.PartitionKey); + public override Task ReadThroughputAsync(CancellationToken cancellationToken = new CancellationToken()) + { + throw new NotImplementedException(); + } - var itemLinqQueryable = new CosmosQueryableMock( - items - .OrderBy(i => i.Id) - .Select(i => i.Deserialize()) - .AsQueryable() - ); + public override Task ReadThroughputAsync(RequestOptions requestOptions, CancellationToken cancellationToken = new CancellationToken()) + { + throw new NotImplementedException(); + } - return itemLinqQueryable; - } + public override Task ReplaceThroughputAsync(int throughput, RequestOptions requestOptions = null, CancellationToken cancellationToken = new CancellationToken()) + { + throw new NotImplementedException(); + } - public async IAsyncEnumerable QueryAsync(string partitionKey, Func, IQueryable> applyQuery) - { - if (applyQuery == null) - { - throw new ArgumentNullException(nameof(applyQuery)); - } + public override Task ReplaceThroughputAsync(ThroughputProperties throughputProperties, RequestOptions requestOptions = null, CancellationToken cancellationToken = new CancellationToken()) + { + throw new NotImplementedException(); + } - ThrowNextExceptionIfPresent(new InvocationInformation(nameof(QueryAsync))); + public override Task ReplaceItemStreamAsync(Stream streamPayload, string id, PartitionKey partitionKey, ItemRequestOptions requestOptions = null, CancellationToken cancellationToken = new CancellationToken()) + { + throw new NotImplementedException(); + } - var partition = partitionKey == null - ? (PartitionKey?)null - : new PartitionKey(partitionKey); - - var items = _containerData.GetItemsInPartition(partition); + public override Task ReadManyItemsStreamAsync(IReadOnlyList<(string id, PartitionKey partitionKey)> items, ReadManyRequestOptions readManyRequestOptions = null, CancellationToken cancellationToken = new CancellationToken()) + { + throw new NotImplementedException(); + } - var itemLinqQueryable = new CosmosQueryableMock( - items - .OrderBy(i => i.Id) - .Select(i => i.Deserialize()) - .AsQueryable() - ); + public override Task> ReadManyItemsAsync(IReadOnlyList<(string id, PartitionKey partitionKey)> items, ReadManyRequestOptions readManyRequestOptions = null, CancellationToken cancellationToken = new CancellationToken()) + { + throw new NotImplementedException(); + } - var results = applyQuery(itemLinqQueryable).ToList(); + public override Task> PatchItemAsync(string id, PartitionKey partitionKey, IReadOnlyList patchOperations, PatchItemRequestOptions requestOptions = null, CancellationToken cancellationToken = new CancellationToken()) + { + throw new NotImplementedException(); + } - foreach (var result in results) - { - yield return result; - } - } + public override Task PatchItemStreamAsync(string id, PartitionKey partitionKey, IReadOnlyList patchOperations, PatchItemRequestOptions requestOptions = null, CancellationToken cancellationToken = new CancellationToken()) + { + throw new NotImplementedException(); + } - public void Reset() - { - _containerData.Clear(); - } + public override Task DeleteItemStreamAsync(string id, PartitionKey partitionKey, ItemRequestOptions requestOptions = null, CancellationToken cancellationToken = new CancellationToken()) + { + throw new NotImplementedException(); + } - public void QueueExceptionToBeThrown(CosmosException exceptionToThrow, Func condition = null) - { - condition ??= (_ => true); - _exceptionsToThrow.Enqueue((exceptionToThrow, condition)); - } + public override FeedIterator GetItemQueryStreamIterator(QueryDefinition queryDefinition, string continuationToken = null, QueryRequestOptions requestOptions = null) + { + throw new NotImplementedException(); + } - public void TheNextWriteToDocumentRequiresEtagAndWillRaiseAConcurrencyException(PartitionKey partitionKey, string id) - { - var item = _containerData.GetItem(id, partitionKey); - - if (item == null) - { - throw new InvalidOperationException($"Could not find item '{id}' in partition '{partitionKey}'"); - } - - item.ScheduleMismatchETagOnNextUpdate(); - } + public override FeedIterator GetItemQueryIterator(QueryDefinition queryDefinition, string continuationToken = null, QueryRequestOptions requestOptions = null) + { + throw new NotImplementedException(); + } - public void AdvanceTime(int seconds) - { - _containerData.AdvanceTime(seconds); - } + public override FeedIterator GetItemQueryStreamIterator(string queryText = null, string continuationToken = null, QueryRequestOptions requestOptions = null) + { + throw new NotImplementedException(); + } + + public override FeedIterator GetItemQueryIterator(string queryText = null, string continuationToken = null, QueryRequestOptions requestOptions = null) + { + throw new NotImplementedException(); + } + + public override FeedIterator GetItemQueryStreamIterator(FeedRange feedRange, QueryDefinition queryDefinition, string continuationToken, QueryRequestOptions requestOptions = null) + { + throw new NotImplementedException(); + } + + public override FeedIterator GetItemQueryIterator(FeedRange feedRange, QueryDefinition queryDefinition, string continuationToken = null, QueryRequestOptions requestOptions = null) + { + throw new NotImplementedException(); + } - private static ResponseMessage ToCosmosResponseMessage(Response response, Stream streamPayload) - { - var statusCode = response.IsUpdate ? HttpStatusCode.OK : HttpStatusCode.Created; + public override ChangeFeedProcessorBuilder GetChangeFeedProcessorBuilder(string processorName, ChangesHandler onChangesDelegate) + { + throw new NotImplementedException(); + } - var responseMessage = new ResponseMessage(statusCode) { Content = streamPayload }; - responseMessage.Headers.Add("etag", response.Item.ETag); + public override ChangeFeedProcessorBuilder GetChangeFeedEstimatorBuilder(string processorName, ChangesEstimationHandler estimationDelegate, TimeSpan? estimationPeriod = null) + { + throw new NotImplementedException(); + } - return responseMessage; + public override ChangeFeedEstimator GetChangeFeedEstimator(string processorName, Container leaseContainer) + { + throw new NotImplementedException(); + } + + public override TransactionalBatch CreateTransactionalBatch(PartitionKey partitionKey) + { + throw new NotImplementedException(); + } + + public override Task> GetFeedRangesAsync(CancellationToken cancellationToken = new CancellationToken()) + { + throw new NotImplementedException(); + } + + public override FeedIterator GetChangeFeedStreamIterator(ChangeFeedStartFrom changeFeedStartFrom, ChangeFeedMode changeFeedMode, ChangeFeedRequestOptions changeFeedRequestOptions = null) + { + throw new NotImplementedException(); + } + + public override FeedIterator GetChangeFeedIterator(ChangeFeedStartFrom changeFeedStartFrom, ChangeFeedMode changeFeedMode, ChangeFeedRequestOptions changeFeedRequestOptions = null) + { + throw new NotImplementedException(); + } + + public override ChangeFeedProcessorBuilder GetChangeFeedProcessorBuilder(string processorName, ChangeFeedHandler onChangesDelegate) + { + throw new NotImplementedException(); + } + + public override ChangeFeedProcessorBuilder GetChangeFeedProcessorBuilderWithManualCheckpoint(string processorName, ChangeFeedHandlerWithManualCheckpoint onChangesDelegate) + { + throw new NotImplementedException(); + } + + public override ChangeFeedProcessorBuilder GetChangeFeedProcessorBuilder(string processorName, ChangeFeedStreamHandler onChangesDelegate) + { + throw new NotImplementedException(); + } + #endregion + + private static ResponseMessage ToCosmosResponseMessage(Response response, Stream streamPayload) + { + var statusCode = response.IsUpdate ? HttpStatusCode.OK : HttpStatusCode.Created; + + var responseMessage = new ResponseMessage(statusCode) { Content = streamPayload }; + responseMessage.Headers.Add("etag", response.Item.ETag); + + return responseMessage; + } + + private static ItemResponse ToCosmosItemResponse(Response response) + { + return new MockItemResponse( + response.Item.Deserialize(), + response.IsUpdate ? HttpStatusCode.OK : HttpStatusCode.Created, + response.Item.ETag + ); + } + + private void ThrowNextExceptionIfPresent(InvocationInformation invocationInformation) + { + if (!_exceptionsToThrow.TryPeek(out var peekedException)) + { + return; } - private static ItemResponse ToCosmosItemResponse(Response response) + var condition = peekedException.condition(invocationInformation); + if (!condition) { - return new MockItemResponse( - response.Item.Deserialize(), - response.IsUpdate ? HttpStatusCode.OK : HttpStatusCode.Created, - response.Item.ETag - ); + return; } - private void ThrowNextExceptionIfPresent(InvocationInformation invocationInformation) + if (_exceptionsToThrow.TryDequeue(out var exceptionToThrow) && peekedException == exceptionToThrow && exceptionToThrow.exception != null) { - if (!_exceptionsToThrow.TryPeek(out var peekedException)) - { - return; - } - - var condition = peekedException.condition(invocationInformation); - if (!condition) - { - return; - } - - - if (_exceptionsToThrow.TryDequeue(out var exceptionToThrow) && peekedException == exceptionToThrow && exceptionToThrow.exception != null) - { - throw exceptionToThrow.exception; - } + throw exceptionToThrow.exception; } } } \ No newline at end of file diff --git a/CosmosTestHelpers/ContainerMockData/ContainerData.cs b/CosmosTestHelpers/ContainerMockData/ContainerData.cs index d5c41fa..36bbfba 100644 --- a/CosmosTestHelpers/ContainerMockData/ContainerData.cs +++ b/CosmosTestHelpers/ContainerMockData/ContainerData.cs @@ -2,279 +2,278 @@ using System.Net; using Microsoft.Azure.Cosmos; -namespace CosmosTestHelpers.ContainerMockData -{ - internal class ContainerData - { - private readonly UniqueKeyPolicy _uniqueKeyPolicy; - private readonly int _defaultDocumentTimeToLive; - private static readonly PartitionKey NonePartitionKey = new PartitionKey("###PartitionKeyNone###"); - private static readonly PartitionKey NullPartitionKey = new PartitionKey("###PartitionKeyNull###"); +namespace CosmosTestHelpers.ContainerMockData; - private readonly SemaphoreSlim _updateSemaphore = new SemaphoreSlim(1, 1); - private readonly ConcurrentDictionary> _data; +internal class ContainerData +{ + private readonly UniqueKeyPolicy _uniqueKeyPolicy; + private readonly int _defaultDocumentTimeToLive; + private static readonly PartitionKey NonePartitionKey = new PartitionKey("###PartitionKeyNone###"); + private static readonly PartitionKey NullPartitionKey = new PartitionKey("###PartitionKeyNull###"); - private int _currentTimer; + private readonly SemaphoreSlim _updateSemaphore = new SemaphoreSlim(1, 1); + private readonly ConcurrentDictionary> _data; - public event ContainerMock.DocumentChangedEvent DocumentChanged; + private int _currentTimer; - public ContainerData(UniqueKeyPolicy uniqueKeyPolicy, int defaultDocumentTimeToLive) - { - GuardAgainstInvalidUniqueKeyPolicy(uniqueKeyPolicy); + public event ContainerMock.DocumentChangedEvent DocumentChanged; - _uniqueKeyPolicy = uniqueKeyPolicy; - _defaultDocumentTimeToLive = defaultDocumentTimeToLive; + public ContainerData(UniqueKeyPolicy uniqueKeyPolicy, int defaultDocumentTimeToLive) + { + GuardAgainstInvalidUniqueKeyPolicy(uniqueKeyPolicy); - _data = new ConcurrentDictionary>(); - _currentTimer = 0; - } + _uniqueKeyPolicy = uniqueKeyPolicy; + _defaultDocumentTimeToLive = defaultDocumentTimeToLive; - public IEnumerable GetAllItems() - { - return _data.Values.SelectMany(partition => partition.Values); - } + _data = new ConcurrentDictionary>(); + _currentTimer = 0; + } - public IEnumerable GetItemsInPartition(PartitionKey? partitionKey) - { - // As of Microsoft.Azure.Cosmos v3 a null partition key causes a cross partition query - return partitionKey.HasValue - ? GetPartitionFromKey(partitionKey.Value).Values - : GetAllItems(); - } + public IEnumerable GetAllItems() + { + return _data.Values.SelectMany(partition => partition.Values); + } - public ContainerItem GetItem(string id, PartitionKey partitionKey) - { - var partition = GetPartitionFromKey(partitionKey); + public IEnumerable GetItemsInPartition(PartitionKey? partitionKey) + { + // As of Microsoft.Azure.Cosmos v3 a null partition key causes a cross partition query + return partitionKey.HasValue + ? GetPartitionFromKey(partitionKey.Value).Values + : GetAllItems(); + } - return partition.TryGetValue(id, out var containerItem) ? containerItem : null; - } + public ContainerItem GetItem(string id, PartitionKey partitionKey) + { + var partition = GetPartitionFromKey(partitionKey); - public async Task AddItem(string json, PartitionKey partitionKey, ItemRequestOptions requestOptions = null, CancellationToken cancellationToken = default) - { - var id = JsonHelpers.GetIdFromJson(json); - var partition = GetPartitionFromKey(partitionKey); + return partition.TryGetValue(id, out var containerItem) ? containerItem : null; + } - await _updateSemaphore.WaitAsync(cancellationToken); - try - { - if (partition.ContainsKey(id)) - { - throw new ObjectAlreadyExistsException(); - } + public async Task AddItem(string json, PartitionKey partitionKey, ItemRequestOptions requestOptions = null, CancellationToken cancellationToken = default) + { + var id = JsonHelpers.GetIdFromJson(json); + var partition = GetPartitionFromKey(partitionKey); - return await UpsertItem(json, partitionKey, requestOptions, cancellationToken); - } - finally + await _updateSemaphore.WaitAsync(cancellationToken); + try + { + if (partition.ContainsKey(id)) { - _updateSemaphore.Release(); + throw new ObjectAlreadyExistsException(); } - } - public async Task UpsertItem(string json, PartitionKey partitionKey, ItemRequestOptions requestOptions, CancellationToken cancellationToken) + return await UpsertItem(json, partitionKey, requestOptions, cancellationToken); + } + finally { - var partition = GetPartitionFromKey(partitionKey); - var id = JsonHelpers.GetIdFromJson(json); - var ttl = JsonHelpers.GetTtlFromJson(json, _defaultDocumentTimeToLive); + _updateSemaphore.Release(); + } + } + + public async Task UpsertItem(string json, PartitionKey partitionKey, ItemRequestOptions requestOptions, CancellationToken cancellationToken) + { + var partition = GetPartitionFromKey(partitionKey); + var id = JsonHelpers.GetIdFromJson(json); + var ttl = JsonHelpers.GetTtlFromJson(json, _defaultDocumentTimeToLive); - GuardAgainstInvalidId(id); + GuardAgainstInvalidId(id); - var isUpdate = partition.TryGetValue(id, out var existingItem); + var isUpdate = partition.TryGetValue(id, out var existingItem); - if (existingItem != null) + if (existingItem != null) + { + if (existingItem.RequireETagOnNextUpdate) { - if (existingItem.RequireETagOnNextUpdate) - { - if (string.IsNullOrWhiteSpace(requestOptions?.IfMatchEtag)) - { - throw new InvalidOperationException("An eTag must be provided as a concurrency exception is queued"); - } - } - - if (existingItem.HasScheduledETagMismatch) + if (string.IsNullOrWhiteSpace(requestOptions?.IfMatchEtag)) { - existingItem.ChangeETag(); - throw new ETagMismatchException(); + throw new InvalidOperationException("An eTag must be provided as a concurrency exception is queued"); } } - if (IsUniqueKeyViolation(json, partition.Values.Where(i => i.Id != id))) - { - throw new UniqueConstraintViolationException(); - } - - if (isUpdate && requestOptions?.IfMatchEtag != null && requestOptions.IfMatchEtag != existingItem!.ETag) + if (existingItem.HasScheduledETagMismatch) { + existingItem.ChangeETag(); throw new ETagMismatchException(); } + } - var newItem = new ContainerItem( - id, - json, - partitionKey, - GetExpiryTime(ttl, _currentTimer) - ); + if (IsUniqueKeyViolation(json, partition.Values.Where(i => i.Id != id))) + { + throw new UniqueConstraintViolationException(); + } - partition[id] = newItem; + if (isUpdate && requestOptions?.IfMatchEtag != null && requestOptions.IfMatchEtag != existingItem!.ETag) + { + throw new ETagMismatchException(); + } - if (DocumentChanged != null) - { - await DocumentChanged.Invoke(new List { newItem.Deserialize() }, cancellationToken); - } + var newItem = new ContainerItem( + id, + json, + partitionKey, + GetExpiryTime(ttl, _currentTimer) + ); - return new Response(newItem, isUpdate); - } + partition[id] = newItem; - public async Task ReplaceItem(string id, string json, PartitionKey partitionKey, ItemRequestOptions requestOptions, CancellationToken cancellationToken) + if (DocumentChanged != null) { - var partition = GetPartitionFromKey(partitionKey); + await DocumentChanged.Invoke(new List { newItem.Deserialize() }, cancellationToken); + } - if (!partition.TryGetValue(id, out _)) - { - throw new NotFoundException(); - } + return new Response(newItem, isUpdate); + } - return await UpsertItem(json, partitionKey, requestOptions, cancellationToken); - } + public async Task ReplaceItem(string id, string json, PartitionKey partitionKey, ItemRequestOptions requestOptions, CancellationToken cancellationToken) + { + var partition = GetPartitionFromKey(partitionKey); - public void RemoveItem(string id, PartitionKey partitionKey, ItemRequestOptions requestOptions) + if (!partition.TryGetValue(id, out _)) { - var existingItem = GetItem(id, partitionKey); - if (existingItem == null) - { - throw new NotFoundException(); - } + throw new NotFoundException(); + } - if (requestOptions?.IfMatchEtag != null && requestOptions.IfMatchEtag != existingItem.ETag) - { - throw new ETagMismatchException(); - } + return await UpsertItem(json, partitionKey, requestOptions, cancellationToken); + } - RemoveItemInternal(id, partitionKey); + public void RemoveItem(string id, PartitionKey partitionKey, ItemRequestOptions requestOptions) + { + var existingItem = GetItem(id, partitionKey); + if (existingItem == null) + { + throw new NotFoundException(); } - private void RemoveItemInternal(string id, PartitionKey partitionKey) + if (requestOptions?.IfMatchEtag != null && requestOptions.IfMatchEtag != existingItem.ETag) { - var partition = GetPartitionFromKey(partitionKey); - - partition.Remove(id, out _); + throw new ETagMismatchException(); } - private void RemoveExpiredItems() - { - foreach (var partition in _data.Values) - { - foreach (var item in partition.Values) - { - if (!item.ExpiryTime.HasValue) - { - continue; - } + RemoveItemInternal(id, partitionKey); + } - if (_currentTimer < item.ExpiryTime.Value) - { - continue; - } + private void RemoveItemInternal(string id, PartitionKey partitionKey) + { + var partition = GetPartitionFromKey(partitionKey); - RemoveItemInternal(item.Id, item.PartitionKey); - } - } - } + partition.Remove(id, out _); + } - private bool IsUniqueKeyViolation(string json, IEnumerable otherItemsInPartition) + private void RemoveExpiredItems() + { + foreach (var partition in _data.Values) { - if (_uniqueKeyPolicy == null) + foreach (var item in partition.Values) { - return false; - } - - var items = otherItemsInPartition.ToList(); + if (!item.ExpiryTime.HasValue) + { + continue; + } - foreach (var uniqueKey in _uniqueKeyPolicy.UniqueKeys) - { - var uniqueKeyValue = JsonHelpers.GetUniqueKeyValueFromJson(json, uniqueKey); - if (items.Any(i => JsonHelpers.GetUniqueKeyValueFromJson(i.Json, uniqueKey).SetEquals(uniqueKeyValue))) + if (_currentTimer < item.ExpiryTime.Value) { - return true; + continue; } - } - return false; + RemoveItemInternal(item.Id, item.PartitionKey); + } } + } - private static void GuardAgainstInvalidId(string id) + private bool IsUniqueKeyViolation(string json, IEnumerable otherItemsInPartition) + { + if (_uniqueKeyPolicy == null) { - if (id.Contains("/") || id.Contains(@"\") || id.Contains("#") || id.Contains("?")) - { - throw new InvalidOperationException("CosmosDb does not escape the following characters: '/' '\\', '#', '?' in the URI when retrieving an item by ID. Please encode the ID to remove them"); - } + return false; } - private static void GuardAgainstInvalidUniqueKeyPolicy(UniqueKeyPolicy uniqueKeyPolicy) + var items = otherItemsInPartition.ToList(); + + foreach (var uniqueKey in _uniqueKeyPolicy.UniqueKeys) { - if (uniqueKeyPolicy == null) + var uniqueKeyValue = JsonHelpers.GetUniqueKeyValueFromJson(json, uniqueKey); + if (items.Any(i => JsonHelpers.GetUniqueKeyValueFromJson(i.Json, uniqueKey).SetEquals(uniqueKeyValue))) { - return; + return true; } + } - var uniqueKeyPaths = uniqueKeyPolicy.UniqueKeys - .SelectMany(i => i.Paths) - .Select(p => p.TrimStart('/')); + return false; + } - if (uniqueKeyPaths.Contains("id")) - { - throw new CosmosException( - "The unique key path cannot contain system properties. 'id' is a system property.", - HttpStatusCode.BadRequest, - 0, - String.Empty, - 0 - ); - } + private static void GuardAgainstInvalidId(string id) + { + if (id.Contains("/") || id.Contains(@"\") || id.Contains("#") || id.Contains("?")) + { + throw new InvalidOperationException("CosmosDb does not escape the following characters: '/' '\\', '#', '?' in the URI when retrieving an item by ID. Please encode the ID to remove them"); } + } - private ConcurrentDictionary GetPartitionFromKey(PartitionKey partitionKey) + private static void GuardAgainstInvalidUniqueKeyPolicy(UniqueKeyPolicy uniqueKeyPolicy) + { + if (uniqueKeyPolicy == null) { - var normalizedPartitionKey = partitionKey; + return; + } - if (partitionKey == PartitionKey.None) - { - normalizedPartitionKey = NonePartitionKey; - } - else if (partitionKey == PartitionKey.Null) - { - normalizedPartitionKey = NullPartitionKey; - } + var uniqueKeyPaths = uniqueKeyPolicy.UniqueKeys + .SelectMany(i => i.Paths) + .Select(p => p.TrimStart('/')); - return _data.GetOrAdd(normalizedPartitionKey, new ConcurrentDictionary()); + if (uniqueKeyPaths.Contains("id")) + { + throw new CosmosException( + "The unique key path cannot contain system properties. 'id' is a system property.", + HttpStatusCode.BadRequest, + 0, + String.Empty, + 0 + ); } + } - private static int? GetExpiryTime(int ttl, int currentTimer) - { - if (ttl < 0) - { - return null; - } + private ConcurrentDictionary GetPartitionFromKey(PartitionKey partitionKey) + { + var normalizedPartitionKey = partitionKey; - return currentTimer + ttl; + if (partitionKey == PartitionKey.None) + { + normalizedPartitionKey = NonePartitionKey; } - - public void Clear() + else if (partitionKey == PartitionKey.Null) { - _data.Clear(); + normalizedPartitionKey = NullPartitionKey; + } + + return _data.GetOrAdd(normalizedPartitionKey, new ConcurrentDictionary()); + } - _currentTimer = 0; + private static int? GetExpiryTime(int ttl, int currentTimer) + { + if (ttl < 0) + { + return null; } - public void AdvanceTime(int seconds) + return currentTimer + ttl; + } + + public void Clear() + { + _data.Clear(); + + _currentTimer = 0; + } + + public void AdvanceTime(int seconds) + { + if (seconds < 0) { - if (seconds < 0) - { - throw new ArgumentException("Seconds must be a positive value", nameof(seconds)); - } + throw new ArgumentException("Seconds must be a positive value", nameof(seconds)); + } - _currentTimer += seconds; + _currentTimer += seconds; - RemoveExpiredItems(); - } + RemoveExpiredItems(); } } \ No newline at end of file diff --git a/CosmosTestHelpers/ContainerMockData/ContainerItem.cs b/CosmosTestHelpers/ContainerMockData/ContainerItem.cs index 72d2d5e..ce45d99 100644 --- a/CosmosTestHelpers/ContainerMockData/ContainerItem.cs +++ b/CosmosTestHelpers/ContainerMockData/ContainerItem.cs @@ -1,48 +1,47 @@ using Microsoft.Azure.Cosmos; using Newtonsoft.Json; -namespace CosmosTestHelpers.ContainerMockData +namespace CosmosTestHelpers.ContainerMockData; + +internal class ContainerItem { - internal class ContainerItem + public ContainerItem(string id, string json, PartitionKey partitionKey, int? expiryTime) { - public ContainerItem(string id, string json, PartitionKey partitionKey, int? expiryTime) - { - Id = id; - ExpiryTime = expiryTime; - Json = json; - PartitionKey = partitionKey; - ETag = GenerateETag(); - } + Id = id; + ExpiryTime = expiryTime; + Json = json; + PartitionKey = partitionKey; + ETag = GenerateETag(); + } - public PartitionKey PartitionKey { get; } - public string Id { get; } - public string Json { get; } - public string ETag { get; private set; } - public int? ExpiryTime { get; } + public PartitionKey PartitionKey { get; } + public string Id { get; } + public string Json { get; } + public string ETag { get; private set; } + public int? ExpiryTime { get; } - public bool RequireETagOnNextUpdate { get; private set; } - public bool HasScheduledETagMismatch { get; private set; } + public bool RequireETagOnNextUpdate { get; private set; } + public bool HasScheduledETagMismatch { get; private set; } - public T Deserialize() - { - return JsonConvert.DeserializeObject(Json); - } + public T Deserialize() + { + return JsonConvert.DeserializeObject(Json); + } - public void ScheduleMismatchETagOnNextUpdate() - { - RequireETagOnNextUpdate = true; - HasScheduledETagMismatch = true; - } + public void ScheduleMismatchETagOnNextUpdate() + { + RequireETagOnNextUpdate = true; + HasScheduledETagMismatch = true; + } - public void ChangeETag() - { - ETag = GenerateETag(); - HasScheduledETagMismatch = false; - } + public void ChangeETag() + { + ETag = GenerateETag(); + HasScheduledETagMismatch = false; + } - private static string GenerateETag() - { - return $"\"{Guid.NewGuid()}\""; - } + private static string GenerateETag() + { + return $"\"{Guid.NewGuid()}\""; } } \ No newline at end of file diff --git a/CosmosTestHelpers/ContainerMockData/ContainerMockException.cs b/CosmosTestHelpers/ContainerMockData/ContainerMockException.cs index e045d0d..c2c2dfe 100644 --- a/CosmosTestHelpers/ContainerMockData/ContainerMockException.cs +++ b/CosmosTestHelpers/ContainerMockData/ContainerMockException.cs @@ -2,39 +2,38 @@ using System.Runtime.Serialization; using Microsoft.Azure.Cosmos; -namespace CosmosTestHelpers.ContainerMockData +namespace CosmosTestHelpers.ContainerMockData; + +[Serializable] +public class ContainerMockException : Exception { - [Serializable] - public class ContainerMockException : Exception - { - public HttpStatusCode StatusCode { get; } + public HttpStatusCode StatusCode { get; } - public ContainerMockException(HttpStatusCode statusCode) - { - StatusCode = statusCode; - } + public ContainerMockException(HttpStatusCode statusCode) + { + StatusCode = statusCode; + } - public ContainerMockException(HttpStatusCode statusCode, string message) - : base(message) - { - StatusCode = statusCode; - } + public ContainerMockException(HttpStatusCode statusCode, string message) + : base(message) + { + StatusCode = statusCode; + } - public ContainerMockException(HttpStatusCode statusCode, string message, Exception inner) - : base(message, inner) - { - StatusCode = statusCode; - } + public ContainerMockException(HttpStatusCode statusCode, string message, Exception inner) + : base(message, inner) + { + StatusCode = statusCode; + } - protected ContainerMockException( - SerializationInfo info, - StreamingContext context) : base(info, context) - { - } + protected ContainerMockException( + SerializationInfo info, + StreamingContext context) : base(info, context) + { + } - public CosmosException ToCosmosException() - { - return new CosmosException(Message, StatusCode, 0, string.Empty, 0); - } + public CosmosException ToCosmosException() + { + return new CosmosException(Message, StatusCode, 0, string.Empty, 0); } } \ No newline at end of file diff --git a/CosmosTestHelpers/ContainerMockData/ETagMismatchException.cs b/CosmosTestHelpers/ContainerMockData/ETagMismatchException.cs index 2414c9e..ae32910 100644 --- a/CosmosTestHelpers/ContainerMockData/ETagMismatchException.cs +++ b/CosmosTestHelpers/ContainerMockData/ETagMismatchException.cs @@ -1,33 +1,32 @@ using System.Net; using System.Runtime.Serialization; -namespace CosmosTestHelpers.ContainerMockData +namespace CosmosTestHelpers.ContainerMockData; + +[Serializable] +public class ETagMismatchException : ContainerMockException { - [Serializable] - public class ETagMismatchException : ContainerMockException - { - public const HttpStatusCode DefaultStatusCode = HttpStatusCode.PreconditionFailed; + public const HttpStatusCode DefaultStatusCode = HttpStatusCode.PreconditionFailed; - public ETagMismatchException() - : base(DefaultStatusCode, "Precondition failed") - { - } + public ETagMismatchException() + : base(DefaultStatusCode, "Precondition failed") + { + } - public ETagMismatchException(string message) - : base(DefaultStatusCode, message) - { - } + public ETagMismatchException(string message) + : base(DefaultStatusCode, message) + { + } - public ETagMismatchException(string message, Exception inner) - : base(DefaultStatusCode, message, inner) - { - } + public ETagMismatchException(string message, Exception inner) + : base(DefaultStatusCode, message, inner) + { + } - protected ETagMismatchException( - SerializationInfo info, - StreamingContext context) - : base(info, context) - { - } + protected ETagMismatchException( + SerializationInfo info, + StreamingContext context) + : base(info, context) + { } } \ No newline at end of file diff --git a/CosmosTestHelpers/ContainerMockData/JsonHelpers.cs b/CosmosTestHelpers/ContainerMockData/JsonHelpers.cs index f327f04..cfdfd4a 100644 --- a/CosmosTestHelpers/ContainerMockData/JsonHelpers.cs +++ b/CosmosTestHelpers/ContainerMockData/JsonHelpers.cs @@ -1,83 +1,82 @@ using Microsoft.Azure.Cosmos; using Newtonsoft.Json.Linq; -namespace CosmosTestHelpers.ContainerMockData +namespace CosmosTestHelpers.ContainerMockData; + +internal static class JsonHelpers { - internal class JsonHelpers + public static string GetValueFromJson(string json, string path) { - public static string GetValueFromJson(string json, string path) - { - var jObject = JObject.Parse(json); - var jPath = path.Substring(1).Replace('/', '.'); - var jTokenProperty = jObject.SelectToken(jPath); - - if (jTokenProperty == null) - { - throw new ArgumentException($"Could not extract property key ({path}) from json: Path not found."); - } + var jObject = JObject.Parse(json); + var jPath = path.Substring(1).Replace('/', '.'); + var jTokenProperty = jObject.SelectToken(jPath); - if (jTokenProperty.Type != JTokenType.String && jTokenProperty.Type != JTokenType.Null) - { - throw new ArgumentException($"Could not extract property key ({path}) from json. Only string or null values supported."); - } - - return jTokenProperty.Type == JTokenType.Null - ? null - : jTokenProperty.Value(); + if (jTokenProperty == null) + { + throw new ArgumentException($"Could not extract property key ({path}) from json: Path not found."); } - public static string GetIdFromJson(string json) + if (jTokenProperty.Type != JTokenType.String && jTokenProperty.Type != JTokenType.Null) { - return GetValueFromJson(json, "/id"); + throw new ArgumentException($"Could not extract property key ({path}) from json. Only string or null values supported."); } - public static int GetTtlFromJson(string json, int defaultDocumentTimeToLive) - { - var ttl = (int?)null; + return jTokenProperty.Type == JTokenType.Null + ? null + : jTokenProperty.Value(); + } - var jObject = JObject.Parse(json); - var jTokenProperty = jObject.SelectToken("ttl"); + public static string GetIdFromJson(string json) + { + return GetValueFromJson(json, "/id"); + } - if (jTokenProperty != null) - { - if (jTokenProperty.Type != JTokenType.Integer && jTokenProperty.Type != JTokenType.Null) - { - //TODO: What does real Cosmos do in this situation? - throw new ArgumentException($"Could not extract ttl from json. Only int or null values supported.", nameof(json)); - } + public static int GetTtlFromJson(string json, int defaultDocumentTimeToLive) + { + var ttl = (int?)null; + + var jObject = JObject.Parse(json); + var jTokenProperty = jObject.SelectToken("ttl"); - ttl = jTokenProperty.Type == JTokenType.Null ? (int?)null : jTokenProperty.Value(); + if (jTokenProperty != null) + { + if (jTokenProperty.Type != JTokenType.Integer && jTokenProperty.Type != JTokenType.Null) + { + //TODO: What does real Cosmos do in this situation? + throw new ArgumentException($"Could not extract ttl from json. Only int or null values supported.", nameof(json)); } - return ttl ?? defaultDocumentTimeToLive; + ttl = jTokenProperty.Type == JTokenType.Null ? (int?)null : jTokenProperty.Value(); } - public static ISet<(string path, string value)> GetUniqueKeyValueFromJson(string json, UniqueKey uniqueKey) - { - var uniqueKeyValue = new HashSet<(string, string)>(); - var jObject = JObject.Parse(json); + return ttl ?? defaultDocumentTimeToLive; + } - foreach (var path in uniqueKey.Paths) - { - var jPath = path.Substring(1).Replace('/', '.'); - var property = jObject.SelectToken(jPath); + public static ISet<(string path, string value)> GetUniqueKeyValueFromJson(string json, UniqueKey uniqueKey) + { + var uniqueKeyValue = new HashSet<(string, string)>(); + var jObject = JObject.Parse(json); - if (property == null) - { - throw new InvalidOperationException("Unique key not present"); - } + foreach (var path in uniqueKey.Paths) + { + var jPath = path.Substring(1).Replace('/', '.'); + var property = jObject.SelectToken(jPath); - var pathValue = property.Value(); + if (property == null) + { + throw new InvalidOperationException("Unique key not present"); + } - if (pathValue == null) - { - throw new InvalidOperationException("Unique key is null"); - } + var pathValue = property.Value(); - uniqueKeyValue.Add((path, pathValue)); + if (pathValue == null) + { + throw new InvalidOperationException("Unique key is null"); } - return uniqueKeyValue; + uniqueKeyValue.Add((path, pathValue)); } + + return uniqueKeyValue; } } \ No newline at end of file diff --git a/CosmosTestHelpers/ContainerMockData/NotFoundException.cs b/CosmosTestHelpers/ContainerMockData/NotFoundException.cs index 445a180..e58533e 100644 --- a/CosmosTestHelpers/ContainerMockData/NotFoundException.cs +++ b/CosmosTestHelpers/ContainerMockData/NotFoundException.cs @@ -1,33 +1,32 @@ using System.Net; using System.Runtime.Serialization; -namespace CosmosTestHelpers.ContainerMockData +namespace CosmosTestHelpers.ContainerMockData; + +[Serializable] +public class NotFoundException : ContainerMockException { - [Serializable] - public class NotFoundException : ContainerMockException - { - public const HttpStatusCode DefaultStatusCode = HttpStatusCode.NotFound; + public const HttpStatusCode DefaultStatusCode = HttpStatusCode.NotFound; - public NotFoundException() - : base(DefaultStatusCode, "Not found") - { - } + public NotFoundException() + : base(DefaultStatusCode, "Not found") + { + } - public NotFoundException(string message) - : base(DefaultStatusCode, message) - { - } + public NotFoundException(string message) + : base(DefaultStatusCode, message) + { + } - public NotFoundException(string message, Exception inner) - : base(DefaultStatusCode, message, inner) - { - } + public NotFoundException(string message, Exception inner) + : base(DefaultStatusCode, message, inner) + { + } - protected NotFoundException( - SerializationInfo info, - StreamingContext context) - : base(info, context) - { - } + protected NotFoundException( + SerializationInfo info, + StreamingContext context) + : base(info, context) + { } } \ No newline at end of file diff --git a/CosmosTestHelpers/ContainerMockData/ObjectAlreadyExistsException.cs b/CosmosTestHelpers/ContainerMockData/ObjectAlreadyExistsException.cs index bb04c8b..b3e59cb 100644 --- a/CosmosTestHelpers/ContainerMockData/ObjectAlreadyExistsException.cs +++ b/CosmosTestHelpers/ContainerMockData/ObjectAlreadyExistsException.cs @@ -1,33 +1,32 @@ using System.Net; using System.Runtime.Serialization; -namespace CosmosTestHelpers.ContainerMockData +namespace CosmosTestHelpers.ContainerMockData; + +[Serializable] +public class ObjectAlreadyExistsException : ContainerMockException { - [Serializable] - public class ObjectAlreadyExistsException : ContainerMockException - { - public const HttpStatusCode DefaultStatusCode = HttpStatusCode.Conflict; + public const HttpStatusCode DefaultStatusCode = HttpStatusCode.Conflict; - public ObjectAlreadyExistsException() - : base(DefaultStatusCode, "Object already exists.") - { - } + public ObjectAlreadyExistsException() + : base(DefaultStatusCode, "Object already exists.") + { + } - public ObjectAlreadyExistsException(string message) - : base(DefaultStatusCode, message) - { - } + public ObjectAlreadyExistsException(string message) + : base(DefaultStatusCode, message) + { + } - public ObjectAlreadyExistsException(string message, Exception inner) - : base(DefaultStatusCode, message, inner) - { - } + public ObjectAlreadyExistsException(string message, Exception inner) + : base(DefaultStatusCode, message, inner) + { + } - protected ObjectAlreadyExistsException( - SerializationInfo info, - StreamingContext context) - : base(info, context) - { - } + protected ObjectAlreadyExistsException( + SerializationInfo info, + StreamingContext context) + : base(info, context) + { } } \ No newline at end of file diff --git a/CosmosTestHelpers/ContainerMockData/Response.cs b/CosmosTestHelpers/ContainerMockData/Response.cs index 1c26876..b36d150 100644 --- a/CosmosTestHelpers/ContainerMockData/Response.cs +++ b/CosmosTestHelpers/ContainerMockData/Response.cs @@ -1,14 +1,13 @@ -namespace CosmosTestHelpers.ContainerMockData +namespace CosmosTestHelpers.ContainerMockData; + +internal class Response { - internal class Response + public Response(ContainerItem item, bool isUpdate) { - public Response(ContainerItem item, bool isUpdate) - { - Item = item; - IsUpdate = isUpdate; - } - - public ContainerItem Item { get; } - public bool IsUpdate { get; } + Item = item; + IsUpdate = isUpdate; } + + public ContainerItem Item { get; } + public bool IsUpdate { get; } } \ No newline at end of file diff --git a/CosmosTestHelpers/ContainerMockData/UniqueConstraintViolationException.cs b/CosmosTestHelpers/ContainerMockData/UniqueConstraintViolationException.cs index 47fefd7..5012be7 100644 --- a/CosmosTestHelpers/ContainerMockData/UniqueConstraintViolationException.cs +++ b/CosmosTestHelpers/ContainerMockData/UniqueConstraintViolationException.cs @@ -1,33 +1,32 @@ using System.Net; using System.Runtime.Serialization; -namespace CosmosTestHelpers.ContainerMockData +namespace CosmosTestHelpers.ContainerMockData; + +[Serializable] +public class UniqueConstraintViolationException : ContainerMockException { - [Serializable] - public class UniqueConstraintViolationException : ContainerMockException - { - public const HttpStatusCode DefaultStatusCode = HttpStatusCode.Conflict; + public const HttpStatusCode DefaultStatusCode = HttpStatusCode.Conflict; - public UniqueConstraintViolationException() - : base(DefaultStatusCode, "Conflict") - { - } + public UniqueConstraintViolationException() + : base(DefaultStatusCode, "Conflict") + { + } - public UniqueConstraintViolationException(string message) - : base(DefaultStatusCode, message) - { - } + public UniqueConstraintViolationException(string message) + : base(DefaultStatusCode, message) + { + } - public UniqueConstraintViolationException(string message, Exception inner) - : base(DefaultStatusCode, message, inner) - { - } + public UniqueConstraintViolationException(string message, Exception inner) + : base(DefaultStatusCode, message, inner) + { + } - protected UniqueConstraintViolationException( - SerializationInfo info, - StreamingContext context) - : base(info, context) - { - } + protected UniqueConstraintViolationException( + SerializationInfo info, + StreamingContext context) + : base(info, context) + { } } \ No newline at end of file diff --git a/CosmosTestHelpers/CosmosExpressionValidator.cs b/CosmosTestHelpers/CosmosExpressionValidator.cs deleted file mode 100644 index 439e46d..0000000 --- a/CosmosTestHelpers/CosmosExpressionValidator.cs +++ /dev/null @@ -1,158 +0,0 @@ -using System.Linq.Expressions; -using System.Net; -using System.Reflection; -using Microsoft.Azure.Cosmos; -using Microsoft.Azure.Cosmos.Linq; - -namespace CosmosTestHelpers -{ - /// - /// This class can be used to rewrite the in memory queries to ensure the results match those from running ComosDb queries. - /// Please write integration tests for anything you implement. - /// - public class CosmosExpressionValidator : ExpressionVisitor - { - private static readonly Dictionary> AllowList = new Dictionary> - { - { typeof(string), new List { "Concat", "Contains", "Count", "EndsWith", "IndexOf", "Replace", "Reverse", "StartsWith", "SubString", "ToLower", "ToUpper", "TrimEnd", "TrimStart" } }, - { typeof(Math), new List { "Abs", " Acos", " Asin", " Atan", " Ceiling", " Cos", " Exp", " Floor", " Log", " Log10", " Pow", " Round", " Sign", " Sin", " Sqrt", " Tan", " Truncate" } }, - { typeof(Array), new List { "Concat", "Contains", "Count" } }, - { typeof(Queryable), new List { "Select", "Contains", "Where", "Single", "SelectMany", "OrderBy", "OrderByDescending", "ThenBy", "ThenByDescending", "Count", "Sum", "Min", "Max", "Average", "CountAsync", "SumAsync", "MinAsync", "MaxAsync", "AverageAsync", "Skip", "Take" } }, - // Any is only on enumerable as it is supported as a subquery but not as an aggregation - { typeof(Enumerable), new List { "Select", "Contains", "Where", "Single", "SelectMany", "OrderBy", "OrderByDescending", "ThenBy", "ThenByDescending", "Count", "Sum", "Min", "Max", "Average", "CountAsync", "SumAsync", "MinAsync", "MaxAsync", "AverageAsync", "Skip", "Take", "Any" } }, - { typeof(object), new List { "ToString" } } - }; - - private static readonly MethodInfo _isNull = typeof(CosmosLinqExtensions).GetMethod(nameof(CosmosLinqExtensions.IsNull)); - - private static readonly MethodInfo _isDefined = typeof(CosmosLinqExtensions).GetMethod(nameof(CosmosLinqExtensions.IsDefined)); - - public override Expression Visit(Expression node) - { - return base.Visit(node); - } - - protected override Expression VisitMember(MemberExpression node) - { - var underlyingType = Nullable.GetUnderlyingType(node.Type); - if (node.NodeType == ExpressionType.MemberAccess && underlyingType != null && underlyingType.IsEnum) - { - return base.VisitMember(node); - } - - return base.VisitMember(node); - } - - protected override Expression VisitMethodCall(MethodCallExpression node) - { - GuardInvalidMethods(node); - - if (HandleIsNullAndIsDefined(node, out var replacementExpression)) - { - return base.Visit(replacementExpression); - } - - return base.VisitMethodCall(node); - } - - protected override Expression VisitBinary(BinaryExpression node) - { - if (node.NodeType == ExpressionType.ExclusiveOr) - { - return Expression.Equal(Expression.Constant(true), Expression.Constant(false)); - } - - return base.VisitBinary(HandleNullableEnumEquality(node)); - } - - protected override Expression VisitUnary(UnaryExpression node) - { - if (node.NodeType == ExpressionType.Not && node.Operand.NodeType == ExpressionType.ExclusiveOr) - { - return Expression.Equal(Expression.Constant(true), Expression.Constant(false)); - } - - return base.VisitUnary(node); - } - - private static bool HandleIsNullAndIsDefined(MethodCallExpression methodCallExpression, out Expression replacementExpression) - { - if (methodCallExpression.Method == _isDefined) - { - replacementExpression = Expression.Constant(true); - return true; - } - - if (methodCallExpression.Method == _isNull) - { - replacementExpression = Expression.Equal(methodCallExpression.Arguments.SingleOrDefault() ?? Expression.Constant(null), Expression.Constant(null)); - return true; - } - - replacementExpression = null; - return false; - } - - /// - /// Cosmos does not error if you compare the value of a null enum to null but c# does. - /// This substitutes a dummy value to avoid the null reference exceptions - /// - private static BinaryExpression HandleNullableEnumEquality(BinaryExpression node) - { - BinaryExpression HandleNullableEnumEquality(ExpressionType nodeType, Expression modelSide, Expression constantSide) - { - if (modelSide.NodeType == ExpressionType.Convert && modelSide.Type == typeof(int?) && modelSide is UnaryExpression leftConvert && constantSide.NodeType == ExpressionType.Convert && constantSide is UnaryExpression rightConvert) - { - var enumType = Nullable.GetUnderlyingType(leftConvert.Operand.Type) ?? leftConvert.Operand.Type; - if (leftConvert.Operand.NodeType == ExpressionType.MemberAccess && enumType.IsEnum && leftConvert.Operand is MemberExpression leftMember && leftMember.Member.Name == "Value") - { - var nullableEnumType = typeof(Nullable<>).MakeGenericType(enumType); - var convertedDefaultValueForNullableEnum = Expression.Convert(Expression.Constant(int.MinValue), nullableEnumType); - - var coalesce = Expression.Coalesce(leftMember.Expression, convertedDefaultValueForNullableEnum); - var coalescedConvert = Expression.Convert(Expression.MakeMemberAccess(coalesce, leftMember.Member), modelSide.Type); - - var coalescedConstant = Expression.Convert(Expression.Coalesce(rightConvert.Operand, convertedDefaultValueForNullableEnum), typeof(int?)); - - return nodeType == ExpressionType.Equal ? Expression.Equal(coalescedConvert, coalescedConstant) : Expression.NotEqual(coalescedConvert, coalescedConstant); - } - } - - return nodeType == ExpressionType.Equal ? Expression.Equal(modelSide, constantSide) : Expression.NotEqual(modelSide, constantSide); - } - - if (node.NodeType == ExpressionType.Equal || node.NodeType == ExpressionType.NotEqual) - { - node = HandleNullableEnumEquality(node.NodeType, node.Left, node.Right); - node = HandleNullableEnumEquality(node.NodeType, node.Right, node.Left); - } - - return node; - } - - private void GuardInvalidMethods(MethodCallExpression node) - { - if (node.Method.DeclaringType == typeof(ContainerMock)) - { - return; - } - - if (!AllowList.ContainsKey(node.Method.DeclaringType)) - { - var nodeObject = node.Object; - if (node.Method.ReturnType.IsPrimitive && (nodeObject == null || nodeObject.Type != typeof(TModel))) - { - return; - } - - throw new CosmosException($"Methods from {node.Method.DeclaringType} are not supported by Cosmos", HttpStatusCode.BadRequest, 0, string.Empty, 0); - } - - var allowList = AllowList[node.Method.DeclaringType]; - if (!allowList.Contains(node.Method.Name)) - { - throw new CosmosException($"{node.Method.DeclaringType}.{node.Method.Name} is not supported by Cosmos", HttpStatusCode.BadRequest, 0, string.Empty, 0); - } - } - } -} \ No newline at end of file diff --git a/CosmosTestHelpers/CosmosExpressionValidatorConstants.cs b/CosmosTestHelpers/CosmosExpressionValidatorConstants.cs new file mode 100644 index 0000000..424715d --- /dev/null +++ b/CosmosTestHelpers/CosmosExpressionValidatorConstants.cs @@ -0,0 +1,155 @@ +using System.Linq.Expressions; +using System.Net; +using System.Reflection; +using Microsoft.Azure.Cosmos; +using Microsoft.Azure.Cosmos.Linq; + +namespace CosmosTestHelpers; + +/// +/// This class can be used to rewrite the in memory queries to ensure the results match those from running ComosDb queries. +/// Please write integration tests for anything you implement. +/// +public class CosmosExpressionValidator : ExpressionVisitor +{ + protected override Expression VisitMember(MemberExpression node) + { + var underlyingType = Nullable.GetUnderlyingType(node.Type); + if (node.NodeType == ExpressionType.MemberAccess && underlyingType is { IsEnum: true }) + { + return base.VisitMember(node); + } + + return base.VisitMember(node); + } + + protected override Expression VisitMethodCall(MethodCallExpression node) + { + GuardInvalidMethods(node); + + if (HandleIsNullAndIsDefined(node, out var replacementExpression)) + { + return base.Visit(replacementExpression); + } + + return base.VisitMethodCall(node); + } + + protected override Expression VisitBinary(BinaryExpression node) + { + if (node.NodeType == ExpressionType.ExclusiveOr) + { + return Expression.Equal(Expression.Constant(true), Expression.Constant(false)); + } + + return base.VisitBinary(HandleNullableEnumEquality(node)); + } + + protected override Expression VisitUnary(UnaryExpression node) + { + if (node.NodeType == ExpressionType.Not && node.Operand.NodeType == ExpressionType.ExclusiveOr) + { + return Expression.Equal(Expression.Constant(true), Expression.Constant(false)); + } + + return base.VisitUnary(node); + } + + private static bool HandleIsNullAndIsDefined(MethodCallExpression methodCallExpression, out Expression replacementExpression) + { + if (methodCallExpression.Method == CosmosExpressionValidatorConstants.IsDefined) + { + replacementExpression = Expression.Constant(true); + return true; + } + + if (methodCallExpression.Method == CosmosExpressionValidatorConstants.IsNull) + { + replacementExpression = Expression.Equal(methodCallExpression.Arguments.SingleOrDefault() ?? Expression.Constant(null), Expression.Constant(null)); + return true; + } + + replacementExpression = null; + return false; + } + + /// + /// Cosmos does not error if you compare the value of a null enum to null but c# does. + /// This substitutes a dummy value to avoid the null reference exceptions + /// + private static BinaryExpression HandleNullableEnumEquality(BinaryExpression node) + { + BinaryExpression HandleNullableEnumEqualityInner(ExpressionType nodeType, Expression modelSide, Expression constantSide) + { + if (modelSide.NodeType == ExpressionType.Convert && modelSide.Type == typeof(int?) && modelSide is UnaryExpression leftConvert && constantSide.NodeType == ExpressionType.Convert && constantSide is UnaryExpression rightConvert) + { + var enumType = Nullable.GetUnderlyingType(leftConvert.Operand.Type) ?? leftConvert.Operand.Type; + if (leftConvert.Operand.NodeType == ExpressionType.MemberAccess && enumType.IsEnum && leftConvert.Operand is MemberExpression leftMember && leftMember.Member.Name == "Value") + { + var nullableEnumType = typeof(Nullable<>).MakeGenericType(enumType); + var convertedDefaultValueForNullableEnum = Expression.Convert(Expression.Constant(int.MinValue), nullableEnumType); + + var coalesce = Expression.Coalesce(leftMember.Expression, convertedDefaultValueForNullableEnum); + var coalescedConvert = Expression.Convert(Expression.MakeMemberAccess(coalesce, leftMember.Member), modelSide.Type); + + var coalescedConstant = Expression.Convert(Expression.Coalesce(rightConvert.Operand, convertedDefaultValueForNullableEnum), typeof(int?)); + + return nodeType == ExpressionType.Equal ? Expression.Equal(coalescedConvert, coalescedConstant) : Expression.NotEqual(coalescedConvert, coalescedConstant); + } + } + + return nodeType == ExpressionType.Equal ? Expression.Equal(modelSide, constantSide) : Expression.NotEqual(modelSide, constantSide); + } + + if (node.NodeType == ExpressionType.Equal || node.NodeType == ExpressionType.NotEqual) + { + node = HandleNullableEnumEqualityInner(node.NodeType, node.Left, node.Right); + node = HandleNullableEnumEqualityInner(node.NodeType, node.Right, node.Left); + } + + return node; + } + + private void GuardInvalidMethods(MethodCallExpression node) + { + if (node.Method.DeclaringType == typeof(ContainerMock)) + { + return; + } + + if (!CosmosExpressionValidatorConstants.AllowList.ContainsKey(node.Method.DeclaringType)) + { + var nodeObject = node.Object; + if (node.Method.ReturnType.IsPrimitive && (nodeObject == null || nodeObject.Type != typeof(TModel))) + { + return; + } + + throw new CosmosException($"Methods from {node.Method.DeclaringType} are not supported by Cosmos", HttpStatusCode.BadRequest, 0, string.Empty, 0); + } + + var allowList = CosmosExpressionValidatorConstants.AllowList[node.Method.DeclaringType]; + if (!allowList.Contains(node.Method.Name)) + { + throw new CosmosException($"{node.Method.DeclaringType}.{node.Method.Name} is not supported by Cosmos", HttpStatusCode.BadRequest, 0, string.Empty, 0); + } + } +} + +internal static class CosmosExpressionValidatorConstants +{ + public static readonly Dictionary> AllowList = new() + { + { typeof(string), new List { "Concat", "Contains", "Count", "EndsWith", "IndexOf", "Replace", "Reverse", "StartsWith", "SubString", "ToLower", "ToUpper", "TrimEnd", "TrimStart" } }, + { typeof(Math), new List { "Abs", "Acos", "Asin", "Atan", "Ceiling", "Cos", "Exp", "Floor", "Log", "Log10", "Pow", "Round", "Sign", "Sin", "Sqrt", "Tan", "Truncate" } }, + { typeof(Array), new List { "Concat", "Contains", "Count" } }, + { typeof(Queryable), new List { "Select", "Contains", "Where", "Single", "SelectMany", "OrderBy", "OrderByDescending", "ThenBy", "ThenByDescending", "Count", "Sum", "Min", "Max", "Average", "CountAsync", "SumAsync", "MinAsync", "MaxAsync", "AverageAsync", "Skip", "Take" } }, + // Any is only on enumerable as it is supported as a sub-query but not as an aggregation + { typeof(Enumerable), new List { "Select", "Contains", "Where", "Single", "SelectMany", "OrderBy", "OrderByDescending", "ThenBy", "ThenByDescending", "Count", "Sum", "Min", "Max", "Average", "CountAsync", "SumAsync", "MinAsync", "MaxAsync", "AverageAsync", "Skip", "Take", "Any" } }, + { typeof(object), new List { "ToString" } } + }; + + public static readonly MethodInfo IsNull = typeof(CosmosLinqExtensions).GetMethod(nameof(CosmosLinqExtensions.IsNull)); + + public static readonly MethodInfo IsDefined = typeof(CosmosLinqExtensions).GetMethod(nameof(CosmosLinqExtensions.IsDefined)); +} \ No newline at end of file diff --git a/CosmosTestHelpers/CosmosQueryableMock.cs b/CosmosTestHelpers/CosmosQueryableMock.cs index bb54613..2f4130a 100644 --- a/CosmosTestHelpers/CosmosQueryableMock.cs +++ b/CosmosTestHelpers/CosmosQueryableMock.cs @@ -1,65 +1,64 @@ using System.Collections; using System.Linq.Expressions; -namespace CosmosTestHelpers +namespace CosmosTestHelpers; + +public class CosmosQueryableMock : IOrderedQueryable, IQueryProvider { - public class CosmosQueryableMock : IOrderedQueryable, IQueryProvider - { - private EnumerableQuery _underlying; + private EnumerableQuery _underlying; - public CosmosQueryableMock(IQueryable partition) - { - _underlying = new EnumerableQuery(partition.Expression); - } + public CosmosQueryableMock(IQueryable partition) + { + _underlying = new EnumerableQuery(partition.Expression); + } - public IEnumerator GetEnumerator() - { - var expression = ValidateExpression(Expression); - _underlying = new EnumerableQuery(expression); - return ((IOrderedQueryable)_underlying).GetEnumerator(); - } + public IEnumerator GetEnumerator() + { + var expression = ValidateExpression(Expression); + _underlying = new EnumerableQuery(expression); + return ((IOrderedQueryable)_underlying).GetEnumerator(); + } - IEnumerator IEnumerable.GetEnumerator() - { - var expression = ValidateExpression(Expression); - _underlying = new EnumerableQuery(expression); - return((IEnumerable)_underlying).GetEnumerator(); - } + IEnumerator IEnumerable.GetEnumerator() + { + var expression = ValidateExpression(Expression); + _underlying = new EnumerableQuery(expression); + return((IEnumerable)_underlying).GetEnumerator(); + } - public Type ElementType => ((IQueryable)_underlying).ElementType; + public Type ElementType => ((IQueryable)_underlying).ElementType; - public Expression Expression => ((IQueryable)_underlying).Expression; + public Expression Expression => ((IQueryable)_underlying).Expression; - public IQueryProvider Provider => this; + public IQueryProvider Provider => this; - public IQueryable CreateQuery(Expression expression) - { - return((IQueryProvider)_underlying).CreateQuery(expression); - } + public IQueryable CreateQuery(Expression expression) + { + return((IQueryProvider)_underlying).CreateQuery(expression); + } - public IQueryable CreateQuery(Expression expression) - { - var queryProvider = ((IQueryProvider)_underlying).CreateQuery(expression); - return new CosmosQueryableMock(queryProvider); - } + public IQueryable CreateQuery(Expression expression) + { + var queryProvider = ((IQueryProvider)_underlying).CreateQuery(expression); + return new CosmosQueryableMock(queryProvider); + } - public object Execute(Expression expression) - { - expression = ValidateExpression(expression); - return ((IQueryProvider)_underlying).Execute(expression); - } + public object Execute(Expression expression) + { + expression = ValidateExpression(expression); + return ((IQueryProvider)_underlying).Execute(expression); + } - public TResult Execute(Expression expression) - { - expression = ValidateExpression(expression); - return ((IQueryProvider)_underlying).Execute(expression); - } + public TResult Execute(Expression expression) + { + expression = ValidateExpression(expression); + return ((IQueryProvider)_underlying).Execute(expression); + } - private Expression ValidateExpression(Expression expression) - { - var validator = new CosmosExpressionValidator(); - var newExpression = validator.Visit(expression); - return newExpression; - } + private Expression ValidateExpression(Expression expression) + { + var validator = new CosmosExpressionValidator(); + var newExpression = validator.Visit(expression); + return newExpression; } } \ No newline at end of file diff --git a/CosmosTestHelpers/CosmosTestHelpers.csproj b/CosmosTestHelpers/CosmosTestHelpers.csproj index 7da6322..c4566d8 100644 --- a/CosmosTestHelpers/CosmosTestHelpers.csproj +++ b/CosmosTestHelpers/CosmosTestHelpers.csproj @@ -20,7 +20,7 @@ - + diff --git a/CosmosTestHelpers/InvocationInformation.cs b/CosmosTestHelpers/InvocationInformation.cs index 346fcf0..bb2e910 100644 --- a/CosmosTestHelpers/InvocationInformation.cs +++ b/CosmosTestHelpers/InvocationInformation.cs @@ -1,12 +1,3 @@ -namespace CosmosTestHelpers -{ - public class InvocationInformation - { - public string MethodName { get; } +namespace CosmosTestHelpers; - public InvocationInformation(string methodName) - { - MethodName = methodName; - } - } -} \ No newline at end of file +public record InvocationInformation(string MethodName); \ No newline at end of file diff --git a/CosmosTestHelpers/MockItemResponse.cs b/CosmosTestHelpers/MockItemResponse.cs index 8ee5177..e814a7d 100644 --- a/CosmosTestHelpers/MockItemResponse.cs +++ b/CosmosTestHelpers/MockItemResponse.cs @@ -1,43 +1,42 @@ using System.Net; using Microsoft.Azure.Cosmos; -namespace CosmosTestHelpers +namespace CosmosTestHelpers; + +public class MockItemResponse : ItemResponse { - public class MockItemResponse : ItemResponse - { - public override T Resource { get; } + public override T Resource { get; } - public override HttpStatusCode StatusCode { get; } + public override HttpStatusCode StatusCode { get; } - public override string ETag { get; } + public override string ETag { get; } - public MockItemResponse(T resource) - { - Resource = resource; - } + public MockItemResponse(T resource) + { + Resource = resource; + } - public MockItemResponse(T resource, string eTag) - { - Resource = resource; - ETag = eTag; - } + public MockItemResponse(T resource, string eTag) + { + Resource = resource; + ETag = eTag; + } - public MockItemResponse(T resource, HttpStatusCode statusCode) - { - Resource = resource; - StatusCode = statusCode; - } + public MockItemResponse(T resource, HttpStatusCode statusCode) + { + Resource = resource; + StatusCode = statusCode; + } - public MockItemResponse(HttpStatusCode statusCode) - { - StatusCode = statusCode; - } + public MockItemResponse(HttpStatusCode statusCode) + { + StatusCode = statusCode; + } - public MockItemResponse(T resource, HttpStatusCode statusCode, string eTag) - { - Resource = resource; - StatusCode = statusCode; - ETag = eTag; - } + public MockItemResponse(T resource, HttpStatusCode statusCode, string eTag) + { + Resource = resource; + StatusCode = statusCode; + ETag = eTag; } } \ No newline at end of file diff --git a/CosmosTestHelpers/TestContainerItem.cs b/CosmosTestHelpers/TestContainerItem.cs index d251868..3058e35 100644 --- a/CosmosTestHelpers/TestContainerItem.cs +++ b/CosmosTestHelpers/TestContainerItem.cs @@ -1,16 +1,3 @@ -namespace CosmosTestHelpers -{ - public class TestContainerItem - { - public string PartitionKey { get; } - public string Id { get; } - public T Document { get; } +namespace CosmosTestHelpers; - public TestContainerItem(string partitionKey, string id, T document) - { - PartitionKey = partitionKey; - Id = id; - Document = document; - } - } -} \ No newline at end of file +public record TestContainerItem(string PartitionKey, string Id, T Document); \ No newline at end of file diff --git a/README.md b/README.md index d160fc4..1dbe632 100644 --- a/README.md +++ b/README.md @@ -1 +1,6 @@ -# cosmos-test-helper \ No newline at end of file +# Cosmos Test Helpers + +A in-memory mock for a Cosmos `Container` for use in tests. + +> Note: This does not replicate all of a `Containers` functionality, +> but does cover the basics \ No newline at end of file