Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add ProtoDefinition to WireMockContainer #1250

Merged
merged 9 commits into from
Feb 12, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
</PropertyGroup>

<PropertyGroup>
<VersionPrefix>1.7.1</VersionPrefix>
<VersionPrefix>1.7.2-preview-01</VersionPrefix>
<PackageIcon>WireMock.Net-Logo.png</PackageIcon>
<PackageProjectUrl>https://github.com/WireMock-Net/WireMock.Net</PackageProjectUrl>
<PackageLicenseExpression>Apache-2.0</PackageLicenseExpression>
Expand Down
9 changes: 9 additions & 0 deletions src/WireMock.Net.RestClient/IWireMockAdminApi.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down
36 changes: 34 additions & 2 deletions src/WireMock.Net.Testcontainers/WireMockConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using DotNet.Testcontainers.Builders;
using DotNet.Testcontainers.Configurations;
using JetBrains.Annotations;
using Stef.Validation;

namespace WireMock.Net.Testcontainers;

Expand All @@ -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;
Expand Down Expand Up @@ -74,7 +77,8 @@ public WireMockConfiguration(WireMockConfiguration oldValue, WireMockConfigurati
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>
Expand Down Expand Up @@ -107,7 +111,35 @@ public WireMockConfiguration WithWatchStaticMappings(bool includeSubDirectories)
/// <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);
}
}
35 changes: 30 additions & 5 deletions src/WireMock.Net.Testcontainers/WireMockContainer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
using JetBrains.Annotations;
using Microsoft.Extensions.Logging;
using RestEase;
using Stef.Validation;
using WireMock.Client;
using WireMock.Client.Extensions;
using WireMock.Http;
Expand Down Expand Up @@ -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>
Expand Down Expand Up @@ -175,8 +176,6 @@ protected override ValueTask DisposeAsyncCore()
_enhancedFileSystemWatcher = null;
}

Started -= WireMockContainer_Started;

return base.DisposeAsyncCore();
}

Expand All @@ -195,10 +194,17 @@ private void ValidateIfRunning()
}
}

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;
Expand All @@ -214,6 +220,25 @@ private void WireMockContainer_Started(object sender, EventArgs e)
_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
Expand Down
17 changes: 17 additions & 0 deletions src/WireMock.Net.Testcontainers/WireMockContainerBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,23 @@ public WireMockContainerBuilder AddUrl(string url)
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");
Expand Down
17 changes: 11 additions & 6 deletions src/WireMock.Net/Server/WireMockServer.Admin.cs
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ public AdminPaths(WireMockServerSettings settings)
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
Expand Down Expand Up @@ -147,6 +148,9 @@ private void InitAdmin()
// __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

Expand Down Expand Up @@ -369,7 +373,7 @@ private IResponseMessage MappingCodeGet(IRequestMessage requestMessage)
{
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");
Expand All @@ -383,15 +387,16 @@ private IResponseMessage MappingCodeGet(IRequestMessage requestMessage)
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)
Expand Down Expand Up @@ -465,7 +470,7 @@ private IResponseMessage MappingsGet(IRequestMessage requestMessage)

