diff --git a/src/Testcontainers.Keycloak/KeycloakBuilder.cs b/src/Testcontainers.Keycloak/KeycloakBuilder.cs index e61810f01..1f255cd3a 100644 --- a/src/Testcontainers.Keycloak/KeycloakBuilder.cs +++ b/src/Testcontainers.Keycloak/KeycloakBuilder.cs @@ -8,6 +8,8 @@ public sealed class KeycloakBuilder : ContainerBuilder predicate = v => v.Major >= 25; + + var image = DockerResourceConfiguration.Image; + + // https://www.keycloak.org/docs/latest/release_notes/index.html#management-port-for-metrics-and-health-endpoints. + var isMajorVersionGreaterOrEqual25 = image.MatchLatestOrNightly() || image.MatchVersion(predicate); + + var waitStrategy = Wait.ForUnixContainer() + .UntilHttpRequestIsSucceeded(request => + request.ForPath("/health/ready").ForPort(isMajorVersionGreaterOrEqual25 ? KeycloakHealthPort : KeycloakPort)); + + var keycloakBuilder = DockerResourceConfiguration.WaitStrategies.Count() > 1 ? this : WithWaitStrategy(waitStrategy); + return new KeycloakContainer(keycloakBuilder.DockerResourceConfiguration); } /// @@ -70,11 +85,10 @@ protected override KeycloakBuilder Init() .WithImage(KeycloakImage) .WithCommand("start-dev") .WithPortBinding(KeycloakPort, true) + .WithPortBinding(KeycloakHealthPort, true) .WithUsername(DefaultUsername) .WithPassword(DefaultPassword) - .WithEnvironment("KC_HEALTH_ENABLED", "true") - .WithWaitStrategy(Wait.ForUnixContainer().UntilHttpRequestIsSucceeded(request => - request.ForPath("/health/ready").ForPort(KeycloakPort))); + .WithEnvironment("KC_HEALTH_ENABLED", "true"); } /// diff --git a/src/Testcontainers.Keycloak/Usings.cs b/src/Testcontainers.Keycloak/Usings.cs index 8e5c20fd5..f82c6873d 100644 --- a/src/Testcontainers.Keycloak/Usings.cs +++ b/src/Testcontainers.Keycloak/Usings.cs @@ -1,4 +1,5 @@ global using System; +global using System.Linq; global using Docker.DotNet.Models; global using DotNet.Testcontainers; global using DotNet.Testcontainers.Builders; diff --git a/src/Testcontainers/Images/DockerImage.cs b/src/Testcontainers/Images/DockerImage.cs index b37ae0c4a..468d292ee 100644 --- a/src/Testcontainers/Images/DockerImage.cs +++ b/src/Testcontainers/Images/DockerImage.cs @@ -2,6 +2,7 @@ namespace DotNet.Testcontainers.Images { using System; using System.Linq; + using System.Text.RegularExpressions; using JetBrains.Annotations; /// @@ -10,6 +11,8 @@ public sealed class DockerImage : IImage { private const string LatestTag = "latest"; + private const string NightlyTag = "nightly"; + private static readonly Func GetDockerImage = MatchImage.Match; private static readonly char[] TrimChars = { ' ', ':', '/' }; @@ -107,6 +110,25 @@ public DockerImage( /// public string GetHostname() => _lazyHostname.Value; + /// + public bool MatchLatestOrNightly() + { + return MatchVersion((string tag) => LatestTag.Equals(tag) || NightlyTag.Equals(tag)); + } + + /// + public bool MatchVersion(Predicate predicate) + { + return predicate(Tag); + } + + /// + public bool MatchVersion(Predicate predicate) + { + var versionMatch = Regex.Match(Tag, "^\\d+(\\.\\d+)?(\\.\\d+)?", RegexOptions.None, TimeSpan.FromSeconds(1)); + return versionMatch.Success && Version.TryParse(versionMatch.Value, out var version) && predicate(version); + } + private static string TrimOrDefault(string value, string defaultValue = default) { return string.IsNullOrEmpty(value) ? defaultValue : value.Trim(TrimChars); diff --git a/src/Testcontainers/Images/FutureDockerImage.cs b/src/Testcontainers/Images/FutureDockerImage.cs index 02271788b..23817169d 100644 --- a/src/Testcontainers/Images/FutureDockerImage.cs +++ b/src/Testcontainers/Images/FutureDockerImage.cs @@ -1,5 +1,6 @@ namespace DotNet.Testcontainers.Images { + using System; using System.Threading; using System.Threading.Tasks; using Docker.DotNet.Models; @@ -74,6 +75,24 @@ public string GetHostname() return _configuration.Image.GetHostname(); } + /// + public bool MatchLatestOrNightly() + { + return _configuration.Image.MatchLatestOrNightly(); + } + + /// + public bool MatchVersion(Predicate predicate) + { + return _configuration.Image.MatchVersion(predicate); + } + + /// + public bool MatchVersion(Predicate predicate) + { + return _configuration.Image.MatchVersion(predicate); + } + /// public async Task CreateAsync(CancellationToken ct = default) { diff --git a/src/Testcontainers/Images/IImage.cs b/src/Testcontainers/Images/IImage.cs index b9badc5d3..307879d23 100644 --- a/src/Testcontainers/Images/IImage.cs +++ b/src/Testcontainers/Images/IImage.cs @@ -1,5 +1,6 @@ namespace DotNet.Testcontainers.Images { + using System; using JetBrains.Annotations; /// @@ -41,5 +42,25 @@ public interface IImage /// The registry hostname. [CanBeNull] string GetHostname(); + + /// + /// Checks if the tag matches either the latest or nightly tag. + /// + /// True if the tag matches the latest or nightly tag, otherwise false. + bool MatchLatestOrNightly(); + + /// + /// Checks if the tag matches the specified predicate. + /// + /// The predicate to match the tag against. + /// True if the tag matches the predicate, otherwise false. + bool MatchVersion(Predicate predicate); + + /// + /// Checks if the tag matches the specified predicate. + /// + /// The predicate to match the tag against. + /// True if the tag matches the predicate, otherwise false. + bool MatchVersion(Predicate predicate); } } diff --git a/tests/Testcontainers.Keycloak.Tests/KeycloakContainerTest.cs b/tests/Testcontainers.Keycloak.Tests/KeycloakContainerTest.cs index 85f6f69f5..23884b4d8 100644 --- a/tests/Testcontainers.Keycloak.Tests/KeycloakContainerTest.cs +++ b/tests/Testcontainers.Keycloak.Tests/KeycloakContainerTest.cs @@ -1,8 +1,13 @@ -namespace Testcontainers.Keycloak.Tests; +namespace Testcontainers.Keycloak; -public sealed class KeycloakContainerTest : IAsyncLifetime +public abstract class KeycloakContainerTest : IAsyncLifetime { - private readonly KeycloakContainer _keycloakContainer = new KeycloakBuilder().Build(); + private readonly KeycloakContainer _keycloakContainer; + + private KeycloakContainerTest(KeycloakContainer keycloakContainer) + { + _keycloakContainer = keycloakContainer; + } public Task InitializeAsync() { @@ -42,4 +47,22 @@ public async Task MasterRealmIsEnabled() // Then Assert.True(masterRealm.Enabled); } + + [UsedImplicitly] + public sealed class KeycloakDefaultConfiguration : KeycloakContainerTest + { + public KeycloakDefaultConfiguration() + : base(new KeycloakBuilder().Build()) + { + } + } + + [UsedImplicitly] + public sealed class KeycloakV25Configuration : KeycloakContainerTest + { + public KeycloakV25Configuration() + : base(new KeycloakBuilder().WithImage("quay.io/keycloak/keycloak:25.0").Build()) + { + } + } } \ No newline at end of file diff --git a/tests/Testcontainers.Keycloak.Tests/Usings.cs b/tests/Testcontainers.Keycloak.Tests/Usings.cs index 66a86e46d..b6c54f314 100644 --- a/tests/Testcontainers.Keycloak.Tests/Usings.cs +++ b/tests/Testcontainers.Keycloak.Tests/Usings.cs @@ -2,5 +2,6 @@ global using System.Net; global using System.Net.Http; global using System.Threading.Tasks; +global using JetBrains.Annotations; global using Keycloak.Net; global using Xunit; \ No newline at end of file diff --git a/tests/Testcontainers.Tests/Fixtures/Images/HealthCheckFixture.cs b/tests/Testcontainers.Tests/Fixtures/Images/HealthCheckFixture.cs index db4034d3e..932c95526 100644 --- a/tests/Testcontainers.Tests/Fixtures/Images/HealthCheckFixture.cs +++ b/tests/Testcontainers.Tests/Fixtures/Images/HealthCheckFixture.cs @@ -1,5 +1,6 @@ namespace DotNet.Testcontainers.Tests.Fixtures { + using System; using System.IO; using System.Threading.Tasks; using DotNet.Testcontainers.Builders; @@ -27,6 +28,21 @@ public string GetHostname() return _image.GetHostname(); } + public bool MatchLatestOrNightly() + { + return _image.MatchLatestOrNightly(); + } + + public bool MatchVersion(Predicate predicate) + { + return _image.MatchVersion(predicate); + } + + public bool MatchVersion(Predicate predicate) + { + return _image.MatchVersion(predicate); + } + public Task InitializeAsync() { return _image.CreateAsync(); diff --git a/tests/Testcontainers.Tests/Unit/Images/TestcontainersImageTest.cs b/tests/Testcontainers.Tests/Unit/Images/TestcontainersImageTest.cs index 4eacc1c80..bab17526a 100644 --- a/tests/Testcontainers.Tests/Unit/Images/TestcontainersImageTest.cs +++ b/tests/Testcontainers.Tests/Unit/Images/TestcontainersImageTest.cs @@ -59,5 +59,72 @@ public void WhenImageNameGetsAssigned(DockerImageFixtureSerializable serializabl Assert.Equal(expected.Tag, dockerImage.Tag); Assert.Equal(expected.FullName, dockerImage.FullName); } + + [Fact] + public void MatchLatestOrNightly_TagIsLatest_ReturnsTrue() + { + // Given + IImage dockerImage = new DockerImage("foo:latest"); + + // When + var result = dockerImage.MatchLatestOrNightly(); + + // Then + Assert.True(result); + } + + [Fact] + public void MatchLatestOrNightly_TagIsNightly_ReturnsTrue() + { + // Given + IImage dockerImage = new DockerImage("foo:nightly"); + + // When + var result = dockerImage.MatchLatestOrNightly(); + + // Then + Assert.True(result); + } + + [Fact] + public void MatchLatestOrNightly_TagIsNeither_ReturnsFalse() + { + // Given + IImage dockerImage = new DockerImage("foo:1.0.0"); + + // When + var result = dockerImage.MatchLatestOrNightly(); + + // Then + Assert.False(result); + } + + [Fact] + public void MatchVersion_ReturnsTrue_WhenVersionMatchesPredicate() + { + // Given + Predicate predicate = v => v.Major == 1 && v.Minor == 0 && v.Build == 0; + IImage dockerImage = new DockerImage("foo:1.0.0"); + + // When + var result = dockerImage.MatchVersion(predicate); + + // Then + Assert.True(result); + } + + [Fact] + public void MatchVersion_ReturnsFalse_WhenVersionDoesNotMatchPredicate() + { + // Given + Predicate predicate = v => v.Major == 0 && v.Minor == 0 && v.Build == 1; + IImage dockerImage = new DockerImage("foo:1.0.0"); + + // When + var result = dockerImage.MatchVersion(predicate); + + // Then + Assert.False(result); + } } }