diff --git a/.github/workflows/cicd.yml b/.github/workflows/cicd.yml
index 02852656b..5c6f4a431 100644
--- a/.github/workflows/cicd.yml
+++ b/.github/workflows/cicd.yml
@@ -64,6 +64,7 @@ jobs:
{ name: "Testcontainers.Keycloak", runs-on: "ubuntu-22.04" },
{ name: "Testcontainers.Kusto", runs-on: "ubuntu-22.04" },
{ name: "Testcontainers.LocalStack", runs-on: "ubuntu-22.04" },
+ { name: "Testcontainers.LowkeyVault", runs-on: "ubuntu-22.04" },
{ name: "Testcontainers.MariaDb", runs-on: "ubuntu-22.04" },
{ name: "Testcontainers.Milvus", runs-on: "ubuntu-22.04" },
{ name: "Testcontainers.Minio", runs-on: "ubuntu-22.04" },
diff --git a/Directory.Packages.props b/Directory.Packages.props
index c73d7305c..65716d486 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -75,5 +75,8 @@
+
+
+
diff --git a/Testcontainers.sln b/Testcontainers.sln
index 720ad9de1..42316356d 100644
--- a/Testcontainers.sln
+++ b/Testcontainers.sln
@@ -69,6 +69,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.Kusto", "src
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.LocalStack", "src\Testcontainers.LocalStack\Testcontainers.LocalStack.csproj", "{3792268A-EF08-4569-8118-991E08FD61C4}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.LowkeyVault", "src\Testcontainers.LowkeyVault\Testcontainers.LowkeyVault.csproj", "{436486CE-E855-43DA-A2C7-9832E98BD86E}"
+EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.MariaDb", "src\Testcontainers.MariaDb\Testcontainers.MariaDb.csproj", "{4B204EB3-C478-422E-9B6F-62DF3871291A}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.Milvus", "src\Testcontainers.Milvus\Testcontainers.Milvus.csproj", "{B024E315-831F-429D-92AA-44B839AC10F4}"
@@ -177,6 +179,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.Kusto.Tests"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.LocalStack.Tests", "tests\Testcontainers.LocalStack.Tests\Testcontainers.LocalStack.Tests.csproj", "{728CBE16-1D52-4F84-AF01-7229E6013512}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.LowkeyVault.Tests", "tests\Testcontainers.LowkeyVault.Tests\Testcontainers.LowkeyVault.Tests.csproj", "{CB4F241B-EB79-49D5-A45F-050BEE2191B8}"
+EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.MariaDb.Tests", "tests\Testcontainers.MariaDb.Tests\Testcontainers.MariaDb.Tests.csproj", "{7F0AE083-9DB8-4BD4-91F7-C199DCC7301D}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.Milvus.Tests", "tests\Testcontainers.Milvus.Tests\Testcontainers.Milvus.Tests.csproj", "{5247DF94-32F3-4ED6-AE71-6AB4F4078E6D}"
@@ -244,9 +248,6 @@ Global
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
- GlobalSection(SolutionProperties) = preSolution
- HideSolutionNode = FALSE
- EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{5365F780-0E6C-41F0-B1B9-7DC34368F80C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{5365F780-0E6C-41F0-B1B9-7DC34368F80C}.Debug|Any CPU.Build.0 = Debug|Any CPU
@@ -448,6 +449,10 @@ Global
{64A87DE5-29B0-4A54-9E74-560484D8C7C0}.Debug|Any CPU.Build.0 = Debug|Any CPU
{64A87DE5-29B0-4A54-9E74-560484D8C7C0}.Release|Any CPU.ActiveCfg = Release|Any CPU
{64A87DE5-29B0-4A54-9E74-560484D8C7C0}.Release|Any CPU.Build.0 = Release|Any CPU
+ {436486CE-E855-43DA-A2C7-9832E98BD86E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {436486CE-E855-43DA-A2C7-9832E98BD86E}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {436486CE-E855-43DA-A2C7-9832E98BD86E}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {436486CE-E855-43DA-A2C7-9832E98BD86E}.Release|Any CPU.Build.0 = Release|Any CPU
{380BB29B-F556-404D-B13B-CA250599C565}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{380BB29B-F556-404D-B13B-CA250599C565}.Debug|Any CPU.Build.0 = Debug|Any CPU
{380BB29B-F556-404D-B13B-CA250599C565}.Release|Any CPU.ActiveCfg = Release|Any CPU
@@ -696,11 +701,18 @@ Global
{EBA72C3B-57D5-43FF-A5B4-3D55B3B6D4C2}.Debug|Any CPU.Build.0 = Debug|Any CPU
{EBA72C3B-57D5-43FF-A5B4-3D55B3B6D4C2}.Release|Any CPU.ActiveCfg = Release|Any CPU
{EBA72C3B-57D5-43FF-A5B4-3D55B3B6D4C2}.Release|Any CPU.Build.0 = Release|Any CPU
+ {CB4F241B-EB79-49D5-A45F-050BEE2191B8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {CB4F241B-EB79-49D5-A45F-050BEE2191B8}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {CB4F241B-EB79-49D5-A45F-050BEE2191B8}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {CB4F241B-EB79-49D5-A45F-050BEE2191B8}.Release|Any CPU.Build.0 = Release|Any CPU
{E901DF14-6F05-4FC2-825A-3055FAD33561}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{E901DF14-6F05-4FC2-825A-3055FAD33561}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E901DF14-6F05-4FC2-825A-3055FAD33561}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E901DF14-6F05-4FC2-825A-3055FAD33561}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{5365F780-0E6C-41F0-B1B9-7DC34368F80C} = {673F23AE-7694-4BB9-ABD4-136D6C13634E}
{AB9C1563-07C7-4685-BACD-BB1FF64B3611} = {673F23AE-7694-4BB9-ABD4-136D6C13634E}
@@ -752,6 +764,7 @@ Global
{7D5C6816-0DD2-4E13-A585-033B5D3C80D5} = {673F23AE-7694-4BB9-ABD4-136D6C13634E}
{68F8600D-24E9-4E03-9E25-5F6EB338EAC1} = {673F23AE-7694-4BB9-ABD4-136D6C13634E}
{64A87DE5-29B0-4A54-9E74-560484D8C7C0} = {673F23AE-7694-4BB9-ABD4-136D6C13634E}
+ {436486CE-E855-43DA-A2C7-9832E98BD86E} = {673F23AE-7694-4BB9-ABD4-136D6C13634E}
{380BB29B-F556-404D-B13B-CA250599C565} = {673F23AE-7694-4BB9-ABD4-136D6C13634E}
{84911C93-C2A9-46E9-AE5E-D567306589E5} = {673F23AE-7694-4BB9-ABD4-136D6C13634E}
{EC76857B-A3B8-4B7A-A1B0-8D867A4D1733} = {673F23AE-7694-4BB9-ABD4-136D6C13634E}
@@ -814,6 +827,7 @@ Global
{27CDB869-A150-4593-958F-6F26E5391E7C} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF}
{DDB41BC8-5826-4D97-9C5F-001151E3FFD6} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF}
{EBA72C3B-57D5-43FF-A5B4-3D55B3B6D4C2} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF}
+ {CB4F241B-EB79-49D5-A45F-050BEE2191B8} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF}
{E901DF14-6F05-4FC2-825A-3055FAD33561} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF}
EndGlobalSection
EndGlobal
diff --git a/docs/modules/index.md b/docs/modules/index.md
index f263408cc..f04973794 100644
--- a/docs/modules/index.md
+++ b/docs/modules/index.md
@@ -49,6 +49,7 @@ await moduleNameContainer.StartAsync();
| Keycloak | `quay.io/keycloak/keycloak:21.1` | [NuGet](https://www.nuget.org/packages/Testcontainers.Keycloak) | [Source](https://github.com/testcontainers/testcontainers-dotnet/tree/develop/src/Testcontainers.Keycloak) |
| Kusto emulator | `mcr.microsoft.com/azuredataexplorer/kustainer-linux:latest` | [NuGet](https://www.nuget.org/packages/Testcontainers.Kusto) | [Source](https://github.com/testcontainers/testcontainers-dotnet/tree/develop/src/Testcontainers.Kusto) |
| LocalStack | `localstack/localstack:2.0` | [NuGet](https://www.nuget.org/packages/Testcontainers.LocalStack) | [Source](https://github.com/testcontainers/testcontainers-dotnet/tree/develop/src/Testcontainers.LocalStack) |
+| Lowkey Vault | `nagyesta/lowkey-vault:2.7.1-ubi9-minimal` | [NuGet](https://www.nuget.org/packages/Testcontainers.LowkeyVault) | [Source](https://github.com/testcontainers/testcontainers-dotnet/tree/develop/src/Testcontainers.LowkeyVault) |
| MariaDB | `mariadb:10.10` | [NuGet](https://www.nuget.org/packages/Testcontainers.MariaDb) | [Source](https://github.com/testcontainers/testcontainers-dotnet/tree/develop/src/Testcontainers.MariaDb) |
| Milvus | `milvusdb/milvus:v2.3.10` | [NuGet](https://www.nuget.org/packages/Testcontainers.Milvus) | [Source](https://github.com/testcontainers/testcontainers-dotnet/tree/develop/src/Testcontainers.Milvus) |
| MinIO | `minio/minio:RELEASE.2023-01-31T02-24-19Z` | [NuGet](https://www.nuget.org/packages/Testcontainers.Minio) | [Source](https://github.com/testcontainers/testcontainers-dotnet/tree/develop/src/Testcontainers.Minio) |
diff --git a/src/Testcontainers.LowkeyVault/.editorconfig b/src/Testcontainers.LowkeyVault/.editorconfig
new file mode 100644
index 000000000..6f066619d
--- /dev/null
+++ b/src/Testcontainers.LowkeyVault/.editorconfig
@@ -0,0 +1 @@
+root = true
\ No newline at end of file
diff --git a/src/Testcontainers.LowkeyVault/LowkeyVaultBuilder.cs b/src/Testcontainers.LowkeyVault/LowkeyVaultBuilder.cs
new file mode 100644
index 000000000..2294a9a86
--- /dev/null
+++ b/src/Testcontainers.LowkeyVault/LowkeyVaultBuilder.cs
@@ -0,0 +1,86 @@
+namespace Testcontainers.LowkeyVault;
+
+///
+[PublicAPI]
+public sealed class LowkeyVaultBuilder : ContainerBuilder
+{
+ public const string LowkeyVaultImage = "nagyesta/lowkey-vault:2.7.1-ubi9-minimal";
+
+ public const ushort LowkeyVaultPort = 8443;
+
+ public const ushort LowkeyVaultTokenPort = 8080;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public LowkeyVaultBuilder()
+ : this(new LowkeyVaultConfiguration())
+ {
+ DockerResourceConfiguration = Init().DockerResourceConfiguration;
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The Docker resource configuration.
+ private LowkeyVaultBuilder(LowkeyVaultConfiguration dockerResourceConfiguration)
+ : base(dockerResourceConfiguration)
+ {
+ DockerResourceConfiguration = dockerResourceConfiguration;
+ }
+
+ ///
+ protected override LowkeyVaultConfiguration DockerResourceConfiguration { get; }
+
+ ///
+ /// Collects and appends Lowkey Vault arguments to the container start.
+ ///
+ ///
+ /// The method adds the provided arguments to the LOWKEY_ARGS environment variable.
+ /// E.g. --LOWKEY_DEBUG_REQUEST_LOG=true.
+ ///
+ /// The arguments to add to the LOWKEY_ARGS environment variable.
+ /// A configured instance of .
+ public LowkeyVaultBuilder WithArguments(IEnumerable arguments)
+ {
+ return Merge(DockerResourceConfiguration, new LowkeyVaultConfiguration(arguments: arguments));
+ }
+
+ ///
+ public override LowkeyVaultContainer Build()
+ {
+ Validate();
+
+ var lowkeyVaultBusBuilder = WithEnvironment("LOWKEY_ARGS", string.Join(" ", DockerResourceConfiguration.Arguments));
+ return new LowkeyVaultContainer(lowkeyVaultBusBuilder.DockerResourceConfiguration);
+ }
+
+ ///
+ protected override LowkeyVaultBuilder Init()
+ {
+ return base.Init()
+ .WithImage(LowkeyVaultImage)
+ .WithPortBinding(LowkeyVaultPort, true)
+ .WithPortBinding(LowkeyVaultTokenPort, true)
+ .WithArguments(new[] { "--LOWKEY_VAULT_RELAXED_PORTS=true" })
+ .WithWaitStrategy(Wait.ForUnixContainer().UntilMessageIsLogged("(?s).*Started LowkeyVaultApp.*$"));
+ }
+
+ ///
+ protected override LowkeyVaultBuilder Clone(IResourceConfiguration resourceConfiguration)
+ {
+ return Merge(DockerResourceConfiguration, new LowkeyVaultConfiguration(resourceConfiguration));
+ }
+
+ ///
+ protected override LowkeyVaultBuilder Clone(IContainerConfiguration resourceConfiguration)
+ {
+ return Merge(DockerResourceConfiguration, new LowkeyVaultConfiguration(resourceConfiguration));
+ }
+
+ ///
+ protected override LowkeyVaultBuilder Merge(LowkeyVaultConfiguration oldValue, LowkeyVaultConfiguration newValue)
+ {
+ return new LowkeyVaultBuilder(new LowkeyVaultConfiguration(oldValue, newValue));
+ }
+}
\ No newline at end of file
diff --git a/src/Testcontainers.LowkeyVault/LowkeyVaultConfiguration.cs b/src/Testcontainers.LowkeyVault/LowkeyVaultConfiguration.cs
new file mode 100644
index 000000000..69a72550d
--- /dev/null
+++ b/src/Testcontainers.LowkeyVault/LowkeyVaultConfiguration.cs
@@ -0,0 +1,61 @@
+namespace Testcontainers.LowkeyVault;
+
+///
+[PublicAPI]
+public sealed class LowkeyVaultConfiguration : ContainerConfiguration
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The arguments to add to the LOWKEY_ARGS environment variable.
+ public LowkeyVaultConfiguration(IEnumerable arguments = null)
+ {
+ Arguments = arguments;
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The Docker resource configuration.
+ public LowkeyVaultConfiguration(IResourceConfiguration resourceConfiguration)
+ : base(resourceConfiguration)
+ {
+ // Passes the configuration upwards to the base implementations to create an updated immutable copy.
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The Docker resource configuration.
+ public LowkeyVaultConfiguration(IContainerConfiguration resourceConfiguration)
+ : base(resourceConfiguration)
+ {
+ // Passes the configuration upwards to the base implementations to create an updated immutable copy.
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The Docker resource configuration.
+ public LowkeyVaultConfiguration(LowkeyVaultConfiguration resourceConfiguration)
+ : this(new LowkeyVaultConfiguration(), resourceConfiguration)
+ {
+ // Passes the configuration upwards to the base implementations to create an updated immutable copy.
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The old Docker resource configuration.
+ /// The new Docker resource configuration.
+ public LowkeyVaultConfiguration(LowkeyVaultConfiguration oldValue, LowkeyVaultConfiguration newValue)
+ : base(oldValue, newValue)
+ {
+ Arguments = BuildConfiguration.Combine(oldValue.Arguments, newValue.Arguments);
+ }
+
+ ///
+ /// Gets the arguments that are added to the LOWKEY_ARGS environment variable.
+ ///
+ public IEnumerable Arguments { get; }
+}
\ No newline at end of file
diff --git a/src/Testcontainers.LowkeyVault/LowkeyVaultContainer.cs b/src/Testcontainers.LowkeyVault/LowkeyVaultContainer.cs
new file mode 100644
index 000000000..9d53942dd
--- /dev/null
+++ b/src/Testcontainers.LowkeyVault/LowkeyVaultContainer.cs
@@ -0,0 +1,90 @@
+namespace Testcontainers.LowkeyVault;
+
+///
+[PublicAPI]
+public sealed class LowkeyVaultContainer : DockerContainer
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The container configuration.
+ public LowkeyVaultContainer(LowkeyVaultConfiguration configuration)
+ : base(configuration)
+ {
+ }
+
+ ///
+ /// Gets the base HTTPS address for the Lowkey Vault service.
+ ///
+ /// The base address URL.
+ public string GetBaseAddress()
+ {
+ return new UriBuilder(Uri.UriSchemeHttps, Hostname, GetMappedPublicPort(LowkeyVaultBuilder.LowkeyVaultPort)).ToString();
+ }
+
+ ///
+ /// Gets the URL used to request the authentication token.
+ ///
+ /// The authentication token URL.
+ public string GetAuthTokenUrl()
+ {
+ const string identityAuthTokenUriPath = "/metadata/identity/oauth2/token";
+ return new UriBuilder(Uri.UriSchemeHttp, Hostname, GetMappedPublicPort(LowkeyVaultBuilder.LowkeyVaultTokenPort), identityAuthTokenUriPath).ToString();
+ }
+
+ ///
+ /// Gets the default certificate from the Lowkey Vault service.
+ ///
+ /// A collection containing the default .
+ public async Task GetCertificateAsync()
+ {
+ const string defaultCertFilePathUriPath = "/metadata/default-cert/lowkey-vault.p12";
+
+ var requestUri = new UriBuilder(Uri.UriSchemeHttp, Hostname, GetMappedPublicPort(LowkeyVaultBuilder.LowkeyVaultTokenPort), defaultCertFilePathUriPath).Uri;
+
+ using var httpClient = new HttpClient();
+
+ using var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, requestUri);
+
+ using var httpResponseMessage = await httpClient.SendAsync(httpRequestMessage)
+ .ConfigureAwait(false);
+
+ httpResponseMessage.EnsureSuccessStatusCode();
+
+ var certificateBytes = await httpResponseMessage.Content.ReadAsByteArrayAsync()
+ .ConfigureAwait(false);
+
+ var certificatePassword = await GetCertificatePasswordAsync()
+ .ConfigureAwait(false);
+
+#if NET9_0_OR_GREATER
+ return X509CertificateLoader.LoadPkcs12Collection(certificateBytes, certificatePassword);
+#else
+ var certificate = new X509Certificate2(certificateBytes, certificatePassword);
+ return new X509Certificate2Collection(certificate);
+#endif
+ }
+
+ ///
+ /// Gets the password for the default certificate from the Lowkey Vault service.
+ ///
+ /// The default certificate password.
+ public async Task GetCertificatePasswordAsync()
+ {
+ const string defaultCertPasswordUriPath = "/metadata/default-cert/password";
+
+ var requestUri = new UriBuilder(Uri.UriSchemeHttp, Hostname, GetMappedPublicPort(LowkeyVaultBuilder.LowkeyVaultTokenPort), defaultCertPasswordUriPath).Uri;
+
+ using var httpClient = new HttpClient();
+
+ using var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, requestUri);
+
+ using var httpResponseMessage = await httpClient.SendAsync(httpRequestMessage)
+ .ConfigureAwait(false);
+
+ httpResponseMessage.EnsureSuccessStatusCode();
+
+ return await httpResponseMessage.Content.ReadAsStringAsync()
+ .ConfigureAwait(false);
+ }
+}
\ No newline at end of file
diff --git a/src/Testcontainers.LowkeyVault/Testcontainers.LowkeyVault.csproj b/src/Testcontainers.LowkeyVault/Testcontainers.LowkeyVault.csproj
new file mode 100644
index 000000000..9a25b9c4d
--- /dev/null
+++ b/src/Testcontainers.LowkeyVault/Testcontainers.LowkeyVault.csproj
@@ -0,0 +1,12 @@
+
+
+ net8.0;net9.0;netstandard2.0;netstandard2.1
+ latest
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Testcontainers.LowkeyVault/Usings.cs b/src/Testcontainers.LowkeyVault/Usings.cs
new file mode 100644
index 000000000..74dd2925d
--- /dev/null
+++ b/src/Testcontainers.LowkeyVault/Usings.cs
@@ -0,0 +1,10 @@
+global using System;
+global using System.Collections.Generic;
+global using System.Net.Http;
+global using System.Security.Cryptography.X509Certificates;
+global using System.Threading.Tasks;
+global using Docker.DotNet.Models;
+global using DotNet.Testcontainers.Builders;
+global using DotNet.Testcontainers.Configurations;
+global using DotNet.Testcontainers.Containers;
+global using JetBrains.Annotations;
\ No newline at end of file
diff --git a/tests/Testcontainers.LowkeyVault.Tests/.editorconfig b/tests/Testcontainers.LowkeyVault.Tests/.editorconfig
new file mode 100644
index 000000000..6f066619d
--- /dev/null
+++ b/tests/Testcontainers.LowkeyVault.Tests/.editorconfig
@@ -0,0 +1 @@
+root = true
\ No newline at end of file
diff --git a/tests/Testcontainers.LowkeyVault.Tests/LowkeyVaultContainerTest.cs b/tests/Testcontainers.LowkeyVault.Tests/LowkeyVaultContainerTest.cs
new file mode 100644
index 000000000..36a001b1f
--- /dev/null
+++ b/tests/Testcontainers.LowkeyVault.Tests/LowkeyVaultContainerTest.cs
@@ -0,0 +1,165 @@
+namespace Testcontainers.LowkeyVault;
+
+public abstract class LowkeyVaultContainerTest : IAsyncLifetime
+{
+ private readonly LowkeyVaultContainer _lowkeyVaultContainer = new LowkeyVaultBuilder().Build();
+
+ protected abstract TokenCredential GetTokenCredential();
+
+ public Task InitializeAsync()
+ {
+ return _lowkeyVaultContainer.StartAsync();
+ }
+
+ public Task DisposeAsync()
+ {
+ return _lowkeyVaultContainer.DisposeAsync().AsTask();
+ }
+
+ [Fact]
+ [Trait(nameof(DockerCli.DockerPlatform), nameof(DockerCli.DockerPlatform.Linux))]
+ public async Task ServerCertificateValidationSucceedsWithTrustedCertificate()
+ {
+ // Given
+ var baseAddress = _lowkeyVaultContainer.GetBaseAddress();
+
+ var certificates = await _lowkeyVaultContainer.GetCertificateAsync();
+
+ using var httpMessageHandler = new HttpClientHandler();
+ httpMessageHandler.ServerCertificateCustomValidationCallback = (_, cert, _, _) => certificates.IndexOf(cert) > -1;
+
+ using var httpClient = new HttpClient(httpMessageHandler);
+ httpClient.BaseAddress = new Uri(baseAddress);
+
+ // When
+ using var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, "management/vault");
+
+ using var httpResponseMessage = await httpClient.SendAsync(httpRequestMessage)
+ .ConfigureAwait(true);
+
+ // Then
+ Assert.Equal(HttpStatusCode.OK, httpResponseMessage.StatusCode);
+ }
+
+ [Fact]
+ [Trait(nameof(DockerCli.DockerPlatform), nameof(DockerCli.DockerPlatform.Linux))]
+ public async Task GetSecretReturnsSetSecret()
+ {
+ // Given
+ const string secretName = "name";
+
+ const string secretValue = "value";
+
+ var baseAddress = _lowkeyVaultContainer.GetBaseAddress();
+
+ var secretClient = new SecretClient(new Uri(baseAddress), GetTokenCredential(), GetSecretClientOptions());
+
+ await secretClient.SetSecretAsync(secretName, secretValue)
+ .ConfigureAwait(true);
+
+ // When
+ var keyVaultSecret = await secretClient.GetSecretAsync(secretName)
+ .ConfigureAwait(true);
+
+ // Then
+ Assert.NotNull(keyVaultSecret.Value);
+ Assert.Equal(secretName, keyVaultSecret.Value.Name);
+ Assert.Equal(secretValue, keyVaultSecret.Value.Value);
+ }
+
+ [Fact]
+ [Trait(nameof(DockerCli.DockerPlatform), nameof(DockerCli.DockerPlatform.Linux))]
+ public async Task DownloadCertificateReturnsCreatedCertificate()
+ {
+ // Given
+ const string certificateName = "certificate";
+
+ const string subject = "CN=localhost";
+
+ var baseAddress = _lowkeyVaultContainer.GetBaseAddress();
+
+ var certificateClient = new CertificateClient(new Uri(baseAddress), GetTokenCredential(), GetCertificateClientOptions());
+
+ var certificatePolicy = new CertificatePolicy("self", subject);
+ certificatePolicy.KeyType = CertificateKeyType.Rsa;
+ certificatePolicy.KeySize = 2048;
+ certificatePolicy.ContentType = CertificateContentType.Pem;
+ certificatePolicy.Exportable = true;
+ certificatePolicy.ValidityInMonths = 12;
+
+ // When
+ var certificateOperation = await certificateClient.StartCreateCertificateAsync(certificateName, certificatePolicy)
+ .ConfigureAwait(true);
+
+ await certificateOperation.WaitForCompletionAsync()
+ .ConfigureAwait(true);
+
+ var response = await certificateClient.DownloadCertificateAsync(certificateName)
+ .ConfigureAwait(true);
+
+ using var certificate = response!.Value;
+
+ // Then
+ Assert.Equal(subject, certificate.Subject);
+ Assert.NotNull(certificate.GetRSAPublicKey());
+ Assert.NotNull(certificate.GetRSAPrivateKey());
+ }
+
+ private static SecretClientOptions GetSecretClientOptions()
+ {
+ var httpMessageHandler = new HttpClientHandler();
+ httpMessageHandler.ServerCertificateCustomValidationCallback = (_, _, _, _) => true;
+
+ var secretClientOptions = new SecretClientOptions();
+ secretClientOptions.Transport = new HttpClientTransport(httpMessageHandler);
+ secretClientOptions.DisableChallengeResourceVerification = true;
+ return secretClientOptions;
+ }
+
+ private static CertificateClientOptions GetCertificateClientOptions()
+ {
+ var httpMessageHandler = new HttpClientHandler();
+ httpMessageHandler.ServerCertificateCustomValidationCallback = (_, _, _, _) => true;
+
+ var secretClientOptions = new CertificateClientOptions();
+ secretClientOptions.Transport = new HttpClientTransport(httpMessageHandler);
+ secretClientOptions.DisableChallengeResourceVerification = true;
+ return secretClientOptions;
+ }
+
+ [UsedImplicitly]
+ public sealed class AzureCredentialConfiguration : LowkeyVaultContainerTest
+ {
+ protected override TokenCredential GetTokenCredential()
+ {
+ // This isn't a recommended approach. It stops you from running multiple containers
+ // at the same time.
+ const EnvironmentVariableTarget envVarTarget = EnvironmentVariableTarget.Process;
+ Environment.SetEnvironmentVariable("IDENTITY_ENDPOINT", _lowkeyVaultContainer.GetAuthTokenUrl(), envVarTarget);
+ Environment.SetEnvironmentVariable("IDENTITY_HEADER", "header", envVarTarget);
+ return new DefaultAzureCredential();
+ }
+ }
+
+ [UsedImplicitly]
+ public sealed class NoopCredentialConfiguration : LowkeyVaultContainerTest
+ {
+ protected override TokenCredential GetTokenCredential()
+ {
+ return new NoopCredential();
+ }
+
+ private sealed class NoopCredential : TokenCredential
+ {
+ public override AccessToken GetToken(TokenRequestContext requestContext, CancellationToken cancellationToken)
+ {
+ return new AccessToken("noop", DateTimeOffset.UtcNow.AddHours(1));
+ }
+
+ public override ValueTask GetTokenAsync(TokenRequestContext requestContext, CancellationToken cancellationToken)
+ {
+ return new ValueTask(GetToken(requestContext, cancellationToken));
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/Testcontainers.LowkeyVault.Tests/Testcontainers.LowkeyVault.Tests.csproj b/tests/Testcontainers.LowkeyVault.Tests/Testcontainers.LowkeyVault.Tests.csproj
new file mode 100644
index 000000000..82b9967bd
--- /dev/null
+++ b/tests/Testcontainers.LowkeyVault.Tests/Testcontainers.LowkeyVault.Tests.csproj
@@ -0,0 +1,20 @@
+
+
+ net9.0
+ false
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/tests/Testcontainers.LowkeyVault.Tests/Usings.cs b/tests/Testcontainers.LowkeyVault.Tests/Usings.cs
new file mode 100644
index 000000000..fd554ccb8
--- /dev/null
+++ b/tests/Testcontainers.LowkeyVault.Tests/Usings.cs
@@ -0,0 +1,14 @@
+global using System;
+global using System.Net;
+global using System.Net.Http;
+global using System.Security.Cryptography.X509Certificates;
+global using System.Threading;
+global using System.Threading.Tasks;
+global using Azure.Core;
+global using Azure.Core.Pipeline;
+global using Azure.Identity;
+global using Azure.Security.KeyVault.Certificates;
+global using Azure.Security.KeyVault.Secrets;
+global using DotNet.Testcontainers.Commons;
+global using JetBrains.Annotations;
+global using Xunit;
\ No newline at end of file