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