Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

feat: Add Keycloak module #880

Merged
merged 8 commits into from
May 12, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions Testcontainers.sln
Original file line number Diff line number Diff line change
Expand Up @@ -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}"
Expand Down Expand Up @@ -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}"
Expand Down Expand Up @@ -166,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
Expand Down Expand Up @@ -270,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
Expand Down Expand Up @@ -357,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}
Expand All @@ -383,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}
Expand Down
1 change: 1 addition & 0 deletions src/Testcontainers.Keycloak/.editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
root = true
111 changes: 111 additions & 0 deletions src/Testcontainers.Keycloak/KeycloakBuilder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
namespace Testcontainers.Keycloak;

/// <inheritdoc />
[PublicAPI]
public sealed class KeycloakBuilder : ContainerBuilder<KeycloakBuilder, KeycloakContainer, KeycloakConfiguration>
{
public const string KeycloakImage = "quay.io/keycloak/keycloak:21.1";

public const ushort KeycloakPort = 8080;

public const string DefaultUsername = "admin";

public const string DefaultPassword = "admin";

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

/// <summary>
/// Initializes a new instance of the <see cref="KeycloakBuilder" /> class.
/// </summary>
/// <param name="resourceConfiguration">The Docker resource configuration.</param>
private KeycloakBuilder(KeycloakConfiguration resourceConfiguration)
: base(resourceConfiguration)
{
DockerResourceConfiguration = resourceConfiguration;
}

/// <inheritdoc />
protected override KeycloakConfiguration DockerResourceConfiguration { get; }

/// <summary>
/// Sets the Keycloak admin username.
/// </summary>
/// <param name="username">The Keycloak admin username.</param>
/// <returns>A configured instance of <see cref="KeycloakBuilder" />.</returns>
public KeycloakBuilder WithUsername(string username)
{
return Merge(DockerResourceConfiguration, new KeycloakConfiguration(username: username))
.WithEnvironment("KEYCLOAK_ADMIN", username);
}

/// <summary>
/// Sets the Keycloak admin password.
/// </summary>
/// <param name="password">The Keycloak admin password.</param>
/// <returns>A configured instance of <see cref="KeycloakBuilder" />.</returns>
public KeycloakBuilder WithPassword(string password)
{
return Merge(DockerResourceConfiguration, new KeycloakConfiguration(password: password))
.WithEnvironment("KEYCLOAK_ADMIN_PASSWORD", password);
}

/// <inheritdoc />
public override KeycloakContainer Build()
{
Validate();
return new KeycloakContainer(DockerResourceConfiguration, TestcontainersSettings.Logger);
}

/// <inheritdoc />
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)));
}

/// <inheritdoc />
protected override void Validate()
{
base.Validate();

_ = Guard.Argument(DockerResourceConfiguration.Username, nameof(DockerResourceConfiguration.Username))
.NotNull()
.NotEmpty();

_ = Guard.Argument(DockerResourceConfiguration.Password, nameof(DockerResourceConfiguration.Password))
.NotNull()
.NotEmpty();
}

/// <inheritdoc />
protected override KeycloakBuilder Clone(IResourceConfiguration<CreateContainerParameters> resourceConfiguration)
{
return Merge(DockerResourceConfiguration, new KeycloakConfiguration(resourceConfiguration));
}

/// <inheritdoc />
protected override KeycloakBuilder Clone(IContainerConfiguration resourceConfiguration)
{
return Merge(DockerResourceConfiguration, new KeycloakConfiguration(resourceConfiguration));
}

/// <inheritdoc />
protected override KeycloakBuilder Merge(KeycloakConfiguration oldValue, KeycloakConfiguration newValue)
{
return new KeycloakBuilder(new KeycloakConfiguration(oldValue, newValue));
}
}
69 changes: 69 additions & 0 deletions src/Testcontainers.Keycloak/KeycloakConfiguration.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
namespace Testcontainers.Keycloak;

