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

* <PackageReference Include="ProtoBufJsonConverter" Version="0.2.0-preview-04" />

* http version

* tests

* .WithHttpVersion("2")

* <PackageReference Include="ProtoBufJsonConverter" Version="0.2.0" />

* HttpVersionParser
This commit is contained in:
Stef Heyenrath
2024-02-16 17:16:51 +01:00
committed by GitHub
parent 801546fae7
commit 6ac95cf57d
129 changed files with 4585 additions and 1556 deletions

View File

@@ -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}

View File

@@ -25,12 +25,14 @@
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=XUA/@EntryIndexedValue">XUA</s:String>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Flurl/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=funcs/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Grpc/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=guidb/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Guids/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Heyenrath/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Jmes/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=openapi/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Pacticipant/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=protobuf/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Raml/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=randomizer/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Scriban/@EntryIndexedValue">True</s:Boolean>

View File

@@ -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);
}
}

View File

@@ -0,0 +1,23 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Google.Protobuf" Version="3.25.1" />
<PackageReference Include="Grpc.Net.Client" Version="2.59.0" />
<PackageReference Include="Grpc.Tools" Version="2.60.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<Protobuf Include="greet.proto" GrpcServices="Client" />
</ItemGroup>
</Project>

View File

@@ -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;
}

View File

@@ -3,7 +3,7 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<DefineConstants>$(DefineConstants);GRAPHQL;MIMEKIT</DefineConstants>
<DefineConstants>$(DefineConstants);GRAPHQL;MIMEKIT;PROTOBUF</DefineConstants>
</PropertyGroup>
<ItemGroup>

View File

@@ -42,6 +42,24 @@ namespace WireMock.Net.ConsoleApplication
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 @@ namespace WireMock.Net.ConsoleApplication
.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 @@ namespace WireMock.Net.ConsoleApplication
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 @@ namespace WireMock.Net.ConsoleApplication
//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<string, Type> { { "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 @@ namespace WireMock.Net.ConsoleApplication
Url = "http://localhost:9999",
ReplaceSettings = new ProxyUrlReplaceSettings
{
OldValue = "old",
NewValue = "new"
OldValue = "old",
NewValue = "new"
}
})
);

View File

@@ -98,4 +98,9 @@ public class MappingModel
/// The probability when this request should be matched. Value is between 0 and 1. [Optional]
/// </summary>
public double? Probability { get; set; }
/// <summary>
/// The Grpc ProtoDefinition which is used for this mapping (request and response). [Optional]
/// </summary>
public string? ProtoDefinition { get; set; }
}

View File

@@ -70,13 +70,22 @@ public class MatcherModel
/// ContentTransferEncoding Matcher (base64)
/// </summary>
public MatcherModel? ContentTransferEncodingMatcher { get; set; }
#endregion
#region MimePartMatcher + ProtoBufMatcher
/// <summary>
/// Content Matcher
/// </summary>
public MatcherModel? ContentMatcher { get; set; }
#endregion
#region ProtoBufMatcher
/// <summary>
/// The full type of the protobuf (request/response) message object. Format is "{package-name}.{type-name}".
/// </summary>
public string? ProtoBufMessageType { get; set; }
#endregion
#region XPathMatcher
/// <summary>
/// Array of namespace prefix and uri. (optional)
@@ -86,7 +95,7 @@ public class MatcherModel
#region GraphQLMatcher
/// <summary>
/// Mapping of custom GraphQL Scalar name to ClrType. (optional)
/// Mapping of custom GraphQL Scalar name to ClrType. (optional)
/// </summary>
public IDictionary<string, Type>? CustomScalars { get; set; }
#endregion

View File

@@ -28,6 +28,11 @@ public class RequestModel
/// </summary>
public string[]? Methods { get; set; }
/// <summary>
/// The HTTP Version
/// </summary>
public string? HttpVersion { get; set; }
/// <summary>
/// Reject on match for Methods.
/// </summary>

View File

@@ -35,7 +35,7 @@ public class ResponseModel
public bool? BodyAsJsonIndented { get; set; }
/// <summary>
/// Gets or sets the body (as bytearray).
/// Gets or sets the body (as byte array).
/// </summary>
public byte[]? BodyAsBytes { get; set; }
@@ -84,6 +84,11 @@ public class ResponseModel
/// </summary>
public string? HeadersRaw { get; set; }
/// <summary>
/// Gets or sets the Trailing Headers.
/// </summary>
public IDictionary<string, object>? TrailingHeaders { get; set; }
/// <summary>
/// Gets or sets the delay in milliseconds.
/// </summary>
@@ -123,4 +128,16 @@ public class ResponseModel
/// Gets or sets the WebProxy settings.
/// </summary>
public WebProxyModel? WebProxy { get; set; }
#region ProtoBuf
/// <summary>
/// Gets or sets the proto definition.
/// </summary>
public string? ProtoDefinition { get; set; }
/// <summary>
/// Gets or sets the full type of the protobuf (request/response) message object. Format is "{package-name}.{type-name}".
/// </summary>
public string? ProtoBufMessageType { get; set; }
#endregion
}

View File

@@ -55,6 +55,11 @@ public class LogRequestModel
/// </summary>
public string Method { get; set; }
/// <summary>
/// The HTTP Version.
/// </summary>
public string HttpVersion { get; set; } = null!;
/// <summary>
/// The Headers.
/// </summary>

View File

@@ -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
/// </summary>
public QueryParameterMultipleValueSupport? QueryParameterMultipleValueSupport { get; set; }
/// <summary>
/// A list of Grpc ProtoDefinitions which can be used.
/// </summary>
public Dictionary<string, string>? ProtoDefinitions { get; set; }
#if NETSTANDARD1_3_OR_GREATER || NET461
/// <summary>
/// Server client certificate mode

View File

@@ -63,6 +63,11 @@ public interface IRequestMessage
/// </summary>
string Method { get; }
/// <summary>
/// Gets the HTTP Version.
/// </summary>
string HttpVersion { get; }
/// <summary>
/// Gets the headers.
/// </summary>
@@ -94,23 +99,27 @@ public interface IRequestMessage
IBodyData? BodyData { get; }
/// <summary>
/// The original body as string. Convenience getter for Handlebars and WireMockAssertions.
/// The original body as string.
/// Convenience getter for Handlebars and WireMockAssertions.
/// </summary>
string? Body { get; }
/// <summary>
/// The body (as JSON object). Convenience getter for Handlebars and WireMockAssertions.
/// The body (as JSON object).
/// Convenience getter for Handlebars and WireMockAssertions.
/// </summary>
object? BodyAsJson { get; }
/// <summary>
/// The body (as bytearray). Convenience getter for Handlebars and WireMockAssertions.
/// The body (as bytearray).
/// Convenience getter for Handlebars and WireMockAssertions.
/// </summary>
byte[]? BodyAsBytes { get; }
#if MIMEKIT
/// <summary>
/// The original body as MimeMessage. Convenience getter for Handlebars and WireMockAssertions.
/// The original body as MimeMessage.
/// Convenience getter for Handlebars and WireMockAssertions.
/// </summary>
object? BodyAsMimeMessage { get; }
#endif

View File

@@ -40,11 +40,16 @@ public interface IResponseMessage
/// </summary>
IDictionary<string, WireMockList<string>>? Headers { get; }
/// <summary>
/// Gets the trailing headers.
/// </summary>
IDictionary<string, WireMockList<string>>? TrailingHeaders { get; }
/// <summary>
/// Gets or sets the status code.
/// </summary>
object? StatusCode { get; }
/// <summary>
/// Adds the header.
/// </summary>
@@ -53,9 +58,23 @@ public interface IResponseMessage
void AddHeader(string name, string value);
/// <summary>
/// Adds the header.
/// Adds the trailing header.
/// </summary>
/// <param name="name">The name.</param>
/// <param name="values">The values.</param>
void AddHeader(string name, params string[] values);
/// <summary>
/// Adds the trailing header.
/// </summary>
/// <param name="name">The name.</param>
/// <param name="value">The value.</param>
void AddTrailingHeader(string name, string value);
/// <summary>
/// Adds the header.
/// </summary>
/// <param name="name">The name.</param>
/// <param name="values">The values.</param>
void AddTrailingHeader(string name, params string[] values);
}

View File

@@ -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;
/// <summary>
@@ -10,7 +13,7 @@ namespace WireMock.Util;
public interface IBodyData
{
/// <summary>
/// The body (as bytearray).
/// The body (as byte array).
/// </summary>
byte[]? BodyAsBytes { get; set; }
@@ -26,6 +29,7 @@ public interface IBodyData
/// <summary>
/// The body (as JSON object).
/// Also used for ProtoBuf.
/// </summary>
object? BodyAsJson { get; set; }
@@ -68,4 +72,16 @@ public interface IBodyData
/// Defines if this BodyData is the result of a dynamically created response-string. (
/// </summary>
public string? IsFuncUsed { get; set; }
#region ProtoBuf
/// <summary>
/// Gets or sets the proto definition.
/// </summary>
public Func<IdOrText>? ProtoDefinition { get; set; }
/// <summary>
/// Gets or sets the full type of the protobuf (request/response) message object. Format is "{package-name}.{type-name}".
/// </summary>
public string? ProtoBufMessageType { get; set; }
#endregion
}

View File

@@ -0,0 +1,33 @@
namespace WireMock.Models;
/// <summary>
/// A structure defining an (optional) Id and a Text.
/// </summary>
public readonly struct IdOrText
{
/// <summary>
/// The Id [optional].
/// </summary>
public string? Id { get; }
/// <summary>
/// The Text.
/// </summary>
public string Text { get; }
/// <summary>
/// When Id is defined, return the Id, else the Text.
/// </summary>
public string Value => Id ?? Text;
/// <summary>
/// Create a IdOrText
/// </summary>
/// <param name="id">The Id [optional]</param>
/// <param name="text">The Text.</param>
public IdOrText(string? id, string text)
{
Id = id;
Text = text;
}
}

View File

@@ -38,5 +38,10 @@ public enum BodyType
/// <summary>
/// Body is a String which is x-www-form-urlencoded.
/// </summary>
FormUrlEncoded
FormUrlEncoded,
/// <summary>
/// Body is a ProtoBuf Byte array
/// </summary>
ProtoBuf
}

View File

@@ -31,7 +31,7 @@
</PropertyGroup>
<PropertyGroup Condition=" '$(TargetFramework)' == 'netstandard2.0' or '$(TargetFramework)' == 'netstandard2.1'">
<DefineConstants>$(DefineConstants);GRAPHQL;MIMEKIT</DefineConstants>
<DefineConstants>$(DefineConstants);GRAPHQL;MIMEKIT;PROTOBUF</DefineConstants>
</PropertyGroup>
<ItemGroup>

View File

@@ -44,11 +44,11 @@ public partial class WireMockAssertions
}
[CustomAssertion]
public AndConstraint<WireMockAssertions> WithBodyAsJson(IValueMatcher matcher, string because = "", params object[] becauseArgs)
public AndConstraint<WireMockAssertions> 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 @@ public partial class WireMockAssertions
return new AndConstraint<WireMockAssertions>(this);
}
private AndConstraint<WireMockAssertions> ExecuteAssertionWithBodyAsJsonValueMatcher(
IValueMatcher matcher,
private AndConstraint<WireMockAssertions> ExecuteAssertionWithBodyAsIObjectMatcher(
IObjectMatcher matcher,
string because,
object[] becauseArgs,
Func<IReadOnlyList<IRequestMessage>, bool> condition,
@@ -134,13 +134,13 @@ public partial class WireMockAssertions
.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)
);

View File

@@ -33,6 +33,9 @@ internal class CSharpCodeMatcher : ICSharpCodeMatcher
/// <inheritdoc />
public MatchBehaviour MatchBehaviour { get; }
/// <inheritdoc />
public object Value { get; }
private readonly AnyOf<string, StringPattern>[] _patterns;
/// <summary>
@@ -54,6 +57,7 @@ internal class CSharpCodeMatcher : ICSharpCodeMatcher
_patterns = Guard.NotNull(patterns);
MatchBehaviour = matchBehaviour;
MatchOperator = matchOperator;
Value = patterns;
}
public MatchResult IsMatch(string? input)
@@ -160,34 +164,34 @@ internal class CSharpCodeMatcher : ICSharpCodeMatcher
}
#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