private IResponseMessage MappingsCodeGet(IRequestMessage requestMessage)
{
var converterType = GetMappingConverterType(requestMessage);
var converterType = GetEnumFromQuery(requestMessage, MappingConverterType.Server);

var code = _mappingBuilder.ToCSharpCode(converterType);

Expand Down
16 changes: 16 additions & 0 deletions src/WireMock.Net/Server/WireMockServer.AdminFiles.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand Down
9 changes: 8 additions & 1 deletion src/WireMock.Net/Server/WireMockServer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -602,7 +602,14 @@ public WireMockServer AddProtoDefinition(string id, params string[] protoDefinit

_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;
}
Expand Down
10 changes: 10 additions & 0 deletions test/WireMock.Net.Tests/Constants.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// Copyright © WireMock.Net

namespace WireMock.Net.Tests;

internal static class Constants
{
internal const int NumStaticMappings = 10;

internal const int NumAdminMappings = 36;
}
41 changes: 34 additions & 7 deletions test/WireMock.Net.Tests/Testcontainers/TestcontainersTests.Grpc.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
#if NET6_0_OR_GREATER
using System;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
Expand Down Expand Up @@ -129,9 +128,9 @@ public async Task WireMockContainer_Build_Grpc_TestPortsAndUrls2()
[Fact]
public async Task WireMockContainer_Build_Grpc_ProtoDefinitionFromJson_UsingGrpcGeneratedClient()
{
var wireMockContainer = await Given_WireMockContainerIsStartedForHttpAndGrpc();
var wireMockContainer = await Given_WireMockContainerIsStartedForHttpAndGrpcAsync();

await Given_ProtoBufMappingIsAddedViaAdminInterfaceAsync(wireMockContainer);
await Given_ProtoBufMappingIsAddedViaAdminInterfaceAsync(wireMockContainer, "protobuf-mapping-1.json");

var reply = await When_GrpcClient_Calls_SayHelloAsync(wireMockContainer);

Expand All @@ -140,22 +139,50 @@ public async Task WireMockContainer_Build_Grpc_ProtoDefinitionFromJson_UsingGrpc
await wireMockContainer.StopAsync();
}

private static async Task<WireMockContainer> Given_WireMockContainerIsStartedForHttpAndGrpc()
[Fact]
public async Task WireMockContainer_Build_Grpc_ProtoDefinitionAtServerLevel_UsingGrpcGeneratedClient()
{
var wireMockContainer = await Given_WireMockContainerWithProtoDefinitionAtServerLevelIsStartedForHttpAndGrpcAsync();

await Given_ProtoBufMappingIsAddedViaAdminInterfaceAsync(wireMockContainer, "protobuf-mapping-4.json");

var reply = await When_GrpcClient_Calls_SayHelloAsync(wireMockContainer);

Then_ReplyMessage_Should_BeCorrect(reply);

await wireMockContainer.StopAsync();
}

private static async Task<WireMockContainer> Given_WireMockContainerIsStartedForHttpAndGrpcAsync()
{
var wireMockContainer = new WireMockContainerBuilder()
.WithAutoRemove(true)
.WithCleanUp(true)
.AddUrl("grpc://*:9090")
.Build();

await wireMockContainer.StartAsync();

return wireMockContainer;
}

private static async Task<WireMockContainer> Given_WireMockContainerWithProtoDefinitionAtServerLevelIsStartedForHttpAndGrpcAsync()
{
var wireMockContainer = new WireMockContainerBuilder()
.WithAutoRemove(true)
.WithCleanUp(true)
.AddUrl("grpc://*:9090")
.AddProtoDefinition("my-greeter", ReadFile("greet.proto"))
.Build();

await wireMockContainer.StartAsync();

return wireMockContainer;
}

private static async Task Given_ProtoBufMappingIsAddedViaAdminInterfaceAsync(WireMockContainer wireMockContainer)
private static async Task Given_ProtoBufMappingIsAddedViaAdminInterfaceAsync(WireMockContainer wireMockContainer, string filename)
{
var mappingsJson = ReadMappingFile("protobuf-mapping-1.json");
var mappingsJson = ReadFile(filename);

using var httpClient = wireMockContainer.CreateClient();

Expand All @@ -178,7 +205,7 @@ private static void Then_ReplyMessage_Should_BeCorrect(HelloReply reply)
reply.Message.Should().Be("hello stef POST");
}

private static string ReadMappingFile(string filename)
private static string ReadFile(string filename)
{
return File.ReadAllText(Path.Combine(Directory.GetCurrentDirectory(), "__admin", "mappings", filename));
}
Expand Down
3 changes: 3 additions & 0 deletions test/WireMock.Net.Tests/WireMock.Net.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,9 @@
<None Update="__admin\mappings\*.xml">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="__admin\mappings\*.proto">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="__admin\mappings\subdirectory\*.xml">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
Expand Down
Loading
Loading