Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Update CosmosDb image to vnext-preview version #1324

Open
wants to merge 7 commits into
base: develop
Choose a base branch
from
56 changes: 26 additions & 30 deletions src/Testcontainers.CosmosDb/CosmosDbBuilder.cs
Original file line number Diff line number Diff line change
@@ -1,21 +1,23 @@
using System.Net;
using System.Net.Sockets;

namespace Testcontainers.CosmosDb;

/// <inheritdoc cref="ContainerBuilder{TBuilderEntity, TContainerEntity, TConfigurationEntity}" />
[PublicAPI]
public sealed class CosmosDbBuilder : ContainerBuilder<CosmosDbBuilder, CosmosDbContainer, CosmosDbConfiguration>
{
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";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it necessary to use the preview version as a default? I know that this image is quite new but users can always:

new CosmosDbBuilder()
.WithImage("mcr.microsoft.com/cosmosdb/linux/azure-cosmos-emulator:vnext-preview")
.Build();

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The advantage of this PR is the use of this image as default, as it delivers significantly better results compared to the current :latest. Additionally, some configurations made in this PR are not compatible between the :latest and :vnext-preview versions. Currently, the :vnext-preview version shows considerably better results than :latest.

public const string DefaultAccountKey = "C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==";
public readonly ushort CosmosDbPort;

/// <summary>
/// Initializes a new instance of the <see cref="CosmosDbBuilder" /> class.
/// </summary>
public CosmosDbBuilder()
: this(new CosmosDbConfiguration())
{
CosmosDbPort = GetAvailablePort();
DockerResourceConfiguration = Init().DockerResourceConfiguration;
}

Expand Down Expand Up @@ -44,8 +46,10 @@ protected override CosmosDbBuilder Init()
{
return base.Init()
.WithImage(CosmosDbImage)
.WithPortBinding(CosmosDbPort, true)
.WithWaitStrategy(Wait.ForUnixContainer().AddCustomWaitStrategy(new WaitUntil()));
.WithEnvironment("ENABLE_EXPLORER", "false")
.WithEnvironment("PORT", CosmosDbPort.ToString())
.WithPortBinding(CosmosDbPort, CosmosDbPort)
.WithWaitStrategy(Wait.ForUnixContainer().UntilHttpRequestIsSucceeded(request => request.ForPort(CosmosDbPort)));
}

/// <inheritdoc />
Expand All @@ -66,32 +70,24 @@ protected override CosmosDbBuilder Merge(CosmosDbConfiguration oldValue, CosmosD
return new CosmosDbBuilder(new CosmosDbConfiguration(oldValue, newValue));
}

/// <inheritdoc cref="IWaitUntil" />
private sealed class WaitUntil : IWaitUntil
/// <summary>
/// Gets an available port.
/// </summary>
private static ushort GetAvailablePort()
{
/// <inheritdoc />
public async Task<bool> 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
}
}
46 changes: 9 additions & 37 deletions src/Testcontainers.CosmosDb/CosmosDbContainer.cs
Original file line number Diff line number Diff line change
@@ -1,16 +1,21 @@
using Microsoft.Azure.Cosmos;

namespace Testcontainers.CosmosDb;

/// <inheritdoc cref="DockerContainer" />
[PublicAPI]
public sealed class CosmosDbContainer : DockerContainer
{
private readonly int _port;

/// <summary>
/// Initializes a new instance of the <see cref="CosmosDbContainer" /> class.
/// </summary>
/// <param name="configuration">The container configuration.</param>
public CosmosDbContainer(CosmosDbConfiguration configuration)
: base(configuration)
{
_port = int.Parse(configuration.PortBindings.First().Value);
}

/// <summary>
Expand All @@ -20,47 +25,14 @@ public CosmosDbContainer(CosmosDbConfiguration configuration)
public string GetConnectionString()
{
var properties = new Dictionary<string, string>();
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)));
}

/// <summary>
/// Gets a configured HTTP message handler that automatically trusts the CosmosDb Emulator's certificate.
/// </summary>
public HttpMessageHandler HttpMessageHandler => new UriRewriter(Hostname, GetMappedPublicPort(CosmosDbBuilder.CosmosDbPort));

/// <summary>
/// Gets a configured HTTP client that automatically trusts the CosmosDb Emulator's certificate.
/// </summary>
public HttpClient HttpClient => new HttpClient(HttpMessageHandler);

/// <summary>
/// Rewrites the HTTP requests to target the running CosmosDb Emulator instance.
/// Gets a configured cosmos client with connection mode set to Gateway.
/// </summary>
private sealed class UriRewriter : DelegatingHandler
{
private readonly string _hostname;
public CosmosClient CosmosClient => new(GetConnectionString(), new() { ConnectionMode = ConnectionMode.Gateway });

private readonly ushort _port;

/// <summary>
/// Initializes a new instance of the <see cref="UriRewriter" /> class.
/// </summary>
/// <param name="hostname">The target hostname.</param>
/// <param name="port">The target port.</param>
public UriRewriter(string hostname, ushort port)
: base(new HttpClientHandler { ServerCertificateCustomValidationCallback = (_, _, _, _) => true })
{
_hostname = hostname;
_port = port;
}

/// <inheritdoc />
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
request.RequestUri = new UriBuilder(Uri.UriSchemeHttps, _hostname, _port, request.RequestUri.PathAndQuery).Uri;
return base.SendAsync(request, cancellationToken);
}
}
}
}
3 changes: 2 additions & 1 deletion src/Testcontainers.CosmosDb/Testcontainers.CosmosDb.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="JetBrains.Annotations" VersionOverride="2023.3.0" PrivateAssets="All"/>
<PackageReference Include="Microsoft.Azure.Cosmos" Version="3.46.0"/>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="../Testcontainers/Testcontainers.csproj"/>
</ItemGroup>
</Project>
</Project>
60 changes: 49 additions & 11 deletions tests/Testcontainers.CosmosDb.Tests/CosmosDbContainerTest.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
namespace Testcontainers.CosmosDb;
using System;
using System.Linq;

namespace Testcontainers.CosmosDb.Tests;

public sealed class CosmosDbContainerTest : IAsyncLifetime
{
Expand All @@ -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<DatabaseProperties>().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<FakeItem>(
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; }
}
}
Loading