View File

@@ -46,7 +46,7 @@
</ItemGroup>
<ItemGroup Condition=" '$(TargetFramework)' == 'netstandard2.0' or '$(TargetFramework)' == 'netstandard2.1' or '$(TargetFramework)' == 'netcoreapp3.1' or '$(TargetFramework)' == 'net5.0' or '$(TargetFramework)' == 'net6.0' or '$(TargetFramework)' == 'net7.0' or '$(TargetFramework)' == 'net8.0'">
<PackageReference Include="CS-Script" Version="4.4.2" />
<PackageReference Include="CS-Script" Version="4.8.13" />
</ItemGroup>
</Project>

View File

@@ -69,12 +69,12 @@ public interface IMapping
int? StateTimes { get; }
/// <summary>
/// The Request matcher.
/// The RequestMatcher.
/// </summary>
IRequestMatcher RequestMatcher { get; }
/// <summary>
/// The Provider.
/// The ResponseProvider.
/// </summary>
IResponseProvider Provider { get; }
@@ -136,6 +136,11 @@ public interface IMapping
/// </summary>
double? Probability { get; }
/// <summary>
/// The Grpc ProtoDefinition which is used for this mapping (request and response). [Optional]
/// </summary>
IdOrText? ProtoDefinition { get; }
/// <summary>
/// ProvideResponseAsync
/// </summary>
@@ -150,4 +155,44 @@ public interface IMapping
/// <param name="nextState">The Next State.</param>
/// <returns>The <see cref="IRequestMatchResult"/>.</returns>
IRequestMatchResult GetRequestMatchResult(IRequestMessage requestMessage, string? nextState);
}
/// <summary>
/// Define the scenario.
/// </summary>
/// <param name="scenario">The scenario.</param>
/// <returns>The <see cref="IMapping"/>.</returns>
IMapping WithScenario(string scenario);
/// <summary>
/// Define the probability when this request should be matched. [Optional]
/// </summary>
/// <param name="probability">The probability.</param>
/// <returns>The <see cref="IMapping"/>.</returns>
IMapping WithProbability(double probability);
/// <summary>
/// Define a Grpc ProtoDefinition which is used for this mapping (request and response).
/// </summary>
/// <param name="protoDefinition">The proto definition as text.</param>
/// <returns>The <see cref="IMapping"/>.</returns>
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,
*/

View File

@@ -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; }
/// <inheritdoc />
public string? Scenario { get; }
public string? Scenario { get; private set; }
/// <inheritdoc />
public string? ExecutionConditionState { get; }
@@ -76,7 +77,10 @@ public class Mapping : IMapping
public object? Data { get; }
/// <inheritdoc />
public double? Probability { get; }
public double? Probability { get; private set; }
/// <inheritdoc />
public IdOrText? ProtoDefinition { get; private set; }
/// <summary>
/// Initializes a new instance of the <see cref="Mapping"/> class.
@@ -98,8 +102,8 @@ public class Mapping : IMapping
/// <param name="useWebhooksFireAndForget">Use Fire and Forget for the defined webhook(s). [Optional]</param>
/// <param name="timeSettings">The TimeSettings. [Optional]</param>
/// <param name="data">The data object. [Optional]</param>
/// <param name="probability">Define the probability when this request should be matched. [Optional]</param>
public Mapping(
public Mapping
(
Guid guid,
DateTime updatedAt,
string? title,
@@ -116,8 +120,8 @@ public class Mapping : IMapping
IWebhook[]? webhooks,
bool? useWebhooksFireAndForget,
ITimeSettings? timeSettings,
object? data,
double? probability)
object? data
)
{
Guid = guid;
UpdatedAt = updatedAt;
@@ -136,7 +140,6 @@ public class Mapping : IMapping
UseWebhooksFireAndForget = useWebhooksFireAndForget;
TimeSettings = timeSettings;
Data = data;
Probability = probability;
}
/// <inheritdoc />
@@ -168,4 +171,25 @@ public class Mapping : IMapping
return result;
}
/// <inheritdoc />
public IMapping WithProbability(double probability)
{
Probability = Guard.NotNull(probability);
return this;
}
/// <inheritdoc />
public IMapping WithScenario(string scenario)
{
Scenario = Guard.NotNullOrWhiteSpace(scenario);
return this;
}
/// <inheritdoc />
public IMapping WithProtoDefinition(IdOrText protoDefinition)
{
ProtoDefinition = protoDefinition;
return this;
}
}

View File

@@ -6,6 +6,8 @@ using Stef.Validation;
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 @@ public class MappingBuilder : IMappingBuilder
{
_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)

View File

@@ -17,6 +17,15 @@ public class ExactMatcher : IStringMatcher, IIgnoreCaseMatcher
/// <inheritdoc />
public MatchBehaviour MatchBehaviour { get; }
/// <summary>
/// Initializes a new instance of the <see cref="ExactMatcher"/> class.
/// </summary>
/// <param name="matchBehaviour">The match behaviour.</param>
/// <param name="value">The string value.</param>
public ExactMatcher(MatchBehaviour matchBehaviour, string value) : this(matchBehaviour, true, MatchOperator.Or, new AnyOf<string, StringPattern>(value))
{
}
/// <summary>
/// Initializes a new instance of the <see cref="ExactMatcher"/> class.
/// </summary>

View File

@@ -9,15 +9,8 @@ namespace WireMock.Matchers;
/// <seealso cref="IObjectMatcher" />
public class ExactObjectMatcher : IObjectMatcher
{
/// <summary>
/// Gets the value as object.
/// </summary>
public object? ValueAsObject { get; }
/// <summary>
/// Gets the value as byte[].
/// </summary>
public byte[]? ValueAsBytes { get; }
/// <inheritdoc />
public object Value { get; }
/// <inheritdoc />
public MatchBehaviour MatchBehaviour { get; }
@@ -37,7 +30,7 @@ public class ExactObjectMatcher : IObjectMatcher
/// <param name="value">The value.</param>
public ExactObjectMatcher(MatchBehaviour matchBehaviour, object value)
{
ValueAsObject = Guard.NotNull(value);
Value = Guard.NotNull(value);
MatchBehaviour = matchBehaviour;
}
@@ -56,21 +49,21 @@ public class ExactObjectMatcher : IObjectMatcher
/// <param name="value">The value.</param>
public ExactObjectMatcher(MatchBehaviour matchBehaviour, byte[] value)
{
ValueAsBytes = Guard.NotNull(value);
Value = Guard.NotNull(value);
MatchBehaviour = matchBehaviour;
}
/// <inheritdoc />
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));

View File

@@ -0,0 +1,18 @@
using System.Threading;
using System.Threading.Tasks;
namespace WireMock.Matchers;
/// <summary>
/// IBytesMatcher
/// </summary>
public interface IBytesMatcher : IMatcher
{
/// <summary>
/// Determines whether the specified input is match.
/// </summary>
/// <param name="input">The input byte array.</param>
/// <param name="cancellationToken">The CancellationToken [optional].</param>
/// <returns>MatchResult</returns>
Task<MatchResult> IsMatchAsync(byte[]? input, CancellationToken cancellationToken = default);
}

View File

@@ -0,0 +1,18 @@
using System.Threading;
using System.Threading.Tasks;
namespace WireMock.Matchers;
/// <summary>
/// IDecodeBytesMatcher
/// </summary>
public interface IDecodeBytesMatcher
{
/// <summary>
/// Decode byte array to an object.
/// </summary>
/// <param name="input">The byte array</param>
/// <param name="cancellationToken">The CancellationToken [optional].</param>
/// <returns>object</returns>
Task<object?> DecodeAsync(byte[]? input, CancellationToken cancellationToken = default);
}

View File

@@ -0,0 +1,9 @@
namespace WireMock.Matchers;
/// <summary>
/// IJsonMatcher
/// <seealso cref="IObjectMatcher"/> and <seealso cref="IIgnoreCaseMatcher"/>.
/// </summary>
public interface IJsonMatcher : IObjectMatcher, IIgnoreCaseMatcher
{
}

View File

@@ -5,6 +5,12 @@ namespace WireMock.Matchers;
/// </summary>
public interface IObjectMatcher : IMatcher
{
/// <summary>
/// Gets the value (can be a string or an object).
/// </summary>
/// <returns>Value</returns>
object Value { get; }
/// <summary>
/// Determines whether the specified input is match.
/// </summary>

View File

@@ -0,0 +1,8 @@
namespace WireMock.Matchers;
/// <summary>
/// IProtoBufMatcher
/// </summary>
public interface IProtoBufMatcher : IDecodeBytesMatcher, IBytesMatcher
{
}

View File

@@ -1,14 +0,0 @@
namespace WireMock.Matchers;
/// <summary>
/// IValueMatcher
/// </summary>
/// <seealso cref="IObjectMatcher" />
public interface IValueMatcher : IObjectMatcher
{
/// <summary>
/// Gets the value (can be a string or an object).
/// </summary>
/// <returns>Value</returns>
object Value { get; }
}

View File

@@ -11,7 +11,7 @@ namespace WireMock.Matchers;
/// <summary>
/// JsonPathMatcher
/// </summary>
/// <seealso cref="IMatcher" />
/// <seealso cref="IStringMatcher" />
/// <seealso cref="IObjectMatcher" />
public class JsonPathMatcher : IStringMatcher, IObjectMatcher
{
@@ -20,6 +20,9 @@ public class JsonPathMatcher : IStringMatcher, IObjectMatcher
/// <inheritdoc />
public MatchBehaviour MatchBehaviour { get; }
/// <inheritdoc />
public object Value { get; }
/// <summary>
/// Initializes a new instance of the <see cref="JsonPathMatcher"/> class.
/// </summary>
@@ -52,6 +55,7 @@ public class JsonPathMatcher : IStringMatcher, IObjectMatcher
_patterns = Guard.NotNull(patterns);
MatchBehaviour = matchBehaviour;
MatchOperator = matchOperator;
Value = patterns;
}
/// <inheritdoc />
@@ -119,7 +123,7 @@ public class JsonPathMatcher : IStringMatcher, IObjectMatcher
// 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);
}

View File

@@ -16,6 +16,9 @@ public class JmesPathMatcher : IStringMatcher, IObjectMatcher
{
private readonly AnyOf<string, StringPattern>[] _patterns;
/// <inheritdoc />
public object Value { get; }
/// <inheritdoc />
public MatchBehaviour MatchBehaviour { get; }
@@ -59,6 +62,7 @@ public class JmesPathMatcher : IStringMatcher, IObjectMatcher
_patterns = Guard.NotNull(patterns);
MatchBehaviour = matchBehaviour;
MatchOperator = matchOperator;
Value = patterns;
}
/// <inheritdoc />

View File

@@ -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;
/// <summary>
/// JsonMatcher
/// </summary>
public class JsonMatcher : IValueMatcher, IIgnoreCaseMatcher
public class JsonMatcher : IJsonMatcher
{
/// <inheritdoc />
public virtual string Name => "JsonMatcher";
public virtual string Name => nameof(JsonMatcher);
/// <inheritdoc cref="IValueMatcher.Value"/>
/// <inheritdoc />
public object Value { get; }
/// <inheritdoc />
@@ -59,7 +58,7 @@ public class JsonMatcher : IValueMatcher, IIgnoreCaseMatcher
IgnoreCase = ignoreCase;
Value = value;
_valueAsJToken = ConvertValueToJToken(value);
_valueAsJToken = JsonUtils.ConvertValueToJToken(value);
_jTokenConverter = ignoreCase ? Rename : jToken => jToken;
}
@@ -74,7 +73,7 @@ public class JsonMatcher : IValueMatcher, IIgnoreCaseMatcher
{
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 @@ public class JsonMatcher : IValueMatcher, IIgnoreCaseMatcher
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();

View File

@@ -22,6 +22,9 @@ public class LinqMatcher : IObjectMatcher, IStringMatcher
/// <inheritdoc />
public MatchBehaviour MatchBehaviour { get; }
/// <inheritdoc />
public object Value { get; }
/// <summary>
/// Initializes a new instance of the <see cref="LinqMatcher"/> class.
/// </summary>
@@ -61,6 +64,7 @@ public class LinqMatcher : IObjectMatcher, IStringMatcher
_patterns = Guard.NotNull(patterns);
MatchBehaviour = matchBehaviour;
MatchOperator = matchOperator;
Value = patterns;
}
/// <inheritdoc />