/// <inheritdoc />
[PublicAPI]
public sealed class KeycloakConfiguration : ContainerConfiguration
{
/// <summary>
/// Initializes a new instance of the <see cref="KeycloakConfiguration" /> class.
/// </summary>
/// <param name="username">The admin username.</param>
/// <param name="password">The admin password.</param>
public KeycloakConfiguration(string username = null, string password = null)
{
Username = username;
Password = password;
}

/// <summary>
/// Initializes a new instance of the <see cref="KeycloakConfiguration" /> class.
/// </summary>
/// <param name="resourceConfiguration">The Docker resource configuration.</param>
public KeycloakConfiguration(IResourceConfiguration<CreateContainerParameters> resourceConfiguration)
: base(resourceConfiguration)
{
// Passes the configuration upwards to the base implementations to create an updated immutable copy.
}

/// <summary>
/// Initializes a new instance of the <see cref="KeycloakConfiguration" /> class.
/// </summary>
/// <param name="resourceConfiguration">The Docker resource configuration.</param>
public KeycloakConfiguration(IContainerConfiguration resourceConfiguration)
: base(resourceConfiguration)
{
// Passes the configuration upwards to the base implementations to create an updated immutable copy.
}

/// <summary>
/// Initializes a new instance of the <see cref="KeycloakConfiguration" /> class.
/// </summary>
/// <param name="resourceConfiguration">The Docker resource configuration.</param>
public KeycloakConfiguration(KeycloakConfiguration resourceConfiguration)
: this(new KeycloakConfiguration(), resourceConfiguration)
{
// Passes the configuration upwards to the base implementations to create an updated immutable copy.
}

/// <summary>
/// Initializes a new instance of the <see cref="KeycloakConfiguration" /> class.
/// </summary>
/// <param name="oldValue">The old Docker resource configuration.</param>
/// <param name="newValue">The new Docker resource configuration.</param>
public KeycloakConfiguration(KeycloakConfiguration oldValue, KeycloakConfiguration newValue)
: base(oldValue, newValue)
{
Username = BuildConfiguration.Combine(oldValue.Username, newValue.Username);
Password = BuildConfiguration.Combine(oldValue.Password, newValue.Password);
}

/// <summary>
/// Gets the admin username.
/// </summary>
public string Username { get; } = null!;

/// <summary>
/// Gets the admin password.
/// </summary>
public string Password { get; } = null!;
}
28 changes: 28 additions & 0 deletions src/Testcontainers.Keycloak/KeycloakContainer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
namespace Testcontainers.Keycloak;

/// <inheritdoc />
[PublicAPI]
public sealed class KeycloakContainer : DockerContainer
{
private readonly KeycloakConfiguration _configuration;

/// <summary>
/// Initializes a new instance of the <see cref="KeycloakContainer" /> class.
/// </summary>
/// <param name="configuration">The container configuration.</param>
/// <param name="logger">The logger.</param>
public KeycloakContainer(KeycloakConfiguration configuration, ILogger logger)
: base(configuration, logger)
{
_configuration = configuration;
}

/// <summary>
/// Gets the Keycloak base address.
/// </summary>
/// <returns>The Keycloak base address.</returns>
public string GetBaseAddress()
{
return new UriBuilder(Uri.UriSchemeHttp, Hostname, GetMappedPublicPort(KeycloakBuilder.KeycloakPort)).ToString();
}
}
13 changes: 13 additions & 0 deletions src/Testcontainers.Keycloak/Testcontainers.Keycloak.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>netstandard2.0;netstandard2.1</TargetFrameworks>
<LangVersion>latest</LangVersion>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.1" PrivateAssets="All"/>
<PackageReference Include="JetBrains.Annotations" Version="2022.3.1" PrivateAssets="All"/>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="$(SolutionDir)src/Testcontainers/Testcontainers.csproj"/>
</ItemGroup>
</Project>
9 changes: 9 additions & 0 deletions src/Testcontainers.Keycloak/Usings.cs
Original file line number Diff line number Diff line change
@@ -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;
1 change: 1 addition & 0 deletions tests/Testcontainers.Keycloak.Tests/.editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
root = true
45 changes: 45 additions & 0 deletions tests/Testcontainers.Keycloak.Tests/KeycloakContainerTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
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 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);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net6.0</TargetFrameworks>
<IsPackable>false</IsPackable>
<IsPublishable>false</IsPublishable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.4.1"/>
<PackageReference Include="coverlet.collector" Version="3.2.0"/>
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5"/>
<PackageReference Include="xunit" Version="2.4.2"/>
<PackageReference Include="Keycloak.Net.Core" Version="1.0.20"/>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="$(SolutionDir)src/Testcontainers.Keycloak/Testcontainers.Keycloak.csproj"/>
<ProjectReference Include="$(SolutionDir)tests/Testcontainers.Commons/Testcontainers.Commons.csproj"/>
</ItemGroup>
</Project>
6 changes: 6 additions & 0 deletions tests/Testcontainers.Keycloak.Tests/Usings.cs
Original file line number Diff line number Diff line change
@@ -0,0 +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;