Add MultiPart/MimePart Request Matcher (#981)

* wip

* .

* mm

* x

* .

* .

* .

* tests

* .

* more tests

* trans

* x

* win

* fix

* .

* tests
This commit is contained in:
Stef Heyenrath
2023-08-03 15:55:46 +02:00
committed by GitHub
parent a58ead7b4e
commit 4688f556b5
45 changed files with 2022 additions and 903 deletions

View File

@@ -39,14 +39,38 @@ public class MatcherModel
/// <summary>
/// The Operator to use when multiple patterns are defined. Optional.
/// - null = Same as "or".
/// - "or" = Only one pattern should match.
/// - "or" = Only one pattern is required to match.
/// - "and" = All patterns should match.
/// - "average" = The average value from all patterns.
/// </summary>
public string? MatchOperator { get; set; }
#region JsonPartialMatcher and JsonPartialWildcardMatcher
/// <summary>
/// Support Regex, only used for JsonPartialMatcher.
/// Support Regex.
/// </summary>
public bool? Regex { get; set; }
#endregion
#region MimePartMatcher
/// <summary>
/// ContentType Matcher (image/png; name=image.png)
/// </summary>
public MatcherModel? ContentTypeMatcher { get; set; }
/// <summary>
/// ContentDisposition Matcher (attachment; filename=image.png)
/// </summary>
public MatcherModel? ContentDispositionMatcher { get; set; }
/// <summary>
/// ContentTransferEncoding Matcher (base64)
/// </summary>
public MatcherModel? ContentTransferEncodingMatcher { get; set; }
/// <summary>
/// Content Matcher
/// </summary>
public MatcherModel? ContentMatcher { get; set; }
#endregion
}

View File

@@ -96,32 +96,39 @@ public interface IRequestMessage
/// <summary>
/// The original body as string. Convenience getter for Handlebars.
/// </summary>
string Body { get; }
string? Body { get; }
/// <summary>
/// The body (as JSON object). Convenience getter for Handlebars.
/// </summary>
object BodyAsJson { get; }
object? BodyAsJson { get; }
/// <summary>
/// The body (as bytearray). Convenience getter for Handlebars.
/// </summary>
byte[] BodyAsBytes { get; }
byte[]? BodyAsBytes { get; }
#if MIMEKIT
/// <summary>
/// The original body as MimeMessage. Convenience getter for Handlebars.
/// </summary>
object? BodyAsMimeMessage { get; }
#endif
/// <summary>
/// The detected body type. Convenience getter for Handlebars.
/// </summary>
string DetectedBodyType { get; }
string? DetectedBodyType { get; }
/// <summary>
/// The detected body type from the Content-Type header. Convenience getter for Handlebars.
/// </summary>
string DetectedBodyTypeFromContentType { get; }
string? DetectedBodyTypeFromContentType { get; }
/// <summary>
/// The detected compression from the Content-Encoding header. Convenience getter for Handlebars.
/// </summary>
string DetectedCompression { get; }
string? DetectedCompression { get; }
/// <summary>
/// Gets the Host

View File

@@ -30,6 +30,10 @@
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
</PropertyGroup>
<PropertyGroup Condition=" '$(TargetFramework)' == 'netstandard2.0' or '$(TargetFramework)' == 'netstandard2.1'" >
<DefineConstants>$(DefineConstants);GRAPHQL;MIMEKIT</DefineConstants>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="JetBrains.Annotations" Version="2022.3.1" PrivateAssets="All" />
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.1" PrivateAssets="All" />

View File

@@ -31,21 +31,14 @@ internal static class HttpRequestMessageHelper
MediaTypeHeaderValue.TryParse(value, out contentType);
}
switch (requestMessage.BodyData?.DetectedBodyType)
httpRequestMessage.Content = requestMessage.BodyData?.DetectedBodyType switch
{
case BodyType.Bytes:
httpRequestMessage.Content = ByteArrayContentHelper.Create(requestMessage.BodyData.BodyAsBytes!, contentType);
break;
case BodyType.Json:
httpRequestMessage.Content = StringContentHelper.Create(JsonConvert.SerializeObject(requestMessage.BodyData.BodyAsJson), contentType);
break;
case BodyType.String:
case BodyType.FormUrlEncoded:
httpRequestMessage.Content = StringContentHelper.Create(requestMessage.BodyData.BodyAsString!, contentType);
break;
}
BodyType.Bytes => ByteArrayContentHelper.Create(requestMessage.BodyData.BodyAsBytes!, contentType),
BodyType.Json => StringContentHelper.Create(JsonConvert.SerializeObject(requestMessage.BodyData.BodyAsJson), contentType),
BodyType.String => StringContentHelper.Create(requestMessage.BodyData.BodyAsString!, contentType),
BodyType.FormUrlEncoded => StringContentHelper.Create(requestMessage.BodyData.BodyAsString!, contentType),
_ => httpRequestMessage.Content
};
// Overwrite the host header
httpRequestMessage.Headers.Host = new Uri(url).Authority;

