diff --git a/src/Ephemerally.Azure.Cosmos.Xunit/CosmosClientFixture.cs b/src/Ephemerally.Azure.Cosmos.Xunit/CosmosClientFixture.cs new file mode 100644 index 0000000..4a1bab6 --- /dev/null +++ b/src/Ephemerally.Azure.Cosmos.Xunit/CosmosClientFixture.cs @@ -0,0 +1,53 @@ +using Microsoft.Azure.Cosmos; +using System.Diagnostics.CodeAnalysis; +using System.Net.Sockets; +using Xunit; + +namespace Ephemerally.Azure.Cosmos.Xunit; + +[SuppressMessage("ReSharper", "UseConfigureAwaitFalse")] +public class CosmosClientFixture : + IAsyncDisposable, + IAsyncLifetime +{ + private readonly Lazy> _client; + + public CosmosClient Client => _client.Value.Result; + + protected Task GetClient() => _client.Value; + + public CosmosClientFixture() + { + _client = new(CreateClientAsync); + } + + protected virtual Task CreateClientAsync() => Task.FromResult(CosmosEmulator.GetClient()); + + public virtual Task InitializeAsync() => _client.Value; + + public virtual async Task DisposeAsync() + { + if (!_client.IsValueCreated) return; + + await IgnoreSocketException(async () => + { + var client = await GetClient(); + client.Dispose(); + }); + } + + async ValueTask IAsyncDisposable.DisposeAsync() => + await ((IAsyncLifetime)this).DisposeAsync(); + + protected static async Task IgnoreSocketException(Func action) + { + try + { + await action(); + } + catch (HttpRequestException ex) when (ex.InnerException is SocketException { SocketErrorCode: SocketError.ConnectionRefused }) + { + // If the emulator or instance is not accessible, we don't need to do anything + } + } +} \ No newline at end of file diff --git a/src/Ephemerally.Azure.Cosmos.Xunit/CosmosEmulator.cs b/src/Ephemerally.Azure.Cosmos.Xunit/CosmosEmulator.cs new file mode 100644 index 0000000..fc15cd8 --- /dev/null +++ b/src/Ephemerally.Azure.Cosmos.Xunit/CosmosEmulator.cs @@ -0,0 +1,27 @@ +using Microsoft.Azure.Cosmos; + +namespace Ephemerally.Azure.Cosmos.Xunit; + +public static class CosmosEmulator +{ + public static CosmosClient GetClient(Action configureOptions = null) + { + var options = new CosmosClientOptions + { + RequestTimeout = TimeSpan.FromSeconds(30), + ServerCertificateCustomValidationCallback = (_, _, _) => true, + ConnectionMode = ConnectionMode.Gateway, + LimitToEndpoint = true + }; + configureOptions?.Invoke(options); + return new( + AccountEndpoint, + AuthKey, + options); + } + + public const string + AccountEndpoint = "https://localhost:8081", + AuthKey = "C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==", + ConnectionString = $"AccountEndpoint={AccountEndpoint};AccountKey={AuthKey};"; +} \ No newline at end of file diff --git a/src/Ephemerally.Azure.Cosmos.Xunit/EphemeralCosmosContainerFixture.cs b/src/Ephemerally.Azure.Cosmos.Xunit/EphemeralCosmosContainerFixture.cs index 8c2b71c..f1acd43 100644 --- a/src/Ephemerally.Azure.Cosmos.Xunit/EphemeralCosmosContainerFixture.cs +++ b/src/Ephemerally.Azure.Cosmos.Xunit/EphemeralCosmosContainerFixture.cs @@ -1,27 +1,43 @@ -using Microsoft.Azure.Cosmos; -using Xunit; +using System.Diagnostics.CodeAnalysis; namespace Ephemerally.Azure.Cosmos.Xunit; -internal abstract class EphemeralCosmosContainerFixture : - IAsyncDisposable, - IAsyncLifetime +[SuppressMessage("ReSharper", "UseConfigureAwaitFalse")] +public class EphemeralCosmosContainerFixture : EphemeralCosmosDatabaseFixture { - private Lazy> _container; + private readonly Lazy> _container; - protected EphemeralCosmosContainerFixture( - Database database, - EphemeralCreationOptions options) + public EphemeralCosmosContainer Container => _container.Value.Result; + + protected Task GetContainer() => _container.Value; + + public EphemeralCosmosContainerFixture() + { + _container = new(CreateContainerAsync); + } + + protected virtual async Task CreateContainerAsync() { - _container = new(async () => await database.CreateEphemeralContainerAsync(options).ConfigureAwait(false)); + var db = await GetDatabase(); + return await db.CreateEphemeralContainerAsync(); } - async Task IAsyncLifetime.InitializeAsync() => - await _container.Value.ConfigureAwait(false); + public override async Task InitializeAsync() + { + await base.InitializeAsync(); + await _container.Value; + } - async Task IAsyncLifetime.DisposeAsync() => - await ((IAsyncDisposable)this).DisposeAsync().ConfigureAwait(false); + public override async Task DisposeAsync() + { + if (!_container.IsValueCreated) return; - ValueTask IAsyncDisposable.DisposeAsync() => - _container.Value.Result.DisposeAsync(); -} + await IgnoreSocketException(async () => + { + var container = await GetContainer(); + await container.DisposeAsync(); + }); + + await base.DisposeAsync(); + } +} \ No newline at end of file diff --git a/src/Ephemerally.Azure.Cosmos.Xunit/EphemeralCosmosDatabaseFixture.cs b/src/Ephemerally.Azure.Cosmos.Xunit/EphemeralCosmosDatabaseFixture.cs new file mode 100644 index 0000000..c807bc0 --- /dev/null +++ b/src/Ephemerally.Azure.Cosmos.Xunit/EphemeralCosmosDatabaseFixture.cs @@ -0,0 +1,43 @@ +using System.Diagnostics.CodeAnalysis; + +namespace Ephemerally.Azure.Cosmos.Xunit; + +[SuppressMessage("ReSharper", "UseConfigureAwaitFalse")] +public class EphemeralCosmosDatabaseFixture : CosmosClientFixture +{ + private readonly Lazy> _database; + + public EphemeralCosmosDatabase Database => _database.Value.Result; + + protected Task GetDatabase() => _database.Value; + + public EphemeralCosmosDatabaseFixture() + { + _database = new(CreateDatabaseAsync); + } + + protected virtual async Task CreateDatabaseAsync() + { + var client = await GetClient(); + return await client.CreateEphemeralDatabaseAsync(); + } + + public override async Task InitializeAsync() + { + await base.InitializeAsync(); + await _database.Value; + } + + public override async Task DisposeAsync() + { + if (!_database.IsValueCreated) return; + + await IgnoreSocketException(async () => + { + var db = await GetDatabase(); + await db.DisposeAsync(); + }); + + await base.DisposeAsync(); + } +} \ No newline at end of file diff --git a/tests/Ephemerally.Azure.Cosmos.Tests/CosmosEmulator.cs b/tests/Ephemerally.Azure.Cosmos.Tests/CosmosEmulator.cs deleted file mode 100644 index d5e0efc..0000000 --- a/tests/Ephemerally.Azure.Cosmos.Tests/CosmosEmulator.cs +++ /dev/null @@ -1,15 +0,0 @@ -using Microsoft.Azure.Cosmos; - -namespace Ephemerally.Azure.Cosmos.Tests; - -internal static class CosmosEmulator -{ - public static CosmosClient Client { get; } = new( - "https://localhost:8081", - "C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==", new CosmosClientOptions - { - RequestTimeout = TimeSpan.FromSeconds(5) - }); - - public const string ConnectionString = "AccountEndpoint=https://localhost:8081/;AccountKey=C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==;"; -} \ No newline at end of file diff --git a/tests/Ephemerally.Azure.Cosmos.Tests/CosmosEmulatorFixture.cs b/tests/Ephemerally.Azure.Cosmos.Tests/CosmosEmulatorFixture.cs index 47f0bfd..4c1b48b 100644 --- a/tests/Ephemerally.Azure.Cosmos.Tests/CosmosEmulatorFixture.cs +++ b/tests/Ephemerally.Azure.Cosmos.Tests/CosmosEmulatorFixture.cs @@ -1,4 +1,6 @@ -namespace Ephemerally.Azure.Cosmos.Tests; +using Ephemerally.Azure.Cosmos.Xunit; + +namespace Ephemerally.Azure.Cosmos.Tests; [SetUpFixture] [FixtureLifeCycle(LifeCycle.SingleInstance)] @@ -7,9 +9,7 @@ public class CosmosEmulatorFixture [OneTimeSetUp] public static async Task OneTimeSetUp() { - await CosmosEmulator.Client.ConnectOrThrowAsync(); + using var client = CosmosEmulator.GetClient(); + await client.ConnectOrThrowAsync(); } - - [OneTimeTearDown] - public static void OneTimeTearDown() => CosmosEmulator.Client.Dispose(); } \ No newline at end of file diff --git a/tests/Ephemerally.Azure.Cosmos.Tests/EphemeralContainerTests.cs b/tests/Ephemerally.Azure.Cosmos.Tests/EphemeralContainerTests.cs index 6dcc836..d71f7e5 100644 --- a/tests/Ephemerally.Azure.Cosmos.Tests/EphemeralContainerTests.cs +++ b/tests/Ephemerally.Azure.Cosmos.Tests/EphemeralContainerTests.cs @@ -1,4 +1,5 @@ -using Microsoft.Azure.Cosmos; +using Ephemerally.Azure.Cosmos.Xunit; +using Microsoft.Azure.Cosmos; namespace Ephemerally.Azure.Cosmos.Tests; @@ -7,7 +8,7 @@ public class EphemeralContainerTests [Test] public async Task Should_create_container_and_tear_it_down() { - var client = CosmosEmulator.Client; + using var client = CosmosEmulator.GetClient(); await using var db = await client.CreateEphemeralDatabaseAsync(); var sut = await db.CreateEphemeralContainerAsync(); @@ -21,7 +22,7 @@ public async Task Should_create_container_and_tear_it_down() [Test] public async Task User_supplied_container_should_be_torn_down() { - var client = CosmosEmulator.Client; + using var client = CosmosEmulator.GetClient(); await using var db = await client.CreateEphemeralDatabaseAsync(); var userSuppliedContainer = (await db.CreateContainerAsync("user-supplied-container", "/id")).Container; @@ -38,7 +39,7 @@ public async Task User_supplied_container_should_be_torn_down() [Test] public async Task CleanupBehavior_SelfAndExpired_should_remove_self_and_expired_orphaned_container() { - var client = CosmosEmulator.Client; + using var client = CosmosEmulator.GetClient(); await using var db = await client.CreateEphemeralDatabaseAsync(); var orphanContainer = await db.CreateEphemeralContainerAsync(new EphemeralCreationOptions(DateTimeOffset.MinValue)); @@ -58,7 +59,7 @@ public async Task CleanupBehavior_SelfAndExpired_should_remove_self_and_expired_ [Test] public async Task CleanupBehavior_SelfOnly_should_remove_self_and_not_remove_unexpired_orphaned_container() { - var client = CosmosEmulator.Client; + using var client = CosmosEmulator.GetClient(); await using var db = await client.CreateEphemeralDatabaseAsync(); var orphanContainer = await db.CreateEphemeralContainerAsync(new EphemeralCreationOptions(DateTimeOffset.MaxValue)); @@ -77,7 +78,7 @@ public async Task CleanupBehavior_SelfOnly_should_remove_self_and_not_remove_une [Test] public async Task CleanupBehavior_SelfOnly_should_remove_self_and_not_remove_expired_orphaned_container() { - var client = CosmosEmulator.Client; + using var client = CosmosEmulator.GetClient(); await using var db = await client.CreateEphemeralDatabaseAsync(); var orphanContainer = await db.CreateEphemeralContainerAsync(new EphemeralCreationOptions(DateTimeOffset.MinValue)); @@ -96,7 +97,7 @@ public async Task CleanupBehavior_SelfOnly_should_remove_self_and_not_remove_exp [Test] public async Task CleanupBehavior_NoCleanup_should_not_remove_anything() { - var client = CosmosEmulator.Client; + using var client = CosmosEmulator.GetClient(); await using var db = await client.CreateEphemeralDatabaseAsync(); var orphanContainer = await db.CreateEphemeralContainerAsync(new EphemeralCreationOptions(DateTimeOffset.MinValue)); @@ -117,7 +118,7 @@ public async Task Should_create_container_when_container_properties_Id_is_provid { const string userSuppliedId = "user-supplied-id"; - var client = CosmosEmulator.Client; + using var client = CosmosEmulator.GetClient(); await using var db = await client.CreateEphemeralDatabaseAsync(); await using var sut = await db.CreateEphemeralContainerAsync(containerProperties: new ContainerProperties { Id = userSuppliedId }); @@ -129,7 +130,7 @@ public async Task Should_create_container_when_container_properties_Id_is_provid [Test] public async Task Should_create_container_when_container_properties_Id_is_not_provided() { - var client = CosmosEmulator.Client; + using var client = CosmosEmulator.GetClient(); await using var db = await client.CreateEphemeralDatabaseAsync(); await using var sut = await db.CreateEphemeralContainerAsync(containerProperties: new()); @@ -142,7 +143,7 @@ public async Task Should_create_container_when_container_properties_PartitionKey { const string userSuppliedKey = "/userSuppliedKey"; - var client = CosmosEmulator.Client; + using var client = CosmosEmulator.GetClient(); await using var db = await client.CreateEphemeralDatabaseAsync(); await using var sut = await db.CreateEphemeralContainerAsync(containerProperties: new ContainerProperties { PartitionKeyPath = userSuppliedKey }); @@ -153,7 +154,7 @@ public async Task Should_create_container_when_container_properties_PartitionKey [Test] public async Task Should_create_container_when_container_properties_PartitionKeyPath_is_not_provided() { - var client = CosmosEmulator.Client; + using var client = CosmosEmulator.GetClient(); await using var db = await client.CreateEphemeralDatabaseAsync(); await using var sut = await db.CreateEphemeralContainerAsync(containerProperties: new()); diff --git a/tests/Ephemerally.Azure.Cosmos.Tests/EphemeralDatabaseTests.cs b/tests/Ephemerally.Azure.Cosmos.Tests/EphemeralDatabaseTests.cs index 80383a0..bf6b6d5 100644 --- a/tests/Ephemerally.Azure.Cosmos.Tests/EphemeralDatabaseTests.cs +++ b/tests/Ephemerally.Azure.Cosmos.Tests/EphemeralDatabaseTests.cs @@ -1,3 +1,5 @@ +using Ephemerally.Azure.Cosmos.Xunit; + namespace Ephemerally.Azure.Cosmos.Tests; public class EphemeralDatabaseTests @@ -5,7 +7,7 @@ public class EphemeralDatabaseTests [Test] public async Task Should_create_database_and_tear_it_down() { - var client = CosmosEmulator.Client; + using var client = CosmosEmulator.GetClient(); var sut = await client.CreateEphemeralDatabaseAsync(); Assert.That(await sut.ExistsAsync(), Is.True); @@ -18,7 +20,7 @@ public async Task Should_create_database_and_tear_it_down() [Test] public async Task User_supplied_database_should_be_torn_down() { - var client = CosmosEmulator.Client; + using var client = CosmosEmulator.GetClient(); var userSuppliedDatabase = (await client.CreateDatabaseAsync("user-supplied-database")).Database; @@ -34,7 +36,7 @@ public async Task User_supplied_database_should_be_torn_down() [Test] public async Task CleanupBehavior_SelfAndExpired_should_remove_self_and_expired_orphaned_database() { - var client = CosmosEmulator.Client; + using var client = CosmosEmulator.GetClient(); var orphanDb = await client.CreateEphemeralDatabaseAsync(new EphemeralCreationOptions(TimeSpan.Zero)); Assert.That(await orphanDb.ExistsAsync(), Is.True); @@ -52,7 +54,7 @@ public async Task CleanupBehavior_SelfAndExpired_should_remove_self_and_expired_ [Test] public async Task CleanupBehavior_SelfAndExpired_should_remove_self_and_not_remove_unexpired_orphaned_database() { - var client = CosmosEmulator.Client; + using var client = CosmosEmulator.GetClient(); await using var orphanDb = await client.CreateEphemeralDatabaseAsync(); Assert.That(await orphanDb.ExistsAsync(), Is.True); @@ -70,7 +72,7 @@ public async Task CleanupBehavior_SelfAndExpired_should_remove_self_and_not_remo [Test] public async Task CleanupBehavior_SelfOnly_should_remove_self_only_and_not_remove_expired_orphaned_database() { - var client = CosmosEmulator.Client; + using var client = CosmosEmulator.GetClient(); await using var orphanDb = await client.CreateEphemeralDatabaseAsync(new EphemeralCreationOptions(TimeSpan.Zero)); Assert.That(await orphanDb.ExistsAsync(), Is.True); @@ -88,7 +90,7 @@ public async Task CleanupBehavior_SelfOnly_should_remove_self_only_and_not_remov [Test] public async Task CleanupBehavior_NoCleanup_should_not_remove_anything() { - var client = CosmosEmulator.Client; + using var client = CosmosEmulator.GetClient(); await using var orphanDb = await client.CreateEphemeralDatabaseAsync(new EphemeralCreationOptions(TimeSpan.Zero)); Assert.That(await orphanDb.ExistsAsync(), Is.True); diff --git a/tests/Ephemerally.Azure.Cosmos.Tests/Ephemerally.Azure.Cosmos.Tests.csproj b/tests/Ephemerally.Azure.Cosmos.Tests/Ephemerally.Azure.Cosmos.Tests.csproj index 204ad10..a3c492f 100644 --- a/tests/Ephemerally.Azure.Cosmos.Tests/Ephemerally.Azure.Cosmos.Tests.csproj +++ b/tests/Ephemerally.Azure.Cosmos.Tests/Ephemerally.Azure.Cosmos.Tests.csproj @@ -1,10 +1,11 @@ - + net6.0 enable - enable - + disable + latest + false true @@ -18,6 +19,7 @@ + diff --git a/tests/Ephemerally.Azure.Cosmos.Tests/InternalExtensionTests.cs b/tests/Ephemerally.Azure.Cosmos.Tests/InternalExtensionTests.cs index e9a3ba9..61e27c3 100644 --- a/tests/Ephemerally.Azure.Cosmos.Tests/InternalExtensionTests.cs +++ b/tests/Ephemerally.Azure.Cosmos.Tests/InternalExtensionTests.cs @@ -1,11 +1,14 @@ -namespace Ephemerally.Azure.Cosmos.Tests; +using Ephemerally.Azure.Cosmos.Xunit; + +namespace Ephemerally.Azure.Cosmos.Tests; public class InternalExtensionTests { [Test] public async Task GetExpiredContainersAsync_should_return_empty_when_no_containers_present() { - await using var db = await CosmosEmulator.Client.CreateEphemeralDatabaseAsync(); + using var client = CosmosEmulator.GetClient(); + await using var db = await client.CreateEphemeralDatabaseAsync(); var expiredContainers = await db.GetExpiredContainersAsync(); Assert.That(expiredContainers, Is.Empty); } @@ -13,7 +16,8 @@ public async Task GetExpiredContainersAsync_should_return_empty_when_no_containe [Test] public async Task GetExpiredContainersAsync_should_return_empty_when_containers_present_but_none_expired() { - await using var db = await CosmosEmulator.Client.CreateEphemeralDatabaseAsync(); + using var client = CosmosEmulator.GetClient(); + await using var db = await client.CreateEphemeralDatabaseAsync(); // We don't need 'using' here because the containers will be cleaned up by the database var container = await db.CreateEphemeralContainerAsync(new EphemeralCreationOptions(TimeSpan.FromMinutes(1))); @@ -27,7 +31,8 @@ public async Task GetExpiredContainersAsync_should_return_empty_when_containers_ [Test] public async Task GetExpiredContainersAsync_should_return_one_when_containers_present_and_one_expired() { - await using var db = await CosmosEmulator.Client.CreateEphemeralDatabaseAsync(); + using var client = CosmosEmulator.GetClient(); + await using var db = await client.CreateEphemeralDatabaseAsync(); // We don't need 'using' here because the containers will be cleaned up by the database var container = await db.CreateEphemeralContainerAsync(new EphemeralCreationOptions(TimeSpan.Zero));