View File

@@ -23,4 +23,11 @@ internal static class MatchBehaviourHelper
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);
}
}

View File

@@ -17,6 +17,9 @@ public class NotNullOrEmptyMatcher : IObjectMatcher, IStringMatcher
/// <inheritdoc />
public MatchBehaviour MatchBehaviour { get; }
/// <inheritdoc />
public object Value { get; }
/// <summary>
/// Initializes a new instance of the <see cref="NotNullOrEmptyMatcher"/> class.
/// </summary>
@@ -24,6 +27,7 @@ public class NotNullOrEmptyMatcher : IObjectMatcher, IStringMatcher
public NotNullOrEmptyMatcher(MatchBehaviour matchBehaviour = MatchBehaviour.AcceptOnMatch)
{
MatchBehaviour = matchBehaviour;
Value = string.Empty;
}
/// <inheritdoc />

View File

@@ -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;
/// <summary>
/// Grpc ProtoBuf Matcher
/// </summary>
/// <inheritdoc cref="IProtoBufMatcher"/>
public class ProtoBufMatcher : IProtoBufMatcher
{
/// <inheritdoc />
public string Name => nameof(ProtoBufMatcher);
/// <inheritdoc />
public MatchBehaviour MatchBehaviour { get; }
/// <summary>
/// The Func to define The proto definition as text.
/// </summary>
public Func<IdOrText> ProtoDefinition { get; }
/// <summary>
/// The full type of the protobuf (request/response) message object. Format is "{package-name}.{type-name}".
/// </summary>
public string MessageType { get; }
/// <summary>
/// The Matcher to use (optional).
/// </summary>
public IObjectMatcher? Matcher { get; }
private static readonly Converter ProtoBufToJsonConverter = SingletonFactory<Converter>.GetInstance();
/// <summary>
/// Initializes a new instance of the <see cref="ProtoBufMatcher"/> class.
/// </summary>
/// <param name="protoDefinition">The proto definition.</param>
/// <param name="messageType">The full type of the protobuf (request/response) message object. Format is "{package-name}.{type-name}".</param>
/// <param name="matchBehaviour">The match behaviour. (default = "AcceptOnMatch")</param>
/// <param name="matcher">The optional jsonMatcher to use to match the ProtoBuf as (json) object.</param>
public ProtoBufMatcher(
Func<IdOrText> protoDefinition,
string messageType,
MatchBehaviour matchBehaviour = MatchBehaviour.AcceptOnMatch,
IObjectMatcher? matcher = null
)
{
ProtoDefinition = Guard.NotNull(protoDefinition);
MessageType = Guard.NotNullOrWhiteSpace(messageType);
Matcher = matcher;
MatchBehaviour = matchBehaviour;
}
/// <inheritdoc />
public async Task<MatchResult> 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);
}
/// <inheritdoc />
public Task<object?> DecodeAsync(byte[]? input, CancellationToken cancellationToken = default)
{
return DecodeAsync(input, false, cancellationToken);
}
private async Task<object?> 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

View File

@@ -26,10 +26,8 @@ public abstract class RequestMessageCompositeMatcher : IRequestMatcher
/// <param name="type">The CompositeMatcherType type (Defaults to 'And')</param>
protected RequestMessageCompositeMatcher(IEnumerable<IRequestMatcher> requestMatchers, CompositeMatcherType type = CompositeMatcherType.And)
{
Guard.NotNull(requestMatchers);
RequestMatchers = Guard.NotNull(requestMatchers);
_type = type;
RequestMatchers = requestMatchers;
}
/// <inheritdoc />

View File

@@ -26,7 +26,7 @@ public class RequestMessageGraphQLMatcher : IRequestMatcher
/// </summary>
/// <param name="matchBehaviour">The match behaviour.</param>
/// <param name="schema">The schema.</param>
/// <param name="customScalars">A dictionary defining the custom scalars used in this schema. (optional)</param>
/// <param name="customScalars">A dictionary defining the custom scalars used in this schema. [optional]</param>
public RequestMessageGraphQLMatcher(MatchBehaviour matchBehaviour, string schema, IDictionary<string, Type>? customScalars = null) :
this(CreateMatcherArray(matchBehaviour, schema, customScalars))
{
@@ -38,7 +38,7 @@ public class RequestMessageGraphQLMatcher : IRequestMatcher
/// </summary>
/// <param name="matchBehaviour">The match behaviour.</param>
/// <param name="schema">The schema.</param>
/// <param name="customScalars">A dictionary defining the custom scalars used in this schema. (optional)</param>
/// <param name="customScalars">A dictionary defining the custom scalars used in this schema. [optional]</param>
public RequestMessageGraphQLMatcher(MatchBehaviour matchBehaviour, GraphQL.Types.ISchema schema, IDictionary<string, Type>? customScalars = null) :
this(CreateMatcherArray(matchBehaviour, new AnyOfTypes.AnyOf<string, WireMock.Models.StringPattern, GraphQL.Types.ISchema>(schema), customScalars))
{
@@ -94,8 +94,7 @@ public class RequestMessageGraphQLMatcher : IRequestMatcher
#if GRAPHQL
private static IMatcher[] CreateMatcherArray(
MatchBehaviour matchBehaviour,
AnyOfTypes.AnyOf<string, WireMock.Models.StringPattern,
GraphQL.Types.ISchema> schema,
AnyOfTypes.AnyOf<string, WireMock.Models.StringPattern, GraphQL.Types.ISchema> schema,
IDictionary<string, Type>? customScalars
)
{

View File

@@ -0,0 +1,86 @@
using System;
using System.Linq;
using Stef.Validation;
namespace WireMock.Matchers.Request;
/// <summary>
/// The request HTTP Version matcher.
/// </summary>
public class RequestMessageHttpVersionMatcher : IRequestMatcher
{
/// <summary>
/// The matcher.
/// </summary>
public IStringMatcher? Matcher { get; }
/// <summary>
/// The func.
/// </summary>
public Func<string, bool>? Func { get; }
/// <summary>
/// The <see cref="MatchBehaviour"/>
/// </summary>
public MatchBehaviour Behaviour { get; }
/// <summary>
/// The HTTP Version
/// </summary>
public string? HttpVersion { get; }
/// <summary>
/// Initializes a new instance of the <see cref="RequestMessageHttpVersionMatcher"/> class.
/// </summary>
/// <param name="matchBehaviour">The match behaviour.</param>
/// <param name="httpVersion">The HTTP Version.</param>
public RequestMessageHttpVersionMatcher(MatchBehaviour matchBehaviour, string httpVersion) :
this(matchBehaviour, new ExactMatcher(matchBehaviour, httpVersion))
{
HttpVersion = httpVersion;
Behaviour = matchBehaviour;
}
/// <summary>
/// Initializes a new instance of the <see cref="RequestMessageClientIPMatcher"/> class.
/// </summary>
/// <param name="matchBehaviour">The match behaviour.</param>
/// <param name="matcher">The matcher.</param>
public RequestMessageHttpVersionMatcher(MatchBehaviour matchBehaviour, IStringMatcher matcher)
{
Matcher = Guard.NotNull(matcher);
Behaviour = matchBehaviour;
HttpVersion = matcher.GetPatterns().FirstOrDefault();
}
/// <summary>
/// Initializes a new instance of the <see cref="RequestMessageClientIPMatcher"/> class.
/// </summary>
/// <param name="func">The function.</param>
public RequestMessageHttpVersionMatcher(Func<string, bool> func)
{
Func = Guard.NotNull(func);
}
/// <inheritdoc />
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;
}
}

View File

@@ -0,0 +1,43 @@
using System;
using WireMock.Models;
namespace WireMock.Matchers.Request;
/// <summary>
/// The request body Grpc ProtoBuf matcher.
/// </summary>
public class RequestMessageProtoBufMatcher : IRequestMatcher
{
/// <summary>
/// The ProtoBufMatcher.
/// </summary>
public IProtoBufMatcher? Matcher { get; }
/// <summary>
/// Initializes a new instance of the <see cref="RequestMessageProtoBufMatcher"/> class.
/// </summary>
/// <param name="matchBehaviour">The match behaviour. (default = "AcceptOnMatch")</param>
/// <param name="protoDefinition">The Func to define The proto definition as text.</param>
/// <param name="messageType">The full type of the protobuf (request/response) message object. Format is "{package-name}.{type-name}".</param>
/// <param name="matcher">The optional matcher to use to match the ProtoBuf as (json) object.</param>
public RequestMessageProtoBufMatcher(MatchBehaviour matchBehaviour, Func<IdOrText> 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
}
/// <inheritdoc />
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;
}
}

View File

@@ -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;
/// <summary>
@@ -9,7 +12,7 @@ namespace WireMock.Util;
/// </summary>
public class BodyData : IBodyData
{
/// <inheritdoc cref="IBodyData.Encoding" />
/// <inheritdoc />
public Encoding? Encoding { get; set; }
/// <inheritdoc />
@@ -18,30 +21,38 @@ public class BodyData : IBodyData
/// <inheritdoc />
public IDictionary<string, string>? BodyAsFormUrlEncoded { get; set; }
/// <inheritdoc cref="IBodyData.BodyAsJson" />
/// <inheritdoc />
public object? BodyAsJson { get; set; }
/// <inheritdoc cref="IBodyData.BodyAsBytes" />
/// <inheritdoc />
public byte[]? BodyAsBytes { get; set; }
/// <inheritdoc cref="IBodyData.BodyAsJsonIndented" />
/// <inheritdoc />
public bool? BodyAsJsonIndented { get; set; }
/// <inheritdoc cref="IBodyData.BodyAsFile" />
/// <inheritdoc />
public string? BodyAsFile { get; set; }
/// <inheritdoc cref="IBodyData.BodyAsFileIsCached" />
/// <inheritdoc />
public bool? BodyAsFileIsCached { get; set; }
/// <inheritdoc cref="IBodyData.DetectedBodyType" />
/// <inheritdoc />
public BodyType? DetectedBodyType { get; set; }
/// <inheritdoc cref="IBodyData.DetectedBodyTypeFromContentType" />
/// <inheritdoc />
public BodyType? DetectedBodyTypeFromContentType { get; set; }
/// <inheritdoc cref="IRequestMessage.DetectedCompression" />
/// <inheritdoc />
public string? DetectedCompression { get; set; }
/// <inheritdoc />
public string? IsFuncUsed { get; set; }
#region ProtoBuf
/// <inheritdoc />
public Func<IdOrText>? ProtoDefinition { get; set; }
/// <inheritdoc />
public string? ProtoBufMessageType { get; set; }
#endregion
}

View File

@@ -0,0 +1,61 @@
using System;
using System.Collections.Generic;
using AnyOfTypes;
using Newtonsoft.Json;
namespace WireMock.Models;
/// <summary>
/// GraphQLSchemaDetails
/// </summary>
public class GraphQLSchemaDetails
{
/// <summary>
/// The GraphQL schema as a string.
/// </summary>
public string? SchemaAsString { get; set; }
/// <summary>
/// The GraphQL schema as a StringPattern.
/// </summary>
public StringPattern? SchemaAsStringPattern { get; set; }
#if GRAPHQL
/// <summary>
/// The GraphQL schema as a <seealso cref="GraphQL.Types.ISchema"/>.
/// </summary>
public GraphQL.Types.ISchema? SchemaAsISchema { get; set; }
/// <summary>
/// The GraphQL Schema.
/// </summary>
[JsonIgnore]
public AnyOf<string, StringPattern, GraphQL.Types.ISchema>? Schema
{
get
{
if (SchemaAsString != null)
{
return SchemaAsString;
}
if (SchemaAsStringPattern != null)
{
return SchemaAsStringPattern;
}
if (SchemaAsISchema != null)
{
return new AnyOf<string, StringPattern, GraphQL.Types.ISchema>(SchemaAsISchema);
}
return null;
}
}
#endif
/// <summary>
/// The custom Scalars to define for this schema.
/// </summary>
public IDictionary<string, Type>? CustomScalars { get; set; }
}

View File

@@ -46,6 +46,18 @@ namespace WireMock.Owin
options.ClientCertificateValidation = (_, _, _) => true;
}
});
if (urlDetail.IsHttp2)
{
listenOptions.Protocols = HttpProtocols.Http2;
}
});
}
else if (urlDetail.IsHttp2)
{
kestrelOptions.ListenAnyIP(urlDetail.Port, listenOptions =>
{
listenOptions.Protocols = HttpProtocols.Http2;
});
}
else

