From ddcbe02f7729ce7d49a93d3eceb151c0697bee6c Mon Sep 17 00:00:00 2001 From: Nelson Nobre Date: Tue, 31 Dec 2024 03:02:06 +0000 Subject: [PATCH 1/4] feat: Update CosmosDb image to vnext-preview version --- .../CosmosDbBuilder.cs | 56 ++++++++--------- .../CosmosDbContainer.cs | 46 +++----------- .../Testcontainers.CosmosDb.csproj | 3 +- .../CosmosDbContainerTest.cs | 60 +++++++++++++++---- 4 files changed, 86 insertions(+), 79 deletions(-) diff --git a/src/Testcontainers.CosmosDb/CosmosDbBuilder.cs b/src/Testcontainers.CosmosDb/CosmosDbBuilder.cs index bde981f22..8e0495094 100644 --- a/src/Testcontainers.CosmosDb/CosmosDbBuilder.cs +++ b/src/Testcontainers.CosmosDb/CosmosDbBuilder.cs @@ -1,14 +1,15 @@ +using System.Net; +using System.Net.Sockets; + namespace Testcontainers.CosmosDb; /// [PublicAPI] public sealed class CosmosDbBuilder : ContainerBuilder { - public const string CosmosDbImage = "mcr.microsoft.com/cosmosdb/linux/azure-cosmos-emulator:latest"; - - public const ushort CosmosDbPort = 8081; - + public const string CosmosDbImage = "mcr.microsoft.com/cosmosdb/linux/azure-cosmos-emulator:vnext-preview"; public const string DefaultAccountKey = "C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw=="; + public readonly ushort CosmosDbPort; /// /// Initializes a new instance of the class. @@ -16,6 +17,7 @@ public sealed class CosmosDbBuilder : ContainerBuilder request.ForPort(CosmosDbPort))); } /// @@ -66,32 +70,24 @@ protected override CosmosDbBuilder Merge(CosmosDbConfiguration oldValue, CosmosD return new CosmosDbBuilder(new CosmosDbConfiguration(oldValue, newValue)); } - /// - private sealed class WaitUntil : IWaitUntil + /// + /// Gets an available port. + /// + private static ushort GetAvailablePort() { - /// - public async Task UntilAsync(IContainer container) +#if NET8_0_OR_GREATER + using (var listener = new TcpListener(IPAddress.Loopback, 0)) { - // CosmosDB's preconfigured HTTP client will redirect the request to the container. - const string requestUri = "https://localhost/_explorer/emulator.pem"; - - var httpClient = ((CosmosDbContainer)container).HttpClient; - - try - { - using var httpResponse = await httpClient.GetAsync(requestUri) - .ConfigureAwait(false); - - return httpResponse.IsSuccessStatusCode; - } - catch (Exception) - { - return false; - } - finally - { - httpClient.Dispose(); - } + listener.Start(); + return (ushort)((IPEndPoint)listener.LocalEndpoint).Port; } +#else + var listener = new TcpListener(IPAddress.Loopback, 0); + listener.Start(); + + var port = (ushort)((IPEndPoint)listener.LocalEndpoint).Port; + listener.Stop(); + return port; +#endif } } \ No newline at end of file diff --git a/src/Testcontainers.CosmosDb/CosmosDbContainer.cs b/src/Testcontainers.CosmosDb/CosmosDbContainer.cs index b7b0a57d7..6bc14c3d5 100644 --- a/src/Testcontainers.CosmosDb/CosmosDbContainer.cs +++ b/src/Testcontainers.CosmosDb/CosmosDbContainer.cs @@ -1,9 +1,13 @@ +using Microsoft.Azure.Cosmos; + namespace Testcontainers.CosmosDb; /// [PublicAPI] public sealed class CosmosDbContainer : DockerContainer { + private readonly int _port; + /// /// Initializes a new instance of the class. /// @@ -11,6 +15,7 @@ public sealed class CosmosDbContainer : DockerContainer public CosmosDbContainer(CosmosDbConfiguration configuration) : base(configuration) { + _port = int.Parse(configuration.PortBindings.First().Value); } /// @@ -20,47 +25,14 @@ public CosmosDbContainer(CosmosDbConfiguration configuration) public string GetConnectionString() { var properties = new Dictionary(); - properties.Add("AccountEndpoint", new UriBuilder(Uri.UriSchemeHttps, Hostname, GetMappedPublicPort(CosmosDbBuilder.CosmosDbPort)).ToString()); + properties.Add("AccountEndpoint", new UriBuilder(Uri.UriSchemeHttp, Hostname, _port).ToString()); properties.Add("AccountKey", CosmosDbBuilder.DefaultAccountKey); return string.Join(";", properties.Select(property => string.Join("=", property.Key, property.Value))); } /// - /// Gets a configured HTTP message handler that automatically trusts the CosmosDb Emulator's certificate. - /// - public HttpMessageHandler HttpMessageHandler => new UriRewriter(Hostname, GetMappedPublicPort(CosmosDbBuilder.CosmosDbPort)); - - /// - /// Gets a configured HTTP client that automatically trusts the CosmosDb Emulator's certificate. - /// - public HttpClient HttpClient => new HttpClient(HttpMessageHandler); - - /// - /// Rewrites the HTTP requests to target the running CosmosDb Emulator instance. + /// Gets a configured cosmos client with connection mode set to Gateway. /// - private sealed class UriRewriter : DelegatingHandler - { - private readonly string _hostname; + public CosmosClient CosmosClient => new(GetConnectionString(), new() { ConnectionMode = ConnectionMode.Gateway }); - private readonly ushort _port; - - /// - /// Initializes a new instance of the class. - /// - /// The target hostname. - /// The target port. - public UriRewriter(string hostname, ushort port) - : base(new HttpClientHandler { ServerCertificateCustomValidationCallback = (_, _, _, _) => true }) - { - _hostname = hostname; - _port = port; - } - - /// - protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) - { - request.RequestUri = new UriBuilder(Uri.UriSchemeHttps, _hostname, _port, request.RequestUri.PathAndQuery).Uri; - return base.SendAsync(request, cancellationToken); - } - } -} \ No newline at end of file +} diff --git a/src/Testcontainers.CosmosDb/Testcontainers.CosmosDb.csproj b/src/Testcontainers.CosmosDb/Testcontainers.CosmosDb.csproj index 9a25b9c4d..936856b42 100644 --- a/src/Testcontainers.CosmosDb/Testcontainers.CosmosDb.csproj +++ b/src/Testcontainers.CosmosDb/Testcontainers.CosmosDb.csproj @@ -5,8 +5,9 @@ + - \ No newline at end of file + diff --git a/tests/Testcontainers.CosmosDb.Tests/CosmosDbContainerTest.cs b/tests/Testcontainers.CosmosDb.Tests/CosmosDbContainerTest.cs index 16ee4f4ee..9d6d6fe25 100644 --- a/tests/Testcontainers.CosmosDb.Tests/CosmosDbContainerTest.cs +++ b/tests/Testcontainers.CosmosDb.Tests/CosmosDbContainerTest.cs @@ -1,4 +1,7 @@ -namespace Testcontainers.CosmosDb; +using System; +using System.Linq; + +namespace Testcontainers.CosmosDb.Tests; public sealed class CosmosDbContainerTest : IAsyncLifetime { @@ -14,24 +17,59 @@ public Task DisposeAsync() return _cosmosDbContainer.DisposeAsync().AsTask(); } - [Fact(Skip = "The Cosmos DB Linux Emulator Docker image does not run on Microsoft's CI environment (GitHub, Azure DevOps).")] // https://github.com/Azure/azure-cosmos-db-emulator-docker/issues/45. + [Fact] + [Trait(nameof(DockerCli.DockerPlatform), nameof(DockerCli.DockerPlatform.Linux))] + public async Task CreateDatabaseAndContainerSuccessful() + { + // Given + using var cosmosClient = _cosmosDbContainer.CosmosClient; + + + // When + var database = (await cosmosClient.CreateDatabaseIfNotExistsAsync("fakedb")).Database; + await database.CreateContainerIfNotExistsAsync("fakecontainer", "/id"); + var properties = (await cosmosClient.GetDatabaseQueryIterator().ReadNextAsync()).First(); + + + // Then + Assert.Equal("fakedb", properties.Id); + } + + + [Fact] [Trait(nameof(DockerCli.DockerPlatform), nameof(DockerCli.DockerPlatform.Linux))] - public async Task AccountPropertiesIdReturnsLocalhost() + public async Task RetrieveItemCreated() { // Given - using var httpClient = _cosmosDbContainer.HttpClient; + using var cosmosClient = _cosmosDbContainer.CosmosClient; - var cosmosClientOptions = new CosmosClientOptions(); - cosmosClientOptions.ConnectionMode = ConnectionMode.Gateway; - cosmosClientOptions.HttpClientFactory = () => httpClient; + var database = (await cosmosClient.CreateDatabaseIfNotExistsAsync("dbfake")).Database; + await database.CreateContainerIfNotExistsAsync("containerfake", "/id"); + var container = database.GetContainer("containerfake"); + + var id = Guid.NewGuid().ToString(); + var name = Guid.NewGuid().ToString(); - using var cosmosClient = new CosmosClient(_cosmosDbContainer.GetConnectionString(), cosmosClientOptions); // When - var accountProperties = await cosmosClient.ReadAccountAsync() - .ConfigureAwait(true); + var response = await container.CreateItemAsync( + new FakeItem { id = id, Name = name }, + new PartitionKey(id)); + + var itemResponse = await container.ReadItemAsync( + id, + new PartitionKey(id)); + // Then - Assert.Equal("localhost", accountProperties.Id); + Assert.Equal(id, itemResponse.Resource.id); + Assert.Equal(name, itemResponse.Resource.Name); + } + + + private class FakeItem + { + public string id { get; set; } + public string Name { get; set; } } } \ No newline at end of file From 4827b454a6d17b7494a89d7d1e7f7083d59743a3 Mon Sep 17 00:00:00 2001 From: Nelson Nobre Date: Tue, 31 Dec 2024 15:45:19 +0000 Subject: [PATCH 2/4] Improved API port mapping and reintroduced the rewritten HTTP client. --- .../CosmosDbBuilder.cs | 31 ++--------- .../CosmosDbContainer.cs | 52 +++++++++++++++++-- .../Testcontainers.CosmosDb.csproj | 2 +- .../CosmosDbContainerTest.cs | 6 +-- 4 files changed, 56 insertions(+), 35 deletions(-) diff --git a/src/Testcontainers.CosmosDb/CosmosDbBuilder.cs b/src/Testcontainers.CosmosDb/CosmosDbBuilder.cs index 8e0495094..4a584437f 100644 --- a/src/Testcontainers.CosmosDb/CosmosDbBuilder.cs +++ b/src/Testcontainers.CosmosDb/CosmosDbBuilder.cs @@ -8,8 +8,10 @@ namespace Testcontainers.CosmosDb; public sealed class CosmosDbBuilder : ContainerBuilder { public const string CosmosDbImage = "mcr.microsoft.com/cosmosdb/linux/azure-cosmos-emulator:vnext-preview"; + + public const ushort CosmosDbPort = 8081; + public const string DefaultAccountKey = "C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw=="; - public readonly ushort CosmosDbPort; /// /// Initializes a new instance of the class. @@ -17,7 +19,6 @@ public sealed class CosmosDbBuilder : ContainerBuilder request.ForPort(CosmosDbPort))); } @@ -69,25 +69,4 @@ protected override CosmosDbBuilder Merge(CosmosDbConfiguration oldValue, CosmosD { return new CosmosDbBuilder(new CosmosDbConfiguration(oldValue, newValue)); } - - /// - /// Gets an available port. - /// - private static ushort GetAvailablePort() - { -#if NET8_0_OR_GREATER - using (var listener = new TcpListener(IPAddress.Loopback, 0)) - { - listener.Start(); - return (ushort)((IPEndPoint)listener.LocalEndpoint).Port; - } -#else - var listener = new TcpListener(IPAddress.Loopback, 0); - listener.Start(); - - var port = (ushort)((IPEndPoint)listener.LocalEndpoint).Port; - listener.Stop(); - return port; -#endif - } -} \ No newline at end of file +} diff --git a/src/Testcontainers.CosmosDb/CosmosDbContainer.cs b/src/Testcontainers.CosmosDb/CosmosDbContainer.cs index 6bc14c3d5..1f9938dd5 100644 --- a/src/Testcontainers.CosmosDb/CosmosDbContainer.cs +++ b/src/Testcontainers.CosmosDb/CosmosDbContainer.cs @@ -6,8 +6,6 @@ namespace Testcontainers.CosmosDb; [PublicAPI] public sealed class CosmosDbContainer : DockerContainer { - private readonly int _port; - /// /// Initializes a new instance of the class. /// @@ -15,7 +13,6 @@ public sealed class CosmosDbContainer : DockerContainer public CosmosDbContainer(CosmosDbConfiguration configuration) : base(configuration) { - _port = int.Parse(configuration.PortBindings.First().Value); } /// @@ -25,7 +22,7 @@ public CosmosDbContainer(CosmosDbConfiguration configuration) public string GetConnectionString() { var properties = new Dictionary(); - properties.Add("AccountEndpoint", new UriBuilder(Uri.UriSchemeHttp, Hostname, _port).ToString()); + properties.Add("AccountEndpoint", new UriBuilder(Uri.UriSchemeHttp, Hostname, GetMappedPublicPort(CosmosDbBuilder.CosmosDbPort)).ToString()); properties.Add("AccountKey", CosmosDbBuilder.DefaultAccountKey); return string.Join(";", properties.Select(property => string.Join("=", property.Key, property.Value))); } @@ -33,6 +30,51 @@ public string GetConnectionString() /// /// Gets a configured cosmos client with connection mode set to Gateway. /// - public CosmosClient CosmosClient => new(GetConnectionString(), new() { ConnectionMode = ConnectionMode.Gateway }); + public CosmosClient CosmosClient + => new CosmosClient( + GetConnectionString(), + new() + { + ConnectionMode = ConnectionMode.Gateway, + HttpClientFactory = () => new(new UriRewriter(Hostname, GetMappedPublicPort(CosmosDbBuilder.CosmosDbPort))) + }); + + /// + /// Gets a configured HTTP message handler that automatically trusts the CosmosDb Emulator's certificate. + /// + public HttpMessageHandler HttpMessageHandler => new UriRewriter(Hostname, GetMappedPublicPort(CosmosDbBuilder.CosmosDbPort)); + + /// + /// Gets a configured HTTP client that automatically trusts the CosmosDb Emulator's certificate. + /// + public HttpClient HttpClient => new HttpClient(HttpMessageHandler); + /// + /// Rewrites the HTTP requests to target the running CosmosDb Emulator instance. + /// + private sealed class UriRewriter : DelegatingHandler + { + private readonly string _hostname; + + private readonly ushort _port; + + /// + /// Initializes a new instance of the class. + /// + /// The target hostname. + /// The target port. + public UriRewriter(string hostname, ushort port) + : base(new HttpClientHandler()) + { + _hostname = hostname; + _port = port; + } + + /// + protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + request.RequestUri = new UriBuilder(Uri.UriSchemeHttp, _hostname, _port, request.RequestUri.PathAndQuery).Uri; + return base.SendAsync(request, cancellationToken); + } + } } diff --git a/src/Testcontainers.CosmosDb/Testcontainers.CosmosDb.csproj b/src/Testcontainers.CosmosDb/Testcontainers.CosmosDb.csproj index 936856b42..f356fb9de 100644 --- a/src/Testcontainers.CosmosDb/Testcontainers.CosmosDb.csproj +++ b/src/Testcontainers.CosmosDb/Testcontainers.CosmosDb.csproj @@ -5,7 +5,7 @@ - + diff --git a/tests/Testcontainers.CosmosDb.Tests/CosmosDbContainerTest.cs b/tests/Testcontainers.CosmosDb.Tests/CosmosDbContainerTest.cs index 9d6d6fe25..b13b8d70c 100644 --- a/tests/Testcontainers.CosmosDb.Tests/CosmosDbContainerTest.cs +++ b/tests/Testcontainers.CosmosDb.Tests/CosmosDbContainerTest.cs @@ -28,11 +28,11 @@ public async Task CreateDatabaseAndContainerSuccessful() // When var database = (await cosmosClient.CreateDatabaseIfNotExistsAsync("fakedb")).Database; await database.CreateContainerIfNotExistsAsync("fakecontainer", "/id"); - var properties = (await cosmosClient.GetDatabaseQueryIterator().ReadNextAsync()).First(); + var databaseProperties = (await cosmosClient.GetDatabaseQueryIterator().ReadNextAsync()).First(); // Then - Assert.Equal("fakedb", properties.Id); + Assert.Equal("fakedb", databaseProperties.Id); } @@ -72,4 +72,4 @@ private class FakeItem public string id { get; set; } public string Name { get; set; } } -} \ No newline at end of file +} From b9912651800a350f7849eea0df5065460014ff77 Mon Sep 17 00:00:00 2001 From: Nelson Nobre Date: Tue, 31 Dec 2024 15:47:53 +0000 Subject: [PATCH 3/4] Remove unused namespaces from CosmosDbBuilder --- src/Testcontainers.CosmosDb/CosmosDbBuilder.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/Testcontainers.CosmosDb/CosmosDbBuilder.cs b/src/Testcontainers.CosmosDb/CosmosDbBuilder.cs index 4a584437f..c59dc71f2 100644 --- a/src/Testcontainers.CosmosDb/CosmosDbBuilder.cs +++ b/src/Testcontainers.CosmosDb/CosmosDbBuilder.cs @@ -1,6 +1,3 @@ -using System.Net; -using System.Net.Sockets; - namespace Testcontainers.CosmosDb; /// From 87e978e035ae18a271d1271b3576cbf387f65a19 Mon Sep 17 00:00:00 2001 From: Nelson Nobre Date: Thu, 9 Jan 2025 20:31:41 +0000 Subject: [PATCH 4/4] fix: Removed dependency on Cosmos Client and improved Cosmos wait strategy --- .../CosmosDbBuilder.cs | 30 ++++++++++++++++++- .../CosmosDbContainer.cs | 14 --------- .../Testcontainers.CosmosDb.csproj | 1 - .../CosmosDbContainerTest.cs | 16 ++++++++-- 4 files changed, 43 insertions(+), 18 deletions(-) diff --git a/src/Testcontainers.CosmosDb/CosmosDbBuilder.cs b/src/Testcontainers.CosmosDb/CosmosDbBuilder.cs index c59dc71f2..0dca5d28d 100644 --- a/src/Testcontainers.CosmosDb/CosmosDbBuilder.cs +++ b/src/Testcontainers.CosmosDb/CosmosDbBuilder.cs @@ -46,7 +46,7 @@ protected override CosmosDbBuilder Init() .WithImage(CosmosDbImage) .WithEnvironment("ENABLE_EXPLORER", "false") .WithPortBinding(CosmosDbPort, true) - .WithWaitStrategy(Wait.ForUnixContainer().UntilHttpRequestIsSucceeded(request => request.ForPort(CosmosDbPort))); + .WithWaitStrategy(Wait.ForUnixContainer().AddCustomWaitStrategy(new WaitUntil())); } /// @@ -66,4 +66,32 @@ protected override CosmosDbBuilder Merge(CosmosDbConfiguration oldValue, CosmosD { return new CosmosDbBuilder(new CosmosDbConfiguration(oldValue, newValue)); } + + /// + private sealed class WaitUntil : IWaitUntil + { + /// + public async Task UntilAsync(IContainer container) + { + // CosmosDB's preconfigured HTTP client will redirect the request to the container. + const string REQUEST_URI = "http://localhost"; + + using var httpClient = ((CosmosDbContainer)container).HttpClient; + + try + { + using var httpResponse = await httpClient.GetAsync(REQUEST_URI) + .ConfigureAwait(false); + + if (httpResponse.IsSuccessStatusCode) + { + await Task.Delay(2_000); + return true; + } + } + catch { } + + return false; + } + } } diff --git a/src/Testcontainers.CosmosDb/CosmosDbContainer.cs b/src/Testcontainers.CosmosDb/CosmosDbContainer.cs index 1f9938dd5..36946d4af 100644 --- a/src/Testcontainers.CosmosDb/CosmosDbContainer.cs +++ b/src/Testcontainers.CosmosDb/CosmosDbContainer.cs @@ -1,5 +1,3 @@ -using Microsoft.Azure.Cosmos; - namespace Testcontainers.CosmosDb; /// @@ -27,18 +25,6 @@ public string GetConnectionString() return string.Join(";", properties.Select(property => string.Join("=", property.Key, property.Value))); } - /// - /// Gets a configured cosmos client with connection mode set to Gateway. - /// - public CosmosClient CosmosClient - => new CosmosClient( - GetConnectionString(), - new() - { - ConnectionMode = ConnectionMode.Gateway, - HttpClientFactory = () => new(new UriRewriter(Hostname, GetMappedPublicPort(CosmosDbBuilder.CosmosDbPort))) - }); - /// /// Gets a configured HTTP message handler that automatically trusts the CosmosDb Emulator's certificate. /// diff --git a/src/Testcontainers.CosmosDb/Testcontainers.CosmosDb.csproj b/src/Testcontainers.CosmosDb/Testcontainers.CosmosDb.csproj index f356fb9de..7abec513c 100644 --- a/src/Testcontainers.CosmosDb/Testcontainers.CosmosDb.csproj +++ b/src/Testcontainers.CosmosDb/Testcontainers.CosmosDb.csproj @@ -5,7 +5,6 @@ - diff --git a/tests/Testcontainers.CosmosDb.Tests/CosmosDbContainerTest.cs b/tests/Testcontainers.CosmosDb.Tests/CosmosDbContainerTest.cs index b13b8d70c..dd16ff265 100644 --- a/tests/Testcontainers.CosmosDb.Tests/CosmosDbContainerTest.cs +++ b/tests/Testcontainers.CosmosDb.Tests/CosmosDbContainerTest.cs @@ -22,7 +22,13 @@ public Task DisposeAsync() public async Task CreateDatabaseAndContainerSuccessful() { // Given - using var cosmosClient = _cosmosDbContainer.CosmosClient; + using var cosmosClient = new CosmosClient( + _cosmosDbContainer.GetConnectionString(), + new() + { + ConnectionMode = ConnectionMode.Gateway, + HttpClientFactory = () => _cosmosDbContainer.HttpClient + }); // When @@ -41,7 +47,13 @@ public async Task CreateDatabaseAndContainerSuccessful() public async Task RetrieveItemCreated() { // Given - using var cosmosClient = _cosmosDbContainer.CosmosClient; + using var cosmosClient = new CosmosClient( + _cosmosDbContainer.GetConnectionString(), + new() + { + ConnectionMode = ConnectionMode.Gateway, + HttpClientFactory = () => _cosmosDbContainer.HttpClient + }); var database = (await cosmosClient.CreateDatabaseIfNotExistsAsync("dbfake")).Database; await database.CreateContainerIfNotExistsAsync("containerfake", "/id");