// Copyright © WireMock.Net
using System;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Runtime.InteropServices;
using Docker.DotNet.Models;
using DotNet.Testcontainers.Builders;
using DotNet.Testcontainers.Configurations;
using JetBrains.Annotations;
using Stef.Validation;
using WireMock.Net.Testcontainers.Utils;
using WireMock.Util;
namespace WireMock.Net.Testcontainers;
///
/// A specific fluent Docker container builder for WireMock.Net
///
public sealed class WireMockContainerBuilder : ContainerBuilder
{
private const string DefaultLogger = "WireMockConsoleLogger";
private OSPlatform? _imageOS;
///
/// Initializes a new instance of the class.
///
public WireMockContainerBuilder() : this(new WireMockConfiguration())
{
DockerResourceConfiguration = Init().DockerResourceConfiguration;
}
///
/// Automatically use the correct image for WireMock.
/// For Linux this is "sheyenrath/wiremock.net-alpine:latest"
/// For Windows this is "sheyenrath/wiremock.net-windows:latest"
///
/// A configured instance of
[PublicAPI]
public WireMockContainerBuilder WithImage()
{
_imageOS ??= TestcontainersUtils.GetImageOSAsync.Value.GetAwaiter().GetResult();
return WithImage(_imageOS.Value);
}
///
/// Automatically use a Linux image for WireMock. This is "sheyenrath/wiremock.net-alpine:latest"
///
/// A configured instance of
[PublicAPI]
public WireMockContainerBuilder WithLinuxImage()
{
return WithImage(OSPlatform.Linux);
}
///
/// Automatically use a Windows image for WireMock. This is "sheyenrath/wiremock.net-windows:latest"
///
/// A configured instance of
[PublicAPI]
public WireMockContainerBuilder WithWindowsImage()
{
return WithImage(OSPlatform.Windows);
}
///
/// Sets a custom WireMock.Net image for which to create the container.
///
/// The image name.
/// A configured instance of
[PublicAPI]
public new WireMockContainerBuilder WithImage(string image)
{
return WithCustomImage(image);
}
///
/// Sets a custom WireMock.Net image for which to create the container.
///
/// The image name.
/// A configured instance of
[PublicAPI]
public WireMockContainerBuilder WithCustomImage(string image)
{
_imageOS ??= TestcontainersUtils.GetImageOSAsync.Value.GetAwaiter().GetResult();
return base.WithImage(image);
}
///
/// Set the admin username and password for the container (basic authentication).
///
/// The admin username.
/// The admin password.
/// A configured instance of
public WireMockContainerBuilder WithAdminUserNameAndPassword(string username, string password)
{
Guard.NotNull(username);
Guard.NotNull(password);
if (string.IsNullOrEmpty(username) && string.IsNullOrEmpty(password))
{
return this;
}
return Merge(DockerResourceConfiguration, new WireMockConfiguration(username, password))
.WithCommand($"--AdminUserName {username}", $"--AdminPassword {password}");
}
///
/// Use the WireMockNullLogger.
///
/// A configured instance of
[PublicAPI]
public WireMockContainerBuilder WithNullLogger()
{
return WithCommand("--WireMockLogger WireMockNullLogger");
}
///
/// Defines if the static mappings should be read at startup (default set to false).
///
/// A configured instance of
[PublicAPI]
public WireMockContainerBuilder WithReadStaticMappings()
{
return WithCommand("--ReadStaticMappings true");
}
///
/// Watch the static mapping files + folder for changes when running.
///
/// Also look in SubDirectories.
/// A configured instance of
[PublicAPI]
public WireMockContainerBuilder WithWatchStaticMappings(bool includeSubDirectories)
{
DockerResourceConfiguration.WithWatchStaticMappings(includeSubDirectories);
return
WithCommand("--ReadStaticMappings true").
WithCommand("--WatchStaticMappings true").
WithCommand("--WatchStaticMappingsInSubdirectories", includeSubDirectories);
}
///
/// Specifies the path for the (static) mapping json files.
///
/// The path
/// Also look in SubDirectories.
/// A configured instance of
[PublicAPI]
public WireMockContainerBuilder WithMappings(string path, bool includeSubDirectories = false)
{
Guard.NotNullOrEmpty(path);
DockerResourceConfiguration.WithStaticMappingsPath(path);
return WithWatchStaticMappings(includeSubDirectories);
}
///
/// Use Http version 2.
///
/// A configured instance of
[PublicAPI]
public WireMockContainerBuilder WithHttp2()
{
return WithCommand("--UseHttp2 true");
}
///
/// Adds another URL to the WireMock container. By default, the WireMock container will listen on http://*:80.
///
/// This method can be used to also host the WireMock container on another port or protocol (like grpc).
///
/// grpc://*:9090
/// A configured instance of
[PublicAPI]
public WireMockContainerBuilder AddUrl(string url)
{
if (!PortUtils.TryExtract(Guard.NotNullOrEmpty(url), out _, out _, out _, out _, out var port))
{
throw new ArgumentException("The URL is not valid.", nameof(url));
}
DockerResourceConfiguration.WithAdditionalUrl(url);
return WithPortBinding(port, true);
}
///
/// Add a Grpc ProtoDefinition at server-level.
///
/// Unique identifier for the ProtoDefinition.
/// The ProtoDefinition as text.
///
[PublicAPI]
public WireMockContainerBuilder AddProtoDefinition(string id, params string[] protoDefinition)
{
Guard.NotNullOrWhiteSpace(id);
Guard.NotNullOrEmpty(protoDefinition);
DockerResourceConfiguration.AddProtoDefinition(id, protoDefinition);
return this;
}
private WireMockContainerBuilder WithCommand(string param, bool value)
{
return !value ? this : WithCommand($"{param} true");
}
private WireMockContainerBuilder(WireMockConfiguration dockerResourceConfiguration) : base(dockerResourceConfiguration)
{
DockerResourceConfiguration = dockerResourceConfiguration;
}
///
protected override WireMockConfiguration DockerResourceConfiguration { get; }
///
public override WireMockContainer Build()
{
var builder = this;
// In case no image has been set, set the image using internal logic.
if (DockerResourceConfiguration.Image == null)
{
builder = WithImage();
}
// In case the _imageOS is not set, determine it from the Image FullName.
if (_imageOS == null)
{
if (builder.DockerResourceConfiguration.Image.FullName.IndexOf("wiremock", StringComparison.OrdinalIgnoreCase) < 0)
{
throw new InvalidOperationException("It's only possible to use a wiremock docker image.");
}
_imageOS = builder.DockerResourceConfiguration.Image.FullName.IndexOf("windows", StringComparison.OrdinalIgnoreCase) >= 0 ? OSPlatform.Windows : OSPlatform.Linux;
}
if (!string.IsNullOrEmpty(builder.DockerResourceConfiguration.StaticMappingsPath))
{
builder = builder.WithBindMount(builder.DockerResourceConfiguration.StaticMappingsPath, ContainerInfoProvider.Info[_imageOS.Value].MappingsPath);
}
if (builder.DockerResourceConfiguration.AdditionalUrls.Any())
{
builder = builder.WithCommand($"--Urls http://*:80 {string.Join(" ", builder.DockerResourceConfiguration.AdditionalUrls)}");
}
builder.Validate();
var waitForContainerOS = _imageOS == OSPlatform.Windows ? Wait.ForWindowsContainer() : Wait.ForUnixContainer();
builder = builder
.WithWaitStrategy(waitForContainerOS
.UntilMessageIsLogged("WireMock.Net server running", waitStrategy => waitStrategy.WithTimeout(TimeSpan.FromSeconds(30)))
.UntilHttpRequestIsSucceeded(httpWaitStrategy => httpWaitStrategy
.ForPort(WireMockContainer.ContainerPort)
.WithMethod(HttpMethod.Get)
.WithBasicAuthentication(DockerResourceConfiguration)
.ForPath("/__admin/health")
.ForStatusCode(HttpStatusCode.OK)
.ForResponseMessageMatching(async httpResponseMessage =>
{
var content = await httpResponseMessage.Content.ReadAsStringAsync();
return content?.Contains("Healthy") == true;
})
)
.AddCustomWaitStrategy(new WireMockWaitStrategy())
);
return new WireMockContainer(builder.DockerResourceConfiguration);
}
///
protected override WireMockContainerBuilder Init()
{
var builder = base.Init();
return builder
.WithPortBinding(WireMockContainer.ContainerPort, true)
.WithCommand($"--WireMockLogger {DefaultLogger}");
}
///
protected override WireMockContainerBuilder Clone(IContainerConfiguration resourceConfiguration)
{
return Merge(DockerResourceConfiguration, new WireMockConfiguration(resourceConfiguration));
}
///
protected override WireMockContainerBuilder Clone(IResourceConfiguration resourceConfiguration)
{
return Merge(DockerResourceConfiguration, new WireMockConfiguration(resourceConfiguration));
}
///
protected override WireMockContainerBuilder Merge(WireMockConfiguration oldValue, WireMockConfiguration newValue)
{
return new WireMockContainerBuilder(new WireMockConfiguration(oldValue, newValue));
}
private WireMockContainerBuilder WithImage(OSPlatform os)
{
_imageOS = os;
return WithImage(ContainerInfoProvider.Info[os].Image);
}
}