View File

@@ -110,18 +110,22 @@ namespace WireMock.Owin
{
try
{
#if NETCOREAPP3_1 || NET5_0_OR_GREATER
var appLifetime = _host.Services.GetRequiredService<Microsoft.Extensions.Hosting.IHostApplicationLifetime>();
#else
var appLifetime = _host.Services.GetRequiredService<IApplicationLifetime>();
#endif
appLifetime.ApplicationStarted.Register(() =>
{
var addresses = _host.ServerFeatures
.Get<Microsoft.AspNetCore.Hosting.Server.Features.IServerAddressesFeature>()
.Get<Microsoft.AspNetCore.Hosting.Server.Features.IServerAddressesFeature>()!
.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);
}

View File

@@ -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; }

View File

@@ -14,6 +14,8 @@ internal class HostUrlOptions
public HostingScheme HostingScheme { get; set; }
public bool? UseHttp2 { get; set; }
public IReadOnlyList<HostUrlDetails> GetDetails()
{
var list = new List<HostUrlDetails>();
@@ -23,25 +25,25 @@ internal class HostUrlOptions
{
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 });
}
}
}

View File

@@ -5,18 +5,17 @@ using IResponse = Microsoft.Owin.IOwinResponse;
using IResponse = Microsoft.AspNetCore.Http.HttpResponse;
#endif
namespace WireMock.Owin.Mappers
namespace WireMock.Owin.Mappers;
/// <summary>
/// IOwinResponseMapper
/// </summary>
internal interface IOwinResponseMapper
{
/// <summary>
/// IOwinResponseMapper
/// Map ResponseMessage to IResponse.
/// </summary>
internal interface IOwinResponseMapper
{
/// <summary>
/// Map ResponseMessage to IResponse.
/// </summary>
/// <param name="responseMessage">The ResponseMessage</param>
/// <param name="response">The OwinResponse/HttpResponse</param>
Task MapAsync(IResponseMessage? responseMessage, IResponse response);
}
}
/// <param name="responseMessage">The ResponseMessage</param>
/// <param name="response">The OwinResponse/HttpResponse</param>
Task MapAsync(IResponseMessage? responseMessage, IResponse response);
}

View File

@@ -8,103 +8,102 @@ using WireMock.Util;
#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;
/// <summary>
/// OwinRequestMapper
/// </summary>
internal class OwinRequestMapper : IOwinRequestMapper
{
/// <summary>
/// OwinRequestMapper
/// </summary>
internal class OwinRequestMapper : IOwinRequestMapper
/// <inheritdoc />
public async Task<RequestMessage> MapAsync(IRequest request, IWireMockMiddlewareOptions options)
{
/// <inheritdoc cref="IOwinRequestMapper.MapAsync"/>
public async Task<RequestMessage> MapAsync(IRequest request, IWireMockMiddlewareOptions options)
var (urlDetails, clientIP) = ParseRequest(request);
var method = request.Method;
var httpVersion = HttpVersionParser.Parse(request.Protocol);
var headers = new Dictionary<string, string[]>();
IEnumerable<string>? contentEncodingHeader = null;
foreach (var header in request.Headers)
{
var (urlDetails, clientIP) = ParseRequest(request);
headers.Add(header.Key, header.Value!);
string method = request.Method;
var headers = new Dictionary<string, string[]>();
IEnumerable<string>? contentEncodingHeader = null;
foreach (var header in request.Headers)
if (string.Equals(header.Key, HttpKnownHeaderNames.ContentEncoding, StringComparison.OrdinalIgnoreCase))
{
headers.Add(header.Key, header.Value!);
if (string.Equals(header.Key, HttpKnownHeaderNames.ContentEncoding, StringComparison.OrdinalIgnoreCase))
{
contentEncodingHeader = header.Value;
}
contentEncodingHeader = header.Value;
}
}
var cookies = new Dictionary<string, string>();
if (request.Cookies.Any())
var cookies = new Dictionary<string, string>();
if (request.Cookies.Any())
{
foreach (var cookie in request.Cookies)
{
cookies = new Dictionary<string, string>();
foreach (var cookie in request.Cookies)
{
cookies.Add(cookie.Key, cookie.Value);
}
cookies.Add(cookie.Key, cookie.Value);
}
}
IBodyData? body = null;
if (request.Body != null && BodyParser.ShouldParseBody(method, options.AllowBodyForAllHttpMethods == true))
IBodyData? body = null;
if (request.Body != null && BodyParser.ShouldParseBody(method, options.AllowBodyForAllHttpMethods == true))
{
var bodyParserSettings = new BodyParserSettings
{
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);
}
return new RequestMessage(
options,
urlDetails,
method,
clientIP,
body,
headers,
cookies
#if USE_ASPNETCORE
, await request.HttpContext.Connection.GetClientCertificateAsync()
#endif
)
{
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)
{
#if !USE_ASPNETCORE
var urlDetails = UrlUtils.Parse(request.Uri, request.PathBase);
var clientIP = request.RemoteIpAddress;
#else
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();
}
return new RequestMessage(
options,
urlDetails,
method,
clientIP,
body,
headers,
cookies,
httpVersion
#if USE_ASPNETCORE
, await request.HttpContext.Connection.GetClientCertificateAsync()
#endif
return (urlDetails, clientIP);
)
{
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;
#else
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);
}
}

View File

@@ -13,6 +13,8 @@ using WireMock.Http;
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 @@ namespace WireMock.Owin.Mappers
switch (responseMessage.FaultType)
{
case FaultType.EMPTY_RESPONSE:
bytes = IsFault(responseMessage) ? EmptyArray<byte>.Value : GetNormalBody(responseMessage);
bytes = IsFault(responseMessage) ? EmptyArray<byte>.Value : await GetNormalBodyAsync(responseMessage).ConfigureAwait(false);
break;
case FaultType.MALFORMED_RESPONSE_CHUNK:
bytes = GetNormalBody(responseMessage) ?? EmptyArray<byte>.Value;
bytes = await GetNormalBodyAsync(responseMessage).ConfigureAwait(false) ?? EmptyArray<byte>.Value;
if (IsFault(responseMessage))
{
bytes = bytes.Take(bytes.Length / 2).Union(_randomizerBytes.Generate()).ToArray();
@@ -74,18 +76,18 @@ namespace WireMock.Owin.Mappers
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 @@ namespace WireMock.Owin.Mappers
_options.Logger.Warn("Error writing response body. Exception : {0}", ex);
}
}
SetResponseTrailingHeaders(responseMessage, response);
}
private int MapStatusCode(int code)
@@ -125,7 +129,7 @@ namespace WireMock.Owin.Mappers
return responseMessage.FaultPercentage == null || _randomizerDouble.Generate() <= responseMessage.FaultPercentage;
}
private byte[]? GetNormalBody(IResponseMessage responseMessage)
private async Task<byte[]?> GetNormalBodyAsync(IResponseMessage responseMessage)
{
switch (responseMessage.BodyData?.DetectedBodyType)
{
@@ -138,6 +142,12 @@ namespace WireMock.Owin.Mappers
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 @@ namespace WireMock.Owin.Mappers
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 @@ namespace WireMock.Owin.Mappers
}
}
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

View File

@@ -9,7 +9,7 @@ namespace WireMock.RequestBuilders;
/// <summary>
/// The BodyRequestBuilder interface.
/// </summary>
public interface IBodyRequestBuilder : IGraphQLRequestBuilder
public interface IBodyRequestBuilder : IProtoBufRequestBuilder
{
/// <summary>
/// WithBody: IMatcher

View File

@@ -26,6 +26,23 @@ public interface IGraphQLRequestBuilder : IMultiPartRequestBuilder
/// <returns>The <see cref="IRequestBuilder"/>.</returns>
IRequestBuilder WithGraphQLSchema(string schema, IDictionary<string, Type>? customScalars, MatchBehaviour matchBehaviour = MatchBehaviour.AcceptOnMatch);
/// <summary>
/// WithBodyAsGraphQL: The GraphQL schema as a string.
/// </summary>
/// <param name="schema">The GraphQL schema.</param>
/// <param name="matchBehaviour">The match behaviour. (Default is <c>MatchBehaviour.AcceptOnMatch</c>).</param>
/// <returns>The <see cref="IRequestBuilder"/>.</returns>
IRequestBuilder WithBodyAsGraphQL(string schema, MatchBehaviour matchBehaviour = MatchBehaviour.AcceptOnMatch);
/// <summary>
/// WithBodyAsGraphQL: The GraphQL schema as a string.
/// </summary>
/// <param name="schema">The GraphQL schema.</param>
/// <param name="customScalars">A dictionary defining the custom scalars used in this schema. (optional)</param>
/// <param name="matchBehaviour">The match behaviour. (Default is <c>MatchBehaviour.AcceptOnMatch</c>).</param>
/// <returns>The <see cref="IRequestBuilder"/>.</returns>
IRequestBuilder WithBodyAsGraphQL(string schema, IDictionary<string, Type>? customScalars, MatchBehaviour matchBehaviour = MatchBehaviour.AcceptOnMatch);
#if GRAPHQL
/// <summary>
/// WithGraphQLSchema: The GraphQL schema as a ISchema.
@@ -43,5 +60,22 @@ public interface IGraphQLRequestBuilder : IMultiPartRequestBuilder
/// <param name="matchBehaviour">The match behaviour. (Default is <c>MatchBehaviour.AcceptOnMatch</c>).</param>
/// <returns>The <see cref="IRequestBuilder"/>.</returns>
IRequestBuilder WithGraphQLSchema(GraphQL.Types.ISchema schema, IDictionary<string, Type>? customScalars, MatchBehaviour matchBehaviour = MatchBehaviour.AcceptOnMatch);
/// <summary>
/// WithBodyAsGraphQL: The GraphQL schema as a ISchema.
/// </summary>
/// <param name="schema">The GraphQL schema.</param>
/// <param name="matchBehaviour">The match behaviour. (Default is <c>MatchBehaviour.AcceptOnMatch</c>).</param>
/// <returns>The <see cref="IRequestBuilder"/>.</returns>
IRequestBuilder WithBodyAsGraphQL(GraphQL.Types.ISchema schema, MatchBehaviour matchBehaviour = MatchBehaviour.AcceptOnMatch);
/// <summary>
/// WithBodyAsGraphQL: The GraphQL schema as a ISchema.
/// </summary>
/// <param name="schema">The GraphQL schema.</param>
/// <param name="customScalars">A dictionary defining the custom scalars used in this schema. (optional)</param>
/// <param name="matchBehaviour">The match behaviour. (Default is <c>MatchBehaviour.AcceptOnMatch</c>).</param>
/// <returns>The <see cref="IRequestBuilder"/>.</returns>
IRequestBuilder WithBodyAsGraphQL(GraphQL.Types.ISchema schema, IDictionary<string, Type>? customScalars, MatchBehaviour matchBehaviour = MatchBehaviour.AcceptOnMatch);
#endif
}

View File