View File

@@ -128,7 +128,7 @@ public class GraphQLMatcher : IStringMatcher
/// <inheritdoc />
public MatchOperator MatchOperator { get; }
/// <inheritdoc cref="IMatcher.Name"/>
/// <inheritdoc />
public string Name => nameof(GraphQLMatcher);
private static ISchema BuildSchema(string schema)

View File

@@ -0,0 +1,78 @@
using Stef.Validation;
using WireMock.Types;
using WireMock.Util;
namespace WireMock.Matchers.Helpers;
internal static class BodyDataMatchScoreCalculator
{
public static double CalculateMatchScore(IBodyData? requestMessage, IMatcher matcher)
{
Guard.NotNull(matcher);
if (matcher is NotNullOrEmptyMatcher notNullOrEmptyMatcher)
{
switch (requestMessage?.DetectedBodyType)
{
case BodyType.Json:
case BodyType.String:
case BodyType.FormUrlEncoded:
return notNullOrEmptyMatcher.IsMatch(requestMessage.BodyAsString);
case BodyType.Bytes:
return notNullOrEmptyMatcher.IsMatch(requestMessage.BodyAsBytes);
default:
return MatchScores.Mismatch;
}
}
if (matcher is ExactObjectMatcher exactObjectMatcher)
{
// If the body is a byte array, try to match.
var detectedBodyType = requestMessage?.DetectedBodyType;
if (detectedBodyType is BodyType.Bytes or BodyType.String or BodyType.FormUrlEncoded)
{
return exactObjectMatcher.IsMatch(requestMessage?.BodyAsBytes);
}
}
// Check if the matcher is a IObjectMatcher
if (matcher is IObjectMatcher objectMatcher)
{
// If the body is a JSON object, try to match.
if (requestMessage?.DetectedBodyType == BodyType.Json)
{
return objectMatcher.IsMatch(requestMessage.BodyAsJson);
}
// If the body is a byte array, try to match.
if (requestMessage?.DetectedBodyType == BodyType.Bytes)
{
return objectMatcher.IsMatch(requestMessage.BodyAsBytes);
}
}
// Check if the matcher is a IStringMatcher
if (matcher is IStringMatcher stringMatcher)
{
// If the body is a Json or a String, use the BodyAsString to match on.
if (requestMessage?.DetectedBodyType is BodyType.Json or BodyType.String or BodyType.FormUrlEncoded)
{
return stringMatcher.IsMatch(requestMessage.BodyAsString);
}
}
#if MIMEKIT_XXX
if (matcher is MultiPartMatcher multiPartMatcher)
{
// If the body is a String or MultiPart, use the BodyAsString to match on.
if (requestMessage?.DetectedBodyType is BodyType.String or BodyType.MultiPart)
{
return multiPartMatcher.IsMatch(requestMessage.BodyAsString);
}
}
#endif
return MatchScores.Mismatch;
}
}

View File

