Skip to content
Open
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";
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