@@ -0,0 +1,18 @@
using WireMock.Matchers;
using WireMock.Matchers.Request;
namespace WireMock.RequestBuilders;
/// <summary>
/// The HttpVersionBuilder interface.
/// </summary>
public interface IHttpVersionBuilder : IRequestMatcher
{
/// <summary>
/// WithHttpVersion
/// </summary>
/// <param name="version">The HTTP Version to match.</param>
/// <param name="matchBehaviour">The match behaviour. (default = "AcceptOnMatch")</param>
/// <returns>The <see cref="IRequestBuilder"/>.</returns>
IRequestBuilder WithHttpVersion(string version, MatchBehaviour matchBehaviour = MatchBehaviour.AcceptOnMatch);
}

View File

@@ -6,7 +6,7 @@ namespace WireMock.RequestBuilders;
/// <summary>
/// The MultiPartRequestBuilder interface.
/// </summary>
public interface IMultiPartRequestBuilder : IRequestMatcher
public interface IMultiPartRequestBuilder : IHttpVersionBuilder
{
/// <summary>
/// WithMultiPart: IMatcher

View File

@@ -0,0 +1,45 @@
using WireMock.Matchers;
namespace WireMock.RequestBuilders;
/// <summary>
/// The ProtoBufRequestBuilder interface.
/// </summary>
public interface IProtoBufRequestBuilder : IGraphQLRequestBuilder
{
/// <summary>
/// WithGrpcProto
/// </summary>
/// <param name="protoDefinition">The proto definition as text.</param>
/// <param name="messageType">The full type of the protobuf (request/response) message object. Format is "{package-name}.{type-name}".</param>
/// <param name="matchBehaviour">The match behaviour. (default = "AcceptOnMatch")</param>
/// <returns>The <see cref="IRequestBuilder"/>.</returns>
IRequestBuilder WithBodyAsProtoBuf(string protoDefinition, string messageType, MatchBehaviour matchBehaviour = MatchBehaviour.AcceptOnMatch);
/// <summary>
/// WithGrpcProto
/// </summary>
/// <param name="protoDefinition">The proto definition as text.</param>
/// <param name="messageType">The full type of the protobuf (request/response) message object. Format is "{package-name}.{type-name}".</param>
/// <param name="matcher">The matcher to use to match the ProtoBuf as (json) object.</param>
/// <param name="matchBehaviour">The match behaviour. (default = "AcceptOnMatch")</param>
/// <returns>The <see cref="IRequestBuilder"/>.</returns>
IRequestBuilder WithBodyAsProtoBuf(string protoDefinition, string messageType, IObjectMatcher matcher, MatchBehaviour matchBehaviour = MatchBehaviour.AcceptOnMatch);
/// <summary>
/// WithGrpcProto
/// </summary>
/// <param name="messageType">The full type of the protobuf (request/response) message object. Format is "{package-name}.{type-name}".</param>
/// <param name="matchBehaviour">The match behaviour. (default = "AcceptOnMatch")</param>
/// <returns>The <see cref="IRequestBuilder"/>.</returns>
IRequestBuilder WithBodyAsProtoBuf(string messageType, MatchBehaviour matchBehaviour = MatchBehaviour.AcceptOnMatch);
/// <summary>
/// WithGrpcProto
/// </summary>
/// <param name="messageType">The full type of the protobuf (request/response) message object. Format is "{package-name}.{type-name}".</param>
/// <param name="matcher">The matcher to use to match the ProtoBuf as (json) object.</param>
/// <param name="matchBehaviour">The match behaviour. (default = "AcceptOnMatch")</param>
/// <returns>The <see cref="IRequestBuilder"/>.</returns>
IRequestBuilder WithBodyAsProtoBuf(string messageType, IObjectMatcher matcher, MatchBehaviour matchBehaviour = MatchBehaviour.AcceptOnMatch);
}

View File

@@ -0,0 +1,31 @@
using WireMock.Matchers;
using WireMock.Matchers.Request;
namespace WireMock.RequestBuilders;
public partial class Request
{
/// <inheritdoc />
public IRequestBuilder WithBodyAsProtoBuf(string protoDefinition, string messageType, MatchBehaviour matchBehaviour = MatchBehaviour.AcceptOnMatch)
{
return Add(new RequestMessageProtoBufMatcher(matchBehaviour, () => new (null, protoDefinition), messageType));
}
/// <inheritdoc />
public IRequestBuilder WithBodyAsProtoBuf(string protoDefinition, string messageType, IObjectMatcher matcher, MatchBehaviour matchBehaviour = MatchBehaviour.AcceptOnMatch)
{
return Add(new RequestMessageProtoBufMatcher(matchBehaviour, () => new(null, protoDefinition), messageType, matcher));
}
/// <inheritdoc />
public IRequestBuilder WithBodyAsProtoBuf(string messageType, MatchBehaviour matchBehaviour = MatchBehaviour.AcceptOnMatch)
{
return Add(new RequestMessageProtoBufMatcher(matchBehaviour, () => Mapping.ProtoDefinition!.Value, messageType));
}
/// <inheritdoc />
public IRequestBuilder WithBodyAsProtoBuf(string messageType, IObjectMatcher matcher, MatchBehaviour matchBehaviour = MatchBehaviour.AcceptOnMatch)
{
return Add(new RequestMessageProtoBufMatcher(matchBehaviour, () => Mapping.ProtoDefinition!.Value, messageType, matcher));
}
}

View File

@@ -1,39 +0,0 @@
using System.Collections.Generic;
using System;
using WireMock.Matchers;
using WireMock.Matchers.Request;
namespace WireMock.RequestBuilders;
public partial class Request
{
/// <inheritdoc />
public IRequestBuilder WithGraphQLSchema(string schema, MatchBehaviour matchBehaviour = MatchBehaviour.AcceptOnMatch)
{
_requestMatchers.Add(new RequestMessageGraphQLMatcher(matchBehaviour, schema));
return this;
}
/// <inheritdoc />
public IRequestBuilder WithGraphQLSchema(string schema, IDictionary<string, Type>? customScalars, MatchBehaviour matchBehaviour = MatchBehaviour.AcceptOnMatch)
{
_requestMatchers.Add(new RequestMessageGraphQLMatcher(matchBehaviour, schema, customScalars));
return this;
}
#if GRAPHQL
/// <inheritdoc />
public IRequestBuilder WithGraphQLSchema(GraphQL.Types.ISchema schema, MatchBehaviour matchBehaviour = MatchBehaviour.AcceptOnMatch)
{
_requestMatchers.Add(new RequestMessageGraphQLMatcher(matchBehaviour, schema));
return this;
}
/// <inheritdoc />
public IRequestBuilder WithGraphQLSchema(GraphQL.Types.ISchema schema, IDictionary<string, Type>? customScalars, MatchBehaviour matchBehaviour = MatchBehaviour.AcceptOnMatch)
{
_requestMatchers.Add(new RequestMessageGraphQLMatcher(matchBehaviour, schema, customScalars));
return this;
}
#endif
}

View File

@@ -0,0 +1,59 @@
using System.Collections.Generic;
using System;
using WireMock.Matchers;
using WireMock.Matchers.Request;
namespace WireMock.RequestBuilders;
public partial class Request
{
/// <inheritdoc />
public IRequestBuilder WithGraphQLSchema(string schema, MatchBehaviour matchBehaviour = MatchBehaviour.AcceptOnMatch)
{
return WithBodyAsGraphQL(schema, matchBehaviour);
}
/// <inheritdoc />
public IRequestBuilder WithGraphQLSchema(string schema, IDictionary<string, Type>? customScalars, MatchBehaviour matchBehaviour = MatchBehaviour.AcceptOnMatch)
{
return WithBodyAsGraphQL(schema, customScalars, matchBehaviour);
}
/// <inheritdoc />
public IRequestBuilder WithBodyAsGraphQL(string schema, MatchBehaviour matchBehaviour = MatchBehaviour.AcceptOnMatch)
{
return Add(new RequestMessageGraphQLMatcher(matchBehaviour, schema));
}
/// <inheritdoc />
public IRequestBuilder WithBodyAsGraphQL(string schema, IDictionary<string, Type>? customScalars, MatchBehaviour matchBehaviour = MatchBehaviour.AcceptOnMatch)
{
return Add(new RequestMessageGraphQLMatcher(matchBehaviour, schema, customScalars));
}
#if GRAPHQL
/// <inheritdoc />
public IRequestBuilder WithGraphQLSchema(GraphQL.Types.ISchema schema, MatchBehaviour matchBehaviour = MatchBehaviour.AcceptOnMatch)
{
return WithBodyAsGraphQL(schema, matchBehaviour);
}
/// <inheritdoc />
public IRequestBuilder WithGraphQLSchema(GraphQL.Types.ISchema schema, IDictionary<string, Type>? customScalars, MatchBehaviour matchBehaviour = MatchBehaviour.AcceptOnMatch)
{
return WithBodyAsGraphQL(schema, customScalars, matchBehaviour);
}
/// <inheritdoc />
public IRequestBuilder WithBodyAsGraphQL(GraphQL.Types.ISchema schema, MatchBehaviour matchBehaviour = MatchBehaviour.AcceptOnMatch)
{
return Add(new RequestMessageGraphQLMatcher(matchBehaviour, schema));
}
/// <inheritdoc />
public IRequestBuilder WithBodyAsGraphQL(GraphQL.Types.ISchema schema, IDictionary<string, Type>? customScalars, MatchBehaviour matchBehaviour = MatchBehaviour.AcceptOnMatch)
{
return Add(new RequestMessageGraphQLMatcher(matchBehaviour, schema, customScalars));
}
#endif
}

View File

@@ -0,0 +1,13 @@
using WireMock.Matchers;
using WireMock.Matchers.Request;
namespace WireMock.RequestBuilders;
public partial class Request
{
/// <inheritdoc />
public IRequestBuilder WithHttpVersion(string version, MatchBehaviour matchBehaviour = MatchBehaviour.AcceptOnMatch)
{
return Add(new RequestMessageHttpVersionMatcher(matchBehaviour, version));
}
}

View File

@@ -16,6 +16,11 @@ public partial class Request : RequestMessageCompositeMatcher, IRequestBuilder
{
private readonly IList<IRequestMatcher> _requestMatchers;
/// <summary>
/// The link back to the Mapping.
/// </summary>
public IMapping Mapping { get; set; } = null!;
/// <summary>
/// Creates this instance.
/// </summary>
@@ -63,4 +68,15 @@ public partial class Request : RequestMessageCompositeMatcher, IRequestBuilder
{
return _requestMatchers.OfType<T>().FirstOrDefault(func);
}
private IRequestBuilder Add<T>(T requestMatcher) where T : IRequestMatcher
{
foreach (var existing in _requestMatchers.OfType<T>().ToArray())
{
_requestMatchers.Remove(existing);
}
_requestMatchers.Add(requestMatcher);
return this;
}
}

View File

@@ -4,7 +4,6 @@ using System;
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
/// <inheritdoc />
public string Method { get; }
/// <inheritdoc />
public string HttpVersion { get; }
/// <inheritdoc />
public IDictionary<string, WireMockList<string>>? Headers { get; }
@@ -73,14 +75,14 @@ public class RequestMessage : IRequestMessage
public string? Body { get; }
/// <inheritdoc />
public object? BodyAsJson { get; }
public object? BodyAsJson { get; set; }
/// <inheritdoc />
public byte[]? BodyAsBytes { get; }
#if MIMEKIT
/// <inheritdoc />
[JsonIgnore] // Issue 1001
[Newtonsoft.Json.JsonIgnore] // Issue 1001
public object? BodyAsMimeMessage { get; }
#endif
@@ -125,11 +127,13 @@ public class RequestMessage : IRequestMessage
internal RequestMessage(
IWireMockMiddlewareOptions? options,
UrlDetails urlDetails, string method,
UrlDetails urlDetails,
string method,
string clientIP,
IBodyData? bodyData = null,
IDictionary<string, string[]>? headers = null,
IDictionary<string, string>? cookies = null
IDictionary<string, string>? cookies = null,
string httpVersion = "1.1"
#if USE_ASPNETCORE
, X509Certificate2? clientCertificate = null
#endif
@@ -152,6 +156,7 @@ public class RequestMessage : IRequestMessage
AbsolutePathSegments = AbsolutePath.Split('/').Skip(1).ToArray();
Method = method;
HttpVersion = httpVersion;
ClientIP = clientIP;
BodyData = bodyData;

View File

@@ -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 <see cref="IJsonConverter"/>).
/// </summary>
/// <param name="body">The body.</param>
/// <param name="converter">The JsonConverter.</param>
/// <param name="jsonConverter">The <see cref="IJsonConverter"/>.</param>
/// <param name="options">The <see cref="JsonConverterOptions"/> [optional].</param>
/// <returns>A <see cref="IResponseBuilder"/>.</returns>
IResponseBuilder WithBody(object body, IJsonConverter converter, JsonConverterOptions? options = null);
IResponseBuilder WithBody(object body, IJsonConverter jsonConverter, JsonConverterOptions? options = null);
/// <summary>
/// WithBody : Create a string response based on a object (which will be converted to a JSON string using the <see cref="IJsonConverter"/>).
/// </summary>
/// <param name="body">The body.</param>
/// <param name="encoding">The body encoding, can be <c>null</c>.</param>
/// <param name="converter">The JsonConverter.</param>
/// <param name="jsonConverter">The <see cref="IJsonConverter"/>.</param>
/// <param name="options">The <see cref="JsonConverterOptions"/> [optional].</param>
/// <returns>A <see cref="IResponseBuilder"/>.</returns>
IResponseBuilder WithBody(object body, Encoding? encoding, IJsonConverter converter, JsonConverterOptions? options = null);
IResponseBuilder WithBody(object body, Encoding? encoding, IJsonConverter jsonConverter, JsonConverterOptions? options = null);
/// <summary>
/// WithBody : Create a ProtoBuf byte[] response based on a proto definition, message type and the value.
/// </summary>
/// <param name="protoDefinition">The proto definition as text.</param>
/// <param name="messageType">The full type of the protobuf (request/response) message object. Format is "{package-name}.{type-name}".</param>
/// <param name="value">The object to convert to protobuf byte[].</param>
/// <param name="jsonConverter">The <see cref="IJsonConverter"/> [optional]. Default value is NewtonsoftJsonConverter.</param>
/// <param name="options">The <see cref="JsonConverterOptions"/> [optional].</param>
/// <returns>A <see cref="IResponseBuilder"/>.</returns>
IResponseBuilder WithBodyAsProtoBuf(
string protoDefinition,
string messageType,
object value,
IJsonConverter? jsonConverter = null,
JsonConverterOptions? options = null
);
/// <summary>
/// WithBody : Create a ProtoBuf byte[] response based on a proto definition, message type and the value.
/// </summary>
/// <param name="messageType">The full type of the protobuf (request/response) message object. Format is "{package-name}.{type-name}".</param>
/// <param name="value">The object to convert to protobuf byte[].</param>
/// <param name="jsonConverter">The <see cref="IJsonConverter"/> [optional]. Default value is NewtonsoftJsonConverter.</param>
/// <param name="options">The <see cref="JsonConverterOptions"/> [optional].</param>
/// <returns>A <see cref="IResponseBuilder"/>.</returns>
IResponseBuilder WithBodyAsProtoBuf(
string messageType,
object value,
IJsonConverter? jsonConverter = null,
JsonConverterOptions? options = null
);
}

