diff --git a/src/WireMock.Net.RestClient/Extensions/WireMockAdminApiExtensions.cs b/src/WireMock.Net.RestClient/Extensions/WireMockAdminApiExtensions.cs index 3165b4b7..a55dbcff 100644 --- a/src/WireMock.Net.RestClient/Extensions/WireMockAdminApiExtensions.cs +++ b/src/WireMock.Net.RestClient/Extensions/WireMockAdminApiExtensions.cs @@ -1,3 +1,9 @@ +using System; +using System.Net.Http.Headers; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Stef.Validation; using WireMock.Client.Builders; namespace WireMock.Client.Extensions; @@ -7,13 +13,77 @@ namespace WireMock.Client.Extensions; /// public static class WireMockAdminApiExtensions { + private const int MaxRetries = 5; + private const int InitialWaitingTimeInMilliSeconds = 500; + private const string HealthStatusHealthy = "Healthy"; + /// /// Get a new for the . /// - /// See . + /// See . /// - public static AdminApiMappingBuilder GetMappingBuilder(this IWireMockAdminApi api) + public static AdminApiMappingBuilder GetMappingBuilder(this IWireMockAdminApi adminApi) { - return new AdminApiMappingBuilder(api); + return new AdminApiMappingBuilder(adminApi); + } + + /// + /// Set basic authentication to access the . + /// + /// See . + /// The admin username. + /// The admin password. + /// + public static IWireMockAdminApi WithAuthorization(this IWireMockAdminApi adminApi, string username, string password) + { + Guard.NotNull(adminApi); + Guard.NotNullOrEmpty(username); + Guard.NotNullOrEmpty(password); + + adminApi.Authorization = new AuthenticationHeaderValue("Basic", Convert.ToBase64String(Encoding.UTF8.GetBytes($"{username}:{password}"))); + return adminApi; + } + + /// + /// Wait for the WireMock.Net server to be healthy. (The "/__admin/health" returns "Healthy"). + /// + /// See . + /// The maximum number of retries. Default is 5. + /// The optional . + /// A completed Task in case the health endpoint is available, else throws a . + public static async Task WaitForHealthAsync(this IWireMockAdminApi adminApi, int maxRetries = MaxRetries, CancellationToken cancellationToken = default) + { + Guard.NotNull(adminApi); + + var retries = 0; + var waitTime = InitialWaitingTimeInMilliSeconds; + var totalWaitTime = waitTime; + var isHealthy = await IsHealthyAsync(adminApi, cancellationToken); + while (!isHealthy && retries < MaxRetries && !cancellationToken.IsCancellationRequested) + { + waitTime = (int)(InitialWaitingTimeInMilliSeconds * Math.Pow(2, retries)); + await Task.Delay(waitTime, cancellationToken); + isHealthy = await IsHealthyAsync(adminApi, cancellationToken); + retries++; + totalWaitTime += waitTime; + } + + if (retries >= MaxRetries) + { + throw new InvalidOperationException($"The /__admin/health endpoint did not return 'Healthy' after {MaxRetries} retries and {totalWaitTime / 1000.0:0.0} seconds."); + } + } + + private static async Task IsHealthyAsync(IWireMockAdminApi adminApi, CancellationToken cancellationToken) + { + try + { + var status = await adminApi.GetHealthAsync(cancellationToken); + return string.Equals(status, HealthStatusHealthy, StringComparison.OrdinalIgnoreCase); + } + catch + { + return false; + } } } \ No newline at end of file diff --git a/src/WireMock.Net.Testcontainers/WireMockContainer.cs b/src/WireMock.Net.Testcontainers/WireMockContainer.cs index 9a9a0bdf..625bb7ae 100644 --- a/src/WireMock.Net.Testcontainers/WireMockContainer.cs +++ b/src/WireMock.Net.Testcontainers/WireMockContainer.cs @@ -8,6 +8,7 @@ using Microsoft.Extensions.Logging; using RestEase; using Stef.Validation; using WireMock.Client; +using WireMock.Client.Extensions; using WireMock.Http; namespace WireMock.Net.Testcontainers; @@ -47,13 +48,7 @@ public sealed class WireMockContainer : DockerContainer ValidateIfRunning(); var api = RestClient.For(GetPublicUri()); - - if (_configuration.HasBasicAuthentication) - { - api.Authorization = new AuthenticationHeaderValue("Basic", Convert.ToBase64String(Encoding.ASCII.GetBytes($"{_configuration.Username}:{_configuration.Password}"))); - } - - return api; + return _configuration.HasBasicAuthentication ? api.WithAuthorization(_configuration.Username!, _configuration.Password!) : api; } /// diff --git a/test/WireMock.Net.Tests/AdminApi/WireMockAdminApiTests.cs b/test/WireMock.Net.Tests/AdminApi/WireMockAdminApiTests.cs index 6dc03f37..cc22ae77 100644 --- a/test/WireMock.Net.Tests/AdminApi/WireMockAdminApiTests.cs +++ b/test/WireMock.Net.Tests/AdminApi/WireMockAdminApiTests.cs @@ -17,6 +17,7 @@ using VerifyXunit; using WireMock.Admin.Mappings; using WireMock.Admin.Settings; using WireMock.Client; +using WireMock.Client.Extensions; using WireMock.Handlers; using WireMock.Logging; using WireMock.Matchers; @@ -42,15 +43,44 @@ public partial class WireMockAdminApiTests } [Fact] - public async Task IWireMockAdminApi_GetHealthAsync() + public async Task IWireMockAdminApi_WaitForHealthAsync_AndCall_GetHealthAsync_OK() { // Arrange - var server = WireMockServer.StartWithAdminInterface(); + var adminUsername = $"username_{Guid.NewGuid()}"; + var adminPassword = $"password_{Guid.NewGuid()}"; + var server = WireMockServer.Start(w => + { + w.StartAdminInterface = true; + w.AdminUsername = adminUsername; + w.AdminPassword = adminPassword; + }); + var api = RestClient.For(server.Urls[0]) + .WithAuthorization(adminUsername, adminPassword); + + // Act 1 + await api.WaitForHealthAsync().ConfigureAwait(false); + + // Act 2 + var status = await api.GetHealthAsync().ConfigureAwait(false); + status.Should().Be("Healthy"); + } + + [Fact] + public async Task IWireMockAdminApi_WaitForHealthAsync_AndCall_GetHealthAsync_ThrowsException() + { + // Arrange + var server = WireMockServer.Start(w => + { + w.StartAdminInterface = true; + w.AdminUsername = $"username_{Guid.NewGuid()}"; + w.AdminPassword = $"password_{Guid.NewGuid()}"; + }); + var api = RestClient.For(server.Urls[0]); // Act - var status = await api.GetHealthAsync().ConfigureAwait(false); - status.Should().Be("Healthy"); + Func act = () => api.WaitForHealthAsync(maxRetries: 3); + await act.Should().ThrowAsync(); } [Fact] diff --git a/test/WireMock.Net.Tests/Testcontainers/TestcontainersTests.cs b/test/WireMock.Net.Tests/Testcontainers/TestcontainersTests.cs index 27ac9b43..0c588f75 100644 --- a/test/WireMock.Net.Tests/Testcontainers/TestcontainersTests.cs +++ b/test/WireMock.Net.Tests/Testcontainers/TestcontainersTests.cs @@ -1,4 +1,5 @@ #if NET6_0_OR_GREATER +using System; using System.Threading.Tasks; using FluentAssertions; using FluentAssertions.Execution; @@ -13,9 +14,12 @@ public class TestcontainersTests public async Task WireMockContainer_Build_and_StartAsync_and_StopAsync() { // Act + var adminUsername = $"username_{Guid.NewGuid()}"; + var adminPassword = $"password_{Guid.NewGuid()}"; var wireMockContainer = new WireMockContainerBuilder() .WithAutoRemove(true) .WithCleanUp(true) + .WithAdminUserNameAndPassword(adminUsername, adminPassword) .Build(); try