diff --git a/docs/api/wait_strategies.md b/docs/api/wait_strategies.md index 4fa0f36e1..3cb83e602 100644 --- a/docs/api/wait_strategies.md +++ b/docs/api/wait_strategies.md @@ -28,5 +28,30 @@ _ = new TestcontainersBuilder() .Build(); ``` +## Wait until Http Request is successful + +You can wait for an HttpResponseCode or even compare the Response Body of an http request to an exposed port on your container + +```csharp +_ = new TestcontainersBuilder() + .WithWaitStrategy( + Wait.ForUnixContainer() + .UntilHttp() //Default to a GET to localhost:80 [ExposedfromContianer] Expecting 200 with no response body Validation + .Build(); +``` +or with Options +```csharp +_ = new TestcontainersBuilder() + .WithWaitStrategy( + Wait.ForUnixContainer() + .UntilHttp( options => { + options.Port = [YourServiceExposedPort]; + options.Path = [RequestPath]; + options.Method = [HttpRequestMethod]; + options.ExpectedResponseCodes = new(){ HttpStatusCode.OK, HttpStatusCode.Accepted }; + }) + .Build(); +``` + [docker-docs-healthcheck]: https://docs.docker.com/engine/reference/builder/#healthcheck diff --git a/src/Testcontainers/Configurations/UntilHttpOptions.cs b/src/Testcontainers/Configurations/UntilHttpOptions.cs new file mode 100644 index 000000000..82687e8ee --- /dev/null +++ b/src/Testcontainers/Configurations/UntilHttpOptions.cs @@ -0,0 +1,44 @@ +#nullable enable +namespace DotNet.Testcontainers.Configurations +{ + using System; + using System.Collections.Generic; + using System.Net; + using System.Net.Http; + using System.Security; + + /// + /// Configured the Request and Response Behaviour of the UntilHttp Wait + /// + public class UntilHttpOptions + { + public UntilHttpOptions() + { + this.Method = HttpMethod.Get; + this.Path = "/"; + this.Host = "localhost"; + this.Port = 80; + this.ExpectedResponseCodes = new() { HttpStatusCode.OK }; + this.TimeOut = TimeSpan.FromMinutes(1); + this.RequestDelay = 1; + this.MaxRetries = 10; + } + + public HttpMethod Method { get; set; } + public string Path { get; set; } + public string Host { get; set; } + public int Port { get; set; } + public HashSet ExpectedResponseCodes { get; set; } + public string? ExpectedOutput { get; set; } + public HttpContent? RequestContent { get; set; } + public bool UseSecure { get; set; } + public SecureString? AuthString { get; set; } + public bool UseAuth { get; set; } + public TimeSpan TimeOut { get; set; } + public bool ValidateContent { get; set; } + public double RequestDelay { get; set; } + public int MaxRetries { get; set; } + + public Uri Uri => new($"{(this.UseSecure ? "https" : "http")}://{this.Host}:{this.Port}{this.Path}"); + } +} diff --git a/src/Testcontainers/Configurations/WaitStrategies/IWaitForContainerOS.cs b/src/Testcontainers/Configurations/WaitStrategies/IWaitForContainerOS.cs index 8ed758b94..1e87976e4 100644 --- a/src/Testcontainers/Configurations/WaitStrategies/IWaitForContainerOS.cs +++ b/src/Testcontainers/Configurations/WaitStrategies/IWaitForContainerOS.cs @@ -86,6 +86,13 @@ public interface IWaitForContainerOS [PublicAPI] IWaitForContainerOS UntilContainerIsHealthy(long failingStreak = 20); + /// + /// Waits until Http Requests returns Ok + /// + /// A configured instance of . + [PublicAPI] + IWaitForContainerOS UntilHttpSuccess(Action? action = null); + /// /// Returns a collection with all configured wait strategies. /// diff --git a/src/Testcontainers/Configurations/WaitStrategies/UntilHttp.cs b/src/Testcontainers/Configurations/WaitStrategies/UntilHttp.cs new file mode 100644 index 000000000..ff8bfb933 --- /dev/null +++ b/src/Testcontainers/Configurations/WaitStrategies/UntilHttp.cs @@ -0,0 +1,84 @@ +namespace DotNet.Testcontainers.Configurations +{ + using System; + using System.Net.Http; + using System.Net.Http.Headers; + using System.Text.RegularExpressions; + using System.Threading; + using System.Threading.Tasks; + using DotNet.Testcontainers.Containers; + using Microsoft.Extensions.Logging; + + public class UntilHttp : IWaitUntil + { + private readonly UntilHttpOptions Options; + private int RetryCount = 0; + + public UntilHttp(string path) + { + this.Options = new() { Path = path, Method = HttpMethod.Get }; + } + + public UntilHttp(UntilHttpOptions options) + { + this.Options = options; + } + + public async Task Until(ITestcontainersContainer testcontainers, ILogger logger) + { + + try + { + var mappedPort = testcontainers.GetMappedPublicPort(this.Options.Port); + this.Options.Port = mappedPort; + var client = new HttpClient(); + var message = new HttpRequestMessage(this.Options.Method, this.Options.Uri); + if (this.Options.RequestContent is not null && (this.Options.Method == HttpMethod.Post || this.Options.Method == HttpMethod.Put)) + { + message.Content = this.Options.RequestContent; + } + + if (this.Options.UseAuth && this.Options.AuthString is not null) + { + message.Headers.Authorization = AuthenticationHeaderValue.Parse(this.Options.AuthString.ToString()); + } + + var sendTask = Task.Run(async () => + { + HttpResponseMessage response = null; + while (response is null || !this.Options.ExpectedResponseCodes.Contains(response.StatusCode)) + { + response = await client.SendAsync(message); + if (++this.RetryCount > this.Options.MaxRetries) + { + throw new TimeoutException($"Http Wait Failed {this.Options.MaxRetries} Times"); + } + + if (!this.Options.ExpectedResponseCodes.Contains(response.StatusCode)) + { + Thread.Sleep(TimeSpan.FromSeconds(this.Options.RequestDelay)); + } + } + return response; + }); + var completed = sendTask.Wait(this.Options.TimeOut); + if (!completed) + { + throw new TimeoutException($"Http Wait Failed Timed Out after {this.Options.TimeOut}"); + } + + var responseContent = await sendTask.Result.Content.ReadAsStringAsync(); + return !this.Options.ValidateContent || Regex.Match(this.Options.ExpectedOutput, responseContent).Success; + } + catch (Exception) + { + if (++this.RetryCount > this.Options.MaxRetries) + { + throw new TimeoutException($"Http Wait Failed {this.Options.MaxRetries} Times"); + } + + return false; + } + } + } +} diff --git a/src/Testcontainers/Configurations/WaitStrategies/WaitForContainerOS.cs b/src/Testcontainers/Configurations/WaitStrategies/WaitForContainerOS.cs index 9991c0eae..06f0715a3 100644 --- a/src/Testcontainers/Configurations/WaitStrategies/WaitForContainerOS.cs +++ b/src/Testcontainers/Configurations/WaitStrategies/WaitForContainerOS.cs @@ -57,6 +57,15 @@ public virtual IWaitForContainerOS UntilContainerIsHealthy(long failingStreak = return this.AddCustomWaitStrategy(new UntilContainerIsHealthy(failingStreak)); } + /// + public IWaitForContainerOS UntilHttpSuccess(Action action = null) + { + var options = new UntilHttpOptions(); + action?.Invoke(options); + var httpWait = new UntilHttp(options); + return this.AddCustomWaitStrategy(httpWait); + } + /// public IEnumerable Build() { diff --git a/src/Testcontainers/Testcontainers.csproj b/src/Testcontainers/Testcontainers.csproj index abb2e8592..f82421a3c 100644 --- a/src/Testcontainers/Testcontainers.csproj +++ b/src/Testcontainers/Testcontainers.csproj @@ -5,6 +5,7 @@ Debug;Release AnyCPU DotNet.Testcontainers + 10