View File

@@ -9,7 +9,7 @@ namespace WireMock.ResponseBuilders;
public interface IHeadersResponseBuilder : IBodyResponseBuilder
{
/// <summary>
/// The with header.
/// The WithHeader.
/// </summary>
/// <param name="name">The name.</param>
/// <param name="values">The values.</param>
@@ -17,23 +17,52 @@ public interface IHeadersResponseBuilder : IBodyResponseBuilder
IResponseBuilder WithHeader(string name, params string[] values);
/// <summary>
/// The with headers.
/// The WithHeaders.
/// </summary>
/// <param name="headers">The headers.</param>
/// <returns>The <see cref="IResponseBuilder"/>.</returns>
IResponseBuilder WithHeaders(IDictionary<string, string> headers);
/// <summary>
/// The with headers.
/// The WithHeaders.
/// </summary>
/// <param name="headers">The headers.</param>
/// <returns>The <see cref="IResponseBuilder"/>.</returns>
IResponseBuilder WithHeaders(IDictionary<string, string[]> headers);
/// <summary>
/// The with headers.
/// The WithHeaders.
/// </summary>
/// <param name="headers">The headers.</param>
/// <returns>The <see cref="IResponseBuilder"/>.</returns>
IResponseBuilder WithHeaders(IDictionary<string, WireMockList<string>> headers);
/// <summary>
/// The WithTrailingHeader.
/// </summary>
/// <param name="name">The name.</param>
/// <param name="values">The values.</param>
/// <returns>The <see cref="IResponseBuilder"/>.</returns>
IResponseBuilder WithTrailingHeader(string name, params string[] values);
/// <summary>
/// The WithTrailingHeaders.
/// </summary>
/// <param name="headers">The headers.</param>
/// <returns>The <see cref="IResponseBuilder"/>.</returns>
IResponseBuilder WithTrailingHeaders(IDictionary<string, string> headers);
/// <summary>
/// The WithTrailingHeaders.
/// </summary>
/// <param name="headers">The headers.</param>
/// <returns>The <see cref="IResponseBuilder"/>.</returns>
IResponseBuilder WithTrailingHeaders(IDictionary<string, string[]> headers);
/// <summary>
/// The WithTrailingHeaders.
/// </summary>
/// <param name="headers">The headers.</param>
/// <returns>The <see cref="IResponseBuilder"/>.</returns>
IResponseBuilder WithTrailingHeaders(IDictionary<string, WireMockList<string>> headers);
}

View File

@@ -3,6 +3,7 @@ using System.Text;
using System.Threading.Tasks;
using JsonConverter.Abstractions;
using Stef.Validation;
using WireMock.Exceptions;
using WireMock.Types;
using WireMock.Util;
@@ -185,25 +186,79 @@ public partial class Response
}
/// <inheritdoc />
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);
}
/// <inheritdoc />
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;
}
/// <inheritdoc />
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;
}
/// <inheritdoc />
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;
}
}

View File

@@ -0,0 +1,101 @@
using System.Collections.Generic;
using System.Linq;
using Stef.Validation;
using WireMock.Types;
namespace WireMock.ResponseBuilders;
public partial class Response
{
/// <inheritdoc />
public IResponseBuilder WithHeader(string name, params string[] values)
{
Guard.NotNull(name);
ResponseMessage.AddHeader(name, values);
return this;
}
/// <inheritdoc />
public IResponseBuilder WithHeaders(IDictionary<string, string> headers)
{
Guard.NotNull(headers);
ResponseMessage.Headers = headers.ToDictionary(header => header.Key, header => new WireMockList<string>(header.Value));
return this;
}
/// <inheritdoc />
public IResponseBuilder WithHeaders(IDictionary<string, string[]> headers)
{
Guard.NotNull(headers);
ResponseMessage.Headers = headers.ToDictionary(header => header.Key, header => new WireMockList<string>(header.Value));
return this;
}
/// <inheritdoc />
public IResponseBuilder WithHeaders(IDictionary<string, WireMockList<string>> headers)
{
Guard.NotNull(headers);
ResponseMessage.Headers = headers;
return this;
}
/// <inheritdoc />
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
}
/// <inheritdoc />
public IResponseBuilder WithTrailingHeaders(IDictionary<string, string> 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<string>(header.Value));
return this;
#endif
}
/// <inheritdoc />
public IResponseBuilder WithTrailingHeaders(IDictionary<string, string[]> 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<string>(header.Value));
return this;
#endif
}
/// <inheritdoc />
public IResponseBuilder WithTrailingHeaders(IDictionary<string, WireMockList<string>> 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
}
}

View File

@@ -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;
/// <summary>
/// The link back to the mapping.
/// </summary>
public IMapping Mapping { get; set; } = null!;
/// <summary>
/// The minimum random delay in milliseconds.
/// </summary>
@@ -112,7 +118,7 @@ public partial class Response : IResponseBuilder
{
ResponseMessage = responseMessage;
}
/// <inheritdoc cref="IStatusCodeResponseBuilder.WithStatusCode(int)"/>
[PublicAPI]
public IResponseBuilder WithStatusCode(int code)
@@ -156,42 +162,6 @@ public partial class Response : IResponseBuilder
return WithStatusCode((int)HttpStatusCode.NotFound);
}
/// <inheritdoc cref="IHeadersResponseBuilder.WithHeader(string, string[])"/>
public IResponseBuilder WithHeader(string name, params string[] values)
{
Guard.NotNull(name);
ResponseMessage.AddHeader(name, values);
return this;
}
/// <inheritdoc cref="IHeadersResponseBuilder.WithHeaders(IDictionary{string, string})"/>
public IResponseBuilder WithHeaders(IDictionary<string, string> headers)
{
Guard.NotNull(headers);
ResponseMessage.Headers = headers.ToDictionary(header => header.Key, header => new WireMockList<string>(header.Value));
return this;
}
/// <inheritdoc cref="IHeadersResponseBuilder.WithHeaders(IDictionary{string, string[]})"/>
public IResponseBuilder WithHeaders(IDictionary<string, string[]> headers)
{
Guard.NotNull(headers);
ResponseMessage.Headers = headers.ToDictionary(header => header.Key, header => new WireMockList<string>(header.Value));
return this;
}
/// <inheritdoc cref="IHeadersResponseBuilder.WithHeaders(IDictionary{string, WireMockList{string}})"/>
public IResponseBuilder WithHeaders(IDictionary<string, WireMockList<string>> headers)
{
Guard.NotNull(headers);
ResponseMessage.Headers = headers;
return this;
}
/// <inheritdoc cref="ITransformResponseBuilder.WithTransformer(bool)"/>
public IResponseBuilder WithTransformer(bool transformContentFromBodyAsFile)
{
@@ -304,10 +274,30 @@ public partial class Response : IResponseBuilder
{
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<RequestMessageProtoBufMatcher>()?.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)
{

View File

@@ -14,9 +14,12 @@ namespace WireMock;
/// </summary>
public class ResponseMessage : IResponseMessage
{
/// <inheritdoc cref="IResponseMessage.Headers" />
/// <inheritdoc />
public IDictionary<string, WireMockList<string>>? Headers { get; set; } = new Dictionary<string, WireMockList<string>>();
/// <inheritdoc />
public IDictionary<string, WireMockList<string>>? TrailingHeaders { get; set; } = new Dictionary<string, WireMockList<string>>();
/// <inheritdoc cref="IResponseMessage.StatusCode" />
public object? StatusCode { get; set; }
@@ -35,23 +38,43 @@ public class ResponseMessage : IResponseMessage
/// <inheritdoc cref="IResponseMessage.FaultPercentage" />
public double? FaultPercentage { get; set; }
/// <inheritdoc cref="IResponseMessage.AddHeader(string, string)" />
/// <inheritdoc />
public void AddHeader(string name, string value)
{
Headers ??= new Dictionary<string, WireMockList<string>>();
Headers.Add(name, value);
}
/// <inheritdoc cref="IResponseMessage.AddHeader(string, string[])" />
/// <inheritdoc />
public void AddHeader(string name, params string[] values)
{
Guard.NotNullOrEmpty(values);
Headers ??= new Dictionary<string, WireMockList<string>>();
var newHeaderValues = Headers.TryGetValue(name, out WireMockList<string>? existingValues)
var newHeaderValues = Headers.TryGetValue(name, out var existingValues)
? values.Union(existingValues).ToArray()
: values;
Headers[name] = newHeaderValues;
}
/// <inheritdoc />
public void AddTrailingHeader(string name, string value)
{
TrailingHeaders ??= new Dictionary<string, WireMockList<string>>();
TrailingHeaders.Add(name, value);
}
/// <inheritdoc />
public void AddTrailingHeader(string name, params string[] values)
{
Guard.NotNullOrEmpty(values);
TrailingHeaders ??= new Dictionary<string, WireMockList<string>>();
var newHeaderValues = TrailingHeaders.TryGetValue(name, out var existingValues)
? values.Union(existingValues).ToArray()
: values;
TrailingHeaders[name] = newHeaderValues;
}
}

View File

@@ -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<IRequestMessage, Task<IResponseMessage>> responseMessageFunc)
{
_responseMessageFunc = responseMessageFunc;
_responseMessageFunc = Guard.NotNull(responseMessageFunc);
}
public async Task<(IResponseMessage Message, IMapping? Mapping)> ProvideResponseAsync(IMapping mapping, IRequestMessage requestMessage, WireMockServerSettings settings)

View File

@@ -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<IRequestMessage, IResponseMessage> responseMessageFunc)
{
_responseMessageFunc = responseMessageFunc;
_responseMessageFunc = Guard.NotNull(responseMessageFunc);
}
public Task<(IResponseMessage Message, IMapping? Mapping)> ProvideResponseAsync(IMapping mapping, IRequestMessage requestMessage, WireMockServerSettings settings)