@@ -1,4 +1,3 @@
using System;
using System.Linq;
using System.Linq.Dynamic.Core;
using AnyOfTypes;
@@ -122,7 +121,7 @@ public class LinqMatcher : IObjectMatcher, IStringMatcher
return MatchBehaviourHelper.Convert(MatchBehaviour, match);
}
catch (Exception e)
catch
{
if (ThrowException)
{

View File

@@ -1,7 +1,7 @@
namespace WireMock.Matchers;
/// <summary>
/// MatchBehaviour
/// MatchBehaviour (Accept or Reject)
/// </summary>
public enum MatchBehaviour
{

View File

@@ -0,0 +1,125 @@
#if MIMEKIT
using System;
using MimeKit;
using WireMock.Matchers.Helpers;
using WireMock.Util;
namespace WireMock.Matchers;
/// <summary>
/// MimePartMatcher
/// </summary>
public class MimePartMatcher : IMatcher
{
private readonly Func<MimePart, double>[] _funcs;
/// <inheritdoc />
public string Name => nameof(MimePartMatcher);
/// <summary>
/// ContentType Matcher (image/png; name=image.png.)
/// </summary>
public IStringMatcher? ContentTypeMatcher { get; }
/// <summary>
/// ContentDisposition Matcher (attachment; filename=image.png)
/// </summary>
public IStringMatcher? ContentDispositionMatcher { get; }
/// <summary>
/// ContentTransferEncoding Matcher (base64)
/// </summary>
public IStringMatcher? ContentTransferEncodingMatcher { get; }
/// <summary>
/// Content Matcher
/// </summary>
public IMatcher? ContentMatcher { get; }
/// <inheritdoc />
public MatchBehaviour MatchBehaviour { get; }
/// <inheritdoc />
public bool ThrowException { get; }
/// <summary>
/// Initializes a new instance of the <see cref="MimePartMatcher"/> class.
/// </summary>
public MimePartMatcher(
MatchBehaviour matchBehaviour,
IStringMatcher? contentTypeMatcher,
IStringMatcher? contentDispositionMatcher,
IStringMatcher? contentTransferEncodingMatcher,
IMatcher? contentMatcher,
bool throwException = false
)
{
MatchBehaviour = matchBehaviour;
ContentTypeMatcher = contentTypeMatcher;
ContentDispositionMatcher = contentDispositionMatcher;
ContentTransferEncodingMatcher = contentTransferEncodingMatcher;
ContentMatcher = contentMatcher;
ThrowException = throwException;
_funcs = new[]
{
mp => ContentTypeMatcher?.IsMatch(GetContentTypeAsString(mp.ContentType)) ?? MatchScores.Perfect,
mp => ContentDispositionMatcher?.IsMatch(mp.ContentDisposition.ToString().Replace("Content-Disposition: ", string.Empty)) ?? MatchScores.Perfect,
mp => ContentTransferEncodingMatcher?.IsMatch(mp.ContentTransferEncoding.ToString().ToLowerInvariant()) ?? MatchScores.Perfect,
MatchOnContent
};
}
/// <summary>
/// Determines whether the specified MimePart is match.
/// </summary>
/// <param name="mimePart">The MimePart.</param>
/// <returns>A value between 0.0 - 1.0 of the similarity.</returns>
public double IsMatch(MimePart mimePart)
{
var match = MatchScores.Mismatch;
try
{
if (Array.TrueForAll(_funcs, func => MatchScores.IsPerfect(func(mimePart))))
{
match = MatchScores.Perfect;
}
}
catch
{
if (ThrowException)
{
throw;
}
}
return MatchBehaviourHelper.Convert(MatchBehaviour, match);
}
private double MatchOnContent(MimePart mimePart)
{
if (ContentMatcher == null)
{
return MatchScores.Perfect;
}
var bodyParserSettings = new BodyParserSettings
{
Stream = mimePart.Content.Open(),
ContentType = GetContentTypeAsString(mimePart.ContentType),
DeserializeJson = true,
ContentEncoding = null, // mimePart.ContentType.CharsetEncoding.ToString(),
DecompressGZipAndDeflate = true
};
var bodyData = BodyParser.ParseAsync(bodyParserSettings).ConfigureAwait(false).GetAwaiter().GetResult();
return BodyDataMatchScoreCalculator.CalculateMatchScore(bodyData, ContentMatcher);
}
private static string? GetContentTypeAsString(ContentType? contentType)
{
return contentType?.ToString().Replace("Content-Type: ", string.Empty);
}
}
#endif

View File

@@ -2,7 +2,7 @@ using System;
using System.Collections.Generic;
using System.Linq;
using Stef.Validation;
using WireMock.Types;
using WireMock.Matchers.Helpers;
using WireMock.Util;
namespace WireMock.Matchers.Request;
@@ -149,69 +149,11 @@ public class RequestMessageBodyMatcher : IRequestMatcher
return requestMatchResult.AddScore(GetType(), score);
}
private static double CalculateMatchScore(IRequestMessage requestMessage, IMatcher matcher)
{
if (matcher is NotNullOrEmptyMatcher notNullOrEmptyMatcher)
{
switch (requestMessage.BodyData?.DetectedBodyType)
{
case BodyType.Json:
case BodyType.String:
case BodyType.FormUrlEncoded:
return notNullOrEmptyMatcher.IsMatch(requestMessage.BodyData.BodyAsString);
case BodyType.Bytes:
return notNullOrEmptyMatcher.IsMatch(requestMessage.BodyData.BodyAsBytes);
default:
return MatchScores.Mismatch;
}
}
if (matcher is ExactObjectMatcher exactObjectMatcher)
{
// If the body is a byte array, try to match.
var detectedBodyType = requestMessage.BodyData?.DetectedBodyType;
if (detectedBodyType is BodyType.Bytes or BodyType.String or BodyType.FormUrlEncoded)
{
return exactObjectMatcher.IsMatch(requestMessage.BodyData?.BodyAsBytes);
}
}
// Check if the matcher is a IObjectMatcher
if (matcher is IObjectMatcher objectMatcher)
{
// If the body is a JSON object, try to match.
if (requestMessage?.BodyData?.DetectedBodyType == BodyType.Json)
{
return objectMatcher.IsMatch(requestMessage.BodyData.BodyAsJson);
}
// If the body is a byte array, try to match.
if (requestMessage?.BodyData?.DetectedBodyType == BodyType.Bytes)
{
return objectMatcher.IsMatch(requestMessage.BodyData.BodyAsBytes);
}
}
// Check if the matcher is a IStringMatcher
if (matcher is IStringMatcher stringMatcher)
{
// If the body is a Json or a String, use the BodyAsString to match on.
if (requestMessage?.BodyData?.DetectedBodyType is BodyType.Json or BodyType.String or BodyType.FormUrlEncoded)
{
return stringMatcher.IsMatch(requestMessage.BodyData.BodyAsString);
}
}
return MatchScores.Mismatch;
}
private double CalculateMatchScore(IRequestMessage requestMessage)
{
if (Matchers != null)
{
var matchersResult = Matchers.Select(matcher => CalculateMatchScore(requestMessage, matcher)).ToArray();
var matchersResult = Matchers.Select(matcher => BodyDataMatchScoreCalculator.CalculateMatchScore(requestMessage.BodyData, matcher)).ToArray();
return MatchScores.ToScore(matchersResult, MatchOperator);
}

View File

@@ -40,6 +40,7 @@ public class RequestMessageGraphQLMatcher : IRequestMatcher
{
}
#endif
/// <summary>
/// Initializes a new instance of the <see cref="RequestMessageGraphQLMatcher"/> class.
/// </summary>

View File

@@ -0,0 +1,99 @@
using System.Collections.Generic;
using System.Linq;
using Stef.Validation;
using WireMock.Http;
using WireMock.Util;
namespace WireMock.Matchers.Request;
/// <summary>
/// The request body MultiPart matcher.
/// </summary>
public class RequestMessageMultiPartMatcher : IRequestMatcher
{
/// <summary>
/// The matchers.
/// </summary>
public IMatcher[]? Matchers { get; }
/// <summary>
/// The <see cref="MatchOperator"/>
/// </summary>
public MatchOperator MatchOperator { get; } = MatchOperator.Or;
/// <summary>
/// The <see cref="MatchBehaviour"/>
/// </summary>
public MatchBehaviour MatchBehaviour { get; }
/// <summary>
/// Initializes a new instance of the <see cref="RequestMessageMultiPartMatcher"/> class.
/// </summary>
/// <param name="matchers">The matchers.</param>
public RequestMessageMultiPartMatcher(params IMatcher[] matchers)
{
Matchers = Guard.NotNull(matchers);
}
/// <summary>
/// Initializes a new instance of the <see cref="RequestMessageMultiPartMatcher"/> class.
/// </summary>
/// <param name="matchBehaviour">The match behaviour.</param>
/// <param name="matchOperator">The <see cref="MatchOperator"/> to use.</param>
/// <param name="matchers">The matchers.</param>
public RequestMessageMultiPartMatcher(MatchBehaviour matchBehaviour, MatchOperator matchOperator, params IMatcher[] matchers)
{
Matchers = Guard.NotNull(matchers);
MatchBehaviour = matchBehaviour;
MatchOperator = matchOperator;
}
/// <inheritdoc />
public double GetMatchingScore(IRequestMessage requestMessage, IRequestMatchResult requestMatchResult)
{
#if !MIMEKIT
throw new System.NotSupportedException("The MultiPartMatcher can not be used for .NETStandard1.3 or .NET Framework 4.6.1 or lower.");
#else
var match = MatchScores.Mismatch;
if (Matchers?.Any() != true)
{
return requestMatchResult.AddScore(GetType(), match);
}
try
{
var message = MimeKitUtils.GetMimeMessage(requestMessage.BodyData!, requestMessage.Headers![HttpKnownHeaderNames.ContentType].ToString());
var mimePartMatchers = Matchers.OfType<MimePartMatcher>().ToArray();
foreach (var mimePart in message.BodyParts.OfType<MimeKit.MimePart>())
{
var matchesForMimePart = new List<double> { MatchScores.Mismatch };
matchesForMimePart.AddRange(mimePartMatchers.Select(matcher => matcher.IsMatch(mimePart)));
match = matchesForMimePart.Max();
if (MatchScores.IsPerfect(match))
{
if (MatchOperator == MatchOperator.Or)
{
break;
}
}
else
{
match = MatchScores.Mismatch;
break;
}
}
}
catch
{
// Empty
}
return requestMatchResult.AddScore(GetType(), match);
#endif
}
}

View File

@@ -29,17 +29,13 @@ namespace WireMock.Owin.Mappers
var headers = new Dictionary<string, string[]>();
IEnumerable<string>? contentEncodingHeader = null;
if (request.Headers.Any())
foreach (var header in request.Headers)
{
headers = new Dictionary<string, string[]>();
foreach (var header in request.Headers)
{
headers.Add(header.Key, header.Value);
headers.Add(header.Key, header.Value!);
if (string.Equals(header.Key, HttpKnownHeaderNames.ContentEncoding, StringComparison.OrdinalIgnoreCase))
{
contentEncodingHeader = header.Value;
}
if (string.Equals(header.Key, HttpKnownHeaderNames.ContentEncoding, StringComparison.OrdinalIgnoreCase))
{
contentEncodingHeader = header.Value;
}
}

View File

@@ -1,12 +1,11 @@
using WireMock.Matchers;
using WireMock.Matchers.Request;
namespace WireMock.RequestBuilders;
/// <summary>
/// The GraphQLRequestBuilder interface.
/// </summary>
public interface IGraphQLRequestBuilder : IRequestMatcher
public interface IGraphQLRequestBuilder : IMultiPartRequestBuilder
{
/// <summary>
/// WithGraphQLSchema: The GraphQL schema as a string.

View File

@@ -1,5 +1,3 @@
using System;
using JetBrains.Annotations;
using WireMock.Matchers;
namespace WireMock.RequestBuilders;

View File

@@ -0,0 +1,34 @@
using WireMock.Matchers;
using WireMock.Matchers.Request;
namespace WireMock.RequestBuilders;
/// <summary>
/// The MultiPartRequestBuilder interface.
/// </summary>
public interface IMultiPartRequestBuilder : IRequestMatcher
{
/// <summary>
/// WithMultiPart: IMatcher
/// </summary>
/// <param name="matcher">The matcher.</param>
/// <returns>The <see cref="IRequestBuilder"/>.</returns>
IRequestBuilder WithMultiPart(IMatcher matcher);
/// <summary>
/// WithMultiPart: IMatcher[], MatchBehaviour and MatchOperator
/// </summary>
/// <param name="matchers">The matchers.</param>
/// <param name="matchBehaviour">The <see cref="MatchBehaviour"/> to use.</param>
/// <param name="matchOperator">The <see cref="MatchOperator"/> to use.</param>
/// <returns>The <see cref="IRequestBuilder"/>.</returns>
IRequestBuilder WithMultiPart(IMatcher[] matchers, MatchBehaviour matchBehaviour = MatchBehaviour.AcceptOnMatch, MatchOperator matchOperator = MatchOperator.Or);
/// <summary>
/// WithMultiPart: MatchBehaviour and IMatcher[]
/// </summary>
/// <param name="matchBehaviour">The <see cref="MatchBehaviour"/> to use.</param>
/// <param name="matchers">The matchers.</param>
/// <returns>The <see cref="IRequestBuilder"/>.</returns>
IRequestBuilder WithMultiPart(MatchBehaviour matchBehaviour = MatchBehaviour.AcceptOnMatch, params IMatcher[] matchers);
}

View File

@@ -0,0 +1,28 @@
using WireMock.Matchers;
using WireMock.Matchers.Request;
namespace WireMock.RequestBuilders;
public partial class Request
{
/// <inheritdoc />
public IRequestBuilder WithMultiPart(IMatcher matcher)
{
_requestMatchers.Add(new RequestMessageMultiPartMatcher(matcher));
return this;
}
/// <inheritdoc />
public IRequestBuilder WithMultiPart(IMatcher[] matchers, MatchBehaviour matchBehaviour = MatchBehaviour.AcceptOnMatch, MatchOperator matchOperator = MatchOperator.Or)
{
_requestMatchers.Add(new RequestMessageMultiPartMatcher(matchBehaviour, matchOperator, matchers));
return this;
}
/// <inheritdoc />
public IRequestBuilder WithMultiPart(MatchBehaviour matchBehaviour = MatchBehaviour.AcceptOnMatch, params IMatcher[] matchers)
{
_requestMatchers.Add(new RequestMessageMultiPartMatcher(matchBehaviour, MatchOperator.Or, matchers));
return this;
}
}

View File

@@ -8,6 +8,7 @@ using System.Net;
using System.Security.Cryptography.X509Certificates;
#endif
using Stef.Validation;
using WireMock.Http;
using WireMock.Models;
using WireMock.Owin;
using WireMock.Types;
@@ -20,40 +21,40 @@ namespace WireMock;
/// </summary>
public class RequestMessage : IRequestMessage
{
/// <inheritdoc cref="IRequestMessage.ClientIP" />
/// <inheritdoc />
public string ClientIP { get; }
/// <inheritdoc cref="IRequestMessage.Url" />
/// <inheritdoc />
public string Url { get; }
/// <inheritdoc cref="IRequestMessage.AbsoluteUrl" />
/// <inheritdoc />
public string AbsoluteUrl { get; }
/// <inheritdoc cref="IRequestMessage.ProxyUrl" />
/// <inheritdoc />
public string? ProxyUrl { get; set; }
/// <inheritdoc cref="IRequestMessage.DateTime" />
/// <inheritdoc />
public DateTime DateTime { get; set; }
/// <inheritdoc cref="IRequestMessage.Path" />
/// <inheritdoc />
public string Path { get; }
/// <inheritdoc cref="IRequestMessage.AbsolutePath" />
/// <inheritdoc />
public string AbsolutePath { get; }
/// <inheritdoc cref="IRequestMessage.PathSegments" />
/// <inheritdoc />
public string[] PathSegments { get; }
/// <inheritdoc cref="IRequestMessage.AbsolutePathSegments" />
/// <inheritdoc />
public string[] AbsolutePathSegments { get; }
/// <inheritdoc cref="IRequestMessage.Method" />
/// <inheritdoc />
public string Method { get; }
/// <inheritdoc cref="IRequestMessage.Headers" />
/// <inheritdoc />
public IDictionary<string, WireMockList<string>>? Headers { get; }
/// <inheritdoc cref="IRequestMessage.Cookies" />
/// <inheritdoc />
public IDictionary<string, string>? Cookies { get; }
/// <inheritdoc />
@@ -61,45 +62,50 @@ public class RequestMessage : IRequestMessage
/// <inheritdoc />
public IDictionary<string, WireMockList<string>>? QueryIgnoreCase { get; }
/// <inheritdoc cref="IRequestMessage.RawQuery" />
/// <inheritdoc />
public string RawQuery { get; }
/// <inheritdoc cref="IRequestMessage.BodyData" />
/// <inheritdoc />
public IBodyData? BodyData { get; }
/// <inheritdoc cref="IRequestMessage.Body" />
public string Body { get; }
/// <inheritdoc />
public string? Body { get; }
/// <inheritdoc cref="IRequestMessage.BodyAsJson" />
public object BodyAsJson { get; }
/// <inheritdoc />
public object? BodyAsJson { get; }
/// <inheritdoc cref="IRequestMessage.BodyAsBytes" />
public byte[] BodyAsBytes { get; }
/// <inheritdoc />
public byte[]? BodyAsBytes { get; }
/// <inheritdoc cref="IRequestMessage.DetectedBodyType" />
public string DetectedBodyType { get; }
#if MIMEKIT
/// <inheritdoc />
public object? BodyAsMimeMessage { get; }
#endif
/// <inheritdoc cref="IRequestMessage.DetectedBodyTypeFromContentType" />
public string DetectedBodyTypeFromContentType { get; }
/// <inheritdoc />
public string? DetectedBodyType { get; }
/// <inheritdoc cref="IRequestMessage.DetectedCompression" />
public string DetectedCompression { get; }
/// <inheritdoc />
public string? DetectedBodyTypeFromContentType { get; }
/// <inheritdoc cref="IRequestMessage.Host" />
/// <inheritdoc />
public string? DetectedCompression { get; }
/// <inheritdoc />
public string Host { get; }
/// <inheritdoc cref="IRequestMessage.Protocol" />
/// <inheritdoc />
public string Protocol { get; }
/// <inheritdoc cref="IRequestMessage.Port" />
/// <inheritdoc />
public int Port { get; }
/// <inheritdoc cref="IRequestMessage.Origin" />
/// <inheritdoc />
public string Origin { get; }
#if USE_ASPNETCORE
/// <inheritdoc cref="IRequestMessage.ClientCertificate" />
/// <inheritdoc />
public X509Certificate2? ClientCertificate { get; }
#endif
@@ -139,11 +145,11 @@ public class RequestMessage : IRequestMessage
#if USE_ASPNETCORE
, X509Certificate2? clientCertificate = null
#endif
)
)
{
Guard.NotNull(urlDetails, nameof(urlDetails));
Guard.NotNull(method, nameof(method));
Guard.NotNull(clientIP, nameof(clientIP));
Guard.NotNull(urlDetails);
Guard.NotNull(method);
Guard.NotNull(clientIP);
AbsoluteUrl = urlDetails.AbsoluteUrl.ToString();
Url = urlDetails.Url.ToString();
@@ -166,10 +172,22 @@ public class RequestMessage : IRequestMessage
Body = BodyData?.BodyAsString;
BodyAsJson = BodyData?.BodyAsJson;
BodyAsBytes = BodyData?.BodyAsBytes;
DetectedBodyType = BodyData?.DetectedBodyType.ToString();
DetectedBodyTypeFromContentType = BodyData?.DetectedBodyTypeFromContentType.ToString();
DetectedCompression = BodyData?.DetectedCompression;
#if MIMEKIT
try
{
BodyAsMimeMessage = MimeKitUtils.GetMimeMessage(BodyData, headers![HttpKnownHeaderNames.ContentType].First());
}
catch
{
// Ignore exception from MimeMessage.Load
}
#endif
Headers = headers?.ToDictionary(header => header.Key, header => new WireMockList<string>(header.Value));
Cookies = cookies;
RawQuery = urlDetails.Url.Query;

View File

@@ -19,6 +19,7 @@ using WireMock.Types;
using WireMock.Util;
using static WireMock.Util.CSharpFormatter;
namespace WireMock.Serialization;
internal class MappingConverter
@@ -48,6 +49,7 @@ internal class MappingConverter
var methodMatcher = request.GetRequestMessageMatcher<RequestMessageMethodMatcher>();
var requestMessageBodyMatcher = request.GetRequestMessageMatcher<RequestMessageBodyMatcher>();
var requestMessageGraphQLMatcher = request.GetRequestMessageMatcher<RequestMessageGraphQLMatcher>();
var requestMessageMultiPartMatcher = request.GetRequestMessageMatcher<RequestMessageMultiPartMatcher>();
var sb = new StringBuilder();
@@ -114,8 +116,18 @@ internal class MappingConverter
sb.AppendLine($" .WithGraphQLSchema({GetString(graphQLMatcher)})");
}
}
else
#endif
#if MIMEKIT
if (requestMessageMultiPartMatcher is { Matchers: { } })
{
if (requestMessageMultiPartMatcher.Matchers.OfType<MimePartMatcher>().Any())
{
sb.AppendLine(" // .WithMultiPart() is not yet supported");
}
}
#endif
if (requestMessageBodyMatcher is { Matchers: { } })
{
if (requestMessageBodyMatcher.Matchers.OfType<WildcardMatcher>().FirstOrDefault() is { } wildcardMatcher && wildcardMatcher.GetPatterns().Any())
@@ -185,7 +197,7 @@ internal class MappingConverter
{
sb.AppendLine($" .WithBody({ToCSharpStringLiteral(bodyStringValue)})");
}
else if(bodyData.BodyAsJson is {} jsonBody)
else if (bodyData.BodyAsJson is { } jsonBody)
{
var anonymousObjectDefinition = ConvertToAnonymousObjectDefinition(jsonBody);
sb.AppendLine($" .WithBodyAsJson({anonymousObjectDefinition})");
@@ -228,6 +240,7 @@ internal class MappingConverter
var methodMatcher = request.GetRequestMessageMatcher<RequestMessageMethodMatcher>();
var bodyMatcher = request.GetRequestMessageMatcher<RequestMessageBodyMatcher>();
var graphQLMatcher = request.GetRequestMessageMatcher<RequestMessageGraphQLMatcher>();
var multiPartMatcher = request.GetRequestMessageMatcher<RequestMessageMultiPartMatcher>();
var mappingModel = new MappingModel
{
@@ -323,19 +336,20 @@ internal class MappingConverter
mappingModel.Webhooks = mapping.Webhooks.Select(WebhookMapper.Map).ToArray();
}
var graphQLOrBodyMatchers = graphQLMatcher?.Matchers ?? bodyMatcher?.Matchers;
var matchOperator = graphQLMatcher?.MatchOperator ?? bodyMatcher?.MatchOperator;
if (graphQLOrBodyMatchers != null && matchOperator != null)
var bodyMatchers = multiPartMatcher?.Matchers ?? graphQLMatcher?.Matchers ?? bodyMatcher?.Matchers;
var matchOperator = multiPartMatcher?.MatchOperator ?? graphQLMatcher?.MatchOperator ?? bodyMatcher?.MatchOperator;
if (bodyMatchers != null && matchOperator != null)
{
mappingModel.Request.Body = new BodyModel();
if (graphQLOrBodyMatchers.Length == 1)
if (bodyMatchers.Length == 1)
{
mappingModel.Request.Body.Matcher = _mapper.Map(graphQLOrBodyMatchers[0]);
mappingModel.Request.Body.Matcher = _mapper.Map(bodyMatchers[0]);
}
else if (graphQLOrBodyMatchers.Length > 1)
else if (bodyMatchers.Length > 1)
{
mappingModel.Request.Body.Matchers = _mapper.Map(graphQLOrBodyMatchers);
mappingModel.Request.Body.Matchers = _mapper.Map(bodyMatchers);
mappingModel.Request.Body.MatchOperator = matchOperator.ToString();
}
}
@@ -520,6 +534,4 @@ internal class MappingConverter
return newDictionary;
}
}

View File

@@ -75,6 +75,11 @@ internal class MatcherMapper
case nameof(GraphQLMatcher):
return new GraphQLMatcher(stringPatterns[0].GetPattern(), matchBehaviour, throwExceptionWhenMatcherFails, matchOperator);
#endif
#if MIMEKIT
case nameof(MimePartMatcher):
return CreateMimePartMatcher(matchBehaviour, matcher, throwExceptionWhenMatcherFails);
#endif
case nameof(RegexMatcher):
return new RegexMatcher(matchBehaviour, stringPatterns, ignoreCase, throwExceptionWhenMatcherFails, useRegexExtended, matchOperator);
@@ -126,12 +131,7 @@ internal class MatcherMapper
public MatcherModel[]? Map(IEnumerable<IMatcher>? matchers)
{
if (matchers == null)
{
return null;
}
return matchers.Where(m => m != null).Select(Map).ToArray()!;
return matchers?.Where(m => m != null).Select(Map).ToArray();
}
public MatcherModel? Map(IMatcher? matcher)
@@ -195,6 +195,15 @@ internal class MatcherMapper
case ExactObjectMatcher exactObjectMatcher:
model.Pattern = exactObjectMatcher.ValueAsObject ?? exactObjectMatcher.ValueAsBytes;
break;
#if MIMEKIT
case MimePartMatcher mimePartMatcher:
model.ContentDispositionMatcher = Map(mimePartMatcher.ContentDispositionMatcher);
model.ContentMatcher = Map(mimePartMatcher.ContentMatcher);
model.ContentTransferEncodingMatcher = Map(mimePartMatcher.ContentTransferEncodingMatcher);
model.ContentTypeMatcher = Map(mimePartMatcher.ContentTypeMatcher);
break;
#endif
}
return model;
@@ -224,7 +233,7 @@ internal class MatcherMapper
return new[] { new AnyOf<string, StringPattern>(new StringPattern { Pattern = pattern, PatternAsFile = patternAsFile }) };
}
return new AnyOf<string, StringPattern>[0];
return EmptyArray<AnyOf<string, StringPattern>>.Value;
}
private static ExactObjectMatcher CreateExactObjectMatcher(MatchBehaviour matchBehaviour, AnyOf<string, StringPattern> stringPattern, bool throwException)
@@ -241,4 +250,16 @@ internal class MatcherMapper
return new ExactObjectMatcher(matchBehaviour, bytePattern, throwException);
}
#if MIMEKIT
private MimePartMatcher CreateMimePartMatcher(MatchBehaviour matchBehaviour, MatcherModel? matcher, bool throwExceptionWhenMatcherFails)
{
var contentTypeMatcher = Map(matcher?.ContentTypeMatcher) as IStringMatcher;
var contentDispositionMatcher = Map(matcher?.ContentDispositionMatcher) as IStringMatcher;
var contentTransferEncodingMatcher = Map(matcher?.ContentTransferEncodingMatcher) as IStringMatcher;
var contentMatcher = Map(matcher?.ContentMatcher);
return new MimePartMatcher(matchBehaviour, contentTypeMatcher, contentDispositionMatcher, contentTransferEncodingMatcher, contentMatcher, throwExceptionWhenMatcherFails);
}
#endif
}

