Add ProtoDefinition to WireMockContainer (#1250)

* AddProtoDefinitionAsync

* ...

* Body

* "

* .

* .

* .

* [Fact(Skip = "new docker is needed")]

* x
This commit is contained in:
Stef Heyenrath
2025-02-12 06:08:55 +01:00
committed by GitHub
parent a02ff47db6
commit e8de5aa73c
17 changed files with 203 additions and 37 deletions

View File

@@ -311,6 +311,15 @@ public interface IWireMockAdminApi
[Delete("files/{filename}")]
Task<StatusModel> DeleteFileAsync([Path] string filename, CancellationToken cancellationToken = default);
/// <summary>
/// Add a Grpc ProtoDefinition at server-level.
/// </summary>
/// <param name="id">Unique identifier for the ProtoDefinition.</param>
/// <param name="protoDefinition">The ProtoDefinition as text.</param>
/// <param name="cancellationToken">The optional cancellationToken.</param>
[Post("protodefinitions/{id}")]
Task<StatusModel> AddProtoDefinitionAsync([Path] string id, [Body] string body, CancellationToken cancellationToken = default);
/// <summary>
/// Check if a file exists
/// </summary>

View File

@@ -6,6 +6,7 @@ using Docker.DotNet.Models;
using DotNet.Testcontainers.Builders;
using DotNet.Testcontainers.Configurations;
using JetBrains.Annotations;
using Stef.Validation;
namespace WireMock.Net.Testcontainers;
@@ -28,6 +29,8 @@ public sealed class WireMockConfiguration : ContainerConfiguration
public List<string> AdditionalUrls { get; private set; } = [];
public Dictionary<string, string[]> ProtoDefinitions { get; set; } = new();
public WireMockConfiguration(string? username = null, string? password = null)
{
Username = username;
@@ -74,7 +77,8 @@ public sealed class WireMockConfiguration : ContainerConfiguration
StaticMappingsPath = BuildConfiguration.Combine(oldValue.StaticMappingsPath, newValue.StaticMappingsPath);
WatchStaticMappings = BuildConfiguration.Combine(oldValue.WatchStaticMappings, newValue.WatchStaticMappings);
WatchStaticMappingsInSubdirectories = BuildConfiguration.Combine(oldValue.WatchStaticMappingsInSubdirectories, newValue.WatchStaticMappingsInSubdirectories);
AdditionalUrls = BuildConfiguration.Combine(oldValue.AdditionalUrls.AsEnumerable(), newValue.AdditionalUrls.AsEnumerable()).ToList();
AdditionalUrls = Combine(oldValue.AdditionalUrls, newValue.AdditionalUrls);
ProtoDefinitions = Combine(oldValue.ProtoDefinitions, newValue.ProtoDefinitions);
}
/// <summary>
@@ -107,7 +111,35 @@ public sealed class WireMockConfiguration : ContainerConfiguration
/// <returns><see cref="WireMockConfiguration"/></returns>
public WireMockConfiguration WithAdditionalUrl(string url)
{
AdditionalUrls.Add(url);
AdditionalUrls.Add(Guard.NotNullOrWhiteSpace(url));
return this;
}
/// <summary>
/// Add a Grpc ProtoDefinition at server-level.
/// </summary>
/// <param name="id">Unique identifier for the ProtoDefinition.</param>
/// <param name="protoDefinition">The ProtoDefinition as text.</param>
/// <returns><see cref="WireMockConfiguration"/></returns>
public WireMockConfiguration AddProtoDefinition(string id, params string[] protoDefinition)
{
Guard.NotNullOrWhiteSpace(id);
Guard.NotNullOrEmpty(protoDefinition);
ProtoDefinitions[id] = protoDefinition;
return this;
}
private static List<T> Combine<T>(List<T> oldValue, List<T> newValue)
{
return oldValue.Concat(newValue).ToList();
}
private static Dictionary<TKey, TValue> Combine<TKey, TValue>(Dictionary<TKey, TValue> oldValue, Dictionary<TKey, TValue> newValue)
{
return newValue
.Concat(oldValue.Where(item => !newValue.Keys.Contains(item.Key)))
.ToDictionary(item => item.Key, item => item.Value);
}
}

View File