View File

@@ -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<IRequestMessage, WireMockServerSettings, Task<IResponseMessage>> 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)

View File

@@ -32,6 +32,7 @@ internal class LogEntryMapper
ProxyUrl = logEntry.RequestMessage.ProxyUrl,
Query = logEntry.RequestMessage.Query,
Method = logEntry.RequestMessage.Method,
HttpVersion = logEntry.RequestMessage.HttpVersion,
Headers = logEntry.RequestMessage.Headers,
Cookies = logEntry.RequestMessage.Cookies
};

View File

@@ -14,7 +14,6 @@ using WireMock.Matchers.Request;
using WireMock.Models;
using WireMock.RequestBuilders;
using WireMock.ResponseBuilders;
using WireMock.Settings;
using WireMock.Types;
using WireMock.Util;
@@ -48,8 +47,10 @@ internal class MappingConverter
var paramsMatchers = request.GetRequestMessageMatchers<RequestMessageParamMatcher>();
var methodMatcher = request.GetRequestMessageMatcher<RequestMessageMethodMatcher>();
var requestMessageBodyMatcher = request.GetRequestMessageMatcher<RequestMessageBodyMatcher>();
var requestMessageHttpVersionMatcher = request.GetRequestMessageMatcher<RequestMessageHttpVersionMatcher>();
var requestMessageGraphQLMatcher = request.GetRequestMessageMatcher<RequestMessageGraphQLMatcher>();
var requestMessageMultiPartMatcher = request.GetRequestMessageMatcher<RequestMessageMultiPartMatcher>();
var requestMessageProtoBufMatcher = request.GetRequestMessageMatcher<RequestMessageProtoBufMatcher>();
var sb = new StringBuilder();
@@ -108,6 +109,11 @@ internal class MappingConverter
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 @@ internal class MappingConverter
}
#endif
#if PROTOBUF
if (requestMessageProtoBufMatcher is { Matcher: { } })
{
sb.AppendLine(" // .WithBodyAsProtoBuf() is not yet supported");
}
#endif
if (requestMessageBodyMatcher is { Matchers: { } })
{
if (requestMessageBodyMatcher.Matchers.OfType<WildcardMatcher>().FirstOrDefault() is { } wildcardMatcher && wildcardMatcher.GetPatterns().Any())
@@ -182,6 +195,14 @@ internal class MappingConverter
}
}
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 @@ internal class MappingConverter
case BodyType.FormUrlEncoded:
sb.AppendLine($" .WithBody({ToCSharpStringLiteral(bodyData.BodyAsString)})");
break;
case BodyType.Json:
if (bodyData.BodyAsJson is string bodyStringValue)
{
@@ -239,6 +261,8 @@ internal class MappingConverter
var bodyMatcher = request.GetRequestMessageMatcher<RequestMessageBodyMatcher>();
var graphQLMatcher = request.GetRequestMessageMatcher<RequestMessageGraphQLMatcher>();
var multiPartMatcher = request.GetRequestMessageMatcher<RequestMessageMultiPartMatcher>();
var protoBufMatcher = request.GetRequestMessageMatcher<RequestMessageProtoBufMatcher>();
var httpVersionMatcher = request.GetRequestMessageMatcher<RequestMessageHttpVersionMatcher>();
var mappingModel = new MappingModel
{
@@ -253,6 +277,7 @@ internal class MappingConverter
WhenStateIs = mapping.ExecutionConditionState,
SetStateTo = mapping.NextState,
Data = mapping.Data,
ProtoDefinition = mapping.ProtoDefinition?.Value,
Probability = mapping.Probability,
Request = new RequestModel
{
@@ -290,6 +315,11 @@ internal class MappingConverter
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 @@ internal class MappingConverter
mappingModel.Response.Delay = (int?)(response.Delay == Timeout.InfiniteTimeSpan ? TimeSpan.MaxValue.TotalMilliseconds : response.Delay?.TotalMilliseconds);
}
var nonNullableWebHooks = mapping.Webhooks?.Where(wh => wh != null).ToArray() ?? EmptyArray<IWebhook>.Value;
var nonNullableWebHooks = mapping.Webhooks?.ToArray() ?? EmptyArray<IWebhook>.Value;
if (nonNullableWebHooks.Length == 1)
{
mappingModel.Webhook = WebhookMapper.Map(nonNullableWebHooks[0]);
@@ -339,20 +369,40 @@ internal class MappingConverter
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<ProtoBufMatcher>().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 @@ internal class MappingConverter
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 @@ internal class MappingConverter
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 @@ internal class MappingConverter
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();

View File

@@ -25,29 +25,25 @@ internal class MatcherMapper
public IMatcher[]? Map(IEnumerable<MatcherModel>? matchers)
{
if (matchers == null)
{
return null;
}
return matchers.Select(Map).Where(m => m != null).ToArray()!;
return matchers?.Select(Map).OfType<IMatcher>().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 @@ internal class MatcherMapper
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 @@ internal class MatcherMapper
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 @@ internal class MatcherMapper
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<IMatcher>? matchers)
public MatcherModel[]? Map(IEnumerable<IMatcher>? matchers, Action<MatcherModel>? afterMap = null)
{
return matchers?.Where(m => m != null).Select(Map).ToArray();
return matchers?.Select(m => Map(m, afterMap)).OfType<MatcherModel>().ToArray();
}
public MatcherModel? Map(IMatcher? matcher)
public MatcherModel? Map(IMatcher? matcher, Action<MatcherModel>? afterMap = null)
{
if (matcher == null)
{
@@ -194,14 +195,9 @@ internal class MatcherMapper
}
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 @@ internal class MatcherMapper
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 @@ internal class MatcherMapper
}
#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 @@ internal class MatcherMapper
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
}

View File

@@ -41,6 +41,7 @@ internal class ProxyMappingConverter
var paramMatchers = request?.GetRequestMessageMatchers<RequestMessageParamMatcher>();
var methodMatcher = request?.GetRequestMessageMatcher<RequestMessageMethodMatcher>();
var bodyMatcher = request?.GetRequestMessageMatcher<RequestMessageBodyMatcher>();
var httpVersionMatcher = request?.GetRequestMessageMatcher<RequestMessageHttpVersionMatcher>();
var newRequest = Request.Create();
@@ -70,6 +71,16 @@ internal class ProxyMappingConverter
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 @@ internal class ProxyMappingConverter
webhooks: null,
useWebhooksFireAndForget: null,
timeSettings: null,
data: mapping?.Data,
probability: null
data: mapping?.Data
);
}
}

View File

@@ -184,4 +184,21 @@ public interface IRespondWithAProvider
/// <param name="probability">The probability when this request should be matched. Value is between 0 and 1.</param>
/// <returns>The <see cref="IRespondWithAProvider"/>.</returns>
IRespondWithAProvider WithProbability(double probability);
/// <summary>
/// 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.
/// </summary>
/// <param name="protoDefinitionOrId">The proto definition as text or as id.</param>
/// <returns>The <see cref="IRespondWithAProvider"/>.</returns>
IRespondWithAProvider WithProtoDefinition(string protoDefinitionOrId);
/// <summary>
/// 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.
/// </summary>
/// <param name="graphQLSchemaOrId">The GraphQL Schema as text or as id.</param>
/// <param name="customScalars">A dictionary defining the custom scalars used in this schema. [optional]</param>
/// <returns>The <see cref="IRespondWithAProvider"/>.</returns>
IRespondWithAProvider WithGraphQLSchema(string graphQLSchemaOrId, IDictionary<string, Type>? customScalars = null);
}

View File