View File

@@ -0,0 +1,42 @@
#if MIMEKIT
using System;
using System.IO;
using System.Text;
using MimeKit;
using WireMock.Http;
using WireMock.Types;
namespace WireMock.Util;
internal static class MimeKitUtils
{
public static MimeMessage GetMimeMessage(IBodyData? bodyData, string contentTypeHeaderValue)
{
var bytes = bodyData?.DetectedBodyType switch
{
// If the body is bytes, use the BodyAsBytes to match on.
BodyType.Bytes => bodyData.BodyAsBytes!,
// If the body is a String or MultiPart, use the BodyAsString to match on.
BodyType.String or BodyType.MultiPart => Encoding.UTF8.GetBytes(bodyData.BodyAsString!),
_ => throw new NotSupportedException()
};
var fixedBytes = FixBytes(bytes, contentTypeHeaderValue);
return MimeMessage.Load(new MemoryStream(fixedBytes));
}
private static byte[] FixBytes(byte[] bytes, WireMockList<string> contentType)
{
var contentTypeBytes = Encoding.UTF8.GetBytes($"{HttpKnownHeaderNames.ContentType}: {contentType}\r\n\r\n");
var result = new byte[contentTypeBytes.Length + bytes.Length];
Buffer.BlockCopy(contentTypeBytes, 0, result, 0, contentTypeBytes.Length);
Buffer.BlockCopy(bytes, 0, result, contentTypeBytes.Length, bytes.Length);
return result;
}
}
#endif

