diff --git a/Directory.Build.props b/Directory.Build.props index f69ba615..7e51d15e 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -4,7 +4,7 @@ - 1.7.1 + 1.7.2-preview-01 WireMock.Net-Logo.png https://github.com/WireMock-Net/WireMock.Net Apache-2.0 diff --git a/src/WireMock.Net.RestClient/IWireMockAdminApi.cs b/src/WireMock.Net.RestClient/IWireMockAdminApi.cs index fc4e4d72..5cd33546 100644 --- a/src/WireMock.Net.RestClient/IWireMockAdminApi.cs +++ b/src/WireMock.Net.RestClient/IWireMockAdminApi.cs @@ -311,6 +311,15 @@ public interface IWireMockAdminApi [Delete("files/{filename}")] Task DeleteFileAsync([Path] string filename, CancellationToken cancellationToken = default); + /// + /// Add a Grpc ProtoDefinition at server-level. + /// + /// Unique identifier for the ProtoDefinition. + /// The ProtoDefinition as text. + /// The optional cancellationToken. + [Post("protodefinitions/{id}")] + Task AddProtoDefinitionAsync([Path] string id, [Body] string body, CancellationToken cancellationToken = default); + /// /// Check if a file exists /// diff --git a/src/WireMock.Net.Testcontainers/WireMockConfiguration.cs b/src/WireMock.Net.Testcontainers/WireMockConfiguration.cs index 159e9901..f7260670 100644 --- a/src/WireMock.Net.Testcontainers/WireMockConfiguration.cs +++ b/src/WireMock.Net.Testcontainers/WireMockConfiguration.cs @@ -6,6 +6,7 @@ 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 AdditionalUrls { get; private set; } = []; + public Dictionary ProtoDefinitions { get; set; } = new(); + public WireMockConfiguration(string? username = null, string? password = null) { Username = username; @@ -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); } /// @@ -107,7 +111,35 @@ public WireMockConfiguration WithWatchStaticMappings(bool includeSubDirectories) /// public WireMockConfiguration WithAdditionalUrl(string url) { - AdditionalUrls.Add(url); + AdditionalUrls.Add(Guard.NotNullOrWhiteSpace(url)); + return this; + } + + /// + /// Add a Grpc ProtoDefinition at server-level. + /// + /// Unique identifier for the ProtoDefinition. + /// The ProtoDefinition as text. + /// + public WireMockConfiguration AddProtoDefinition(string id, params string[] protoDefinition) + { + Guard.NotNullOrWhiteSpace(id); + Guard.NotNullOrEmpty(protoDefinition); + + ProtoDefinitions[id] = protoDefinition; + return this; } + + private static List Combine(List oldValue, List newValue) + { + return oldValue.Concat(newValue).ToList(); + } + + private static Dictionary Combine(Dictionary oldValue, Dictionary newValue) + { + return newValue + .Concat(oldValue.Where(item => !newValue.Keys.Contains(item.Key))) + .ToDictionary(item => item.Key, item => item.Value); + } } \ No newline at end of file diff --git a/src/WireMock.Net.Testcontainers/WireMockContainer.cs b/src/WireMock.Net.Testcontainers/WireMockContainer.cs index b03a10bb..f87c99be 100644 --- a/src/WireMock.Net.Testcontainers/WireMockContainer.cs +++ b/src/WireMock.Net.Testcontainers/WireMockContainer.cs @@ -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; @@ -40,9 +41,9 @@ public sealed class WireMockContainer : DockerContainer /// The container configuration. 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); } /// @@ -175,8 +176,6 @@ protected override ValueTask DisposeAsyncCore() _enhancedFileSystemWatcher = null; } - Started -= WireMockContainer_Started; - return base.DisposeAsyncCore(); } @@ -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; @@ -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 diff --git a/src/WireMock.Net.Testcontainers/WireMockContainerBuilder.cs b/src/WireMock.Net.Testcontainers/WireMockContainerBuilder.cs index ea2e9074..ef634ff9 100644 --- a/src/WireMock.Net.Testcontainers/WireMockContainerBuilder.cs +++ b/src/WireMock.Net.Testcontainers/WireMockContainerBuilder.cs @@ -164,6 +164,23 @@ public WireMockContainerBuilder AddUrl(string 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"); diff --git a/src/WireMock.Net/Server/WireMockServer.Admin.cs b/src/WireMock.Net/Server/WireMockServer.Admin.cs index e79ff52d..6b6bea8c 100644 --- a/src/WireMock.Net/Server/WireMockServer.Admin.cs +++ b/src/WireMock.Net/Server/WireMockServer.Admin.cs @@ -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 @@ -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 @@ -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"); @@ -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(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(values.FirstOrDefault(), true, out var parsed)) { return parsed; } - return MappingConverterType.Server; + return defaultValue; } private IMapping? FindMappingByGuid(IRequestMessage requestMessage) @@ -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); diff --git a/src/WireMock.Net/Server/WireMockServer.AdminFiles.cs b/src/WireMock.Net/Server/WireMockServer.AdminFiles.cs index 7d2ccb3b..2629fbb1 100644 --- a/src/WireMock.Net/Server/WireMockServer.AdminFiles.cs +++ b/src/WireMock.Net/Server/WireMockServer.AdminFiles.cs @@ -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) { diff --git a/src/WireMock.Net/Server/WireMockServer.cs b/src/WireMock.Net/Server/WireMockServer.cs index 1d91f490..c99d6ab5 100644 --- a/src/WireMock.Net/Server/WireMockServer.cs +++ b/src/WireMock.Net/Server/WireMockServer.cs @@ -602,7 +602,14 @@ public WireMockServer AddProtoDefinition(string id, params string[] protoDefinit _settings.ProtoDefinitions ??= new Dictionary(); - _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; } diff --git a/test/WireMock.Net.Tests/Constants.cs b/test/WireMock.Net.Tests/Constants.cs new file mode 100644 index 00000000..a7b36152 --- /dev/null +++ b/test/WireMock.Net.Tests/Constants.cs @@ -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; +} \ No newline at end of file diff --git a/test/WireMock.Net.Tests/Testcontainers/TestcontainersTests.Grpc.cs b/test/WireMock.Net.Tests/Testcontainers/TestcontainersTests.Grpc.cs index 365ccc58..f1b7cc10 100644 --- a/test/WireMock.Net.Tests/Testcontainers/TestcontainersTests.Grpc.cs +++ b/test/WireMock.Net.Tests/Testcontainers/TestcontainersTests.Grpc.cs @@ -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; @@ -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); @@ -140,12 +139,40 @@ public async Task WireMockContainer_Build_Grpc_ProtoDefinitionFromJson_UsingGrpc await wireMockContainer.StopAsync(); } - private static async Task 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 Given_WireMockContainerIsStartedForHttpAndGrpcAsync() + { + var wireMockContainer = new WireMockContainerBuilder() + .WithAutoRemove(true) + .WithCleanUp(true) + .AddUrl("grpc://*:9090") + .Build(); + + await wireMockContainer.StartAsync(); + + return wireMockContainer; + } + + private static async Task Given_WireMockContainerWithProtoDefinitionAtServerLevelIsStartedForHttpAndGrpcAsync() { var wireMockContainer = new WireMockContainerBuilder() .WithAutoRemove(true) .WithCleanUp(true) .AddUrl("grpc://*:9090") + .AddProtoDefinition("my-greeter", ReadFile("greet.proto")) .Build(); await wireMockContainer.StartAsync(); @@ -153,9 +180,9 @@ private static async Task Given_WireMockContainerIsStartedFor 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(); @@ -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)); } diff --git a/test/WireMock.Net.Tests/WireMock.Net.Tests.csproj b/test/WireMock.Net.Tests/WireMock.Net.Tests.csproj index 038dafaf..8aa2e242 100644 --- a/test/WireMock.Net.Tests/WireMock.Net.Tests.csproj +++ b/test/WireMock.Net.Tests/WireMock.Net.Tests.csproj @@ -154,6 +154,9 @@ PreserveNewest + + PreserveNewest + PreserveNewest diff --git a/test/WireMock.Net.Tests/WireMockServer.Admin.cs b/test/WireMock.Net.Tests/WireMockServer.Admin.cs index 684633f9..4a26a7bf 100644 --- a/test/WireMock.Net.Tests/WireMockServer.Admin.cs +++ b/test/WireMock.Net.Tests/WireMockServer.Admin.cs @@ -26,8 +26,6 @@ namespace WireMock.Net.Tests; public class WireMockServerAdminTests { - private const int NumStaticMappings = 10; - private static string GetCurrentFolder() { return Directory.GetCurrentDirectory(); @@ -40,8 +38,8 @@ public void WireMockServer_Admin_ResetMappings() string folder = Path.Combine(GetCurrentFolder(), "__admin", "mappings"); server.ReadStaticMappings(folder); - Check.That(server.Mappings).HasSize(NumStaticMappings); - Check.That(server.MappingModels).HasSize(NumStaticMappings); + Check.That(server.Mappings).HasSize(Constants.NumStaticMappings); + Check.That(server.MappingModels).HasSize(Constants.NumStaticMappings); // Act server.ResetMappings(); @@ -220,7 +218,7 @@ public void WireMockServer_Admin_ReadStaticMappings() server.ReadStaticMappings(folder); var mappings = server.Mappings.ToArray(); - Check.That(mappings).HasSize(NumStaticMappings); + Check.That(mappings).HasSize(Constants.NumStaticMappings); server.Stop(); } diff --git a/test/WireMock.Net.Tests/WireMockServer.Proxy.cs b/test/WireMock.Net.Tests/WireMockServer.Proxy.cs index 0513983a..f2564c2e 100644 --- a/test/WireMock.Net.Tests/WireMockServer.Proxy.cs +++ b/test/WireMock.Net.Tests/WireMockServer.Proxy.cs @@ -119,7 +119,7 @@ public async Task WireMockServer_Proxy_AdminTrue_With_SaveMapping_Is_True_And_Sa } // Assert - server.Mappings.Should().HaveCount(37); + server.Mappings.Should().HaveCount(Constants.NumAdminMappings + 2); } [Fact] diff --git a/test/WireMock.Net.Tests/WireMockServer.Settings.cs b/test/WireMock.Net.Tests/WireMockServer.Settings.cs index f6a8ab96..91defc85 100644 --- a/test/WireMock.Net.Tests/WireMockServer.Settings.cs +++ b/test/WireMock.Net.Tests/WireMockServer.Settings.cs @@ -75,8 +75,6 @@ public void WireMockServer_WireMockServerSettings_StartAdminInterfaceFalse_Basic [Fact] public void WireMockServer_WireMockServerSettings_PriorityFromAllAdminMappingsIsLow_When_StartAdminInterface_IsTrue() { - const int count = 35; - // Assign and Act var server = WireMockServer.Start(new WireMockServerSettings { @@ -85,15 +83,13 @@ public void WireMockServer_WireMockServerSettings_PriorityFromAllAdminMappingsIs // Assert server.Mappings.Should().NotBeNull(); - server.Mappings.Should().HaveCount(count); + server.Mappings.Should().HaveCount(Constants.NumAdminMappings); server.Mappings.All(m => m.Priority == WireMockConstants.AdminPriority).Should().BeTrue(); } [Fact] public void WireMockServer_WireMockServerSettings_ProxyAndRecordSettings_ProxyPriority_IsMinus2000000_When_StartAdminInterface_IsTrue() { - const int count = 36; - // Assign and Act var server = WireMockServer.Start(new WireMockServerSettings { @@ -106,9 +102,9 @@ public void WireMockServer_WireMockServerSettings_ProxyAndRecordSettings_ProxyPr // Assert server.Mappings.Should().NotBeNull(); - server.Mappings.Should().HaveCount(count); + server.Mappings.Should().HaveCount(Constants.NumAdminMappings + 1); - server.Mappings.Count(m => m.Priority == WireMockConstants.AdminPriority).Should().Be(count - 1); + server.Mappings.Count(m => m.Priority == WireMockConstants.AdminPriority).Should().Be(Constants.NumAdminMappings); server.Mappings.Count(m => m.Priority == WireMockConstants.ProxyPriority).Should().Be(1); } diff --git a/test/WireMock.Net.Tests/__admin/mappings/greet.proto b/test/WireMock.Net.Tests/__admin/mappings/greet.proto new file mode 100644 index 00000000..f4e1ead0 --- /dev/null +++ b/test/WireMock.Net.Tests/__admin/mappings/greet.proto @@ -0,0 +1,21 @@ +syntax = "proto3"; + +package greet; + +service Greeter { + rpc SayHello (HelloRequest) returns (HelloReply); +} + +message HelloRequest { + string name = 1; +} + +message HelloReply { + string message = 1; + enum PhoneType { + none = 0; + mobile = 1; + home = 2; + } + PhoneType phoneType = 2; +} \ No newline at end of file diff --git a/test/WireMock.Net.Tests/__admin/mappings/protobuf-mapping-2.json b/test/WireMock.Net.Tests/__admin/mappings/protobuf-mapping-2.json index 41f1e95e..68ec82ff 100644 --- a/test/WireMock.Net.Tests/__admin/mappings/protobuf-mapping-2.json +++ b/test/WireMock.Net.Tests/__admin/mappings/protobuf-mapping-2.json @@ -31,7 +31,7 @@ }, "Response": { "BodyAsJson": { - "message": "hello {{request.BodyAsJson.name}}" + "message": "hello {{request.BodyAsJson.name}} {{request.method}}" }, "UseTransformer": true, "TransformerType": "Handlebars", diff --git a/test/WireMock.Net.Tests/__admin/mappings/protobuf-mapping-4.json b/test/WireMock.Net.Tests/__admin/mappings/protobuf-mapping-4.json index a56d4354..930dc941 100644 --- a/test/WireMock.Net.Tests/__admin/mappings/protobuf-mapping-4.json +++ b/test/WireMock.Net.Tests/__admin/mappings/protobuf-mapping-4.json @@ -23,7 +23,7 @@ }, "Response": { "BodyAsJson": { - "message": "hello {{request.BodyAsJson.name}}" + "message": "hello {{request.BodyAsJson.name}} {{request.method}}" }, "UseTransformer": true, "TransformerType": "Handlebars",