@@ -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 @@ internal class RespondWithAProvider : IRespondWithAProvider
/// <param name="provider">The provider.</param>
public void RespondWith(IResponseProvider provider)
{
var mapping = new Mapping(
var mapping = new Mapping
(
Guid,
_dateTimeUtils.UtcNow,
_title,
@@ -93,14 +95,23 @@ internal class RespondWithAProvider : IRespondWithAProvider
Webhooks,
_useWebhookFireAndForget,
TimeSettings,
Data,
_probability);
Data
);
if (_probability != null)
{
mapping.WithProbability(_probability.Value);
}
if (_protoDefinition != null)
{
mapping.WithProtoDefinition(_protoDefinition.Value);
}
_registrationCallback(mapping, _saveToFile);
}
/// <inheritdoc />
[PublicAPI]
public IRespondWithAProvider WithData(object data)
{
Data = data;
@@ -117,7 +128,6 @@ internal class RespondWithAProvider : IRespondWithAProvider
public IRespondWithAProvider WithGuid(Guid guid)
{
Guid = guid;
return this;
}
@@ -133,7 +143,6 @@ internal class RespondWithAProvider : IRespondWithAProvider
public IRespondWithAProvider WithDescription(string description)
{
_description = description;
return this;
}
@@ -141,7 +150,6 @@ internal class RespondWithAProvider : IRespondWithAProvider
public IRespondWithAProvider WithPath(string path)
{
_path = path;
return this;
}
@@ -149,15 +157,13 @@ internal class RespondWithAProvider : IRespondWithAProvider
public IRespondWithAProvider AtPriority(int priority)
{
_priority = priority;
return this;
}
/// <inheritdoc />
public IRespondWithAProvider InScenario(string scenario)
{
_scenario = scenario;
_scenario = Guard.NotNullOrWhiteSpace(scenario);
return this;
}
@@ -209,9 +215,7 @@ internal class RespondWithAProvider : IRespondWithAProvider
/// <inheritdoc />
public IRespondWithAProvider WithTimeSettings(ITimeSettings timeSettings)
{
Guard.NotNull(timeSettings, nameof(timeSettings));
TimeSettings = timeSettings;
TimeSettings = Guard.NotNull(timeSettings);
return this;
}
@@ -219,10 +223,9 @@ internal class RespondWithAProvider : IRespondWithAProvider
/// <inheritdoc />
public IRespondWithAProvider WithWebhook(params IWebhook[] webhooks)
{
Guard.HasNoNulls(webhooks, nameof(webhooks));
Guard.HasNoNulls(webhooks);
Webhooks = webhooks;
return this;
}
@@ -283,13 +286,45 @@ internal class RespondWithAProvider : IRespondWithAProvider
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;
}
/// <inheritdoc />
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;
}
/// <inheritdoc />
public IRespondWithAProvider WithGraphQLSchema(string graphQLSchemaOrId, IDictionary<string, Type>? 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 @@ internal class RespondWithAProvider : IRespondWithAProvider
string method,
IDictionary<string, WireMockList<string>>? headers,
bool useTransformer,
TransformerType transformerType)
TransformerType transformerType
)
{
return new Webhook
{

View File

@@ -231,9 +231,11 @@ public partial class WireMockServer
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 @@ public partial class WireMockServer
_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;

View File

@@ -195,6 +195,11 @@ public partial class WireMockServer
requestBuilder = requestBuilder.UsingMethod(matchBehaviour, matchOperator, requestModel.Methods);
}
if (requestModel.HttpVersion != null)
{
requestBuilder = requestBuilder.WithHttpVersion(requestModel.HttpVersion);
}
if (requestModel.Headers != null)
{
foreach (var headerModel in requestModel.Headers.Where(h => h.Matchers != null))

View File

@@ -7,6 +7,7 @@ using System.Linq;
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.Handlers;
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 partial class WireMockServer : IWireMockServer
return new WireMockServer(settings);
}
/// <summary>
/// Starts this WireMockServer with the specified settings.
/// </summary>
/// <param name="action">The action to configure the WireMockServerSettings.</param>
/// <returns>The <see cref="WireMockServer"/>.</returns>
[PublicAPI]
public static WireMockServer Start(Action<WireMockServerSettings> action)
{
Guard.NotNull(action);
var settings = new WireMockServerSettings();
action(settings);
return new WireMockServer(settings);
}
/// <summary>
/// Start this WireMockServer.
/// </summary>
/// <param name="port">The port.</param>
/// <param name="ssl">The SSL support.</param>
/// <param name="useSSL">The SSL support.</param>
/// <param name="useHttp2">Use HTTP 2 (needed for Grpc).</param>
/// <returns>The <see cref="WireMockServer"/>.</returns>
[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 partial class WireMockServer : IWireMockServer
/// Start this WireMockServer with the admin interface.
/// </summary>
/// <param name="port">The port.</param>
/// <param name="ssl">The SSL support.</param>
/// <param name="useSSL">The SSL support.</param>
/// <param name="useHttp2">Use HTTP 2 (needed for Grpc).</param>
/// <returns>The <see cref="WireMockServer"/>.</returns>
[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 partial class WireMockServer : IWireMockServer
[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 @@ public partial class WireMockServer : IWireMockServer
urlOptions = new HostUrlOptions
{
HostingScheme = settings.HostingScheme.Value,
UseHttp2 = settings.UseHttp2,
Port = settings.Port
};
}
@@ -337,6 +361,7 @@ public partial class WireMockServer : IWireMockServer
urlOptions = new HostUrlOptions
{
HostingScheme = settings.UseSSL == true ? HostingScheme.Https : HostingScheme.Http,
UseHttp2 = settings.UseHttp2,
Port = settings.Port
};
}
@@ -573,6 +598,49 @@ public partial class WireMockServer : IWireMockServer
return _mappingBuilder.Given(requestMatcher, saveToFile);
}
/// <summary>
/// Add a Grpc ProtoDefinition at server-level.
/// </summary>
/// <param name="id">Unique identifier for the ProtoDefinition.</param>
/// <param name="protoDefinition">The ProtoDefinition as text.</param>
/// <returns><see cref="WireMockServer"/></returns>
[PublicAPI]
public WireMockServer AddProtoDefinition(string id, string protoDefinition)
{
Guard.NotNullOrWhiteSpace(id);
Guard.NotNullOrWhiteSpace(protoDefinition);
_settings.ProtoDefinitions ??= new Dictionary<string, string>();
_settings.ProtoDefinitions[id] = protoDefinition;
return this;
}
/// <summary>
/// Add a GraphQL Schema at server-level.
/// </summary>
/// <param name="id">Unique identifier for the GraphQL Schema.</param>
/// <param name="graphQLSchema">The GraphQL Schema as string or StringPattern.</param>
/// <param name="customScalars">A dictionary defining the custom scalars used in this schema. [optional]</param>
/// <returns><see cref="WireMockServer"/></returns>
[PublicAPI]
public WireMockServer AddGraphQLSchema(string id, AnyOf<string, StringPattern> graphQLSchema, Dictionary<string, Type>? customScalars = null)
{
Guard.NotNullOrWhiteSpace(id);
Guard.NotNullOrWhiteSpace(graphQLSchema);
_settings.GraphQLSchemas ??= new Dictionary<string, GraphQLSchemaDetails>();
_settings.GraphQLSchemas[id] = new GraphQLSchemaDetails
{
SchemaAsString = graphQLSchema,
CustomScalars = customScalars
};
return this;
}
/// <inheritdoc />
[PublicAPI]
public string? MappingToCSharpCode(Guid guid, MappingConverterType converterType)

View File

@@ -3,6 +3,7 @@ using System.Collections;
using System.Collections.Generic;
using System.Linq;
using WireMock.Extensions;
using WireMock.Util;
namespace WireMock.Settings;
@@ -148,4 +149,10 @@ internal class SimpleSettingsParser
{
return GetValue(name, values => values.FirstOrDefault());
}
public T? GetObjectValueFromJson<T>(string name)
{
var value = GetValue(name, values => values.FirstOrDefault());
return string.IsNullOrWhiteSpace(value) ? default : JsonUtils.DeserializeObject<T>(value);
}
}

View File

@@ -11,6 +11,7 @@ using WireMock.Matchers;
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; }
/// <summary>
/// Gets or sets to use HTTP 2 (used for Grpc).
/// </summary>
[PublicAPI]
public bool? UseHttp2 { get; set; }
/// <summary>
/// Gets or sets whether to start admin interface.
/// </summary>
@@ -301,4 +308,16 @@ public class WireMockServerSettings
/// </summary>
[JsonIgnore]
public CultureInfo Culture { get; set; } = CultureInfo.CurrentCulture;
/// <summary>
/// A list of Grpc ProtoDefinitions which can be used.
/// </summary>
[PublicAPI]
public Dictionary<string, string>? ProtoDefinitions { get; set; }
/// <summary>
/// A list of GraphQL Schemas which can be used.
/// </summary>
[PublicAPI]
public Dictionary<string, GraphQLSchemaDetails>? GraphQLSchemas { get; set; }
}

View File

@@ -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 class WireMockServerSettingsParser
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<Dictionary<string, GraphQLSchemaDetails>>(nameof(settings.GraphQLSchemas)),
HandleRequestsSynchronously = parser.GetBoolValue(nameof(WireMockServerSettings.HandleRequestsSynchronously)),
HostingScheme = parser.GetEnumValue<HostingScheme>(nameof(WireMockServerSettings.HostingScheme)),
MaxRequestLogCount = parser.GetIntValue(nameof(WireMockServerSettings.MaxRequestLogCount)),
ProtoDefinitions = parser.GetObjectValueFromJson<Dictionary<string, string>>(nameof(settings.ProtoDefinitions)),
QueryParameterMultipleValueSupport = parser.GetEnumValue<QueryParameterMultipleValueSupport>(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<HostingScheme>(nameof(WireMockServerSettings.HostingScheme)),
DoNotSaveDynamicResponseInLogEntry = parser.GetBoolValue(nameof(WireMockServerSettings.DoNotSaveDynamicResponseInLogEntry)),
QueryParameterMultipleValueSupport = parser.GetEnumValue<QueryParameterMultipleValueSupport>(nameof(WireMockServerSettings.QueryParameterMultipleValueSupport)),
Culture = parser.GetValue(nameof(WireMockServerSettings.Culture), strings => CultureInfoUtils.Parse(strings.FirstOrDefault()), CultureInfo.CurrentCulture)
};
#if USE_ASPNETCORE
@@ -98,7 +103,6 @@ public static class WireMockServerSettingsParser
{
settings.Logger = logger;
}
break;
}
}

View File

@@ -92,17 +92,14 @@ internal class Transformer : ITransformer
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 @@ internal class Transformer : ITransformer
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 @@ internal class Transformer : ITransformer
Encoding = original.Encoding,
DetectedBodyType = original.DetectedBodyType,
DetectedBodyTypeFromContentType = original.DetectedBodyTypeFromContentType,
ProtoDefinition = original.ProtoDefinition,
ProtoBufMessageType = original.ProtoBufMessageType,
BodyAsJson = jToken
};
}

View File

@@ -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 @@ internal static class BodyParser
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 @@ internal static class BodyParser
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;

View File

@@ -0,0 +1,24 @@
using System;
using System.Text.RegularExpressions;
using Stef.Validation;
namespace WireMock.Util;
/// <summary>
/// https://en.wikipedia.org/wiki/HTTP
/// </summary>
internal static class HttpVersionParser
{
private static readonly Regex HttpVersionRegex = new(@"HTTP/(\d+(\.\d+)?(?!\.))", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant | RegexOptions.Compiled, TimeSpan.FromMilliseconds(100));
/// <summary>
/// Try to extract the version (as a string) from the protocol.
/// </summary>
/// <param name="protocol">The protocol, something like "HTTP/1.1" or "HTTP/2".</param>
/// <returns>The version ("1.1" or "2") if found and valid, else empty string.</returns>
internal static string Parse(string protocol)
{
var match = HttpVersionRegex.Match(Guard.NotNull(protocol));
return match.Success ? match.Groups[1].Value : string.Empty;
}
}

View File

@@ -1,4 +1,5 @@
using System;
using System.Collections;
using System.Diagnostics.CodeAnalysis;
using System.Text;
using Newtonsoft.Json;
@@ -106,4 +107,23 @@ internal static class JsonUtils
_ => 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);
}
}
}

View File

@@ -34,11 +34,12 @@ internal static class PortUtils
}
/// <summary>
/// Extract the if-isHttps, protocol, host and port from a URL.
/// Extract the isHttps, isHttp2, protocol, host and port from a URL.
/// </summary>
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 @@ internal static class PortUtils
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);

View File

@@ -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<byte[]> 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<byte>();
}
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<Converter>.GetInstance().ConvertAsync(request, cancellationToken).ConfigureAwait(false);
}
}
#endif

View File

@@ -0,0 +1,24 @@
namespace WireMock.Util;
internal static class SingletonLock
{
public static readonly object Lock = new();
}
internal static class SingletonFactory<T> where T : class, new()
{
private static T? _instance;
public static T GetInstance()
{
if (_instance == null)
{
lock (SingletonLock.Lock)
{
_instance ??= new T();
}
}
return _instance;
}
}

View File

@@ -51,17 +51,16 @@
</PropertyGroup>
<PropertyGroup Condition="'$(TargetFramework)' != 'netstandard1.3' and '$(TargetFramework)' != 'net451' and '$(TargetFramework)' != 'net452' and '$(TargetFramework)' != 'net46' and '$(TargetFramework)' != 'net461'">
<DefineConstants>$(DefineConstants);GRAPHQL;MIMEKIT</DefineConstants>
<DefineConstants>$(DefineConstants);GRAPHQL;MIMEKIT;PROTOBUF</DefineConstants>
</PropertyGroup>
<PropertyGroup Condition="'$(TargetFramework)' != 'netstandard1.3' and '$(TargetFramework)' != 'net451' and '$(TargetFramework)' != 'net452' and '$(TargetFramework)' != 'net46' ">
<DefineConstants>$(DefineConstants);TRAILINGHEADERS</DefineConstants>
</PropertyGroup>
<ItemGroup>
<Compile Remove="Matchers\MultiPartMatcher.cs" />
<Compile Remove="Util\FileSystemWatcherExtensions.cs" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="JetBrains.Annotations" Version="2023.3.0" PrivateAssets="All" />
<PackageReference Include="JsonConverter.Abstractions" Version="0.4.0" />
<PackageReference Include="JsonConverter.Abstractions" Version="0.5.0" />
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.1" PrivateAssets="All" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="NJsonSchema.Extensions" Version="0.1.0" />
@@ -154,6 +153,7 @@
<PackageReference Include="GraphQL" Version="7.5.0" />
<PackageReference Include="GraphQL.NewtonsoftJson" Version="7.5.0" />
<PackageReference Include="MimeKitLite" Version="4.1.0.1" />
<PackageReference Include="ProtoBufJsonConverter" Version="0.2.0" />
</ItemGroup>
<ItemGroup Condition=" '$(TargetFramework)' == 'netcoreapp3.1' ">

View File

@@ -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<IWireMockAdminApi>(server.Url);
var getMappingsResult = await api.GetMappingsAsync().ConfigureAwait(false);
await Verifier.Verify(getMappingsResult, VerifySettings);
server.Stop();
}
}
#endif

View File

@@ -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
}
]

Some files were not shown because too many files have changed in this diff Show More