View File

@@ -0,0 +1,12 @@
using System.IO;
using System.Text;
namespace WireMock.Util;
internal static class StreamUtils
{
public static Stream CreateStream(string s)
{
return new MemoryStream(Encoding.UTF8.GetBytes(s));
}
}

View File

@@ -51,10 +51,11 @@
</PropertyGroup>
<PropertyGroup Condition="'$(TargetFramework)' != 'netstandard1.3' and '$(TargetFramework)' != 'net451' and '$(TargetFramework)' != 'net452' and '$(TargetFramework)' != 'net46' and '$(TargetFramework)' != 'net461'">
<DefineConstants>$(DefineConstants);GRAPHQL</DefineConstants>
<DefineConstants>$(DefineConstants);GRAPHQL;MIMEKIT</DefineConstants>
</PropertyGroup>
<ItemGroup>
<Compile Remove="Matchers\MultiPartMatcher.cs" />
<Compile Remove="Util\FileSystemWatcherExtensions.cs" />
</ItemGroup>
@@ -160,6 +161,7 @@
<ItemGroup Condition="'$(TargetFramework)' != 'netstandard1.3' and '$(TargetFramework)' != 'net451' and '$(TargetFramework)' != 'net452' and '$(TargetFramework)' != 'net46' and '$(TargetFramework)' != 'net461'">
<PackageReference Include="GraphQL" Version="7.5.0" />
<PackageReference Include="GraphQL.NewtonsoftJson" Version="7.5.0" />
<PackageReference Include="MimeKitLite" Version="4.1.0.1" PrivateAssets="all" />
</ItemGroup>
<ItemGroup Condition=" '$(TargetFramework)' == 'netcoreapp3.1' ">