diff --git a/WireMock.Net Solution.sln b/WireMock.Net Solution.sln index 02034e54..3f501b3b 100644 --- a/WireMock.Net Solution.sln +++ b/WireMock.Net Solution.sln @@ -148,6 +148,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WireMock.Net.xUnit.v3", "sr EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WireMock.Net.NUnit", "src\WireMock.Net.NUnit\WireMock.Net.NUnit.csproj", "{2DBBD70D-8051-441F-92BB-FF9B8B4B4982}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WireMock.Net.Console.MimePart", "examples\WireMock.Net.Console.MimePart\WireMock.Net.Console.MimePart.csproj", "{4005E20C-D42B-138A-79BE-B3F5420C563F}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -362,6 +364,10 @@ Global {2DBBD70D-8051-441F-92BB-FF9B8B4B4982}.Debug|Any CPU.Build.0 = Debug|Any CPU {2DBBD70D-8051-441F-92BB-FF9B8B4B4982}.Release|Any CPU.ActiveCfg = Release|Any CPU {2DBBD70D-8051-441F-92BB-FF9B8B4B4982}.Release|Any CPU.Build.0 = Release|Any CPU + {4005E20C-D42B-138A-79BE-B3F5420C563F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4005E20C-D42B-138A-79BE-B3F5420C563F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4005E20C-D42B-138A-79BE-B3F5420C563F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4005E20C-D42B-138A-79BE-B3F5420C563F}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -421,6 +427,7 @@ Global {B47413AA-55D3-49A7-896A-17ADBFF72407} = {8F890C6F-9ACC-438D-928A-AD61CDA862F2} {4F46BD02-BEBC-4B2D-B857-4169AD1FB067} = {8F890C6F-9ACC-438D-928A-AD61CDA862F2} {2DBBD70D-8051-441F-92BB-FF9B8B4B4982} = {8F890C6F-9ACC-438D-928A-AD61CDA862F2} + {4005E20C-D42B-138A-79BE-B3F5420C563F} = {985E0ADB-D4B4-473A-AA40-567E279B7946} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {DC539027-9852-430C-B19F-FD035D018458} diff --git a/examples/WireMock.Net.Console.MimePart/MainApp.cs b/examples/WireMock.Net.Console.MimePart/MainApp.cs new file mode 100644 index 00000000..0178f2b2 --- /dev/null +++ b/examples/WireMock.Net.Console.MimePart/MainApp.cs @@ -0,0 +1,82 @@ +// Copyright © WireMock.Net + +using Newtonsoft.Json; +using WireMock.Logging; +using WireMock.Matchers; +using WireMock.RequestBuilders; +using WireMock.ResponseBuilders; +using WireMock.Server; +using WireMock.Settings; + +namespace WireMock.Net.Console.MimePart; + +// Test this CURL: +// curl -X POST http://localhost:9091/multipart -F "plainText=This is some plain text;type=text/plain" -F "jsonData={ `"Key`": `"Value`" };type=application/json" -F "image=@image.png;type=image/png" +// +// curl -X POST http://localhost:9091/multipart2 -F "plainText=This is some plain text;type=text/plain" -F "jsonData={ `"Key`": `"Value`" };type=application/json" -F "image=@image.png;type=image/png" + +public static class MainApp +{ + public static async Task RunAsync() + { + using var server = WireMockServer.Start(new WireMockServerSettings + { + Port = 9091, + StartAdminInterface = true, + + ReadStaticMappings = true, + //WatchStaticMappings = true, + //WatchStaticMappingsInSubdirectories = true, + + Logger = new WireMockConsoleLogger() + }); + System.Console.WriteLine("WireMockServer listening at {0}", string.Join(",", server.Urls)); + + 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("application/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("form-data; name=\"image\"; filename=\"image.png\""); + var imagePngContentTransferEncodingMatcher = new ExactMatcher("default"); + 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") + ); + + // server.SaveStaticMappings(); + + System.Console.WriteLine(JsonConvert.SerializeObject(server.MappingModels, Formatting.Indented)); + + System.Console.WriteLine("Press any key to stop the server"); + System.Console.ReadKey(); + server.Stop(); + + System.Console.WriteLine("Displaying all requests"); + var allRequests = server.LogEntries; + System.Console.WriteLine(JsonConvert.SerializeObject(allRequests, Formatting.Indented)); + + System.Console.WriteLine("Press any key to quit"); + System.Console.ReadKey(); + } +} \ No newline at end of file diff --git a/examples/WireMock.Net.Console.MimePart/Program.cs b/examples/WireMock.Net.Console.MimePart/Program.cs new file mode 100644 index 00000000..85d66ba5 --- /dev/null +++ b/examples/WireMock.Net.Console.MimePart/Program.cs @@ -0,0 +1,23 @@ +// Copyright © WireMock.Net + +using System.Reflection; +using log4net; +using log4net.Config; +using log4net.Repository; + +namespace WireMock.Net.Console.MimePart; + +static class Program +{ + private static readonly ILoggerRepository LogRepository = LogManager.GetRepository(Assembly.GetEntryAssembly()); + private static readonly ILog Log = LogManager.GetLogger(typeof(Program)); + + static async Task Main(params string[] args) + { + Log.Info("Starting WireMock.Net.Console.MimePart..."); + + XmlConfigurator.Configure(LogRepository, new FileInfo("log4net.config")); + + await MainApp.RunAsync(); + } +} \ No newline at end of file diff --git a/examples/WireMock.Net.Console.MimePart/WireMock.Net.Console.MimePart.csproj b/examples/WireMock.Net.Console.MimePart/WireMock.Net.Console.MimePart.csproj new file mode 100644 index 00000000..537b650a --- /dev/null +++ b/examples/WireMock.Net.Console.MimePart/WireMock.Net.Console.MimePart.csproj @@ -0,0 +1,29 @@ + + + + Exe + net8.0 + $(DefineConstants);GRAPHQL;MIMEKIT;PROTOBUF + enable + enable + + + + + PreserveNewest + + + + + + + + + + + + PreserveNewest + + + + \ No newline at end of file diff --git a/examples/WireMock.Net.Console.MimePart/__admin/mappings/b9c82182-e469-41da-bcaf-b6e3157fefdc.json b/examples/WireMock.Net.Console.MimePart/__admin/mappings/b9c82182-e469-41da-bcaf-b6e3157fefdc.json new file mode 100644 index 00000000..0bc68029 --- /dev/null +++ b/examples/WireMock.Net.Console.MimePart/__admin/mappings/b9c82182-e469-41da-bcaf-b6e3157fefdc.json @@ -0,0 +1,79 @@ +{ + "Guid": "b9c82182-e469-41da-bcaf-b6e3157fefdc", + "UpdatedAt": "2025-12-18T17:21:57.3879723Z", + "Request": { + "Path": { + "Matchers": [ + { + "Name": "WildcardMatcher", + "Pattern": "/multipart2", + "IgnoreCase": false + } + ] + }, + "Methods": [ + "POST" + ], + "Body": { + "MatcherName": "MultiPartMatcher", + "Matchers": [ + { + "Name": "MimePartMatcher", + "ContentTypeMatcher": { + "Name": "ContentTypeMatcher", + "Pattern": "text/plain", + "IgnoreCase": false + }, + "ContentMatcher": { + "Name": "ExactMatcher", + "Pattern": "This is some plain text", + "IgnoreCase": false + } + }, + { + "Name": "MimePartMatcher", + "ContentTypeMatcher": { + "Name": "ContentTypeMatcher", + "Pattern": "application/json", + "IgnoreCase": false + }, + "ContentMatcher": { + "Name": "JsonMatcher", + "Pattern": { + "Key": "Value" + }, + "IgnoreCase": true, + "Regex": false + } + }, + { + "Name": "MimePartMatcher", + "ContentTypeMatcher": { + "Name": "ContentTypeMatcher", + "Pattern": "image/png", + "IgnoreCase": false + }, + "ContentDispositionMatcher": { + "Name": "ExactMatcher", + "Pattern": "form-data; name=\"image\"; filename=\"image.png\"", + "IgnoreCase": false + }, + "ContentTransferEncodingMatcher": { + "Name": "ExactMatcher", + "Pattern": "default", + "IgnoreCase": false + }, + "ContentMatcher": { + "Name": "ExactObjectMatcher", + "Pattern": "iVBORw0KGgoAAAANSUhEUgAAAAIAAAACAgMAAAAP2OW3AAAADFBMVEX/tID/vpH/pWX/sHidUyjlAAAADElEQVR4XmMQYNgAAADkAMHebX3mAAAAAElFTkSuQmCC" + } + } + ], + "MatchOperator": "Or" + } + }, + "Response": { + "BodyDestination": "SameAsSource", + "Body": "MultiPart2 is ok" + } +} \ No newline at end of file diff --git a/examples/WireMock.Net.Console.MimePart/log4net.config b/examples/WireMock.Net.Console.MimePart/log4net.config new file mode 100644 index 00000000..feae9952 --- /dev/null +++ b/examples/WireMock.Net.Console.MimePart/log4net.config @@ -0,0 +1,20 @@ + + + +
+ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/WireMock.Net.Abstractions/Admin/Mappings/BodyModel.cs b/src/WireMock.Net.Abstractions/Admin/Mappings/BodyModel.cs index d958bfd9..30b8469e 100644 --- a/src/WireMock.Net.Abstractions/Admin/Mappings/BodyModel.cs +++ b/src/WireMock.Net.Abstractions/Admin/Mappings/BodyModel.cs @@ -8,6 +8,12 @@ namespace WireMock.Admin.Mappings; [FluentBuilder.AutoGenerateBuilder] public class BodyModel { + /// + /// The name of the body matcher. + /// Currently only "MultiPartMatcher" is supported. + /// + public string? MatcherName { get; set; } + /// /// Gets or sets the matcher. /// diff --git a/src/WireMock.Net.Minimal/Matchers/CompositeMatcher.cs b/src/WireMock.Net.Minimal/Matchers/CompositeMatcher.cs new file mode 100644 index 00000000..3bd2dd26 --- /dev/null +++ b/src/WireMock.Net.Minimal/Matchers/CompositeMatcher.cs @@ -0,0 +1,46 @@ +// Copyright © WireMock.Net + +using System; + +namespace WireMock.Matchers; + +/// +/// Represents a matcher that combines multiple matching strategies into a single composite operation. +/// +public class CompositeMatcher : IMatcher +{ + /// + public string Name => nameof(CompositeMatcher); + + /// + /// The logical operator used to combine the results of the matchers. + /// + public MatchOperator MatchOperator { get; } + + /// + public MatchBehaviour MatchBehaviour { get; } + + /// + /// All matchers. + /// + public IMatcher[] Matchers { get; } + + /// + /// Initializes a new instance of the CompositeMatcher class with the specified matchers, operator, and match behaviour. + /// + /// An array of matchers to be combined. Cannot be null or contain null elements. + /// The logical operator used to combine the results of the matchers. + /// The behaviour that determines how the composite matcher interprets the combined results. + public CompositeMatcher(IMatcher[] matchers, MatchOperator matchOperator, MatchBehaviour matchBehaviour) + { + Matchers = matchers; + MatchOperator = matchOperator; + MatchBehaviour = matchBehaviour; + } + + /// + public string GetCSharpCodeArguments() + { + throw new NotImplementedException(); + } +} diff --git a/src/WireMock.Net.Minimal/Matchers/Request/RequestMessageMultiPartMatcher.cs b/src/WireMock.Net.Minimal/Matchers/Request/RequestMessageMultiPartMatcher.cs index 31e2ff93..e5e9de7e 100644 --- a/src/WireMock.Net.Minimal/Matchers/Request/RequestMessageMultiPartMatcher.cs +++ b/src/WireMock.Net.Minimal/Matchers/Request/RequestMessageMultiPartMatcher.cs @@ -12,6 +12,11 @@ namespace WireMock.Matchers.Request; /// public class RequestMessageMultiPartMatcher : IRequestMatcher { + /// + /// The name of this matcher. + /// + public const string MatcherName = "MultiPartMatcher"; + private readonly IMimeKitUtils _mimeKitUtils = LoadMimeKitUtils(); /// @@ -22,7 +27,7 @@ public class RequestMessageMultiPartMatcher : IRequestMatcher /// /// The /// - public MatchOperator MatchOperator { get; } = MatchOperator.Or; + public MatchOperator MatchOperator { get; } = MatchOperator.And; /// /// The diff --git a/src/WireMock.Net.Minimal/Owin/MappingMatcher.cs b/src/WireMock.Net.Minimal/Owin/MappingMatcher.cs index 0180e865..fc74469e 100644 --- a/src/WireMock.Net.Minimal/Owin/MappingMatcher.cs +++ b/src/WireMock.Net.Minimal/Owin/MappingMatcher.cs @@ -27,6 +27,11 @@ internal class MappingMatcher(IWireMockMiddlewareOptions options, IRandomizerDou foreach (var mapping in mappings) { + if (mapping.Guid == new Guid("b9c82182-e469-41da-bcaf-b6e3157fefdb") || mapping.Guid == new Guid("b9c82182-e469-41da-bcaf-b6e3157fefdc")) + { + int x = 9; + } + try { var nextState = GetNextState(mapping); diff --git a/src/WireMock.Net.Minimal/RequestBuilders/Request.WithMultiPart.cs b/src/WireMock.Net.Minimal/RequestBuilders/Request.WithMultiPart.cs index 9e89e20a..6394fad8 100644 --- a/src/WireMock.Net.Minimal/RequestBuilders/Request.WithMultiPart.cs +++ b/src/WireMock.Net.Minimal/RequestBuilders/Request.WithMultiPart.cs @@ -15,7 +15,7 @@ public partial class Request } /// - public IRequestBuilder WithMultiPart(IMatcher[] matchers, MatchBehaviour matchBehaviour = MatchBehaviour.AcceptOnMatch, MatchOperator matchOperator = MatchOperator.Or) + public IRequestBuilder WithMultiPart(IMatcher[] matchers, MatchBehaviour matchBehaviour = MatchBehaviour.AcceptOnMatch, MatchOperator matchOperator = MatchOperator.And) { _requestMatchers.Add(new RequestMessageMultiPartMatcher(matchBehaviour, matchOperator, matchers)); return this; diff --git a/src/WireMock.Net.Minimal/Serialization/MatcherMapper.cs b/src/WireMock.Net.Minimal/Serialization/MatcherMapper.cs index 39ad8fa4..4757e3bd 100644 --- a/src/WireMock.Net.Minimal/Serialization/MatcherMapper.cs +++ b/src/WireMock.Net.Minimal/Serialization/MatcherMapper.cs @@ -25,9 +25,9 @@ internal class MatcherMapper _settings = Guard.NotNull(settings); } - public IMatcher[]? Map(IEnumerable? matchers) + public IMatcher[] Map(IEnumerable? matchers) { - return matchers?.Select(Map).OfType().ToArray(); + return matchers?.Select(Map).OfType().ToArray() ?? []; } public IMatcher? Map(MatcherModel? matcherModel) diff --git a/src/WireMock.Net.Minimal/Server/WireMockServer.ConvertMapping.cs b/src/WireMock.Net.Minimal/Server/WireMockServer.ConvertMapping.cs index d0f7622b..5258efd7 100644 --- a/src/WireMock.Net.Minimal/Server/WireMockServer.ConvertMapping.cs +++ b/src/WireMock.Net.Minimal/Server/WireMockServer.ConvertMapping.cs @@ -6,6 +6,7 @@ using System.Linq; using Stef.Validation; using WireMock.Admin.Mappings; using WireMock.Matchers; +using WireMock.Matchers.Request; using WireMock.RequestBuilders; using WireMock.ResponseBuilders; using WireMock.Serialization; @@ -253,7 +254,15 @@ public partial class WireMockServer else if (requestModel.Body?.Matchers != null) { var matchOperator = StringUtils.ParseMatchOperator(requestModel.Body.MatchOperator); - requestBuilder = requestBuilder.WithBody(_matcherMapper.Map(requestModel.Body.Matchers)!, matchOperator); + + if (requestModel.Body.MatcherName == RequestMessageMultiPartMatcher.MatcherName) + { + requestBuilder = requestBuilder.WithMultiPart(_matcherMapper.Map(requestModel.Body.Matchers), matchOperator: matchOperator); + } + else + { + requestBuilder = requestBuilder.WithBody(_matcherMapper.Map(requestModel.Body.Matchers), matchOperator); + } } return requestBuilder; diff --git a/src/WireMock.Net.Shared/Matchers/Helpers/BodyDataMatchScoreCalculator.cs b/src/WireMock.Net.Shared/Matchers/Helpers/BodyDataMatchScoreCalculator.cs index 3e179835..1dcbd265 100644 --- a/src/WireMock.Net.Shared/Matchers/Helpers/BodyDataMatchScoreCalculator.cs +++ b/src/WireMock.Net.Shared/Matchers/Helpers/BodyDataMatchScoreCalculator.cs @@ -8,26 +8,26 @@ namespace WireMock.Matchers.Helpers; internal static class BodyDataMatchScoreCalculator { - internal static MatchResult CalculateMatchScore(IBodyData? requestMessage, IMatcher matcher) + internal static MatchResult CalculateMatchScore(IBodyData? bodyData, IMatcher matcher) { Guard.NotNull(matcher); - if (requestMessage == null) + if (bodyData == null) { return default; } if (matcher is NotNullOrEmptyMatcher notNullOrEmptyMatcher) { - switch (requestMessage.DetectedBodyType) + switch (bodyData.DetectedBodyType) { case BodyType.Json: case BodyType.String: case BodyType.FormUrlEncoded: - return notNullOrEmptyMatcher.IsMatch(requestMessage.BodyAsString); + return notNullOrEmptyMatcher.IsMatch(bodyData.BodyAsString); case BodyType.Bytes: - return notNullOrEmptyMatcher.IsMatch(requestMessage.BodyAsBytes); + return notNullOrEmptyMatcher.IsMatch(bodyData.BodyAsBytes); default: return default; @@ -37,37 +37,37 @@ internal static class BodyDataMatchScoreCalculator if (matcher is ExactObjectMatcher { Value: byte[] } exactObjectMatcher) { // If the body is a byte array, try to match. - return exactObjectMatcher.IsMatch(requestMessage.BodyAsBytes); + return exactObjectMatcher.IsMatch(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.DetectedBodyType == BodyType.Json) + if (bodyData.DetectedBodyType == BodyType.Json) { - return objectMatcher.IsMatch(requestMessage.BodyAsJson); + return objectMatcher.IsMatch(bodyData.BodyAsJson); } // If the body is a byte array, try to match. - if (requestMessage.DetectedBodyType == BodyType.Bytes) + if (bodyData.DetectedBodyType == BodyType.Bytes) { - return objectMatcher.IsMatch(requestMessage.BodyAsBytes); + return objectMatcher.IsMatch(bodyData.BodyAsBytes); } } - // In case the matcher is a IStringMatcher and If body is a Json or a String, use the BodyAsString to match on. - if (matcher is IStringMatcher stringMatcher && requestMessage.DetectedBodyType is BodyType.Json or BodyType.String or BodyType.FormUrlEncoded) + // In case the matcher is a IStringMatcher and if body is a Json or a String, use the BodyAsString to match on. + if (matcher is IStringMatcher stringMatcher && bodyData.DetectedBodyType is BodyType.Json or BodyType.String or BodyType.FormUrlEncoded) { - return stringMatcher.IsMatch(requestMessage.BodyAsString); + return stringMatcher.IsMatch(bodyData.BodyAsString); } // In case the matcher is a IProtoBufMatcher, use the BodyAsBytes to match on. if (matcher is IProtoBufMatcher protoBufMatcher) { - return protoBufMatcher.IsMatchAsync(requestMessage.BodyAsBytes).GetAwaiter().GetResult(); + return protoBufMatcher.IsMatchAsync(bodyData.BodyAsBytes).GetAwaiter().GetResult(); } return default; } -} +} \ No newline at end of file diff --git a/src/WireMock.Net.Shared/RequestBuilders/IMultiPartRequestBuilder.cs b/src/WireMock.Net.Shared/RequestBuilders/IMultiPartRequestBuilder.cs index 30e23a97..23f8e74a 100644 --- a/src/WireMock.Net.Shared/RequestBuilders/IMultiPartRequestBuilder.cs +++ b/src/WireMock.Net.Shared/RequestBuilders/IMultiPartRequestBuilder.cs @@ -23,7 +23,7 @@ public interface IMultiPartRequestBuilder : IHttpVersionBuilder /// The to use. /// The to use. /// The . - IRequestBuilder WithMultiPart(IMatcher[] matchers, MatchBehaviour matchBehaviour = MatchBehaviour.AcceptOnMatch, MatchOperator matchOperator = MatchOperator.Or); + IRequestBuilder WithMultiPart(IMatcher[] matchers, MatchBehaviour matchBehaviour = MatchBehaviour.AcceptOnMatch, MatchOperator matchOperator = MatchOperator.And); /// /// WithMultiPart: MatchBehaviour and IMatcher[]