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