From 6ac95cf57d6ab8877392adaa05b181e9558f9413 Mon Sep 17 00:00:00 2001 From: Stef Heyenrath Date: Fri, 16 Feb 2024 17:16:51 +0100 Subject: [PATCH] Add Grpc ProtoBuf support (request-response) (#1047) * ProtoBuf * . * x * --- * x * fx * ... * sc * ... * . * groen * x * fix tests * ok!? * fix tests * fix tests * ! * x * 6 * . * x * ivaluematcher * transformer * . * sc * . * mapping * x * tra * com * ... * . * . * . * AddProtoDefinition * . * set * grpahj * . * . * IdOrText * ... * async * async2 * . * t * nuget * * http version * tests * .WithHttpVersion("2") * * HttpVersionParser --- WireMock.Net Solution.sln | 7 + WireMock.Net Solution.sln.DotSettings | 2 + .../Program.cs | 21 + .../WireMock.Net.Console.GrpcClient.csproj | 23 + .../greet.proto | 33 + .../WireMock.Net.Console.NET8.csproj | 2 +- .../MainApp.cs | 110 +- .../Admin/Mappings/MappingModel.cs | 5 + .../Admin/Mappings/MatcherModel.cs | 11 +- .../Admin/Mappings/RequestModel.cs | 5 + .../Admin/Mappings/ResponseModel.cs | 19 +- .../Admin/Requests/LogRequestModel.cs | 5 + .../Admin/Settings/SettingsModel.cs | 7 + .../IRequestMessage.cs | 17 +- .../IResponseMessage.cs | 23 +- .../Models/IBodyData.cs | 18 +- .../Models/IdOrText.cs | 33 + .../Types/BodyType.cs | 7 +- .../WireMock.Net.Abstractions.csproj | 2 +- .../Assertions/WireMockAssertions.WithBody.cs | 12 +- .../Matchers/CSharpCodeMatcher.cs | 56 +- .../WireMock.Net.Matchers.CSharpCode.csproj | 2 +- src/WireMock.Net/IMapping.cs | 51 +- src/WireMock.Net/Mapping.cs | 38 +- src/WireMock.Net/MappingBuilder.cs | 11 + src/WireMock.Net/Matchers/ExactMatcher.cs | 9 + .../Matchers/ExactObjectMatcher.cs | 25 +- src/WireMock.Net/Matchers/IBytesMatcher.cs | 18 + src/WireMock.Net/Matchers/IDecodeMatcher.cs | 18 + src/WireMock.Net/Matchers/IJsonMatcher.cs | 9 + src/WireMock.Net/Matchers/IObjectMatcher.cs | 6 + src/WireMock.Net/Matchers/IProtoBufMatcher.cs | 8 + src/WireMock.Net/Matchers/IValueMatcher.cs | 14 - src/WireMock.Net/Matchers/JSONPathMatcher.cs | 8 +- src/WireMock.Net/Matchers/JmesPathMatcher.cs | 4 + src/WireMock.Net/Matchers/JsonMatcher.cs | 30 +- src/WireMock.Net/Matchers/LinqMatcher.cs | 4 + .../Matchers/MatchBehaviourHelper.cs | 7 + .../Matchers/NotNullOrEmptyMatcher.cs | 4 + src/WireMock.Net/Matchers/ProtoBufMatcher.cs | 114 + .../Request/RequestMessageCompositeMatcher.cs | 4 +- .../Request/RequestMessageGraphQLMatcher.cs | 7 +- .../RequestMessageHttpVersionMatcher.cs | 86 + .../Request/RequestMessageProtoBufMatcher.cs | 43 + src/WireMock.Net/Models/BodyData.cs | 31 +- .../Models/GraphQLSchemaDetails.cs | 61 + .../Owin/AspNetCoreSelfHost.NETStandard.cs | 12 + src/WireMock.Net/Owin/AspNetCoreSelfHost.cs | 10 +- src/WireMock.Net/Owin/HostUrlDetails.cs | 2 + src/WireMock.Net/Owin/HostUrlOptions.cs | 14 +- .../Owin/Mappers/IOwinResponseMapper.cs | 23 +- .../Owin/Mappers/OwinRequestMapper.cs | 147 +- .../Owin/Mappers/OwinResponseMapper.cs | 57 +- .../RequestBuilders/IBodyRequestBuilder.cs | 2 +- .../RequestBuilders/IGraphQLRequestBuilder.cs | 34 + .../RequestBuilders/IHttpVersionBuilder.cs | 18 + .../IMultiPartRequestBuilder.cs | 2 +- .../IProtoBufRequestBuilder.cs | 45 + .../Request.WithBodyAsProtoBuf.cs | 31 + .../RequestBuilders/Request.WithGraphQL.cs | 39 - .../Request.WithGraphQLSchema.cs | 59 + .../Request.WithHttpVersion.cs | 13 + src/WireMock.Net/RequestBuilders/Request.cs | 16 + src/WireMock.Net/RequestMessage.cs | 15 +- .../ResponseBuilders/IBodyResponseBuilder.cs | 40 +- .../IHeadersResponseBuilder.cs | 37 +- .../ResponseBuilders/Response.WithBody.cs | 65 +- .../ResponseBuilders/Response.WithHeaders.cs | 101 + src/WireMock.Net/ResponseBuilders/Response.cs | 68 +- src/WireMock.Net/ResponseMessage.cs | 31 +- .../DynamicAsyncResponseProvider.cs | 3 +- .../DynamicResponseProvider.cs | 3 +- .../ProxyAsyncResponseProvider.cs | 5 +- .../Serialization/LogEntryMapper.cs | 1 + .../Serialization/MappingConverter.cs | 162 +- .../Serialization/MatcherMapper.cs | 94 +- .../Serialization/ProxyMappingConverter.cs | 14 +- .../Server/IRespondWithAProvider.cs | 17 + .../Server/RespondWithAProvider.cs | 72 +- .../Server/WireMockServer.Admin.cs | 3 + .../Server/WireMockServer.ConvertMapping.cs | 5 + src/WireMock.Net/Server/WireMockServer.cs | 82 +- .../Settings/SimpleSettingsParser.cs | 7 + .../Settings/WireMockServerSettings.cs | 19 + .../Settings/WireMockServerSettingsParser.cs | 14 +- src/WireMock.Net/Transformers/Transformer.cs | 21 +- src/WireMock.Net/Util/BodyParser.cs | 12 +- src/WireMock.Net/Util/HttpVersionParser.cs | 24 + src/WireMock.Net/Util/JsonUtils.cs | 20 + src/WireMock.Net/Util/PortUtils.cs | 8 +- src/WireMock.Net/Util/ProtoBufUtils.cs | 41 + src/WireMock.Net/Util/SingletonFactory.cs | 24 + src/WireMock.Net/WireMock.Net.csproj | 12 +- .../WireMockAdminApiTests.GetMappingsAsync.cs | 128 ++ ...ouldReturnCorrectMappingModel.verified.txt | 0 ...y_And_ProxyUrlReplaceSettings.verified.txt | 0 ...dminApi_GetMappingByGuidAsync.verified.txt | 0 ...Api_GetMappingCodeByGuidAsync.verified.txt | 0 ...uldReturnCorrectMappingModels.verified.txt | 235 ++ ...eMockAdminApi_GetMappingsCode.verified.txt | 0 .../{ => AdminApi}/WireMockAdminApiTests.cs | 2037 +++++++++-------- .../Grpc/WireMockServerTests.Grpc.cs | 205 ++ test/WireMock.Net.Tests/Grpc/greet.proto | 33 + .../Matchers/GraphQLMatcherTests.cs | 1 - .../Matchers/ProtoBufMatcherTests.cs | 108 + .../Owin/GlobalExceptionMiddlewareTests.cs | 2 +- .../Owin/Mappers/OwinResponseMapperTests.cs | 2 +- .../Owin/WireMockMiddlewareTests.cs | 6 +- .../RequestBuilderWithProtoBufTests.cs | 65 + ..._LogEntry_Check_BodyTypeBytes.verified.txt | 1 + ...ry_Check_ResponseBodyTypeFile.verified.txt | 3 +- ...Should_NotSave_StringResponse.verified.txt | 1 + ...Mapper_Map_LogEntry_WithFault.verified.txt | 3 +- .../MappingConverterTests.ToCSharpCode.cs | 10 +- ...sProtoBuf_ReturnsCorrectModel.verified.txt | 61 + ...sProtoBuf_ReturnsCorrectModel.verified.txt | 53 + ...d_Cookie_ReturnsCorrectModel.verified.txt} | 0 ...sProtoBuf_ReturnsCorrectModel.verified.txt | 35 + ...ithHeader_ReturnsCorrectModel.verified.txt | 14 + ...thHeaders_ReturnsCorrectModel.verified.txt | 14 + ...ingHeader_ReturnsCorrectModel.verified.txt | 15 + ...ngHeaders_ReturnsCorrectModel.verified.txt | 14 + .../Serialization/MappingConverterTests.cs | 233 +- .../Serialization/MatcherMapperTests.cs | 150 +- .../Settings/SimpleSettingsParserTests.cs | 18 + .../Util/HttpVersionParserTests.cs | 39 + .../WireMock.Net.Tests/Util/PortUtilsTests.cs | 44 +- .../WireMock.Net.Tests.csproj | 20 +- .../WireMock.Net.Tests/WireMockServerTests.cs | 7 +- 129 files changed, 4580 insertions(+), 1551 deletions(-) create mode 100644 examples/WireMock.Net.Console.GrpcClient/Program.cs create mode 100644 examples/WireMock.Net.Console.GrpcClient/WireMock.Net.Console.GrpcClient.csproj create mode 100644 examples/WireMock.Net.Console.GrpcClient/greet.proto create mode 100644 src/WireMock.Net.Abstractions/Models/IdOrText.cs create mode 100644 src/WireMock.Net/Matchers/IBytesMatcher.cs create mode 100644 src/WireMock.Net/Matchers/IDecodeMatcher.cs create mode 100644 src/WireMock.Net/Matchers/IJsonMatcher.cs create mode 100644 src/WireMock.Net/Matchers/IProtoBufMatcher.cs delete mode 100644 src/WireMock.Net/Matchers/IValueMatcher.cs create mode 100644 src/WireMock.Net/Matchers/ProtoBufMatcher.cs create mode 100644 src/WireMock.Net/Matchers/Request/RequestMessageHttpVersionMatcher.cs create mode 100644 src/WireMock.Net/Matchers/Request/RequestMessageProtoBufMatcher.cs create mode 100644 src/WireMock.Net/Models/GraphQLSchemaDetails.cs create mode 100644 src/WireMock.Net/RequestBuilders/IHttpVersionBuilder.cs create mode 100644 src/WireMock.Net/RequestBuilders/IProtoBufRequestBuilder.cs create mode 100644 src/WireMock.Net/RequestBuilders/Request.WithBodyAsProtoBuf.cs delete mode 100644 src/WireMock.Net/RequestBuilders/Request.WithGraphQL.cs create mode 100644 src/WireMock.Net/RequestBuilders/Request.WithGraphQLSchema.cs create mode 100644 src/WireMock.Net/RequestBuilders/Request.WithHttpVersion.cs create mode 100644 src/WireMock.Net/ResponseBuilders/Response.WithHeaders.cs create mode 100644 src/WireMock.Net/Util/HttpVersionParser.cs create mode 100644 src/WireMock.Net/Util/ProtoBufUtils.cs create mode 100644 src/WireMock.Net/Util/SingletonFactory.cs create mode 100644 test/WireMock.Net.Tests/AdminApi/WireMockAdminApiTests.GetMappingsAsync.cs rename test/WireMock.Net.Tests/{ => AdminApi}/WireMockAdminApiTests.IWireMockAdminApi_GetMappingAsync_WithBodyModelMatcherModel_WithoutMethods_ShouldReturnCorrectMappingModel.verified.txt (100%) rename test/WireMock.Net.Tests/{ => AdminApi}/WireMockAdminApiTests.IWireMockAdminApi_GetMappingAsync_WithProxy_And_ProxyUrlReplaceSettings.verified.txt (100%) rename test/WireMock.Net.Tests/{ => AdminApi}/WireMockAdminApiTests.IWireMockAdminApi_GetMappingByGuidAsync.verified.txt (100%) rename test/WireMock.Net.Tests/{ => AdminApi}/WireMockAdminApiTests.IWireMockAdminApi_GetMappingCodeByGuidAsync.verified.txt (100%) create mode 100644 test/WireMock.Net.Tests/AdminApi/WireMockAdminApiTests.IWireMockAdminApi_GetMappingsAsync_WithBodyAsProtoBuf_ShouldReturnCorrectMappingModels.verified.txt rename test/WireMock.Net.Tests/{ => AdminApi}/WireMockAdminApiTests.IWireMockAdminApi_GetMappingsCode.verified.txt (100%) rename test/WireMock.Net.Tests/{ => AdminApi}/WireMockAdminApiTests.cs (97%) create mode 100644 test/WireMock.Net.Tests/Grpc/WireMockServerTests.Grpc.cs create mode 100644 test/WireMock.Net.Tests/Grpc/greet.proto create mode 100644 test/WireMock.Net.Tests/Matchers/ProtoBufMatcherTests.cs create mode 100644 test/WireMock.Net.Tests/RequestBuilders/RequestBuilderWithProtoBufTests.cs create mode 100644 test/WireMock.Net.Tests/Serialization/MappingConverterTests.ToMappingModel_Mapping_WithBodyAsProtoBuf_ReturnsCorrectModel.verified.txt create mode 100644 test/WireMock.Net.Tests/Serialization/MappingConverterTests.ToMappingModel_Request_WithBodyAsProtoBuf_ReturnsCorrectModel.verified.txt rename test/WireMock.Net.Tests/Serialization/{MappingConverterTests.ToMappingModel_WithHeader_And_Cookie_ReturnsCorrectModel.verified.txt => MappingConverterTests.ToMappingModel_Request_WithHeader_And_Cookie_ReturnsCorrectModel.verified.txt} (100%) create mode 100644 test/WireMock.Net.Tests/Serialization/MappingConverterTests.ToMappingModel_Response_WithBodyAsProtoBuf_ReturnsCorrectModel.verified.txt create mode 100644 test/WireMock.Net.Tests/Serialization/MappingConverterTests.ToMappingModel_Response_WithHeader_ReturnsCorrectModel.verified.txt create mode 100644 test/WireMock.Net.Tests/Serialization/MappingConverterTests.ToMappingModel_Response_WithHeaders_ReturnsCorrectModel.verified.txt create mode 100644 test/WireMock.Net.Tests/Serialization/MappingConverterTests.ToMappingModel_Response_WithTrailingHeader_ReturnsCorrectModel.verified.txt create mode 100644 test/WireMock.Net.Tests/Serialization/MappingConverterTests.ToMappingModel_Response_WithTrailingHeaders_ReturnsCorrectModel.verified.txt create mode 100644 test/WireMock.Net.Tests/Util/HttpVersionParserTests.cs diff --git a/WireMock.Net Solution.sln b/WireMock.Net Solution.sln index 8fabcff04..7bc052d01 100644 --- a/WireMock.Net Solution.sln +++ b/WireMock.Net Solution.sln @@ -112,6 +112,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WireMock.Net.Console.NET8", EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WireMockAzureQueueProxy", "examples\WireMockAzureQueueProxy\WireMockAzureQueueProxy.csproj", "{7FC0B409-2682-40EE-B3B9-3930D6769D01}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WireMock.Net.Console.GrpcClient", "examples\WireMock.Net.Console.GrpcClient\WireMock.Net.Console.GrpcClient.csproj", "{B1580A38-84E7-44BE-8FE7-3EE5031D74A1}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -262,6 +264,10 @@ Global {7FC0B409-2682-40EE-B3B9-3930D6769D01}.Debug|Any CPU.Build.0 = Debug|Any CPU {7FC0B409-2682-40EE-B3B9-3930D6769D01}.Release|Any CPU.ActiveCfg = Release|Any CPU {7FC0B409-2682-40EE-B3B9-3930D6769D01}.Release|Any CPU.Build.0 = Release|Any CPU + {B1580A38-84E7-44BE-8FE7-3EE5031D74A1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B1580A38-84E7-44BE-8FE7-3EE5031D74A1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B1580A38-84E7-44BE-8FE7-3EE5031D74A1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B1580A38-84E7-44BE-8FE7-3EE5031D74A1}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -305,6 +311,7 @@ Global {941229D6-191B-4B5E-AC81-0905EBF4F19D} = {985E0ADB-D4B4-473A-AA40-567E279B7946} {1EA72C0F-92E9-486B-8FFE-53F992BFC4AA} = {985E0ADB-D4B4-473A-AA40-567E279B7946} {7FC0B409-2682-40EE-B3B9-3930D6769D01} = {985E0ADB-D4B4-473A-AA40-567E279B7946} + {B1580A38-84E7-44BE-8FE7-3EE5031D74A1} = {985E0ADB-D4B4-473A-AA40-567E279B7946} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {DC539027-9852-430C-B19F-FD035D018458} diff --git a/WireMock.Net Solution.sln.DotSettings b/WireMock.Net Solution.sln.DotSettings index a02612291..ace990494 100644 --- a/WireMock.Net Solution.sln.DotSettings +++ b/WireMock.Net Solution.sln.DotSettings @@ -25,12 +25,14 @@ XUA True True + True True True True True True True + True True True True diff --git a/examples/WireMock.Net.Console.GrpcClient/Program.cs b/examples/WireMock.Net.Console.GrpcClient/Program.cs new file mode 100644 index 000000000..37b3df497 --- /dev/null +++ b/examples/WireMock.Net.Console.GrpcClient/Program.cs @@ -0,0 +1,21 @@ +using Greet; +using Grpc.Net.Client; + +namespace WireMock.Net.Console.GrpcClient; + +internal class Program +{ + static async Task Main(string[] args) + { + var channel = GrpcChannel.ForAddress("http://localhost:9093/grpc3", new GrpcChannelOptions + { + Credentials = Grpc.Core.ChannelCredentials.Insecure + }); + + var client = new Greeter.GreeterClient(channel); + + var reply = await client.SayHelloAsync(new HelloRequest { Name = "stef" }); + + System.Console.WriteLine("Greeting: " + reply.Message); + } +} \ No newline at end of file diff --git a/examples/WireMock.Net.Console.GrpcClient/WireMock.Net.Console.GrpcClient.csproj b/examples/WireMock.Net.Console.GrpcClient/WireMock.Net.Console.GrpcClient.csproj new file mode 100644 index 000000000..c87f3b10d --- /dev/null +++ b/examples/WireMock.Net.Console.GrpcClient/WireMock.Net.Console.GrpcClient.csproj @@ -0,0 +1,23 @@ + + + + Exe + net8.0 + enable + enable + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + diff --git a/examples/WireMock.Net.Console.GrpcClient/greet.proto b/examples/WireMock.Net.Console.GrpcClient/greet.proto new file mode 100644 index 000000000..6f9e10fa5 --- /dev/null +++ b/examples/WireMock.Net.Console.GrpcClient/greet.proto @@ -0,0 +1,33 @@ +// Copyright 2019 The gRPC Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto3"; + +package greet; + +// The greeting service definition. +service Greeter { + // Sends a greeting + rpc SayHello (HelloRequest) returns (HelloReply); +} + +// The request message containing the user's name. +message HelloRequest { + string name = 1; +} + +// The response message containing the greetings +message HelloReply { + string message = 1; +} \ No newline at end of file diff --git a/examples/WireMock.Net.Console.NET8/WireMock.Net.Console.NET8.csproj b/examples/WireMock.Net.Console.NET8/WireMock.Net.Console.NET8.csproj index 071afcc25..ff5b2a2fa 100644 --- a/examples/WireMock.Net.Console.NET8/WireMock.Net.Console.NET8.csproj +++ b/examples/WireMock.Net.Console.NET8/WireMock.Net.Console.NET8.csproj @@ -3,7 +3,7 @@ Exe net8.0 - $(DefineConstants);GRAPHQL;MIMEKIT + $(DefineConstants);GRAPHQL;MIMEKIT;PROTOBUF diff --git a/examples/WireMock.Net.Console.Net452.Classic/MainApp.cs b/examples/WireMock.Net.Console.Net452.Classic/MainApp.cs index f5949bc6e..14fa5e1a6 100644 --- a/examples/WireMock.Net.Console.Net452.Classic/MainApp.cs +++ b/examples/WireMock.Net.Console.Net452.Classic/MainApp.cs @@ -42,6 +42,24 @@ public class Todo public static class MainApp { + private const string ProtoDefinition = @" +syntax = ""proto3""; + +package greet; + +service Greeter { + rpc SayHello (HelloRequest) returns (HelloReply); +} + +message HelloRequest { + string name = 1; +} + +message HelloReply { + string message = 1; +} +"; + private const string TestSchema = @" scalar DateTime scalar MyCustomScalar @@ -115,17 +133,14 @@ public static void Run() .WithBodyAsJson(rm => todos[int.Parse(rm.Query!["id"].ToString())]) ); - var httpClient = server.CreateClient(); - //server.Stop(); - - var httpAndHttpsWithPort = WireMockServer.Start(new WireMockServerSettings + using var httpAndHttpsWithPort = WireMockServer.Start(new WireMockServerSettings { HostingScheme = HostingScheme.HttpAndHttps, Port = 12399 }); httpAndHttpsWithPort.Stop(); - var httpAndHttpsFree = WireMockServer.Start(new WireMockServerSettings + using var httpAndHttpsFree = WireMockServer.Start(new WireMockServerSettings { HostingScheme = HostingScheme.HttpAndHttps }); @@ -134,11 +149,14 @@ public static void Run() string url1 = "http://localhost:9091/"; string url2 = "http://localhost:9092/"; string url3 = "https://localhost:9443/"; + string urlGrpc = "grpc://localhost:9093/"; + string urlGrpcSSL = "grpcs://localhost:9094/"; server = WireMockServer.Start(new WireMockServerSettings { + // CorsPolicyOptions = CorsPolicyOptions.AllowAll, AllowCSharpCodeMatcher = true, - Urls = new[] { url1, url2, url3 }, + Urls = new[] { url1, url2, url3, urlGrpc, urlGrpcSSL }, StartAdminInterface = true, ReadStaticMappings = true, SaveUnmatchedRequests = true, @@ -171,17 +189,91 @@ public static void Run() //server.SetAzureADAuthentication("6c2a4722-f3b9-4970-b8fc-fac41e29stef", "8587fde1-7824-42c7-8592-faf92b04stef"); // server.AllowPartialMapping(); + +#if PROTOBUF + var protoBufJsonMatcher = new JsonPartialWildcardMatcher(new { name = "*" }); + server + .Given(Request.Create() + .UsingPost() + .WithHttpVersion("2") + .WithPath("/grpc/greet.Greeter/SayHello") + .WithBodyAsProtoBuf(ProtoDefinition, "greet.HelloRequest", protoBufJsonMatcher) + ) + .RespondWith(Response.Create() + .WithHeader("Content-Type", "application/grpc") + .WithBodyAsProtoBuf(ProtoDefinition, "greet.HelloReply", + new + { + message = "hello {{request.BodyAsJson.name}}" + } + ) + .WithTrailingHeader("grpc-status", "0") + .WithTransformer() + ); + + server + .Given(Request.Create() + .UsingPost() + .WithHttpVersion("2") + .WithPath("/grpc2/greet.Greeter/SayHello") + .WithBodyAsProtoBuf("greet.HelloRequest", protoBufJsonMatcher) + ) + .WithProtoDefinition(ProtoDefinition) + .RespondWith(Response.Create() + .WithHeader("Content-Type", "application/grpc") + .WithBodyAsProtoBuf("greet.HelloReply", + new + { + message = "hello {{request.BodyAsJson.name}}" + } + ) + .WithTrailingHeader("grpc-status", "0") + .WithTransformer() + ); + + server + .AddProtoDefinition("my-greeter", ProtoDefinition) + .Given(Request.Create() + .UsingPost() + .WithPath("/grpc3/greet.Greeter/SayHello") + .WithBodyAsProtoBuf("greet.HelloRequest", protoBufJsonMatcher) + ) + .WithProtoDefinition("my-greeter") + .RespondWith(Response.Create() + .WithHeader("Content-Type", "application/grpc") + .WithBodyAsProtoBuf("greet.HelloReply", + new + { + message = "hello {{request.BodyAsJson.name}}" + } + ) + .WithTrailingHeader("grpc-status", "0") + .WithTransformer() + ); +#endif + #if GRAPHQL var customScalars = new Dictionary { { "MyCustomScalar", typeof(int) } }; server .Given(Request.Create() .WithPath("/graphql") .UsingPost() - .WithGraphQLSchema(TestSchema, customScalars) + .WithBodyAsGraphQL(TestSchema, customScalars) ) .RespondWith(Response.Create() .WithBody("GraphQL is ok") ); + + //server + // .AddGraphQLSchema("my-graphql", TestSchema, customScalars) + // .Given(Request.Create() + // .WithPath("/graphql2") + // .UsingPost() + // ) + // .WithGraphQLSchema("my-graphql") + // .RespondWith(Response.Create() + // .WithBody("GraphQL is ok") + // ); #endif #if MIMEKIT @@ -336,8 +428,8 @@ public static void Run() Url = "http://localhost:9999", ReplaceSettings = new ProxyUrlReplaceSettings { - OldValue = "old", - NewValue = "new" + OldValue = "old", + NewValue = "new" } }) ); diff --git a/src/WireMock.Net.Abstractions/Admin/Mappings/MappingModel.cs b/src/WireMock.Net.Abstractions/Admin/Mappings/MappingModel.cs index 9f6061a23..e570065ce 100644 --- a/src/WireMock.Net.Abstractions/Admin/Mappings/MappingModel.cs +++ b/src/WireMock.Net.Abstractions/Admin/Mappings/MappingModel.cs @@ -98,4 +98,9 @@ public class MappingModel /// The probability when this request should be matched. Value is between 0 and 1. [Optional] /// public double? Probability { get; set; } + + /// + /// The Grpc ProtoDefinition which is used for this mapping (request and response). [Optional] + /// + public string? ProtoDefinition { get; set; } } \ No newline at end of file diff --git a/src/WireMock.Net.Abstractions/Admin/Mappings/MatcherModel.cs b/src/WireMock.Net.Abstractions/Admin/Mappings/MatcherModel.cs index 994b8d05b..6e133ab47 100644 --- a/src/WireMock.Net.Abstractions/Admin/Mappings/MatcherModel.cs +++ b/src/WireMock.Net.Abstractions/Admin/Mappings/MatcherModel.cs @@ -70,13 +70,22 @@ public class MatcherModel /// ContentTransferEncoding Matcher (base64) /// public MatcherModel? ContentTransferEncodingMatcher { get; set; } + #endregion + #region MimePartMatcher + ProtoBufMatcher /// /// Content Matcher /// public MatcherModel? ContentMatcher { get; set; } #endregion + #region ProtoBufMatcher + /// + /// The full type of the protobuf (request/response) message object. Format is "{package-name}.{type-name}". + /// + public string? ProtoBufMessageType { get; set; } + #endregion + #region XPathMatcher /// /// Array of namespace prefix and uri. (optional) @@ -86,7 +95,7 @@ public class MatcherModel #region GraphQLMatcher /// - /// Mapping of custom GraphQL Scalar name to ClrType. (optional) + /// Mapping of custom GraphQL Scalar name to ClrType. (optional) /// public IDictionary? CustomScalars { get; set; } #endregion diff --git a/src/WireMock.Net.Abstractions/Admin/Mappings/RequestModel.cs b/src/WireMock.Net.Abstractions/Admin/Mappings/RequestModel.cs index b3b25cd84..8953c7326 100644 --- a/src/WireMock.Net.Abstractions/Admin/Mappings/RequestModel.cs +++ b/src/WireMock.Net.Abstractions/Admin/Mappings/RequestModel.cs @@ -28,6 +28,11 @@ public class RequestModel /// public string[]? Methods { get; set; } + /// + /// The HTTP Version + /// + public string? HttpVersion { get; set; } + /// /// Reject on match for Methods. /// diff --git a/src/WireMock.Net.Abstractions/Admin/Mappings/ResponseModel.cs b/src/WireMock.Net.Abstractions/Admin/Mappings/ResponseModel.cs index 0dacb64eb..1857cea96 100644 --- a/src/WireMock.Net.Abstractions/Admin/Mappings/ResponseModel.cs +++ b/src/WireMock.Net.Abstractions/Admin/Mappings/ResponseModel.cs @@ -35,7 +35,7 @@ public class ResponseModel public bool? BodyAsJsonIndented { get; set; } /// - /// Gets or sets the body (as bytearray). + /// Gets or sets the body (as byte array). /// public byte[]? BodyAsBytes { get; set; } @@ -84,6 +84,11 @@ public class ResponseModel /// public string? HeadersRaw { get; set; } + /// + /// Gets or sets the Trailing Headers. + /// + public IDictionary? TrailingHeaders { get; set; } + /// /// Gets or sets the delay in milliseconds. /// @@ -123,4 +128,16 @@ public class ResponseModel /// Gets or sets the WebProxy settings. /// public WebProxyModel? WebProxy { get; set; } + + #region ProtoBuf + /// + /// Gets or sets the proto definition. + /// + public string? ProtoDefinition { get; set; } + + /// + /// Gets or sets the full type of the protobuf (request/response) message object. Format is "{package-name}.{type-name}". + /// + public string? ProtoBufMessageType { get; set; } + #endregion } \ No newline at end of file diff --git a/src/WireMock.Net.Abstractions/Admin/Requests/LogRequestModel.cs b/src/WireMock.Net.Abstractions/Admin/Requests/LogRequestModel.cs index 16d0c4153..f1e7f84a1 100644 --- a/src/WireMock.Net.Abstractions/Admin/Requests/LogRequestModel.cs +++ b/src/WireMock.Net.Abstractions/Admin/Requests/LogRequestModel.cs @@ -55,6 +55,11 @@ public class LogRequestModel /// public string Method { get; set; } + /// + /// The HTTP Version. + /// + public string HttpVersion { get; set; } = null!; + /// /// The Headers. /// diff --git a/src/WireMock.Net.Abstractions/Admin/Settings/SettingsModel.cs b/src/WireMock.Net.Abstractions/Admin/Settings/SettingsModel.cs index 9f02d969e..a82f7ae42 100644 --- a/src/WireMock.Net.Abstractions/Admin/Settings/SettingsModel.cs +++ b/src/WireMock.Net.Abstractions/Admin/Settings/SettingsModel.cs @@ -1,4 +1,6 @@ +using System.Collections.Generic; using System.Text.RegularExpressions; +using JetBrains.Annotations; using WireMock.Handlers; using WireMock.Types; @@ -114,6 +116,11 @@ public class SettingsModel /// public QueryParameterMultipleValueSupport? QueryParameterMultipleValueSupport { get; set; } + /// + /// A list of Grpc ProtoDefinitions which can be used. + /// + public Dictionary? ProtoDefinitions { get; set; } + #if NETSTANDARD1_3_OR_GREATER || NET461 /// /// Server client certificate mode diff --git a/src/WireMock.Net.Abstractions/IRequestMessage.cs b/src/WireMock.Net.Abstractions/IRequestMessage.cs index c94d95a64..42d9c99fd 100644 --- a/src/WireMock.Net.Abstractions/IRequestMessage.cs +++ b/src/WireMock.Net.Abstractions/IRequestMessage.cs @@ -63,6 +63,11 @@ public interface IRequestMessage /// string Method { get; } + /// + /// Gets the HTTP Version. + /// + string HttpVersion { get; } + /// /// Gets the headers. /// @@ -94,23 +99,27 @@ public interface IRequestMessage IBodyData? BodyData { get; } /// - /// The original body as string. Convenience getter for Handlebars and WireMockAssertions. + /// The original body as string. + /// Convenience getter for Handlebars and WireMockAssertions. /// string? Body { get; } /// - /// The body (as JSON object). Convenience getter for Handlebars and WireMockAssertions. + /// The body (as JSON object). + /// Convenience getter for Handlebars and WireMockAssertions. /// object? BodyAsJson { get; } /// - /// The body (as bytearray). Convenience getter for Handlebars and WireMockAssertions. + /// The body (as bytearray). + /// Convenience getter for Handlebars and WireMockAssertions. /// byte[]? BodyAsBytes { get; } #if MIMEKIT /// - /// The original body as MimeMessage. Convenience getter for Handlebars and WireMockAssertions. + /// The original body as MimeMessage. + /// Convenience getter for Handlebars and WireMockAssertions. /// object? BodyAsMimeMessage { get; } #endif diff --git a/src/WireMock.Net.Abstractions/IResponseMessage.cs b/src/WireMock.Net.Abstractions/IResponseMessage.cs index b1499b1ad..1acdc3a88 100644 --- a/src/WireMock.Net.Abstractions/IResponseMessage.cs +++ b/src/WireMock.Net.Abstractions/IResponseMessage.cs @@ -40,11 +40,16 @@ public interface IResponseMessage /// IDictionary>? Headers { get; } + /// + /// Gets the trailing headers. + /// + IDictionary>? TrailingHeaders { get; } + /// /// Gets or sets the status code. /// object? StatusCode { get; } - + /// /// Adds the header. /// @@ -53,9 +58,23 @@ public interface IResponseMessage void AddHeader(string name, string value); /// - /// Adds the header. + /// Adds the trailing header. /// /// The name. /// The values. void AddHeader(string name, params string[] values); + + /// + /// Adds the trailing header. + /// + /// The name. + /// The value. + void AddTrailingHeader(string name, string value); + + /// + /// Adds the header. + /// + /// The name. + /// The values. + void AddTrailingHeader(string name, params string[] values); } \ No newline at end of file diff --git a/src/WireMock.Net.Abstractions/Models/IBodyData.cs b/src/WireMock.Net.Abstractions/Models/IBodyData.cs index 557be21c0..e74e6193a 100644 --- a/src/WireMock.Net.Abstractions/Models/IBodyData.cs +++ b/src/WireMock.Net.Abstractions/Models/IBodyData.cs @@ -1,7 +1,10 @@ +using System; using System.Collections.Generic; using System.Text; +using WireMock.Models; using WireMock.Types; +// ReSharper disable once CheckNamespace namespace WireMock.Util; /// @@ -10,7 +13,7 @@ namespace WireMock.Util; public interface IBodyData { /// - /// The body (as bytearray). + /// The body (as byte array). /// byte[]? BodyAsBytes { get; set; } @@ -26,6 +29,7 @@ public interface IBodyData /// /// The body (as JSON object). + /// Also used for ProtoBuf. /// object? BodyAsJson { get; set; } @@ -68,4 +72,16 @@ public interface IBodyData /// Defines if this BodyData is the result of a dynamically created response-string. ( /// public string? IsFuncUsed { get; set; } + + #region ProtoBuf + /// + /// Gets or sets the proto definition. + /// + public Func? ProtoDefinition { get; set; } + + /// + /// Gets or sets the full type of the protobuf (request/response) message object. Format is "{package-name}.{type-name}". + /// + public string? ProtoBufMessageType { get; set; } + #endregion } \ No newline at end of file diff --git a/src/WireMock.Net.Abstractions/Models/IdOrText.cs b/src/WireMock.Net.Abstractions/Models/IdOrText.cs new file mode 100644 index 000000000..5d1a4c584 --- /dev/null +++ b/src/WireMock.Net.Abstractions/Models/IdOrText.cs @@ -0,0 +1,33 @@ +namespace WireMock.Models; + +/// +/// A structure defining an (optional) Id and a Text. +/// +public readonly struct IdOrText +{ + /// + /// The Id [optional]. + /// + public string? Id { get; } + + /// + /// The Text. + /// + public string Text { get; } + + /// + /// When Id is defined, return the Id, else the Text. + /// + public string Value => Id ?? Text; + + /// + /// Create a IdOrText + /// + /// The Id [optional] + /// The Text. + public IdOrText(string? id, string text) + { + Id = id; + Text = text; + } +} \ No newline at end of file diff --git a/src/WireMock.Net.Abstractions/Types/BodyType.cs b/src/WireMock.Net.Abstractions/Types/BodyType.cs index a03534d1a..47a62ae22 100644 --- a/src/WireMock.Net.Abstractions/Types/BodyType.cs +++ b/src/WireMock.Net.Abstractions/Types/BodyType.cs @@ -38,5 +38,10 @@ public enum BodyType /// /// Body is a String which is x-www-form-urlencoded. /// - FormUrlEncoded + FormUrlEncoded, + + /// + /// Body is a ProtoBuf Byte array + /// + ProtoBuf } \ No newline at end of file diff --git a/src/WireMock.Net.Abstractions/WireMock.Net.Abstractions.csproj b/src/WireMock.Net.Abstractions/WireMock.Net.Abstractions.csproj index 29aa28054..32fa99b03 100644 --- a/src/WireMock.Net.Abstractions/WireMock.Net.Abstractions.csproj +++ b/src/WireMock.Net.Abstractions/WireMock.Net.Abstractions.csproj @@ -31,7 +31,7 @@ - $(DefineConstants);GRAPHQL;MIMEKIT + $(DefineConstants);GRAPHQL;MIMEKIT;PROTOBUF diff --git a/src/WireMock.Net.FluentAssertions/Assertions/WireMockAssertions.WithBody.cs b/src/WireMock.Net.FluentAssertions/Assertions/WireMockAssertions.WithBody.cs index 051d46aad..e1078f744 100644 --- a/src/WireMock.Net.FluentAssertions/Assertions/WireMockAssertions.WithBody.cs +++ b/src/WireMock.Net.FluentAssertions/Assertions/WireMockAssertions.WithBody.cs @@ -44,11 +44,11 @@ public AndConstraint WithBody(IStringMatcher matcher, string } [CustomAssertion] - public AndConstraint WithBodyAsJson(IValueMatcher matcher, string because = "", params object[] becauseArgs) + public AndConstraint WithBodyAsJson(IObjectMatcher matcher, string because = "", params object[] becauseArgs) { var (filter, condition) = BuildFilterAndCondition(r => r.BodyAsJson, matcher); - return ExecuteAssertionWithBodyAsJsonValueMatcher(matcher, because, becauseArgs, condition, filter, r => r.BodyAsJson); + return ExecuteAssertionWithBodyAsIObjectMatcher(matcher, because, becauseArgs, condition, filter, r => r.BodyAsJson); } [CustomAssertion] @@ -89,8 +89,8 @@ private AndConstraint ExecuteAssertionWithBodyStringMatcher( return new AndConstraint(this); } - private AndConstraint ExecuteAssertionWithBodyAsJsonValueMatcher( - IValueMatcher matcher, + private AndConstraint ExecuteAssertionWithBodyAsIObjectMatcher( + IObjectMatcher matcher, string because, object[] becauseArgs, Func, bool> condition, @@ -134,13 +134,13 @@ private AndConstraint ExecuteAssertionWithBodyAsBytesExactOb .ForCondition(requests => _callsCount == 0 || requests.Any()) .FailWith( MessageFormatNoCalls, - matcher.ValueAsObject ?? matcher.ValueAsBytes + matcher.Value ) .Then .ForCondition(condition) .FailWith( MessageFormat, - _ => matcher.ValueAsObject ?? matcher.ValueAsBytes, + _ => matcher.Value, requests => requests.Select(expression) ); diff --git a/src/WireMock.Net.Matchers.CSharpCode/Matchers/CSharpCodeMatcher.cs b/src/WireMock.Net.Matchers.CSharpCode/Matchers/CSharpCodeMatcher.cs index 0ad92cdc5..90dcd3bc7 100644 --- a/src/WireMock.Net.Matchers.CSharpCode/Matchers/CSharpCodeMatcher.cs +++ b/src/WireMock.Net.Matchers.CSharpCode/Matchers/CSharpCodeMatcher.cs @@ -33,6 +33,9 @@ internal class CSharpCodeMatcher : ICSharpCodeMatcher /// public MatchBehaviour MatchBehaviour { get; } + /// + public object Value { get; } + private readonly AnyOf[] _patterns; /// @@ -54,6 +57,7 @@ public CSharpCodeMatcher(MatchBehaviour matchBehaviour, MatchOperator matchOpera _patterns = Guard.NotNull(patterns); MatchBehaviour = matchBehaviour; MatchOperator = matchOperator; + Value = patterns; } public MatchResult IsMatch(string? input) @@ -160,34 +164,34 @@ private bool IsMatch(dynamic input, string pattern) } #elif (NETSTANDARD2_0 || NETSTANDARD2_1 || NETCOREAPP3_1 || NET5_0_OR_GREATER) - Assembly assembly; - try - { - assembly = CSScriptLib.CSScript.Evaluator.CompileCode(source); - } - catch (Exception ex) - { - throw new WireMockException($"CSharpCodeMatcher: Unable to compile code `{source}` for WireMock.CodeHelper", ex); - } + Assembly assembly; + try + { + assembly = CSScriptLib.CSScript.Evaluator.CompileCode(source); + } + catch (Exception ex) + { + throw new WireMockException($"CSharpCodeMatcher: Unable to compile code `{source}` for WireMock.CodeHelper", ex); + } - dynamic script; - try - { - script = CSScripting.ReflectionExtensions.CreateObject(assembly, "*"); - } - catch (Exception ex) - { - throw new WireMockException("CSharpCodeMatcher: Unable to create object from assembly", ex); - } + dynamic script; + try + { + script = CSScripting.ReflectionExtensions.CreateObject(assembly, "*"); + } + catch (Exception ex) + { + throw new WireMockException("CSharpCodeMatcher: Unable to create object from assembly", ex); + } - try - { - result = script.IsMatch(inputValue); - } - catch (Exception ex) - { - throw new WireMockException("CSharpCodeMatcher: Problem calling method 'IsMatch' in WireMock.CodeHelper", ex); - } + try + { + result = script.IsMatch(inputValue); + } + catch (Exception ex) + { + throw new WireMockException("CSharpCodeMatcher: Problem calling method 'IsMatch' in WireMock.CodeHelper", ex); + } #else throw new NotSupportedException("The 'CSharpCodeMatcher' cannot be used in netstandard 1.3"); #endif diff --git a/src/WireMock.Net.Matchers.CSharpCode/WireMock.Net.Matchers.CSharpCode.csproj b/src/WireMock.Net.Matchers.CSharpCode/WireMock.Net.Matchers.CSharpCode.csproj index 2ba81efc8..97440b252 100644 --- a/src/WireMock.Net.Matchers.CSharpCode/WireMock.Net.Matchers.CSharpCode.csproj +++ b/src/WireMock.Net.Matchers.CSharpCode/WireMock.Net.Matchers.CSharpCode.csproj @@ -46,7 +46,7 @@ - + \ No newline at end of file diff --git a/src/WireMock.Net/IMapping.cs b/src/WireMock.Net/IMapping.cs index 20b3e947b..b052dd4fe 100644 --- a/src/WireMock.Net/IMapping.cs +++ b/src/WireMock.Net/IMapping.cs @@ -69,12 +69,12 @@ public interface IMapping int? StateTimes { get; } /// - /// The Request matcher. + /// The RequestMatcher. /// IRequestMatcher RequestMatcher { get; } /// - /// The Provider. + /// The ResponseProvider. /// IResponseProvider Provider { get; } @@ -136,6 +136,11 @@ public interface IMapping /// double? Probability { get; } + /// + /// The Grpc ProtoDefinition which is used for this mapping (request and response). [Optional] + /// + IdOrText? ProtoDefinition { get; } + /// /// ProvideResponseAsync /// @@ -150,4 +155,44 @@ public interface IMapping /// The Next State. /// The . IRequestMatchResult GetRequestMatchResult(IRequestMessage requestMessage, string? nextState); -} \ No newline at end of file + + /// + /// Define the scenario. + /// + /// The scenario. + /// The . + IMapping WithScenario(string scenario); + + /// + /// Define the probability when this request should be matched. [Optional] + /// + /// The probability. + /// The . + IMapping WithProbability(double probability); + + /// + /// Define a Grpc ProtoDefinition which is used for this mapping (request and response). + /// + /// The proto definition as text. + /// The . + IMapping WithProtoDefinition(IdOrText protoDefinition); +} + +/* + executionConditionState">State in which the current mapping can occur. [Optional] + nextState">The next state which will occur after the current mapping execution. [Optional] + stateTimes">Only when the current state is executed this number, the next state which will occur. [Optional] + webhooks">The Webhooks. [Optional] + useWebhooksFireAndForget">Use Fire and Forget for the defined webhook(s). [Optional] + timeSettings">The TimeSettings. [Optional] + data">The data object. [Optional] + + + string? executionConditionState, + string? nextState, + int? stateTimes, + IWebhook[]? webhooks, + bool? useWebhooksFireAndForget, + ITimeSettings? timeSettings, + object? data, +*/ \ No newline at end of file diff --git a/src/WireMock.Net/Mapping.cs b/src/WireMock.Net/Mapping.cs index 019c208b5..dd9c2b4de 100644 --- a/src/WireMock.Net/Mapping.cs +++ b/src/WireMock.Net/Mapping.cs @@ -1,5 +1,6 @@ using System; using System.Threading.Tasks; +using Stef.Validation; using WireMock.Matchers.Request; using WireMock.Models; using WireMock.ResponseProviders; @@ -31,7 +32,7 @@ public class Mapping : IMapping public int Priority { get; } /// - public string? Scenario { get; } + public string? Scenario { get; private set; } /// public string? ExecutionConditionState { get; } @@ -76,7 +77,10 @@ public class Mapping : IMapping public object? Data { get; } /// - public double? Probability { get; } + public double? Probability { get; private set; } + + /// + public IdOrText? ProtoDefinition { get; private set; } /// /// Initializes a new instance of the class. @@ -98,8 +102,8 @@ public class Mapping : IMapping /// Use Fire and Forget for the defined webhook(s). [Optional] /// The TimeSettings. [Optional] /// The data object. [Optional] - /// Define the probability when this request should be matched. [Optional] - public Mapping( + public Mapping + ( Guid guid, DateTime updatedAt, string? title, @@ -116,8 +120,8 @@ public Mapping( IWebhook[]? webhooks, bool? useWebhooksFireAndForget, ITimeSettings? timeSettings, - object? data, - double? probability) + object? data + ) { Guid = guid; UpdatedAt = updatedAt; @@ -136,7 +140,6 @@ public Mapping( UseWebhooksFireAndForget = useWebhooksFireAndForget; TimeSettings = timeSettings; Data = data; - Probability = probability; } /// @@ -168,4 +171,25 @@ public IRequestMatchResult GetRequestMatchResult(IRequestMessage requestMessage, return result; } + + /// + public IMapping WithProbability(double probability) + { + Probability = Guard.NotNull(probability); + return this; + } + + /// + public IMapping WithScenario(string scenario) + { + Scenario = Guard.NotNullOrWhiteSpace(scenario); + return this; + } + + /// + public IMapping WithProtoDefinition(IdOrText protoDefinition) + { + ProtoDefinition = protoDefinition; + return this; + } } \ No newline at end of file diff --git a/src/WireMock.Net/MappingBuilder.cs b/src/WireMock.Net/MappingBuilder.cs index da6f479f7..8103ba2a0 100644 --- a/src/WireMock.Net/MappingBuilder.cs +++ b/src/WireMock.Net/MappingBuilder.cs @@ -6,6 +6,8 @@ using WireMock.Admin.Mappings; using WireMock.Matchers.Request; using WireMock.Owin; +using WireMock.RequestBuilders; +using WireMock.ResponseBuilders; using WireMock.Serialization; using WireMock.Server; using WireMock.Settings; @@ -146,6 +148,15 @@ private void RegisterMapping(IMapping mapping, bool saveToFile) { _mappingToFileSaver.SaveMappingToFile(mapping); } + + // Link this mapping to the Request + ((Request)mapping.RequestMatcher).Mapping = mapping; + + // Link this mapping to the Response + if (mapping.Provider is Response response) + { + response.Mapping = mapping; + } } private static string ToJson(object value) diff --git a/src/WireMock.Net/Matchers/ExactMatcher.cs b/src/WireMock.Net/Matchers/ExactMatcher.cs index 4d67eed99..0c6580fef 100644 --- a/src/WireMock.Net/Matchers/ExactMatcher.cs +++ b/src/WireMock.Net/Matchers/ExactMatcher.cs @@ -17,6 +17,15 @@ public class ExactMatcher : IStringMatcher, IIgnoreCaseMatcher /// public MatchBehaviour MatchBehaviour { get; } + /// + /// Initializes a new instance of the class. + /// + /// The match behaviour. + /// The string value. + public ExactMatcher(MatchBehaviour matchBehaviour, string value) : this(matchBehaviour, true, MatchOperator.Or, new AnyOf(value)) + { + } + /// /// Initializes a new instance of the class. /// diff --git a/src/WireMock.Net/Matchers/ExactObjectMatcher.cs b/src/WireMock.Net/Matchers/ExactObjectMatcher.cs index bc01e70ef..ea5bbbff1 100644 --- a/src/WireMock.Net/Matchers/ExactObjectMatcher.cs +++ b/src/WireMock.Net/Matchers/ExactObjectMatcher.cs @@ -9,15 +9,8 @@ namespace WireMock.Matchers; /// public class ExactObjectMatcher : IObjectMatcher { - /// - /// Gets the value as object. - /// - public object? ValueAsObject { get; } - - /// - /// Gets the value as byte[]. - /// - public byte[]? ValueAsBytes { get; } + /// + public object Value { get; } /// public MatchBehaviour MatchBehaviour { get; } @@ -37,7 +30,7 @@ public ExactObjectMatcher(object value) : this(MatchBehaviour.AcceptOnMatch, val /// The value. public ExactObjectMatcher(MatchBehaviour matchBehaviour, object value) { - ValueAsObject = Guard.NotNull(value); + Value = Guard.NotNull(value); MatchBehaviour = matchBehaviour; } @@ -56,21 +49,21 @@ public ExactObjectMatcher(byte[] value) : this(MatchBehaviour.AcceptOnMatch, val /// The value. public ExactObjectMatcher(MatchBehaviour matchBehaviour, byte[] value) { - ValueAsBytes = Guard.NotNull(value); + Value = Guard.NotNull(value); MatchBehaviour = matchBehaviour; } /// public MatchResult IsMatch(object? input) { - bool equals = false; - if (ValueAsObject != null) + bool equals; + if (Value is byte[] valueAsBytes && input is byte[] inputAsBytes) { - equals = Equals(ValueAsObject, input); + equals = valueAsBytes.SequenceEqual(inputAsBytes); } - else if (input != null) + else { - equals = ValueAsBytes?.SequenceEqual((byte[])input) == true; + equals = Equals(Value, input); } return MatchBehaviourHelper.Convert(MatchBehaviour, MatchScores.ToScore(equals)); diff --git a/src/WireMock.Net/Matchers/IBytesMatcher.cs b/src/WireMock.Net/Matchers/IBytesMatcher.cs new file mode 100644 index 000000000..2870a1488 --- /dev/null +++ b/src/WireMock.Net/Matchers/IBytesMatcher.cs @@ -0,0 +1,18 @@ +using System.Threading; +using System.Threading.Tasks; + +namespace WireMock.Matchers; + +/// +/// IBytesMatcher +/// +public interface IBytesMatcher : IMatcher +{ + /// + /// Determines whether the specified input is match. + /// + /// The input byte array. + /// The CancellationToken [optional]. + /// MatchResult + Task IsMatchAsync(byte[]? input, CancellationToken cancellationToken = default); +} \ No newline at end of file diff --git a/src/WireMock.Net/Matchers/IDecodeMatcher.cs b/src/WireMock.Net/Matchers/IDecodeMatcher.cs new file mode 100644 index 000000000..ffc520f82 --- /dev/null +++ b/src/WireMock.Net/Matchers/IDecodeMatcher.cs @@ -0,0 +1,18 @@ +using System.Threading; +using System.Threading.Tasks; + +namespace WireMock.Matchers; + +/// +/// IDecodeBytesMatcher +/// +public interface IDecodeBytesMatcher +{ + /// + /// Decode byte array to an object. + /// + /// The byte array + /// The CancellationToken [optional]. + /// object + Task DecodeAsync(byte[]? input, CancellationToken cancellationToken = default); +} \ No newline at end of file diff --git a/src/WireMock.Net/Matchers/IJsonMatcher.cs b/src/WireMock.Net/Matchers/IJsonMatcher.cs new file mode 100644 index 000000000..f2cc35bc4 --- /dev/null +++ b/src/WireMock.Net/Matchers/IJsonMatcher.cs @@ -0,0 +1,9 @@ +namespace WireMock.Matchers; + +/// +/// IJsonMatcher +/// and . +/// +public interface IJsonMatcher : IObjectMatcher, IIgnoreCaseMatcher +{ +} \ No newline at end of file diff --git a/src/WireMock.Net/Matchers/IObjectMatcher.cs b/src/WireMock.Net/Matchers/IObjectMatcher.cs index 439a469b9..8ed2b02f7 100644 --- a/src/WireMock.Net/Matchers/IObjectMatcher.cs +++ b/src/WireMock.Net/Matchers/IObjectMatcher.cs @@ -5,6 +5,12 @@ namespace WireMock.Matchers; /// public interface IObjectMatcher : IMatcher { + /// + /// Gets the value (can be a string or an object). + /// + /// Value + object Value { get; } + /// /// Determines whether the specified input is match. /// diff --git a/src/WireMock.Net/Matchers/IProtoBufMatcher.cs b/src/WireMock.Net/Matchers/IProtoBufMatcher.cs new file mode 100644 index 000000000..576ee9c15 --- /dev/null +++ b/src/WireMock.Net/Matchers/IProtoBufMatcher.cs @@ -0,0 +1,8 @@ +namespace WireMock.Matchers; + +/// +/// IProtoBufMatcher +/// +public interface IProtoBufMatcher : IDecodeBytesMatcher, IBytesMatcher +{ +} \ No newline at end of file diff --git a/src/WireMock.Net/Matchers/IValueMatcher.cs b/src/WireMock.Net/Matchers/IValueMatcher.cs deleted file mode 100644 index c2b8e94e8..000000000 --- a/src/WireMock.Net/Matchers/IValueMatcher.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace WireMock.Matchers; - -/// -/// IValueMatcher -/// -/// -public interface IValueMatcher : IObjectMatcher -{ - /// - /// Gets the value (can be a string or an object). - /// - /// Value - object Value { get; } -} \ No newline at end of file diff --git a/src/WireMock.Net/Matchers/JSONPathMatcher.cs b/src/WireMock.Net/Matchers/JSONPathMatcher.cs index 1971616b2..f1183fd06 100644 --- a/src/WireMock.Net/Matchers/JSONPathMatcher.cs +++ b/src/WireMock.Net/Matchers/JSONPathMatcher.cs @@ -11,7 +11,7 @@ namespace WireMock.Matchers; /// /// JsonPathMatcher /// -/// +/// /// public class JsonPathMatcher : IStringMatcher, IObjectMatcher { @@ -20,6 +20,9 @@ public class JsonPathMatcher : IStringMatcher, IObjectMatcher /// public MatchBehaviour MatchBehaviour { get; } + /// + public object Value { get; } + /// /// Initializes a new instance of the class. /// @@ -52,6 +55,7 @@ public JsonPathMatcher( _patterns = Guard.NotNull(patterns); MatchBehaviour = matchBehaviour; MatchOperator = matchOperator; + Value = patterns; } /// @@ -119,7 +123,7 @@ private double IsMatch(JToken jToken) // The SelectToken method can accept a string path to a child token ( i.e. "Manufacturers[0].Products[0].Price"). // In that case it will return a JValue (some type) which does not implement the IEnumerable interface. var values = _patterns.Select(pattern => array.SelectToken(pattern.GetPattern()) != null).ToArray(); - + return MatchScores.ToScore(values, MatchOperator); } diff --git a/src/WireMock.Net/Matchers/JmesPathMatcher.cs b/src/WireMock.Net/Matchers/JmesPathMatcher.cs index 0f1488cd1..f87c7a1ad 100644 --- a/src/WireMock.Net/Matchers/JmesPathMatcher.cs +++ b/src/WireMock.Net/Matchers/JmesPathMatcher.cs @@ -16,6 +16,9 @@ public class JmesPathMatcher : IStringMatcher, IObjectMatcher { private readonly AnyOf[] _patterns; + /// + public object Value { get; } + /// public MatchBehaviour MatchBehaviour { get; } @@ -59,6 +62,7 @@ public JmesPathMatcher( _patterns = Guard.NotNull(patterns); MatchBehaviour = matchBehaviour; MatchOperator = matchOperator; + Value = patterns; } /// diff --git a/src/WireMock.Net/Matchers/JsonMatcher.cs b/src/WireMock.Net/Matchers/JsonMatcher.cs index 6ff6bfce5..a259ab5d5 100644 --- a/src/WireMock.Net/Matchers/JsonMatcher.cs +++ b/src/WireMock.Net/Matchers/JsonMatcher.cs @@ -1,5 +1,4 @@ using System; -using System.Collections; using System.Linq; using Newtonsoft.Json.Linq; using Stef.Validation; @@ -10,12 +9,12 @@ namespace WireMock.Matchers; /// /// JsonMatcher /// -public class JsonMatcher : IValueMatcher, IIgnoreCaseMatcher +public class JsonMatcher : IJsonMatcher { /// - public virtual string Name => "JsonMatcher"; + public virtual string Name => nameof(JsonMatcher); - /// + /// public object Value { get; } /// @@ -59,7 +58,7 @@ public JsonMatcher(MatchBehaviour matchBehaviour, object value, bool ignoreCase IgnoreCase = ignoreCase; Value = value; - _valueAsJToken = ConvertValueToJToken(value); + _valueAsJToken = JsonUtils.ConvertValueToJToken(value); _jTokenConverter = ignoreCase ? Rename : jToken => jToken; } @@ -74,7 +73,7 @@ public MatchResult IsMatch(object? input) { try { - var inputAsJToken = ConvertValueToJToken(input); + var inputAsJToken = JsonUtils.ConvertValueToJToken(input); var match = IsMatch(_jTokenConverter(_valueAsJToken), _jTokenConverter(inputAsJToken)); score = MatchScores.ToScore(match); @@ -99,25 +98,6 @@ protected virtual bool IsMatch(JToken value, JToken input) return JToken.DeepEquals(value, input); } - private static JToken ConvertValueToJToken(object value) - { - // Check if JToken, string, IEnumerable or object - switch (value) - { - case JToken tokenValue: - return tokenValue; - - case string stringValue: - return JsonUtils.Parse(stringValue); - - case IEnumerable enumerableValue: - return JArray.FromObject(enumerableValue); - - default: - return JObject.FromObject(value); - } - } - private static string? ToUpper(string? input) { return input?.ToUpperInvariant(); diff --git a/src/WireMock.Net/Matchers/LinqMatcher.cs b/src/WireMock.Net/Matchers/LinqMatcher.cs index 8c6b92289..6fdf88a8e 100644 --- a/src/WireMock.Net/Matchers/LinqMatcher.cs +++ b/src/WireMock.Net/Matchers/LinqMatcher.cs @@ -22,6 +22,9 @@ public class LinqMatcher : IObjectMatcher, IStringMatcher /// public MatchBehaviour MatchBehaviour { get; } + /// + public object Value { get; } + /// /// Initializes a new instance of the class. /// @@ -61,6 +64,7 @@ public LinqMatcher( _patterns = Guard.NotNull(patterns); MatchBehaviour = matchBehaviour; MatchOperator = matchOperator; + Value = patterns; } /// diff --git a/src/WireMock.Net/Matchers/MatchBehaviourHelper.cs b/src/WireMock.Net/Matchers/MatchBehaviourHelper.cs index f90889e41..12e4864b1 100644 --- a/src/WireMock.Net/Matchers/MatchBehaviourHelper.cs +++ b/src/WireMock.Net/Matchers/MatchBehaviourHelper.cs @@ -23,4 +23,11 @@ internal static double Convert(MatchBehaviour matchBehaviour, double match) return match <= MatchScores.Tolerance ? MatchScores.Perfect : MatchScores.Mismatch; } + + internal static MatchResult Convert(MatchBehaviour matchBehaviour, MatchResult result) + { + return matchBehaviour == MatchBehaviour.AcceptOnMatch ? + result : + new MatchResult(Convert(matchBehaviour, result.Score), result.Exception); + } } \ No newline at end of file diff --git a/src/WireMock.Net/Matchers/NotNullOrEmptyMatcher.cs b/src/WireMock.Net/Matchers/NotNullOrEmptyMatcher.cs index 4f7ee89f5..7c746a863 100644 --- a/src/WireMock.Net/Matchers/NotNullOrEmptyMatcher.cs +++ b/src/WireMock.Net/Matchers/NotNullOrEmptyMatcher.cs @@ -17,6 +17,9 @@ public class NotNullOrEmptyMatcher : IObjectMatcher, IStringMatcher /// public MatchBehaviour MatchBehaviour { get; } + /// + public object Value { get; } + /// /// Initializes a new instance of the class. /// @@ -24,6 +27,7 @@ public class NotNullOrEmptyMatcher : IObjectMatcher, IStringMatcher public NotNullOrEmptyMatcher(MatchBehaviour matchBehaviour = MatchBehaviour.AcceptOnMatch) { MatchBehaviour = matchBehaviour; + Value = string.Empty; } /// diff --git a/src/WireMock.Net/Matchers/ProtoBufMatcher.cs b/src/WireMock.Net/Matchers/ProtoBufMatcher.cs new file mode 100644 index 000000000..45a221844 --- /dev/null +++ b/src/WireMock.Net/Matchers/ProtoBufMatcher.cs @@ -0,0 +1,114 @@ +#if PROTOBUF +using System; +using System.Threading; +using System.Threading.Tasks; +using ProtoBufJsonConverter; +using ProtoBufJsonConverter.Models; +using Stef.Validation; +using WireMock.Models; +using WireMock.Util; + +namespace WireMock.Matchers; + +/// +/// Grpc ProtoBuf Matcher +/// +/// +public class ProtoBufMatcher : IProtoBufMatcher +{ + /// + public string Name => nameof(ProtoBufMatcher); + + /// + public MatchBehaviour MatchBehaviour { get; } + + /// + /// The Func to define The proto definition as text. + /// + public Func ProtoDefinition { get; } + + /// + /// The full type of the protobuf (request/response) message object. Format is "{package-name}.{type-name}". + /// + public string MessageType { get; } + + /// + /// The Matcher to use (optional). + /// + public IObjectMatcher? Matcher { get; } + + private static readonly Converter ProtoBufToJsonConverter = SingletonFactory.GetInstance(); + + /// + /// Initializes a new instance of the class. + /// + /// The proto definition. + /// The full type of the protobuf (request/response) message object. Format is "{package-name}.{type-name}". + /// The match behaviour. (default = "AcceptOnMatch") + /// The optional jsonMatcher to use to match the ProtoBuf as (json) object. + public ProtoBufMatcher( + Func protoDefinition, + string messageType, + MatchBehaviour matchBehaviour = MatchBehaviour.AcceptOnMatch, + IObjectMatcher? matcher = null + ) + { + ProtoDefinition = Guard.NotNull(protoDefinition); + MessageType = Guard.NotNullOrWhiteSpace(messageType); + Matcher = matcher; + MatchBehaviour = matchBehaviour; + } + + /// + public async Task IsMatchAsync(byte[]? input, CancellationToken cancellationToken = default) + { + var result = new MatchResult(); + + if (input != null) + { + try + { + var instance = await DecodeAsync(input, true, cancellationToken).ConfigureAwait(false); + + result = Matcher?.IsMatch(instance) ?? new MatchResult(MatchScores.Perfect); + } + catch (Exception e) + { + result = new MatchResult(MatchScores.Mismatch, e); + } + } + + return MatchBehaviourHelper.Convert(MatchBehaviour, result); + } + + /// + public Task DecodeAsync(byte[]? input, CancellationToken cancellationToken = default) + { + return DecodeAsync(input, false, cancellationToken); + } + + private async Task DecodeAsync(byte[]? input, bool throwException, CancellationToken cancellationToken) + { + if (input == null) + { + return null; + } + + var request = new ConvertToObjectRequest(ProtoDefinition().Text, MessageType, input); + + try + { + return await ProtoBufToJsonConverter.ConvertAsync(request, cancellationToken).ConfigureAwait(false); + } + catch + { + if (throwException) + { + throw; + } + + return null; + } + } +} +#endif \ No newline at end of file diff --git a/src/WireMock.Net/Matchers/Request/RequestMessageCompositeMatcher.cs b/src/WireMock.Net/Matchers/Request/RequestMessageCompositeMatcher.cs index 0ecd738ed..12cbe721b 100644 --- a/src/WireMock.Net/Matchers/Request/RequestMessageCompositeMatcher.cs +++ b/src/WireMock.Net/Matchers/Request/RequestMessageCompositeMatcher.cs @@ -26,10 +26,8 @@ public abstract class RequestMessageCompositeMatcher : IRequestMatcher /// The CompositeMatcherType type (Defaults to 'And') protected RequestMessageCompositeMatcher(IEnumerable requestMatchers, CompositeMatcherType type = CompositeMatcherType.And) { - Guard.NotNull(requestMatchers); - + RequestMatchers = Guard.NotNull(requestMatchers); _type = type; - RequestMatchers = requestMatchers; } /// diff --git a/src/WireMock.Net/Matchers/Request/RequestMessageGraphQLMatcher.cs b/src/WireMock.Net/Matchers/Request/RequestMessageGraphQLMatcher.cs index 0250deb97..c8211963b 100644 --- a/src/WireMock.Net/Matchers/Request/RequestMessageGraphQLMatcher.cs +++ b/src/WireMock.Net/Matchers/Request/RequestMessageGraphQLMatcher.cs @@ -26,7 +26,7 @@ public class RequestMessageGraphQLMatcher : IRequestMatcher /// /// The match behaviour. /// The schema. - /// A dictionary defining the custom scalars used in this schema. (optional) + /// A dictionary defining the custom scalars used in this schema. [optional] public RequestMessageGraphQLMatcher(MatchBehaviour matchBehaviour, string schema, IDictionary? customScalars = null) : this(CreateMatcherArray(matchBehaviour, schema, customScalars)) { @@ -38,7 +38,7 @@ public RequestMessageGraphQLMatcher(MatchBehaviour matchBehaviour, string schema /// /// The match behaviour. /// The schema. - /// A dictionary defining the custom scalars used in this schema. (optional) + /// A dictionary defining the custom scalars used in this schema. [optional] public RequestMessageGraphQLMatcher(MatchBehaviour matchBehaviour, GraphQL.Types.ISchema schema, IDictionary? customScalars = null) : this(CreateMatcherArray(matchBehaviour, new AnyOfTypes.AnyOf(schema), customScalars)) { @@ -94,8 +94,7 @@ private IReadOnlyList CalculateMatchResults(IRequestMessage request #if GRAPHQL private static IMatcher[] CreateMatcherArray( MatchBehaviour matchBehaviour, - AnyOfTypes.AnyOf schema, + AnyOfTypes.AnyOf schema, IDictionary? customScalars ) { diff --git a/src/WireMock.Net/Matchers/Request/RequestMessageHttpVersionMatcher.cs b/src/WireMock.Net/Matchers/Request/RequestMessageHttpVersionMatcher.cs new file mode 100644 index 000000000..3d2bdb62d --- /dev/null +++ b/src/WireMock.Net/Matchers/Request/RequestMessageHttpVersionMatcher.cs @@ -0,0 +1,86 @@ +using System; +using System.Linq; +using Stef.Validation; + +namespace WireMock.Matchers.Request; + +/// +/// The request HTTP Version matcher. +/// +public class RequestMessageHttpVersionMatcher : IRequestMatcher +{ + /// + /// The matcher. + /// + public IStringMatcher? Matcher { get; } + + /// + /// The func. + /// + public Func? Func { get; } + + /// + /// The + /// + public MatchBehaviour Behaviour { get; } + + /// + /// The HTTP Version + /// + public string? HttpVersion { get; } + + /// + /// Initializes a new instance of the class. + /// + /// The match behaviour. + /// The HTTP Version. + public RequestMessageHttpVersionMatcher(MatchBehaviour matchBehaviour, string httpVersion) : + this(matchBehaviour, new ExactMatcher(matchBehaviour, httpVersion)) + { + HttpVersion = httpVersion; + Behaviour = matchBehaviour; + } + + /// + /// Initializes a new instance of the class. + /// + /// The match behaviour. + /// The matcher. + public RequestMessageHttpVersionMatcher(MatchBehaviour matchBehaviour, IStringMatcher matcher) + { + Matcher = Guard.NotNull(matcher); + Behaviour = matchBehaviour; + HttpVersion = matcher.GetPatterns().FirstOrDefault(); + } + + /// + /// Initializes a new instance of the class. + /// + /// The function. + public RequestMessageHttpVersionMatcher(Func func) + { + Func = Guard.NotNull(func); + } + + /// + public double GetMatchingScore(IRequestMessage requestMessage, IRequestMatchResult requestMatchResult) + { + var (score, exception) = GetMatchResult(requestMessage).Expand(); + return requestMatchResult.AddScore(GetType(), score, exception); + } + + private MatchResult GetMatchResult(IRequestMessage requestMessage) + { + if (Matcher != null) + { + return Matcher.IsMatch(requestMessage.HttpVersion); + } + + if (Func != null) + { + return MatchScores.ToScore(Func(requestMessage.HttpVersion)); + } + + return default; + } +} \ No newline at end of file diff --git a/src/WireMock.Net/Matchers/Request/RequestMessageProtoBufMatcher.cs b/src/WireMock.Net/Matchers/Request/RequestMessageProtoBufMatcher.cs new file mode 100644 index 000000000..004d1b70d --- /dev/null +++ b/src/WireMock.Net/Matchers/Request/RequestMessageProtoBufMatcher.cs @@ -0,0 +1,43 @@ +using System; +using WireMock.Models; + +namespace WireMock.Matchers.Request; + +/// +/// The request body Grpc ProtoBuf matcher. +/// +public class RequestMessageProtoBufMatcher : IRequestMatcher +{ + /// + /// The ProtoBufMatcher. + /// + public IProtoBufMatcher? Matcher { get; } + + /// + /// Initializes a new instance of the class. + /// + /// The match behaviour. (default = "AcceptOnMatch") + /// The Func to define The proto definition as text. + /// The full type of the protobuf (request/response) message object. Format is "{package-name}.{type-name}". + /// The optional matcher to use to match the ProtoBuf as (json) object. + public RequestMessageProtoBufMatcher(MatchBehaviour matchBehaviour, Func protoDefinition, string messageType, IObjectMatcher? matcher = null) + { +#if PROTOBUF + Matcher = new ProtoBufMatcher(protoDefinition, messageType, matchBehaviour, matcher); +#else + throw new System.NotSupportedException("The ProtoBufMatcher can not be used for .NETStandard1.3 or .NET Framework 4.6.1 or lower."); +#endif + } + + /// + public double GetMatchingScore(IRequestMessage requestMessage, IRequestMatchResult requestMatchResult) + { + var (score, exception) = GetMatchResult(requestMessage).Expand(); + return requestMatchResult.AddScore(GetType(), score, exception); + } + + private MatchResult GetMatchResult(IRequestMessage requestMessage) + { + return Matcher?.IsMatchAsync(requestMessage.BodyAsBytes).GetAwaiter().GetResult() ?? default; + } +} \ No newline at end of file diff --git a/src/WireMock.Net/Models/BodyData.cs b/src/WireMock.Net/Models/BodyData.cs index a8036e50f..71b63205e 100644 --- a/src/WireMock.Net/Models/BodyData.cs +++ b/src/WireMock.Net/Models/BodyData.cs @@ -1,7 +1,10 @@ +using System; using System.Collections.Generic; using System.Text; +using WireMock.Models; using WireMock.Types; +// ReSharper disable once CheckNamespace namespace WireMock.Util; /// @@ -9,7 +12,7 @@ namespace WireMock.Util; /// public class BodyData : IBodyData { - /// + /// public Encoding? Encoding { get; set; } /// @@ -18,30 +21,38 @@ public class BodyData : IBodyData /// public IDictionary? BodyAsFormUrlEncoded { get; set; } - /// + /// public object? BodyAsJson { get; set; } - /// + /// public byte[]? BodyAsBytes { get; set; } - - /// + + /// public bool? BodyAsJsonIndented { get; set; } - /// + /// public string? BodyAsFile { get; set; } - /// + /// public bool? BodyAsFileIsCached { get; set; } - /// + /// public BodyType? DetectedBodyType { get; set; } - /// + /// public BodyType? DetectedBodyTypeFromContentType { get; set; } - /// + /// public string? DetectedCompression { get; set; } /// public string? IsFuncUsed { get; set; } + + #region ProtoBuf + /// + public Func? ProtoDefinition { get; set; } + + /// + public string? ProtoBufMessageType { get; set; } + #endregion } \ No newline at end of file diff --git a/src/WireMock.Net/Models/GraphQLSchemaDetails.cs b/src/WireMock.Net/Models/GraphQLSchemaDetails.cs new file mode 100644 index 000000000..8188dcf93 --- /dev/null +++ b/src/WireMock.Net/Models/GraphQLSchemaDetails.cs @@ -0,0 +1,61 @@ +using System; +using System.Collections.Generic; +using AnyOfTypes; +using Newtonsoft.Json; + +namespace WireMock.Models; + +/// +/// GraphQLSchemaDetails +/// +public class GraphQLSchemaDetails +{ + /// + /// The GraphQL schema as a string. + /// + public string? SchemaAsString { get; set; } + + /// + /// The GraphQL schema as a StringPattern. + /// + public StringPattern? SchemaAsStringPattern { get; set; } + +#if GRAPHQL + /// + /// The GraphQL schema as a . + /// + public GraphQL.Types.ISchema? SchemaAsISchema { get; set; } + + /// + /// The GraphQL Schema. + /// + [JsonIgnore] + public AnyOf? Schema + { + get + { + if (SchemaAsString != null) + { + return SchemaAsString; + } + + if (SchemaAsStringPattern != null) + { + return SchemaAsStringPattern; + } + + if (SchemaAsISchema != null) + { + return new AnyOf(SchemaAsISchema); + } + + return null; + } + } +#endif + + /// + /// The custom Scalars to define for this schema. + /// + public IDictionary? CustomScalars { get; set; } +} \ No newline at end of file diff --git a/src/WireMock.Net/Owin/AspNetCoreSelfHost.NETStandard.cs b/src/WireMock.Net/Owin/AspNetCoreSelfHost.NETStandard.cs index 91ac3a260..a5f3a1ba9 100644 --- a/src/WireMock.Net/Owin/AspNetCoreSelfHost.NETStandard.cs +++ b/src/WireMock.Net/Owin/AspNetCoreSelfHost.NETStandard.cs @@ -46,6 +46,18 @@ private static void SetHttpsAndUrls(KestrelServerOptions kestrelOptions, IWireMo options.ClientCertificateValidation = (_, _, _) => true; } }); + + if (urlDetail.IsHttp2) + { + listenOptions.Protocols = HttpProtocols.Http2; + } + }); + } + else if (urlDetail.IsHttp2) + { + kestrelOptions.ListenAnyIP(urlDetail.Port, listenOptions => + { + listenOptions.Protocols = HttpProtocols.Http2; }); } else diff --git a/src/WireMock.Net/Owin/AspNetCoreSelfHost.cs b/src/WireMock.Net/Owin/AspNetCoreSelfHost.cs index acdbc1967..b55643f78 100644 --- a/src/WireMock.Net/Owin/AspNetCoreSelfHost.cs +++ b/src/WireMock.Net/Owin/AspNetCoreSelfHost.cs @@ -110,18 +110,22 @@ private Task RunHost(CancellationToken token) { try { +#if NETCOREAPP3_1 || NET5_0_OR_GREATER + var appLifetime = _host.Services.GetRequiredService(); +#else var appLifetime = _host.Services.GetRequiredService(); +#endif appLifetime.ApplicationStarted.Register(() => { var addresses = _host.ServerFeatures - .Get() + .Get()! .Addresses; - foreach (string address in addresses) + foreach (var address in addresses) { Urls.Add(address.Replace("0.0.0.0", "localhost").Replace("[::]", "localhost")); - PortUtils.TryExtract(address, out _, out _, out _, out int port); + PortUtils.TryExtract(address, out _, out _, out _, out _, out var port); Ports.Add(port); } diff --git a/src/WireMock.Net/Owin/HostUrlDetails.cs b/src/WireMock.Net/Owin/HostUrlDetails.cs index 56682bfff..8c30c8e44 100644 --- a/src/WireMock.Net/Owin/HostUrlDetails.cs +++ b/src/WireMock.Net/Owin/HostUrlDetails.cs @@ -7,6 +7,8 @@ internal struct HostUrlDetails { public bool IsHttps { get; set; } + public bool IsHttp2 { get; set; } + public string Url { get; set; } public string Scheme { get; set; } diff --git a/src/WireMock.Net/Owin/HostUrlOptions.cs b/src/WireMock.Net/Owin/HostUrlOptions.cs index 8d514e8fc..617bdc011 100644 --- a/src/WireMock.Net/Owin/HostUrlOptions.cs +++ b/src/WireMock.Net/Owin/HostUrlOptions.cs @@ -14,6 +14,8 @@ internal class HostUrlOptions public HostingScheme HostingScheme { get; set; } + public bool? UseHttp2 { get; set; } + public IReadOnlyList GetDetails() { var list = new List(); @@ -23,25 +25,25 @@ public IReadOnlyList GetDetails() { var port = Port > 0 ? Port.Value : FindFreeTcpPort(); var scheme = HostingScheme == HostingScheme.Https ? "https" : "http"; - list.Add(new HostUrlDetails { IsHttps = HostingScheme == HostingScheme.Https, Url = $"{scheme}://{Localhost}:{port}", Scheme = scheme, Host = Localhost, Port = port }); + list.Add(new HostUrlDetails { IsHttps = HostingScheme == HostingScheme.Https, IsHttp2 = UseHttp2 == true, Url = $"{scheme}://{Localhost}:{port}", Scheme = scheme, Host = Localhost, Port = port }); } if (HostingScheme == HostingScheme.HttpAndHttps) { var httpPort = Port > 0 ? Port.Value : FindFreeTcpPort(); - list.Add(new HostUrlDetails { IsHttps = false, Url = $"http://{Localhost}:{httpPort}", Scheme = "http", Host = Localhost, Port = httpPort }); + list.Add(new HostUrlDetails { IsHttps = false, IsHttp2 = UseHttp2 == true, Url = $"http://{Localhost}:{httpPort}", Scheme = "http", Host = Localhost, Port = httpPort }); var httpsPort = FindFreeTcpPort(); // In this scenario, always get a free port for https. - list.Add(new HostUrlDetails { IsHttps = true, Url = $"https://{Localhost}:{httpsPort}", Scheme = "https", Host = Localhost, Port = httpsPort }); + list.Add(new HostUrlDetails { IsHttps = true, IsHttp2 = UseHttp2 == true, Url = $"https://{Localhost}:{httpsPort}", Scheme = "https", Host = Localhost, Port = httpsPort }); } } else { - foreach (string url in Urls) + foreach (var url in Urls) { - if (PortUtils.TryExtract(url, out var isHttps, out var protocol, out var host, out var port)) + if (PortUtils.TryExtract(url, out var isHttps, out var isGrpc, out var protocol, out var host, out var port)) { - list.Add(new HostUrlDetails { IsHttps = isHttps, Url = url, Scheme = protocol, Host = host, Port = port }); + list.Add(new HostUrlDetails { IsHttps = isHttps, IsHttp2 = isGrpc, Url = url, Scheme = protocol, Host = host, Port = port }); } } } diff --git a/src/WireMock.Net/Owin/Mappers/IOwinResponseMapper.cs b/src/WireMock.Net/Owin/Mappers/IOwinResponseMapper.cs index 34c5954c1..511a08f0b 100644 --- a/src/WireMock.Net/Owin/Mappers/IOwinResponseMapper.cs +++ b/src/WireMock.Net/Owin/Mappers/IOwinResponseMapper.cs @@ -5,18 +5,17 @@ using IResponse = Microsoft.AspNetCore.Http.HttpResponse; #endif -namespace WireMock.Owin.Mappers +namespace WireMock.Owin.Mappers; + +/// +/// IOwinResponseMapper +/// +internal interface IOwinResponseMapper { /// - /// IOwinResponseMapper + /// Map ResponseMessage to IResponse. /// - internal interface IOwinResponseMapper - { - /// - /// Map ResponseMessage to IResponse. - /// - /// The ResponseMessage - /// The OwinResponse/HttpResponse - Task MapAsync(IResponseMessage? responseMessage, IResponse response); - } -} \ No newline at end of file + /// The ResponseMessage + /// The OwinResponse/HttpResponse + Task MapAsync(IResponseMessage? responseMessage, IResponse response); +} diff --git a/src/WireMock.Net/Owin/Mappers/OwinRequestMapper.cs b/src/WireMock.Net/Owin/Mappers/OwinRequestMapper.cs index c49dc7854..f9eb1cb39 100644 --- a/src/WireMock.Net/Owin/Mappers/OwinRequestMapper.cs +++ b/src/WireMock.Net/Owin/Mappers/OwinRequestMapper.cs @@ -8,103 +8,102 @@ #if !USE_ASPNETCORE using IRequest = Microsoft.Owin.IOwinRequest; #else -using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Extensions; using IRequest = Microsoft.AspNetCore.Http.HttpRequest; #endif -namespace WireMock.Owin.Mappers +namespace WireMock.Owin.Mappers; + +/// +/// OwinRequestMapper +/// +internal class OwinRequestMapper : IOwinRequestMapper { - /// - /// OwinRequestMapper - /// - internal class OwinRequestMapper : IOwinRequestMapper + /// + public async Task MapAsync(IRequest request, IWireMockMiddlewareOptions options) { - /// - public async Task MapAsync(IRequest request, IWireMockMiddlewareOptions options) - { - var (urlDetails, clientIP) = ParseRequest(request); + var (urlDetails, clientIP) = ParseRequest(request); - string method = request.Method; - - var headers = new Dictionary(); - IEnumerable? contentEncodingHeader = null; - foreach (var header in request.Headers) - { - headers.Add(header.Key, header.Value!); + var method = request.Method; + var httpVersion = HttpVersionParser.Parse(request.Protocol); - if (string.Equals(header.Key, HttpKnownHeaderNames.ContentEncoding, StringComparison.OrdinalIgnoreCase)) - { - contentEncodingHeader = header.Value; - } - } + var headers = new Dictionary(); + IEnumerable? contentEncodingHeader = null; + foreach (var header in request.Headers) + { + headers.Add(header.Key, header.Value!); - var cookies = new Dictionary(); - if (request.Cookies.Any()) + if (string.Equals(header.Key, HttpKnownHeaderNames.ContentEncoding, StringComparison.OrdinalIgnoreCase)) { - cookies = new Dictionary(); - foreach (var cookie in request.Cookies) - { - cookies.Add(cookie.Key, cookie.Value); - } + contentEncodingHeader = header.Value; } + } - IBodyData? body = null; - if (request.Body != null && BodyParser.ShouldParseBody(method, options.AllowBodyForAllHttpMethods == true)) + var cookies = new Dictionary(); + if (request.Cookies.Any()) + { + foreach (var cookie in request.Cookies) { - var bodyParserSettings = new BodyParserSettings - { - Stream = request.Body, - ContentType = request.ContentType, - DeserializeJson = !options.DisableJsonBodyParsing.GetValueOrDefault(false), - ContentEncoding = contentEncodingHeader?.FirstOrDefault(), - DecompressGZipAndDeflate = !options.DisableRequestBodyDecompressing.GetValueOrDefault(false) - }; - - body = await BodyParser.ParseAsync(bodyParserSettings).ConfigureAwait(false); + cookies.Add(cookie.Key, cookie.Value); } + } - return new RequestMessage( - options, - urlDetails, - method, - clientIP, - body, - headers, - cookies -#if USE_ASPNETCORE - , await request.HttpContext.Connection.GetClientCertificateAsync() -#endif - ) + IBodyData? body = null; + if (request.Body != null && BodyParser.ShouldParseBody(method, options.AllowBodyForAllHttpMethods == true)) + { + var bodyParserSettings = new BodyParserSettings { - DateTime = DateTime.UtcNow + Stream = request.Body, + ContentType = request.ContentType, + DeserializeJson = !options.DisableJsonBodyParsing.GetValueOrDefault(false), + ContentEncoding = contentEncodingHeader?.FirstOrDefault(), + DecompressGZipAndDeflate = !options.DisableRequestBodyDecompressing.GetValueOrDefault(false) }; + + body = await BodyParser.ParseAsync(bodyParserSettings).ConfigureAwait(false); } - private static (UrlDetails UrlDetails, string ClientIP) ParseRequest(IRequest request) + return new RequestMessage( + options, + urlDetails, + method, + clientIP, + body, + headers, + cookies, + httpVersion +#if USE_ASPNETCORE + , await request.HttpContext.Connection.GetClientCertificateAsync() +#endif + ) { + DateTime = DateTime.UtcNow + }; + } + + private static (UrlDetails UrlDetails, string ClientIP) ParseRequest(IRequest request) + { #if !USE_ASPNETCORE - var urlDetails = UrlUtils.Parse(request.Uri, request.PathBase); - var clientIP = request.RemoteIpAddress; + var urlDetails = UrlUtils.Parse(request.Uri, request.PathBase); + var clientIP = request.RemoteIpAddress; #else - var urlDetails = UrlUtils.Parse(new Uri(request.GetEncodedUrl()), request.PathBase); + var urlDetails = UrlUtils.Parse(new Uri(request.GetEncodedUrl()), request.PathBase); - var connection = request.HttpContext.Connection; - string clientIP; - if (connection.RemoteIpAddress is null) - { - clientIP = string.Empty; - } - else if (connection.RemoteIpAddress.IsIPv4MappedToIPv6) - { - clientIP = connection.RemoteIpAddress.MapToIPv4().ToString(); - } - else - { - clientIP = connection.RemoteIpAddress.ToString(); - } -#endif - return (urlDetails, clientIP); + var connection = request.HttpContext.Connection; + string clientIP; + if (connection.RemoteIpAddress is null) + { + clientIP = string.Empty; + } + else if (connection.RemoteIpAddress.IsIPv4MappedToIPv6) + { + clientIP = connection.RemoteIpAddress.MapToIPv4().ToString(); + } + else + { + clientIP = connection.RemoteIpAddress.ToString(); } +#endif + return (urlDetails, clientIP); } } \ No newline at end of file diff --git a/src/WireMock.Net/Owin/Mappers/OwinResponseMapper.cs b/src/WireMock.Net/Owin/Mappers/OwinResponseMapper.cs index 5062117fd..c1ceb39f4 100644 --- a/src/WireMock.Net/Owin/Mappers/OwinResponseMapper.cs +++ b/src/WireMock.Net/Owin/Mappers/OwinResponseMapper.cs @@ -13,6 +13,8 @@ using WireMock.ResponseBuilders; using WireMock.Types; using Stef.Validation; +using WireMock.Util; + #if !USE_ASPNETCORE using IResponse = Microsoft.Owin.IOwinResponse; #else @@ -62,11 +64,11 @@ public async Task MapAsync(IResponseMessage? responseMessage, IResponse response switch (responseMessage.FaultType) { case FaultType.EMPTY_RESPONSE: - bytes = IsFault(responseMessage) ? EmptyArray.Value : GetNormalBody(responseMessage); + bytes = IsFault(responseMessage) ? EmptyArray.Value : await GetNormalBodyAsync(responseMessage).ConfigureAwait(false); break; case FaultType.MALFORMED_RESPONSE_CHUNK: - bytes = GetNormalBody(responseMessage) ?? EmptyArray.Value; + bytes = await GetNormalBodyAsync(responseMessage).ConfigureAwait(false) ?? EmptyArray.Value; if (IsFault(responseMessage)) { bytes = bytes.Take(bytes.Length / 2).Union(_randomizerBytes.Generate()).ToArray(); @@ -74,18 +76,18 @@ public async Task MapAsync(IResponseMessage? responseMessage, IResponse response break; default: - bytes = GetNormalBody(responseMessage); + bytes = await GetNormalBodyAsync(responseMessage).ConfigureAwait(false); break; } var statusCodeType = responseMessage.StatusCode?.GetType(); switch (statusCodeType) { - case { } typeAsIntOrEnum when typeAsIntOrEnum == typeof(int) || typeAsIntOrEnum == typeof(int?) || typeAsIntOrEnum.GetTypeInfo().IsEnum: + case { } when statusCodeType == typeof(int) || statusCodeType == typeof(int?) || statusCodeType.GetTypeInfo().IsEnum: response.StatusCode = MapStatusCode((int)responseMessage.StatusCode!); break; - case { } typeAsString when typeAsString == typeof(string): + case { } when statusCodeType == typeof(string): // Note: this case will also match on null int.TryParse(responseMessage.StatusCode as string, out var result); response.StatusCode = MapStatusCode(result); @@ -108,6 +110,8 @@ public async Task MapAsync(IResponseMessage? responseMessage, IResponse response _options.Logger.Warn("Error writing response body. Exception : {0}", ex); } } + + SetResponseTrailingHeaders(responseMessage, response); } private int MapStatusCode(int code) @@ -125,7 +129,7 @@ private bool IsFault(IResponseMessage responseMessage) return responseMessage.FaultPercentage == null || _randomizerDouble.Generate() <= responseMessage.FaultPercentage; } - private byte[]? GetNormalBody(IResponseMessage responseMessage) + private async Task GetNormalBodyAsync(IResponseMessage responseMessage) { switch (responseMessage.BodyData?.DetectedBodyType) { @@ -138,6 +142,12 @@ private bool IsFault(IResponseMessage responseMessage) var jsonBody = JsonConvert.SerializeObject(responseMessage.BodyData.BodyAsJson, new JsonSerializerSettings { Formatting = formatting, NullValueHandling = NullValueHandling.Ignore }); return (responseMessage.BodyData.Encoding ?? _utf8NoBom).GetBytes(jsonBody); +#if PROTOBUF + case BodyType.ProtoBuf: + var protoDefinition = responseMessage.BodyData.ProtoDefinition?.Invoke().Text; + return await ProtoBufUtils.GetProtoBufMessageWithHeaderAsync(protoDefinition, responseMessage.BodyData.ProtoBufMessageType, responseMessage.BodyData.BodyAsJson).ConfigureAwait(false); +#endif + case BodyType.Bytes: return responseMessage.BodyData.BodyAsBytes; @@ -157,16 +167,17 @@ private static void SetResponseHeaders(IResponseMessage responseMessage, IRespon new[] { DateTime.UtcNow.ToString(CultureInfo.InvariantCulture.DateTimeFormat.RFC1123Pattern, CultureInfo.InvariantCulture) - }); + } + ); // Set other headers foreach (var item in responseMessage.Headers!) { var headerName = item.Key; var value = item.Value; - if (ResponseHeadersToFix.ContainsKey(headerName)) + if (ResponseHeadersToFix.TryGetValue(headerName, out var action)) { - ResponseHeadersToFix[headerName]?.Invoke(response, value); + action?.Invoke(response, value); } else { @@ -179,6 +190,34 @@ private static void SetResponseHeaders(IResponseMessage responseMessage, IRespon } } + private static void SetResponseTrailingHeaders(IResponseMessage responseMessage, IResponse response) + { + if (responseMessage.TrailingHeaders == null) + { + return; + } + +#if TRAILINGHEADERS + foreach (var item in responseMessage.TrailingHeaders) + { + var headerName = item.Key; + var value = item.Value; + if (ResponseHeadersToFix.TryGetValue(headerName, out var action)) + { + action?.Invoke(response, value); + } + else + { + // Check if this trailing header can be added to the response + if (response.SupportsTrailers() && !HttpKnownHeaderNames.IsRestrictedResponseHeader(headerName)) + { + response.AppendTrailer(headerName, new Microsoft.Extensions.Primitives.StringValues(value.ToArray())); + } + } + } +#endif + } + private static void AppendResponseHeader(IResponse response, string headerName, string[] values) { #if !USE_ASPNETCORE diff --git a/src/WireMock.Net/RequestBuilders/IBodyRequestBuilder.cs b/src/WireMock.Net/RequestBuilders/IBodyRequestBuilder.cs index 8bb6e92ef..49751bd45 100644 --- a/src/WireMock.Net/RequestBuilders/IBodyRequestBuilder.cs +++ b/src/WireMock.Net/RequestBuilders/IBodyRequestBuilder.cs @@ -9,7 +9,7 @@ namespace WireMock.RequestBuilders; /// /// The BodyRequestBuilder interface. /// -public interface IBodyRequestBuilder : IGraphQLRequestBuilder +public interface IBodyRequestBuilder : IProtoBufRequestBuilder { /// /// WithBody: IMatcher diff --git a/src/WireMock.Net/RequestBuilders/IGraphQLRequestBuilder.cs b/src/WireMock.Net/RequestBuilders/IGraphQLRequestBuilder.cs index d22401a41..7dc3cac28 100644 --- a/src/WireMock.Net/RequestBuilders/IGraphQLRequestBuilder.cs +++ b/src/WireMock.Net/RequestBuilders/IGraphQLRequestBuilder.cs @@ -26,6 +26,23 @@ public interface IGraphQLRequestBuilder : IMultiPartRequestBuilder /// The . IRequestBuilder WithGraphQLSchema(string schema, IDictionary? customScalars, MatchBehaviour matchBehaviour = MatchBehaviour.AcceptOnMatch); + /// + /// WithBodyAsGraphQL: The GraphQL schema as a string. + /// + /// The GraphQL schema. + /// The match behaviour. (Default is MatchBehaviour.AcceptOnMatch). + /// The . + IRequestBuilder WithBodyAsGraphQL(string schema, MatchBehaviour matchBehaviour = MatchBehaviour.AcceptOnMatch); + + /// + /// WithBodyAsGraphQL: The GraphQL schema as a string. + /// + /// The GraphQL schema. + /// A dictionary defining the custom scalars used in this schema. (optional) + /// The match behaviour. (Default is MatchBehaviour.AcceptOnMatch). + /// The . + IRequestBuilder WithBodyAsGraphQL(string schema, IDictionary? customScalars, MatchBehaviour matchBehaviour = MatchBehaviour.AcceptOnMatch); + #if GRAPHQL /// /// WithGraphQLSchema: The GraphQL schema as a ISchema. @@ -43,5 +60,22 @@ public interface IGraphQLRequestBuilder : IMultiPartRequestBuilder /// The match behaviour. (Default is MatchBehaviour.AcceptOnMatch). /// The . IRequestBuilder WithGraphQLSchema(GraphQL.Types.ISchema schema, IDictionary? customScalars, MatchBehaviour matchBehaviour = MatchBehaviour.AcceptOnMatch); + + /// + /// WithBodyAsGraphQL: The GraphQL schema as a ISchema. + /// + /// The GraphQL schema. + /// The match behaviour. (Default is MatchBehaviour.AcceptOnMatch). + /// The . + IRequestBuilder WithBodyAsGraphQL(GraphQL.Types.ISchema schema, MatchBehaviour matchBehaviour = MatchBehaviour.AcceptOnMatch); + + /// + /// WithBodyAsGraphQL: The GraphQL schema as a ISchema. + /// + /// The GraphQL schema. + /// A dictionary defining the custom scalars used in this schema. (optional) + /// The match behaviour. (Default is MatchBehaviour.AcceptOnMatch). + /// The . + IRequestBuilder WithBodyAsGraphQL(GraphQL.Types.ISchema schema, IDictionary? customScalars, MatchBehaviour matchBehaviour = MatchBehaviour.AcceptOnMatch); #endif } \ No newline at end of file diff --git a/src/WireMock.Net/RequestBuilders/IHttpVersionBuilder.cs b/src/WireMock.Net/RequestBuilders/IHttpVersionBuilder.cs new file mode 100644 index 000000000..b25c1bac2 --- /dev/null +++ b/src/WireMock.Net/RequestBuilders/IHttpVersionBuilder.cs @@ -0,0 +1,18 @@ +using WireMock.Matchers; +using WireMock.Matchers.Request; + +namespace WireMock.RequestBuilders; + +/// +/// The HttpVersionBuilder interface. +/// +public interface IHttpVersionBuilder : IRequestMatcher +{ + /// + /// WithHttpVersion + /// + /// The HTTP Version to match. + /// The match behaviour. (default = "AcceptOnMatch") + /// The . + IRequestBuilder WithHttpVersion(string version, MatchBehaviour matchBehaviour = MatchBehaviour.AcceptOnMatch); +} \ No newline at end of file diff --git a/src/WireMock.Net/RequestBuilders/IMultiPartRequestBuilder.cs b/src/WireMock.Net/RequestBuilders/IMultiPartRequestBuilder.cs index 75a07be6b..4db8bc175 100644 --- a/src/WireMock.Net/RequestBuilders/IMultiPartRequestBuilder.cs +++ b/src/WireMock.Net/RequestBuilders/IMultiPartRequestBuilder.cs @@ -6,7 +6,7 @@ namespace WireMock.RequestBuilders; /// /// The MultiPartRequestBuilder interface. /// -public interface IMultiPartRequestBuilder : IRequestMatcher +public interface IMultiPartRequestBuilder : IHttpVersionBuilder { /// /// WithMultiPart: IMatcher diff --git a/src/WireMock.Net/RequestBuilders/IProtoBufRequestBuilder.cs b/src/WireMock.Net/RequestBuilders/IProtoBufRequestBuilder.cs new file mode 100644 index 000000000..e13ec1e5d --- /dev/null +++ b/src/WireMock.Net/RequestBuilders/IProtoBufRequestBuilder.cs @@ -0,0 +1,45 @@ +using WireMock.Matchers; + +namespace WireMock.RequestBuilders; + +/// +/// The ProtoBufRequestBuilder interface. +/// +public interface IProtoBufRequestBuilder : IGraphQLRequestBuilder +{ + /// + /// WithGrpcProto + /// + /// The proto definition as text. + /// The full type of the protobuf (request/response) message object. Format is "{package-name}.{type-name}". + /// The match behaviour. (default = "AcceptOnMatch") + /// The . + IRequestBuilder WithBodyAsProtoBuf(string protoDefinition, string messageType, MatchBehaviour matchBehaviour = MatchBehaviour.AcceptOnMatch); + + /// + /// WithGrpcProto + /// + /// The proto definition as text. + /// The full type of the protobuf (request/response) message object. Format is "{package-name}.{type-name}". + /// The matcher to use to match the ProtoBuf as (json) object. + /// The match behaviour. (default = "AcceptOnMatch") + /// The . + IRequestBuilder WithBodyAsProtoBuf(string protoDefinition, string messageType, IObjectMatcher matcher, MatchBehaviour matchBehaviour = MatchBehaviour.AcceptOnMatch); + + /// + /// WithGrpcProto + /// + /// The full type of the protobuf (request/response) message object. Format is "{package-name}.{type-name}". + /// The match behaviour. (default = "AcceptOnMatch") + /// The . + IRequestBuilder WithBodyAsProtoBuf(string messageType, MatchBehaviour matchBehaviour = MatchBehaviour.AcceptOnMatch); + + /// + /// WithGrpcProto + /// + /// The full type of the protobuf (request/response) message object. Format is "{package-name}.{type-name}". + /// The matcher to use to match the ProtoBuf as (json) object. + /// The match behaviour. (default = "AcceptOnMatch") + /// The . + IRequestBuilder WithBodyAsProtoBuf(string messageType, IObjectMatcher matcher, MatchBehaviour matchBehaviour = MatchBehaviour.AcceptOnMatch); +} \ No newline at end of file diff --git a/src/WireMock.Net/RequestBuilders/Request.WithBodyAsProtoBuf.cs b/src/WireMock.Net/RequestBuilders/Request.WithBodyAsProtoBuf.cs new file mode 100644 index 000000000..2c3eb187b --- /dev/null +++ b/src/WireMock.Net/RequestBuilders/Request.WithBodyAsProtoBuf.cs @@ -0,0 +1,31 @@ +using WireMock.Matchers; +using WireMock.Matchers.Request; + +namespace WireMock.RequestBuilders; + +public partial class Request +{ + /// + public IRequestBuilder WithBodyAsProtoBuf(string protoDefinition, string messageType, MatchBehaviour matchBehaviour = MatchBehaviour.AcceptOnMatch) + { + return Add(new RequestMessageProtoBufMatcher(matchBehaviour, () => new (null, protoDefinition), messageType)); + } + + /// + public IRequestBuilder WithBodyAsProtoBuf(string protoDefinition, string messageType, IObjectMatcher matcher, MatchBehaviour matchBehaviour = MatchBehaviour.AcceptOnMatch) + { + return Add(new RequestMessageProtoBufMatcher(matchBehaviour, () => new(null, protoDefinition), messageType, matcher)); + } + + /// + public IRequestBuilder WithBodyAsProtoBuf(string messageType, MatchBehaviour matchBehaviour = MatchBehaviour.AcceptOnMatch) + { + return Add(new RequestMessageProtoBufMatcher(matchBehaviour, () => Mapping.ProtoDefinition!.Value, messageType)); + } + + /// + public IRequestBuilder WithBodyAsProtoBuf(string messageType, IObjectMatcher matcher, MatchBehaviour matchBehaviour = MatchBehaviour.AcceptOnMatch) + { + return Add(new RequestMessageProtoBufMatcher(matchBehaviour, () => Mapping.ProtoDefinition!.Value, messageType, matcher)); + } +} \ No newline at end of file diff --git a/src/WireMock.Net/RequestBuilders/Request.WithGraphQL.cs b/src/WireMock.Net/RequestBuilders/Request.WithGraphQL.cs deleted file mode 100644 index 164ba3327..000000000 --- a/src/WireMock.Net/RequestBuilders/Request.WithGraphQL.cs +++ /dev/null @@ -1,39 +0,0 @@ -using System.Collections.Generic; -using System; -using WireMock.Matchers; -using WireMock.Matchers.Request; - -namespace WireMock.RequestBuilders; - -public partial class Request -{ - /// - public IRequestBuilder WithGraphQLSchema(string schema, MatchBehaviour matchBehaviour = MatchBehaviour.AcceptOnMatch) - { - _requestMatchers.Add(new RequestMessageGraphQLMatcher(matchBehaviour, schema)); - return this; - } - - /// - public IRequestBuilder WithGraphQLSchema(string schema, IDictionary? customScalars, MatchBehaviour matchBehaviour = MatchBehaviour.AcceptOnMatch) - { - _requestMatchers.Add(new RequestMessageGraphQLMatcher(matchBehaviour, schema, customScalars)); - return this; - } - -#if GRAPHQL - /// - public IRequestBuilder WithGraphQLSchema(GraphQL.Types.ISchema schema, MatchBehaviour matchBehaviour = MatchBehaviour.AcceptOnMatch) - { - _requestMatchers.Add(new RequestMessageGraphQLMatcher(matchBehaviour, schema)); - return this; - } - - /// - public IRequestBuilder WithGraphQLSchema(GraphQL.Types.ISchema schema, IDictionary? customScalars, MatchBehaviour matchBehaviour = MatchBehaviour.AcceptOnMatch) - { - _requestMatchers.Add(new RequestMessageGraphQLMatcher(matchBehaviour, schema, customScalars)); - return this; - } -#endif -} \ No newline at end of file diff --git a/src/WireMock.Net/RequestBuilders/Request.WithGraphQLSchema.cs b/src/WireMock.Net/RequestBuilders/Request.WithGraphQLSchema.cs new file mode 100644 index 000000000..43113c5b1 --- /dev/null +++ b/src/WireMock.Net/RequestBuilders/Request.WithGraphQLSchema.cs @@ -0,0 +1,59 @@ +using System.Collections.Generic; +using System; +using WireMock.Matchers; +using WireMock.Matchers.Request; + +namespace WireMock.RequestBuilders; + +public partial class Request +{ + /// + public IRequestBuilder WithGraphQLSchema(string schema, MatchBehaviour matchBehaviour = MatchBehaviour.AcceptOnMatch) + { + return WithBodyAsGraphQL(schema, matchBehaviour); + } + + /// + public IRequestBuilder WithGraphQLSchema(string schema, IDictionary? customScalars, MatchBehaviour matchBehaviour = MatchBehaviour.AcceptOnMatch) + { + return WithBodyAsGraphQL(schema, customScalars, matchBehaviour); + } + + /// + public IRequestBuilder WithBodyAsGraphQL(string schema, MatchBehaviour matchBehaviour = MatchBehaviour.AcceptOnMatch) + { + return Add(new RequestMessageGraphQLMatcher(matchBehaviour, schema)); + } + + /// + public IRequestBuilder WithBodyAsGraphQL(string schema, IDictionary? customScalars, MatchBehaviour matchBehaviour = MatchBehaviour.AcceptOnMatch) + { + return Add(new RequestMessageGraphQLMatcher(matchBehaviour, schema, customScalars)); + } + +#if GRAPHQL + /// + public IRequestBuilder WithGraphQLSchema(GraphQL.Types.ISchema schema, MatchBehaviour matchBehaviour = MatchBehaviour.AcceptOnMatch) + { + return WithBodyAsGraphQL(schema, matchBehaviour); + } + + /// + public IRequestBuilder WithGraphQLSchema(GraphQL.Types.ISchema schema, IDictionary? customScalars, MatchBehaviour matchBehaviour = MatchBehaviour.AcceptOnMatch) + { + return WithBodyAsGraphQL(schema, customScalars, matchBehaviour); + } + + /// + public IRequestBuilder WithBodyAsGraphQL(GraphQL.Types.ISchema schema, MatchBehaviour matchBehaviour = MatchBehaviour.AcceptOnMatch) + { + return Add(new RequestMessageGraphQLMatcher(matchBehaviour, schema)); + } + + /// + public IRequestBuilder WithBodyAsGraphQL(GraphQL.Types.ISchema schema, IDictionary? customScalars, MatchBehaviour matchBehaviour = MatchBehaviour.AcceptOnMatch) + { + return Add(new RequestMessageGraphQLMatcher(matchBehaviour, schema, customScalars)); + } +#endif +} \ No newline at end of file diff --git a/src/WireMock.Net/RequestBuilders/Request.WithHttpVersion.cs b/src/WireMock.Net/RequestBuilders/Request.WithHttpVersion.cs new file mode 100644 index 000000000..ef1844306 --- /dev/null +++ b/src/WireMock.Net/RequestBuilders/Request.WithHttpVersion.cs @@ -0,0 +1,13 @@ +using WireMock.Matchers; +using WireMock.Matchers.Request; + +namespace WireMock.RequestBuilders; + +public partial class Request +{ + /// + public IRequestBuilder WithHttpVersion(string version, MatchBehaviour matchBehaviour = MatchBehaviour.AcceptOnMatch) + { + return Add(new RequestMessageHttpVersionMatcher(matchBehaviour, version)); + } +} \ No newline at end of file diff --git a/src/WireMock.Net/RequestBuilders/Request.cs b/src/WireMock.Net/RequestBuilders/Request.cs index 10255d5bd..f375b6987 100644 --- a/src/WireMock.Net/RequestBuilders/Request.cs +++ b/src/WireMock.Net/RequestBuilders/Request.cs @@ -16,6 +16,11 @@ public partial class Request : RequestMessageCompositeMatcher, IRequestBuilder { private readonly IList _requestMatchers; + /// + /// The link back to the Mapping. + /// + public IMapping Mapping { get; set; } = null!; + /// /// Creates this instance. /// @@ -63,4 +68,15 @@ public IList GetRequestMessageMatchers() where T : IRequestMatcher { return _requestMatchers.OfType().FirstOrDefault(func); } + + private IRequestBuilder Add(T requestMatcher) where T : IRequestMatcher + { + foreach (var existing in _requestMatchers.OfType().ToArray()) + { + _requestMatchers.Remove(existing); + } + + _requestMatchers.Add(requestMatcher); + return this; + } } \ No newline at end of file diff --git a/src/WireMock.Net/RequestMessage.cs b/src/WireMock.Net/RequestMessage.cs index 087fb5926..20b4518e9 100644 --- a/src/WireMock.Net/RequestMessage.cs +++ b/src/WireMock.Net/RequestMessage.cs @@ -4,7 +4,6 @@ using System.Collections.Generic; using System.Linq; using System.Net; -using Newtonsoft.Json; #if USE_ASPNETCORE using System.Security.Cryptography.X509Certificates; #endif @@ -51,6 +50,9 @@ public class RequestMessage : IRequestMessage /// public string Method { get; } + /// + public string HttpVersion { get; } + /// public IDictionary>? Headers { get; } @@ -73,14 +75,14 @@ public class RequestMessage : IRequestMessage public string? Body { get; } /// - public object? BodyAsJson { get; } + public object? BodyAsJson { get; set; } /// public byte[]? BodyAsBytes { get; } #if MIMEKIT /// - [JsonIgnore] // Issue 1001 + [Newtonsoft.Json.JsonIgnore] // Issue 1001 public object? BodyAsMimeMessage { get; } #endif @@ -125,11 +127,13 @@ internal RequestMessage( internal RequestMessage( IWireMockMiddlewareOptions? options, - UrlDetails urlDetails, string method, + UrlDetails urlDetails, + string method, string clientIP, IBodyData? bodyData = null, IDictionary? headers = null, - IDictionary? cookies = null + IDictionary? cookies = null, + string httpVersion = "1.1" #if USE_ASPNETCORE , X509Certificate2? clientCertificate = null #endif @@ -152,6 +156,7 @@ internal RequestMessage( AbsolutePathSegments = AbsolutePath.Split('/').Skip(1).ToArray(); Method = method; + HttpVersion = httpVersion; ClientIP = clientIP; BodyData = bodyData; diff --git a/src/WireMock.Net/ResponseBuilders/IBodyResponseBuilder.cs b/src/WireMock.Net/ResponseBuilders/IBodyResponseBuilder.cs index 65318d21b..709259830 100644 --- a/src/WireMock.Net/ResponseBuilders/IBodyResponseBuilder.cs +++ b/src/WireMock.Net/ResponseBuilders/IBodyResponseBuilder.cs @@ -91,18 +91,50 @@ public interface IBodyResponseBuilder : IFaultResponseBuilder /// WithBody : Create a string response based on a object (which will be converted to a JSON string using the ). /// /// The body. - /// The JsonConverter. + /// The . /// The [optional]. /// A . - IResponseBuilder WithBody(object body, IJsonConverter converter, JsonConverterOptions? options = null); + IResponseBuilder WithBody(object body, IJsonConverter jsonConverter, JsonConverterOptions? options = null); /// /// WithBody : Create a string response based on a object (which will be converted to a JSON string using the ). /// /// The body. /// The body encoding, can be null. - /// The JsonConverter. + /// The . /// The [optional]. /// A . - IResponseBuilder WithBody(object body, Encoding? encoding, IJsonConverter converter, JsonConverterOptions? options = null); + IResponseBuilder WithBody(object body, Encoding? encoding, IJsonConverter jsonConverter, JsonConverterOptions? options = null); + + /// + /// WithBody : Create a ProtoBuf byte[] response based on a proto definition, message type and the value. + /// + /// The proto definition as text. + /// The full type of the protobuf (request/response) message object. Format is "{package-name}.{type-name}". + /// The object to convert to protobuf byte[]. + /// The [optional]. Default value is NewtonsoftJsonConverter. + /// The [optional]. + /// A . + IResponseBuilder WithBodyAsProtoBuf( + string protoDefinition, + string messageType, + object value, + IJsonConverter? jsonConverter = null, + JsonConverterOptions? options = null + ); + + /// + /// WithBody : Create a ProtoBuf byte[] response based on a proto definition, message type and the value. + /// + /// The full type of the protobuf (request/response) message object. Format is "{package-name}.{type-name}". + /// The object to convert to protobuf byte[]. + /// The [optional]. Default value is NewtonsoftJsonConverter. + /// The [optional]. + /// A . + IResponseBuilder WithBodyAsProtoBuf( + string messageType, + object value, + IJsonConverter? jsonConverter = null, + JsonConverterOptions? options = null + ); } \ No newline at end of file diff --git a/src/WireMock.Net/ResponseBuilders/IHeadersResponseBuilder.cs b/src/WireMock.Net/ResponseBuilders/IHeadersResponseBuilder.cs index 812a70a23..7d195c532 100644 --- a/src/WireMock.Net/ResponseBuilders/IHeadersResponseBuilder.cs +++ b/src/WireMock.Net/ResponseBuilders/IHeadersResponseBuilder.cs @@ -9,7 +9,7 @@ namespace WireMock.ResponseBuilders; public interface IHeadersResponseBuilder : IBodyResponseBuilder { /// - /// The with header. + /// The WithHeader. /// /// The name. /// The values. @@ -17,23 +17,52 @@ public interface IHeadersResponseBuilder : IBodyResponseBuilder IResponseBuilder WithHeader(string name, params string[] values); /// - /// The with headers. + /// The WithHeaders. /// /// The headers. /// The . IResponseBuilder WithHeaders(IDictionary headers); /// - /// The with headers. + /// The WithHeaders. /// /// The headers. /// The . IResponseBuilder WithHeaders(IDictionary headers); /// - /// The with headers. + /// The WithHeaders. /// /// The headers. /// The . IResponseBuilder WithHeaders(IDictionary> headers); + + /// + /// The WithTrailingHeader. + /// + /// The name. + /// The values. + /// The . + IResponseBuilder WithTrailingHeader(string name, params string[] values); + + /// + /// The WithTrailingHeaders. + /// + /// The headers. + /// The . + IResponseBuilder WithTrailingHeaders(IDictionary headers); + + /// + /// The WithTrailingHeaders. + /// + /// The headers. + /// The . + IResponseBuilder WithTrailingHeaders(IDictionary headers); + + /// + /// The WithTrailingHeaders. + /// + /// The headers. + /// The . + IResponseBuilder WithTrailingHeaders(IDictionary> headers); } \ No newline at end of file diff --git a/src/WireMock.Net/ResponseBuilders/Response.WithBody.cs b/src/WireMock.Net/ResponseBuilders/Response.WithBody.cs index ea95b7da9..1fd6c451e 100644 --- a/src/WireMock.Net/ResponseBuilders/Response.WithBody.cs +++ b/src/WireMock.Net/ResponseBuilders/Response.WithBody.cs @@ -3,6 +3,7 @@ using System.Threading.Tasks; using JsonConverter.Abstractions; using Stef.Validation; +using WireMock.Exceptions; using WireMock.Types; using WireMock.Util; @@ -185,25 +186,79 @@ public IResponseBuilder WithBodyAsJson(Func> bodyF } /// - public IResponseBuilder WithBody(object body, IJsonConverter converter, JsonConverterOptions? options = null) + public IResponseBuilder WithBody(object body, IJsonConverter jsonConverter, JsonConverterOptions? options = null) { - return WithBody(body, null, converter, options); + return WithBody(body, null, jsonConverter, options); } /// - public IResponseBuilder WithBody(object body, Encoding? encoding, IJsonConverter converter, JsonConverterOptions? options = null) + public IResponseBuilder WithBody(object body, Encoding? encoding, IJsonConverter jsonConverter, JsonConverterOptions? options = null) { Guard.NotNull(body); - Guard.NotNull(converter); + Guard.NotNull(jsonConverter); ResponseMessage.BodyDestination = null; ResponseMessage.BodyData = new BodyData { Encoding = encoding, DetectedBodyType = BodyType.String, - BodyAsString = converter.Serialize(body, options) + BodyAsString = jsonConverter.Serialize(body, options) }; return this; } + + /// + public IResponseBuilder WithBodyAsProtoBuf( + string protoDefinition, + string messageType, + object value, + IJsonConverter? jsonConverter = null, + JsonConverterOptions? options = null + ) + { + Guard.NotNullOrWhiteSpace(protoDefinition); + Guard.NotNullOrWhiteSpace(messageType); + Guard.NotNull(value); + +#if !PROTOBUF + throw new System.NotSupportedException("The WithBodyAsProtoBuf method can not be used for .NETStandard1.3 or .NET Framework 4.6.1 or lower."); +#else + ResponseMessage.BodyDestination = null; + ResponseMessage.BodyData = new BodyData + { + DetectedBodyType = BodyType.ProtoBuf, + BodyAsJson = value, + ProtoDefinition = () => new (null, protoDefinition), + ProtoBufMessageType = messageType + }; +#endif + return this; + } + + /// + public IResponseBuilder WithBodyAsProtoBuf( + string messageType, + object value, + IJsonConverter? jsonConverter = null, + JsonConverterOptions? options = null + ) + { + Guard.NotNullOrWhiteSpace(messageType); + Guard.NotNull(value); + +#if !PROTOBUF + throw new System.NotSupportedException("The WithBodyAsProtoBuf method can not be used for .NETStandard1.3 or .NET Framework 4.6.1 or lower."); +#else + ResponseMessage.BodyDestination = null; + ResponseMessage.BodyData = new BodyData + { + DetectedBodyType = BodyType.ProtoBuf, + BodyAsJson = value, + ProtoDefinition = () => Mapping.ProtoDefinition ?? throw new WireMockException("ProtoDefinition cannot be resolved. You probably forgot to call .WithProtoDefinition(...) on the mapping."), + ProtoBufMessageType = messageType + }; +#endif + return this; + } } \ No newline at end of file diff --git a/src/WireMock.Net/ResponseBuilders/Response.WithHeaders.cs b/src/WireMock.Net/ResponseBuilders/Response.WithHeaders.cs new file mode 100644 index 000000000..08d2be926 --- /dev/null +++ b/src/WireMock.Net/ResponseBuilders/Response.WithHeaders.cs @@ -0,0 +1,101 @@ +using System.Collections.Generic; +using System.Linq; +using Stef.Validation; +using WireMock.Types; + +namespace WireMock.ResponseBuilders; + +public partial class Response +{ + /// + public IResponseBuilder WithHeader(string name, params string[] values) + { + Guard.NotNull(name); + + ResponseMessage.AddHeader(name, values); + return this; + } + + /// + public IResponseBuilder WithHeaders(IDictionary headers) + { + Guard.NotNull(headers); + + ResponseMessage.Headers = headers.ToDictionary(header => header.Key, header => new WireMockList(header.Value)); + return this; + } + + /// + public IResponseBuilder WithHeaders(IDictionary headers) + { + Guard.NotNull(headers); + + ResponseMessage.Headers = headers.ToDictionary(header => header.Key, header => new WireMockList(header.Value)); + return this; + } + + /// + public IResponseBuilder WithHeaders(IDictionary> headers) + { + Guard.NotNull(headers); + + ResponseMessage.Headers = headers; + return this; + } + + /// + public IResponseBuilder WithTrailingHeader(string name, params string[] values) + { +#if !TRAILINGHEADERS + throw new System.NotSupportedException("The WithBodyAsProtoBuf method can not be used for .NETStandard1.3 or .NET Framework 4.6.1 or lower."); +#else + + Guard.NotNull(name); + + ResponseMessage.AddTrailingHeader(name, values); + return this; +#endif + } + + /// + public IResponseBuilder WithTrailingHeaders(IDictionary headers) + { +#if !TRAILINGHEADERS + throw new System.NotSupportedException("The WithBodyAsProtoBuf method can not be used for .NETStandard1.3 or .NET Framework 4.6.1 or lower."); +#else + + Guard.NotNull(headers); + + ResponseMessage.TrailingHeaders = headers.ToDictionary(header => header.Key, header => new WireMockList(header.Value)); + return this; +#endif + } + + /// + public IResponseBuilder WithTrailingHeaders(IDictionary headers) + { +#if !TRAILINGHEADERS + throw new System.NotSupportedException("The WithBodyAsProtoBuf method can not be used for .NETStandard1.3 or .NET Framework 4.6.1 or lower."); +#else + + Guard.NotNull(headers); + + ResponseMessage.TrailingHeaders = headers.ToDictionary(header => header.Key, header => new WireMockList(header.Value)); + return this; +#endif + } + + /// + public IResponseBuilder WithTrailingHeaders(IDictionary> headers) + { +#if !TRAILINGHEADERS + throw new System.NotSupportedException("The WithBodyAsProtoBuf method can not be used for .NETStandard1.3 or .NET Framework 4.6.1 or lower."); +#else + + Guard.NotNull(headers); + + ResponseMessage.TrailingHeaders = headers; + return this; +#endif + } +} \ No newline at end of file diff --git a/src/WireMock.Net/ResponseBuilders/Response.cs b/src/WireMock.Net/ResponseBuilders/Response.cs index 9540aa621..7932dcfde 100644 --- a/src/WireMock.Net/ResponseBuilders/Response.cs +++ b/src/WireMock.Net/ResponseBuilders/Response.cs @@ -1,19 +1,20 @@ // This source file is based on mock4net by Alexandre Victoor which is licensed under the Apache 2.0 License. // For more details see 'mock4net/LICENSE.txt' and 'mock4net/readme.md' in this project root. using System; -using System.Collections.Generic; -using System.Linq; using System.Net; using System.Threading; using System.Threading.Tasks; using JetBrains.Annotations; using Stef.Validation; +using WireMock.Matchers.Request; using WireMock.Proxy; +using WireMock.RequestBuilders; using WireMock.Settings; using WireMock.Transformers; using WireMock.Transformers.Handlebars; using WireMock.Transformers.Scriban; using WireMock.Types; +using WireMock.Util; namespace WireMock.ResponseBuilders; @@ -26,6 +27,11 @@ public partial class Response : IResponseBuilder private TimeSpan? _delay; + /// + /// The link back to the mapping. + /// + public IMapping Mapping { get; set; } = null!; + /// /// The minimum random delay in milliseconds. /// @@ -112,7 +118,7 @@ private Response(ResponseMessage responseMessage) { ResponseMessage = responseMessage; } - + /// [PublicAPI] public IResponseBuilder WithStatusCode(int code) @@ -156,42 +162,6 @@ public IResponseBuilder WithNotFound() return WithStatusCode((int)HttpStatusCode.NotFound); } - /// - public IResponseBuilder WithHeader(string name, params string[] values) - { - Guard.NotNull(name); - - ResponseMessage.AddHeader(name, values); - return this; - } - - /// - public IResponseBuilder WithHeaders(IDictionary headers) - { - Guard.NotNull(headers); - - ResponseMessage.Headers = headers.ToDictionary(header => header.Key, header => new WireMockList(header.Value)); - return this; - } - - /// - public IResponseBuilder WithHeaders(IDictionary headers) - { - Guard.NotNull(headers); - - ResponseMessage.Headers = headers.ToDictionary(header => header.Key, header => new WireMockList(header.Value)); - return this; - } - - /// - public IResponseBuilder WithHeaders(IDictionary> headers) - { - Guard.NotNull(headers); - - ResponseMessage.Headers = headers; - return this; - } - /// public IResponseBuilder WithTransformer(bool transformContentFromBodyAsFile) { @@ -304,10 +274,30 @@ string RemoveFirstOccurrence(string source, string find) { responseMessage.Headers = ResponseMessage.Headers; } + + // Copy TrailingHeaders from ResponseMessage (if defined) + if (ResponseMessage.TrailingHeaders?.Count > 0) + { + responseMessage.TrailingHeaders = ResponseMessage.TrailingHeaders; + } } if (UseTransformer) { + // Check if the body matcher is a RequestMessageProtoBufMatcher and try to to decode the byte-array to a BodyAsJson. + if (mapping.RequestMatcher is Request requestMatcher && requestMessage is RequestMessage request) + { + var protoBufMatcher = requestMatcher.GetRequestMessageMatcher()?.Matcher; + if (protoBufMatcher != null) + { + var decoded = await protoBufMatcher.DecodeAsync(request.BodyData?.BodyAsBytes).ConfigureAwait(false); + if (decoded != null) + { + request.BodyAsJson = JsonUtils.ConvertValueToJToken(decoded); + } + } + } + ITransformer responseMessageTransformer; switch (TransformerType) { diff --git a/src/WireMock.Net/ResponseMessage.cs b/src/WireMock.Net/ResponseMessage.cs index e1c258b2f..6596f7f7e 100644 --- a/src/WireMock.Net/ResponseMessage.cs +++ b/src/WireMock.Net/ResponseMessage.cs @@ -14,9 +14,12 @@ namespace WireMock; /// public class ResponseMessage : IResponseMessage { - /// + /// public IDictionary>? Headers { get; set; } = new Dictionary>(); + /// + public IDictionary>? TrailingHeaders { get; set; } = new Dictionary>(); + /// public object? StatusCode { get; set; } @@ -35,23 +38,43 @@ public class ResponseMessage : IResponseMessage /// public double? FaultPercentage { get; set; } - /// + /// public void AddHeader(string name, string value) { Headers ??= new Dictionary>(); Headers.Add(name, value); } - /// + /// public void AddHeader(string name, params string[] values) { Guard.NotNullOrEmpty(values); Headers ??= new Dictionary>(); - var newHeaderValues = Headers.TryGetValue(name, out WireMockList? existingValues) + var newHeaderValues = Headers.TryGetValue(name, out var existingValues) ? values.Union(existingValues).ToArray() : values; Headers[name] = newHeaderValues; } + + /// + public void AddTrailingHeader(string name, string value) + { + TrailingHeaders ??= new Dictionary>(); + TrailingHeaders.Add(name, value); + } + + /// + public void AddTrailingHeader(string name, params string[] values) + { + Guard.NotNullOrEmpty(values); + + TrailingHeaders ??= new Dictionary>(); + var newHeaderValues = TrailingHeaders.TryGetValue(name, out var existingValues) + ? values.Union(existingValues).ToArray() + : values; + + TrailingHeaders[name] = newHeaderValues; + } } \ No newline at end of file diff --git a/src/WireMock.Net/ResponseProviders/DynamicAsyncResponseProvider.cs b/src/WireMock.Net/ResponseProviders/DynamicAsyncResponseProvider.cs index c70a06d5e..90201a34b 100644 --- a/src/WireMock.Net/ResponseProviders/DynamicAsyncResponseProvider.cs +++ b/src/WireMock.Net/ResponseProviders/DynamicAsyncResponseProvider.cs @@ -1,5 +1,6 @@ using System; using System.Threading.Tasks; +using Stef.Validation; using WireMock.Settings; namespace WireMock.ResponseProviders; @@ -10,7 +11,7 @@ internal class DynamicAsyncResponseProvider : IResponseProvider public DynamicAsyncResponseProvider(Func> responseMessageFunc) { - _responseMessageFunc = responseMessageFunc; + _responseMessageFunc = Guard.NotNull(responseMessageFunc); } public async Task<(IResponseMessage Message, IMapping? Mapping)> ProvideResponseAsync(IMapping mapping, IRequestMessage requestMessage, WireMockServerSettings settings) diff --git a/src/WireMock.Net/ResponseProviders/DynamicResponseProvider.cs b/src/WireMock.Net/ResponseProviders/DynamicResponseProvider.cs index ecf0bda9d..2c151a1e9 100644 --- a/src/WireMock.Net/ResponseProviders/DynamicResponseProvider.cs +++ b/src/WireMock.Net/ResponseProviders/DynamicResponseProvider.cs @@ -1,5 +1,6 @@ using System; using System.Threading.Tasks; +using Stef.Validation; using WireMock.Settings; namespace WireMock.ResponseProviders; @@ -10,7 +11,7 @@ internal class DynamicResponseProvider : IResponseProvider public DynamicResponseProvider(Func responseMessageFunc) { - _responseMessageFunc = responseMessageFunc; + _responseMessageFunc = Guard.NotNull(responseMessageFunc); } public Task<(IResponseMessage Message, IMapping? Mapping)> ProvideResponseAsync(IMapping mapping, IRequestMessage requestMessage, WireMockServerSettings settings) diff --git a/src/WireMock.Net/ResponseProviders/ProxyAsyncResponseProvider.cs b/src/WireMock.Net/ResponseProviders/ProxyAsyncResponseProvider.cs index 9aab96024..2cc6d853a 100644 --- a/src/WireMock.Net/ResponseProviders/ProxyAsyncResponseProvider.cs +++ b/src/WireMock.Net/ResponseProviders/ProxyAsyncResponseProvider.cs @@ -1,5 +1,6 @@ using System; using System.Threading.Tasks; +using Stef.Validation; using WireMock.Settings; namespace WireMock.ResponseProviders; @@ -11,8 +12,8 @@ internal class ProxyAsyncResponseProvider : IResponseProvider public ProxyAsyncResponseProvider(Func> responseMessageFunc, WireMockServerSettings settings) { - _responseMessageFunc = responseMessageFunc; - _settings = settings; + _responseMessageFunc = Guard.NotNull(responseMessageFunc); + _settings = Guard.NotNull(settings); } public async Task<(IResponseMessage Message, IMapping? Mapping)> ProvideResponseAsync(IMapping mapping, IRequestMessage requestMessage, WireMockServerSettings settings) diff --git a/src/WireMock.Net/Serialization/LogEntryMapper.cs b/src/WireMock.Net/Serialization/LogEntryMapper.cs index 7fbc84b4e..61e7a766d 100644 --- a/src/WireMock.Net/Serialization/LogEntryMapper.cs +++ b/src/WireMock.Net/Serialization/LogEntryMapper.cs @@ -32,6 +32,7 @@ public LogEntryModel Map(ILogEntry logEntry) ProxyUrl = logEntry.RequestMessage.ProxyUrl, Query = logEntry.RequestMessage.Query, Method = logEntry.RequestMessage.Method, + HttpVersion = logEntry.RequestMessage.HttpVersion, Headers = logEntry.RequestMessage.Headers, Cookies = logEntry.RequestMessage.Cookies }; diff --git a/src/WireMock.Net/Serialization/MappingConverter.cs b/src/WireMock.Net/Serialization/MappingConverter.cs index c7cac97ab..a83eabf1c 100644 --- a/src/WireMock.Net/Serialization/MappingConverter.cs +++ b/src/WireMock.Net/Serialization/MappingConverter.cs @@ -14,7 +14,6 @@ using WireMock.Models; using WireMock.RequestBuilders; using WireMock.ResponseBuilders; -using WireMock.Settings; using WireMock.Types; using WireMock.Util; @@ -48,8 +47,10 @@ public string ToCSharpCode(IMapping mapping, MappingConverterSettings? settings var paramsMatchers = request.GetRequestMessageMatchers(); var methodMatcher = request.GetRequestMessageMatcher(); var requestMessageBodyMatcher = request.GetRequestMessageMatcher(); + var requestMessageHttpVersionMatcher = request.GetRequestMessageMatcher(); var requestMessageGraphQLMatcher = request.GetRequestMessageMatcher(); var requestMessageMultiPartMatcher = request.GetRequestMessageMatcher(); + var requestMessageProtoBufMatcher = request.GetRequestMessageMatcher(); var sb = new StringBuilder(); @@ -108,6 +109,11 @@ public string ToCSharpCode(IMapping mapping, MappingConverterSettings? settings sb.AppendLine($" .WithCookie(\"{cookieMatcher.Name}\", {ToValueArguments(GetStringArray(cookieMatcher.Matchers!))}, true)"); } + if (requestMessageHttpVersionMatcher?.HttpVersion != null) + { + sb.AppendLine($" .WithHttpVersion({requestMessageHttpVersionMatcher.HttpVersion})"); + } + #if GRAPHQL if (requestMessageGraphQLMatcher is { Matchers: { } }) { @@ -128,6 +134,13 @@ public string ToCSharpCode(IMapping mapping, MappingConverterSettings? settings } #endif +#if PROTOBUF + if (requestMessageProtoBufMatcher is { Matcher: { } }) + { + sb.AppendLine(" // .WithBodyAsProtoBuf() is not yet supported"); + } +#endif + if (requestMessageBodyMatcher is { Matchers: { } }) { if (requestMessageBodyMatcher.Matchers.OfType().FirstOrDefault() is { } wildcardMatcher && wildcardMatcher.GetPatterns().Any()) @@ -182,6 +195,14 @@ public string ToCSharpCode(IMapping mapping, MappingConverterSettings? settings } } + if (response.ResponseMessage.TrailingHeaders is { }) + { + foreach (var header in response.ResponseMessage.TrailingHeaders) + { + sb.AppendLine($" .WithTrailingHeader(\"{header.Key}\", {ToValueArguments(header.Value.ToArray())})"); + } + } + if (response.ResponseMessage.BodyData is { } bodyData) { switch (response.ResponseMessage.BodyData.DetectedBodyType) @@ -190,6 +211,7 @@ public string ToCSharpCode(IMapping mapping, MappingConverterSettings? settings case BodyType.FormUrlEncoded: sb.AppendLine($" .WithBody({ToCSharpStringLiteral(bodyData.BodyAsString)})"); break; + case BodyType.Json: if (bodyData.BodyAsJson is string bodyStringValue) { @@ -239,6 +261,8 @@ public MappingModel ToMappingModel(IMapping mapping) var bodyMatcher = request.GetRequestMessageMatcher(); var graphQLMatcher = request.GetRequestMessageMatcher(); var multiPartMatcher = request.GetRequestMessageMatcher(); + var protoBufMatcher = request.GetRequestMessageMatcher(); + var httpVersionMatcher = request.GetRequestMessageMatcher(); var mappingModel = new MappingModel { @@ -253,6 +277,7 @@ public MappingModel ToMappingModel(IMapping mapping) WhenStateIs = mapping.ExecutionConditionState, SetStateTo = mapping.NextState, Data = mapping.Data, + ProtoDefinition = mapping.ProtoDefinition?.Value, Probability = mapping.Probability, Request = new RequestModel { @@ -290,6 +315,11 @@ public MappingModel ToMappingModel(IMapping mapping) mappingModel.Request.MethodsMatchOperator = methodMatcher.Methods.Length > 1 ? methodMatcher.MatchOperator.ToString() : null; } + if (httpVersionMatcher?.HttpVersion != null) + { + mappingModel.Request.HttpVersion = httpVersionMatcher.HttpVersion; + } + if (clientIPMatcher is { Matchers: { } }) { var clientIPMatchers = _mapper.Map(clientIPMatcher.Matchers); @@ -329,7 +359,7 @@ public MappingModel ToMappingModel(IMapping mapping) mappingModel.Response.Delay = (int?)(response.Delay == Timeout.InfiniteTimeSpan ? TimeSpan.MaxValue.TotalMilliseconds : response.Delay?.TotalMilliseconds); } - var nonNullableWebHooks = mapping.Webhooks?.Where(wh => wh != null).ToArray() ?? EmptyArray.Value; + var nonNullableWebHooks = mapping.Webhooks?.ToArray() ?? EmptyArray.Value; if (nonNullableWebHooks.Length == 1) { mappingModel.Webhook = WebhookMapper.Map(nonNullableWebHooks[0]); @@ -339,20 +369,40 @@ public MappingModel ToMappingModel(IMapping mapping) mappingModel.Webhooks = mapping.Webhooks.Select(WebhookMapper.Map).ToArray(); } - var bodyMatchers = multiPartMatcher?.Matchers ?? graphQLMatcher?.Matchers ?? bodyMatcher?.Matchers; - var matchOperator = multiPartMatcher?.MatchOperator ?? graphQLMatcher?.MatchOperator ?? bodyMatcher?.MatchOperator; + var bodyMatchers = + protoBufMatcher?.Matcher != null ? new[] { protoBufMatcher.Matcher } : null ?? + multiPartMatcher?.Matchers ?? + graphQLMatcher?.Matchers ?? + bodyMatcher?.Matchers; - if (bodyMatchers != null && matchOperator != null) + var matchOperator = + multiPartMatcher?.MatchOperator ?? + graphQLMatcher?.MatchOperator ?? + bodyMatcher?.MatchOperator ?? + MatchOperator.Or; + + if (bodyMatchers != null) { + void AfterMap(MatcherModel matcherModel) + { +#if PROTOBUF + // In case the ProtoDefinition is defined at the Mapping level, clear the Pattern at the Matcher level + if (bodyMatchers?.OfType().Any() == true && mappingModel.ProtoDefinition != null) + { + matcherModel.Pattern = null; + } +#endif + } + mappingModel.Request.Body = new BodyModel(); if (bodyMatchers.Length == 1) { - mappingModel.Request.Body.Matcher = _mapper.Map(bodyMatchers[0]); + mappingModel.Request.Body.Matcher = _mapper.Map(bodyMatchers[0], AfterMap); } else if (bodyMatchers.Length > 1) { - mappingModel.Request.Body.Matchers = _mapper.Map(bodyMatchers); + mappingModel.Request.Body.Matchers = _mapper.Map(bodyMatchers, AfterMap); mappingModel.Request.Body.MatchOperator = matchOperator.ToString(); } } @@ -390,6 +440,11 @@ public MappingModel ToMappingModel(IMapping mapping) mappingModel.Response.Headers = MapHeaders(response.ResponseMessage.Headers); } + if (response.ResponseMessage.TrailingHeaders is { Count: > 0 }) + { + mappingModel.Response.TrailingHeaders = MapHeaders(response.ResponseMessage.TrailingHeaders); + } + if (response.UseTransformer) { mappingModel.Response.UseTransformer = response.UseTransformer; @@ -402,43 +457,7 @@ public MappingModel ToMappingModel(IMapping mapping) mappingModel.Response.UseTransformerForBodyAsFile = response.UseTransformerForBodyAsFile; } - if (response.ResponseMessage.BodyData != null) - { - switch (response.ResponseMessage.BodyData?.DetectedBodyType) - { - case BodyType.String: - case BodyType.FormUrlEncoded: - mappingModel.Response.Body = response.ResponseMessage.BodyData.BodyAsString; - break; - - case BodyType.Json: - mappingModel.Response.BodyAsJson = response.ResponseMessage.BodyData.BodyAsJson; - if (response.ResponseMessage.BodyData.BodyAsJsonIndented == true) - { - mappingModel.Response.BodyAsJsonIndented = response.ResponseMessage.BodyData.BodyAsJsonIndented; - } - break; - - case BodyType.Bytes: - mappingModel.Response.BodyAsBytes = response.ResponseMessage.BodyData.BodyAsBytes; - break; - - case BodyType.File: - mappingModel.Response.BodyAsFile = response.ResponseMessage.BodyData.BodyAsFile; - mappingModel.Response.BodyAsFileIsCached = response.ResponseMessage.BodyData.BodyAsFileIsCached; - break; - } - - if (response.ResponseMessage.BodyData?.Encoding != null && response.ResponseMessage.BodyData.Encoding.WebName != "utf-8") - { - mappingModel.Response.BodyEncoding = new EncodingModel - { - EncodingName = response.ResponseMessage.BodyData.Encoding.EncodingName, - CodePage = response.ResponseMessage.BodyData.Encoding.CodePage, - WebName = response.ResponseMessage.BodyData.Encoding.WebName - }; - } - } + MapResponse(response, mappingModel); if (response.ResponseMessage.FaultType != FaultType.NONE) { @@ -453,6 +472,61 @@ public MappingModel ToMappingModel(IMapping mapping) return mappingModel; } + private static void MapResponse(Response response, MappingModel mappingModel) + { + if (response.ResponseMessage.BodyData == null) + { + return; + } + + switch (response.ResponseMessage.BodyData?.DetectedBodyType) + { + case BodyType.String: + case BodyType.FormUrlEncoded: + mappingModel.Response.Body = response.ResponseMessage.BodyData.BodyAsString; + break; + + case BodyType.Json: + mappingModel.Response.BodyAsJson = response.ResponseMessage.BodyData.BodyAsJson; + if (response.ResponseMessage.BodyData.BodyAsJsonIndented == true) + { + mappingModel.Response.BodyAsJsonIndented = response.ResponseMessage.BodyData.BodyAsJsonIndented; + } + break; + + case BodyType.ProtoBuf: + // If the ProtoDefinition is not defined at the MappingModel, get the ProtoDefinition from the ResponseMessage. + if (mappingModel.ProtoDefinition == null) + { + mappingModel.Response.ProtoDefinition = response.ResponseMessage.BodyData.ProtoDefinition?.Invoke().Value; + } + + mappingModel.Response.ProtoBufMessageType = response.ResponseMessage.BodyData.ProtoBufMessageType; + mappingModel.Response.BodyAsBytes = null; + mappingModel.Response.BodyAsJson = response.ResponseMessage.BodyData.BodyAsJson; + break; + + case BodyType.Bytes: + mappingModel.Response.BodyAsBytes = response.ResponseMessage.BodyData.BodyAsBytes; + break; + + case BodyType.File: + mappingModel.Response.BodyAsFile = response.ResponseMessage.BodyData.BodyAsFile; + mappingModel.Response.BodyAsFileIsCached = response.ResponseMessage.BodyData.BodyAsFileIsCached; + break; + } + + if (response.ResponseMessage.BodyData?.Encoding != null && response.ResponseMessage.BodyData.Encoding.WebName != "utf-8") + { + mappingModel.Response.BodyEncoding = new EncodingModel + { + EncodingName = response.ResponseMessage.BodyData.Encoding.EncodingName, + CodePage = response.ResponseMessage.BodyData.Encoding.CodePage, + WebName = response.ResponseMessage.BodyData.Encoding.WebName + }; + } + } + private static string GetString(IStringMatcher stringMatcher) { return stringMatcher.GetPatterns().Select(p => ToCSharpStringLiteral(p.GetPattern())).First(); diff --git a/src/WireMock.Net/Serialization/MatcherMapper.cs b/src/WireMock.Net/Serialization/MatcherMapper.cs index d70de60e3..5298c0276 100644 --- a/src/WireMock.Net/Serialization/MatcherMapper.cs +++ b/src/WireMock.Net/Serialization/MatcherMapper.cs @@ -25,29 +25,25 @@ public MatcherMapper(WireMockServerSettings settings) public IMatcher[]? Map(IEnumerable? matchers) { - if (matchers == null) - { - return null; - } - return matchers.Select(Map).Where(m => m != null).ToArray()!; + return matchers?.Select(Map).OfType().ToArray(); } - public IMatcher? Map(MatcherModel? matcher) + public IMatcher? Map(MatcherModel? matcherModel) { - if (matcher == null) + if (matcherModel == null) { return null; } - string[] parts = matcher.Name.Split('.'); + string[] parts = matcherModel.Name.Split('.'); string matcherName = parts[0]; string? matcherType = parts.Length > 1 ? parts[1] : null; - var stringPatterns = ParseStringPatterns(matcher); - var matchBehaviour = matcher.RejectOnMatch == true ? MatchBehaviour.RejectOnMatch : MatchBehaviour.AcceptOnMatch; - var matchOperator = StringUtils.ParseMatchOperator(matcher.MatchOperator); - bool ignoreCase = matcher.IgnoreCase == true; + var stringPatterns = ParseStringPatterns(matcherModel); + var matchBehaviour = matcherModel.RejectOnMatch == true ? MatchBehaviour.RejectOnMatch : MatchBehaviour.AcceptOnMatch; + var matchOperator = StringUtils.ParseMatchOperator(matcherModel.MatchOperator); + bool ignoreCase = matcherModel.IgnoreCase == true; bool useRegexExtended = _settings.UseRegexExtended == true; - bool useRegex = matcher.Regex == true; + bool useRegex = matcherModel.Regex == true; switch (matcherName) { @@ -72,26 +68,31 @@ public MatcherMapper(WireMockServerSettings settings) return CreateExactObjectMatcher(matchBehaviour, stringPatterns[0]); #if GRAPHQL case nameof(GraphQLMatcher): - return new GraphQLMatcher(stringPatterns[0].GetPattern(), matcher.CustomScalars, matchBehaviour, matchOperator); + return new GraphQLMatcher(stringPatterns[0].GetPattern(), matcherModel.CustomScalars, matchBehaviour, matchOperator); #endif #if MIMEKIT case nameof(MimePartMatcher): - return CreateMimePartMatcher(matchBehaviour, matcher); + return CreateMimePartMatcher(matchBehaviour, matcherModel); +#endif + +#if PROTOBUF + case nameof(ProtoBufMatcher): + return CreateProtoBufMatcher(matchBehaviour, stringPatterns[0].GetPattern(), matcherModel); #endif case nameof(RegexMatcher): return new RegexMatcher(matchBehaviour, stringPatterns, ignoreCase, useRegexExtended, matchOperator); case nameof(JsonMatcher): - var valueForJsonMatcher = matcher.Pattern ?? matcher.Patterns; + var valueForJsonMatcher = matcherModel.Pattern ?? matcherModel.Patterns; return new JsonMatcher(matchBehaviour, valueForJsonMatcher!, ignoreCase); case nameof(JsonPartialMatcher): - var valueForJsonPartialMatcher = matcher.Pattern ?? matcher.Patterns; + var valueForJsonPartialMatcher = matcherModel.Pattern ?? matcherModel.Patterns; return new JsonPartialMatcher(matchBehaviour, valueForJsonPartialMatcher!, ignoreCase, useRegex); case nameof(JsonPartialWildcardMatcher): - var valueForJsonPartialWildcardMatcher = matcher.Pattern ?? matcher.Patterns; + var valueForJsonPartialWildcardMatcher = matcherModel.Pattern ?? matcherModel.Patterns; return new JsonPartialWildcardMatcher(matchBehaviour, valueForJsonPartialWildcardMatcher!, ignoreCase, useRegex); case nameof(JsonPathMatcher): @@ -101,7 +102,7 @@ public MatcherMapper(WireMockServerSettings settings) return new JmesPathMatcher(matchBehaviour, matchOperator, stringPatterns); case nameof(XPathMatcher): - return new XPathMatcher(matchBehaviour, matchOperator, matcher.XmlNamespaceMap, stringPatterns); + return new XPathMatcher(matchBehaviour, matchOperator, matcherModel.XmlNamespaceMap, stringPatterns); case nameof(WildcardMatcher): return new WildcardMatcher(matchBehaviour, stringPatterns, ignoreCase, matchOperator); @@ -121,19 +122,19 @@ public MatcherMapper(WireMockServerSettings settings) default: if (_settings.CustomMatcherMappings != null && _settings.CustomMatcherMappings.ContainsKey(matcherName)) { - return _settings.CustomMatcherMappings[matcherName](matcher); + return _settings.CustomMatcherMappings[matcherName](matcherModel); } throw new NotSupportedException($"Matcher '{matcherName}' is not supported."); } } - public MatcherModel[]? Map(IEnumerable? matchers) + public MatcherModel[]? Map(IEnumerable? matchers, Action? afterMap = null) { - return matchers?.Where(m => m != null).Select(Map).ToArray(); + return matchers?.Select(m => Map(m, afterMap)).OfType().ToArray(); } - public MatcherModel? Map(IMatcher? matcher) + public MatcherModel? Map(IMatcher? matcher, Action? afterMap = null) { if (matcher == null) { @@ -194,14 +195,9 @@ public MatcherMapper(WireMockServerSettings settings) } break; - // If the matcher is a IValueMatcher, get the value (can be string or object). - case IValueMatcher valueMatcher: - model.Pattern = valueMatcher.Value; - break; - - // If the matcher is a ExactObjectMatcher, get the ValueAsObject or ValueAsBytes. - case ExactObjectMatcher exactObjectMatcher: - model.Pattern = exactObjectMatcher.ValueAsObject ?? exactObjectMatcher.ValueAsBytes; + // If the matcher is a IObjectMatcher, get the value (can be string or object or byte[]). + case IObjectMatcher objectMatcher: + model.Pattern = objectMatcher.Value; break; #if MIMEKIT @@ -212,8 +208,18 @@ public MatcherMapper(WireMockServerSettings settings) model.ContentTypeMatcher = Map(mimePartMatcher.ContentTypeMatcher); break; #endif + +#if PROTOBUF + case ProtoBufMatcher protoBufMatcher: + model.Pattern = protoBufMatcher.ProtoDefinition().Value; + model.ProtoBufMessageType = protoBufMatcher.MessageType; + model.ContentMatcher = Map(protoBufMatcher.Matcher); + break; +#endif } + afterMap?.Invoke(model); + return model; } @@ -260,7 +266,7 @@ private static ExactObjectMatcher CreateExactObjectMatcher(MatchBehaviour matchB } #if MIMEKIT - private MimePartMatcher CreateMimePartMatcher(MatchBehaviour matchBehaviour, MatcherModel? matcher) + private MimePartMatcher CreateMimePartMatcher(MatchBehaviour matchBehaviour, MatcherModel matcher) { var contentTypeMatcher = Map(matcher?.ContentTypeMatcher) as IStringMatcher; var contentDispositionMatcher = Map(matcher?.ContentDispositionMatcher) as IStringMatcher; @@ -270,4 +276,28 @@ private MimePartMatcher CreateMimePartMatcher(MatchBehaviour matchBehaviour, Mat return new MimePartMatcher(matchBehaviour, contentTypeMatcher, contentDispositionMatcher, contentTransferEncodingMatcher, contentMatcher); } #endif + +#if PROTOBUF + private ProtoBufMatcher CreateProtoBufMatcher(MatchBehaviour? matchBehaviour, string protoDefinitionOrId, MatcherModel matcher) + { + var objectMatcher = Map(matcher.ContentMatcher) as IObjectMatcher; + + IdOrText protoDefinition; + if (_settings.ProtoDefinitions?.TryGetValue(protoDefinitionOrId, out var protoDefinitionFromSettings) == true) + { + protoDefinition = new(protoDefinitionOrId, protoDefinitionFromSettings); + } + else + { + protoDefinition = new(null, protoDefinitionOrId); + } + + return new ProtoBufMatcher( + () => protoDefinition, + matcher!.ProtoBufMessageType!, + matchBehaviour ?? MatchBehaviour.AcceptOnMatch, + objectMatcher + ); + } +#endif } \ No newline at end of file diff --git a/src/WireMock.Net/Serialization/ProxyMappingConverter.cs b/src/WireMock.Net/Serialization/ProxyMappingConverter.cs index 3878c30f9..9bd07fe74 100644 --- a/src/WireMock.Net/Serialization/ProxyMappingConverter.cs +++ b/src/WireMock.Net/Serialization/ProxyMappingConverter.cs @@ -41,6 +41,7 @@ public ProxyMappingConverter(WireMockServerSettings settings, IGuidUtils guidUti var paramMatchers = request?.GetRequestMessageMatchers(); var methodMatcher = request?.GetRequestMessageMatcher(); var bodyMatcher = request?.GetRequestMessageMatcher(); + var httpVersionMatcher = request?.GetRequestMessageMatcher(); var newRequest = Request.Create(); @@ -70,6 +71,16 @@ public ProxyMappingConverter(WireMockServerSettings settings, IGuidUtils guidUti newRequest.UsingMethod(requestMessage.Method); } + // HttpVersion + if (useDefinedRequestMatchers && httpVersionMatcher?.HttpVersion is not null) + { + newRequest.WithHttpVersion(httpVersionMatcher.HttpVersion); + } + else + { + newRequest.WithHttpVersion(requestMessage.HttpVersion); + } + // QueryParams if (useDefinedRequestMatchers && paramMatchers is not null) { @@ -188,8 +199,7 @@ public ProxyMappingConverter(WireMockServerSettings settings, IGuidUtils guidUti webhooks: null, useWebhooksFireAndForget: null, timeSettings: null, - data: mapping?.Data, - probability: null + data: mapping?.Data ); } } \ No newline at end of file diff --git a/src/WireMock.Net/Server/IRespondWithAProvider.cs b/src/WireMock.Net/Server/IRespondWithAProvider.cs index ea3bec77a..7220ba11d 100644 --- a/src/WireMock.Net/Server/IRespondWithAProvider.cs +++ b/src/WireMock.Net/Server/IRespondWithAProvider.cs @@ -184,4 +184,21 @@ IRespondWithAProvider WithWebhook( /// The probability when this request should be matched. Value is between 0 and 1. /// The . IRespondWithAProvider WithProbability(double probability); + + /// + /// Define a Grpc ProtoDefinition which is used for the request and the response. + /// This can be a ProtoDefinition as a string, or an id when the ProtoDefinitions are defined at the WireMockServer. + /// + /// The proto definition as text or as id. + /// The . + IRespondWithAProvider WithProtoDefinition(string protoDefinitionOrId); + + /// + /// Define a GraphQL Schema which is used for the request and the response. + /// This can be a GraphQL Schema as a string, or an id when the GraphQL Schema are defined at the WireMockServer. + /// + /// The GraphQL Schema as text or as id. + /// A dictionary defining the custom scalars used in this schema. [optional] + /// The . + IRespondWithAProvider WithGraphQLSchema(string graphQLSchemaOrId, IDictionary? customScalars = null); } \ No newline at end of file diff --git a/src/WireMock.Net/Server/RespondWithAProvider.cs b/src/WireMock.Net/Server/RespondWithAProvider.cs index 7b7740fa8..c3e793030 100644 --- a/src/WireMock.Net/Server/RespondWithAProvider.cs +++ b/src/WireMock.Net/Server/RespondWithAProvider.cs @@ -2,7 +2,6 @@ // For more details see 'mock4net/LICENSE.txt' and 'mock4net/readme.md' in this project root. using System; using System.Collections.Generic; -using JetBrains.Annotations; using Stef.Validation; using WireMock.Matchers.Request; using WireMock.Models; @@ -34,6 +33,8 @@ internal class RespondWithAProvider : IRespondWithAProvider private int _timesInSameState = 1; private bool? _useWebhookFireAndForget; private double? _probability; + private IdOrText? _protoDefinition; + private GraphQLSchemaDetails? _graphQLSchemaDetails; public Guid Guid { get; private set; } @@ -76,7 +77,8 @@ public RespondWithAProvider( /// The provider. public void RespondWith(IResponseProvider provider) { - var mapping = new Mapping( + var mapping = new Mapping + ( Guid, _dateTimeUtils.UtcNow, _title, @@ -93,14 +95,23 @@ public void RespondWith(IResponseProvider provider) Webhooks, _useWebhookFireAndForget, TimeSettings, - Data, - _probability); + Data + ); + + if (_probability != null) + { + mapping.WithProbability(_probability.Value); + } + + if (_protoDefinition != null) + { + mapping.WithProtoDefinition(_protoDefinition.Value); + } _registrationCallback(mapping, _saveToFile); } /// - [PublicAPI] public IRespondWithAProvider WithData(object data) { Data = data; @@ -117,7 +128,6 @@ public IRespondWithAProvider WithGuid(string guid) public IRespondWithAProvider WithGuid(Guid guid) { Guid = guid; - return this; } @@ -133,7 +143,6 @@ public IRespondWithAProvider WithTitle(string title) public IRespondWithAProvider WithDescription(string description) { _description = description; - return this; } @@ -141,7 +150,6 @@ public IRespondWithAProvider WithDescription(string description) public IRespondWithAProvider WithPath(string path) { _path = path; - return this; } @@ -149,15 +157,13 @@ public IRespondWithAProvider WithPath(string path) public IRespondWithAProvider AtPriority(int priority) { _priority = priority; - return this; } /// public IRespondWithAProvider InScenario(string scenario) { - _scenario = scenario; - + _scenario = Guard.NotNullOrWhiteSpace(scenario); return this; } @@ -209,9 +215,7 @@ public IRespondWithAProvider WillSetStateTo(int state, int? times = 1) /// public IRespondWithAProvider WithTimeSettings(ITimeSettings timeSettings) { - Guard.NotNull(timeSettings, nameof(timeSettings)); - - TimeSettings = timeSettings; + TimeSettings = Guard.NotNull(timeSettings); return this; } @@ -219,10 +223,9 @@ public IRespondWithAProvider WithTimeSettings(ITimeSettings timeSettings) /// public IRespondWithAProvider WithWebhook(params IWebhook[] webhooks) { - Guard.HasNoNulls(webhooks, nameof(webhooks)); + Guard.HasNoNulls(webhooks); Webhooks = webhooks; - return this; } @@ -283,13 +286,45 @@ public IRespondWithAProvider WithWebhook( public IRespondWithAProvider WithWebhookFireAndForget(bool useWebhooksFireAndForget) { _useWebhookFireAndForget = useWebhooksFireAndForget; - return this; } public IRespondWithAProvider WithProbability(double probability) { _probability = Guard.Condition(probability, p => p is >= 0 and <= 1.0); + return this; + } + + /// + public IRespondWithAProvider WithProtoDefinition(string protoDefinitionOrId) + { + Guard.NotNullOrWhiteSpace(protoDefinitionOrId); + + if (_settings.ProtoDefinitions?.TryGetValue(protoDefinitionOrId, out var protoDefinition) == true) + { + _protoDefinition = new (protoDefinitionOrId, protoDefinition); + } + else + { + _protoDefinition = new(null, protoDefinitionOrId); + } + + return this; + } + + /// + public IRespondWithAProvider WithGraphQLSchema(string graphQLSchemaOrId, IDictionary? customScalars = null) + { + Guard.NotNullOrWhiteSpace(graphQLSchemaOrId); + + if (_settings.GraphQLSchemas?.TryGetValue(graphQLSchemaOrId, out _graphQLSchemaDetails) != true) + { + _graphQLSchemaDetails = new GraphQLSchemaDetails + { + SchemaAsString = graphQLSchemaOrId, + CustomScalars = customScalars + }; + } return this; } @@ -299,7 +334,8 @@ private static IWebhook InitWebhook( string method, IDictionary>? headers, bool useTransformer, - TransformerType transformerType) + TransformerType transformerType + ) { return new Webhook { diff --git a/src/WireMock.Net/Server/WireMockServer.Admin.cs b/src/WireMock.Net/Server/WireMockServer.Admin.cs index 6e5e41a18..72ba9bd1f 100644 --- a/src/WireMock.Net/Server/WireMockServer.Admin.cs +++ b/src/WireMock.Net/Server/WireMockServer.Admin.cs @@ -231,9 +231,11 @@ private IResponseMessage SettingsGet(IRequestMessage requestMessage) DisableRequestBodyDecompressing = _settings.DisableRequestBodyDecompressing, DoNotSaveDynamicResponseInLogEntry = _settings.DoNotSaveDynamicResponseInLogEntry, GlobalProcessingDelay = (int?)_options.RequestProcessingDelay?.TotalMilliseconds, + // GraphQLSchemas TODO HandleRequestsSynchronously = _settings.HandleRequestsSynchronously, HostingScheme = _settings.HostingScheme, MaxRequestLogCount = _settings.MaxRequestLogCount, + ProtoDefinitions = _settings.ProtoDefinitions, QueryParameterMultipleValueSupport = _settings.QueryParameterMultipleValueSupport, ReadStaticMappings = _settings.ReadStaticMappings, RequestLogExpirationDuration = _settings.RequestLogExpirationDuration, @@ -268,6 +270,7 @@ private IResponseMessage SettingsUpdate(IRequestMessage requestMessage) _settings.DoNotSaveDynamicResponseInLogEntry = settings.DoNotSaveDynamicResponseInLogEntry; _settings.HandleRequestsSynchronously = settings.HandleRequestsSynchronously; _settings.MaxRequestLogCount = settings.MaxRequestLogCount; + _settings.ProtoDefinitions = settings.ProtoDefinitions; _settings.ProxyAndRecordSettings = TinyMapperUtils.Instance.Map(settings.ProxyAndRecordSettings); _settings.QueryParameterMultipleValueSupport = settings.QueryParameterMultipleValueSupport; _settings.ReadStaticMappings = settings.ReadStaticMappings; diff --git a/src/WireMock.Net/Server/WireMockServer.ConvertMapping.cs b/src/WireMock.Net/Server/WireMockServer.ConvertMapping.cs index 67d5d2722..f67fad171 100644 --- a/src/WireMock.Net/Server/WireMockServer.ConvertMapping.cs +++ b/src/WireMock.Net/Server/WireMockServer.ConvertMapping.cs @@ -195,6 +195,11 @@ private void ConvertMappingsAndRegisterAsRespondProvider(IReadOnlyList h.Matchers != null)) diff --git a/src/WireMock.Net/Server/WireMockServer.cs b/src/WireMock.Net/Server/WireMockServer.cs index 3e1d3212f..60fbf11db 100644 --- a/src/WireMock.Net/Server/WireMockServer.cs +++ b/src/WireMock.Net/Server/WireMockServer.cs @@ -7,6 +7,7 @@ using System.Net; using System.Net.Http; using System.Threading; +using AnyOfTypes; using JetBrains.Annotations; using Newtonsoft.Json; using Stef.Validation; @@ -18,6 +19,7 @@ using WireMock.Http; using WireMock.Logging; using WireMock.Matchers.Request; +using WireMock.Models; using WireMock.Owin; using WireMock.RequestBuilders; using WireMock.ResponseProviders; @@ -209,19 +211,38 @@ public static WireMockServer Start(WireMockServerSettings settings) return new WireMockServer(settings); } + /// + /// Starts this WireMockServer with the specified settings. + /// + /// The action to configure the WireMockServerSettings. + /// The . + [PublicAPI] + public static WireMockServer Start(Action action) + { + Guard.NotNull(action); + + var settings = new WireMockServerSettings(); + + action(settings); + + return new WireMockServer(settings); + } + /// /// Start this WireMockServer. /// /// The port. - /// The SSL support. + /// The SSL support. + /// Use HTTP 2 (needed for Grpc). /// The . [PublicAPI] - public static WireMockServer Start(int? port = 0, bool ssl = false) + public static WireMockServer Start(int? port = 0, bool useSSL = false, bool useHttp2 = false) { return new WireMockServer(new WireMockServerSettings { Port = port, - UseSSL = ssl + UseSSL = useSSL, + UseHttp2 = useHttp2 }); } @@ -245,15 +266,17 @@ public static WireMockServer Start(params string[] urls) /// Start this WireMockServer with the admin interface. /// /// The port. - /// The SSL support. + /// The SSL support. + /// Use HTTP 2 (needed for Grpc). /// The . [PublicAPI] - public static WireMockServer StartWithAdminInterface(int? port = 0, bool ssl = false) + public static WireMockServer StartWithAdminInterface(int? port = 0, bool useSSL = false, bool useHttp2 = false) { return new WireMockServer(new WireMockServerSettings { Port = port, - UseSSL = ssl, + UseSSL = useSSL, + UseHttp2 = useHttp2, StartAdminInterface = true }); } @@ -266,7 +289,7 @@ public static WireMockServer StartWithAdminInterface(int? port = 0, bool ssl = f [PublicAPI] public static WireMockServer StartWithAdminInterface(params string[] urls) { - Guard.NotNullOrEmpty(urls, nameof(urls)); + Guard.NotNullOrEmpty(urls); return new WireMockServer(new WireMockServerSettings { @@ -329,6 +352,7 @@ protected WireMockServer(WireMockServerSettings settings) urlOptions = new HostUrlOptions { HostingScheme = settings.HostingScheme.Value, + UseHttp2 = settings.UseHttp2, Port = settings.Port }; } @@ -337,6 +361,7 @@ protected WireMockServer(WireMockServerSettings settings) urlOptions = new HostUrlOptions { HostingScheme = settings.UseSSL == true ? HostingScheme.Https : HostingScheme.Http, + UseHttp2 = settings.UseHttp2, Port = settings.Port }; } @@ -573,6 +598,49 @@ public IRespondWithAProvider Given(IRequestMatcher requestMatcher, bool saveToFi return _mappingBuilder.Given(requestMatcher, saveToFile); } + /// + /// Add a Grpc ProtoDefinition at server-level. + /// + /// Unique identifier for the ProtoDefinition. + /// The ProtoDefinition as text. + /// + [PublicAPI] + public WireMockServer AddProtoDefinition(string id, string protoDefinition) + { + Guard.NotNullOrWhiteSpace(id); + Guard.NotNullOrWhiteSpace(protoDefinition); + + _settings.ProtoDefinitions ??= new Dictionary(); + + _settings.ProtoDefinitions[id] = protoDefinition; + + return this; + } + + /// + /// Add a GraphQL Schema at server-level. + /// + /// Unique identifier for the GraphQL Schema. + /// The GraphQL Schema as string or StringPattern. + /// A dictionary defining the custom scalars used in this schema. [optional] + /// + [PublicAPI] + public WireMockServer AddGraphQLSchema(string id, AnyOf graphQLSchema, Dictionary? customScalars = null) + { + Guard.NotNullOrWhiteSpace(id); + Guard.NotNullOrWhiteSpace(graphQLSchema); + + _settings.GraphQLSchemas ??= new Dictionary(); + + _settings.GraphQLSchemas[id] = new GraphQLSchemaDetails + { + SchemaAsString = graphQLSchema, + CustomScalars = customScalars + }; + + return this; + } + /// [PublicAPI] public string? MappingToCSharpCode(Guid guid, MappingConverterType converterType) diff --git a/src/WireMock.Net/Settings/SimpleSettingsParser.cs b/src/WireMock.Net/Settings/SimpleSettingsParser.cs index 91295e654..6daed1e97 100644 --- a/src/WireMock.Net/Settings/SimpleSettingsParser.cs +++ b/src/WireMock.Net/Settings/SimpleSettingsParser.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using WireMock.Extensions; +using WireMock.Util; namespace WireMock.Settings; @@ -148,4 +149,10 @@ public string GetStringValue(string name, string defaultValue) { return GetValue(name, values => values.FirstOrDefault()); } + + public T? GetObjectValueFromJson(string name) + { + var value = GetValue(name, values => values.FirstOrDefault()); + return string.IsNullOrWhiteSpace(value) ? default : JsonUtils.DeserializeObject(value); + } } \ No newline at end of file diff --git a/src/WireMock.Net/Settings/WireMockServerSettings.cs b/src/WireMock.Net/Settings/WireMockServerSettings.cs index d944dfc19..4bfdd0451 100644 --- a/src/WireMock.Net/Settings/WireMockServerSettings.cs +++ b/src/WireMock.Net/Settings/WireMockServerSettings.cs @@ -11,6 +11,7 @@ using WireMock.RegularExpressions; using WireMock.Types; using System.Globalization; +using WireMock.Models; #if USE_ASPNETCORE using Microsoft.Extensions.DependencyInjection; #endif @@ -43,6 +44,12 @@ public class WireMockServerSettings [PublicAPI] public HostingScheme? HostingScheme { get; set; } + /// + /// Gets or sets to use HTTP 2 (used for Grpc). + /// + [PublicAPI] + public bool? UseHttp2 { get; set; } + /// /// Gets or sets whether to start admin interface. /// @@ -301,4 +308,16 @@ public class WireMockServerSettings /// [JsonIgnore] public CultureInfo Culture { get; set; } = CultureInfo.CurrentCulture; + + /// + /// A list of Grpc ProtoDefinitions which can be used. + /// + [PublicAPI] + public Dictionary? ProtoDefinitions { get; set; } + + /// + /// A list of GraphQL Schemas which can be used. + /// + [PublicAPI] + public Dictionary? GraphQLSchemas { get; set; } } \ No newline at end of file diff --git a/src/WireMock.Net/Settings/WireMockServerSettingsParser.cs b/src/WireMock.Net/Settings/WireMockServerSettingsParser.cs index 20c18d5f2..707aef107 100644 --- a/src/WireMock.Net/Settings/WireMockServerSettingsParser.cs +++ b/src/WireMock.Net/Settings/WireMockServerSettingsParser.cs @@ -1,10 +1,12 @@ using System.Collections; +using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Linq; using JetBrains.Annotations; using Stef.Validation; using WireMock.Logging; +using WireMock.Models; using WireMock.Types; using WireMock.Util; @@ -47,23 +49,26 @@ public static bool TryParseArguments(string[] args, IDictionary? environment, [N AllowCSharpCodeMatcher = parser.GetBoolValue(nameof(WireMockServerSettings.AllowCSharpCodeMatcher)), AllowOnlyDefinedHttpStatusCodeInResponse = parser.GetBoolValue(nameof(WireMockServerSettings.AllowOnlyDefinedHttpStatusCodeInResponse)), AllowPartialMapping = parser.GetBoolValue(nameof(WireMockServerSettings.AllowPartialMapping)), + Culture = parser.GetValue(nameof(WireMockServerSettings.Culture), strings => CultureInfoUtils.Parse(strings.FirstOrDefault()), CultureInfo.CurrentCulture), DisableJsonBodyParsing = parser.GetBoolValue(nameof(WireMockServerSettings.DisableJsonBodyParsing)), DisableRequestBodyDecompressing = parser.GetBoolValue(nameof(WireMockServerSettings.DisableRequestBodyDecompressing)), DisableDeserializeFormUrlEncoded = parser.GetBoolValue(nameof(WireMockServerSettings.DisableDeserializeFormUrlEncoded)), + DoNotSaveDynamicResponseInLogEntry = parser.GetBoolValue(nameof(WireMockServerSettings.DoNotSaveDynamicResponseInLogEntry)), + GraphQLSchemas = parser.GetObjectValueFromJson>(nameof(settings.GraphQLSchemas)), HandleRequestsSynchronously = parser.GetBoolValue(nameof(WireMockServerSettings.HandleRequestsSynchronously)), + HostingScheme = parser.GetEnumValue(nameof(WireMockServerSettings.HostingScheme)), MaxRequestLogCount = parser.GetIntValue(nameof(WireMockServerSettings.MaxRequestLogCount)), + ProtoDefinitions = parser.GetObjectValueFromJson>(nameof(settings.ProtoDefinitions)), + QueryParameterMultipleValueSupport = parser.GetEnumValue(nameof(WireMockServerSettings.QueryParameterMultipleValueSupport)), ReadStaticMappings = parser.GetBoolValue(nameof(WireMockServerSettings.ReadStaticMappings)), RequestLogExpirationDuration = parser.GetIntValue(nameof(WireMockServerSettings.RequestLogExpirationDuration)), SaveUnmatchedRequests = parser.GetBoolValue(nameof(WireMockServerSettings.SaveUnmatchedRequests)), StartAdminInterface = parser.GetBoolValue(nameof(WireMockServerSettings.StartAdminInterface), true), StartTimeout = parser.GetIntValue(nameof(WireMockServerSettings.StartTimeout), WireMockServerSettings.DefaultStartTimeout), + UseHttp2 = parser.GetBoolValue(nameof(WireMockServerSettings.UseHttp2)), UseRegexExtended = parser.GetBoolValue(nameof(WireMockServerSettings.UseRegexExtended), true), WatchStaticMappings = parser.GetBoolValue(nameof(WireMockServerSettings.WatchStaticMappings)), WatchStaticMappingsInSubdirectories = parser.GetBoolValue(nameof(WireMockServerSettings.WatchStaticMappingsInSubdirectories)), - HostingScheme = parser.GetEnumValue(nameof(WireMockServerSettings.HostingScheme)), - DoNotSaveDynamicResponseInLogEntry = parser.GetBoolValue(nameof(WireMockServerSettings.DoNotSaveDynamicResponseInLogEntry)), - QueryParameterMultipleValueSupport = parser.GetEnumValue(nameof(WireMockServerSettings.QueryParameterMultipleValueSupport)), - Culture = parser.GetValue(nameof(WireMockServerSettings.Culture), strings => CultureInfoUtils.Parse(strings.FirstOrDefault()), CultureInfo.CurrentCulture) }; #if USE_ASPNETCORE @@ -98,7 +103,6 @@ private static void ParseLoggerSettings(WireMockServerSettings settings, IWireMo { settings.Logger = logger; } - break; } } diff --git a/src/WireMock.Net/Transformers/Transformer.cs b/src/WireMock.Net/Transformers/Transformer.cs index f8d47d1fc..d2ff5158b 100644 --- a/src/WireMock.Net/Transformers/Transformer.cs +++ b/src/WireMock.Net/Transformers/Transformer.cs @@ -92,17 +92,14 @@ public ResponseMessage Transform(IMapping mapping, IRequestMessage requestMessag responseMessage.FaultPercentage = original.FaultPercentage; responseMessage.Headers = TransformHeaders(transformerContext, model, original.Headers); + responseMessage.TrailingHeaders = TransformHeaders(transformerContext, model, original.TrailingHeaders); - switch (original.StatusCode) + responseMessage.StatusCode = original.StatusCode switch { - case int statusCodeAsInteger: - responseMessage.StatusCode = statusCodeAsInteger; - break; - - case string statusCodeAsString: - responseMessage.StatusCode = transformerContext.ParseAndRender(statusCodeAsString, model); - break; - } + int statusCodeAsInteger => statusCodeAsInteger, + string statusCodeAsString => transformerContext.ParseAndRender(statusCodeAsString, model), + _ => responseMessage.StatusCode + }; return responseMessage; } @@ -123,13 +120,13 @@ public ResponseMessage Transform(IMapping mapping, IRequestMessage requestMessag switch (original.DetectedBodyType) { case BodyType.Json: + case BodyType.ProtoBuf: return TransformBodyAsJson(transformerContext, options, model, original); case BodyType.File: return TransformBodyAsFile(transformerContext, model, original, useTransformerForBodyAsFile); - case BodyType.String: - case BodyType.FormUrlEncoded: + case BodyType.String or BodyType.FormUrlEncoded: return TransformBodyAsString(transformerContext, model, original); default: @@ -191,6 +188,8 @@ private IBodyData TransformBodyAsJson(ITransformerContext transformerContext, Re Encoding = original.Encoding, DetectedBodyType = original.DetectedBodyType, DetectedBodyTypeFromContentType = original.DetectedBodyTypeFromContentType, + ProtoDefinition = original.ProtoDefinition, + ProtoBufMessageType = original.ProtoBufMessageType, BodyAsJson = jToken }; } diff --git a/src/WireMock.Net/Util/BodyParser.cs b/src/WireMock.Net/Util/BodyParser.cs index 5e7e037e5..e285461d7 100644 --- a/src/WireMock.Net/Util/BodyParser.cs +++ b/src/WireMock.Net/Util/BodyParser.cs @@ -59,6 +59,11 @@ internal static class BodyParser FormUrlEncodedMatcher }; + private static readonly IStringMatcher[] GrpcContentTypesMatchers = { + new WildcardMatcher("application/grpc", true), + new WildcardMatcher("application/grpc+proto", true) + }; + public static bool ShouldParseBody(string? httpMethod, bool allowBodyForAllHttpMethods) { if (string.IsNullOrEmpty(httpMethod)) @@ -85,7 +90,7 @@ public static bool ShouldParseBody(string? httpMethod, bool allowBodyForAllHttpM public static BodyType DetectBodyTypeFromContentType(string? contentTypeValue) { - if (string.IsNullOrEmpty(contentTypeValue) || !MediaTypeHeaderValue.TryParse(contentTypeValue, out MediaTypeHeaderValue? contentType)) + if (string.IsNullOrEmpty(contentTypeValue) || !MediaTypeHeaderValue.TryParse(contentTypeValue, out var contentType)) { return BodyType.Bytes; } @@ -105,6 +110,11 @@ public static BodyType DetectBodyTypeFromContentType(string? contentTypeValue) return BodyType.Json; } + if (GrpcContentTypesMatchers.Any(matcher => matcher.IsMatch(contentType.MediaType).IsPerfect())) + { + return BodyType.ProtoBuf; + } + if (MultipartContentTypesMatchers.Any(matcher => matcher.IsMatch(contentType.MediaType).IsPerfect())) { return BodyType.MultiPart; diff --git a/src/WireMock.Net/Util/HttpVersionParser.cs b/src/WireMock.Net/Util/HttpVersionParser.cs new file mode 100644 index 000000000..aeb42757e --- /dev/null +++ b/src/WireMock.Net/Util/HttpVersionParser.cs @@ -0,0 +1,24 @@ +using System; +using System.Text.RegularExpressions; +using Stef.Validation; + +namespace WireMock.Util; + +/// +/// https://en.wikipedia.org/wiki/HTTP +/// +internal static class HttpVersionParser +{ + private static readonly Regex HttpVersionRegex = new(@"HTTP/(\d+(\.\d+)?(?!\.))", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant | RegexOptions.Compiled, TimeSpan.FromMilliseconds(100)); + + /// + /// Try to extract the version (as a string) from the protocol. + /// + /// The protocol, something like "HTTP/1.1" or "HTTP/2". + /// The version ("1.1" or "2") if found and valid, else empty string. + internal static string Parse(string protocol) + { + var match = HttpVersionRegex.Match(Guard.NotNull(protocol)); + return match.Success ? match.Groups[1].Value : string.Empty; + } +} \ No newline at end of file diff --git a/src/WireMock.Net/Util/JsonUtils.cs b/src/WireMock.Net/Util/JsonUtils.cs index 3d69ba016..104fecece 100644 --- a/src/WireMock.Net/Util/JsonUtils.cs +++ b/src/WireMock.Net/Util/JsonUtils.cs @@ -1,4 +1,5 @@ using System; +using System.Collections; using System.Diagnostics.CodeAnalysis; using System.Text; using Newtonsoft.Json; @@ -106,4 +107,23 @@ public static T ParseJTokenToObject(object? value) _ => throw new NotSupportedException($"Unable to convert value to {typeof(T)}.") }; } + + public static JToken ConvertValueToJToken(object value) + { + // Check if JToken, string, IEnumerable or object + switch (value) + { + case JToken tokenValue: + return tokenValue; + + case string stringValue: + return Parse(stringValue); + + case IEnumerable enumerableValue: + return JArray.FromObject(enumerableValue); + + default: + return JObject.FromObject(value); + } + } } \ No newline at end of file diff --git a/src/WireMock.Net/Util/PortUtils.cs b/src/WireMock.Net/Util/PortUtils.cs index 169f50e60..cfd6eec70 100644 --- a/src/WireMock.Net/Util/PortUtils.cs +++ b/src/WireMock.Net/Util/PortUtils.cs @@ -34,11 +34,12 @@ public static int FindFreeTcpPort() } /// - /// Extract the if-isHttps, protocol, host and port from a URL. + /// Extract the isHttps, isHttp2, protocol, host and port from a URL. /// - public static bool TryExtract(string url, out bool isHttps, [NotNullWhen(true)] out string? protocol, [NotNullWhen(true)] out string? host, out int port) + public static bool TryExtract(string url, out bool isHttps, out bool isHttp2, [NotNullWhen(true)] out string? protocol, [NotNullWhen(true)] out string? host, out int port) { isHttps = false; + isHttp2 = false; protocol = null; host = null; port = default; @@ -47,7 +48,8 @@ public static bool TryExtract(string url, out bool isHttps, [NotNullWhen(true)] if (match.Success) { protocol = match.Groups["proto"].Value; - isHttps = protocol.StartsWith("https", StringComparison.OrdinalIgnoreCase); + isHttps = protocol.StartsWith("https", StringComparison.OrdinalIgnoreCase) || protocol.StartsWith("grpcs", StringComparison.OrdinalIgnoreCase); + isHttp2 = protocol.StartsWith("grpc", StringComparison.OrdinalIgnoreCase); host = match.Groups["host"].Value; return int.TryParse(match.Groups["port"].Value, out port); diff --git a/src/WireMock.Net/Util/ProtoBufUtils.cs b/src/WireMock.Net/Util/ProtoBufUtils.cs new file mode 100644 index 000000000..44a5d07ce --- /dev/null +++ b/src/WireMock.Net/Util/ProtoBufUtils.cs @@ -0,0 +1,41 @@ +#if PROTOBUF +using System; +using System.Threading; +using System.Threading.Tasks; +using JsonConverter.Abstractions; +using ProtoBufJsonConverter; +using ProtoBufJsonConverter.Models; + +namespace WireMock.Util; + +internal static class ProtoBufUtils +{ + internal static async Task GetProtoBufMessageWithHeaderAsync( + string? protoDefinition, + string? messageType, + object? value, + IJsonConverter? jsonConverter = null, + JsonConverterOptions? options = null, + CancellationToken cancellationToken = default + ) + { + if (string.IsNullOrWhiteSpace(protoDefinition) || string.IsNullOrWhiteSpace(messageType) || value is null) + { + return Array.Empty(); + } + + var request = new ConvertToProtoBufRequest(protoDefinition, messageType, value, true); + + if (jsonConverter != null) + { + request = request.WithJsonConverter(jsonConverter); + if (options != null) + { + request = request.WithJsonConverterOptions(options); + } + } + + return await SingletonFactory.GetInstance().ConvertAsync(request, cancellationToken).ConfigureAwait(false); + } +} +#endif \ No newline at end of file diff --git a/src/WireMock.Net/Util/SingletonFactory.cs b/src/WireMock.Net/Util/SingletonFactory.cs new file mode 100644 index 000000000..2899ecbe2 --- /dev/null +++ b/src/WireMock.Net/Util/SingletonFactory.cs @@ -0,0 +1,24 @@ +namespace WireMock.Util; + +internal static class SingletonLock +{ + public static readonly object Lock = new(); +} + +internal static class SingletonFactory where T : class, new() +{ + private static T? _instance; + + public static T GetInstance() + { + if (_instance == null) + { + lock (SingletonLock.Lock) + { + _instance ??= new T(); + } + } + + return _instance; + } +} \ No newline at end of file diff --git a/src/WireMock.Net/WireMock.Net.csproj b/src/WireMock.Net/WireMock.Net.csproj index 7cdca02a7..7da97acd3 100644 --- a/src/WireMock.Net/WireMock.Net.csproj +++ b/src/WireMock.Net/WireMock.Net.csproj @@ -51,17 +51,16 @@ - $(DefineConstants);GRAPHQL;MIMEKIT + $(DefineConstants);GRAPHQL;MIMEKIT;PROTOBUF - - - - + + $(DefineConstants);TRAILINGHEADERS + - + @@ -154,6 +153,7 @@ + diff --git a/test/WireMock.Net.Tests/AdminApi/WireMockAdminApiTests.GetMappingsAsync.cs b/test/WireMock.Net.Tests/AdminApi/WireMockAdminApiTests.GetMappingsAsync.cs new file mode 100644 index 000000000..637db397b --- /dev/null +++ b/test/WireMock.Net.Tests/AdminApi/WireMockAdminApiTests.GetMappingsAsync.cs @@ -0,0 +1,128 @@ +#if !(NET452 || NET461 || NETCOREAPP3_1) +using System.Threading.Tasks; +using RestEase; +using VerifyXunit; +using WireMock.Client; +using WireMock.Matchers; +using WireMock.RequestBuilders; +using WireMock.ResponseBuilders; +using WireMock.Server; +using Xunit; + +namespace WireMock.Net.Tests.AdminApi; + +public partial class WireMockAdminApiTests +{ + private const string ProtoDefinition = @" +syntax = ""proto3""; + +package greet; + +service Greeter { + rpc SayHello (HelloRequest) returns (HelloReply); +} + +message HelloRequest { + string name = 1; +} + +message HelloReply { + string message = 1; +} +"; + + [Fact] + public async Task IWireMockAdminApi_GetMappingsAsync_WithBodyAsProtoBuf_ShouldReturnCorrectMappingModels() + { + // Arrange + using var server = WireMockServer.StartWithAdminInterface(); + + var protoBufJsonMatcher = new JsonPartialWildcardMatcher(new { name = "*" }); + + server + .Given(Request.Create() + .UsingPost() + .WithPath("/grpc/greet.Greeter/SayHello") + .WithBodyAsProtoBuf(ProtoDefinition, "greet.HelloRequest", protoBufJsonMatcher) + ) + .RespondWith(Response.Create() + .WithHeader("Content-Type", "application/grpc") + .WithBodyAsProtoBuf(ProtoDefinition, "greet.HelloReply", + new + { + message = "hello {{request.BodyAsJson.name}}" + } + ) + .WithTrailingHeader("grpc-status", "0") + .WithTransformer() + ); + + server + .Given(Request.Create() + .UsingPost() + .WithPath("/grpc2/greet.Greeter/SayHello") + .WithBodyAsProtoBuf("greet.HelloRequest", protoBufJsonMatcher) + ) + .WithProtoDefinition(ProtoDefinition) + .RespondWith(Response.Create() + .WithHeader("Content-Type", "application/grpc") + .WithBodyAsProtoBuf("greet.HelloReply", + new + { + message = "hello {{request.BodyAsJson.name}}" + } + ) + .WithTrailingHeader("grpc-status", "0") + .WithTransformer() + ); + + server + .AddProtoDefinition("my-greeter", ProtoDefinition) + .Given(Request.Create() + .UsingPost() + .WithPath("/grpc3/greet.Greeter/SayHello") + .WithBodyAsProtoBuf("greet.HelloRequest", protoBufJsonMatcher) + ) + .WithProtoDefinition("my-greeter") + .RespondWith(Response.Create() + .WithHeader("Content-Type", "application/grpc") + .WithBodyAsProtoBuf("greet.HelloReply", + new + { + message = "hello {{request.BodyAsJson.name}}" + } + ) + .WithTrailingHeader("grpc-status", "0") + .WithTransformer() + ); + + server + .AddProtoDefinition("my-greeter", ProtoDefinition) + .Given(Request.Create() + .UsingPost() + .WithPath("/grpc4/greet.Greeter/SayHello") + .WithBodyAsProtoBuf("greet.HelloRequest") + ) + .WithProtoDefinition("my-greeter") + .RespondWith(Response.Create() + .WithHeader("Content-Type", "application/grpc") + .WithBodyAsProtoBuf("greet.HelloReply", + new + { + message = "hello {{request.BodyAsJson.name}}" + } + ) + .WithTrailingHeader("grpc-status", "0") + .WithTransformer() + ); + + // Act + var api = RestClient.For(server.Url); + var getMappingsResult = await api.GetMappingsAsync().ConfigureAwait(false); + + await Verifier.Verify(getMappingsResult, VerifySettings); + + server.Stop(); + } +} +#endif \ No newline at end of file diff --git a/test/WireMock.Net.Tests/WireMockAdminApiTests.IWireMockAdminApi_GetMappingAsync_WithBodyModelMatcherModel_WithoutMethods_ShouldReturnCorrectMappingModel.verified.txt b/test/WireMock.Net.Tests/AdminApi/WireMockAdminApiTests.IWireMockAdminApi_GetMappingAsync_WithBodyModelMatcherModel_WithoutMethods_ShouldReturnCorrectMappingModel.verified.txt similarity index 100% rename from test/WireMock.Net.Tests/WireMockAdminApiTests.IWireMockAdminApi_GetMappingAsync_WithBodyModelMatcherModel_WithoutMethods_ShouldReturnCorrectMappingModel.verified.txt rename to test/WireMock.Net.Tests/AdminApi/WireMockAdminApiTests.IWireMockAdminApi_GetMappingAsync_WithBodyModelMatcherModel_WithoutMethods_ShouldReturnCorrectMappingModel.verified.txt diff --git a/test/WireMock.Net.Tests/WireMockAdminApiTests.IWireMockAdminApi_GetMappingAsync_WithProxy_And_ProxyUrlReplaceSettings.verified.txt b/test/WireMock.Net.Tests/AdminApi/WireMockAdminApiTests.IWireMockAdminApi_GetMappingAsync_WithProxy_And_ProxyUrlReplaceSettings.verified.txt similarity index 100% rename from test/WireMock.Net.Tests/WireMockAdminApiTests.IWireMockAdminApi_GetMappingAsync_WithProxy_And_ProxyUrlReplaceSettings.verified.txt rename to test/WireMock.Net.Tests/AdminApi/WireMockAdminApiTests.IWireMockAdminApi_GetMappingAsync_WithProxy_And_ProxyUrlReplaceSettings.verified.txt diff --git a/test/WireMock.Net.Tests/WireMockAdminApiTests.IWireMockAdminApi_GetMappingByGuidAsync.verified.txt b/test/WireMock.Net.Tests/AdminApi/WireMockAdminApiTests.IWireMockAdminApi_GetMappingByGuidAsync.verified.txt similarity index 100% rename from test/WireMock.Net.Tests/WireMockAdminApiTests.IWireMockAdminApi_GetMappingByGuidAsync.verified.txt rename to test/WireMock.Net.Tests/AdminApi/WireMockAdminApiTests.IWireMockAdminApi_GetMappingByGuidAsync.verified.txt diff --git a/test/WireMock.Net.Tests/WireMockAdminApiTests.IWireMockAdminApi_GetMappingCodeByGuidAsync.verified.txt b/test/WireMock.Net.Tests/AdminApi/WireMockAdminApiTests.IWireMockAdminApi_GetMappingCodeByGuidAsync.verified.txt similarity index 100% rename from test/WireMock.Net.Tests/WireMockAdminApiTests.IWireMockAdminApi_GetMappingCodeByGuidAsync.verified.txt rename to test/WireMock.Net.Tests/AdminApi/WireMockAdminApiTests.IWireMockAdminApi_GetMappingCodeByGuidAsync.verified.txt diff --git a/test/WireMock.Net.Tests/AdminApi/WireMockAdminApiTests.IWireMockAdminApi_GetMappingsAsync_WithBodyAsProtoBuf_ShouldReturnCorrectMappingModels.verified.txt b/test/WireMock.Net.Tests/AdminApi/WireMockAdminApiTests.IWireMockAdminApi_GetMappingsAsync_WithBodyAsProtoBuf_ShouldReturnCorrectMappingModels.verified.txt new file mode 100644 index 000000000..a3de19dbc --- /dev/null +++ b/test/WireMock.Net.Tests/AdminApi/WireMockAdminApiTests.IWireMockAdminApi_GetMappingsAsync_WithBodyAsProtoBuf_ShouldReturnCorrectMappingModels.verified.txt @@ -0,0 +1,235 @@ +[ + { + Guid: Guid_1, + UpdatedAt: DateTime_1, + Request: { + Path: { + Matchers: [ + { + Name: WildcardMatcher, + Pattern: /grpc/greet.Greeter/SayHello, + IgnoreCase: false + } + ] + }, + Methods: [ + POST + ], + Body: { + Matcher: { + Name: ProtoBufMatcher, + Pattern: +syntax = "proto3"; + +package greet; + +service Greeter { + rpc SayHello (HelloRequest) returns (HelloReply); +} + +message HelloRequest { + string name = 1; +} + +message HelloReply { + string message = 1; +} +, + ContentMatcher: { + Name: JsonPartialWildcardMatcher, + Pattern: { + name: * + }, + IgnoreCase: false, + Regex: false + }, + ProtoBufMessageType: greet.HelloRequest + } + } + }, + Response: { + BodyAsJson: { + message: hello {{request.BodyAsJson.name}} + }, + UseTransformer: true, + TransformerType: Handlebars, + TransformerReplaceNodeOptions: EvaluateAndTryToConvert, + Headers: { + Content-Type: application/grpc + }, + TrailingHeaders: { + grpc-status: 0 + }, + ProtoDefinition: +syntax = "proto3"; + +package greet; + +service Greeter { + rpc SayHello (HelloRequest) returns (HelloReply); +} + +message HelloRequest { + string name = 1; +} + +message HelloReply { + string message = 1; +} +, + ProtoBufMessageType: greet.HelloReply + } + }, + { + Guid: Guid_2, + UpdatedAt: DateTime_2, + Request: { + Path: { + Matchers: [ + { + Name: WildcardMatcher, + Pattern: /grpc2/greet.Greeter/SayHello, + IgnoreCase: false + } + ] + }, + Methods: [ + POST + ], + Body: { + Matcher: { + Name: ProtoBufMatcher, + ContentMatcher: { + Name: JsonPartialWildcardMatcher, + Pattern: { + name: * + }, + IgnoreCase: false, + Regex: false + }, + ProtoBufMessageType: greet.HelloRequest + } + } + }, + Response: { + BodyAsJson: { + message: hello {{request.BodyAsJson.name}} + }, + UseTransformer: true, + TransformerType: Handlebars, + TransformerReplaceNodeOptions: EvaluateAndTryToConvert, + Headers: { + Content-Type: application/grpc + }, + TrailingHeaders: { + grpc-status: 0 + }, + ProtoBufMessageType: greet.HelloReply + }, + ProtoDefinition: +syntax = "proto3"; + +package greet; + +service Greeter { + rpc SayHello (HelloRequest) returns (HelloReply); +} + +message HelloRequest { + string name = 1; +} + +message HelloReply { + string message = 1; +} + + }, + { + Guid: Guid_3, + UpdatedAt: DateTime_3, + Request: { + Path: { + Matchers: [ + { + Name: WildcardMatcher, + Pattern: /grpc3/greet.Greeter/SayHello, + IgnoreCase: false + } + ] + }, + Methods: [ + POST + ], + Body: { + Matcher: { + Name: ProtoBufMatcher, + ContentMatcher: { + Name: JsonPartialWildcardMatcher, + Pattern: { + name: * + }, + IgnoreCase: false, + Regex: false + }, + ProtoBufMessageType: greet.HelloRequest + } + } + }, + Response: { + BodyAsJson: { + message: hello {{request.BodyAsJson.name}} + }, + UseTransformer: true, + TransformerType: Handlebars, + TransformerReplaceNodeOptions: EvaluateAndTryToConvert, + Headers: { + Content-Type: application/grpc + }, + TrailingHeaders: { + grpc-status: 0 + }, + ProtoBufMessageType: greet.HelloReply + }, + ProtoDefinition: my-greeter + }, + { + Guid: Guid_4, + UpdatedAt: DateTime_4, + Request: { + Path: { + Matchers: [ + { + Name: WildcardMatcher, + Pattern: /grpc4/greet.Greeter/SayHello, + IgnoreCase: false + } + ] + }, + Methods: [ + POST + ], + Body: { + Matcher: { + Name: ProtoBufMatcher, + ProtoBufMessageType: greet.HelloRequest + } + } + }, + Response: { + BodyAsJson: { + message: hello {{request.BodyAsJson.name}} + }, + UseTransformer: true, + TransformerType: Handlebars, + TransformerReplaceNodeOptions: EvaluateAndTryToConvert, + Headers: { + Content-Type: application/grpc + }, + TrailingHeaders: { + grpc-status: 0 + }, + ProtoBufMessageType: greet.HelloReply + }, + ProtoDefinition: my-greeter + } +] \ No newline at end of file diff --git a/test/WireMock.Net.Tests/WireMockAdminApiTests.IWireMockAdminApi_GetMappingsCode.verified.txt b/test/WireMock.Net.Tests/AdminApi/WireMockAdminApiTests.IWireMockAdminApi_GetMappingsCode.verified.txt similarity index 100% rename from test/WireMock.Net.Tests/WireMockAdminApiTests.IWireMockAdminApi_GetMappingsCode.verified.txt rename to test/WireMock.Net.Tests/AdminApi/WireMockAdminApiTests.IWireMockAdminApi_GetMappingsCode.verified.txt diff --git a/test/WireMock.Net.Tests/WireMockAdminApiTests.cs b/test/WireMock.Net.Tests/AdminApi/WireMockAdminApiTests.cs similarity index 97% rename from test/WireMock.Net.Tests/WireMockAdminApiTests.cs rename to test/WireMock.Net.Tests/AdminApi/WireMockAdminApiTests.cs index 587cbbb24..7eaaccbdd 100644 --- a/test/WireMock.Net.Tests/WireMockAdminApiTests.cs +++ b/test/WireMock.Net.Tests/AdminApi/WireMockAdminApiTests.cs @@ -1,1018 +1,1019 @@ -#if !(NET452 || NET461 || NETCOREAPP3_1) -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Net; -using System.Net.Http; -using System.Net.Http.Headers; -using System.Text; -using System.Threading.Tasks; -using FluentAssertions; -using Moq; -using NFluent; -using RestEase; -using VerifyTests; -using VerifyXunit; -using WireMock.Admin.Mappings; -using WireMock.Admin.Settings; -using WireMock.Client; -using WireMock.Handlers; -using WireMock.Logging; -using WireMock.Matchers; -using WireMock.Models; -using WireMock.Net.Tests.VerifyExtensions; -using WireMock.RequestBuilders; -using WireMock.ResponseBuilders; -using WireMock.Server; -using WireMock.Settings; -using WireMock.Types; -using Xunit; - -namespace WireMock.Net.Tests; - -[UsesVerify] -public class WireMockAdminApiTests -{ - private static readonly VerifySettings VerifySettings = new(); - static WireMockAdminApiTests() - { - VerifyNewtonsoftJson.Enable(VerifySettings); - } - - [Fact] - public async Task IWireMockAdminApi_GetSettingsAsync() - { - // Arrange - var server = WireMockServer.StartWithAdminInterface(); - var api = RestClient.For(server.Urls[0]); - - // Act - var settings = await api.GetSettingsAsync().ConfigureAwait(false); - Check.That(settings).IsNotNull(); - } - - [Fact] - public async Task IWireMockAdminApi_PostSettingsAsync() - { - // Arrange - var server = WireMockServer.StartWithAdminInterface(); - var api = RestClient.For(server.Urls[0]); - - // Act - var settings = new SettingsModel(); - var status = await api.PostSettingsAsync(settings).ConfigureAwait(false); - Check.That(status.Status).Equals("Settings updated"); - } - - [Fact] - public async Task IWireMockAdminApi_PutSettingsAsync() - { - // Arrange - var server = WireMockServer.StartWithAdminInterface(); - var api = RestClient.For(server.Urls[0]); - - // Act - var settings = new SettingsModel(); - var status = await api.PutSettingsAsync(settings).ConfigureAwait(false); - Check.That(status.Status).Equals("Settings updated"); - } - - // https://github.com/WireMock-Net/WireMock.Net/issues/325 - [Fact] - public async Task IWireMockAdminApi_PutMappingAsync() - { - // Arrange - var server = WireMockServer.StartWithAdminInterface(); - var api = RestClient.For(server.Urls[0]); - - // Act - var model = new MappingModel - { - Request = new RequestModel { Path = "/1" }, - Response = new ResponseModel { Body = "txt", StatusCode = 200 }, - Priority = 500, - Title = "test" - }; - var result = await api.PutMappingAsync(new Guid("a0000000-0000-0000-0000-000000000000"), model).ConfigureAwait(false); - - // Assert - Check.That(result).IsNotNull(); - Check.That(result.Status).Equals("Mapping added or updated"); - Check.That(result.Guid).IsNotNull(); - - var mapping = server.Mappings.Single(m => m.Priority == 500); - Check.That(mapping).IsNotNull(); - Check.That(mapping.Title).Equals("test"); - - server.Stop(); - } - - [Theory] - [InlineData(null, null)] - [InlineData(-1, -1)] - [InlineData(0, 0)] - [InlineData(200, 200)] - [InlineData("200", "200")] - public async Task IWireMockAdminApi_PostMappingAsync_WithStatusCode(object statusCode, object expectedStatusCode) - { - // Arrange - var server = WireMockServer.StartWithAdminInterface(); - var api = RestClient.For(server.Urls[0]); - - // Act - var model = new MappingModel - { - Request = new RequestModel { Path = "/1" }, - Response = new ResponseModel { Body = "txt", StatusCode = statusCode }, - Priority = 500, - Title = "test" - }; - var result = await api.PostMappingAsync(model).ConfigureAwait(false); - - // Assert - Check.That(result).IsNotNull(); - Check.That(result.Status).IsNotNull(); - Check.That(result.Guid).IsNotNull(); - - var mapping = server.Mappings.Single(m => m.Priority == 500); - Check.That(mapping).IsNotNull(); - Check.That(mapping.Title).Equals("test"); - - var response = await mapping.ProvideResponseAsync(new RequestMessage(new UrlDetails("http://localhost/1"), "GET", "")).ConfigureAwait(false); - Check.That(response.Message.StatusCode).Equals(expectedStatusCode); - - server.Stop(); - } - - [Fact] - public async Task IWireMockAdminApi_PostMappingsAsync() - { - // Arrange - var server = WireMockServer.StartWithAdminInterface(); - var api = RestClient.For(server.Urls[0]); - - // Act - var model1 = new MappingModel - { - Request = new RequestModel { Path = "/1" }, - Response = new ResponseModel { Body = "txt 1" }, - Title = "test 1" - }; - var model2 = new MappingModel - { - Request = new RequestModel { Path = "/2" }, - Response = new ResponseModel { Body = "txt 2" }, - Title = "test 2" - }; - var result = await api.PostMappingsAsync(new[] { model1, model2 }).ConfigureAwait(false); - - // Assert - Check.That(result).IsNotNull(); - Check.That(result.Status).IsNotNull(); - Check.That(result.Guid).IsNull(); - Check.That(server.Mappings.Where(m => !m.IsAdminInterface)).HasSize(2); - - server.Stop(); - } - - [Fact] - public async Task IWireMockAdminApi_PostMappingsAsync_WithDuplicateGuids_Should_Return_400() - { - // Arrange - var guid = Guid.Parse("1b731398-4a5b-457f-a6e3-d65e541c428f"); - var server = WireMockServer.StartWithAdminInterface(); - var api = RestClient.For(server.Urls[0]); - - // Act - var model1WithGuid = new MappingModel - { - Guid = guid, - Request = new RequestModel { Path = "/1g" }, - Response = new ResponseModel { Body = "txt 1g" }, - Title = "test 1g" - }; - var model2WithGuid = new MappingModel - { - Guid = guid, - Request = new RequestModel { Path = "/2g" }, - Response = new ResponseModel { Body = "txt 2g" }, - Title = "test 2g" - }; - var model1 = new MappingModel - { - Request = new RequestModel { Path = "/1" }, - Response = new ResponseModel { Body = "txt 1" }, - Title = "test 1" - }; - var model2 = new MappingModel - { - Request = new RequestModel { Path = "/2" }, - Response = new ResponseModel { Body = "txt 2" }, - Title = "test 2" - }; - - var models = new[] - { - model1WithGuid, - model2WithGuid, - model1, - model2 - }; - - var sutMethod = async () => await api.PostMappingsAsync(models); - var exceptionAssertions = await sutMethod.Should().ThrowAsync(); - exceptionAssertions.Which.Content.Should().Be(@"{""Status"":""The following Guids are duplicate : '1b731398-4a5b-457f-a6e3-d65e541c428f' (Parameter 'mappingModels')""}"); - - server.Stop(); - } - - [Fact] - public async Task IWireMockAdminApi_FindRequestsAsync() - { - // Arrange - var server = WireMockServer.Start(new WireMockServerSettings - { - StartAdminInterface = true, - Logger = new WireMockNullLogger() - }); - server - .Given(Request.Create().WithPath("/foo").UsingGet()) - .RespondWith(Response.Create()); - - var serverUrl = "http://localhost:" + server.Ports[0]; - await new HttpClient().GetAsync(serverUrl + "/foo").ConfigureAwait(false); - var api = RestClient.For(serverUrl); - - // Act - var requests = await api.FindRequestsAsync(new RequestModel { Methods = new[] { "GET" } }).ConfigureAwait(false); - - // Assert - requests.Should().HaveCount(1); - var requestLogged = requests.First(); - requestLogged.Request.Method.Should().Be("GET"); - requestLogged.Request.Body.Should().BeNull(); - requestLogged.Request.Path.Should().Be("/foo"); - } - - [Fact] - public async Task IWireMockAdminApi_FindRequestsByMappingGuidAsync_Found() - { - // Arrange - var mappingGuid = Guid.NewGuid(); - var server = WireMockServer.Start(new WireMockServerSettings - { - StartAdminInterface = true, - Logger = new WireMockNullLogger() - }); - server - .Given(Request.Create().WithPath("/foo").UsingGet()) - .WithGuid(mappingGuid) - .RespondWith(Response.Create()); - - var serverUrl = "http://localhost:" + server.Ports[0]; - using var client = new HttpClient(); - await client.GetAsync(serverUrl + "/foo").ConfigureAwait(false); - await client.GetAsync(serverUrl + "/foo?bar=baz").ConfigureAwait(false); - var api = RestClient.For(serverUrl); - - // Act - var logEntryModels = await api.FindRequestsByMappingGuidAsync(mappingGuid).ConfigureAwait(false); - - // Assert - logEntryModels.Should().HaveCount(2); - logEntryModels[0].Should().NotBeNull(); - logEntryModels[0]!.Request.Method.Should().Be("GET"); - logEntryModels[0]!.Request.Body.Should().BeNull(); - logEntryModels[0]!.Request.Path.Should().Be("/foo"); - logEntryModels[0]!.Request.Query.Should().BeNullOrEmpty(); - logEntryModels[1].Should().NotBeNull(); - logEntryModels[1]!.Request.Method.Should().Be("GET"); - logEntryModels[1]!.Request.Body.Should().BeNull(); - logEntryModels[1]!.Request.Path.Should().Be("/foo"); - logEntryModels[1]!.Request.Query.Should().BeEquivalentTo(new Dictionary> - { - {"bar", new WireMockList("baz")} - }); - } - - [Fact] - public async Task IWireMockAdminApi_FindRequestsByMappingGuidAsync_NotFound() - { - // Arrange - var server = WireMockServer.Start(new WireMockServerSettings - { - StartAdminInterface = true, - Logger = new WireMockNullLogger() - }); - server - .Given(Request.Create().WithPath("/foo").UsingGet()) - .WithGuid(Guid.NewGuid()) - .RespondWith(Response.Create()); - - var serverUrl = "http://localhost:" + server.Ports[0]; - await new HttpClient().GetAsync(serverUrl + "/foo").ConfigureAwait(false); - var api = RestClient.For(serverUrl); - - // Act - var logEntryModels = await api.FindRequestsByMappingGuidAsync(Guid.NewGuid()).ConfigureAwait(false); - - // Assert - logEntryModels.Should().BeEmpty(); - } - - [Fact] - public async Task IWireMockAdminApi_FindRequestsByMappingGuidAsync_Invalid_ShouldReturnBadRequest() - { - // Arrange - var server = WireMockServer.Start(new WireMockServerSettings - { - StartAdminInterface = true, - Logger = new WireMockNullLogger() - }); - - // Act - var result = await server.CreateClient().GetAsync("/__admin/requests/find?mappingGuid=x"); - - // Assert - result.StatusCode.Should().Be(HttpStatusCode.BadRequest); - } - - [Fact] - public async Task IWireMockAdminApi_GetRequestsAsync() - { - // Arrange - var server = WireMockServer.Start(new WireMockServerSettings - { - StartAdminInterface = true, - Logger = new WireMockNullLogger() - }); - var serverUrl = "http://localhost:" + server.Ports[0]; - await new HttpClient().GetAsync(serverUrl + "/foo").ConfigureAwait(false); - var api = RestClient.For(serverUrl); - - // Act - var requests = await api.GetRequestsAsync().ConfigureAwait(false); - - // Assert - Check.That(requests).HasSize(1); - var requestLogged = requests.First(); - Check.That(requestLogged.Request.Method).IsEqualTo("GET"); - Check.That(requestLogged.Request.Body).IsNull(); - Check.That(requestLogged.Request.Path).IsEqualTo("/foo"); - } - - [Fact] - public async Task IWireMockAdminApi_GetRequestsAsync_JsonApi() - { - // Arrange - var server = WireMockServer.Start(new WireMockServerSettings - { - StartAdminInterface = true, - Logger = new WireMockNullLogger() - }); - string serverUrl = server.Urls[0]; - string data = "{\"data\":[{\"type\":\"program\",\"attributes\":{\"alias\":\"T000001\",\"title\":\"Title Group Entity\"}}]}"; - string jsonApiAcceptHeader = "application/vnd.api+json"; - string jsonApiContentType = "application/vnd.api+json"; - - var request = new HttpRequestMessage(HttpMethod.Post, serverUrl); - request.Headers.Accept.Clear(); - request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(jsonApiAcceptHeader)); - request.Content = new StringContent(data); - request.Content.Headers.ContentType = MediaTypeHeaderValue.Parse(jsonApiContentType); - - var response = await new HttpClient().SendAsync(request); - Check.That(response).IsNotNull(); - - var api = RestClient.For(serverUrl); - - // Act - var requests = await api.GetRequestsAsync().ConfigureAwait(false); - - // Assert - Check.That(requests).HasSize(1); - var requestLogged = requests.First(); - Check.That(requestLogged.Request.Method).IsEqualTo("POST"); - Check.That(requestLogged.Request.Body).IsNotNull(); - Check.That(requestLogged.Request.Body).Contains("T000001"); - } - - [Fact] - public async Task IWireMockAdminApi_GetMappingAsync_WithBodyModelMatcherModel_WithoutMethods_ShouldReturnCorrectMappingModel() - { - // Arrange - var guid = Guid.Parse("90356dba-b36c-469a-a17e-669cd84f1f05"); - var server = WireMockServer.StartWithAdminInterface(); - var api = RestClient.For(server.Url); - - // Act - var model = new MappingModel - { - Guid = guid, - Request = new RequestModel - { - Path = "/1", - Body = new BodyModel - { - Matcher = new MatcherModel - { - Name = "RegexMatcher", - Pattern = "hello", - IgnoreCase = true - } - } - }, - Response = new ResponseModel { Body = "world" } - }; - var postMappingResult = await api.PostMappingAsync(model).ConfigureAwait(false); - - // Assert - postMappingResult.Should().NotBeNull(); - - var mapping = server.Mappings.FirstOrDefault(m => m.Guid == guid); - mapping.Should().NotBeNull(); - - var getMappingResult = await api.GetMappingAsync(guid).ConfigureAwait(false); - - await Verifier.Verify(getMappingResult, VerifySettings).DontScrubGuids(); - - server.Stop(); - } - - [Fact] - public async Task IWireMockAdminApi_GetMappingAsync_WithProxy_And_ProxyUrlReplaceSettings() - { - // Arrange - var guid = Guid.Parse("90356dba-b36c-469a-a17e-669cd84f1f05"); - var server = WireMockServer.StartWithAdminInterface(); - var api = RestClient.For(server.Url); - - // Act - var model = new MappingModel - { - Guid = guid, - Request = new RequestModel - { - Path = "/1", - Body = new BodyModel - { - Matcher = new MatcherModel - { - Name = "RegexMatcher", - Pattern = "hello", - IgnoreCase = true - } - } - }, - Response = new ResponseModel - { - ProxyUrl = "https://my-proxy.com", - ProxyUrlReplaceSettings = new ProxyUrlReplaceSettingsModel - { - OldValue = "x", - NewValue = "y", - IgnoreCase = true - } - } - }; - var postMappingResult = await api.PostMappingAsync(model).ConfigureAwait(false); - - // Assert - postMappingResult.Should().NotBeNull(); - - var mapping = server.Mappings.FirstOrDefault(m => m.Guid == guid); - mapping.Should().NotBeNull(); - - var getMappingResult = await api.GetMappingAsync(guid).ConfigureAwait(false); - - await Verifier.Verify(getMappingResult, VerifySettings).DontScrubGuids(); - - server.Stop(); - } - - [Fact] - public async Task IWireMockAdminApi_GetRequestsAsync_Json() - { - // Arrange - var server = WireMockServer.Start(new WireMockServerSettings - { - StartAdminInterface = true, - Logger = new WireMockNullLogger() - }); - string serverUrl = server.Urls[0]; - string data = "{\"alias\": \"T000001\"}"; - string jsonAcceptHeader = "application/json"; - string jsonApiContentType = "application/json"; - - var request = new HttpRequestMessage(HttpMethod.Post, serverUrl); - request.Headers.Accept.Clear(); - request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(jsonAcceptHeader)); - request.Content = new StringContent(data); - request.Content.Headers.ContentType = MediaTypeHeaderValue.Parse(jsonApiContentType); - var response = await new HttpClient().SendAsync(request); - Check.That(response).IsNotNull(); - - var api = RestClient.For(serverUrl); - - // Act - var requests = await api.GetRequestsAsync().ConfigureAwait(false); - - // Assert - Check.That(requests).HasSize(1); - var requestLogged = requests.First(); - Check.That(requestLogged.Request.Method).IsEqualTo("POST"); - Check.That(requestLogged.Request.Body).IsNotNull(); - Check.That(requestLogged.Request.Body).Contains("T000001"); - } - - [Fact] - public async Task IWireMockAdminApi_PostFileAsync_Ascii() - { - // Arrange - var filesystemHandlerMock = new Mock(MockBehavior.Strict); - filesystemHandlerMock.Setup(fs => fs.GetMappingFolder()).Returns("__admin/mappings"); - filesystemHandlerMock.Setup(fs => fs.FolderExists(It.IsAny())).Returns(true); - filesystemHandlerMock.Setup(fs => fs.WriteFile(It.IsAny(), It.IsAny())); - - var server = WireMockServer.Start(new WireMockServerSettings - { - UseSSL = false, - StartAdminInterface = true, - FileSystemHandler = filesystemHandlerMock.Object - }); - - var api = RestClient.For(server.Urls[0]); - - // Act - var request = await api.PostFileAsync("filename.txt", "abc").ConfigureAwait(false); - - // Assert - Check.That(request.Guid).IsNull(); - Check.That(request.Status).Contains("File"); - - // Verify - filesystemHandlerMock.Verify(fs => fs.GetMappingFolder(), Times.Once); - filesystemHandlerMock.Verify(fs => fs.FolderExists(It.IsAny()), Times.Once); - filesystemHandlerMock.Verify(fs => fs.WriteFile(It.Is(p => p == "filename.txt"), It.IsAny()), Times.Once); - filesystemHandlerMock.VerifyNoOtherCalls(); - - server.Stop(); - } - - [Fact] - public async Task IWireMockAdminApi_PutFileAsync_Ascii() - { - // Arrange - var filesystemHandlerMock = new Mock(MockBehavior.Strict); - filesystemHandlerMock.Setup(fs => fs.FileExists(It.IsAny())).Returns(true); - filesystemHandlerMock.Setup(fs => fs.WriteFile(It.IsAny(), It.IsAny())); - - var server = WireMockServer.Start(new WireMockServerSettings - { - UseSSL = false, - StartAdminInterface = true, - FileSystemHandler = filesystemHandlerMock.Object - }); - - var api = RestClient.For(server.Urls[0]); - - // Act - var request = await api.PutFileAsync("filename.txt", "abc-abc").ConfigureAwait(false); - - // Assert - Check.That(request.Guid).IsNull(); - Check.That(request.Status).Contains("File"); - - // Verify - filesystemHandlerMock.Verify(fs => fs.WriteFile(It.Is(p => p == "filename.txt"), It.IsAny()), Times.Once); - filesystemHandlerMock.Verify(fs => fs.FileExists(It.Is(p => p == "filename.txt")), Times.Once); - filesystemHandlerMock.VerifyNoOtherCalls(); - - server.Stop(); - } - - [Fact] - public void IWireMockAdminApi_PutFileAsync_NotFound() - { - // Arrange - var filesystemHandlerMock = new Mock(MockBehavior.Strict); - filesystemHandlerMock.Setup(fs => fs.FileExists(It.IsAny())).Returns(false); - - var server = WireMockServer.Start(new WireMockServerSettings - { - UseSSL = false, - StartAdminInterface = true, - FileSystemHandler = filesystemHandlerMock.Object - }); - - var api = RestClient.For(server.Urls[0]); - - // Act and Assert - Check.ThatAsyncCode(() => api.PutFileAsync("filename.txt", "xxx")).Throws(); - - // Verify - filesystemHandlerMock.Verify(fs => fs.FileExists(It.Is(p => p == "filename.txt")), Times.Once); - filesystemHandlerMock.VerifyNoOtherCalls(); - - server.Stop(); - } - - [Fact] - public void IWireMockAdminApi_GetFileAsync_NotFound() - { - // Arrange - var filesystemHandlerMock = new Mock(MockBehavior.Strict); - filesystemHandlerMock.Setup(fs => fs.FileExists(It.IsAny())).Returns(false); - filesystemHandlerMock.Setup(fs => fs.ReadFile(It.IsAny())).Returns(Encoding.ASCII.GetBytes("Here's a string.")); - - var server = WireMockServer.Start(new WireMockServerSettings - { - UseSSL = false, - StartAdminInterface = true, - FileSystemHandler = filesystemHandlerMock.Object - }); - - var api = RestClient.For(server.Urls[0]); - - // Act and Assert - Check.ThatAsyncCode(() => api.GetFileAsync("filename.txt")).Throws(); - - // Verify - filesystemHandlerMock.Verify(fs => fs.FileExists(It.Is(p => p == "filename.txt")), Times.Once); - filesystemHandlerMock.VerifyNoOtherCalls(); - - server.Stop(); - } - - [Fact] - public async Task IWireMockAdminApi_GetFileAsync_Found() - { - // Arrange - string data = "Here's a string."; - var filesystemHandlerMock = new Mock(MockBehavior.Strict); - filesystemHandlerMock.Setup(fs => fs.FileExists(It.IsAny())).Returns(true); - filesystemHandlerMock.Setup(fs => fs.ReadFile(It.IsAny())).Returns(Encoding.ASCII.GetBytes(data)); - - var server = WireMockServer.Start(new WireMockServerSettings - { - UseSSL = false, - StartAdminInterface = true, - FileSystemHandler = filesystemHandlerMock.Object - }); - - var api = RestClient.For(server.Urls[0]); - - // Act - string file = await api.GetFileAsync("filename.txt").ConfigureAwait(false); - - // Assert - Check.That(file).Equals(data); - - // Verify - filesystemHandlerMock.Verify(fs => fs.FileExists(It.Is(p => p == "filename.txt")), Times.Once); - filesystemHandlerMock.Verify(fs => fs.ReadFile(It.Is(p => p == "filename.txt")), Times.Once); - filesystemHandlerMock.VerifyNoOtherCalls(); - - server.Stop(); - } - - [Fact] - public async Task IWireMockAdminApi_DeleteFileAsync_Ok() - { - // Arrange - var filesystemHandlerMock = new Mock(MockBehavior.Strict); - filesystemHandlerMock.Setup(fs => fs.FileExists(It.IsAny())).Returns(true); - filesystemHandlerMock.Setup(fs => fs.DeleteFile(It.IsAny())); - - var server = WireMockServer.Start(new WireMockServerSettings - { - UseSSL = false, - StartAdminInterface = true, - FileSystemHandler = filesystemHandlerMock.Object - }); - - var api = RestClient.For(server.Urls[0]); - - // Act - await api.DeleteFileAsync("filename.txt").ConfigureAwait(false); - - // Verify - filesystemHandlerMock.Verify(fs => fs.FileExists(It.Is(p => p == "filename.txt")), Times.Once); - filesystemHandlerMock.Verify(fs => fs.DeleteFile(It.Is(p => p == "filename.txt")), Times.Once); - filesystemHandlerMock.VerifyNoOtherCalls(); - - server.Stop(); - } - - [Fact] - public void IWireMockAdminApi_DeleteFileAsync_NotFound() - { - // Arrange - var filesystemHandlerMock = new Mock(MockBehavior.Strict); - filesystemHandlerMock.Setup(fs => fs.FileExists(It.IsAny())).Returns(false); - filesystemHandlerMock.Setup(fs => fs.DeleteFile(It.IsAny())); - - var server = WireMockServer.Start(new WireMockServerSettings - { - UseSSL = false, - StartAdminInterface = true, - FileSystemHandler = filesystemHandlerMock.Object - }); - - var api = RestClient.For(server.Urls[0]); - - // Act and Assert - Check.ThatAsyncCode(() => api.DeleteFileAsync("filename.txt")).Throws(); - - // Verify - filesystemHandlerMock.Verify(fs => fs.FileExists(It.Is(p => p == "filename.txt")), Times.Once); - filesystemHandlerMock.VerifyNoOtherCalls(); - - server.Stop(); - } - - [Fact] - public void IWireMockAdminApi_FileExistsAsync_NotFound() - { - // Arrange - var filesystemHandlerMock = new Mock(MockBehavior.Strict); - filesystemHandlerMock.Setup(fs => fs.FileExists(It.IsAny())).Returns(false); - - var server = WireMockServer.Start(new WireMockServerSettings - { - UseSSL = false, - StartAdminInterface = true, - FileSystemHandler = filesystemHandlerMock.Object - }); - - var api = RestClient.For(server.Urls[0]); - - // Act and Assert - Check.ThatAsyncCode(() => api.FileExistsAsync("filename.txt")).Throws(); - - // Verify - filesystemHandlerMock.Verify(fs => fs.FileExists(It.Is(p => p == "filename.txt")), Times.Once); - filesystemHandlerMock.VerifyNoOtherCalls(); - - server.Stop(); - } - - [Fact] - public async Task IWireMockAdminApi_DeleteScenarioUsingDeleteAsync() - { - // Arrange - var name = "x"; - var server = WireMockServer.StartWithAdminInterface(); - var api = RestClient.For(server.Urls[0]); - - // Act - var status = await api.DeleteScenarioAsync(name).ConfigureAwait(false); - status.Status.Should().Be("No scenario found by name 'x'."); - } - - [Fact] - public async Task IWireMockAdminApi_DeleteScenarioUsingPostAsync() - { - // Arrange - var name = "x"; - var server = WireMockServer.StartWithAdminInterface(); - var api = RestClient.For(server.Urls[0]); - - // Act - var status = await api.ResetScenarioAsync(name).ConfigureAwait(false); - status.Status.Should().Be("No scenario found by name 'x'."); - } - - [Fact] - public async Task IWireMockAdminApi_GetMappingByGuidAsync() - { - // Arrange - var guid = Guid.Parse("90356dba-b36c-469a-a17e-669cd84f1f05"); - var server = WireMockServer.StartWithAdminInterface(); - - server - .Given( - Request.Create() - .WithPath("/foo1") - .WithParam("p1", "xyz") - .UsingGet() - ) - .WithGuid(guid) - .RespondWith( - Response.Create() - .WithStatusCode(200) - .WithBody("1") - ); - - // Act - var api = RestClient.For(server.Url); - var getMappingResult = await api.GetMappingAsync(guid).ConfigureAwait(false); - - // Assert - var mapping = server.Mappings.FirstOrDefault(m => m.Guid == guid); - mapping.Should().NotBeNull(); - - await Verifier.Verify(getMappingResult, VerifySettings).DontScrubGuids(); - - server.Stop(); - } - - [Fact] - public async Task IWireMockAdminApi_GetMappingCodeByGuidAsync() - { - // Arrange - var guid = Guid.Parse("90356dba-b36c-469a-a17e-669cd84f1f05"); - var server = WireMockServer.StartWithAdminInterface(); - - server - .Given( - Request.Create() - .WithPath("/foo1") - .WithParam("p1", "xyz") - .UsingGet() - ) - .WithGuid(guid) - .RespondWith( - Response.Create() - .WithStatusCode(200) - .WithBody("1") - ); - - // Act - var api = RestClient.For(server.Url); - - var mappings = await api.GetMappingsAsync().ConfigureAwait(false); - mappings.Should().HaveCount(1); - - var code = await api.GetMappingCodeAsync(guid).ConfigureAwait(false); - - // Assert - await Verifier.Verify(code).DontScrubDateTimes().DontScrubGuids(); - - server.Stop(); - } - - [Fact] - public async Task IWireMockAdminApi_GetMappingsCode() - { - // Arrange - var guid1 = Guid.Parse("90356dba-b36c-469a-a17e-669cd84f1f05"); - var guid2 = Guid.Parse("1b731398-4a5b-457f-a6e3-d65e541c428f"); - var guid3 = Guid.Parse("f74fd144-df53-404f-8e35-da22a640bd5f"); - var guid4 = Guid.Parse("4126DEC8-470B-4EFF-93BB-C24F83B8B1FD"); - var server = WireMockServer.StartWithAdminInterface(); - - server - .Given( - Request.Create() - .WithPath("/foo1") - .WithParam("p1", "xyz") - .UsingGet() - ) - .WithGuid(guid1) - .RespondWith( - Response.Create() - .WithStatusCode(200) - .WithBody("1") - ); - - server - .Given( - Request.Create() - .WithPath("/foo2") - .WithParam("p2", "abc") - .WithHeader("h1", "W/\"234f2q3r\"") - .UsingPost() - ) - .WithGuid(guid2) - .RespondWith( - Response.Create() - .WithStatusCode("201") - .WithHeader("hk", "hv") - .WithHeader("ETag", "W/\"168d8e\"") - .WithBody("2") - ); - - server - .Given( - Request.Create() - .WithUrl("https://localhost/test") - .UsingDelete() - ) - .WithGuid(guid3) - .RespondWith( - Response.Create() - .WithStatusCode(HttpStatusCode.AlreadyReported) - .WithBodyAsJson(new { @as = 1, b = 1.2, d = true, e = false, f = new[] { 1, 2, 3, 4 }, g = new { z1 = 1, z2 = 2, z3 = new[] { "a", "b", "c" }, z4 = new[] { new { a = 1, b = 2 }, new { a = 2, b = 3 } } }, date_field = new DateTime(2023, 05, 08, 11, 20, 19), string_field_with_date = "2021-03-13T21:04:00Z", multiline_text = @"This -is -multiline -text -" }) - ); - - server - .Given( - Request.Create() - .WithPath("/foo3") - .WithBody(new JsonPartialMatcher(new { a = 1, b = 2 })) - .UsingPost() - ) - .WithGuid(guid4) - .RespondWith( - Response.Create() - .WithStatusCode(200) - .WithBody("Line1\r\nSome \"value\" in Line2") - ); - - // Act - var api = RestClient.For(server.Url); - - var mappings = await api.GetMappingsAsync().ConfigureAwait(false); - mappings.Should().HaveCount(4); - - var code = await api.GetMappingsCodeAsync().ConfigureAwait(false); - - // Assert - await Verifier.Verify(code).DontScrubDateTimes().DontScrubGuids(); - - server.Stop(); - } - - [Fact] - public async Task IWireMockAdminApi_OpenApiConvert_Yml() - { - // Arrange - var openApiDocument = await File.ReadAllTextAsync(Path.Combine("OpenApiParser", "petstore.yml")); - - var server = WireMockServer.StartWithAdminInterface(); - var api = RestClient.For(server.Url); - - // Act - var mappings = await api.OpenApiConvertAsync(openApiDocument).ConfigureAwait(false); - - // Assert - server.MappingModels.Should().BeEmpty(); - mappings.Should().HaveCount(20); - - server.Stop(); - } - - [Fact] - public async Task IWireMockAdminApi_OpenApiConvert_Json() - { - // Arrange - var openApiDocument = await File.ReadAllTextAsync(Path.Combine("OpenApiParser", "petstore-openapi3.json")); - - var server = WireMockServer.StartWithAdminInterface(); - var api = RestClient.For(server.Url); - - // Act - var mappings = await api.OpenApiConvertAsync(openApiDocument).ConfigureAwait(false); - - // Assert - server.MappingModels.Should().BeEmpty(); - mappings.Should().HaveCount(19); - - server.Stop(); - } - - [Fact] - public async Task IWireMockAdminApi_OpenApiSave_Json() - { - // Arrange - var openApiDocument = await File.ReadAllTextAsync(Path.Combine("OpenApiParser", "petstore-openapi3.json")); - - var server = WireMockServer.StartWithAdminInterface(); - var api = RestClient.For(server.Url); - - // Act - var statusModel = await api.OpenApiSaveAsync(openApiDocument).ConfigureAwait(false); - - // Assert - statusModel.Status.Should().Be("OpenApi document converted to Mappings"); - server.MappingModels.Should().HaveCount(19); - - server.Stop(); - } - - [Fact] - public async Task IWireMockAdminApi_OpenApiSave_Yml() - { - // Arrange - var openApiDocument = await File.ReadAllTextAsync(Path.Combine("OpenApiParser", "petstore.yml")); - - var server = WireMockServer.StartWithAdminInterface(); - var api = RestClient.For(server.Url); - - // Act - var mappings = await api.OpenApiConvertAsync(openApiDocument).ConfigureAwait(false); - - // Assert - server.MappingModels.Should().BeEmpty(); - mappings.Should().HaveCount(20); - - server.Stop(); - } -} -#endif +#if !(NET452 || NET461 || NETCOREAPP3_1) +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Text; +using System.Threading.Tasks; +using FluentAssertions; +using Moq; +using NFluent; +using RestEase; +using VerifyTests; +using VerifyXunit; +using WireMock.Admin.Mappings; +using WireMock.Admin.Settings; +using WireMock.Client; +using WireMock.Handlers; +using WireMock.Logging; +using WireMock.Matchers; +using WireMock.Models; +using WireMock.Net.Tests.VerifyExtensions; +using WireMock.RequestBuilders; +using WireMock.ResponseBuilders; +using WireMock.Server; +using WireMock.Settings; +using WireMock.Types; +using Xunit; + +namespace WireMock.Net.Tests.AdminApi; + +[UsesVerify] +public partial class WireMockAdminApiTests +{ + private static readonly VerifySettings VerifySettings = new(); + + static WireMockAdminApiTests() + { + VerifyNewtonsoftJson.Enable(VerifySettings); + } + + [Fact] + public async Task IWireMockAdminApi_GetSettingsAsync() + { + // Arrange + var server = WireMockServer.StartWithAdminInterface(); + var api = RestClient.For(server.Urls[0]); + + // Act + var settings = await api.GetSettingsAsync().ConfigureAwait(false); + Check.That(settings).IsNotNull(); + } + + [Fact] + public async Task IWireMockAdminApi_PostSettingsAsync() + { + // Arrange + var server = WireMockServer.StartWithAdminInterface(); + var api = RestClient.For(server.Urls[0]); + + // Act + var settings = new SettingsModel(); + var status = await api.PostSettingsAsync(settings).ConfigureAwait(false); + Check.That(status.Status).Equals("Settings updated"); + } + + [Fact] + public async Task IWireMockAdminApi_PutSettingsAsync() + { + // Arrange + var server = WireMockServer.StartWithAdminInterface(); + var api = RestClient.For(server.Urls[0]); + + // Act + var settings = new SettingsModel(); + var status = await api.PutSettingsAsync(settings).ConfigureAwait(false); + Check.That(status.Status).Equals("Settings updated"); + } + + // https://github.com/WireMock-Net/WireMock.Net/issues/325 + [Fact] + public async Task IWireMockAdminApi_PutMappingAsync() + { + // Arrange + var server = WireMockServer.StartWithAdminInterface(); + var api = RestClient.For(server.Urls[0]); + + // Act + var model = new MappingModel + { + Request = new RequestModel { Path = "/1" }, + Response = new ResponseModel { Body = "txt", StatusCode = 200 }, + Priority = 500, + Title = "test" + }; + var result = await api.PutMappingAsync(new Guid("a0000000-0000-0000-0000-000000000000"), model).ConfigureAwait(false); + + // Assert + Check.That(result).IsNotNull(); + Check.That(result.Status).Equals("Mapping added or updated"); + Check.That(result.Guid).IsNotNull(); + + var mapping = server.Mappings.Single(m => m.Priority == 500); + Check.That(mapping).IsNotNull(); + Check.That(mapping.Title).Equals("test"); + + server.Stop(); + } + + [Theory] + [InlineData(null, null)] + [InlineData(-1, -1)] + [InlineData(0, 0)] + [InlineData(200, 200)] + [InlineData("200", "200")] + public async Task IWireMockAdminApi_PostMappingAsync_WithStatusCode(object statusCode, object expectedStatusCode) + { + // Arrange + var server = WireMockServer.StartWithAdminInterface(); + var api = RestClient.For(server.Urls[0]); + + // Act + var model = new MappingModel + { + Request = new RequestModel { Path = "/1" }, + Response = new ResponseModel { Body = "txt", StatusCode = statusCode }, + Priority = 500, + Title = "test" + }; + var result = await api.PostMappingAsync(model).ConfigureAwait(false); + + // Assert + Check.That(result).IsNotNull(); + Check.That(result.Status).IsNotNull(); + Check.That(result.Guid).IsNotNull(); + + var mapping = server.Mappings.Single(m => m.Priority == 500); + Check.That(mapping).IsNotNull(); + Check.That(mapping.Title).Equals("test"); + + var response = await mapping.ProvideResponseAsync(new RequestMessage(new UrlDetails("http://localhost/1"), "GET", "")).ConfigureAwait(false); + Check.That(response.Message.StatusCode).Equals(expectedStatusCode); + + server.Stop(); + } + + [Fact] + public async Task IWireMockAdminApi_PostMappingsAsync() + { + // Arrange + var server = WireMockServer.StartWithAdminInterface(); + var api = RestClient.For(server.Urls[0]); + + // Act + var model1 = new MappingModel + { + Request = new RequestModel { Path = "/1" }, + Response = new ResponseModel { Body = "txt 1" }, + Title = "test 1" + }; + var model2 = new MappingModel + { + Request = new RequestModel { Path = "/2" }, + Response = new ResponseModel { Body = "txt 2" }, + Title = "test 2" + }; + var result = await api.PostMappingsAsync(new[] { model1, model2 }).ConfigureAwait(false); + + // Assert + Check.That(result).IsNotNull(); + Check.That(result.Status).IsNotNull(); + Check.That(result.Guid).IsNull(); + Check.That(server.Mappings.Where(m => !m.IsAdminInterface)).HasSize(2); + + server.Stop(); + } + + [Fact] + public async Task IWireMockAdminApi_PostMappingsAsync_WithDuplicateGuids_Should_Return_400() + { + // Arrange + var guid = Guid.Parse("1b731398-4a5b-457f-a6e3-d65e541c428f"); + var server = WireMockServer.StartWithAdminInterface(); + var api = RestClient.For(server.Urls[0]); + + // Act + var model1WithGuid = new MappingModel + { + Guid = guid, + Request = new RequestModel { Path = "/1g" }, + Response = new ResponseModel { Body = "txt 1g" }, + Title = "test 1g" + }; + var model2WithGuid = new MappingModel + { + Guid = guid, + Request = new RequestModel { Path = "/2g" }, + Response = new ResponseModel { Body = "txt 2g" }, + Title = "test 2g" + }; + var model1 = new MappingModel + { + Request = new RequestModel { Path = "/1" }, + Response = new ResponseModel { Body = "txt 1" }, + Title = "test 1" + }; + var model2 = new MappingModel + { + Request = new RequestModel { Path = "/2" }, + Response = new ResponseModel { Body = "txt 2" }, + Title = "test 2" + }; + + var models = new[] + { + model1WithGuid, + model2WithGuid, + model1, + model2 + }; + + var sutMethod = async () => await api.PostMappingsAsync(models); + var exceptionAssertions = await sutMethod.Should().ThrowAsync(); + exceptionAssertions.Which.Content.Should().Be(@"{""Status"":""The following Guids are duplicate : '1b731398-4a5b-457f-a6e3-d65e541c428f' (Parameter 'mappingModels')""}"); + + server.Stop(); + } + + [Fact] + public async Task IWireMockAdminApi_FindRequestsAsync() + { + // Arrange + var server = WireMockServer.Start(new WireMockServerSettings + { + StartAdminInterface = true, + Logger = new WireMockNullLogger() + }); + server + .Given(Request.Create().WithPath("/foo").UsingGet()) + .RespondWith(Response.Create()); + + var serverUrl = "http://localhost:" + server.Ports[0]; + await new HttpClient().GetAsync(serverUrl + "/foo").ConfigureAwait(false); + var api = RestClient.For(serverUrl); + + // Act + var requests = await api.FindRequestsAsync(new RequestModel { Methods = new[] { "GET" } }).ConfigureAwait(false); + + // Assert + requests.Should().HaveCount(1); + var requestLogged = requests.First(); + requestLogged.Request.Method.Should().Be("GET"); + requestLogged.Request.Body.Should().BeNull(); + requestLogged.Request.Path.Should().Be("/foo"); + } + + [Fact] + public async Task IWireMockAdminApi_FindRequestsByMappingGuidAsync_Found() + { + // Arrange + var mappingGuid = Guid.NewGuid(); + var server = WireMockServer.Start(new WireMockServerSettings + { + StartAdminInterface = true, + Logger = new WireMockNullLogger() + }); + server + .Given(Request.Create().WithPath("/foo").UsingGet()) + .WithGuid(mappingGuid) + .RespondWith(Response.Create()); + + var serverUrl = "http://localhost:" + server.Ports[0]; + using var client = new HttpClient(); + await client.GetAsync(serverUrl + "/foo").ConfigureAwait(false); + await client.GetAsync(serverUrl + "/foo?bar=baz").ConfigureAwait(false); + var api = RestClient.For(serverUrl); + + // Act + var logEntryModels = await api.FindRequestsByMappingGuidAsync(mappingGuid).ConfigureAwait(false); + + // Assert + logEntryModels.Should().HaveCount(2); + logEntryModels[0].Should().NotBeNull(); + logEntryModels[0]!.Request.Method.Should().Be("GET"); + logEntryModels[0]!.Request.Body.Should().BeNull(); + logEntryModels[0]!.Request.Path.Should().Be("/foo"); + logEntryModels[0]!.Request.Query.Should().BeNullOrEmpty(); + logEntryModels[1].Should().NotBeNull(); + logEntryModels[1]!.Request.Method.Should().Be("GET"); + logEntryModels[1]!.Request.Body.Should().BeNull(); + logEntryModels[1]!.Request.Path.Should().Be("/foo"); + logEntryModels[1]!.Request.Query.Should().BeEquivalentTo(new Dictionary> + { + {"bar", new WireMockList("baz")} + }); + } + + [Fact] + public async Task IWireMockAdminApi_FindRequestsByMappingGuidAsync_NotFound() + { + // Arrange + var server = WireMockServer.Start(new WireMockServerSettings + { + StartAdminInterface = true, + Logger = new WireMockNullLogger() + }); + server + .Given(Request.Create().WithPath("/foo").UsingGet()) + .WithGuid(Guid.NewGuid()) + .RespondWith(Response.Create()); + + var serverUrl = "http://localhost:" + server.Ports[0]; + await new HttpClient().GetAsync(serverUrl + "/foo").ConfigureAwait(false); + var api = RestClient.For(serverUrl); + + // Act + var logEntryModels = await api.FindRequestsByMappingGuidAsync(Guid.NewGuid()).ConfigureAwait(false); + + // Assert + logEntryModels.Should().BeEmpty(); + } + + [Fact] + public async Task IWireMockAdminApi_FindRequestsByMappingGuidAsync_Invalid_ShouldReturnBadRequest() + { + // Arrange + var server = WireMockServer.Start(new WireMockServerSettings + { + StartAdminInterface = true, + Logger = new WireMockNullLogger() + }); + + // Act + var result = await server.CreateClient().GetAsync("/__admin/requests/find?mappingGuid=x"); + + // Assert + result.StatusCode.Should().Be(HttpStatusCode.BadRequest); + } + + [Fact] + public async Task IWireMockAdminApi_GetRequestsAsync() + { + // Arrange + var server = WireMockServer.Start(new WireMockServerSettings + { + StartAdminInterface = true, + Logger = new WireMockNullLogger() + }); + var serverUrl = "http://localhost:" + server.Ports[0]; + await new HttpClient().GetAsync(serverUrl + "/foo").ConfigureAwait(false); + var api = RestClient.For(serverUrl); + + // Act + var requests = await api.GetRequestsAsync().ConfigureAwait(false); + + // Assert + Check.That(requests).HasSize(1); + var requestLogged = requests.First(); + Check.That(requestLogged.Request.Method).IsEqualTo("GET"); + Check.That(requestLogged.Request.Body).IsNull(); + Check.That(requestLogged.Request.Path).IsEqualTo("/foo"); + } + + [Fact] + public async Task IWireMockAdminApi_GetRequestsAsync_JsonApi() + { + // Arrange + var server = WireMockServer.Start(new WireMockServerSettings + { + StartAdminInterface = true, + Logger = new WireMockNullLogger() + }); + string serverUrl = server.Urls[0]; + string data = "{\"data\":[{\"type\":\"program\",\"attributes\":{\"alias\":\"T000001\",\"title\":\"Title Group Entity\"}}]}"; + string jsonApiAcceptHeader = "application/vnd.api+json"; + string jsonApiContentType = "application/vnd.api+json"; + + var request = new HttpRequestMessage(HttpMethod.Post, serverUrl); + request.Headers.Accept.Clear(); + request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(jsonApiAcceptHeader)); + request.Content = new StringContent(data); + request.Content.Headers.ContentType = MediaTypeHeaderValue.Parse(jsonApiContentType); + + var response = await new HttpClient().SendAsync(request); + Check.That(response).IsNotNull(); + + var api = RestClient.For(serverUrl); + + // Act + var requests = await api.GetRequestsAsync().ConfigureAwait(false); + + // Assert + Check.That(requests).HasSize(1); + var requestLogged = requests.First(); + Check.That(requestLogged.Request.Method).IsEqualTo("POST"); + Check.That(requestLogged.Request.Body).IsNotNull(); + Check.That(requestLogged.Request.Body).Contains("T000001"); + } + + [Fact] + public async Task IWireMockAdminApi_GetMappingAsync_WithBodyModelMatcherModel_WithoutMethods_ShouldReturnCorrectMappingModel() + { + // Arrange + var guid = Guid.Parse("90356dba-b36c-469a-a17e-669cd84f1f05"); + var server = WireMockServer.StartWithAdminInterface(); + var api = RestClient.For(server.Url); + + // Act + var model = new MappingModel + { + Guid = guid, + Request = new RequestModel + { + Path = "/1", + Body = new BodyModel + { + Matcher = new MatcherModel + { + Name = "RegexMatcher", + Pattern = "hello", + IgnoreCase = true + } + } + }, + Response = new ResponseModel { Body = "world" } + }; + var postMappingResult = await api.PostMappingAsync(model).ConfigureAwait(false); + + // Assert + postMappingResult.Should().NotBeNull(); + + var mapping = server.Mappings.FirstOrDefault(m => m.Guid == guid); + mapping.Should().NotBeNull(); + + var getMappingResult = await api.GetMappingAsync(guid).ConfigureAwait(false); + + await Verifier.Verify(getMappingResult, VerifySettings).DontScrubGuids(); + + server.Stop(); + } + + [Fact] + public async Task IWireMockAdminApi_GetMappingAsync_WithProxy_And_ProxyUrlReplaceSettings() + { + // Arrange + var guid = Guid.Parse("90356dba-b36c-469a-a17e-669cd84f1f05"); + var server = WireMockServer.StartWithAdminInterface(); + var api = RestClient.For(server.Url); + + // Act + var model = new MappingModel + { + Guid = guid, + Request = new RequestModel + { + Path = "/1", + Body = new BodyModel + { + Matcher = new MatcherModel + { + Name = "RegexMatcher", + Pattern = "hello", + IgnoreCase = true + } + } + }, + Response = new ResponseModel + { + ProxyUrl = "https://my-proxy.com", + ProxyUrlReplaceSettings = new ProxyUrlReplaceSettingsModel + { + OldValue = "x", + NewValue = "y", + IgnoreCase = true + } + } + }; + var postMappingResult = await api.PostMappingAsync(model).ConfigureAwait(false); + + // Assert + postMappingResult.Should().NotBeNull(); + + var mapping = server.Mappings.FirstOrDefault(m => m.Guid == guid); + mapping.Should().NotBeNull(); + + var getMappingResult = await api.GetMappingAsync(guid).ConfigureAwait(false); + + await Verifier.Verify(getMappingResult, VerifySettings).DontScrubGuids(); + + server.Stop(); + } + + [Fact] + public async Task IWireMockAdminApi_GetRequestsAsync_Json() + { + // Arrange + var server = WireMockServer.Start(new WireMockServerSettings + { + StartAdminInterface = true, + Logger = new WireMockNullLogger() + }); + string serverUrl = server.Urls[0]; + string data = "{\"alias\": \"T000001\"}"; + string jsonAcceptHeader = "application/json"; + string jsonApiContentType = "application/json"; + + var request = new HttpRequestMessage(HttpMethod.Post, serverUrl); + request.Headers.Accept.Clear(); + request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(jsonAcceptHeader)); + request.Content = new StringContent(data); + request.Content.Headers.ContentType = MediaTypeHeaderValue.Parse(jsonApiContentType); + var response = await new HttpClient().SendAsync(request); + Check.That(response).IsNotNull(); + + var api = RestClient.For(serverUrl); + + // Act + var requests = await api.GetRequestsAsync().ConfigureAwait(false); + + // Assert + Check.That(requests).HasSize(1); + var requestLogged = requests.First(); + Check.That(requestLogged.Request.Method).IsEqualTo("POST"); + Check.That(requestLogged.Request.Body).IsNotNull(); + Check.That(requestLogged.Request.Body).Contains("T000001"); + } + + [Fact] + public async Task IWireMockAdminApi_PostFileAsync_Ascii() + { + // Arrange + var filesystemHandlerMock = new Mock(MockBehavior.Strict); + filesystemHandlerMock.Setup(fs => fs.GetMappingFolder()).Returns("__admin/mappings"); + filesystemHandlerMock.Setup(fs => fs.FolderExists(It.IsAny())).Returns(true); + filesystemHandlerMock.Setup(fs => fs.WriteFile(It.IsAny(), It.IsAny())); + + var server = WireMockServer.Start(new WireMockServerSettings + { + UseSSL = false, + StartAdminInterface = true, + FileSystemHandler = filesystemHandlerMock.Object + }); + + var api = RestClient.For(server.Urls[0]); + + // Act + var request = await api.PostFileAsync("filename.txt", "abc").ConfigureAwait(false); + + // Assert + Check.That(request.Guid).IsNull(); + Check.That(request.Status).Contains("File"); + + // Verify + filesystemHandlerMock.Verify(fs => fs.GetMappingFolder(), Times.Once); + filesystemHandlerMock.Verify(fs => fs.FolderExists(It.IsAny()), Times.Once); + filesystemHandlerMock.Verify(fs => fs.WriteFile(It.Is(p => p == "filename.txt"), It.IsAny()), Times.Once); + filesystemHandlerMock.VerifyNoOtherCalls(); + + server.Stop(); + } + + [Fact] + public async Task IWireMockAdminApi_PutFileAsync_Ascii() + { + // Arrange + var filesystemHandlerMock = new Mock(MockBehavior.Strict); + filesystemHandlerMock.Setup(fs => fs.FileExists(It.IsAny())).Returns(true); + filesystemHandlerMock.Setup(fs => fs.WriteFile(It.IsAny(), It.IsAny())); + + var server = WireMockServer.Start(new WireMockServerSettings + { + UseSSL = false, + StartAdminInterface = true, + FileSystemHandler = filesystemHandlerMock.Object + }); + + var api = RestClient.For(server.Urls[0]); + + // Act + var request = await api.PutFileAsync("filename.txt", "abc-abc").ConfigureAwait(false); + + // Assert + Check.That(request.Guid).IsNull(); + Check.That(request.Status).Contains("File"); + + // Verify + filesystemHandlerMock.Verify(fs => fs.WriteFile(It.Is(p => p == "filename.txt"), It.IsAny()), Times.Once); + filesystemHandlerMock.Verify(fs => fs.FileExists(It.Is(p => p == "filename.txt")), Times.Once); + filesystemHandlerMock.VerifyNoOtherCalls(); + + server.Stop(); + } + + [Fact] + public void IWireMockAdminApi_PutFileAsync_NotFound() + { + // Arrange + var filesystemHandlerMock = new Mock(MockBehavior.Strict); + filesystemHandlerMock.Setup(fs => fs.FileExists(It.IsAny())).Returns(false); + + var server = WireMockServer.Start(new WireMockServerSettings + { + UseSSL = false, + StartAdminInterface = true, + FileSystemHandler = filesystemHandlerMock.Object + }); + + var api = RestClient.For(server.Urls[0]); + + // Act and Assert + Check.ThatAsyncCode(() => api.PutFileAsync("filename.txt", "xxx")).Throws(); + + // Verify + filesystemHandlerMock.Verify(fs => fs.FileExists(It.Is(p => p == "filename.txt")), Times.Once); + filesystemHandlerMock.VerifyNoOtherCalls(); + + server.Stop(); + } + + [Fact] + public void IWireMockAdminApi_GetFileAsync_NotFound() + { + // Arrange + var filesystemHandlerMock = new Mock(MockBehavior.Strict); + filesystemHandlerMock.Setup(fs => fs.FileExists(It.IsAny())).Returns(false); + filesystemHandlerMock.Setup(fs => fs.ReadFile(It.IsAny())).Returns(Encoding.ASCII.GetBytes("Here's a string.")); + + var server = WireMockServer.Start(new WireMockServerSettings + { + UseSSL = false, + StartAdminInterface = true, + FileSystemHandler = filesystemHandlerMock.Object + }); + + var api = RestClient.For(server.Urls[0]); + + // Act and Assert + Check.ThatAsyncCode(() => api.GetFileAsync("filename.txt")).Throws(); + + // Verify + filesystemHandlerMock.Verify(fs => fs.FileExists(It.Is(p => p == "filename.txt")), Times.Once); + filesystemHandlerMock.VerifyNoOtherCalls(); + + server.Stop(); + } + + [Fact] + public async Task IWireMockAdminApi_GetFileAsync_Found() + { + // Arrange + string data = "Here's a string."; + var filesystemHandlerMock = new Mock(MockBehavior.Strict); + filesystemHandlerMock.Setup(fs => fs.FileExists(It.IsAny())).Returns(true); + filesystemHandlerMock.Setup(fs => fs.ReadFile(It.IsAny())).Returns(Encoding.ASCII.GetBytes(data)); + + var server = WireMockServer.Start(new WireMockServerSettings + { + UseSSL = false, + StartAdminInterface = true, + FileSystemHandler = filesystemHandlerMock.Object + }); + + var api = RestClient.For(server.Urls[0]); + + // Act + string file = await api.GetFileAsync("filename.txt").ConfigureAwait(false); + + // Assert + Check.That(file).Equals(data); + + // Verify + filesystemHandlerMock.Verify(fs => fs.FileExists(It.Is(p => p == "filename.txt")), Times.Once); + filesystemHandlerMock.Verify(fs => fs.ReadFile(It.Is(p => p == "filename.txt")), Times.Once); + filesystemHandlerMock.VerifyNoOtherCalls(); + + server.Stop(); + } + + [Fact] + public async Task IWireMockAdminApi_DeleteFileAsync_Ok() + { + // Arrange + var filesystemHandlerMock = new Mock(MockBehavior.Strict); + filesystemHandlerMock.Setup(fs => fs.FileExists(It.IsAny())).Returns(true); + filesystemHandlerMock.Setup(fs => fs.DeleteFile(It.IsAny())); + + var server = WireMockServer.Start(new WireMockServerSettings + { + UseSSL = false, + StartAdminInterface = true, + FileSystemHandler = filesystemHandlerMock.Object + }); + + var api = RestClient.For(server.Urls[0]); + + // Act + await api.DeleteFileAsync("filename.txt").ConfigureAwait(false); + + // Verify + filesystemHandlerMock.Verify(fs => fs.FileExists(It.Is(p => p == "filename.txt")), Times.Once); + filesystemHandlerMock.Verify(fs => fs.DeleteFile(It.Is(p => p == "filename.txt")), Times.Once); + filesystemHandlerMock.VerifyNoOtherCalls(); + + server.Stop(); + } + + [Fact] + public void IWireMockAdminApi_DeleteFileAsync_NotFound() + { + // Arrange + var filesystemHandlerMock = new Mock(MockBehavior.Strict); + filesystemHandlerMock.Setup(fs => fs.FileExists(It.IsAny())).Returns(false); + filesystemHandlerMock.Setup(fs => fs.DeleteFile(It.IsAny())); + + var server = WireMockServer.Start(new WireMockServerSettings + { + UseSSL = false, + StartAdminInterface = true, + FileSystemHandler = filesystemHandlerMock.Object + }); + + var api = RestClient.For(server.Urls[0]); + + // Act and Assert + Check.ThatAsyncCode(() => api.DeleteFileAsync("filename.txt")).Throws(); + + // Verify + filesystemHandlerMock.Verify(fs => fs.FileExists(It.Is(p => p == "filename.txt")), Times.Once); + filesystemHandlerMock.VerifyNoOtherCalls(); + + server.Stop(); + } + + [Fact] + public void IWireMockAdminApi_FileExistsAsync_NotFound() + { + // Arrange + var filesystemHandlerMock = new Mock(MockBehavior.Strict); + filesystemHandlerMock.Setup(fs => fs.FileExists(It.IsAny())).Returns(false); + + var server = WireMockServer.Start(new WireMockServerSettings + { + UseSSL = false, + StartAdminInterface = true, + FileSystemHandler = filesystemHandlerMock.Object + }); + + var api = RestClient.For(server.Urls[0]); + + // Act and Assert + Check.ThatAsyncCode(() => api.FileExistsAsync("filename.txt")).Throws(); + + // Verify + filesystemHandlerMock.Verify(fs => fs.FileExists(It.Is(p => p == "filename.txt")), Times.Once); + filesystemHandlerMock.VerifyNoOtherCalls(); + + server.Stop(); + } + + [Fact] + public async Task IWireMockAdminApi_DeleteScenarioUsingDeleteAsync() + { + // Arrange + var name = "x"; + var server = WireMockServer.StartWithAdminInterface(); + var api = RestClient.For(server.Urls[0]); + + // Act + var status = await api.DeleteScenarioAsync(name).ConfigureAwait(false); + status.Status.Should().Be("No scenario found by name 'x'."); + } + + [Fact] + public async Task IWireMockAdminApi_DeleteScenarioUsingPostAsync() + { + // Arrange + var name = "x"; + var server = WireMockServer.StartWithAdminInterface(); + var api = RestClient.For(server.Urls[0]); + + // Act + var status = await api.ResetScenarioAsync(name).ConfigureAwait(false); + status.Status.Should().Be("No scenario found by name 'x'."); + } + + [Fact] + public async Task IWireMockAdminApi_GetMappingByGuidAsync() + { + // Arrange + var guid = Guid.Parse("90356dba-b36c-469a-a17e-669cd84f1f05"); + var server = WireMockServer.StartWithAdminInterface(); + + server + .Given( + Request.Create() + .WithPath("/foo1") + .WithParam("p1", "xyz") + .UsingGet() + ) + .WithGuid(guid) + .RespondWith( + Response.Create() + .WithStatusCode(200) + .WithBody("1") + ); + + // Act + var api = RestClient.For(server.Url); + var getMappingResult = await api.GetMappingAsync(guid).ConfigureAwait(false); + + // Assert + var mapping = server.Mappings.FirstOrDefault(m => m.Guid == guid); + mapping.Should().NotBeNull(); + + await Verifier.Verify(getMappingResult, VerifySettings).DontScrubGuids(); + + server.Stop(); + } + + [Fact] + public async Task IWireMockAdminApi_GetMappingCodeByGuidAsync() + { + // Arrange + var guid = Guid.Parse("90356dba-b36c-469a-a17e-669cd84f1f05"); + var server = WireMockServer.StartWithAdminInterface(); + + server + .Given( + Request.Create() + .WithPath("/foo1") + .WithParam("p1", "xyz") + .UsingGet() + ) + .WithGuid(guid) + .RespondWith( + Response.Create() + .WithStatusCode(200) + .WithBody("1") + ); + + // Act + var api = RestClient.For(server.Url); + + var mappings = await api.GetMappingsAsync().ConfigureAwait(false); + mappings.Should().HaveCount(1); + + var code = await api.GetMappingCodeAsync(guid).ConfigureAwait(false); + + // Assert + await Verifier.Verify(code).DontScrubDateTimes().DontScrubGuids(); + + server.Stop(); + } + + [Fact] + public async Task IWireMockAdminApi_GetMappingsCode() + { + // Arrange + var guid1 = Guid.Parse("90356dba-b36c-469a-a17e-669cd84f1f05"); + var guid2 = Guid.Parse("1b731398-4a5b-457f-a6e3-d65e541c428f"); + var guid3 = Guid.Parse("f74fd144-df53-404f-8e35-da22a640bd5f"); + var guid4 = Guid.Parse("4126DEC8-470B-4EFF-93BB-C24F83B8B1FD"); + var server = WireMockServer.StartWithAdminInterface(); + + server + .Given( + Request.Create() + .WithPath("/foo1") + .WithParam("p1", "xyz") + .UsingGet() + ) + .WithGuid(guid1) + .RespondWith( + Response.Create() + .WithStatusCode(200) + .WithBody("1") + ); + + server + .Given( + Request.Create() + .WithPath("/foo2") + .WithParam("p2", "abc") + .WithHeader("h1", "W/\"234f2q3r\"") + .UsingPost() + ) + .WithGuid(guid2) + .RespondWith( + Response.Create() + .WithStatusCode("201") + .WithHeader("hk", "hv") + .WithHeader("ETag", "W/\"168d8e\"") + .WithBody("2") + ); + + server + .Given( + Request.Create() + .WithUrl("https://localhost/test") + .UsingDelete() + ) + .WithGuid(guid3) + .RespondWith( + Response.Create() + .WithStatusCode(HttpStatusCode.AlreadyReported) + .WithBodyAsJson(new { @as = 1, b = 1.2, d = true, e = false, f = new[] { 1, 2, 3, 4 }, g = new { z1 = 1, z2 = 2, z3 = new[] { "a", "b", "c" }, z4 = new[] { new { a = 1, b = 2 }, new { a = 2, b = 3 } } }, date_field = new DateTime(2023, 05, 08, 11, 20, 19), string_field_with_date = "2021-03-13T21:04:00Z", multiline_text = @"This +is +multiline +text +" }) + ); + + server + .Given( + Request.Create() + .WithPath("/foo3") + .WithBody(new JsonPartialMatcher(new { a = 1, b = 2 })) + .UsingPost() + ) + .WithGuid(guid4) + .RespondWith( + Response.Create() + .WithStatusCode(200) + .WithBody("Line1\r\nSome \"value\" in Line2") + ); + + // Act + var api = RestClient.For(server.Url); + + var mappings = await api.GetMappingsAsync().ConfigureAwait(false); + mappings.Should().HaveCount(4); + + var code = await api.GetMappingsCodeAsync().ConfigureAwait(false); + + // Assert + await Verifier.Verify(code).DontScrubDateTimes().DontScrubGuids(); + + server.Stop(); + } + + [Fact] + public async Task IWireMockAdminApi_OpenApiConvert_Yml() + { + // Arrange + var openApiDocument = await File.ReadAllTextAsync(Path.Combine("OpenApiParser", "petstore.yml")); + + var server = WireMockServer.StartWithAdminInterface(); + var api = RestClient.For(server.Url); + + // Act + var mappings = await api.OpenApiConvertAsync(openApiDocument).ConfigureAwait(false); + + // Assert + server.MappingModels.Should().BeEmpty(); + mappings.Should().HaveCount(20); + + server.Stop(); + } + + [Fact] + public async Task IWireMockAdminApi_OpenApiConvert_Json() + { + // Arrange + var openApiDocument = await File.ReadAllTextAsync(Path.Combine("OpenApiParser", "petstore-openapi3.json")); + + var server = WireMockServer.StartWithAdminInterface(); + var api = RestClient.For(server.Url); + + // Act + var mappings = await api.OpenApiConvertAsync(openApiDocument).ConfigureAwait(false); + + // Assert + server.MappingModels.Should().BeEmpty(); + mappings.Should().HaveCount(19); + + server.Stop(); + } + + [Fact] + public async Task IWireMockAdminApi_OpenApiSave_Json() + { + // Arrange + var openApiDocument = await File.ReadAllTextAsync(Path.Combine("OpenApiParser", "petstore-openapi3.json")); + + var server = WireMockServer.StartWithAdminInterface(); + var api = RestClient.For(server.Url); + + // Act + var statusModel = await api.OpenApiSaveAsync(openApiDocument).ConfigureAwait(false); + + // Assert + statusModel.Status.Should().Be("OpenApi document converted to Mappings"); + server.MappingModels.Should().HaveCount(19); + + server.Stop(); + } + + [Fact] + public async Task IWireMockAdminApi_OpenApiSave_Yml() + { + // Arrange + var openApiDocument = await File.ReadAllTextAsync(Path.Combine("OpenApiParser", "petstore.yml")); + + var server = WireMockServer.StartWithAdminInterface(); + var api = RestClient.For(server.Url); + + // Act + var mappings = await api.OpenApiConvertAsync(openApiDocument).ConfigureAwait(false); + + // Assert + server.MappingModels.Should().BeEmpty(); + mappings.Should().HaveCount(20); + + server.Stop(); + } +} +#endif diff --git a/test/WireMock.Net.Tests/Grpc/WireMockServerTests.Grpc.cs b/test/WireMock.Net.Tests/Grpc/WireMockServerTests.Grpc.cs new file mode 100644 index 000000000..78d4d1e00 --- /dev/null +++ b/test/WireMock.Net.Tests/Grpc/WireMockServerTests.Grpc.cs @@ -0,0 +1,205 @@ +#if PROTOBUF +using System; +using System.Net; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Threading.Tasks; +using FluentAssertions; +using Greet; +using Grpc.Net.Client; +using WireMock.Matchers; +using WireMock.RequestBuilders; +using WireMock.ResponseBuilders; +using WireMock.Server; +using Xunit; + +// ReSharper disable once CheckNamespace +namespace WireMock.Net.Tests; +public partial class WireMockServerTests +{ + private const string ProtoDefinition = @" +syntax = ""proto3""; + +package greet; + +service Greeter { + rpc SayHello (HelloRequest) returns (HelloReply); +} + +message HelloRequest { + string name = 1; +} + +message HelloReply { + string message = 1; +} +"; + + [Theory] + [InlineData("CgRzdGVm")] + [InlineData("AAAAAAYKBHN0ZWY=")] + public async Task WireMockServer_WithBodyAsProtoBuf(string data) + { + // Arrange + var bytes = Convert.FromBase64String(data); + var jsonMatcher = new JsonMatcher(new { name = "stef" }); + + using var server = WireMockServer.Start(); + + server + .Given(Request.Create() + .UsingPost() + .WithPath("/grpc/greet.Greeter/SayHello") + .WithBodyAsProtoBuf(ProtoDefinition, "greet.HelloRequest", jsonMatcher) + ) + .RespondWith(Response.Create() + .WithBodyAsProtoBuf(ProtoDefinition, "greet.HelloReply", + new + { + message = "hello" + } + ) + .WithTrailingHeader("grpc-status", "0") + ); + + // Act + var protoBuf = new ByteArrayContent(bytes); + protoBuf.Headers.ContentType = new MediaTypeHeaderValue("application/grpc-web"); + + var client = server.CreateClient(); + var response = await client.PostAsync("/grpc/greet.Greeter/SayHello", protoBuf); + + // Assert + response.StatusCode.Should().Be(HttpStatusCode.OK); + var responseBytes = await response.Content.ReadAsByteArrayAsync(); + + Convert.ToBase64String(responseBytes).Should().Be("AAAAAAcKBWhlbGxv"); + + server.Stop(); + } + + [Fact] + public async Task WireMockServer_WithBodyAsProtoBuf_InlineProtoDefinition_UsingGrpcGeneratedClient() + { + // Arrange + using var server = WireMockServer.Start(useHttp2: true); + + var jsonMatcher = new JsonMatcher(new { name = "stef" }); + + server + .Given(Request.Create() + .UsingPost() + .WithPath("/greet.Greeter/SayHello") + .WithBodyAsProtoBuf(ProtoDefinition, "greet.HelloRequest", jsonMatcher) + ) + .RespondWith(Response.Create() + .WithHeader("Content-Type", "application/grpc") + .WithTrailingHeader("grpc-status", "0") + .WithBodyAsProtoBuf(ProtoDefinition, "greet.HelloReply", + new + { + message = "hello stef {{request.method}}" + } + ) + .WithTransformer() + ); + + // Act + var channel = GrpcChannel.ForAddress(server.Url!); + + var client = new Greeter.GreeterClient(channel); + + var reply = await client.SayHelloAsync(new HelloRequest { Name = "stef" }); + + // Assert + reply.Message.Should().Be("hello stef POST"); + + server.Stop(); + } + + [Fact] + public async Task WireMockServer_WithBodyAsProtoBuf_MappingProtoDefinition_UsingGrpcGeneratedClient() + { + // Arrange + using var server = WireMockServer.Start(useHttp2: true); + + var jsonMatcher = new JsonMatcher(new { name = "stef" }); + + server + .Given(Request.Create() + .UsingPost() + .WithHttpVersion("2") + .WithPath("/greet.Greeter/SayHello") + .WithBodyAsProtoBuf("greet.HelloRequest", jsonMatcher) + ) + .WithProtoDefinition(ProtoDefinition) + .RespondWith(Response.Create() + .WithHeader("Content-Type", "application/grpc") + .WithTrailingHeader("grpc-status", "0") + .WithBodyAsProtoBuf("greet.HelloReply", + new + { + message = "hello {{request.BodyAsJson.name}} {{request.method}}" + } + ) + .WithTransformer() + ); + + // Act + var channel = GrpcChannel.ForAddress(server.Url!); + + var client = new Greeter.GreeterClient(channel); + + var reply = await client.SayHelloAsync(new HelloRequest { Name = "stef" }); + + // Assert + reply.Message.Should().Be("hello stef POST"); + + server.Stop(); + } + + [Fact] + public async Task WireMockServer_WithBodyAsProtoBuf_ServerProtoDefinition_UsingGrpcGeneratedClient() + { + // Arrange + var id = $"test-{Guid.NewGuid()}"; + + using var server = WireMockServer.Start(useHttp2: true); + + var jsonMatcher = new JsonMatcher(new { name = "stef" }); + + server + .AddProtoDefinition(id, ProtoDefinition) + .Given(Request.Create() + .UsingPost() + .WithHttpVersion("2") + .WithPath("/greet.Greeter/SayHello") + .WithBodyAsProtoBuf("greet.HelloRequest", jsonMatcher) + ) + .WithProtoDefinition(id) + .RespondWith(Response.Create() + .WithHeader("Content-Type", "application/grpc") + .WithTrailingHeader("grpc-status", "0") + .WithBodyAsProtoBuf("greet.HelloReply", + new + { + message = "hello {{request.BodyAsJson.name}} {{request.method}}" + } + ) + .WithTransformer() + ); + + // Act + var channel = GrpcChannel.ForAddress(server.Url!); + + var client = new Greeter.GreeterClient(channel); + + var reply = await client.SayHelloAsync(new HelloRequest { Name = "stef" }); + + // Assert + reply.Message.Should().Be("hello stef POST"); + + server.Stop(); + } +} +#endif \ No newline at end of file diff --git a/test/WireMock.Net.Tests/Grpc/greet.proto b/test/WireMock.Net.Tests/Grpc/greet.proto new file mode 100644 index 000000000..6f9e10fa5 --- /dev/null +++ b/test/WireMock.Net.Tests/Grpc/greet.proto @@ -0,0 +1,33 @@ +// Copyright 2019 The gRPC Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto3"; + +package greet; + +// The greeting service definition. +service Greeter { + // Sends a greeting + rpc SayHello (HelloRequest) returns (HelloReply); +} + +// The request message containing the user's name. +message HelloRequest { + string name = 1; +} + +// The response message containing the greetings +message HelloReply { + string message = 1; +} \ No newline at end of file diff --git a/test/WireMock.Net.Tests/Matchers/GraphQLMatcherTests.cs b/test/WireMock.Net.Tests/Matchers/GraphQLMatcherTests.cs index 724c47ac1..f907c4b1e 100644 --- a/test/WireMock.Net.Tests/Matchers/GraphQLMatcherTests.cs +++ b/test/WireMock.Net.Tests/Matchers/GraphQLMatcherTests.cs @@ -1,7 +1,6 @@ #if GRAPHQL using System; using System.Collections.Generic; -using CSScripting; using FluentAssertions; using GraphQLParser.Exceptions; using WireMock.Exceptions; diff --git a/test/WireMock.Net.Tests/Matchers/ProtoBufMatcherTests.cs b/test/WireMock.Net.Tests/Matchers/ProtoBufMatcherTests.cs new file mode 100644 index 000000000..b6f809d22 --- /dev/null +++ b/test/WireMock.Net.Tests/Matchers/ProtoBufMatcherTests.cs @@ -0,0 +1,108 @@ +#if PROTOBUF +using System; +using System.Threading.Tasks; +using FluentAssertions; +using ProtoBuf; +using WireMock.Matchers; +using WireMock.Models; +using Xunit; + +namespace WireMock.Net.Tests.Matchers; + +public class ProtoBufMatcherTests +{ + private const string MessageType = "greet.HelloRequest"; + private readonly IdOrText _protoDefinition = new(null, @" +syntax = ""proto3""; + +package greet; + +service Greeter { + rpc SayHello (HelloRequest) returns (HelloReply); +} + +message HelloRequest { + string name = 1; +} + +message HelloReply { + string message = 1; +} +"); + + [Fact] + public async Task ProtoBufMatcher_For_ValidProtoBuf_And_ValidMethod_DecodeAsync() + { + // Arrange + var bytes = Convert.FromBase64String("CgRzdGVm"); + + // Act + var matcher = new ProtoBufMatcher(() => _protoDefinition, MessageType); + var result = await matcher.DecodeAsync(bytes).ConfigureAwait(false); + + // Assert + result.Should().BeEquivalentTo(new { name = "stef" }); + } + + [Fact] + public async Task ProtoBufMatcher_For_ValidProtoBuf_And_ValidMethod_NoJsonMatchers_IsMatchAsync() + { + // Arrange + var bytes = Convert.FromBase64String("CgRzdGVm"); + + // Act + var matcher = new ProtoBufMatcher(() => _protoDefinition, MessageType); + var result = await matcher.IsMatchAsync(bytes).ConfigureAwait(false); + + // Assert + result.Score.Should().Be(MatchScores.Perfect); + result.Exception.Should().BeNull(); + } + + [Fact] + public async Task ProtoBufMatcher_For_ValidProtoBuf_And_ValidMethod_Using_JsonMatcher_IsMatchAsync() + { + // Arrange + var jsonMatcher = new JsonMatcher(new { name = "stef" }); + var bytes = Convert.FromBase64String("CgRzdGVm"); + + // Act + var matcher = new ProtoBufMatcher(() => _protoDefinition, MessageType, matcher: jsonMatcher); + var result = await matcher.IsMatchAsync(bytes); + + // Assert + result.Score.Should().Be(MatchScores.Perfect); + result.Exception.Should().BeNull(); + } + + [Fact] + public async Task ProtoBufMatcher_For_InvalidProtoBuf_IsNoMatch() + { + // Arrange + var bytes = new byte[] { 1, 2, 3 }; + + // Act + var matcher = new ProtoBufMatcher(() => _protoDefinition, MessageType); + var result = await matcher.IsMatchAsync(bytes); + + // Assert + result.Score.Should().Be(MatchScores.Mismatch); + result.Exception.Should().BeOfType(); + } + + [Fact] + public async Task ProtoBufMatcher_For_InvalidMethod_IsNoMatchAsync() + { + // Arrange + var bytes = Convert.FromBase64String("CgRzdGVm"); + + // Act + var matcher = new ProtoBufMatcher(() => _protoDefinition, "greet.Greeter.X"); + var result = await matcher.IsMatchAsync(bytes); + + // Assert + result.Score.Should().Be(MatchScores.Mismatch); + result.Exception.Should().BeOfType(); + } +} +#endif \ No newline at end of file diff --git a/test/WireMock.Net.Tests/Owin/GlobalExceptionMiddlewareTests.cs b/test/WireMock.Net.Tests/Owin/GlobalExceptionMiddlewareTests.cs index 5b1926c95..a85bafa61 100644 --- a/test/WireMock.Net.Tests/Owin/GlobalExceptionMiddlewareTests.cs +++ b/test/WireMock.Net.Tests/Owin/GlobalExceptionMiddlewareTests.cs @@ -28,7 +28,7 @@ public GlobalExceptionMiddlewareTests() _responseMapperMock = new Mock(); _responseMapperMock.SetupAllProperties(); - _responseMapperMock.Setup(m => m.MapAsync(It.IsAny(), It.IsAny())).Returns(Task.FromResult(true)); + _responseMapperMock.Setup(m => m.MapAsync(It.IsAny(), It.IsAny())).Returns(Task.FromResult(true)); _sut = new GlobalExceptionMiddleware(null, _optionsMock.Object, _responseMapperMock.Object); } diff --git a/test/WireMock.Net.Tests/Owin/Mappers/OwinResponseMapperTests.cs b/test/WireMock.Net.Tests/Owin/Mappers/OwinResponseMapperTests.cs index 65e4b9a70..cc60126ed 100644 --- a/test/WireMock.Net.Tests/Owin/Mappers/OwinResponseMapperTests.cs +++ b/test/WireMock.Net.Tests/Owin/Mappers/OwinResponseMapperTests.cs @@ -202,7 +202,7 @@ public async Task OwinResponseMapper_MapAsync_BodyAsBytes() public async Task OwinResponseMapper_MapAsync_BodyAsJson() { // Arrange - var json = new { t = "x", i = (string)null }; + var json = new { t = "x", i = (string?)null }; var responseMessage = new ResponseMessage { Headers = new Dictionary>(), diff --git a/test/WireMock.Net.Tests/Owin/WireMockMiddlewareTests.cs b/test/WireMock.Net.Tests/Owin/WireMockMiddlewareTests.cs index b7e85b890..cfd2b222e 100644 --- a/test/WireMock.Net.Tests/Owin/WireMockMiddlewareTests.cs +++ b/test/WireMock.Net.Tests/Owin/WireMockMiddlewareTests.cs @@ -70,7 +70,7 @@ public WireMockMiddlewareTests() _responseMapperMock = new Mock(); _responseMapperMock.SetupAllProperties(); - _responseMapperMock.Setup(m => m.MapAsync(It.IsAny(), It.IsAny())).Returns(Task.FromResult(true)); + _responseMapperMock.Setup(m => m.MapAsync(It.IsAny(), It.IsAny())).Returns(Task.FromResult(true)); _matcherMock = new Mock(); _matcherMock.SetupAllProperties(); @@ -216,7 +216,7 @@ public async Task WireMockMiddleware_Invoke_Mapping_Has_ProxyAndRecordSettings_A _mappingMock.SetupGet(m => m.Provider).Returns(responseBuilder); _mappingMock.SetupGet(m => m.Settings).Returns(settings); - var newMappingFromProxy = new Mapping(NewGuid, UpdatedAt, string.Empty, string.Empty, null, settings, Request.Create(), Response.Create(), 0, null, null, null, null, null, false, null, null, null); + var newMappingFromProxy = new Mapping(NewGuid, UpdatedAt, string.Empty, string.Empty, null, settings, Request.Create(), Response.Create(), 0, null, null, null, null, null, false, null, null); _mappingMock.Setup(m => m.ProvideResponseAsync(It.IsAny())).ReturnsAsync((new ResponseMessage(), newMappingFromProxy)); var requestBuilder = Request.Create().UsingAnyMethod(); @@ -270,7 +270,7 @@ public async Task WireMockMiddleware_Invoke_Mapping_Has_ProxyAndRecordSettings_A _mappingMock.SetupGet(m => m.Provider).Returns(responseBuilder); _mappingMock.SetupGet(m => m.Settings).Returns(settings); - var newMappingFromProxy = new Mapping(NewGuid, UpdatedAt, "my-title", "my-description", null, settings, Request.Create(), Response.Create(), 0, null, null, null, null, null, false, null, data: null, probability: null); + var newMappingFromProxy = new Mapping(NewGuid, UpdatedAt, "my-title", "my-description", null, settings, Request.Create(), Response.Create(), 0, null, null, null, null, null, false, null, data: null); _mappingMock.Setup(m => m.ProvideResponseAsync(It.IsAny())).ReturnsAsync((new ResponseMessage(), newMappingFromProxy)); var requestBuilder = Request.Create().UsingAnyMethod(); diff --git a/test/WireMock.Net.Tests/RequestBuilders/RequestBuilderWithProtoBufTests.cs b/test/WireMock.Net.Tests/RequestBuilders/RequestBuilderWithProtoBufTests.cs new file mode 100644 index 000000000..73b76d6f6 --- /dev/null +++ b/test/WireMock.Net.Tests/RequestBuilders/RequestBuilderWithProtoBufTests.cs @@ -0,0 +1,65 @@ +#if PROTOBUF +using System.Collections.Generic; +using FluentAssertions; +using WireMock.Matchers; +using WireMock.Matchers.Request; +using WireMock.RequestBuilders; +using Xunit; + +namespace WireMock.Net.Tests.RequestBuilders; + +public class RequestBuilderWithProtoBufTests +{ + private const string MessageType = "greet.HelloRequest"; + private const string TestProtoDefinition = @" +syntax = ""proto3""; + +package greet; + +service Greeter { + rpc SayHello (HelloRequest) returns (HelloReply); +} + +message HelloRequest { + string name = 1; +} + +message HelloReply { + string message = 1; +} +"; + + [Fact] + public void RequestBuilder_WithGrpcProto_Without_JsonMatcher() + { + // Act + var requestBuilder = (Request)Request.Create().WithBodyAsProtoBuf(TestProtoDefinition, MessageType); + + // Assert + var matchers = requestBuilder.GetPrivateFieldValue>("_requestMatchers"); + matchers.Should().HaveCount(1); + + var protoBufMatcher = (ProtoBufMatcher)((RequestMessageProtoBufMatcher)matchers[0]).Matcher!; + protoBufMatcher.ProtoDefinition().Text.Should().Be(TestProtoDefinition); + protoBufMatcher.MessageType.Should().Be(MessageType); + protoBufMatcher.Matcher.Should().BeNull(); + } + + [Fact] + public void RequestBuilder_WithGrpcProto_With_JsonMatcher() + { + // Act + var jsonMatcher = new JsonMatcher(new { name = "stef" }); + var requestBuilder = (Request)Request.Create().WithBodyAsProtoBuf(TestProtoDefinition, MessageType, jsonMatcher); + + // Assert + var matchers = requestBuilder.GetPrivateFieldValue>("_requestMatchers"); + matchers.Should().HaveCount(1); + + var protoBufMatcher = (ProtoBufMatcher)((RequestMessageProtoBufMatcher)matchers[0]).Matcher!; + protoBufMatcher.ProtoDefinition().Text.Should().Be(TestProtoDefinition); + protoBufMatcher.MessageType.Should().Be(MessageType); + protoBufMatcher.Matcher.Should().BeOfType(); + } +} +#endif \ No newline at end of file diff --git a/test/WireMock.Net.Tests/Serialization/LogEntryMapperTests.LogEntryMapper_Map_LogEntry_Check_BodyTypeBytes.verified.txt b/test/WireMock.Net.Tests/Serialization/LogEntryMapperTests.LogEntryMapper_Map_LogEntry_Check_BodyTypeBytes.verified.txt index f6cd9dbc2..9b343ee61 100644 --- a/test/WireMock.Net.Tests/Serialization/LogEntryMapperTests.LogEntryMapper_Map_LogEntry_Check_BodyTypeBytes.verified.txt +++ b/test/WireMock.Net.Tests/Serialization/LogEntryMapperTests.LogEntryMapper_Map_LogEntry_Check_BodyTypeBytes.verified.txt @@ -6,6 +6,7 @@ Url: http://localhost/, AbsoluteUrl: http://localhost/, Method: post, + HttpVersion: 1.1, BodyAsBytes: AA==, DetectedBodyType: Bytes }, diff --git a/test/WireMock.Net.Tests/Serialization/LogEntryMapperTests.LogEntryMapper_Map_LogEntry_Check_ResponseBodyTypeFile.verified.txt b/test/WireMock.Net.Tests/Serialization/LogEntryMapperTests.LogEntryMapper_Map_LogEntry_Check_ResponseBodyTypeFile.verified.txt index 188dffb8d..07692ab9b 100644 --- a/test/WireMock.Net.Tests/Serialization/LogEntryMapperTests.LogEntryMapper_Map_LogEntry_Check_ResponseBodyTypeFile.verified.txt +++ b/test/WireMock.Net.Tests/Serialization/LogEntryMapperTests.LogEntryMapper_Map_LogEntry_Check_ResponseBodyTypeFile.verified.txt @@ -5,7 +5,8 @@ AbsolutePath: /, Url: http://localhost/, AbsoluteUrl: http://localhost/, - Method: get + Method: get, + HttpVersion: 1.1 }, Response: { BodyAsFile: test, diff --git a/test/WireMock.Net.Tests/Serialization/LogEntryMapperTests.LogEntryMapper_Map_LogEntry_WhenFuncIsUsed_And_DoNotSaveDynamicResponseInLogEntry_Is_True_Should_NotSave_StringResponse.verified.txt b/test/WireMock.Net.Tests/Serialization/LogEntryMapperTests.LogEntryMapper_Map_LogEntry_WhenFuncIsUsed_And_DoNotSaveDynamicResponseInLogEntry_Is_True_Should_NotSave_StringResponse.verified.txt index 445dde151..02b23bcea 100644 --- a/test/WireMock.Net.Tests/Serialization/LogEntryMapperTests.LogEntryMapper_Map_LogEntry_WhenFuncIsUsed_And_DoNotSaveDynamicResponseInLogEntry_Is_True_Should_NotSave_StringResponse.verified.txt +++ b/test/WireMock.Net.Tests/Serialization/LogEntryMapperTests.LogEntryMapper_Map_LogEntry_WhenFuncIsUsed_And_DoNotSaveDynamicResponseInLogEntry_Is_True_Should_NotSave_StringResponse.verified.txt @@ -6,6 +6,7 @@ Url: http://localhost/, AbsoluteUrl: http://localhost/, Method: post, + HttpVersion: 1.1, BodyAsBytes: AA==, DetectedBodyType: Bytes }, diff --git a/test/WireMock.Net.Tests/Serialization/LogEntryMapperTests.LogEntryMapper_Map_LogEntry_WithFault.verified.txt b/test/WireMock.Net.Tests/Serialization/LogEntryMapperTests.LogEntryMapper_Map_LogEntry_WithFault.verified.txt index e658eed8b..125632765 100644 --- a/test/WireMock.Net.Tests/Serialization/LogEntryMapperTests.LogEntryMapper_Map_LogEntry_WithFault.verified.txt +++ b/test/WireMock.Net.Tests/Serialization/LogEntryMapperTests.LogEntryMapper_Map_LogEntry_WithFault.verified.txt @@ -5,7 +5,8 @@ AbsolutePath: /, Url: http://localhost/, AbsoluteUrl: http://localhost/, - Method: get + Method: get, + HttpVersion: 1.1 }, Response: { BodyAsFile: test, diff --git a/test/WireMock.Net.Tests/Serialization/MappingConverterTests.ToCSharpCode.cs b/test/WireMock.Net.Tests/Serialization/MappingConverterTests.ToCSharpCode.cs index c34a32d91..324393e35 100644 --- a/test/WireMock.Net.Tests/Serialization/MappingConverterTests.ToCSharpCode.cs +++ b/test/WireMock.Net.Tests/Serialization/MappingConverterTests.ToCSharpCode.cs @@ -102,7 +102,7 @@ public Task ToCSharpCode_With_Server_And_AddStartIsFalse() return Verifier.Verify(code, VerifySettings); } - private Mapping CreateMapping() + private IMapping CreateMapping() { var guid = new Guid("8e7b9ab7-e18e-4502-8bc9-11e6679811cc"); var request = Request.Create() @@ -119,7 +119,8 @@ private Mapping CreateMapping() .WithDelay(12345) .WithTransformer(); - return new Mapping( + return new Mapping + ( guid, _updatedAt, string.Empty, @@ -136,9 +137,8 @@ private Mapping CreateMapping() null, false, null, - data: null, - probability: 0.3 - ); + data: null + ).WithProbability(0.3); } } #endif \ No newline at end of file diff --git a/test/WireMock.Net.Tests/Serialization/MappingConverterTests.ToMappingModel_Mapping_WithBodyAsProtoBuf_ReturnsCorrectModel.verified.txt b/test/WireMock.Net.Tests/Serialization/MappingConverterTests.ToMappingModel_Mapping_WithBodyAsProtoBuf_ReturnsCorrectModel.verified.txt new file mode 100644 index 000000000..ef641b264 --- /dev/null +++ b/test/WireMock.Net.Tests/Serialization/MappingConverterTests.ToMappingModel_Mapping_WithBodyAsProtoBuf_ReturnsCorrectModel.verified.txt @@ -0,0 +1,61 @@ +{ + Guid: Guid_1, + UpdatedAt: DateTime_1, + Title: , + Description: , + Priority: 41, + Request: { + Path: { + Matchers: [ + { + Name: WildcardMatcher, + Pattern: /grpc/greet.Greeter/SayHello, + IgnoreCase: false + } + ] + }, + Methods: [ + POST + ], + Body: { + Matcher: { + Name: ProtoBufMatcher, + ContentMatcher: { + Name: JsonMatcher, + Pattern: { + name: stef + }, + IgnoreCase: false + }, + ProtoBufMessageType: greet.HelloRequest + } + } + }, + Response: { + BodyAsJson: { + message: hello + }, + TrailingHeaders: { + grpc-status: 0 + }, + ProtoBufMessageType: greet.HelloReply + }, + UseWebhooksFireAndForget: false, + ProtoDefinition: +syntax = "proto3"; + +package greet; + +service Greeter { + rpc SayHello (HelloRequest) returns (HelloReply); +} + +message HelloRequest { + string name = 1; +} + +message HelloReply { + string message = 1; +} + +} \ No newline at end of file diff --git a/test/WireMock.Net.Tests/Serialization/MappingConverterTests.ToMappingModel_Request_WithBodyAsProtoBuf_ReturnsCorrectModel.verified.txt b/test/WireMock.Net.Tests/Serialization/MappingConverterTests.ToMappingModel_Request_WithBodyAsProtoBuf_ReturnsCorrectModel.verified.txt new file mode 100644 index 000000000..26ff26482 --- /dev/null +++ b/test/WireMock.Net.Tests/Serialization/MappingConverterTests.ToMappingModel_Request_WithBodyAsProtoBuf_ReturnsCorrectModel.verified.txt @@ -0,0 +1,53 @@ +{ + Guid: Guid_1, + UpdatedAt: DateTime_1, + Title: , + Description: , + Priority: 41, + Request: { + Path: { + Matchers: [ + { + Name: WildcardMatcher, + Pattern: /grpc/greet.Greeter/SayHello, + IgnoreCase: false + } + ] + }, + Methods: [ + POST + ], + Body: { + Matcher: { + Name: ProtoBufMatcher, + Pattern: +syntax = "proto3"; + +package greet; + +service Greeter { + rpc SayHello (HelloRequest) returns (HelloReply); +} + +message HelloRequest { + string name = 1; +} + +message HelloReply { + string message = 1; +} +, + ContentMatcher: { + Name: JsonMatcher, + Pattern: { + name: stef + }, + IgnoreCase: false + }, + ProtoBufMessageType: greet.HelloRequest + } + } + }, + Response: {}, + UseWebhooksFireAndForget: false +} \ No newline at end of file diff --git a/test/WireMock.Net.Tests/Serialization/MappingConverterTests.ToMappingModel_WithHeader_And_Cookie_ReturnsCorrectModel.verified.txt b/test/WireMock.Net.Tests/Serialization/MappingConverterTests.ToMappingModel_Request_WithHeader_And_Cookie_ReturnsCorrectModel.verified.txt similarity index 100% rename from test/WireMock.Net.Tests/Serialization/MappingConverterTests.ToMappingModel_WithHeader_And_Cookie_ReturnsCorrectModel.verified.txt rename to test/WireMock.Net.Tests/Serialization/MappingConverterTests.ToMappingModel_Request_WithHeader_And_Cookie_ReturnsCorrectModel.verified.txt diff --git a/test/WireMock.Net.Tests/Serialization/MappingConverterTests.ToMappingModel_Response_WithBodyAsProtoBuf_ReturnsCorrectModel.verified.txt b/test/WireMock.Net.Tests/Serialization/MappingConverterTests.ToMappingModel_Response_WithBodyAsProtoBuf_ReturnsCorrectModel.verified.txt new file mode 100644 index 000000000..a7f7c4814 --- /dev/null +++ b/test/WireMock.Net.Tests/Serialization/MappingConverterTests.ToMappingModel_Response_WithBodyAsProtoBuf_ReturnsCorrectModel.verified.txt @@ -0,0 +1,35 @@ +{ + Guid: Guid_1, + UpdatedAt: DateTime_1, + Title: , + Description: , + Priority: 43, + Request: {}, + Response: { + BodyAsJson: { + message: hello + }, + TrailingHeaders: { + grpc-status: 0 + }, + ProtoDefinition: +syntax = "proto3"; + +package greet; + +service Greeter { + rpc SayHello (HelloRequest) returns (HelloReply); +} + +message HelloRequest { + string name = 1; +} + +message HelloReply { + string message = 1; +} +, + ProtoBufMessageType: greet.HelloReply + }, + UseWebhooksFireAndForget: false +} \ No newline at end of file diff --git a/test/WireMock.Net.Tests/Serialization/MappingConverterTests.ToMappingModel_Response_WithHeader_ReturnsCorrectModel.verified.txt b/test/WireMock.Net.Tests/Serialization/MappingConverterTests.ToMappingModel_Response_WithHeader_ReturnsCorrectModel.verified.txt new file mode 100644 index 000000000..99171ab9d --- /dev/null +++ b/test/WireMock.Net.Tests/Serialization/MappingConverterTests.ToMappingModel_Response_WithHeader_ReturnsCorrectModel.verified.txt @@ -0,0 +1,14 @@ +{ + Guid: Guid_1, + UpdatedAt: DateTime_1, + Request: {}, + Response: { + Headers: { + w[]: [ + x, + y + ] + } + }, + UseWebhooksFireAndForget: false +} \ No newline at end of file diff --git a/test/WireMock.Net.Tests/Serialization/MappingConverterTests.ToMappingModel_Response_WithHeaders_ReturnsCorrectModel.verified.txt b/test/WireMock.Net.Tests/Serialization/MappingConverterTests.ToMappingModel_Response_WithHeaders_ReturnsCorrectModel.verified.txt new file mode 100644 index 000000000..99171ab9d --- /dev/null +++ b/test/WireMock.Net.Tests/Serialization/MappingConverterTests.ToMappingModel_Response_WithHeaders_ReturnsCorrectModel.verified.txt @@ -0,0 +1,14 @@ +{ + Guid: Guid_1, + UpdatedAt: DateTime_1, + Request: {}, + Response: { + Headers: { + w[]: [ + x, + y + ] + } + }, + UseWebhooksFireAndForget: false +} \ No newline at end of file diff --git a/test/WireMock.Net.Tests/Serialization/MappingConverterTests.ToMappingModel_Response_WithTrailingHeader_ReturnsCorrectModel.verified.txt b/test/WireMock.Net.Tests/Serialization/MappingConverterTests.ToMappingModel_Response_WithTrailingHeader_ReturnsCorrectModel.verified.txt new file mode 100644 index 000000000..5e6c02b1e --- /dev/null +++ b/test/WireMock.Net.Tests/Serialization/MappingConverterTests.ToMappingModel_Response_WithTrailingHeader_ReturnsCorrectModel.verified.txt @@ -0,0 +1,15 @@ +{ + Guid: Guid_1, + UpdatedAt: DateTime_1, + Request: {}, + Response: { + TrailingHeaders: { + x1: y, + x2: [ + y, + z + ] + } + }, + UseWebhooksFireAndForget: false +} \ No newline at end of file diff --git a/test/WireMock.Net.Tests/Serialization/MappingConverterTests.ToMappingModel_Response_WithTrailingHeaders_ReturnsCorrectModel.verified.txt b/test/WireMock.Net.Tests/Serialization/MappingConverterTests.ToMappingModel_Response_WithTrailingHeaders_ReturnsCorrectModel.verified.txt new file mode 100644 index 000000000..02cafd9eb --- /dev/null +++ b/test/WireMock.Net.Tests/Serialization/MappingConverterTests.ToMappingModel_Response_WithTrailingHeaders_ReturnsCorrectModel.verified.txt @@ -0,0 +1,14 @@ +{ + Guid: Guid_1, + UpdatedAt: DateTime_1, + Request: {}, + Response: { + TrailingHeaders: { + w[]: [ + x, + y + ] + } + }, + UseWebhooksFireAndForget: false +} \ No newline at end of file diff --git a/test/WireMock.Net.Tests/Serialization/MappingConverterTests.cs b/test/WireMock.Net.Tests/Serialization/MappingConverterTests.cs index de85ab34a..a3b39efff 100644 --- a/test/WireMock.Net.Tests/Serialization/MappingConverterTests.cs +++ b/test/WireMock.Net.Tests/Serialization/MappingConverterTests.cs @@ -22,6 +22,23 @@ public partial class MappingConverterTests private readonly Guid _guid = new("c8eeaf99-d5c4-4341-8543-4597c3fd40d9"); private readonly DateTime _updatedAt = new(2022, 12, 4, 11, 12, 13); private readonly WireMockServerSettings _settings = new(); + private const string ProtoDefinition = @" +syntax = ""proto3""; + +package greet; + +service Greeter { + rpc SayHello (HelloRequest) returns (HelloReply); +} + +message HelloRequest { + string name = 1; +} + +message HelloReply { + string message = 1; +} +"; private readonly MappingConverter _sut; @@ -58,7 +75,7 @@ public Task ToMappingModel_With_SingleWebHook() } } }; - var mapping = new Mapping(_guid, _updatedAt, string.Empty, string.Empty, null, _settings, request, response, 0, null, null, null, null, webhooks, false, null, data: null, probability: null); + var mapping = new Mapping(_guid, _updatedAt, string.Empty, string.Empty, null, _settings, request, response, 0, null, null, null, null, webhooks, false, null, data: null); // Act var model = _sut.ToMappingModel(mapping); @@ -131,7 +148,7 @@ public Task ToMappingModel_With_MultipleWebHooks() } } }; - var mapping = new Mapping(_guid, _updatedAt, string.Empty, string.Empty, null, _settings, request, response, 0, null, null, null, null, webhooks, true, null, data: null, probability: null); + var mapping = new Mapping(_guid, _updatedAt, string.Empty, string.Empty, null, _settings, request, response, 0, null, null, null, null, webhooks, true, null, data: null); // Act var model = _sut.ToMappingModel(mapping); @@ -169,7 +186,7 @@ public Task ToMappingModel_WithTitle_And_Description_ReturnsCorrectModel() var description = "my-description"; var request = Request.Create(); var response = Response.Create(); - var mapping = new Mapping(_guid, _updatedAt, title, description, null, _settings, request, response, 0, null, null, null, null, null, false, null, data: null, probability: null); + var mapping = new Mapping(_guid, _updatedAt, title, description, null, _settings, request, response, 0, null, null, null, null, null, false, null, data: null); // Act var model = _sut.ToMappingModel(mapping); @@ -189,7 +206,7 @@ public Task ToMappingModel_WithPriority_ReturnsPriority() // Assign var request = Request.Create(); var response = Response.Create().WithBodyAsJson(new { x = "x" }).WithTransformer(); - var mapping = new Mapping(_guid, _updatedAt, string.Empty, string.Empty, null, _settings, request, response, 42, null, null, null, null, null, false, null, data: null, probability: null); + var mapping = new Mapping(_guid, _updatedAt, string.Empty, string.Empty, null, _settings, request, response, 42, null, null, null, null, null, false, null, data: null); // Act var model = _sut.ToMappingModel(mapping); @@ -218,7 +235,7 @@ public Task ToMappingModel_WithTimeSettings_ReturnsCorrectTimeSettings() End = end, TTL = ttl }; - var mapping = new Mapping(_guid, _updatedAt, string.Empty, string.Empty, null, _settings, request, response, 42, null, null, null, null, null, false, timeSettings, data: null, probability: null); + var mapping = new Mapping(_guid, _updatedAt, string.Empty, string.Empty, null, _settings, request, response, 42, null, null, null, null, null, false, timeSettings, data: null); // Act var model = _sut.ToMappingModel(mapping); @@ -249,7 +266,7 @@ public void ToMappingModel_WithDelayAsTimeSpan_ReturnsCorrectModel() { var request = Request.Create(); var response = Response.Create().WithDelay(test.Delay); - var mapping = new Mapping(Guid.NewGuid(), _updatedAt, string.Empty, string.Empty, string.Empty, _settings, request, response, 42, null, null, null, null, null, false, null, data: null, probability: null); + var mapping = new Mapping(Guid.NewGuid(), _updatedAt, string.Empty, string.Empty, string.Empty, _settings, request, response, 42, null, null, null, null, null, false, null, data: null); // Act var model = _sut.ToMappingModel(mapping); @@ -267,7 +284,7 @@ public Task ToMappingModel_WithDelay_ReturnsCorrectModel() var delay = 1000; var request = Request.Create(); var response = Response.Create().WithDelay(delay); - var mapping = new Mapping(_guid, _updatedAt, string.Empty, string.Empty, null, _settings, request, response, 42, null, null, null, null, null, false, null, data: null, probability: null); + var mapping = new Mapping(_guid, _updatedAt, string.Empty, string.Empty, null, _settings, request, response, 42, null, null, null, null, null, false, null, data: null); // Act var model = _sut.ToMappingModel(mapping); @@ -287,7 +304,7 @@ public Task ToMappingModel_WithRandomMinimumDelay_ReturnsCorrectModel() int minimumDelay = 1000; var request = Request.Create(); var response = Response.Create().WithRandomDelay(minimumDelay); - var mapping = new Mapping(_guid, _updatedAt, string.Empty, string.Empty, null, _settings, request, response, 42, null, null, null, null, null, false, null, data: null, probability: null); + var mapping = new Mapping(_guid, _updatedAt, string.Empty, string.Empty, null, _settings, request, response, 42, null, null, null, null, null, false, null, data: null); // Act var model = _sut.ToMappingModel(mapping); @@ -310,7 +327,7 @@ public Task ToMappingModel_WithRandomDelay_ReturnsCorrectModel() int maximumDelay = 2000; var request = Request.Create(); var response = Response.Create().WithRandomDelay(minimumDelay, maximumDelay); - var mapping = new Mapping(_guid, _updatedAt, string.Empty, string.Empty, null, _settings, request, response, 42, null, null, null, null, null, false, null, data: null, probability: null); + var mapping = new Mapping(_guid, _updatedAt, string.Empty, string.Empty, null, _settings, request, response, 42, null, null, null, null, null, false, null, data: null); // Act var model = _sut.ToMappingModel(mapping); @@ -332,7 +349,8 @@ public Task ToMappingModel_WithProbability_ReturnsCorrectModel() double probability = 0.4; var request = Request.Create(); var response = Response.Create(); - var mapping = new Mapping(_guid, _updatedAt, string.Empty, string.Empty, null, _settings, request, response, 42, null, null, null, null, null, false, null, data: null, probability: probability); + var mapping = new Mapping(_guid, _updatedAt, string.Empty, string.Empty, null, _settings, request, response, 42, null, null, null, null, null, false, null, data: null) + .WithProbability(probability); // Act var model = _sut.ToMappingModel(mapping); @@ -351,7 +369,7 @@ public Task ToMappingModel_Request_WithClientIP_ReturnsCorrectModel() // Arrange var request = Request.Create().WithClientIP("1.2.3.4"); var response = Response.Create(); - var mapping = new Mapping(_guid, _updatedAt, string.Empty, string.Empty, null, _settings, request, response, 42, null, null, null, null, null, false, null, null, null); + var mapping = new Mapping(_guid, _updatedAt, string.Empty, string.Empty, null, _settings, request, response, 42, null, null, null, null, null, false, null, null); // Act var model = _sut.ToMappingModel(mapping); @@ -364,7 +382,7 @@ public Task ToMappingModel_Request_WithClientIP_ReturnsCorrectModel() } [Fact] - public Task ToMappingModel_WithHeader_And_Cookie_ReturnsCorrectModel() + public Task ToMappingModel_Request_WithHeader_And_Cookie_ReturnsCorrectModel() { // Assign var request = Request.Create() @@ -380,8 +398,57 @@ public Task ToMappingModel_WithHeader_And_Cookie_ReturnsCorrectModel() .WithCookie("IgnoreCase_true", "cv-4") .WithCookie("ExactMatcher", new ExactMatcher("c-exact")) ; + var response = Response.Create(); - var mapping = new Mapping(_guid, _updatedAt, null, null, null, _settings, request, response, 0, null, null, null, null, null, false, null, data: null, probability: null); + + var mapping = new Mapping(_guid, _updatedAt, null, null, null, _settings, request, response, 0, null, null, null, null, null, false, null, data: null); + + // Act + var model = _sut.ToMappingModel(mapping); + + // Assert + model.Should().NotBeNull(); + + // Verify + return Verifier.Verify(model); + } + + [Fact] + public Task ToMappingModel_Response_WithHeader_ReturnsCorrectModel() + { + // Assign + var request = Request.Create(); + + var response = Response.Create() + .WithHeader("x1", "y") + .WithHeader("x2", "y", "z") + .WithHeaders(new Dictionary { { "d", "test" } }) + .WithHeaders(new Dictionary { { "d[]", new[] { "v1", "v2" } } }) + .WithHeaders(new Dictionary> { { "w", new WireMockList("x") } }) + .WithHeaders(new Dictionary> { { "w[]", new WireMockList("x", "y") } }); + + var mapping = new Mapping(_guid, _updatedAt, null, null, null, _settings, request, response, 0, null, null, null, null, null, false, null, data: null); + + // Act + var model = _sut.ToMappingModel(mapping); + + // Assert + model.Should().NotBeNull(); + + // Verify + return Verifier.Verify(model); + } + + [Fact] + public Task ToMappingModel_Response_WithHeaders_ReturnsCorrectModel() + { + // Assign + var request = Request.Create(); + + var response = Response.Create() + .WithHeaders(new Dictionary> { { "w[]", new WireMockList("x", "y") } }); + + var mapping = new Mapping(_guid, _updatedAt, null, null, null, _settings, request, response, 0, null, null, null, null, null, false, null, data: null); // Act var model = _sut.ToMappingModel(mapping); @@ -393,6 +460,51 @@ public Task ToMappingModel_WithHeader_And_Cookie_ReturnsCorrectModel() return Verifier.Verify(model); } +#if TRAILINGHEADERS + [Fact] + public Task ToMappingModel_Response_WithTrailingHeader_ReturnsCorrectModel() + { + // Assign + var request = Request.Create(); + + var response = Response.Create() + .WithTrailingHeader("x1", "y") + .WithTrailingHeader("x2", "y", "z"); + + var mapping = new Mapping(_guid, _updatedAt, null, null, null, _settings, request, response, 0, null, null, null, null, null, false, null, data: null); + + // Act + var model = _sut.ToMappingModel(mapping); + + // Assert + model.Should().NotBeNull(); + + // Verify + return Verifier.Verify(model); + } + + [Fact] + public Task ToMappingModel_Response_WithTrailingHeaders_ReturnsCorrectModel() + { + // Assign + var request = Request.Create(); + + var response = Response.Create() + .WithTrailingHeaders(new Dictionary> { { "w[]", new WireMockList("x", "y") } }); + + var mapping = new Mapping(_guid, _updatedAt, null, null, null, _settings, request, response, 0, null, null, null, null, null, false, null, data: null); + + // Act + var model = _sut.ToMappingModel(mapping); + + // Assert + model.Should().NotBeNull(); + + // Verify + return Verifier.Verify(model); + } +#endif + [Fact] public Task ToMappingModel_WithParam_ReturnsCorrectModel() { @@ -405,7 +517,7 @@ public Task ToMappingModel_WithParam_ReturnsCorrectModel() .WithParam("ExactMatcher", new ExactMatcher("exact")) ; var response = Response.Create(); - var mapping = new Mapping(_guid, _updatedAt, null, null, null, _settings, request, response, 0, null, null, null, null, null, false, null, data: null, probability: null); + var mapping = new Mapping(_guid, _updatedAt, null, null, null, _settings, request, response, 0, null, null, null, null, null, false, null, data: null); // Act var model = _sut.ToMappingModel(mapping); @@ -422,7 +534,7 @@ public Task ToMappingModel_WithParam_ReturnsCorrectModel() public Task ToMappingModel_Request_WithBodyAsGraphQLSchema_ReturnsCorrectModel() { // Arrange - var schema = @" + const string schema = @" type Query { greeting:String students:[Student] @@ -437,7 +549,96 @@ type Student { }"; var request = Request.Create().WithBodyAsGraphQLSchema(schema); var response = Response.Create(); - var mapping = new Mapping(_guid, _updatedAt, string.Empty, string.Empty, null, _settings, request, response, 42, null, null, null, null, null, false, null, null, null); + var mapping = new Mapping(_guid, _updatedAt, string.Empty, string.Empty, null, _settings, request, response, 42, null, null, null, null, null, false, null, null); + + // Act + var model = _sut.ToMappingModel(mapping); + + // Assert + model.Should().NotBeNull(); + + // Verify + return Verifier.Verify(model); + } +#endif + +#if PROTOBUF + [Fact] + public Task ToMappingModel_Request_WithBodyAsProtoBuf_ReturnsCorrectModel() + { + // Arrange + var jsonMatcher = new JsonMatcher(new { name = "stef" }); + + var request = Request.Create() + .UsingPost() + .WithPath("/grpc/greet.Greeter/SayHello") + .WithBodyAsProtoBuf(ProtoDefinition, "greet.HelloRequest", jsonMatcher); + + var response = Response.Create(); + + var mapping = new Mapping(_guid, _updatedAt, string.Empty, string.Empty, null, _settings, request, response, 41, null, null, null, null, null, false, null, null); + + // Act + var model = _sut.ToMappingModel(mapping); + + // Assert + model.Should().NotBeNull(); + + // Verify + return Verifier.Verify(model); + } + + [Fact] + public Task ToMappingModel_Response_WithBodyAsProtoBuf_ReturnsCorrectModel() + { + // Arrange + var protobufResponse = new + { + message = "hello" + }; + + var request = Request.Create(); + + var response = Response.Create() + .WithBodyAsProtoBuf(ProtoDefinition, "greet.HelloReply", protobufResponse) + .WithTrailingHeader("grpc-status", "0"); + + var mapping = new Mapping(_guid, _updatedAt, string.Empty, string.Empty, null, _settings, request, response, 43, null, null, null, null, null, false, null, null); + + // Act + var model = _sut.ToMappingModel(mapping); + + // Assert + model.Should().NotBeNull(); + + // Verify + return Verifier.Verify(model); + } + + [Fact] + public Task ToMappingModel_Mapping_WithBodyAsProtoBuf_ReturnsCorrectModel() + { + // Arrange + var jsonMatcher = new JsonMatcher(new { name = "stef" }); + var protobufResponse = new + { + message = "hello" + }; + + var request = Request.Create() + .UsingPost() + .WithPath("/grpc/greet.Greeter/SayHello") + .WithBodyAsProtoBuf("greet.HelloRequest", jsonMatcher); + + var response = Response.Create() + .WithBodyAsProtoBuf("greet.HelloReply", protobufResponse) + .WithTrailingHeader("grpc-status", "0"); + + var mapping = new Mapping(_guid, _updatedAt, string.Empty, string.Empty, null, _settings, request, response, 41, null, null, null, null, null, false, null, null) + .WithProtoDefinition(new (null, ProtoDefinition)); + + ((Request)request).Mapping = mapping; + ((Response)response).Mapping = mapping; // Act var model = _sut.ToMappingModel(mapping); diff --git a/test/WireMock.Net.Tests/Serialization/MatcherMapperTests.cs b/test/WireMock.Net.Tests/Serialization/MatcherMapperTests.cs index 092711e2b..b81ef3964 100644 --- a/test/WireMock.Net.Tests/Serialization/MatcherMapperTests.cs +++ b/test/WireMock.Net.Tests/Serialization/MatcherMapperTests.cs @@ -27,7 +27,7 @@ public MatcherMapperTests() } [Fact] - public void MatcherMapper_Map_IMatcher_Null() + public void MatcherMapper_Map_Matcher_IMatcher_Null() { // Act var model = _sut.Map((IMatcher?)null); @@ -37,7 +37,7 @@ public void MatcherMapper_Map_IMatcher_Null() } [Fact] - public void MatcherMapper_Map_IMatchers_Null() + public void MatcherMapper_Map_Matcher_IMatchers_Null() { // Act var model = _sut.Map((IMatcher[]?)null); @@ -47,7 +47,7 @@ public void MatcherMapper_Map_IMatchers_Null() } [Fact] - public void MatcherMapper_Map_IMatchers() + public void MatcherMapper_Map_Matcher_IMatchers() { // Assign var matcherMock1 = new Mock(); @@ -62,7 +62,7 @@ public void MatcherMapper_Map_IMatchers() #if MIMEKIT [Fact] - public void MatcherMapper_Map_MimePartMatcher() + public void MatcherMapper_Map_Matcher_MimePartMatcher() { // Arrange var bytes = Convert.FromBase64String("c3RlZg=="); @@ -95,7 +95,7 @@ public void MatcherMapper_Map_MimePartMatcher() #endif [Fact] - public void MatcherMapper_Map_IStringMatcher() + public void MatcherMapper_Map_Matcher_IStringMatcher() { // Assign var matcherMock = new Mock(); @@ -115,7 +115,7 @@ public void MatcherMapper_Map_IStringMatcher() } [Fact] - public void MatcherMapper_Map_IStringMatcher_With_PatternAsFile() + public void MatcherMapper_Map_Matcher_IStringMatcher_With_PatternAsFile() { // Arrange var pattern = new StringPattern { Pattern = "p", PatternAsFile = "pf" }; @@ -136,7 +136,7 @@ public void MatcherMapper_Map_IStringMatcher_With_PatternAsFile() } [Fact] - public void MatcherMapper_Map_IIgnoreCaseMatcher() + public void MatcherMapper_Map_Matcher_IIgnoreCaseMatcher() { // Assign var matcherMock = new Mock(); @@ -150,7 +150,7 @@ public void MatcherMapper_Map_IIgnoreCaseMatcher() } [Fact] - public void MatcherMapper_Map_XPathMatcher() + public void MatcherMapper_Map_Matcher_XPathMatcher() { // Assign var xmlNamespaceMap = new[] @@ -171,7 +171,7 @@ public void MatcherMapper_Map_XPathMatcher() #if GRAPHQL [Fact] - public void MatcherMapper_Map_GraphQLMatcher() + public void MatcherMapper_Map_Matcher_GraphQLMatcher() { // Assign const string testSchema = @" @@ -199,6 +199,87 @@ type Mutation { } #endif +#if PROTOBUF + [Fact] + public void MatcherMapper_Map_Matcher_ProtoBufMatcher() + { + // Arrange + IdOrText protoDefinition = new(null, @" +syntax = ""proto3""; + +package greet; + +service Greeter { + rpc SayHello (HelloRequest) returns (HelloReply); +} + +message HelloRequest { + string name = 1; +} + +message HelloReply { + string message = 1; +} +"); + const string messageType = "greet.HelloRequest"; + + var jsonPattern = new { name = "stef" }; + var jsonMatcher = new JsonMatcher(jsonPattern); + + var matcher = new ProtoBufMatcher(() => protoDefinition, messageType, matcher: jsonMatcher); + + // Act + var model = _sut.Map(matcher)!; + + // Assert + model.Name.Should().Be(nameof(ProtoBufMatcher)); + model.Pattern.Should().Be(protoDefinition.Text); + model.ProtoBufMessageType.Should().Be(messageType); + model.ContentMatcher?.Name.Should().Be("JsonMatcher"); + model.ContentMatcher?.Pattern.Should().Be(jsonPattern); + } + + [Fact] + public void MatcherMapper_Map_Matcher_ProtoBufMatcher_WithId() + { + // Arrange + string id = "abc123"; + IdOrText protoDefinition = new(id, @" +syntax = ""proto3""; + +package greet; + +service Greeter { + rpc SayHello (HelloRequest) returns (HelloReply); +} + +message HelloRequest { + string name = 1; +} + +message HelloReply { + string message = 1; +} +"); + const string messageType = "greet.HelloRequest"; + + var jsonPattern = new { name = "stef" }; + var jsonMatcher = new JsonMatcher(jsonPattern); + + var matcher = new ProtoBufMatcher(() => protoDefinition, messageType, matcher: jsonMatcher); + + // Act + var model = _sut.Map(matcher)!; + + // Assert + model.Name.Should().Be(nameof(ProtoBufMatcher)); + model.Pattern.Should().Be(id); + model.ProtoBufMessageType.Should().Be(messageType); + model.ContentMatcher?.Name.Should().Be("JsonMatcher"); + model.ContentMatcher?.Pattern.Should().Be(jsonPattern); + } +#endif + [Fact] public void MatcherMapper_Map_MatcherModel_Null() { @@ -736,7 +817,7 @@ public void MatcherMapper_Map_MatcherModel_ExactObjectMatcher_ValidBase64StringP var matcher = (ExactObjectMatcher)_sut.Map(model)!; // Assert - Check.That(matcher.ValueAsBytes).ContainsExactly(new byte[] { 115, 116, 101, 102 }); + Check.That((byte[])matcher.Value).ContainsExactly(new byte[] { 115, 116, 101, 102 }); } [Fact] @@ -1001,4 +1082,53 @@ type Mutation { matcher.CustomScalars.Should().BeEquivalentTo(customScalars); } #endif + +#if PROTOBUF + [Fact] + public void MatcherMapper_Map_MatcherModel_ProtoBufMatcher() + { + // Arrange + const string protoDefinition = @" +syntax = ""proto3""; + +package greet; + +service Greeter { + rpc SayHello (HelloRequest) returns (HelloReply); +} + +message HelloRequest { + string name = 1; +} + +message HelloReply { + string message = 1; +} +"; + const string messageType = "greet.HelloRequest"; + + var jsonMatcherPattern = new { name = "stef" }; + + var model = new MatcherModel + { + Name = nameof(ProtoBufMatcher), + Pattern = protoDefinition, + ProtoBufMessageType = messageType, + ContentMatcher = new MatcherModel + { + Name = nameof(JsonMatcher), + Pattern = jsonMatcherPattern + } + }; + + // Act + var matcher = (ProtoBufMatcher)_sut.Map(model)!; + + // Assert + matcher.ProtoDefinition().Text.Should().Be(protoDefinition); + matcher.Name.Should().Be(nameof(ProtoBufMatcher)); + matcher.MessageType.Should().Be(messageType); + matcher.Matcher?.Value.Should().Be(jsonMatcherPattern); + } +#endif } \ No newline at end of file diff --git a/test/WireMock.Net.Tests/Settings/SimpleSettingsParserTests.cs b/test/WireMock.Net.Tests/Settings/SimpleSettingsParserTests.cs index d38acee08..bef4ddbf2 100644 --- a/test/WireMock.Net.Tests/Settings/SimpleSettingsParserTests.cs +++ b/test/WireMock.Net.Tests/Settings/SimpleSettingsParserTests.cs @@ -174,4 +174,22 @@ public void SimpleCommandLineParser_Parse_Environment_GetIntValue() Check.That(value3).IsEqualTo(100); Check.That(value4).IsNull(); } + + [Fact] + public void SimpleCommandLineParser_Parse_GetObjectValueFromJson() + { + // Assign + _parser.Parse(new[] { @"--json {""k1"":""v1"",""k2"":""v2""}" }); + + // Act + var value = _parser.GetObjectValueFromJson>("json"); + + // Assert + var expected = new Dictionary + { + { "k1", "v1" }, + { "k2", "v2" } + }; + value.Should().BeEquivalentTo(expected); + } } \ No newline at end of file diff --git a/test/WireMock.Net.Tests/Util/HttpVersionParserTests.cs b/test/WireMock.Net.Tests/Util/HttpVersionParserTests.cs new file mode 100644 index 000000000..a534d27a0 --- /dev/null +++ b/test/WireMock.Net.Tests/Util/HttpVersionParserTests.cs @@ -0,0 +1,39 @@ +using System.Diagnostics.CodeAnalysis; +using FluentAssertions; +using WireMock.Util; +using Xunit; + +namespace WireMock.Net.Tests.Util; + +[ExcludeFromCodeCoverage] +public class HttpVersionParserTests +{ + [Theory] + [InlineData("HTTP/1.1", "1.1")] + [InlineData("HTTP/2", "2")] + [InlineData("http/1.0", "1.0")] + [InlineData("HTTP/3", "3")] + public void Parse_ValidHttpVersion_ReturnsCorrectVersion(string protocol, string expectedVersion) + { + // Act + var version = HttpVersionParser.Parse(protocol); + + // Assert + version.Should().Be(expectedVersion, "the input string is a valid HTTP protocol version"); + } + + [Theory] + [InlineData("HTP/2")] + [InlineData("HTTP/2.2.2")] + [InlineData("HTTP/")] + [InlineData("http//1.1")] + [InlineData("")] + public void Parse_InvalidHttpVersion_ReturnsEmptyString(string protocol) + { + // Act + var version = HttpVersionParser.Parse(protocol); + + // Assert + version.Should().BeEmpty("the input string is not a valid HTTP protocol version"); + } +} \ No newline at end of file diff --git a/test/WireMock.Net.Tests/Util/PortUtilsTests.cs b/test/WireMock.Net.Tests/Util/PortUtilsTests.cs index ebf04918a..2be4444e9 100644 --- a/test/WireMock.Net.Tests/Util/PortUtilsTests.cs +++ b/test/WireMock.Net.Tests/Util/PortUtilsTests.cs @@ -1,5 +1,4 @@ using FluentAssertions; -using NFluent; using WireMock.Util; using Xunit; @@ -11,14 +10,15 @@ public class PortUtilsTests public void PortUtils_TryExtract_InvalidUrl_Returns_False() { // Assign - string url = "test"; + var url = "test"; // Act - bool result = PortUtils.TryExtract(url, out bool isHttps, out string proto, out string host, out int port); + var result = PortUtils.TryExtract(url, out var isHttps, out var isGrpc, out var proto, out var host, out var port); // Assert result.Should().BeFalse(); isHttps.Should().BeFalse(); + isGrpc.Should().BeFalse(); proto.Should().BeNull(); host.Should().BeNull(); port.Should().Be(default(int)); @@ -28,14 +28,15 @@ public void PortUtils_TryExtract_InvalidUrl_Returns_False() public void PortUtils_TryExtract_UrlIsMissingPort_Returns_False() { // Assign - string url = "http://0.0.0.0"; + var url = "http://0.0.0.0"; // Act - bool result = PortUtils.TryExtract(url, out bool isHttps, out string proto, out string host, out int port); + var result = PortUtils.TryExtract(url, out var isHttps, out var isGrpc, out var proto, out var host, out var port); // Assert result.Should().BeFalse(); isHttps.Should().BeFalse(); + isGrpc.Should().BeFalse(); proto.Should().BeNull(); host.Should().BeNull(); port.Should().Be(default(int)); @@ -45,14 +46,15 @@ public void PortUtils_TryExtract_UrlIsMissingPort_Returns_False() public void PortUtils_TryExtract_Http_Returns_True() { // Assign - string url = "http://wiremock.net:1234"; + var url = "http://wiremock.net:1234"; // Act - bool result = PortUtils.TryExtract(url, out bool isHttps, out string proto, out string host, out int port); + var result = PortUtils.TryExtract(url, out var isHttps, out var isGrpc, out var proto, out var host, out var port); // Assert result.Should().BeTrue(); isHttps.Should().BeFalse(); + isGrpc.Should().BeFalse(); proto.Should().Be("http"); host.Should().Be("wiremock.net"); port.Should().Be(1234); @@ -62,31 +64,51 @@ public void PortUtils_TryExtract_Http_Returns_True() public void PortUtils_TryExtract_Https_Returns_True() { // Assign - string url = "https://wiremock.net:5000"; + var url = "https://wiremock.net:5000"; // Act - bool result = PortUtils.TryExtract(url, out bool isHttps, out string proto, out string host, out int port); + var result = PortUtils.TryExtract(url, out var isHttps, out var isGrpc, out var proto, out var host, out var port); // Assert result.Should().BeTrue(); isHttps.Should().BeTrue(); + isGrpc.Should().BeFalse(); proto.Should().Be("https"); host.Should().Be("wiremock.net"); port.Should().Be(5000); } + [Fact] + public void PortUtils_TryExtract_Grpc_Returns_True() + { + // Assign + var url = "grpc://wiremock.net:1234"; + + // Act + var result = PortUtils.TryExtract(url, out var isHttps, out var isGrpc, out var proto, out var host, out var port); + + // Assert + result.Should().BeTrue(); + isHttps.Should().BeFalse(); + isGrpc.Should().BeTrue(); + proto.Should().Be("grpc"); + host.Should().Be("wiremock.net"); + port.Should().Be(1234); + } + [Fact] public void PortUtils_TryExtract_Https0_0_0_0_Returns_True() { // Assign - string url = "https://0.0.0.0:5000"; + var url = "https://0.0.0.0:5000"; // Act - bool result = PortUtils.TryExtract(url, out bool isHttps, out string proto, out string host, out int port); + var result = PortUtils.TryExtract(url, out var isHttps, out var isGrpc, out var proto, out var host, out var port); // Assert result.Should().BeTrue(); isHttps.Should().BeTrue(); + isGrpc.Should().BeFalse(); proto.Should().Be("https"); host.Should().Be("0.0.0.0"); port.Should().Be(5000); diff --git a/test/WireMock.Net.Tests/WireMock.Net.Tests.csproj b/test/WireMock.Net.Tests/WireMock.Net.Tests.csproj index 07d32aeda..d47c90573 100644 --- a/test/WireMock.Net.Tests/WireMock.Net.Tests.csproj +++ b/test/WireMock.Net.Tests/WireMock.Net.Tests.csproj @@ -27,7 +27,11 @@ - $(DefineConstants);GRAPHQL;MIMEKIT + $(DefineConstants);GRAPHQL;MIMEKIT;PROTOBUF + + + + $(DefineConstants);TRAILINGHEADERS @@ -102,7 +106,16 @@ - + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + @@ -119,6 +132,9 @@ PreserveNewest + + Client + PreserveNewest diff --git a/test/WireMock.Net.Tests/WireMockServerTests.cs b/test/WireMock.Net.Tests/WireMockServerTests.cs index c9b07fb76..b0c33570f 100644 --- a/test/WireMock.Net.Tests/WireMockServerTests.cs +++ b/test/WireMock.Net.Tests/WireMockServerTests.cs @@ -108,11 +108,10 @@ public async Task WireMockServer_Should_Support_Https() // Arrange const string body = "example"; var path = $"/foo_{Guid.NewGuid()}"; - var settings = new WireMockServerSettings + var server = WireMockServer.Start(settings => { - UseSSL = true - }; - var server = WireMockServer.Start(settings); + settings.UseSSL = true; + }); server .Given(Request.Create()