// Copyright © WireMock.Net
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using DotNet.Testcontainers.Configurations;
using DotNet.Testcontainers.Containers;
using JetBrains.Annotations;
using Microsoft.Extensions.Logging;
using RestEase;
using Stef.Validation;
using WireMock.Client;
using WireMock.Client.Extensions;
using WireMock.Http;
using WireMock.Net.Testcontainers.Utils;
using WireMock.Util;
namespace WireMock.Net.Testcontainers;
///
/// A container for running WireMock in a docker environment.
///
///
/// Initializes a new instance of the class.
///
/// The container configuration.
public sealed class WireMockContainer(WireMockConfiguration configuration) : DockerContainer(configuration)
{
private const int EnhancedFileSystemWatcherTimeoutMs = 2000;
internal const int ContainerPort = 80;
private readonly WireMockConfiguration _configuration = Guard.NotNull(configuration);
private IWireMockAdminApi? _adminApi;
private EnhancedFileSystemWatcher? _enhancedFileSystemWatcher;
private IDictionary? _publicUris;
///
/// Gets the public Url.
///
[PublicAPI]
public string GetPublicUrl() => GetPublicUri().ToString();
///
/// Gets the public Urls as a dictionary with the internal port as the key.
///
[PublicAPI]
public IDictionary GetPublicUrls() => GetPublicUris().ToDictionary(kvp => kvp.Key, kvp => kvp.Value.ToString());
///
/// Gets the mapped public port for the given container port.
///
[PublicAPI]
public string GetMappedPublicUrl(int containerPort)
{
return GetPublicUris()[containerPort].ToString();
}
///
/// Create a RestEase Admin client which can be used to call the admin REST endpoint.
///
/// A
[PublicAPI]
public IWireMockAdminApi CreateWireMockAdminClient()
{
ValidateIfRunning();
var api = RestClient.For(GetPublicUri());
return _configuration.HasBasicAuthentication ? api.WithAuthorization(_configuration.Username!, _configuration.Password!) : api;
}
///
/// Create a which can be used to call this instance.
///
/// An ordered list of System.Net.Http.DelegatingHandler instances to be invoked
/// as an System.Net.Http.HttpRequestMessage travels from the System.Net.Http.HttpClient
/// to the network and an System.Net.Http.HttpResponseMessage travels from the network
/// back to System.Net.Http.HttpClient. The handlers are invoked in a top-down fashion.
/// That is, the first entry is invoked first for an outbound request message but
/// last for an inbound response message.
///
///
[PublicAPI]
public HttpClient CreateClient(params DelegatingHandler[] handlers)
{
ValidateIfRunning();
var client = HttpClientFactory2.Create(handlers);
client.BaseAddress = GetPublicUri();
return client;
}
///
/// Create a (one for each URL) which can be used to call this instance.
/// The inner handler represents the destination of the HTTP message channel.
///
/// An ordered list of System.Net.Http.DelegatingHandler instances to be invoked
/// as an System.Net.Http.HttpRequestMessage travels from the System.Net.Http.HttpClient
/// to the network and an System.Net.Http.HttpResponseMessage travels from the network
/// back to System.Net.Http.HttpClient. The handlers are invoked in a top-down fashion.
/// That is, the first entry is invoked first for an outbound request message but
/// last for an inbound response message.
///
///
[PublicAPI]
public HttpClient CreateClient(HttpMessageHandler innerHandler, params DelegatingHandler[] handlers)
{
ValidateIfRunning();
var client = HttpClientFactory2.Create(innerHandler, handlers);
client.BaseAddress = GetPublicUri();
return client;
}
///
/// Copies a test host directory or file to the container and triggers a reload of the static mappings if required.
///
/// The source directory or file to be copied.
/// The target directory path to copy the files to.
/// The user ID to set for the copied file or directory. Defaults to 0 (root).
/// The group ID to set for the copied file or directory. Defaults to 0 (root).
/// The POSIX file mode permission.
/// Cancellation token.
/// A task that completes when the directory or file has been copied.
public new async Task CopyAsync(string source, string target, uint uid = 0, uint gid = 0, UnixFileModes fileMode = Unix.FileMode644, CancellationToken ct = default)
{
await base.CopyAsync(source, target, uid, gid, fileMode, ct);
if (_configuration.WatchStaticMappings && await PathStartsWithContainerMappingsPath(target))
{
await ReloadStaticMappingsAsync(target, ct);
}
}
///
/// Reload the static mappings.
///
/// The optional cancellationToken.
public async Task ReloadStaticMappingsAsync(CancellationToken cancellationToken = default)
{
if (_adminApi == null)
{
return;
}
try
{
var result = await _adminApi.ReloadStaticMappingsAsync(cancellationToken);
Logger.LogInformation("WireMock.Net -> ReloadStaticMappings result: {Result}", result);
}
catch (Exception ex)
{
Logger.LogWarning(ex, "WireMock.Net -> Error calling /__admin/mappings/reloadStaticMappings");
}
}
///
/// Performs additional actions after the container is ready.
///
public Task CallAdditionalActionsAfterReadyAsync()
{
Logger.LogInformation("WireMock.Net -> Calling additional actions.");
_adminApi = CreateWireMockAdminClient();
RegisterEnhancedFileSystemWatcher();
return AddProtoDefinitionsAsync();
}
///
protected override ValueTask DisposeAsyncCore()
{
if (_enhancedFileSystemWatcher != null)
{
_enhancedFileSystemWatcher.EnableRaisingEvents = false;
_enhancedFileSystemWatcher.Created -= FileCreatedChangedOrDeleted;
_enhancedFileSystemWatcher.Changed -= FileCreatedChangedOrDeleted;
_enhancedFileSystemWatcher.Deleted -= FileCreatedChangedOrDeleted;
_enhancedFileSystemWatcher.Dispose();
_enhancedFileSystemWatcher = null;
}
return base.DisposeAsyncCore();
}
private static async Task PathStartsWithContainerMappingsPath(string value)
{
var imageOs = await TestcontainersUtils.GetImageOSAsync.Value;
return value.StartsWith(ContainerInfoProvider.Info[imageOs].MappingsPath);
}
private void ValidateIfRunning()
{
if (State != TestcontainersStates.Running)
{
throw new InvalidOperationException("Unable to create HttpClient because the WireMock.Net is not yet running.");
}
}
private void RegisterEnhancedFileSystemWatcher()
{
if (!_configuration.WatchStaticMappings || string.IsNullOrEmpty(_configuration.StaticMappingsPath))
{
return;
}
_enhancedFileSystemWatcher = new EnhancedFileSystemWatcher(_configuration.StaticMappingsPath!, "*.json", EnhancedFileSystemWatcherTimeoutMs)
{
IncludeSubdirectories = _configuration.WatchStaticMappingsInSubdirectories
};
_enhancedFileSystemWatcher.Created += FileCreatedChangedOrDeleted;
_enhancedFileSystemWatcher.Changed += FileCreatedChangedOrDeleted;
_enhancedFileSystemWatcher.Deleted += FileCreatedChangedOrDeleted;
_enhancedFileSystemWatcher.EnableRaisingEvents = true;
}
private async Task AddProtoDefinitionsAsync()
{
foreach (var kvp in _configuration.ProtoDefinitions)
{
Logger.LogInformation("WireMock.Net -> Adding ProtoDefinition '{Id}'", kvp.Key);
foreach (var protoDefinition in kvp.Value)
{
try
{
var result = await _adminApi!.AddProtoDefinitionAsync(kvp.Key, protoDefinition);
Logger.LogInformation("WireMock.Net -> AddProtoDefinition '{Id}' result: {Result}", kvp.Key, result);
}
catch (Exception ex)
{
Logger.LogWarning(ex, "WireMock.Net -> Error adding ProtoDefinition '{Id}'.", kvp.Key);
}
}
}
// Force a reload of static mappings when ProtoDefinitions are added at server-level to fix #1382
if (_configuration.ProtoDefinitions.Count > 0)
{
await ReloadStaticMappingsAsync();
}
}
private async void FileCreatedChangedOrDeleted(object sender, FileSystemEventArgs args)
{
try
{
await ReloadStaticMappingsAsync(args.FullPath);
Logger.LogInformation("WireMock.Net -> ReloadStaticMappings triggered from file change: '{FullPath}'.", args.FullPath);
}
catch (Exception ex)
{
Logger.LogWarning(ex, "WireMock.Net -> Error reloading static mappings from '{FullPath}'.", args.FullPath);
}
}
private async Task ReloadStaticMappingsAsync(string path, CancellationToken cancellationToken = default)
{
Logger.LogInformation("WireMock.Net -> MappingFile created, changed or deleted: '{Path}'. Triggering ReloadStaticMappings.", path);
await ReloadStaticMappingsAsync(cancellationToken);
}
private Uri GetPublicUri() => GetPublicUris()[ContainerPort];
private IDictionary GetPublicUris()
{
if (_publicUris != null)
{
return _publicUris;
}
_publicUris = _configuration.ExposedPorts.Keys
.Select(int.Parse)
.ToDictionary(port => port, port => new UriBuilder(Uri.UriSchemeHttp, Hostname, GetMappedPublicPort(port)).Uri);
foreach (var url in _configuration.AdditionalUrls)
{
if (PortUtils.TryExtract(url, out _, out _, out _, out _, out var port))
{
_publicUris[port] = new UriBuilder(Uri.UriSchemeHttp, Hostname, GetMappedPublicPort(port)).Uri;
}
}
return _publicUris;
}
}