@@ -12,6 +12,7 @@ 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;
@@ -40,9 +41,9 @@ public sealed class WireMockContainer : DockerContainer
/// <param name="configuration">The container configuration.</param>
public WireMockContainer(WireMockConfiguration configuration) : base(configuration)
{
_configuration = Stef.Validation.Guard.NotNull(configuration);
_configuration = Guard.NotNull(configuration);
Started += WireMockContainer_Started;
Started += async (sender, eventArgs) => await WireMockContainerStartedAsync(sender, eventArgs);
}
/// <summary>
@@ -175,8 +176,6 @@ public sealed class WireMockContainer : DockerContainer
_enhancedFileSystemWatcher = null;
}
Started -= WireMockContainer_Started;
return base.DisposeAsyncCore();
}
@@ -195,10 +194,17 @@ public sealed class WireMockContainer : DockerContainer
}
}
private void WireMockContainer_Started(object sender, EventArgs e)
private async Task WireMockContainerStartedAsync(object sender, EventArgs e)
{
_adminApi = CreateWireMockAdminClient();
RegisterEnhancedFileSystemWatcher();
await CallAdditionalActionsAfterStartedAsync();
}
private void RegisterEnhancedFileSystemWatcher()
{
if (!_configuration.WatchStaticMappings || string.IsNullOrEmpty(_configuration.StaticMappingsPath))
{
return;
@@ -214,6 +220,25 @@ public sealed class WireMockContainer : DockerContainer
_enhancedFileSystemWatcher.EnableRaisingEvents = true;
}
private async Task CallAdditionalActionsAfterStartedAsync()
{
foreach (var kvp in _configuration.ProtoDefinitions)
{
Logger.LogInformation("Adding ProtoDefinition {Id}", kvp.Key);
foreach (var protoDefinition in kvp.Value)
{
try
{
await _adminApi!.AddProtoDefinitionAsync(kvp.Key, protoDefinition);
}
catch (Exception ex)
{
Logger.LogWarning(ex, "Error adding ProtoDefinition '{Id}'.", kvp.Key);
}
}
}
}
private async void FileCreatedChangedOrDeleted(object sender, FileSystemEventArgs args)
{
try

View File

@@ -164,6 +164,23 @@ public sealed class WireMockContainerBuilder : ContainerBuilder<WireMockContaine
return WithPortBinding(port, true);
}
/// <summary>
/// Add a Grpc ProtoDefinition at server-level.
/// </summary>
/// <param name="id">Unique identifier for the ProtoDefinition.</param>
/// <param name="protoDefinition">The ProtoDefinition as text.</param>
/// <returns><see cref="WireMockContainerBuilder"/></returns>
[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");

View File

@@ -68,6 +68,7 @@ public partial class WireMockServer
public RegexMatcher ScenariosNameMatcher => new($"^{_prefixEscaped}\\/scenarios\\/.+$");
public RegexMatcher ScenariosNameWithResetMatcher => new($"^{_prefixEscaped}\\/scenarios\\/.+\\/reset$");
public RegexMatcher FilesFilenamePathMatcher => new($"^{_prefixEscaped}\\/files\\/.+$");
public RegexMatcher ProtoDefinitionsIdPathMatcher => new($"^{_prefixEscaped}\\/protodefinitions\\/.+$");
}
#region InitAdmin
@@ -147,6 +148,9 @@ public partial class WireMockServer
// __admin/openapi
Given(Request.Create().WithPath($"{_adminPaths.OpenApi}/convert").UsingPost()).AtPriority(WireMockConstants.AdminPriority).RespondWith(new DynamicResponseProvider(OpenApiConvertToMappings));
Given(Request.Create().WithPath($"{_adminPaths.OpenApi}/save").UsingPost()).AtPriority(WireMockConstants.AdminPriority).RespondWith(new DynamicResponseProvider(OpenApiSaveToMappings));
// __admin/protodefinitions/{id}
Given(Request.Create().WithPath(_adminPaths.ProtoDefinitionsIdPathMatcher).UsingPost()).AtPriority(WireMockConstants.AdminPriority).RespondWith(new DynamicResponseProvider(ProtoDefinitionAdd));
}
#endregion
@@ -369,7 +373,7 @@ public partial class WireMockServer
{
if (TryParseGuidFromRequestMessage(requestMessage, out var guid))
{
var code = _mappingBuilder.ToCSharpCode(guid, GetMappingConverterType(requestMessage));
var code = _mappingBuilder.ToCSharpCode(guid, GetEnumFromQuery(requestMessage, MappingConverterType.Server));
if (code is null)
{
_settings.Logger.Warn("HttpStatusCode set to 404 : Mapping not found");
@@ -383,15 +387,16 @@ public partial class WireMockServer
return ResponseMessageBuilder.Create(HttpStatusCode.BadRequest, "GUID is missing");
}
private static MappingConverterType GetMappingConverterType(IRequestMessage requestMessage)
private static TEnum GetEnumFromQuery<TEnum>(IRequestMessage requestMessage, TEnum defaultValue)
where TEnum : struct
{
if (requestMessage.QueryIgnoreCase?.TryGetValue(nameof(MappingConverterType), out var values) == true &&
Enum.TryParse(values.FirstOrDefault(), true, out MappingConverterType parsed))
if (requestMessage.QueryIgnoreCase?.TryGetValue(typeof(TEnum).Name, out var values) == true &&
Enum.TryParse<TEnum>(values.FirstOrDefault(), true, out var parsed))
{
return parsed;
}
return MappingConverterType.Server;
return defaultValue;
}
private IMapping? FindMappingByGuid(IRequestMessage requestMessage)
@@ -465,7 +470,7 @@ public partial class WireMockServer
private IResponseMessage MappingsCodeGet(IRequestMessage requestMessage)
{
var converterType = GetMappingConverterType(requestMessage);
var converterType = GetEnumFromQuery(requestMessage, MappingConverterType.Server);
var code = _mappingBuilder.ToCSharpCode(converterType);

View File

@@ -13,6 +13,22 @@ public partial class WireMockServer
{
private static readonly Encoding[] FileBodyIsString = [Encoding.UTF8, Encoding.ASCII];
#region ProtoDefinitions/{id}
private IResponseMessage ProtoDefinitionAdd(IRequestMessage requestMessage)
{
if (requestMessage.Body is null)
{
return ResponseMessageBuilder.Create(HttpStatusCode.BadRequest, "Body is null");
}
var id = requestMessage.Path.Split('/').Last();
AddProtoDefinition(id, requestMessage.Body);
return ResponseMessageBuilder.Create(HttpStatusCode.OK, "ProtoDefinition added");
}
#endregion
#region Files/{filename}
private IResponseMessage FilePost(IRequestMessage requestMessage)
{

View File

@@ -602,7 +602,14 @@ public partial class WireMockServer : IWireMockServer
_settings.ProtoDefinitions ??= new Dictionary<string, string[]>();
_settings.ProtoDefinitions[id] = protoDefinition;
if (_settings.ProtoDefinitions.TryGetValue(id, out var existingProtoDefinitions))
{
_settings.ProtoDefinitions[id] = existingProtoDefinitions.Union(protoDefinition).ToArray();
}
else
{
_settings.ProtoDefinitions[id] = protoDefinition;
}
return this;
}