From adb94d62e9ff488a83d46cee79693a4919f0d5f1 Mon Sep 17 00:00:00 2001 From: Valters Melnalksnis Date: Thu, 4 May 2023 08:26:56 +0300 Subject: [PATCH 1/6] feat: Add Keycloak module --- Testcontainers.sln | 14 ++ src/Testcontainers.Keycloak/.editorconfig | 1 + .../KeycloakBuilder.cs | 124 ++++++++++++++++++ .../KeycloakConfiguration.cs | 69 ++++++++++ .../KeycloakContainer.cs | 33 +++++ .../Testcontainers.Keycloak.csproj | 13 ++ src/Testcontainers.Keycloak/Usings.cs | 9 ++ .../.editorconfig | 1 + .../KeycloakContainerTest.cs | 43 ++++++ .../Testcontainers.Keycloak.Tests.csproj | 19 +++ tests/Testcontainers.Keycloak.Tests/Usings.cs | 5 + 11 files changed, 331 insertions(+) create mode 100644 src/Testcontainers.Keycloak/.editorconfig create mode 100644 src/Testcontainers.Keycloak/KeycloakBuilder.cs create mode 100644 src/Testcontainers.Keycloak/KeycloakConfiguration.cs create mode 100644 src/Testcontainers.Keycloak/KeycloakContainer.cs create mode 100644 src/Testcontainers.Keycloak/Testcontainers.Keycloak.csproj create mode 100644 src/Testcontainers.Keycloak/Usings.cs create mode 100644 tests/Testcontainers.Keycloak.Tests/.editorconfig create mode 100644 tests/Testcontainers.Keycloak.Tests/KeycloakContainerTest.cs create mode 100644 tests/Testcontainers.Keycloak.Tests/Testcontainers.Keycloak.Tests.csproj create mode 100644 tests/Testcontainers.Keycloak.Tests/Usings.cs diff --git a/Testcontainers.sln b/Testcontainers.sln index a86fd5cb6..f7c31cf85 100644 --- a/Testcontainers.sln +++ b/Testcontainers.sln @@ -121,6 +121,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.Tests", "tes EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.WebDriver.Tests", "tests\Testcontainers.WebDriver.Tests\Testcontainers.WebDriver.Tests.csproj", "{EBA72C3B-57D5-43FF-A5B4-3D55B3B6D4C2}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.Keycloak", "src\Testcontainers.Keycloak\Testcontainers.Keycloak.csproj", "{AA8834A3-82A7-4E83-8E4C-88D37F74056A}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.Keycloak.Tests", "tests\Testcontainers.Keycloak.Tests\Testcontainers.Keycloak.Tests.csproj", "{4827D606-89D5-4E00-8341-47A6E95817BA}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -346,6 +350,14 @@ 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 + {AA8834A3-82A7-4E83-8E4C-88D37F74056A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AA8834A3-82A7-4E83-8E4C-88D37F74056A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AA8834A3-82A7-4E83-8E4C-88D37F74056A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AA8834A3-82A7-4E83-8E4C-88D37F74056A}.Release|Any CPU.Build.0 = Release|Any CPU + {4827D606-89D5-4E00-8341-47A6E95817BA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4827D606-89D5-4E00-8341-47A6E95817BA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4827D606-89D5-4E00-8341-47A6E95817BA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4827D606-89D5-4E00-8341-47A6E95817BA}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution {3F2E254F-C203-43FD-A078-DC3E2CBC0F9F} = {673F23AE-7694-4BB9-ABD4-136D6C13634E} @@ -402,5 +414,7 @@ Global {1A1983E6-5297-435F-B467-E8E1F11277D6} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF} {27CDB869-A150-4593-958F-6F26E5391E7C} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF} {EBA72C3B-57D5-43FF-A5B4-3D55B3B6D4C2} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF} + {AA8834A3-82A7-4E83-8E4C-88D37F74056A} = {673F23AE-7694-4BB9-ABD4-136D6C13634E} + {4827D606-89D5-4E00-8341-47A6E95817BA} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF} EndGlobalSection EndGlobal diff --git a/src/Testcontainers.Keycloak/.editorconfig b/src/Testcontainers.Keycloak/.editorconfig new file mode 100644 index 000000000..6f066619d --- /dev/null +++ b/src/Testcontainers.Keycloak/.editorconfig @@ -0,0 +1 @@ +root = true \ No newline at end of file diff --git a/src/Testcontainers.Keycloak/KeycloakBuilder.cs b/src/Testcontainers.Keycloak/KeycloakBuilder.cs new file mode 100644 index 000000000..d8677cc14 --- /dev/null +++ b/src/Testcontainers.Keycloak/KeycloakBuilder.cs @@ -0,0 +1,124 @@ +namespace Testcontainers.Keycloak; + +/// +[PublicAPI] +public sealed class KeycloakBuilder : ContainerBuilder +{ + public const string KeycloakImage = "quay.io/keycloak/keycloak:21.1.1"; + public const ushort KeycloakPort = 8080; + public const string DefaultUsername = "admin"; + public const string DefaultPassword = "admin"; + + /// + /// Initializes a new instance of the class. + /// + public KeycloakBuilder() + : this(new()) + { + DockerResourceConfiguration = Init().DockerResourceConfiguration; + } + + /// + /// Initializes a new instance of the class. + /// + /// The Docker resource configuration. + private KeycloakBuilder(KeycloakConfiguration resourceConfiguration) + : base(resourceConfiguration) + { + DockerResourceConfiguration = resourceConfiguration; + } + + /// + protected override KeycloakConfiguration DockerResourceConfiguration { get; } + + /// + /// Sets the Keycloak admin username. + /// + /// The Keycloak admin username. + /// A configured instance of . + public KeycloakBuilder WithUsername(string username) + { + return Merge(DockerResourceConfiguration, new(username: username)) + .WithEnvironment("KEYCLOAK_ADMIN", username) + .WithWaitStrategy(Wait.ForUnixContainer().UntilPortIsAvailable(KeycloakPort).AddCustomWaitStrategy(new WaitUntil(username))); + } + + /// + /// Sets the Keycloak admin password. + /// + /// The Keycloak admin password. + /// A configured instance of . + public KeycloakBuilder WithPassword(string password) + { + return Merge(DockerResourceConfiguration, new(password: password)) + .WithEnvironment("KEYCLOAK_ADMIN_PASSWORD", password); + } + + /// + public override KeycloakContainer Build() + { + Validate(); + return new(DockerResourceConfiguration, TestcontainersSettings.Logger); + } + + /// + protected override KeycloakBuilder Init() + { + return base.Init() + .WithImage(KeycloakImage) + .WithCommand("start-dev") + .WithPortBinding(KeycloakPort, true) + .WithUsername(DefaultUsername) + .WithPassword(DefaultPassword); + } + + /// + protected override void Validate() + { + base.Validate(); + + _ = Guard.Argument(DockerResourceConfiguration.Username, nameof(DockerResourceConfiguration.Username)) + .NotNull() + .NotEmpty(); + + _ = Guard.Argument(DockerResourceConfiguration.Password, nameof(DockerResourceConfiguration.Password)) + .NotNull() + .NotEmpty(); + } + + /// + protected override KeycloakBuilder Clone(IResourceConfiguration resourceConfiguration) + { + return Merge(DockerResourceConfiguration, new(resourceConfiguration)); + } + + /// + protected override KeycloakBuilder Clone(IContainerConfiguration resourceConfiguration) + { + return Merge(DockerResourceConfiguration, new(resourceConfiguration)); + } + + /// + protected override KeycloakBuilder Merge(KeycloakConfiguration oldValue, KeycloakConfiguration newValue) + { + return new(new(oldValue, newValue)); + } + + private sealed class WaitUntil : IWaitUntil + { + private readonly string _pattern; + + public WaitUntil(string username) + { + _pattern = $"Added user '{username}' to realm 'master'"; + } + + public async Task UntilAsync(IContainer container) + { + var (stdout, _) = await container.GetLogsAsync(timestampsEnabled: false) + .ConfigureAwait(false); + + return stdout.Contains(_pattern); + } + } +} diff --git a/src/Testcontainers.Keycloak/KeycloakConfiguration.cs b/src/Testcontainers.Keycloak/KeycloakConfiguration.cs new file mode 100644 index 000000000..b9fd43469 --- /dev/null +++ b/src/Testcontainers.Keycloak/KeycloakConfiguration.cs @@ -0,0 +1,69 @@ +namespace Testcontainers.Keycloak; + +/// +[PublicAPI] +public sealed class KeycloakConfiguration : ContainerConfiguration +{ + /// + /// Initializes a new instance of the class. + /// + /// The admin username. + /// The admin password. + public KeycloakConfiguration(string username = null, string password = null) + { + Username = username; + Password = password; + } + + /// + /// Initializes a new instance of the class. + /// + /// The Docker resource configuration. + public KeycloakConfiguration(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 KeycloakConfiguration(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 KeycloakConfiguration(KeycloakConfiguration resourceConfiguration) + : this(new(), 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 KeycloakConfiguration(KeycloakConfiguration oldValue, KeycloakConfiguration newValue) + : base(oldValue, newValue) + { + Username = BuildConfiguration.Combine(oldValue.Username, newValue.Username); + Password = BuildConfiguration.Combine(oldValue.Password, newValue.Password); + } + + /// + /// Gets the admin username. + /// + public string Username { get; } = null!; + + /// + /// Gets the admin password. + /// + public string Password { get; } = null!; +} diff --git a/src/Testcontainers.Keycloak/KeycloakContainer.cs b/src/Testcontainers.Keycloak/KeycloakContainer.cs new file mode 100644 index 000000000..ddc512d6e --- /dev/null +++ b/src/Testcontainers.Keycloak/KeycloakContainer.cs @@ -0,0 +1,33 @@ +namespace Testcontainers.Keycloak; + +/// +[PublicAPI] +public sealed class KeycloakContainer : DockerContainer +{ + private readonly KeycloakConfiguration _configuration; + + /// + /// Initializes a new instance of the class. + /// + /// The container configuration. + /// The logger. + public KeycloakContainer(KeycloakConfiguration configuration, ILogger logger) + : base(configuration, logger) + { + _configuration = configuration; + } + + /// + /// Gets the base address for this Keycloak instance. + /// + /// The base address for this Keycloak instance + public Uri GetBaseAddress() + { + return new UriBuilder + { + Scheme = "http", + Host = Hostname, + Port = GetMappedPublicPort(KeycloakBuilder.KeycloakPort) + }.Uri; + } +} diff --git a/src/Testcontainers.Keycloak/Testcontainers.Keycloak.csproj b/src/Testcontainers.Keycloak/Testcontainers.Keycloak.csproj new file mode 100644 index 000000000..9e7774559 --- /dev/null +++ b/src/Testcontainers.Keycloak/Testcontainers.Keycloak.csproj @@ -0,0 +1,13 @@ + + + netstandard2.0;netstandard2.1 + latest + + + + + + + + + \ No newline at end of file diff --git a/src/Testcontainers.Keycloak/Usings.cs b/src/Testcontainers.Keycloak/Usings.cs new file mode 100644 index 000000000..5661931d0 --- /dev/null +++ b/src/Testcontainers.Keycloak/Usings.cs @@ -0,0 +1,9 @@ +global using System; +global using System.Threading.Tasks; +global using Docker.DotNet.Models; +global using DotNet.Testcontainers; +global using DotNet.Testcontainers.Builders; +global using DotNet.Testcontainers.Configurations; +global using DotNet.Testcontainers.Containers; +global using JetBrains.Annotations; +global using Microsoft.Extensions.Logging; diff --git a/tests/Testcontainers.Keycloak.Tests/.editorconfig b/tests/Testcontainers.Keycloak.Tests/.editorconfig new file mode 100644 index 000000000..6f066619d --- /dev/null +++ b/tests/Testcontainers.Keycloak.Tests/.editorconfig @@ -0,0 +1 @@ +root = true \ No newline at end of file diff --git a/tests/Testcontainers.Keycloak.Tests/KeycloakContainerTest.cs b/tests/Testcontainers.Keycloak.Tests/KeycloakContainerTest.cs new file mode 100644 index 000000000..8d1d99215 --- /dev/null +++ b/tests/Testcontainers.Keycloak.Tests/KeycloakContainerTest.cs @@ -0,0 +1,43 @@ +namespace Testcontainers.Keycloak.Tests; + +public sealed class KeycloakContainerTest : IAsyncLifetime +{ + private readonly KeycloakContainer _keycloakContainer = new KeycloakBuilder().Build(); + + public Task InitializeAsync() + { + return _keycloakContainer.StartAsync(); + } + + public Task DisposeAsync() + { + return _keycloakContainer.DisposeAsync().AsTask(); + } + + [Fact] + public async Task GetBaseAddressReturnsValidAddress() + { + // Given + using var httpClient = new HttpClient { BaseAddress = _keycloakContainer.GetBaseAddress() }; + + // When + using var response = await httpClient.GetAsync("/realms/master/.well-known/openid-configuration"); + + // Then + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + } + + [Fact] + public async Task MasterRealmIsCreatedAndEnabled() + { + // Given + var baseAddress = _keycloakContainer.GetBaseAddress().AbsoluteUri; + var keycloakClient = new KeycloakClient(baseAddress, KeycloakBuilder.DefaultUsername, KeycloakBuilder.DefaultPassword); + + // When + var masterRealm = await keycloakClient.GetRealmAsync("master"); + + // Then + Assert.True(masterRealm.Enabled); + } +} diff --git a/tests/Testcontainers.Keycloak.Tests/Testcontainers.Keycloak.Tests.csproj b/tests/Testcontainers.Keycloak.Tests/Testcontainers.Keycloak.Tests.csproj new file mode 100644 index 000000000..547d15bea --- /dev/null +++ b/tests/Testcontainers.Keycloak.Tests/Testcontainers.Keycloak.Tests.csproj @@ -0,0 +1,19 @@ + + + net6.0 + false + false + + + + + + + + + + + + + + diff --git a/tests/Testcontainers.Keycloak.Tests/Usings.cs b/tests/Testcontainers.Keycloak.Tests/Usings.cs new file mode 100644 index 000000000..02f69975b --- /dev/null +++ b/tests/Testcontainers.Keycloak.Tests/Usings.cs @@ -0,0 +1,5 @@ +global using System.Net; +global using System.Net.Http; +global using System.Threading.Tasks; +global using Keycloak.Net; +global using Xunit; From e0fba529b2ecc4e30301840f6e093ed1cd1c1f61 Mon Sep 17 00:00:00 2001 From: Andre Hofmeister <9199345+HofmeisterAn@users.noreply.github.com> Date: Mon, 8 May 2023 10:33:01 +0200 Subject: [PATCH 2/6] chore: Apply some minor code formatting (consistency across the repos) --- Testcontainers.sln | 28 +- .../KeycloakBuilder.cs | 239 +++++++++--------- .../KeycloakConfiguration.cs | 114 ++++----- .../KeycloakContainer.cs | 45 ++-- .../Testcontainers.Keycloak.csproj | 22 +- src/Testcontainers.Keycloak/Usings.cs | 2 +- .../KeycloakContainerTest.cs | 80 +++--- .../Testcontainers.Keycloak.Tests.csproj | 35 ++- tests/Testcontainers.Keycloak.Tests/Usings.cs | 3 +- 9 files changed, 284 insertions(+), 284 deletions(-) diff --git a/Testcontainers.sln b/Testcontainers.sln index f7c31cf85..793db4b9c 100644 --- a/Testcontainers.sln +++ b/Testcontainers.sln @@ -31,6 +31,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.K3s", "src\T EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.Kafka", "src\Testcontainers.Kafka\Testcontainers.Kafka.csproj", "{E93E40CE-59AA-4561-9B4C-E7B0A686326E}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.Keycloak", "src\Testcontainers.Keycloak\Testcontainers.Keycloak.csproj", "{AA8834A3-82A7-4E83-8E4C-88D37F74056A}" +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.MariaDb", "src\Testcontainers.MariaDb\Testcontainers.MariaDb.csproj", "{4B204EB3-C478-422E-9B6F-62DF3871291A}" @@ -83,6 +85,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.K3s.Tests", EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.Kafka.Tests", "tests\Testcontainers.Kafka.Tests\Testcontainers.Kafka.Tests.csproj", "{6F2AEE03-629A-4B49-BD5B-25CA3C61FFB7}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.Keycloak.Tests", "tests\Testcontainers.Keycloak.Tests\Testcontainers.Keycloak.Tests.csproj", "{4827D606-89D5-4E00-8341-47A6E95817BA}" +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.MariaDb.Tests", "tests\Testcontainers.MariaDb.Tests\Testcontainers.MariaDb.Tests.csproj", "{7F0AE083-9DB8-4BD4-91F7-C199DCC7301D}" @@ -121,10 +125,6 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.Tests", "tes EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.WebDriver.Tests", "tests\Testcontainers.WebDriver.Tests\Testcontainers.WebDriver.Tests.csproj", "{EBA72C3B-57D5-43FF-A5B4-3D55B3B6D4C2}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.Keycloak", "src\Testcontainers.Keycloak\Testcontainers.Keycloak.csproj", "{AA8834A3-82A7-4E83-8E4C-88D37F74056A}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.Keycloak.Tests", "tests\Testcontainers.Keycloak.Tests\Testcontainers.Keycloak.Tests.csproj", "{4827D606-89D5-4E00-8341-47A6E95817BA}" -EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -170,6 +170,10 @@ Global {E93E40CE-59AA-4561-9B4C-E7B0A686326E}.Debug|Any CPU.Build.0 = Debug|Any CPU {E93E40CE-59AA-4561-9B4C-E7B0A686326E}.Release|Any CPU.ActiveCfg = Release|Any CPU {E93E40CE-59AA-4561-9B4C-E7B0A686326E}.Release|Any CPU.Build.0 = Release|Any CPU + {AA8834A3-82A7-4E83-8E4C-88D37F74056A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AA8834A3-82A7-4E83-8E4C-88D37F74056A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AA8834A3-82A7-4E83-8E4C-88D37F74056A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AA8834A3-82A7-4E83-8E4C-88D37F74056A}.Release|Any CPU.Build.0 = Release|Any CPU {3792268A-EF08-4569-8118-991E08FD61C4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {3792268A-EF08-4569-8118-991E08FD61C4}.Debug|Any CPU.Build.0 = Debug|Any CPU {3792268A-EF08-4569-8118-991E08FD61C4}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -274,6 +278,10 @@ Global {6F2AEE03-629A-4B49-BD5B-25CA3C61FFB7}.Debug|Any CPU.Build.0 = Debug|Any CPU {6F2AEE03-629A-4B49-BD5B-25CA3C61FFB7}.Release|Any CPU.ActiveCfg = Release|Any CPU {6F2AEE03-629A-4B49-BD5B-25CA3C61FFB7}.Release|Any CPU.Build.0 = Release|Any CPU + {4827D606-89D5-4E00-8341-47A6E95817BA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4827D606-89D5-4E00-8341-47A6E95817BA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4827D606-89D5-4E00-8341-47A6E95817BA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4827D606-89D5-4E00-8341-47A6E95817BA}.Release|Any CPU.Build.0 = Release|Any CPU {728CBE16-1D52-4F84-AF01-7229E6013512}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {728CBE16-1D52-4F84-AF01-7229E6013512}.Debug|Any CPU.Build.0 = Debug|Any CPU {728CBE16-1D52-4F84-AF01-7229E6013512}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -350,14 +358,6 @@ 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 - {AA8834A3-82A7-4E83-8E4C-88D37F74056A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {AA8834A3-82A7-4E83-8E4C-88D37F74056A}.Debug|Any CPU.Build.0 = Debug|Any CPU - {AA8834A3-82A7-4E83-8E4C-88D37F74056A}.Release|Any CPU.ActiveCfg = Release|Any CPU - {AA8834A3-82A7-4E83-8E4C-88D37F74056A}.Release|Any CPU.Build.0 = Release|Any CPU - {4827D606-89D5-4E00-8341-47A6E95817BA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {4827D606-89D5-4E00-8341-47A6E95817BA}.Debug|Any CPU.Build.0 = Debug|Any CPU - {4827D606-89D5-4E00-8341-47A6E95817BA}.Release|Any CPU.ActiveCfg = Release|Any CPU - {4827D606-89D5-4E00-8341-47A6E95817BA}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution {3F2E254F-C203-43FD-A078-DC3E2CBC0F9F} = {673F23AE-7694-4BB9-ABD4-136D6C13634E} @@ -369,6 +369,7 @@ Global {84D707E0-C9FA-4327-85DC-0AFEBEA73572} = {673F23AE-7694-4BB9-ABD4-136D6C13634E} {111B840F-9DB0-4166-83E6-0580FD418F07} = {673F23AE-7694-4BB9-ABD4-136D6C13634E} {E93E40CE-59AA-4561-9B4C-E7B0A686326E} = {673F23AE-7694-4BB9-ABD4-136D6C13634E} + {AA8834A3-82A7-4E83-8E4C-88D37F74056A} = {673F23AE-7694-4BB9-ABD4-136D6C13634E} {3792268A-EF08-4569-8118-991E08FD61C4} = {673F23AE-7694-4BB9-ABD4-136D6C13634E} {4B204EB3-C478-422E-9B6F-62DF3871291A} = {673F23AE-7694-4BB9-ABD4-136D6C13634E} {1266E1E6-5CEF-4161-8B45-83282455746E} = {673F23AE-7694-4BB9-ABD4-136D6C13634E} @@ -395,6 +396,7 @@ Global {64F8E9B9-78FD-4E13-BDDF-0340E2D4E1D0} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF} {F0F40AE2-70FF-4191-ADDA-26A19E0D1A0F} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF} {6F2AEE03-629A-4B49-BD5B-25CA3C61FFB7} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF} + {4827D606-89D5-4E00-8341-47A6E95817BA} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF} {728CBE16-1D52-4F84-AF01-7229E6013512} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF} {7F0AE083-9DB8-4BD4-91F7-C199DCC7301D} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF} {5DB1F35F-B714-4B62-84BE-16A33084D3E1} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF} @@ -414,7 +416,5 @@ Global {1A1983E6-5297-435F-B467-E8E1F11277D6} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF} {27CDB869-A150-4593-958F-6F26E5391E7C} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF} {EBA72C3B-57D5-43FF-A5B4-3D55B3B6D4C2} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF} - {AA8834A3-82A7-4E83-8E4C-88D37F74056A} = {673F23AE-7694-4BB9-ABD4-136D6C13634E} - {4827D606-89D5-4E00-8341-47A6E95817BA} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF} EndGlobalSection EndGlobal diff --git a/src/Testcontainers.Keycloak/KeycloakBuilder.cs b/src/Testcontainers.Keycloak/KeycloakBuilder.cs index d8677cc14..b4a3f945d 100644 --- a/src/Testcontainers.Keycloak/KeycloakBuilder.cs +++ b/src/Testcontainers.Keycloak/KeycloakBuilder.cs @@ -4,121 +4,124 @@ [PublicAPI] public sealed class KeycloakBuilder : ContainerBuilder { - public const string KeycloakImage = "quay.io/keycloak/keycloak:21.1.1"; - public const ushort KeycloakPort = 8080; - public const string DefaultUsername = "admin"; - public const string DefaultPassword = "admin"; - - /// - /// Initializes a new instance of the class. - /// - public KeycloakBuilder() - : this(new()) - { - DockerResourceConfiguration = Init().DockerResourceConfiguration; - } - - /// - /// Initializes a new instance of the class. - /// - /// The Docker resource configuration. - private KeycloakBuilder(KeycloakConfiguration resourceConfiguration) - : base(resourceConfiguration) - { - DockerResourceConfiguration = resourceConfiguration; - } - - /// - protected override KeycloakConfiguration DockerResourceConfiguration { get; } - - /// - /// Sets the Keycloak admin username. - /// - /// The Keycloak admin username. - /// A configured instance of . - public KeycloakBuilder WithUsername(string username) - { - return Merge(DockerResourceConfiguration, new(username: username)) - .WithEnvironment("KEYCLOAK_ADMIN", username) - .WithWaitStrategy(Wait.ForUnixContainer().UntilPortIsAvailable(KeycloakPort).AddCustomWaitStrategy(new WaitUntil(username))); - } - - /// - /// Sets the Keycloak admin password. - /// - /// The Keycloak admin password. - /// A configured instance of . - public KeycloakBuilder WithPassword(string password) - { - return Merge(DockerResourceConfiguration, new(password: password)) - .WithEnvironment("KEYCLOAK_ADMIN_PASSWORD", password); - } - - /// - public override KeycloakContainer Build() - { - Validate(); - return new(DockerResourceConfiguration, TestcontainersSettings.Logger); - } - - /// - protected override KeycloakBuilder Init() - { - return base.Init() - .WithImage(KeycloakImage) - .WithCommand("start-dev") - .WithPortBinding(KeycloakPort, true) - .WithUsername(DefaultUsername) - .WithPassword(DefaultPassword); - } - - /// - protected override void Validate() - { - base.Validate(); - - _ = Guard.Argument(DockerResourceConfiguration.Username, nameof(DockerResourceConfiguration.Username)) - .NotNull() - .NotEmpty(); - - _ = Guard.Argument(DockerResourceConfiguration.Password, nameof(DockerResourceConfiguration.Password)) - .NotNull() - .NotEmpty(); - } - - /// - protected override KeycloakBuilder Clone(IResourceConfiguration resourceConfiguration) - { - return Merge(DockerResourceConfiguration, new(resourceConfiguration)); - } - - /// - protected override KeycloakBuilder Clone(IContainerConfiguration resourceConfiguration) - { - return Merge(DockerResourceConfiguration, new(resourceConfiguration)); - } - - /// - protected override KeycloakBuilder Merge(KeycloakConfiguration oldValue, KeycloakConfiguration newValue) - { - return new(new(oldValue, newValue)); - } - - private sealed class WaitUntil : IWaitUntil - { - private readonly string _pattern; - - public WaitUntil(string username) - { - _pattern = $"Added user '{username}' to realm 'master'"; - } - - public async Task UntilAsync(IContainer container) - { - var (stdout, _) = await container.GetLogsAsync(timestampsEnabled: false) - .ConfigureAwait(false); - - return stdout.Contains(_pattern); - } - } -} + public const string KeycloakImage = "quay.io/keycloak/keycloak:21.1.1"; + + public const ushort KeycloakPort = 8080; + + public const string DefaultUsername = "admin"; + + public const string DefaultPassword = "admin"; + + /// + /// Initializes a new instance of the class. + /// + public KeycloakBuilder() + : this(new KeycloakConfiguration()) + { + DockerResourceConfiguration = Init().DockerResourceConfiguration; + } + + /// + /// Initializes a new instance of the class. + /// + /// The Docker resource configuration. + private KeycloakBuilder(KeycloakConfiguration resourceConfiguration) + : base(resourceConfiguration) + { + DockerResourceConfiguration = resourceConfiguration; + } + + /// + protected override KeycloakConfiguration DockerResourceConfiguration { get; } + + /// + /// Sets the Keycloak admin username. + /// + /// The Keycloak admin username. + /// A configured instance of . + public KeycloakBuilder WithUsername(string username) + { + return Merge(DockerResourceConfiguration, new KeycloakConfiguration(username: username)) + .WithEnvironment("KEYCLOAK_ADMIN", username) + .WithWaitStrategy(Wait.ForUnixContainer().UntilPortIsAvailable(KeycloakPort).AddCustomWaitStrategy(new WaitUntil(username))); + } + + /// + /// Sets the Keycloak admin password. + /// + /// The Keycloak admin password. + /// A configured instance of . + public KeycloakBuilder WithPassword(string password) + { + return Merge(DockerResourceConfiguration, new KeycloakConfiguration(password: password)) + .WithEnvironment("KEYCLOAK_ADMIN_PASSWORD", password); + } + + /// + public override KeycloakContainer Build() + { + Validate(); + return new KeycloakContainer(DockerResourceConfiguration, TestcontainersSettings.Logger); + } + + /// + protected override KeycloakBuilder Init() + { + return base.Init() + .WithImage(KeycloakImage) + .WithCommand("start-dev") + .WithPortBinding(KeycloakPort, true) + .WithUsername(DefaultUsername) + .WithPassword(DefaultPassword); + } + + /// + protected override void Validate() + { + base.Validate(); + + _ = Guard.Argument(DockerResourceConfiguration.Username, nameof(DockerResourceConfiguration.Username)) + .NotNull() + .NotEmpty(); + + _ = Guard.Argument(DockerResourceConfiguration.Password, nameof(DockerResourceConfiguration.Password)) + .NotNull() + .NotEmpty(); + } + + /// + protected override KeycloakBuilder Clone(IResourceConfiguration resourceConfiguration) + { + return Merge(DockerResourceConfiguration, new KeycloakConfiguration(resourceConfiguration)); + } + + /// + protected override KeycloakBuilder Clone(IContainerConfiguration resourceConfiguration) + { + return Merge(DockerResourceConfiguration, new KeycloakConfiguration(resourceConfiguration)); + } + + /// + protected override KeycloakBuilder Merge(KeycloakConfiguration oldValue, KeycloakConfiguration newValue) + { + return new KeycloakBuilder(new KeycloakConfiguration(oldValue, newValue)); + } + + private sealed class WaitUntil : IWaitUntil + { + private readonly string _pattern; + + public WaitUntil(string username) + { + _pattern = $"Added user '{username}' to realm 'master'"; + } + + public async Task UntilAsync(IContainer container) + { + var (stdout, _) = await container.GetLogsAsync(timestampsEnabled: false) + .ConfigureAwait(false); + + return stdout.Contains(_pattern); + } + } +} \ No newline at end of file diff --git a/src/Testcontainers.Keycloak/KeycloakConfiguration.cs b/src/Testcontainers.Keycloak/KeycloakConfiguration.cs index b9fd43469..a4ebe8dd2 100644 --- a/src/Testcontainers.Keycloak/KeycloakConfiguration.cs +++ b/src/Testcontainers.Keycloak/KeycloakConfiguration.cs @@ -4,66 +4,66 @@ [PublicAPI] public sealed class KeycloakConfiguration : ContainerConfiguration { - /// - /// Initializes a new instance of the class. - /// - /// The admin username. - /// The admin password. - public KeycloakConfiguration(string username = null, string password = null) - { - Username = username; - Password = password; - } + /// + /// Initializes a new instance of the class. + /// + /// The admin username. + /// The admin password. + public KeycloakConfiguration(string username = null, string password = null) + { + Username = username; + Password = password; + } - /// - /// Initializes a new instance of the class. - /// - /// The Docker resource configuration. - public KeycloakConfiguration(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 KeycloakConfiguration(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 KeycloakConfiguration(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 KeycloakConfiguration(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 KeycloakConfiguration(KeycloakConfiguration resourceConfiguration) - : this(new(), 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 KeycloakConfiguration(KeycloakConfiguration resourceConfiguration) + : this(new KeycloakConfiguration(), 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 KeycloakConfiguration(KeycloakConfiguration oldValue, KeycloakConfiguration newValue) - : base(oldValue, newValue) - { - Username = BuildConfiguration.Combine(oldValue.Username, newValue.Username); - Password = BuildConfiguration.Combine(oldValue.Password, newValue.Password); - } + /// + /// Initializes a new instance of the class. + /// + /// The old Docker resource configuration. + /// The new Docker resource configuration. + public KeycloakConfiguration(KeycloakConfiguration oldValue, KeycloakConfiguration newValue) + : base(oldValue, newValue) + { + Username = BuildConfiguration.Combine(oldValue.Username, newValue.Username); + Password = BuildConfiguration.Combine(oldValue.Password, newValue.Password); + } - /// - /// Gets the admin username. - /// - public string Username { get; } = null!; + /// + /// Gets the admin username. + /// + public string Username { get; } = null!; - /// - /// Gets the admin password. - /// - public string Password { get; } = null!; -} + /// + /// Gets the admin password. + /// + public string Password { get; } = null!; +} \ No newline at end of file diff --git a/src/Testcontainers.Keycloak/KeycloakContainer.cs b/src/Testcontainers.Keycloak/KeycloakContainer.cs index ddc512d6e..13c976dab 100644 --- a/src/Testcontainers.Keycloak/KeycloakContainer.cs +++ b/src/Testcontainers.Keycloak/KeycloakContainer.cs @@ -4,30 +4,25 @@ [PublicAPI] public sealed class KeycloakContainer : DockerContainer { - private readonly KeycloakConfiguration _configuration; + private readonly KeycloakConfiguration _configuration; - /// - /// Initializes a new instance of the class. - /// - /// The container configuration. - /// The logger. - public KeycloakContainer(KeycloakConfiguration configuration, ILogger logger) - : base(configuration, logger) - { - _configuration = configuration; - } + /// + /// Initializes a new instance of the class. + /// + /// The container configuration. + /// The logger. + public KeycloakContainer(KeycloakConfiguration configuration, ILogger logger) + : base(configuration, logger) + { + _configuration = configuration; + } - /// - /// Gets the base address for this Keycloak instance. - /// - /// The base address for this Keycloak instance - public Uri GetBaseAddress() - { - return new UriBuilder - { - Scheme = "http", - Host = Hostname, - Port = GetMappedPublicPort(KeycloakBuilder.KeycloakPort) - }.Uri; - } -} + /// + /// Gets the Keycloak base address. + /// + /// The Keycloak base address. + public string GetBaseAddress() + { + return new UriBuilder(Uri.UriSchemeHttp, Hostname, GetMappedPublicPort(KeycloakBuilder.KeycloakPort)).ToString(); + } +} \ No newline at end of file diff --git a/src/Testcontainers.Keycloak/Testcontainers.Keycloak.csproj b/src/Testcontainers.Keycloak/Testcontainers.Keycloak.csproj index 9e7774559..4c05d521f 100644 --- a/src/Testcontainers.Keycloak/Testcontainers.Keycloak.csproj +++ b/src/Testcontainers.Keycloak/Testcontainers.Keycloak.csproj @@ -1,13 +1,13 @@ - - netstandard2.0;netstandard2.1 - latest - - - - - - - - + + netstandard2.0;netstandard2.1 + latest + + + + + + + + \ No newline at end of file diff --git a/src/Testcontainers.Keycloak/Usings.cs b/src/Testcontainers.Keycloak/Usings.cs index 5661931d0..4ec1e3a96 100644 --- a/src/Testcontainers.Keycloak/Usings.cs +++ b/src/Testcontainers.Keycloak/Usings.cs @@ -6,4 +6,4 @@ global using DotNet.Testcontainers.Configurations; global using DotNet.Testcontainers.Containers; global using JetBrains.Annotations; -global using Microsoft.Extensions.Logging; +global using Microsoft.Extensions.Logging; \ No newline at end of file diff --git a/tests/Testcontainers.Keycloak.Tests/KeycloakContainerTest.cs b/tests/Testcontainers.Keycloak.Tests/KeycloakContainerTest.cs index 8d1d99215..a194181ed 100644 --- a/tests/Testcontainers.Keycloak.Tests/KeycloakContainerTest.cs +++ b/tests/Testcontainers.Keycloak.Tests/KeycloakContainerTest.cs @@ -2,42 +2,44 @@ namespace Testcontainers.Keycloak.Tests; public sealed class KeycloakContainerTest : IAsyncLifetime { - private readonly KeycloakContainer _keycloakContainer = new KeycloakBuilder().Build(); - - public Task InitializeAsync() - { - return _keycloakContainer.StartAsync(); - } - - public Task DisposeAsync() - { - return _keycloakContainer.DisposeAsync().AsTask(); - } - - [Fact] - public async Task GetBaseAddressReturnsValidAddress() - { - // Given - using var httpClient = new HttpClient { BaseAddress = _keycloakContainer.GetBaseAddress() }; - - // When - using var response = await httpClient.GetAsync("/realms/master/.well-known/openid-configuration"); - - // Then - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - } - - [Fact] - public async Task MasterRealmIsCreatedAndEnabled() - { - // Given - var baseAddress = _keycloakContainer.GetBaseAddress().AbsoluteUri; - var keycloakClient = new KeycloakClient(baseAddress, KeycloakBuilder.DefaultUsername, KeycloakBuilder.DefaultPassword); - - // When - var masterRealm = await keycloakClient.GetRealmAsync("master"); - - // Then - Assert.True(masterRealm.Enabled); - } -} + private readonly KeycloakContainer _keycloakContainer = new KeycloakBuilder().Build(); + + public Task InitializeAsync() + { + return _keycloakContainer.StartAsync(); + } + + public Task DisposeAsync() + { + return _keycloakContainer.DisposeAsync().AsTask(); + } + + [Fact] + public async Task GetOpenIdEndpointReturnsHttpStatusCodeOk() + { + // Given + using var httpClient = new HttpClient(); + httpClient.BaseAddress = new Uri(_keycloakContainer.GetBaseAddress()); + + // When + using var response = await httpClient.GetAsync("/realms/master/.well-known/openid-configuration") + .ConfigureAwait(false); + + // Then + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + } + + [Fact] + public async Task MasterRealmIsEnabled() + { + // Given + var keycloakClient = new KeycloakClient(_keycloakContainer.GetBaseAddress(), KeycloakBuilder.DefaultUsername, KeycloakBuilder.DefaultPassword); + + // When + var masterRealm = await keycloakClient.GetRealmAsync("master") + .ConfigureAwait(false); + + // Then + Assert.True(masterRealm.Enabled); + } +} \ No newline at end of file diff --git a/tests/Testcontainers.Keycloak.Tests/Testcontainers.Keycloak.Tests.csproj b/tests/Testcontainers.Keycloak.Tests/Testcontainers.Keycloak.Tests.csproj index 547d15bea..5c5c551e8 100644 --- a/tests/Testcontainers.Keycloak.Tests/Testcontainers.Keycloak.Tests.csproj +++ b/tests/Testcontainers.Keycloak.Tests/Testcontainers.Keycloak.Tests.csproj @@ -1,19 +1,18 @@ - - net6.0 - false - false - - - - - - - - - - - - - - + + net6.0 + false + false + + + + + + + + + + + + + \ No newline at end of file diff --git a/tests/Testcontainers.Keycloak.Tests/Usings.cs b/tests/Testcontainers.Keycloak.Tests/Usings.cs index 02f69975b..66a86e46d 100644 --- a/tests/Testcontainers.Keycloak.Tests/Usings.cs +++ b/tests/Testcontainers.Keycloak.Tests/Usings.cs @@ -1,5 +1,6 @@ +global using System; global using System.Net; global using System.Net.Http; global using System.Threading.Tasks; global using Keycloak.Net; -global using Xunit; +global using Xunit; \ No newline at end of file From d3c706ef1a6d4b448b4bcb0e0592b403e3c0b6d8 Mon Sep 17 00:00:00 2001 From: Andre Hofmeister <9199345+HofmeisterAn@users.noreply.github.com> Date: Mon, 8 May 2023 11:49:18 +0200 Subject: [PATCH 3/6] chore: Replace wait strategy with health check --- .../KeycloakBuilder.cs | 26 ++++--------------- 1 file changed, 5 insertions(+), 21 deletions(-) diff --git a/src/Testcontainers.Keycloak/KeycloakBuilder.cs b/src/Testcontainers.Keycloak/KeycloakBuilder.cs index b4a3f945d..5447c7162 100644 --- a/src/Testcontainers.Keycloak/KeycloakBuilder.cs +++ b/src/Testcontainers.Keycloak/KeycloakBuilder.cs @@ -42,8 +42,7 @@ private KeycloakBuilder(KeycloakConfiguration resourceConfiguration) public KeycloakBuilder WithUsername(string username) { return Merge(DockerResourceConfiguration, new KeycloakConfiguration(username: username)) - .WithEnvironment("KEYCLOAK_ADMIN", username) - .WithWaitStrategy(Wait.ForUnixContainer().UntilPortIsAvailable(KeycloakPort).AddCustomWaitStrategy(new WaitUntil(username))); + .WithEnvironment("KEYCLOAK_ADMIN", username); } /// @@ -70,9 +69,12 @@ protected override KeycloakBuilder Init() return base.Init() .WithImage(KeycloakImage) .WithCommand("start-dev") + .WithCommand("--health-enabled", "true") .WithPortBinding(KeycloakPort, true) .WithUsername(DefaultUsername) - .WithPassword(DefaultPassword); + .WithPassword(DefaultPassword) + .WithWaitStrategy(Wait.ForUnixContainer().UntilHttpRequestIsSucceeded(request => + request.ForPath("/health/ready").ForPort(KeycloakPort))); } /// @@ -106,22 +108,4 @@ protected override KeycloakBuilder Merge(KeycloakConfiguration oldValue, Keycloa { return new KeycloakBuilder(new KeycloakConfiguration(oldValue, newValue)); } - - private sealed class WaitUntil : IWaitUntil - { - private readonly string _pattern; - - public WaitUntil(string username) - { - _pattern = $"Added user '{username}' to realm 'master'"; - } - - public async Task UntilAsync(IContainer container) - { - var (stdout, _) = await container.GetLogsAsync(timestampsEnabled: false) - .ConfigureAwait(false); - - return stdout.Contains(_pattern); - } - } } \ No newline at end of file From 79a653fe76652019df12909319be846da6ec3b91 Mon Sep 17 00:00:00 2001 From: Valters Melnalksnis Date: Thu, 4 May 2023 08:26:56 +0300 Subject: [PATCH 4/6] feat: Add Keycloak module --- Testcontainers.sln | 14 +++ src/Testcontainers.Keycloak/.editorconfig | 1 + .../KeycloakBuilder.cs | 108 ++++++++++++++++++ .../KeycloakConfiguration.cs | 69 +++++++++++ .../KeycloakContainer.cs | 33 ++++++ .../Testcontainers.Keycloak.csproj | 13 +++ src/Testcontainers.Keycloak/Usings.cs | 9 ++ .../.editorconfig | 1 + .../KeycloakContainerTest.cs | 43 +++++++ .../Testcontainers.Keycloak.Tests.csproj | 19 +++ tests/Testcontainers.Keycloak.Tests/Usings.cs | 5 + 11 files changed, 315 insertions(+) create mode 100644 src/Testcontainers.Keycloak/.editorconfig create mode 100644 src/Testcontainers.Keycloak/KeycloakBuilder.cs create mode 100644 src/Testcontainers.Keycloak/KeycloakConfiguration.cs create mode 100644 src/Testcontainers.Keycloak/KeycloakContainer.cs create mode 100644 src/Testcontainers.Keycloak/Testcontainers.Keycloak.csproj create mode 100644 src/Testcontainers.Keycloak/Usings.cs create mode 100644 tests/Testcontainers.Keycloak.Tests/.editorconfig create mode 100644 tests/Testcontainers.Keycloak.Tests/KeycloakContainerTest.cs create mode 100644 tests/Testcontainers.Keycloak.Tests/Testcontainers.Keycloak.Tests.csproj create mode 100644 tests/Testcontainers.Keycloak.Tests/Usings.cs diff --git a/Testcontainers.sln b/Testcontainers.sln index a86fd5cb6..f7c31cf85 100644 --- a/Testcontainers.sln +++ b/Testcontainers.sln @@ -121,6 +121,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.Tests", "tes EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.WebDriver.Tests", "tests\Testcontainers.WebDriver.Tests\Testcontainers.WebDriver.Tests.csproj", "{EBA72C3B-57D5-43FF-A5B4-3D55B3B6D4C2}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.Keycloak", "src\Testcontainers.Keycloak\Testcontainers.Keycloak.csproj", "{AA8834A3-82A7-4E83-8E4C-88D37F74056A}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.Keycloak.Tests", "tests\Testcontainers.Keycloak.Tests\Testcontainers.Keycloak.Tests.csproj", "{4827D606-89D5-4E00-8341-47A6E95817BA}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -346,6 +350,14 @@ 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 + {AA8834A3-82A7-4E83-8E4C-88D37F74056A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AA8834A3-82A7-4E83-8E4C-88D37F74056A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AA8834A3-82A7-4E83-8E4C-88D37F74056A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AA8834A3-82A7-4E83-8E4C-88D37F74056A}.Release|Any CPU.Build.0 = Release|Any CPU + {4827D606-89D5-4E00-8341-47A6E95817BA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4827D606-89D5-4E00-8341-47A6E95817BA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4827D606-89D5-4E00-8341-47A6E95817BA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4827D606-89D5-4E00-8341-47A6E95817BA}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution {3F2E254F-C203-43FD-A078-DC3E2CBC0F9F} = {673F23AE-7694-4BB9-ABD4-136D6C13634E} @@ -402,5 +414,7 @@ Global {1A1983E6-5297-435F-B467-E8E1F11277D6} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF} {27CDB869-A150-4593-958F-6F26E5391E7C} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF} {EBA72C3B-57D5-43FF-A5B4-3D55B3B6D4C2} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF} + {AA8834A3-82A7-4E83-8E4C-88D37F74056A} = {673F23AE-7694-4BB9-ABD4-136D6C13634E} + {4827D606-89D5-4E00-8341-47A6E95817BA} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF} EndGlobalSection EndGlobal diff --git a/src/Testcontainers.Keycloak/.editorconfig b/src/Testcontainers.Keycloak/.editorconfig new file mode 100644 index 000000000..6f066619d --- /dev/null +++ b/src/Testcontainers.Keycloak/.editorconfig @@ -0,0 +1 @@ +root = true \ No newline at end of file diff --git a/src/Testcontainers.Keycloak/KeycloakBuilder.cs b/src/Testcontainers.Keycloak/KeycloakBuilder.cs new file mode 100644 index 000000000..6e6801732 --- /dev/null +++ b/src/Testcontainers.Keycloak/KeycloakBuilder.cs @@ -0,0 +1,108 @@ +namespace Testcontainers.Keycloak; + +/// +[PublicAPI] +public sealed class KeycloakBuilder : ContainerBuilder +{ + public const string KeycloakImage = "quay.io/keycloak/keycloak:21.1.1"; + public const ushort KeycloakPort = 8080; + public const string DefaultUsername = "admin"; + public const string DefaultPassword = "admin"; + + /// + /// Initializes a new instance of the class. + /// + public KeycloakBuilder() + : this(new()) + { + DockerResourceConfiguration = Init().DockerResourceConfiguration; + } + + /// + /// Initializes a new instance of the class. + /// + /// The Docker resource configuration. + private KeycloakBuilder(KeycloakConfiguration resourceConfiguration) + : base(resourceConfiguration) + { + DockerResourceConfiguration = resourceConfiguration; + } + + /// + protected override KeycloakConfiguration DockerResourceConfiguration { get; } + + /// + /// Sets the Keycloak admin username. + /// + /// The Keycloak admin username. + /// A configured instance of . + public KeycloakBuilder WithUsername(string username) + { + return Merge(DockerResourceConfiguration, new(username: username)) + .WithEnvironment("KEYCLOAK_ADMIN", username); + } + + /// + /// Sets the Keycloak admin password. + /// + /// The Keycloak admin password. + /// A configured instance of . + public KeycloakBuilder WithPassword(string password) + { + return Merge(DockerResourceConfiguration, new(password: password)) + .WithEnvironment("KEYCLOAK_ADMIN_PASSWORD", password); + } + + /// + public override KeycloakContainer Build() + { + Validate(); + return new(DockerResourceConfiguration, TestcontainersSettings.Logger); + } + + /// + protected override KeycloakBuilder Init() + { + return base.Init() + .WithImage(KeycloakImage) + .WithCommand("start-dev") + .WithPortBinding(KeycloakPort, true) + .WithUsername(DefaultUsername) + .WithPassword(DefaultPassword) + .WithEnvironment("KC_HEALTH_ENABLED", "true") + .WithWaitStrategy(Wait.ForUnixContainer().UntilHttpRequestIsSucceeded(request => + request.ForPath("/health/ready").ForPort(KeycloakPort))); + } + + /// + protected override void Validate() + { + base.Validate(); + + _ = Guard.Argument(DockerResourceConfiguration.Username, nameof(DockerResourceConfiguration.Username)) + .NotNull() + .NotEmpty(); + + _ = Guard.Argument(DockerResourceConfiguration.Password, nameof(DockerResourceConfiguration.Password)) + .NotNull() + .NotEmpty(); + } + + /// + protected override KeycloakBuilder Clone(IResourceConfiguration resourceConfiguration) + { + return Merge(DockerResourceConfiguration, new(resourceConfiguration)); + } + + /// + protected override KeycloakBuilder Clone(IContainerConfiguration resourceConfiguration) + { + return Merge(DockerResourceConfiguration, new(resourceConfiguration)); + } + + /// + protected override KeycloakBuilder Merge(KeycloakConfiguration oldValue, KeycloakConfiguration newValue) + { + return new(new(oldValue, newValue)); + } +} diff --git a/src/Testcontainers.Keycloak/KeycloakConfiguration.cs b/src/Testcontainers.Keycloak/KeycloakConfiguration.cs new file mode 100644 index 000000000..b9fd43469 --- /dev/null +++ b/src/Testcontainers.Keycloak/KeycloakConfiguration.cs @@ -0,0 +1,69 @@ +namespace Testcontainers.Keycloak; + +/// +[PublicAPI] +public sealed class KeycloakConfiguration : ContainerConfiguration +{ + /// + /// Initializes a new instance of the class. + /// + /// The admin username. + /// The admin password. + public KeycloakConfiguration(string username = null, string password = null) + { + Username = username; + Password = password; + } + + /// + /// Initializes a new instance of the class. + /// + /// The Docker resource configuration. + public KeycloakConfiguration(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 KeycloakConfiguration(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 KeycloakConfiguration(KeycloakConfiguration resourceConfiguration) + : this(new(), 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 KeycloakConfiguration(KeycloakConfiguration oldValue, KeycloakConfiguration newValue) + : base(oldValue, newValue) + { + Username = BuildConfiguration.Combine(oldValue.Username, newValue.Username); + Password = BuildConfiguration.Combine(oldValue.Password, newValue.Password); + } + + /// + /// Gets the admin username. + /// + public string Username { get; } = null!; + + /// + /// Gets the admin password. + /// + public string Password { get; } = null!; +} diff --git a/src/Testcontainers.Keycloak/KeycloakContainer.cs b/src/Testcontainers.Keycloak/KeycloakContainer.cs new file mode 100644 index 000000000..ddc512d6e --- /dev/null +++ b/src/Testcontainers.Keycloak/KeycloakContainer.cs @@ -0,0 +1,33 @@ +namespace Testcontainers.Keycloak; + +/// +[PublicAPI] +public sealed class KeycloakContainer : DockerContainer +{ + private readonly KeycloakConfiguration _configuration; + + /// + /// Initializes a new instance of the class. + /// + /// The container configuration. + /// The logger. + public KeycloakContainer(KeycloakConfiguration configuration, ILogger logger) + : base(configuration, logger) + { + _configuration = configuration; + } + + /// + /// Gets the base address for this Keycloak instance. + /// + /// The base address for this Keycloak instance + public Uri GetBaseAddress() + { + return new UriBuilder + { + Scheme = "http", + Host = Hostname, + Port = GetMappedPublicPort(KeycloakBuilder.KeycloakPort) + }.Uri; + } +} diff --git a/src/Testcontainers.Keycloak/Testcontainers.Keycloak.csproj b/src/Testcontainers.Keycloak/Testcontainers.Keycloak.csproj new file mode 100644 index 000000000..9e7774559 --- /dev/null +++ b/src/Testcontainers.Keycloak/Testcontainers.Keycloak.csproj @@ -0,0 +1,13 @@ + + + netstandard2.0;netstandard2.1 + latest + + + + + + + + + \ No newline at end of file diff --git a/src/Testcontainers.Keycloak/Usings.cs b/src/Testcontainers.Keycloak/Usings.cs new file mode 100644 index 000000000..5661931d0 --- /dev/null +++ b/src/Testcontainers.Keycloak/Usings.cs @@ -0,0 +1,9 @@ +global using System; +global using System.Threading.Tasks; +global using Docker.DotNet.Models; +global using DotNet.Testcontainers; +global using DotNet.Testcontainers.Builders; +global using DotNet.Testcontainers.Configurations; +global using DotNet.Testcontainers.Containers; +global using JetBrains.Annotations; +global using Microsoft.Extensions.Logging; diff --git a/tests/Testcontainers.Keycloak.Tests/.editorconfig b/tests/Testcontainers.Keycloak.Tests/.editorconfig new file mode 100644 index 000000000..6f066619d --- /dev/null +++ b/tests/Testcontainers.Keycloak.Tests/.editorconfig @@ -0,0 +1 @@ +root = true \ No newline at end of file diff --git a/tests/Testcontainers.Keycloak.Tests/KeycloakContainerTest.cs b/tests/Testcontainers.Keycloak.Tests/KeycloakContainerTest.cs new file mode 100644 index 000000000..8d1d99215 --- /dev/null +++ b/tests/Testcontainers.Keycloak.Tests/KeycloakContainerTest.cs @@ -0,0 +1,43 @@ +namespace Testcontainers.Keycloak.Tests; + +public sealed class KeycloakContainerTest : IAsyncLifetime +{ + private readonly KeycloakContainer _keycloakContainer = new KeycloakBuilder().Build(); + + public Task InitializeAsync() + { + return _keycloakContainer.StartAsync(); + } + + public Task DisposeAsync() + { + return _keycloakContainer.DisposeAsync().AsTask(); + } + + [Fact] + public async Task GetBaseAddressReturnsValidAddress() + { + // Given + using var httpClient = new HttpClient { BaseAddress = _keycloakContainer.GetBaseAddress() }; + + // When + using var response = await httpClient.GetAsync("/realms/master/.well-known/openid-configuration"); + + // Then + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + } + + [Fact] + public async Task MasterRealmIsCreatedAndEnabled() + { + // Given + var baseAddress = _keycloakContainer.GetBaseAddress().AbsoluteUri; + var keycloakClient = new KeycloakClient(baseAddress, KeycloakBuilder.DefaultUsername, KeycloakBuilder.DefaultPassword); + + // When + var masterRealm = await keycloakClient.GetRealmAsync("master"); + + // Then + Assert.True(masterRealm.Enabled); + } +} diff --git a/tests/Testcontainers.Keycloak.Tests/Testcontainers.Keycloak.Tests.csproj b/tests/Testcontainers.Keycloak.Tests/Testcontainers.Keycloak.Tests.csproj new file mode 100644 index 000000000..547d15bea --- /dev/null +++ b/tests/Testcontainers.Keycloak.Tests/Testcontainers.Keycloak.Tests.csproj @@ -0,0 +1,19 @@ + + + net6.0 + false + false + + + + + + + + + + + + + + diff --git a/tests/Testcontainers.Keycloak.Tests/Usings.cs b/tests/Testcontainers.Keycloak.Tests/Usings.cs new file mode 100644 index 000000000..02f69975b --- /dev/null +++ b/tests/Testcontainers.Keycloak.Tests/Usings.cs @@ -0,0 +1,5 @@ +global using System.Net; +global using System.Net.Http; +global using System.Threading.Tasks; +global using Keycloak.Net; +global using Xunit; From db8ecf3f11bf7cda1a323e0bb60de7286cb34dfa Mon Sep 17 00:00:00 2001 From: Andre Hofmeister <9199345+HofmeisterAn@users.noreply.github.com> Date: Fri, 12 May 2023 10:06:28 +0200 Subject: [PATCH 5/6] chore: Remove BOM --- src/Testcontainers.Keycloak/KeycloakBuilder.cs | 2 +- src/Testcontainers.Keycloak/KeycloakConfiguration.cs | 2 +- src/Testcontainers.Keycloak/KeycloakContainer.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Testcontainers.Keycloak/KeycloakBuilder.cs b/src/Testcontainers.Keycloak/KeycloakBuilder.cs index 790490d92..7972427a0 100644 --- a/src/Testcontainers.Keycloak/KeycloakBuilder.cs +++ b/src/Testcontainers.Keycloak/KeycloakBuilder.cs @@ -1,4 +1,4 @@ -namespace Testcontainers.Keycloak; +namespace Testcontainers.Keycloak; /// [PublicAPI] diff --git a/src/Testcontainers.Keycloak/KeycloakConfiguration.cs b/src/Testcontainers.Keycloak/KeycloakConfiguration.cs index a4ebe8dd2..b1b2937e8 100644 --- a/src/Testcontainers.Keycloak/KeycloakConfiguration.cs +++ b/src/Testcontainers.Keycloak/KeycloakConfiguration.cs @@ -1,4 +1,4 @@ -namespace Testcontainers.Keycloak; +namespace Testcontainers.Keycloak; /// [PublicAPI] diff --git a/src/Testcontainers.Keycloak/KeycloakContainer.cs b/src/Testcontainers.Keycloak/KeycloakContainer.cs index 13c976dab..b2a9b93fb 100644 --- a/src/Testcontainers.Keycloak/KeycloakContainer.cs +++ b/src/Testcontainers.Keycloak/KeycloakContainer.cs @@ -1,4 +1,4 @@ -namespace Testcontainers.Keycloak; +namespace Testcontainers.Keycloak; /// [PublicAPI] From 859888b688796f01c5157edb51dfd93e04628ad0 Mon Sep 17 00:00:00 2001 From: Andre Hofmeister <9199345+HofmeisterAn@users.noreply.github.com> Date: Fri, 12 May 2023 10:33:47 +0200 Subject: [PATCH 6/6] chore: Pin minor version --- src/Testcontainers.Keycloak/KeycloakBuilder.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Testcontainers.Keycloak/KeycloakBuilder.cs b/src/Testcontainers.Keycloak/KeycloakBuilder.cs index 7972427a0..933cbae0a 100644 --- a/src/Testcontainers.Keycloak/KeycloakBuilder.cs +++ b/src/Testcontainers.Keycloak/KeycloakBuilder.cs @@ -4,7 +4,7 @@ namespace Testcontainers.Keycloak; [PublicAPI] public sealed class KeycloakBuilder : ContainerBuilder { - public const string KeycloakImage = "quay.io/keycloak/keycloak:21.1.1"; + public const string KeycloakImage = "quay.io/keycloak/keycloak:21.1"; public const ushort KeycloakPort = 8080;