diff --git a/WireMock.Net Solution.sln b/WireMock.Net Solution.sln
index 9e767796..e74b95b4 100644
--- a/WireMock.Net Solution.sln
+++ b/WireMock.Net Solution.sln
@@ -111,10 +111,14 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WireMockAzureQueueExample",
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WireMockAzureQueueProxy", "examples\WireMockAzureQueueProxy\WireMockAzureQueueProxy.csproj", "{ADB557D8-D66B-4387-912B-3F73E290B478}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WireMock.Net.Testcontainers", "src\WireMock.Net.Testcontainers\WireMock.Net.Testcontainers.csproj", "{12B016A5-9D8B-4EFE-96C2-CA51BE43367D}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WireMock.Net.Testcontainers", "src\WireMock.Net.Testcontainers\WireMock.Net.Testcontainers.csproj", "{12B016A5-9D8B-4EFE-96C2-CA51BE43367D}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WireMock.Net.TestcontainersExample", "examples\WireMock.Net.TestcontainersExample\WireMock.Net.TestcontainersExample.csproj", "{56A38798-C48B-4A4A-B805-071E05C02CE1}"
EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tools", "tools", "{0147029F-FA4A-44B3-B79A-3C3574054EE4}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MultipartUploader", "tools\MultipartUploader\MultipartUploader.csproj", "{07C30227-ADEC-4BDE-8CDC-849D85A690BB}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -277,6 +281,10 @@ Global
{56A38798-C48B-4A4A-B805-071E05C02CE1}.Debug|Any CPU.Build.0 = Debug|Any CPU
{56A38798-C48B-4A4A-B805-071E05C02CE1}.Release|Any CPU.ActiveCfg = Release|Any CPU
{56A38798-C48B-4A4A-B805-071E05C02CE1}.Release|Any CPU.Build.0 = Release|Any CPU
+ {07C30227-ADEC-4BDE-8CDC-849D85A690BB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {07C30227-ADEC-4BDE-8CDC-849D85A690BB}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {07C30227-ADEC-4BDE-8CDC-849D85A690BB}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {07C30227-ADEC-4BDE-8CDC-849D85A690BB}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -323,6 +331,7 @@ Global
{ADB557D8-D66B-4387-912B-3F73E290B478} = {985E0ADB-D4B4-473A-AA40-567E279B7946}
{12B016A5-9D8B-4EFE-96C2-CA51BE43367D} = {8F890C6F-9ACC-438D-928A-AD61CDA862F2}
{56A38798-C48B-4A4A-B805-071E05C02CE1} = {985E0ADB-D4B4-473A-AA40-567E279B7946}
+ {07C30227-ADEC-4BDE-8CDC-849D85A690BB} = {0147029F-FA4A-44B3-B79A-3C3574054EE4}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {DC539027-9852-430C-B19F-FD035D018458}
diff --git a/examples/WireMock.Net.Console.NET6/WireMock.Net.Console.NET6.csproj b/examples/WireMock.Net.Console.NET6/WireMock.Net.Console.NET6.csproj
index 8954b1f8..c74188d2 100644
--- a/examples/WireMock.Net.Console.NET6/WireMock.Net.Console.NET6.csproj
+++ b/examples/WireMock.Net.Console.NET6/WireMock.Net.Console.NET6.csproj
@@ -3,6 +3,7 @@
Exe
net6.0
+ $(DefineConstants);GRAPHQL;MIMEKIT
@@ -29,6 +30,7 @@
+
diff --git a/examples/WireMock.Net.Console.Net452.Classic/MainApp.cs b/examples/WireMock.Net.Console.Net452.Classic/MainApp.cs
index f7c60bfe..71aeb551 100644
--- a/examples/WireMock.Net.Console.Net452.Classic/MainApp.cs
+++ b/examples/WireMock.Net.Console.Net452.Classic/MainApp.cs
@@ -166,7 +166,7 @@ namespace WireMock.Net.ConsoleApplication
//server.SetAzureADAuthentication("6c2a4722-f3b9-4970-b8fc-fac41e29stef", "8587fde1-7824-42c7-8592-faf92b04stef");
// server.AllowPartialMapping();
-
+#if GRAPHQL
server
.Given(Request.Create()
.WithPath("/graphql")
@@ -176,7 +176,41 @@ namespace WireMock.Net.ConsoleApplication
.RespondWith(Response.Create()
.WithBody("GraphQL is ok")
);
+#endif
+#if MIMEKIT
+ var textPlainContentTypeMatcher = new ContentTypeMatcher("text/plain");
+ var textPlainContentMatcher = new ExactMatcher("This is some plain text");
+ var textPlainMatcher = new MimePartMatcher(MatchBehaviour.AcceptOnMatch, textPlainContentTypeMatcher, null, null, textPlainContentMatcher);
+
+ var textJsonContentTypeMatcher = new ContentTypeMatcher("text/json");
+ var textJsonContentMatcher = new JsonMatcher(new { Key = "Value" }, true);
+ var textJsonMatcher = new MimePartMatcher(MatchBehaviour.AcceptOnMatch, textJsonContentTypeMatcher, null, null, textJsonContentMatcher);
+
+ var imagePngContentTypeMatcher = new ContentTypeMatcher("image/png");
+ var imagePngContentDispositionMatcher = new ExactMatcher("attachment; filename=\"image.png\"");
+ var imagePngContentTransferEncodingMatcher = new ExactMatcher("base64");
+ var imagePngContentMatcher = new ExactObjectMatcher(Convert.FromBase64String("iVBORw0KGgoAAAANSUhEUgAAAAIAAAACAgMAAAAP2OW3AAAADFBMVEX/tID/vpH/pWX/sHidUyjlAAAADElEQVR4XmMQYNgAAADkAMHebX3mAAAAAElFTkSuQmCC"));
+ var imagePngMatcher = new MimePartMatcher(MatchBehaviour.AcceptOnMatch, imagePngContentTypeMatcher, imagePngContentDispositionMatcher, imagePngContentTransferEncodingMatcher, imagePngContentMatcher);
+
+ var matchers = new IMatcher[]
+ {
+ textPlainMatcher,
+ textJsonMatcher,
+ imagePngMatcher
+ };
+
+ server
+ .Given(Request.Create()
+ .WithPath("/multipart")
+ .UsingPost()
+ .WithMultiPart(matchers)
+ )
+ .WithGuid("b9c82182-e469-41da-bcaf-b6e3157fefdb")
+ .RespondWith(Response.Create()
+ .WithBody("MultiPart is ok")
+ );
+#endif
// 400 ms
server
.Given(Request.Create()
@@ -716,8 +750,9 @@ namespace WireMock.Net.ConsoleApplication
}));
server.Given(Request.Create().WithPath(new WildcardMatcher("/multi-webhook", true)).UsingPost())
- .WithWebhook(new[] {
- new Webhook()
+ .WithWebhook
+ (
+ new Webhook
{
Request = new WebhookRequest
{
@@ -725,12 +760,13 @@ namespace WireMock.Net.ConsoleApplication
Method = "post",
BodyData = new BodyData
{
- BodyAsString = "OK 1!", DetectedBodyType = BodyType.String
+ BodyAsString = "OK 1!",
+ DetectedBodyType = BodyType.String
},
Delay = 1000
}
},
- new Webhook()
+ new Webhook
{
Request = new WebhookRequest
{
@@ -745,7 +781,7 @@ namespace WireMock.Net.ConsoleApplication
MaximumRandomDelay = 7000
}
}
- })
+ )
.WithWebhookFireAndForget(true)
.RespondWith(Response.Create().WithBody("a-response"));
diff --git a/src/WireMock.Net.Abstractions/Admin/Mappings/MatcherModel.cs b/src/WireMock.Net.Abstractions/Admin/Mappings/MatcherModel.cs
index 9ef8604c..1e624f64 100644
--- a/src/WireMock.Net.Abstractions/Admin/Mappings/MatcherModel.cs
+++ b/src/WireMock.Net.Abstractions/Admin/Mappings/MatcherModel.cs
@@ -39,14 +39,38 @@ public class MatcherModel
///
/// 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.
///
public string? MatchOperator { get; set; }
+ #region JsonPartialMatcher and JsonPartialWildcardMatcher
///
- /// Support Regex, only used for JsonPartialMatcher.
+ /// Support Regex.
///
public bool? Regex { get; set; }
+ #endregion
+
+ #region MimePartMatcher
+ ///
+ /// ContentType Matcher (image/png; name=image.png)
+ ///
+ public MatcherModel? ContentTypeMatcher { get; set; }
+
+ ///
+ /// ContentDisposition Matcher (attachment; filename=image.png)
+ ///
+ public MatcherModel? ContentDispositionMatcher { get; set; }
+
+ ///
+ /// ContentTransferEncoding Matcher (base64)
+ ///
+ public MatcherModel? ContentTransferEncodingMatcher { get; set; }
+
+ ///
+ /// Content Matcher
+ ///
+ public MatcherModel? ContentMatcher { get; set; }
+ #endregion
}
\ No newline at end of file
diff --git a/src/WireMock.Net.Abstractions/IRequestMessage.cs b/src/WireMock.Net.Abstractions/IRequestMessage.cs
index 7f259b95..f87b9ad6 100644
--- a/src/WireMock.Net.Abstractions/IRequestMessage.cs
+++ b/src/WireMock.Net.Abstractions/IRequestMessage.cs
@@ -96,32 +96,39 @@ public interface IRequestMessage
///
/// The original body as string. Convenience getter for Handlebars.
///
- string Body { get; }
+ string? Body { get; }
///
/// The body (as JSON object). Convenience getter for Handlebars.
///
- object BodyAsJson { get; }
+ object? BodyAsJson { get; }
///
/// The body (as bytearray). Convenience getter for Handlebars.
///
- byte[] BodyAsBytes { get; }
+ byte[]? BodyAsBytes { get; }
+
+#if MIMEKIT
+ ///
+ /// The original body as MimeMessage. Convenience getter for Handlebars.
+ ///
+ object? BodyAsMimeMessage { get; }
+#endif
///
/// The detected body type. Convenience getter for Handlebars.
///
- string DetectedBodyType { get; }
+ string? DetectedBodyType { get; }
///
/// The detected body type from the Content-Type header. Convenience getter for Handlebars.
///
- string DetectedBodyTypeFromContentType { get; }
+ string? DetectedBodyTypeFromContentType { get; }
///
/// The detected compression from the Content-Encoding header. Convenience getter for Handlebars.
///
- string DetectedCompression { get; }
+ string? DetectedCompression { get; }
///
/// Gets the Host
diff --git a/src/WireMock.Net.Abstractions/WireMock.Net.Abstractions.csproj b/src/WireMock.Net.Abstractions/WireMock.Net.Abstractions.csproj
index 01bca530..8658637a 100644
--- a/src/WireMock.Net.Abstractions/WireMock.Net.Abstractions.csproj
+++ b/src/WireMock.Net.Abstractions/WireMock.Net.Abstractions.csproj
@@ -30,6 +30,10 @@
true
+
+ $(DefineConstants);GRAPHQL;MIMEKIT
+
+
diff --git a/src/WireMock.Net/Http/HttpRequestMessageHelper.cs b/src/WireMock.Net/Http/HttpRequestMessageHelper.cs
index 778fb7a1..b2ec4d9a 100644
--- a/src/WireMock.Net/Http/HttpRequestMessageHelper.cs
+++ b/src/WireMock.Net/Http/HttpRequestMessageHelper.cs
@@ -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;
diff --git a/src/WireMock.Net/Matchers/GraphQLMatcher.cs b/src/WireMock.Net/Matchers/GraphQLMatcher.cs
index 92eca1ff..02b3b58f 100644
--- a/src/WireMock.Net/Matchers/GraphQLMatcher.cs
+++ b/src/WireMock.Net/Matchers/GraphQLMatcher.cs
@@ -128,7 +128,7 @@ public class GraphQLMatcher : IStringMatcher
///
public MatchOperator MatchOperator { get; }
- ///
+ ///
public string Name => nameof(GraphQLMatcher);
private static ISchema BuildSchema(string schema)
diff --git a/src/WireMock.Net/Matchers/Helpers/BodyDataMatchScoreCalculator.cs b/src/WireMock.Net/Matchers/Helpers/BodyDataMatchScoreCalculator.cs
new file mode 100644
index 00000000..2050285a
--- /dev/null
+++ b/src/WireMock.Net/Matchers/Helpers/BodyDataMatchScoreCalculator.cs
@@ -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;
+ }
+}
\ No newline at end of file
diff --git a/src/WireMock.Net/Matchers/LinqMatcher.cs b/src/WireMock.Net/Matchers/LinqMatcher.cs
index 34577b60..48f17dbc 100644
--- a/src/WireMock.Net/Matchers/LinqMatcher.cs
+++ b/src/WireMock.Net/Matchers/LinqMatcher.cs
@@ -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)
{
diff --git a/src/WireMock.Net/Matchers/MatchBehaviour.cs b/src/WireMock.Net/Matchers/MatchBehaviour.cs
index f703ff07..6235c5ee 100644
--- a/src/WireMock.Net/Matchers/MatchBehaviour.cs
+++ b/src/WireMock.Net/Matchers/MatchBehaviour.cs
@@ -1,7 +1,7 @@
namespace WireMock.Matchers;
///
-/// MatchBehaviour
+/// MatchBehaviour (Accept or Reject)
///
public enum MatchBehaviour
{
diff --git a/src/WireMock.Net/Matchers/MimePartMatcher.cs b/src/WireMock.Net/Matchers/MimePartMatcher.cs
new file mode 100644
index 00000000..410efd7a
--- /dev/null
+++ b/src/WireMock.Net/Matchers/MimePartMatcher.cs
@@ -0,0 +1,125 @@
+#if MIMEKIT
+using System;
+using MimeKit;
+using WireMock.Matchers.Helpers;
+using WireMock.Util;
+
+namespace WireMock.Matchers;
+
+///
+/// MimePartMatcher
+///
+public class MimePartMatcher : IMatcher
+{
+ private readonly Func[] _funcs;
+
+ ///
+ public string Name => nameof(MimePartMatcher);
+
+ ///
+ /// ContentType Matcher (image/png; name=image.png.)
+ ///
+ public IStringMatcher? ContentTypeMatcher { get; }
+
+ ///
+ /// ContentDisposition Matcher (attachment; filename=image.png)
+ ///
+ public IStringMatcher? ContentDispositionMatcher { get; }
+
+ ///
+ /// ContentTransferEncoding Matcher (base64)
+ ///
+ public IStringMatcher? ContentTransferEncodingMatcher { get; }
+
+ ///
+ /// Content Matcher
+ ///
+ public IMatcher? ContentMatcher { get; }
+
+ ///
+ public MatchBehaviour MatchBehaviour { get; }
+
+ ///
+ public bool ThrowException { get; }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ 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
+ };
+ }
+
+ ///
+ /// Determines whether the specified MimePart is match.
+ ///
+ /// The MimePart.
+ /// A value between 0.0 - 1.0 of the similarity.
+ 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
\ No newline at end of file
diff --git a/src/WireMock.Net/Matchers/Request/RequestMessageBodyMatcher.cs b/src/WireMock.Net/Matchers/Request/RequestMessageBodyMatcher.cs
index 580902f2..7bcb4138 100644
--- a/src/WireMock.Net/Matchers/Request/RequestMessageBodyMatcher.cs
+++ b/src/WireMock.Net/Matchers/Request/RequestMessageBodyMatcher.cs
@@ -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);
}
diff --git a/src/WireMock.Net/Matchers/Request/RequestMessageGraphQLMatcher.cs b/src/WireMock.Net/Matchers/Request/RequestMessageGraphQLMatcher.cs
index 21719ca9..653d2d90 100644
--- a/src/WireMock.Net/Matchers/Request/RequestMessageGraphQLMatcher.cs
+++ b/src/WireMock.Net/Matchers/Request/RequestMessageGraphQLMatcher.cs
@@ -40,6 +40,7 @@ public class RequestMessageGraphQLMatcher : IRequestMatcher
{
}
#endif
+
///
/// Initializes a new instance of the class.
///
diff --git a/src/WireMock.Net/Matchers/Request/RequestMessageMultiPartMatcher.cs b/src/WireMock.Net/Matchers/Request/RequestMessageMultiPartMatcher.cs
new file mode 100644
index 00000000..92c43694
--- /dev/null
+++ b/src/WireMock.Net/Matchers/Request/RequestMessageMultiPartMatcher.cs
@@ -0,0 +1,99 @@
+using System.Collections.Generic;
+using System.Linq;
+using Stef.Validation;
+using WireMock.Http;
+using WireMock.Util;
+
+namespace WireMock.Matchers.Request;
+
+///
+/// The request body MultiPart matcher.
+///
+public class RequestMessageMultiPartMatcher : IRequestMatcher
+{
+ ///
+ /// The matchers.
+ ///
+ public IMatcher[]? Matchers { get; }
+
+ ///
+ /// The
+ ///
+ public MatchOperator MatchOperator { get; } = MatchOperator.Or;
+
+ ///
+ /// The
+ ///
+ public MatchBehaviour MatchBehaviour { get; }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The matchers.
+ public RequestMessageMultiPartMatcher(params IMatcher[] matchers)
+ {
+ Matchers = Guard.NotNull(matchers);
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The match behaviour.
+ /// The to use.
+ /// The matchers.
+ public RequestMessageMultiPartMatcher(MatchBehaviour matchBehaviour, MatchOperator matchOperator, params IMatcher[] matchers)
+ {
+ Matchers = Guard.NotNull(matchers);
+ MatchBehaviour = matchBehaviour;
+ MatchOperator = matchOperator;
+ }
+
+ ///
+ 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().ToArray();
+
+ foreach (var mimePart in message.BodyParts.OfType())
+ {
+ var matchesForMimePart = new List { 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
+ }
+}
\ No newline at end of file
diff --git a/src/WireMock.Net/Owin/Mappers/OwinRequestMapper.cs b/src/WireMock.Net/Owin/Mappers/OwinRequestMapper.cs
index d3848e53..c49dc785 100644
--- a/src/WireMock.Net/Owin/Mappers/OwinRequestMapper.cs
+++ b/src/WireMock.Net/Owin/Mappers/OwinRequestMapper.cs
@@ -29,17 +29,13 @@ namespace WireMock.Owin.Mappers
var headers = new Dictionary();
IEnumerable? contentEncodingHeader = null;
- if (request.Headers.Any())
+ foreach (var header in request.Headers)
{
- headers = new Dictionary();
- 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;
}
}
diff --git a/src/WireMock.Net/RequestBuilders/IGraphQLRequestBuilder.cs b/src/WireMock.Net/RequestBuilders/IGraphQLRequestBuilder.cs
index a90b78ae..6d23d2dd 100644
--- a/src/WireMock.Net/RequestBuilders/IGraphQLRequestBuilder.cs
+++ b/src/WireMock.Net/RequestBuilders/IGraphQLRequestBuilder.cs
@@ -1,12 +1,11 @@
using WireMock.Matchers;
-using WireMock.Matchers.Request;
namespace WireMock.RequestBuilders;
///
/// The GraphQLRequestBuilder interface.
///
-public interface IGraphQLRequestBuilder : IRequestMatcher
+public interface IGraphQLRequestBuilder : IMultiPartRequestBuilder
{
///
/// WithGraphQLSchema: The GraphQL schema as a string.
diff --git a/src/WireMock.Net/RequestBuilders/IMethodRequestBuilder.cs b/src/WireMock.Net/RequestBuilders/IMethodRequestBuilder.cs
index 59dd1675..7e21a6d8 100644
--- a/src/WireMock.Net/RequestBuilders/IMethodRequestBuilder.cs
+++ b/src/WireMock.Net/RequestBuilders/IMethodRequestBuilder.cs
@@ -1,5 +1,3 @@
-using System;
-using JetBrains.Annotations;
using WireMock.Matchers;
namespace WireMock.RequestBuilders;
diff --git a/src/WireMock.Net/RequestBuilders/IMultiPartRequestBuilder.cs b/src/WireMock.Net/RequestBuilders/IMultiPartRequestBuilder.cs
new file mode 100644
index 00000000..75a07be6
--- /dev/null
+++ b/src/WireMock.Net/RequestBuilders/IMultiPartRequestBuilder.cs
@@ -0,0 +1,34 @@
+using WireMock.Matchers;
+using WireMock.Matchers.Request;
+
+namespace WireMock.RequestBuilders;
+
+///
+/// The MultiPartRequestBuilder interface.
+///
+public interface IMultiPartRequestBuilder : IRequestMatcher
+{
+ ///
+ /// WithMultiPart: IMatcher
+ ///
+ /// The matcher.
+ /// The .
+ IRequestBuilder WithMultiPart(IMatcher matcher);
+
+ ///
+ /// WithMultiPart: IMatcher[], MatchBehaviour and MatchOperator
+ ///
+ /// The matchers.
+ /// The to use.
+ /// The to use.
+ /// The .
+ IRequestBuilder WithMultiPart(IMatcher[] matchers, MatchBehaviour matchBehaviour = MatchBehaviour.AcceptOnMatch, MatchOperator matchOperator = MatchOperator.Or);
+
+ ///
+ /// WithMultiPart: MatchBehaviour and IMatcher[]
+ ///
+ /// The to use.
+ /// The matchers.
+ /// The .
+ IRequestBuilder WithMultiPart(MatchBehaviour matchBehaviour = MatchBehaviour.AcceptOnMatch, params IMatcher[] matchers);
+}
\ No newline at end of file
diff --git a/src/WireMock.Net/RequestBuilders/Request.WithMultiPart.cs b/src/WireMock.Net/RequestBuilders/Request.WithMultiPart.cs
new file mode 100644
index 00000000..64011c30
--- /dev/null
+++ b/src/WireMock.Net/RequestBuilders/Request.WithMultiPart.cs
@@ -0,0 +1,28 @@
+using WireMock.Matchers;
+using WireMock.Matchers.Request;
+
+namespace WireMock.RequestBuilders;
+
+public partial class Request
+{
+ ///
+ public IRequestBuilder WithMultiPart(IMatcher matcher)
+ {
+ _requestMatchers.Add(new RequestMessageMultiPartMatcher(matcher));
+ return this;
+ }
+
+ ///
+ public IRequestBuilder WithMultiPart(IMatcher[] matchers, MatchBehaviour matchBehaviour = MatchBehaviour.AcceptOnMatch, MatchOperator matchOperator = MatchOperator.Or)
+ {
+ _requestMatchers.Add(new RequestMessageMultiPartMatcher(matchBehaviour, matchOperator, matchers));
+ return this;
+ }
+
+ ///
+ public IRequestBuilder WithMultiPart(MatchBehaviour matchBehaviour = MatchBehaviour.AcceptOnMatch, params IMatcher[] matchers)
+ {
+ _requestMatchers.Add(new RequestMessageMultiPartMatcher(matchBehaviour, MatchOperator.Or, matchers));
+ return this;
+ }
+}
\ No newline at end of file
diff --git a/src/WireMock.Net/RequestMessage.cs b/src/WireMock.Net/RequestMessage.cs
index d250b0c2..069d7801 100644
--- a/src/WireMock.Net/RequestMessage.cs
+++ b/src/WireMock.Net/RequestMessage.cs
@@ -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;
///
public class RequestMessage : IRequestMessage
{
- ///
+ ///
public string ClientIP { get; }
- ///
+ ///
public string Url { get; }
- ///
+ ///
public string AbsoluteUrl { get; }
- ///
+ ///
public string? ProxyUrl { get; set; }
- ///
+ ///
public DateTime DateTime { get; set; }
- ///
+ ///
public string Path { get; }
- ///
+ ///
public string AbsolutePath { get; }
- ///
+ ///
public string[] PathSegments { get; }
- ///
+ ///
public string[] AbsolutePathSegments { get; }
- ///
+ ///
public string Method { get; }
- ///
+ ///
public IDictionary>? Headers { get; }
- ///
+ ///
public IDictionary? Cookies { get; }
///
@@ -61,45 +62,50 @@ public class RequestMessage : IRequestMessage
///
public IDictionary>? QueryIgnoreCase { get; }
-
- ///
+
+ ///
public string RawQuery { get; }
- ///
+ ///
public IBodyData? BodyData { get; }
- ///
- public string Body { get; }
+ ///
+ public string? Body { get; }
- ///
- public object BodyAsJson { get; }
+ ///
+ public object? BodyAsJson { get; }
- ///
- public byte[] BodyAsBytes { get; }
+ ///
+ public byte[]? BodyAsBytes { get; }
- ///
- public string DetectedBodyType { get; }
+#if MIMEKIT
+ ///
+ public object? BodyAsMimeMessage { get; }
+#endif
- ///
- public string DetectedBodyTypeFromContentType { get; }
+ ///
+ public string? DetectedBodyType { get; }
- ///
- public string DetectedCompression { get; }
+ ///
+ public string? DetectedBodyTypeFromContentType { get; }
- ///
+ ///
+ public string? DetectedCompression { get; }
+
+ ///
public string Host { get; }
- ///
+ ///
public string Protocol { get; }
- ///
+ ///
public int Port { get; }
- ///
+ ///
public string Origin { get; }
#if USE_ASPNETCORE
- ///
+ ///
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(header.Value));
Cookies = cookies;
RawQuery = urlDetails.Url.Query;
diff --git a/src/WireMock.Net/Serialization/MappingConverter.cs b/src/WireMock.Net/Serialization/MappingConverter.cs
index 95a346ee..3a356b4c 100644
--- a/src/WireMock.Net/Serialization/MappingConverter.cs
+++ b/src/WireMock.Net/Serialization/MappingConverter.cs
@@ -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();
var requestMessageBodyMatcher = request.GetRequestMessageMatcher();
var requestMessageGraphQLMatcher = request.GetRequestMessageMatcher();
+ var requestMessageMultiPartMatcher = request.GetRequestMessageMatcher();
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().Any())
+ {
+ sb.AppendLine(" // .WithMultiPart() is not yet supported");
+ }
+ }
+#endif
+
if (requestMessageBodyMatcher is { Matchers: { } })
{
if (requestMessageBodyMatcher.Matchers.OfType().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();
var bodyMatcher = request.GetRequestMessageMatcher();
var graphQLMatcher = request.GetRequestMessageMatcher();
+ var multiPartMatcher = request.GetRequestMessageMatcher();
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;
}
-
-
}
\ No newline at end of file
diff --git a/src/WireMock.Net/Serialization/MatcherMapper.cs b/src/WireMock.Net/Serialization/MatcherMapper.cs
index ffd2cb44..3ed1bc2a 100644
--- a/src/WireMock.Net/Serialization/MatcherMapper.cs
+++ b/src/WireMock.Net/Serialization/MatcherMapper.cs
@@ -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? 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(new StringPattern { Pattern = pattern, PatternAsFile = patternAsFile }) };
}
- return new AnyOf[0];
+ return EmptyArray>.Value;
}
private static ExactObjectMatcher CreateExactObjectMatcher(MatchBehaviour matchBehaviour, AnyOf 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
}
\ No newline at end of file
diff --git a/src/WireMock.Net/Util/MimeKitUtils.cs b/src/WireMock.Net/Util/MimeKitUtils.cs
new file mode 100644
index 00000000..ca62465c
--- /dev/null
+++ b/src/WireMock.Net/Util/MimeKitUtils.cs
@@ -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 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
\ No newline at end of file
diff --git a/src/WireMock.Net/Util/StreamUtils.cs b/src/WireMock.Net/Util/StreamUtils.cs
new file mode 100644
index 00000000..df0b2865
--- /dev/null
+++ b/src/WireMock.Net/Util/StreamUtils.cs
@@ -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));
+ }
+}
\ No newline at end of file
diff --git a/src/WireMock.Net/WireMock.Net.csproj b/src/WireMock.Net/WireMock.Net.csproj
index 46f0e268..1e581877 100644
--- a/src/WireMock.Net/WireMock.Net.csproj
+++ b/src/WireMock.Net/WireMock.Net.csproj
@@ -51,10 +51,11 @@
- $(DefineConstants);GRAPHQL
+ $(DefineConstants);GRAPHQL;MIMEKIT
+
@@ -160,6 +161,7 @@
+
diff --git a/test/WireMock.Net.Tests/Matchers/MimePartMatcherTests.cs b/test/WireMock.Net.Tests/Matchers/MimePartMatcherTests.cs
new file mode 100644
index 00000000..467e72f0
--- /dev/null
+++ b/test/WireMock.Net.Tests/Matchers/MimePartMatcherTests.cs
@@ -0,0 +1,99 @@
+#if MIMEKIT
+using System;
+using System.Linq;
+using FluentAssertions;
+using MimeKit;
+using WireMock.Matchers;
+using WireMock.Util;
+using Xunit;
+
+namespace WireMock.Net.Tests.Matchers;
+
+public class MimePartMatcherTests
+{
+ private const string TestMultiPart = @"From:
+Date: Sun, 23 Jul 2023 16:13:13 +0200
+Subject:
+Message-Id:
+MIME-Version: 1.0
+Content-Type: multipart/mixed; boundary=""=-5XgmpXt0XOfzdtcgNJc2ZQ==""
+
+--=-5XgmpXt0XOfzdtcgNJc2ZQ==
+Content-Type: text/plain; charset=utf-8
+
+This is some plain text
+--=-5XgmpXt0XOfzdtcgNJc2ZQ==
+Content-Type: text/json; charset=utf-8
+
+{
+ ""Key"": ""Value""
+ }
+--=-5XgmpXt0XOfzdtcgNJc2ZQ==
+Content-Type: image/png; name=image.png
+Content-Disposition: attachment; filename=image.png
+Content-Transfer-Encoding: base64
+
+iVBORw0KGgoAAAANSUhEUgAAAAIAAAACAgMAAAAP2OW3AAAADFBMVEX/tID/vpH/pWX/sHidUyjl
+AAAADElEQVR4XmMQYNgAAADkAMHebX3mAAAAAElFTkSuQmCC
+
+--=-5XgmpXt0XOfzdtcgNJc2ZQ==--
+";
+
+ [Fact]
+ public void MimePartMatcher_IsMatch_Part_TextPlain()
+ {
+ // Arrange
+ var message = MimeMessage.Load(StreamUtils.CreateStream(TestMultiPart));
+ var part = (MimePart)message.BodyParts.ToArray()[0];
+
+ // Act
+ var contentTypeMatcher = new ContentTypeMatcher("text/plain");
+ var contentMatcher = new ExactMatcher("This is some plain text");
+
+ var matcher = new MimePartMatcher(MatchBehaviour.AcceptOnMatch, contentTypeMatcher, null, null, contentMatcher);
+ var result = matcher.IsMatch(part);
+
+ // Assert
+ matcher.Name.Should().Be("MimePartMatcher");
+ result.Should().Be(MatchScores.Perfect);
+ }
+
+ [Fact]
+ public void MimePartMatcher_IsMatch_Part_TextJson()
+ {
+ // Arrange
+ var message = MimeMessage.Load(StreamUtils.CreateStream(TestMultiPart));
+ var part = (MimePart)message.BodyParts.ToArray()[1];
+
+ // Act
+ var contentTypeMatcher = new ContentTypeMatcher("text/json");
+ var contentMatcher = new JsonMatcher(new { Key = "Value" }, true);
+
+ var matcher = new MimePartMatcher(MatchBehaviour.AcceptOnMatch, contentTypeMatcher, null, null, contentMatcher);
+ var result = matcher.IsMatch(part);
+
+ // Assert
+ result.Should().Be(MatchScores.Perfect);
+ }
+
+ [Fact]
+ public void MimePartMatcher_IsMatch_Part_ImagePng()
+ {
+ // Arrange
+ var message = MimeMessage.Load(StreamUtils.CreateStream(TestMultiPart));
+ var part = (MimePart)message.BodyParts.ToArray()[2];
+
+ // Act
+ var contentTypeMatcher = new ContentTypeMatcher("image/png");
+ var contentDispositionMatcher = new ExactMatcher("attachment; filename=\"image.png\"");
+ var contentTransferEncodingMatcher = new ExactMatcher("base64");
+ var contentMatcher = new ExactObjectMatcher(Convert.FromBase64String("iVBORw0KGgoAAAANSUhEUgAAAAIAAAACAgMAAAAP2OW3AAAADFBMVEX/tID/vpH/pWX/sHidUyjlAAAADElEQVR4XmMQYNgAAADkAMHebX3mAAAAAElFTkSuQmCC"));
+
+ var matcher = new MimePartMatcher(MatchBehaviour.AcceptOnMatch, contentTypeMatcher, contentDispositionMatcher, contentTransferEncodingMatcher, contentMatcher);
+ var result = matcher.IsMatch(part);
+
+ // Assert
+ result.Should().Be(MatchScores.Perfect);
+ }
+}
+#endif
\ No newline at end of file
diff --git a/test/WireMock.Net.Tests/RequestTests.cs b/test/WireMock.Net.Tests/RequestBuilders/RequestBuilderTests.cs
similarity index 99%
rename from test/WireMock.Net.Tests/RequestTests.cs
rename to test/WireMock.Net.Tests/RequestBuilders/RequestBuilderTests.cs
index 00e604c7..73094505 100644
--- a/test/WireMock.Net.Tests/RequestTests.cs
+++ b/test/WireMock.Net.Tests/RequestBuilders/RequestBuilderTests.cs
@@ -8,9 +8,9 @@ using WireMock.Types;
using WireMock.Util;
using Xunit;
-namespace WireMock.Net.Tests;
+namespace WireMock.Net.Tests.RequestBuilders;
-public class RequestTests
+public class RequestBuilderTests
{
private const string ClientIp = "::1";
diff --git a/test/WireMock.Net.Tests/RequestBuilders/RequestBuilderWithBodyTests.cs b/test/WireMock.Net.Tests/RequestBuilders/RequestBuilderWithBodyTests.cs
index d4a7c704..e12014af 100644
--- a/test/WireMock.Net.Tests/RequestBuilders/RequestBuilderWithBodyTests.cs
+++ b/test/WireMock.Net.Tests/RequestBuilders/RequestBuilderWithBodyTests.cs
@@ -1,17 +1,28 @@
+using System;
using FluentAssertions;
using System.Collections.Generic;
using System.Linq;
+using System.Text;
+using JsonConverter.Abstractions;
+using Moq;
+using Newtonsoft.Json;
+using NFluent;
using WireMock.Matchers;
using WireMock.Matchers.Request;
+using WireMock.Models;
using WireMock.RequestBuilders;
+using WireMock.Types;
+using WireMock.Util;
using Xunit;
namespace WireMock.Net.Tests.RequestBuilders;
public class RequestBuilderWithBodyTests
{
+ private const string ClientIp = "::1";
+
[Fact]
- public void RequestBuilder_WithBody_IMatcher()
+ public void Request_WithBody_IMatcher()
{
// Assign
var matcher = new WildcardMatcher("x");
@@ -26,7 +37,7 @@ public class RequestBuilderWithBodyTests
}
[Fact]
- public void RequestBuilder_WithBody_IMatchers()
+ public void Request_WithBody_IMatchers()
{
// Assign
var matcher1 = new WildcardMatcher("x");
@@ -40,4 +51,409 @@ public class RequestBuilderWithBodyTests
matchers.Should().HaveCount(1);
((RequestMessageBodyMatcher)matchers[0]).Matchers.Should().Contain(new[] { matcher1, matcher2 });
}
+
+ [Fact]
+ public void Request_WithBody_FuncString()
+ {
+ // Assign
+ var requestBuilder = Request.Create().UsingAnyMethod().WithBody(b => b != null && b.Contains("b"));
+
+ // Act
+ var body = new BodyData
+ {
+ BodyAsString = "b",
+ DetectedBodyType = BodyType.String
+ };
+ var request = new RequestMessage(new UrlDetails("http://localhost/foo"), "POST", ClientIp, body);
+
+ // Assert
+ var requestMatchResult = new RequestMatchResult();
+ Check.That(requestBuilder.GetMatchingScore(request, requestMatchResult)).IsEqualTo(1.0);
+ }
+
+ [Fact]
+ public void Request_WithBody_FuncJson()
+ {
+ // Assign
+ var requestBuilder = Request.Create().UsingAnyMethod().WithBody(b => b != null);
+
+ // Act
+ var body = new BodyData
+ {
+ BodyAsJson = 123,
+ DetectedBodyType = BodyType.Json
+ };
+ var request = new RequestMessage(new UrlDetails("http://localhost/foo"), "POST", ClientIp, body);
+
+ // Assert
+ var requestMatchResult = new RequestMatchResult();
+ Check.That(requestBuilder.GetMatchingScore(request, requestMatchResult)).IsEqualTo(1.0);
+ }
+
+ [Fact]
+ public void Request_WithBody_FuncFormUrlEncoded()
+ {
+ // Assign
+ var requestBuilder = Request.Create().UsingAnyMethod().WithBody((IDictionary? values) => values != null);
+
+ // Act
+ var body = new BodyData
+ {
+ BodyAsFormUrlEncoded = new Dictionary(),
+ DetectedBodyTypeFromContentType = BodyType.FormUrlEncoded,
+ DetectedBodyType = BodyType.FormUrlEncoded
+ };
+ var request = new RequestMessage(new UrlDetails("http://localhost/foo"), "POST", ClientIp, body);
+
+ // Assert
+ var requestMatchResult = new RequestMatchResult();
+ Check.That(requestBuilder.GetMatchingScore(request, requestMatchResult)).IsEqualTo(1.0);
+ }
+
+ [Fact]
+ public void Request_WithBody_FuncBodyData()
+ {
+ // Assign
+ var requestBuilder = Request.Create().UsingAnyMethod().WithBody((IBodyData? b) => b != null);
+
+ // Act
+ var body = new BodyData
+ {
+ BodyAsJson = 123,
+ DetectedBodyType = BodyType.Json
+ };
+ var request = new RequestMessage(new UrlDetails("http://localhost/foo"), "POST", ClientIp, body);
+
+ // Assert
+ var requestMatchResult = new RequestMatchResult();
+ Check.That(requestBuilder.GetMatchingScore(request, requestMatchResult)).IsEqualTo(1.0);
+ }
+
+ [Fact]
+ public void Request_WithBody_FuncByteArray()
+ {
+ // Assign
+ var requestBuilder = Request.Create().UsingAnyMethod().WithBody((byte[]? b) => b != null);
+
+ // Act
+ var body = new BodyData
+ {
+ BodyAsBytes = new byte[0],
+ DetectedBodyType = BodyType.Bytes
+ };
+ var request = new RequestMessage(new UrlDetails("http://localhost/foo"), "POST", ClientIp, body);
+
+ // Assert
+ var requestMatchResult = new RequestMatchResult();
+ Check.That(requestBuilder.GetMatchingScore(request, requestMatchResult)).IsEqualTo(1.0);
+ }
+
+ [Fact]
+ public void Request_WithBodyExactMatcher()
+ {
+ // Arrange
+ var requestBuilder = Request.Create().UsingAnyMethod().WithBody(new ExactMatcher("cat"));
+
+ // Act
+ var body = new BodyData
+ {
+ BodyAsString = "cat",
+ DetectedBodyType = BodyType.String
+ };
+ var request = new RequestMessage(new UrlDetails("http://localhost/foo"), "POST", ClientIp, body);
+
+ // Assert
+ var requestMatchResult = new RequestMatchResult();
+ Check.That(requestBuilder.GetMatchingScore(request, requestMatchResult)).IsEqualTo(1.0);
+ }
+
+ [Fact]
+ public void Request_WithBody_BodyDataAsString_Using_WildcardMatcher()
+ {
+ // Arrange
+ var spec = Request.Create().WithPath("/foo").UsingAnyMethod().WithBody(new WildcardMatcher("H*o*"));
+
+ // Act
+ var body = new BodyData
+ {
+ BodyAsString = "Hello world!",
+ DetectedBodyType = BodyType.String
+ };
+ var request = new RequestMessage(new UrlDetails("http://localhost/foo"), "PUT", ClientIp, body);
+
+ // Assert
+ var requestMatchResult = new RequestMatchResult();
+ spec.GetMatchingScore(request, requestMatchResult).Should().Be(1.0);
+ }
+
+ [Fact]
+ public void Request_WithBody_BodyDataAsJson_Using_WildcardMatcher()
+ {
+ // Arrange
+ var spec = Request.Create().WithPath("/foo").UsingAnyMethod().WithBody(new WildcardMatcher("*Hello*"));
+
+ // Act
+ var body = new BodyData
+ {
+ BodyAsJson = new { Hi = "Hello world!" },
+ BodyAsString = "{ Hi = \"Hello world!\" }",
+ DetectedBodyType = BodyType.Json
+ };
+ var request = new RequestMessage(new UrlDetails("http://localhost/foo"), "PUT", ClientIp, body);
+
+ // Assert
+ var requestMatchResult = new RequestMatchResult();
+ spec.GetMatchingScore(request, requestMatchResult).Should().Be(1.0);
+ }
+
+ [Fact]
+ public void Request_WithBody_XPathMatcher_true()
+ {
+ // Arrange
+ var spec = Request.Create().UsingAnyMethod().WithBody(new XPathMatcher("/todo-list[count(todo-item) = 3]"));
+
+ // Act
+ var body = new BodyData
+ {
+ BodyAsString = @"
+
+ abc
+ def
+ xyz
+ ",
+ DetectedBodyType = BodyType.String
+ };
+ var request = new RequestMessage(new UrlDetails("http://localhost/foo"), "PUT", ClientIp, body);
+
+ // Assert
+ var requestMatchResult = new RequestMatchResult();
+ Check.That(spec.GetMatchingScore(request, requestMatchResult)).IsEqualTo(1.0);
+ }
+
+ [Fact]
+ public void Request_WithBody_XPathMatcher_false()
+ {
+ // Arrange
+ var spec = Request.Create().UsingAnyMethod().WithBody(new XPathMatcher("/todo-list[count(todo-item) = 99]"));
+
+ // Act
+ var body = new BodyData
+ {
+ BodyAsString = @"
+
+ abc
+ def
+ xyz
+ ",
+ DetectedBodyType = BodyType.String
+ };
+ var request = new RequestMessage(new UrlDetails("http://localhost/foo"), "PUT", ClientIp, body);
+
+ // Assert
+ var requestMatchResult = new RequestMatchResult();
+ Check.That(spec.GetMatchingScore(request, requestMatchResult)).IsNotEqualTo(1.0);
+ }
+
+ [Fact]
+ public void Request_WithBody_JsonPathMatcher_true()
+ {
+ // Arrange
+ var spec = Request.Create().UsingAnyMethod().WithBody(new JsonPathMatcher("$..things[?(@.name == 'RequiredThing')]"));
+
+ // Act
+ var body = new BodyData
+ {
+ BodyAsString = "{ \"things\": [ { \"name\": \"RequiredThing\" }, { \"name\": \"Wiremock\" } ] }",
+ DetectedBodyType = BodyType.String
+ };
+ var request = new RequestMessage(new UrlDetails("http://localhost/foo"), "PUT", ClientIp, body);
+
+ // Assert
+ var requestMatchResult = new RequestMatchResult();
+ Check.That(spec.GetMatchingScore(request, requestMatchResult)).IsEqualTo(1.0);
+ }
+
+ [Fact]
+ public void Request_WithBodyJson_PathMatcher_false()
+ {
+ // Arrange
+ var spec = Request.Create().UsingAnyMethod().WithBody(new JsonPathMatcher("$.things[?(@.name == 'RequiredThing')]"));
+
+ // Act
+ var body = new BodyData
+ {
+ BodyAsString = "{ \"things\": { \"name\": \"Wiremock\" } }",
+ DetectedBodyType = BodyType.String
+ };
+ var request = new RequestMessage(new UrlDetails("http://localhost/foo"), "PUT", ClientIp, body);
+
+ // Assert
+ var requestMatchResult = new RequestMatchResult();
+ Check.That(spec.GetMatchingScore(request, requestMatchResult)).IsNotEqualTo(1.0);
+ }
+
+ [Fact]
+ public void Request_WithBodyAsJson_Object_JsonPathMatcher_true()
+ {
+ // Arrange
+ var spec = Request.Create().UsingAnyMethod().WithBody(new JsonPathMatcher("$..things[?(@.name == 'RequiredThing')]"));
+
+ // Act
+ string jsonString = "{ \"things\": [ { \"name\": \"RequiredThing\" }, { \"name\": \"Wiremock\" } ] }";
+ var bodyData = new BodyData
+ {
+ BodyAsJson = JsonConvert.DeserializeObject(jsonString),
+ BodyAsString = jsonString,
+ Encoding = Encoding.UTF8,
+ DetectedBodyType = BodyType.Json
+ };
+
+ var request = new RequestMessage(new UrlDetails("http://localhost/foo"), "PUT", ClientIp, bodyData);
+
+ // Assert
+ var requestMatchResult = new RequestMatchResult();
+ Check.That(spec.GetMatchingScore(request, requestMatchResult)).IsEqualTo(1.0);
+ }
+
+ [Fact]
+ public void Request_WithBodyAsJson_Array_JsonPathMatcher_1()
+ {
+ // Arrange
+ var spec = Request.Create().UsingAnyMethod().WithBody(new JsonPathMatcher("$..books[?(@.price < 10)]"));
+
+ // Act
+ string jsonString = "{ \"books\": [ { \"category\": \"test1\", \"price\": 8.95 }, { \"category\": \"test2\", \"price\": 20 } ] }";
+ var bodyData = new BodyData
+ {
+ BodyAsJson = JsonConvert.DeserializeObject(jsonString),
+ BodyAsString = jsonString,
+ Encoding = Encoding.UTF8,
+ DetectedBodyType = BodyType.Json
+ };
+
+ var request = new RequestMessage(new UrlDetails("http://localhost/foo"), "PUT", ClientIp, bodyData);
+
+ // Assert
+ var requestMatchResult = new RequestMatchResult();
+ Check.That(spec.GetMatchingScore(request, requestMatchResult)).IsEqualTo(1.0);
+ }
+
+ [Fact]
+ public void Request_WithBodyAsJson_Array_JsonPathMatcher_2()
+ {
+ // Arrange
+ var spec = Request.Create().UsingAnyMethod().WithBody(new JsonPathMatcher("$..[?(@.Id == 1)]"));
+
+ // Act
+ string jsonString = "{ \"Id\": 1, \"Name\": \"Test\" }";
+ var bodyData = new BodyData
+ {
+ BodyAsJson = JsonConvert.DeserializeObject(jsonString),
+ BodyAsString = jsonString,
+ Encoding = Encoding.UTF8,
+ DetectedBodyType = BodyType.Json
+ };
+
+ var request = new RequestMessage(new UrlDetails("http://localhost/foo"), "PUT", ClientIp, bodyData);
+
+ // Assert
+ var requestMatchResult = new RequestMatchResult();
+ double result = spec.GetMatchingScore(request, requestMatchResult);
+ Check.That(result).IsEqualTo(1.0);
+ }
+
+ [Fact]
+ public void Request_WithBodyAsObject_ExactObjectMatcher_true()
+ {
+ // Assign
+ object body = DateTime.MinValue;
+ var requestBuilder = Request.Create().UsingAnyMethod().WithBody(body);
+
+ var bodyData = new BodyData
+ {
+ BodyAsJson = DateTime.MinValue,
+ DetectedBodyType = BodyType.Json
+ };
+
+ // Act
+ var request = new RequestMessage(new UrlDetails("http://localhost/foo"), "PUT", ClientIp, bodyData);
+
+ // Assert
+ var requestMatchResult = new RequestMatchResult();
+ Check.That(requestBuilder.GetMatchingScore(request, requestMatchResult)).IsEqualTo(1.0);
+ }
+
+ [Fact]
+ public void Request_WithBodyAsJson_UsingObject()
+ {
+ // Assign
+ object body = new
+ {
+ Test = "abc"
+ };
+ var requestBuilder = Request.Create().UsingAnyMethod().WithBodyAsJson(body);
+
+ var bodyData = new BodyData
+ {
+ BodyAsString = JsonConvert.SerializeObject(body),
+ DetectedBodyType = BodyType.String
+ };
+
+ // Act
+ var request = new RequestMessage(new UrlDetails("http://localhost/foo"), "POST", ClientIp, bodyData);
+
+ // Assert
+ var requestMatchResult = new RequestMatchResult();
+ Check.That(requestBuilder.GetMatchingScore(request, requestMatchResult)).IsEqualTo(1.0);
+ }
+
+ [Fact]
+ public void Request_WithBodyAsJson_WithIJsonConverter_UsingObject()
+ {
+ // Assign
+ var jsonConverterMock = new Mock();
+ jsonConverterMock.Setup(j => j.Serialize(It.IsAny