From ec89c64d0f3941d96180a52b5eccd833cff79ace Mon Sep 17 00:00:00 2001 From: Stef Heyenrath Date: Wed, 15 Mar 2023 12:01:53 +0100 Subject: [PATCH 1/8] . --- .../Models/IBodyData.cs | 6 ++ .../Types/BodyType.cs | 62 ++++++++++--------- .../Request/RequestMessageBodyMatcher.cs | 24 ++++++- src/WireMock.Net/Models/BodyData.cs | 4 ++ .../RequestBuilders/IBodyRequestBuilder.cs | 16 +++-- .../RequestBuilders/Request.WithBody.cs | 38 +++++++----- src/WireMock.Net/Util/BodyParser.cs | 29 ++++++++- src/WireMock.Net/Util/BodyParserSettings.cs | 2 + 8 files changed, 127 insertions(+), 54 deletions(-) diff --git a/src/WireMock.Net.Abstractions/Models/IBodyData.cs b/src/WireMock.Net.Abstractions/Models/IBodyData.cs index faafd2c05..557be21c0 100644 --- a/src/WireMock.Net.Abstractions/Models/IBodyData.cs +++ b/src/WireMock.Net.Abstractions/Models/IBodyData.cs @@ -1,3 +1,4 @@ +using System.Collections.Generic; using System.Text; using WireMock.Types; @@ -38,6 +39,11 @@ public interface IBodyData /// string? BodyAsString { get; set; } + /// + /// The body as Form UrlEncoded dictionary. + /// + IDictionary? BodyAsFormUrlEncoded { get; set; } + /// /// The detected body type (detection based on body content). /// diff --git a/src/WireMock.Net.Abstractions/Types/BodyType.cs b/src/WireMock.Net.Abstractions/Types/BodyType.cs index d90f34fb9..a03534d1a 100644 --- a/src/WireMock.Net.Abstractions/Types/BodyType.cs +++ b/src/WireMock.Net.Abstractions/Types/BodyType.cs @@ -1,38 +1,42 @@ -namespace WireMock.Types +namespace WireMock.Types; + +/// +/// The BodyType +/// +public enum BodyType { /// - /// The BodyType + /// No body present /// - public enum BodyType - { - /// - /// No body present - /// - None, + None, - /// - /// Body is a String - /// - String, + /// + /// Body is a String + /// + String, - /// - /// Body is a Json object - /// - Json, + /// + /// Body is a Json object + /// + Json, + + /// + /// Body is a Byte array + /// + Bytes, - /// - /// Body is a Byte array - /// - Bytes, + /// + /// Body is a File + /// + File, - /// - /// Body is a File - /// - File, + /// + /// Body is a MultiPart + /// + MultiPart, - /// - /// Body is a MultiPart - /// - MultiPart - } + /// + /// Body is a String which is x-www-form-urlencoded. + /// + FormUrlEncoded } \ 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 f5e130475..cf46d77a3 100644 --- a/src/WireMock.Net/Matchers/Request/RequestMessageBodyMatcher.cs +++ b/src/WireMock.Net/Matchers/Request/RequestMessageBodyMatcher.cs @@ -1,8 +1,7 @@ using System; +using System.Collections.Generic; using System.Linq; -using AnyOfTypes; using Stef.Validation; -using WireMock.Models; using WireMock.Types; using WireMock.Util; @@ -33,6 +32,11 @@ public class RequestMessageBodyMatcher : IRequestMatcher /// public Func? BodyDataFunc { get; } + /// + /// The body data function for FormUrlEncoded + /// + public Func?, bool>? FormUrlEncodedFunc { get; } + /// /// The matchers. /// @@ -109,6 +113,15 @@ public RequestMessageBodyMatcher(Func func) BodyDataFunc = Guard.NotNull(func); } + /// + /// Initializes a new instance of the class. + /// + /// The function. + public RequestMessageBodyMatcher(Func?, bool> func) + { + FormUrlEncodedFunc = Guard.NotNull(func); + } + /// /// Initializes a new instance of the class. /// @@ -184,7 +197,7 @@ private static double CalculateMatchScore(IRequestMessage requestMessage, IMatch if (matcher is IStringMatcher stringMatcher) { // If the body is a Json or a String, use the BodyAsString to match on. - if (requestMessage?.BodyData?.DetectedBodyType == BodyType.Json || requestMessage?.BodyData?.DetectedBodyType == BodyType.String) + if (requestMessage?.BodyData?.DetectedBodyType is BodyType.Json or BodyType.String) { return stringMatcher.IsMatch(requestMessage.BodyData.BodyAsString); } @@ -206,6 +219,11 @@ private double CalculateMatchScore(IRequestMessage requestMessage) return MatchScores.ToScore(Func(requestMessage.BodyData?.BodyAsString)); } + if (FormUrlEncodedFunc != null) + { + return MatchScores.ToScore(FormUrlEncodedFunc(requestMessage.BodyData?.BodyAsFormUrlEncoded)); + } + if (JsonFunc != null) { return MatchScores.ToScore(JsonFunc(requestMessage.BodyData?.BodyAsJson)); diff --git a/src/WireMock.Net/Models/BodyData.cs b/src/WireMock.Net/Models/BodyData.cs index 7d00e971d..a8036e50f 100644 --- a/src/WireMock.Net/Models/BodyData.cs +++ b/src/WireMock.Net/Models/BodyData.cs @@ -1,3 +1,4 @@ +using System.Collections.Generic; using System.Text; using WireMock.Types; @@ -14,6 +15,9 @@ public class BodyData : IBodyData /// public string? BodyAsString { get; set; } + /// + public IDictionary? BodyAsFormUrlEncoded { get; set; } + /// public object? BodyAsJson { get; set; } diff --git a/src/WireMock.Net/RequestBuilders/IBodyRequestBuilder.cs b/src/WireMock.Net/RequestBuilders/IBodyRequestBuilder.cs index e586dbf9f..f877bfb40 100644 --- a/src/WireMock.Net/RequestBuilders/IBodyRequestBuilder.cs +++ b/src/WireMock.Net/RequestBuilders/IBodyRequestBuilder.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using WireMock.Matchers; using WireMock.Matchers.Request; using WireMock.Util; @@ -54,26 +55,33 @@ public interface IBodyRequestBuilder : IRequestMatcher /// /// The function. /// The . - IRequestBuilder WithBody(Func func); + IRequestBuilder WithBody(Func func); /// /// WithBody: func (byte[]) /// /// The function. /// The . - IRequestBuilder WithBody(Func func); + IRequestBuilder WithBody(Func func); /// /// WithBody: func (json object) /// /// The function. /// The . - IRequestBuilder WithBody(Func func); + IRequestBuilder WithBody(Func func); /// /// WithBody: func (BodyData object) /// /// The function. /// The . - IRequestBuilder WithBody(Func func); + IRequestBuilder WithBody(Func func); + + /// + /// WithBody: Body as form-urlencoded values. + /// + /// The form-urlencoded values. + /// The . + IRequestBuilder WithBody(Func?, bool> form); } \ No newline at end of file diff --git a/src/WireMock.Net/RequestBuilders/Request.WithBody.cs b/src/WireMock.Net/RequestBuilders/Request.WithBody.cs index ca1ca6a29..ff1165c44 100644 --- a/src/WireMock.Net/RequestBuilders/Request.WithBody.cs +++ b/src/WireMock.Net/RequestBuilders/Request.WithBody.cs @@ -1,6 +1,7 @@ // This source file is based on mock4net by Alexandre Victoor which is licensed under the Apache 2.0 License. // For more details see 'mock4net/LICENSE.txt' and 'mock4net/readme.md' in this project root. using System; +using System.Collections.Generic; using WireMock.Matchers; using WireMock.Matchers.Request; using WireMock.Util; @@ -10,21 +11,21 @@ namespace WireMock.RequestBuilders; public partial class Request { - /// + /// public IRequestBuilder WithBody(string body, MatchBehaviour matchBehaviour = MatchBehaviour.AcceptOnMatch) { _requestMatchers.Add(new RequestMessageBodyMatcher(matchBehaviour, body)); return this; } - /// + /// public IRequestBuilder WithBody(byte[] body, MatchBehaviour matchBehaviour = MatchBehaviour.AcceptOnMatch) { _requestMatchers.Add(new RequestMessageBodyMatcher(matchBehaviour, body)); return this; } - /// + /// public IRequestBuilder WithBody(object body, MatchBehaviour matchBehaviour = MatchBehaviour.AcceptOnMatch) { _requestMatchers.Add(new RequestMessageBodyMatcher(matchBehaviour, body)); @@ -46,39 +47,46 @@ public IRequestBuilder WithBody(IMatcher[] matchers, MatchOperator matchOperator return this; } - /// - public IRequestBuilder WithBody(Func func) + /// + public IRequestBuilder WithBody(Func func) { - Guard.NotNull(func, nameof(func)); + Guard.NotNull(func); _requestMatchers.Add(new RequestMessageBodyMatcher(func)); return this; } - /// - public IRequestBuilder WithBody(Func func) + /// + public IRequestBuilder WithBody(Func func) { - Guard.NotNull(func, nameof(func)); + Guard.NotNull(func); _requestMatchers.Add(new RequestMessageBodyMatcher(func)); return this; } - /// - public IRequestBuilder WithBody(Func func) + /// + public IRequestBuilder WithBody(Func func) { - Guard.NotNull(func, nameof(func)); + Guard.NotNull(func); _requestMatchers.Add(new RequestMessageBodyMatcher(func)); return this; } - /// - public IRequestBuilder WithBody(Func func) + /// + public IRequestBuilder WithBody(Func func) { - Guard.NotNull(func, nameof(func)); + Guard.NotNull(func); _requestMatchers.Add(new RequestMessageBodyMatcher(func)); return this; } + + /// + public IRequestBuilder WithBody(Func?, bool> func) + { + _requestMatchers.Add(new RequestMessageBodyMatcher(Guard.NotNull(func))); + return this; + } } \ No newline at end of file diff --git a/src/WireMock.Net/Util/BodyParser.cs b/src/WireMock.Net/Util/BodyParser.cs index 5ff5a9ddd..05fffa9da 100644 --- a/src/WireMock.Net/Util/BodyParser.cs +++ b/src/WireMock.Net/Util/BodyParser.cs @@ -4,6 +4,7 @@ using System.Net.Http.Headers; using System.Text; using System.Threading.Tasks; +using System.Web; using Stef.Validation; using WireMock.Constants; using WireMock.Matchers; @@ -49,14 +50,16 @@ internal static class BodyParser new WildcardMatcher("application/vnd.*+json", true) }; + private static readonly IStringMatcher FormUrlEncodedMatcher = new WildcardMatcher("application/x-www-form-urlencoded", true); + private static readonly IStringMatcher[] TextContentTypeMatchers = { new WildcardMatcher("text/*", true), new RegexMatcher("^application\\/(java|type)script$", true), new WildcardMatcher("application/*xml", true), - new WildcardMatcher("application/x-www-form-urlencoded", true) + FormUrlEncodedMatcher }; - + public static bool ShouldParseBody(string? httpMethod, bool allowBodyForAllHttpMethods) { if (string.IsNullOrEmpty(httpMethod)) @@ -88,6 +91,11 @@ public static BodyType DetectBodyTypeFromContentType(string? contentTypeValue) return BodyType.Bytes; } + if (MatchScores.IsPerfect(FormUrlEncodedMatcher.IsMatch(contentType.MediaType))) + { + return BodyType.FormUrlEncoded; + } + if (TextContentTypeMatchers.Any(matcher => MatchScores.IsPerfect(matcher.IsMatch(contentType.MediaType)))) { return BodyType.String; @@ -133,13 +141,28 @@ public static async Task ParseAsync(BodyParserSettings settings) return data; } - // Try to get the body as String or Json + // Try to get the body as String, FormUrlEncoded or Json try { data.BodyAsString = DefaultEncoding.GetString(data.BodyAsBytes); data.Encoding = DefaultEncoding; data.DetectedBodyType = BodyType.String; + // If string is not null or empty, try to deserialize the string to a IDictionary + if (settings.DeserializeFormUrlEncoded && !string.IsNullOrEmpty(data.BodyAsString)) + { + try + { + var formValues = HttpUtility.ParseQueryString(data.BodyAsString); + data.BodyAsFormUrlEncoded = formValues.AllKeys.ToDictionary(key => key, key => formValues[key]); + data.DetectedBodyType = BodyType.FormUrlEncoded; + } + catch + { + // Deserialize FormUrlEncoded failed, just ignore. + } + } + // If string is not null or empty, try to deserialize the string to a JObject if (settings.DeserializeJson && !string.IsNullOrEmpty(data.BodyAsString)) { diff --git a/src/WireMock.Net/Util/BodyParserSettings.cs b/src/WireMock.Net/Util/BodyParserSettings.cs index a6aec91a2..2ac5f8780 100644 --- a/src/WireMock.Net/Util/BodyParserSettings.cs +++ b/src/WireMock.Net/Util/BodyParserSettings.cs @@ -13,4 +13,6 @@ internal class BodyParserSettings public bool DecompressGZipAndDeflate { get; set; } = true; public bool DeserializeJson { get; set; } = true; + + public bool DeserializeFormUrlEncoded { get; set; } = true; } \ No newline at end of file From dfc01f2e04f9acedb09a4018cdd3be369067b7e4 Mon Sep 17 00:00:00 2001 From: Stef Heyenrath Date: Wed, 15 Mar 2023 12:37:37 +0100 Subject: [PATCH 2/8] x --- src/WireMock.Net/Util/BodyParser.cs | 8 +++----- src/WireMock.Net/Util/QueryStringParser.cs | 16 ++++++++++++++++ 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/src/WireMock.Net/Util/BodyParser.cs b/src/WireMock.Net/Util/BodyParser.cs index 05fffa9da..78bfebf90 100644 --- a/src/WireMock.Net/Util/BodyParser.cs +++ b/src/WireMock.Net/Util/BodyParser.cs @@ -4,7 +4,6 @@ using System.Net.Http.Headers; using System.Text; using System.Threading.Tasks; -using System.Web; using Stef.Validation; using WireMock.Constants; using WireMock.Matchers; @@ -59,7 +58,7 @@ internal static class BodyParser new WildcardMatcher("application/*xml", true), FormUrlEncodedMatcher }; - + public static bool ShouldParseBody(string? httpMethod, bool allowBodyForAllHttpMethods) { if (string.IsNullOrEmpty(httpMethod)) @@ -149,12 +148,11 @@ public static async Task ParseAsync(BodyParserSettings settings) data.DetectedBodyType = BodyType.String; // If string is not null or empty, try to deserialize the string to a IDictionary - if (settings.DeserializeFormUrlEncoded && !string.IsNullOrEmpty(data.BodyAsString)) + if (settings.DeserializeFormUrlEncoded && QueryStringParser.TryParse(data.BodyAsString, out var nameValueCollection)) { try { - var formValues = HttpUtility.ParseQueryString(data.BodyAsString); - data.BodyAsFormUrlEncoded = formValues.AllKeys.ToDictionary(key => key, key => formValues[key]); + data.BodyAsFormUrlEncoded = nameValueCollection; data.DetectedBodyType = BodyType.FormUrlEncoded; } catch diff --git a/src/WireMock.Net/Util/QueryStringParser.cs b/src/WireMock.Net/Util/QueryStringParser.cs index 8e0e5f14a..1f44ea2a2 100644 --- a/src/WireMock.Net/Util/QueryStringParser.cs +++ b/src/WireMock.Net/Util/QueryStringParser.cs @@ -1,6 +1,7 @@ using System; using System.Net; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; using WireMock.Types; @@ -13,6 +14,21 @@ internal static class QueryStringParser { private static readonly Dictionary> Empty = new(); + public static bool TryParse(string? data, [NotNullWhen(true)] out IDictionary? nameValueCollection) + { + if (string.IsNullOrEmpty(data)) + { + nameValueCollection = default; + return false; + } + + nameValueCollection = data! + .Split(new[] { "&" }, StringSplitOptions.RemoveEmptyEntries) + .Select(parameter => parameter.Split(new[] { '=' }, 2, StringSplitOptions.RemoveEmptyEntries)) + .ToDictionary(grouping => grouping[0], grouping => WebUtility.UrlDecode(grouping[1])); + return true; + } + public static IDictionary> Parse(string? queryString, QueryParameterMultipleValueSupport? support = null) { if (string.IsNullOrEmpty(queryString)) From 2c232bcc2ccf5f1434c26f7a1aa4dd557f12e032 Mon Sep 17 00:00:00 2001 From: Stef Heyenrath Date: Wed, 15 Mar 2023 12:47:27 +0100 Subject: [PATCH 3/8] fx --- src/WireMock.Net/Util/BodyParser.cs | 5 ++++- .../RequestMatchers/RequestMessageBodyMatcherTests.cs | 8 ++++---- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/WireMock.Net/Util/BodyParser.cs b/src/WireMock.Net/Util/BodyParser.cs index 78bfebf90..697e475c4 100644 --- a/src/WireMock.Net/Util/BodyParser.cs +++ b/src/WireMock.Net/Util/BodyParser.cs @@ -148,7 +148,10 @@ public static async Task ParseAsync(BodyParserSettings settings) data.DetectedBodyType = BodyType.String; // If string is not null or empty, try to deserialize the string to a IDictionary - if (settings.DeserializeFormUrlEncoded && QueryStringParser.TryParse(data.BodyAsString, out var nameValueCollection)) + if (settings.DeserializeFormUrlEncoded && + data.DetectedBodyTypeFromContentType == BodyType.FormUrlEncoded && + QueryStringParser.TryParse(data.BodyAsString, out var nameValueCollection) + ) { try { diff --git a/test/WireMock.Net.Tests/RequestMatchers/RequestMessageBodyMatcherTests.cs b/test/WireMock.Net.Tests/RequestMatchers/RequestMessageBodyMatcherTests.cs index 59a654cd0..26812d128 100644 --- a/test/WireMock.Net.Tests/RequestMatchers/RequestMessageBodyMatcherTests.cs +++ b/test/WireMock.Net.Tests/RequestMatchers/RequestMessageBodyMatcherTests.cs @@ -431,7 +431,7 @@ public static TheoryData MatchingScoreD // JSON match +++ {json, new RequestMessageBodyMatcher((object? o) => ((dynamic) o!).a == "b"), true}, {json, new RequestMessageBodyMatcher((string? s) => s == json), true}, - {json, new RequestMessageBodyMatcher((byte[]? b) => b.SequenceEqual(Encoding.UTF8.GetBytes(json))), true}, + {json, new RequestMessageBodyMatcher((byte[]? b) => b?.SequenceEqual(Encoding.UTF8.GetBytes(json)) == true), true}, // JSON no match --- {json, new RequestMessageBodyMatcher((object? o) => false), false}, @@ -442,7 +442,7 @@ public static TheoryData MatchingScoreD // string match +++ {str, new RequestMessageBodyMatcher((object? o) => o == null), true}, {str, new RequestMessageBodyMatcher((string? s) => s == str), true}, - {str, new RequestMessageBodyMatcher((byte[]? b) => b.SequenceEqual(Encoding.UTF8.GetBytes(str))), true}, + {str, new RequestMessageBodyMatcher((byte[]? b) => b?.SequenceEqual(Encoding.UTF8.GetBytes(str)) == true), true}, // string no match --- {str, new RequestMessageBodyMatcher((object? o) => false), false}, @@ -453,13 +453,13 @@ public static TheoryData MatchingScoreD // binary match +++ {bytes, new RequestMessageBodyMatcher((object? o) => o == null), true}, {bytes, new RequestMessageBodyMatcher((string? s) => s == null), true}, - {bytes, new RequestMessageBodyMatcher((byte[]? b) => b.SequenceEqual(bytes)), true}, + {bytes, new RequestMessageBodyMatcher((byte[]? b) => b?.SequenceEqual(bytes) == true), true}, // binary no match --- {bytes, new RequestMessageBodyMatcher((object? o) => false), false}, {bytes, new RequestMessageBodyMatcher((string? s) => false), false}, {bytes, new RequestMessageBodyMatcher((byte[]? b) => false), false}, - {bytes, new RequestMessageBodyMatcher(), false }, + {bytes, new RequestMessageBodyMatcher(), false } }; } } From 8471388aa0574350242931384dad438038574938 Mon Sep 17 00:00:00 2001 From: Stef Heyenrath Date: Wed, 15 Mar 2023 17:04:27 +0100 Subject: [PATCH 4/8] fix --- .../RequestWithBodyTests.cs | 25 +++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/test/WireMock.Net.Tests/RequestWithBodyTests.cs b/test/WireMock.Net.Tests/RequestWithBodyTests.cs index cf82fe16a..e09817d90 100644 --- a/test/WireMock.Net.Tests/RequestWithBodyTests.cs +++ b/test/WireMock.Net.Tests/RequestWithBodyTests.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Text; using FluentAssertions; using Newtonsoft.Json; @@ -55,11 +56,31 @@ public void Request_WithBody_FuncJson() 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); + var requestBuilder = Request.Create().UsingAnyMethod().WithBody((IBodyData? b) => b != null); // Act var body = new BodyData @@ -78,7 +99,7 @@ public void Request_WithBody_FuncBodyData() public void Request_WithBody_FuncByteArray() { // Assign - var requestBuilder = Request.Create().UsingAnyMethod().WithBody((byte[] b) => b != null); + var requestBuilder = Request.Create().UsingAnyMethod().WithBody((byte[]? b) => b != null); // Act var body = new BodyData From 2841d497797079cfd0b3b8b1d38fb8a0cf777f76 Mon Sep 17 00:00:00 2001 From: Stef Heyenrath Date: Wed, 15 Mar 2023 17:07:27 +0100 Subject: [PATCH 5/8] f --- src/WireMock.Net/RequestBuilders/IBodyRequestBuilder.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/WireMock.Net/RequestBuilders/IBodyRequestBuilder.cs b/src/WireMock.Net/RequestBuilders/IBodyRequestBuilder.cs index f877bfb40..1477adf60 100644 --- a/src/WireMock.Net/RequestBuilders/IBodyRequestBuilder.cs +++ b/src/WireMock.Net/RequestBuilders/IBodyRequestBuilder.cs @@ -81,7 +81,7 @@ public interface IBodyRequestBuilder : IRequestMatcher /// /// WithBody: Body as form-urlencoded values. /// - /// The form-urlencoded values. + /// The form-urlencoded values. /// The . - IRequestBuilder WithBody(Func?, bool> form); + IRequestBuilder WithBody(Func?, bool> func); } \ No newline at end of file From bfa917ba7a539cb00c3a408f303758301a4d425d Mon Sep 17 00:00:00 2001 From: Stef Heyenrath Date: Thu, 16 Mar 2023 08:24:57 +0100 Subject: [PATCH 6/8] tests --- src/WireMock.Net/Util/BodyParser.cs | 4 +- src/WireMock.Net/Util/QueryStringParser.cs | 22 ++++++--- .../Util/QueryStringParserTests.cs | 45 ++++++++++++++++++- 3 files changed, 62 insertions(+), 9 deletions(-) diff --git a/src/WireMock.Net/Util/BodyParser.cs b/src/WireMock.Net/Util/BodyParser.cs index 697e475c4..2a5ee6b81 100644 --- a/src/WireMock.Net/Util/BodyParser.cs +++ b/src/WireMock.Net/Util/BodyParser.cs @@ -71,7 +71,7 @@ public static bool ShouldParseBody(string? httpMethod, bool allowBodyForAllHttpM return true; } - if (BodyAllowedForMethods.TryGetValue(httpMethod!.ToUpper(), out bool allowed)) + if (BodyAllowedForMethods.TryGetValue(httpMethod!.ToUpper(), out var allowed)) { return allowed; } @@ -150,7 +150,7 @@ public static async Task ParseAsync(BodyParserSettings settings) // If string is not null or empty, try to deserialize the string to a IDictionary if (settings.DeserializeFormUrlEncoded && data.DetectedBodyTypeFromContentType == BodyType.FormUrlEncoded && - QueryStringParser.TryParse(data.BodyAsString, out var nameValueCollection) + QueryStringParser.TryParse(data.BodyAsString, false, out var nameValueCollection) ) { try diff --git a/src/WireMock.Net/Util/QueryStringParser.cs b/src/WireMock.Net/Util/QueryStringParser.cs index 1f44ea2a2..4ae14db1a 100644 --- a/src/WireMock.Net/Util/QueryStringParser.cs +++ b/src/WireMock.Net/Util/QueryStringParser.cs @@ -1,8 +1,8 @@ using System; -using System.Net; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; +using System.Net; using WireMock.Types; namespace WireMock.Util; @@ -14,18 +14,28 @@ internal static class QueryStringParser { private static readonly Dictionary> Empty = new(); - public static bool TryParse(string? data, [NotNullWhen(true)] out IDictionary? nameValueCollection) + public static bool TryParse(string? queryString, bool caseIgnore, [NotNullWhen(true)] out IDictionary? nameValueCollection) { - if (string.IsNullOrEmpty(data)) + if (string.IsNullOrEmpty(queryString)) { nameValueCollection = default; return false; } - nameValueCollection = data! + var parts = queryString! .Split(new[] { "&" }, StringSplitOptions.RemoveEmptyEntries) - .Select(parameter => parameter.Split(new[] { '=' }, 2, StringSplitOptions.RemoveEmptyEntries)) - .ToDictionary(grouping => grouping[0], grouping => WebUtility.UrlDecode(grouping[1])); + .Select(parameter => parameter.Split('=')) + .Distinct(); + + nameValueCollection = caseIgnore ? new Dictionary(StringComparer.OrdinalIgnoreCase) : new Dictionary(); + foreach (var part in parts) + { + if (part.Length == 2) + { + nameValueCollection.Add(part[0], part[1]); + } + } + return true; } diff --git a/test/WireMock.Net.Tests/Util/QueryStringParserTests.cs b/test/WireMock.Net.Tests/Util/QueryStringParserTests.cs index 7ef71388f..58c277b4f 100644 --- a/test/WireMock.Net.Tests/Util/QueryStringParserTests.cs +++ b/test/WireMock.Net.Tests/Util/QueryStringParserTests.cs @@ -1,5 +1,5 @@ -using FluentAssertions; using System.Collections.Generic; +using FluentAssertions; using WireMock.Types; using WireMock.Util; using Xunit; @@ -8,6 +8,49 @@ namespace WireMock.Net.Tests.Util; public class QueryStringParserTests { + [Fact] + public void TryParse_Should_Parse_QueryString() + { + // Arrange + var queryString = "key1=value1&key2=value2"; + var expected = new Dictionary + { + { "key1", "value1" }, + { "key2", "value2" } + }; + + // Act + var result = QueryStringParser.TryParse(queryString, caseIgnore: false, out var actual); + + // Assert + result.Should().BeTrue(); + actual.Should().BeEquivalentTo(expected); + } + + public static IEnumerable QueryStringTestData => new List + { + new object?[] { null, false, false, null }, + new object?[] { string.Empty, false, false, null }, + new object?[] { "test", false, true, new Dictionary { { "test", "" } } }, + new object?[] { "&", false, false, null }, + new object?[] { "&&", false, false, null }, + new object?[] { "a=", false, true, new Dictionary { { "a", "" } } }, + new object?[] { "&a", false, false, null }, + new object?[] { "&a=", false, true, new Dictionary { { "a", "" } } }, + }; + + [Theory] + [MemberData(nameof(QueryStringTestData))] + public void TryParse_Should_Parse_QueryString(string queryString, bool caseIgnore, bool expectedResult, IDictionary expectedOutput) + { + // Act + var result = QueryStringParser.TryParse(queryString, caseIgnore, out var actual); + + // Assert + result.Should().Be(expectedResult); + actual.Should().BeEquivalentTo(expectedOutput); + } + [Fact] public void Parse_WithNullString() { From 7b1965f6f4240e1f902ee2f5a276fe2782098c6f Mon Sep 17 00:00:00 2001 From: Stef Heyenrath Date: Fri, 17 Mar 2023 13:08:43 +0100 Subject: [PATCH 7/8] fix tests --- src/WireMock.Net/Util/QueryStringParser.cs | 2 +- .../Util/QueryStringParserTests.cs | 35 ++++++------------- 2 files changed, 12 insertions(+), 25 deletions(-) diff --git a/src/WireMock.Net/Util/QueryStringParser.cs b/src/WireMock.Net/Util/QueryStringParser.cs index 4ae14db1a..30f862dd5 100644 --- a/src/WireMock.Net/Util/QueryStringParser.cs +++ b/src/WireMock.Net/Util/QueryStringParser.cs @@ -16,7 +16,7 @@ internal static class QueryStringParser public static bool TryParse(string? queryString, bool caseIgnore, [NotNullWhen(true)] out IDictionary? nameValueCollection) { - if (string.IsNullOrEmpty(queryString)) + if (queryString is null) { nameValueCollection = default; return false; diff --git a/test/WireMock.Net.Tests/Util/QueryStringParserTests.cs b/test/WireMock.Net.Tests/Util/QueryStringParserTests.cs index 58c277b4f..c73a0a700 100644 --- a/test/WireMock.Net.Tests/Util/QueryStringParserTests.cs +++ b/test/WireMock.Net.Tests/Util/QueryStringParserTests.cs @@ -8,35 +8,22 @@ namespace WireMock.Net.Tests.Util; public class QueryStringParserTests { - [Fact] - public void TryParse_Should_Parse_QueryString() - { - // Arrange - var queryString = "key1=value1&key2=value2"; - var expected = new Dictionary - { - { "key1", "value1" }, - { "key2", "value2" } - }; - - // Act - var result = QueryStringParser.TryParse(queryString, caseIgnore: false, out var actual); - - // Assert - result.Should().BeTrue(); - actual.Should().BeEquivalentTo(expected); - } - public static IEnumerable QueryStringTestData => new List { new object?[] { null, false, false, null }, - new object?[] { string.Empty, false, false, null }, - new object?[] { "test", false, true, new Dictionary { { "test", "" } } }, - new object?[] { "&", false, false, null }, - new object?[] { "&&", false, false, null }, + new object?[] { string.Empty, false, true, new Dictionary() }, + new object?[] { "test", false, true, new Dictionary() }, + new object?[] { "&", false, true, new Dictionary() }, + new object?[] { "&&", false, true, new Dictionary() }, new object?[] { "a=", false, true, new Dictionary { { "a", "" } } }, - new object?[] { "&a", false, false, null }, + new object?[] { "&a", false, true, new Dictionary() }, new object?[] { "&a=", false, true, new Dictionary { { "a", "" } } }, + new object?[] { "&key1=value1", false, true, new Dictionary { { "key1", "value1" } } }, + new object?[] { "key1=value1", false, true, new Dictionary { { "key1", "value1" } } }, + new object?[] { "key1=value1&key2=value2", false, true, new Dictionary { { "key1", "value1" }, { "key2", "value2" } } }, + new object?[] { "key1=value1&key2=value2&", false, true, new Dictionary { { "key1", "value1" }, { "key2", "value2" } } }, + new object?[] { "key1=value1&&key2=value2", false, true, new Dictionary { { "key1", "value1" }, { "key2", "value2" } } }, + new object?[] { "&key1=value1&key2=value2&&", false, true, new Dictionary { { "key1", "value1" }, { "key2", "value2" } } }, }; [Theory] From d28e1f8932a0ff140c09d70a313fee13e112c49f Mon Sep 17 00:00:00 2001 From: Stef Heyenrath Date: Fri, 17 Mar 2023 14:45:47 +0100 Subject: [PATCH 8/8] add tst --- .../WireMockServerTests.WithBody.cs | 133 +++++++++++------- 1 file changed, 81 insertions(+), 52 deletions(-) diff --git a/test/WireMock.Net.Tests/WireMockServerTests.WithBody.cs b/test/WireMock.Net.Tests/WireMockServerTests.WithBody.cs index d10ee314b..65457bec2 100644 --- a/test/WireMock.Net.Tests/WireMockServerTests.WithBody.cs +++ b/test/WireMock.Net.Tests/WireMockServerTests.WithBody.cs @@ -1,7 +1,9 @@ #if !NET452 +//using System; +using System.Collections.Generic; using System.Net; using System.Net.Http; -using System.Net.Http.Json; +//using System.Net.Http.Json; using System.Threading.Tasks; using FluentAssertions; using WireMock.Matchers; @@ -10,61 +12,88 @@ using WireMock.Server; using Xunit; -namespace WireMock.Net.Tests +namespace WireMock.Net.Tests; + +public partial class WireMockServerTests { - public partial class WireMockServerTests + public class DummyClass { - public class DummyClass - { - public string Hi { get; set; } - } + public string? Hi { get; set; } + } - [Fact] - public async Task WireMockServer_WithBodyAsJson_Using_PostAsJsonAsync_And_WildcardMatcher_ShouldMatch() - { - // Arrange - var server = WireMockServer.Start(); - server.Given( - Request.Create().UsingPost().WithPath("/foo").WithBody(new WildcardMatcher("*Hello*")) - ) - .RespondWith( - Response.Create().WithStatusCode(200) - ); - - var jsonObject = new DummyClass - { - Hi = "Hello World!" - }; - - // Act - var response = await new HttpClient().PostAsJsonAsync("http://localhost:" + server.Ports[0] + "/foo", jsonObject).ConfigureAwait(false); - - // Assert - response.StatusCode.Should().Be(HttpStatusCode.OK); - - server.Stop(); - } - - [Fact] - public async Task WireMockServer_WithBodyAsJson_Using_PostAsync_And_WildcardMatcher_ShouldMatch() + [Fact] + public async Task WireMockServer_WithBodyAsJson_Using_PostAsJsonAsync_And_WildcardMatcher_ShouldMatch() + { + // Arrange + var server = WireMockServer.Start(); + server.Given( + Request.Create().UsingPost().WithPath("/foo").WithBody(new WildcardMatcher("*Hello*")) + ) + .RespondWith( + Response.Create().WithStatusCode(200) + ); + + var jsonObject = new DummyClass { - // Arrange - var server = WireMockServer.Start(); - server.Given( - Request.Create().UsingPost().WithPath("/foo").WithBody(new WildcardMatcher("*Hello*")) - ) - .RespondWith( - Response.Create().WithStatusCode(200) - ); - - // Act - var response = await new HttpClient().PostAsync("http://localhost:" + server.Ports[0] + "/foo", new StringContent("{ Hi = \"Hello World\" }")).ConfigureAwait(false); - - // Assert - response.StatusCode.Should().Be(HttpStatusCode.OK); - - server.Stop(); - } + Hi = "Hello World!" + }; + + // Act + var response = await new HttpClient().PostAsJsonAsync("http://localhost:" + server.Ports[0] + "/foo", jsonObject).ConfigureAwait(false); + + // Assert + response.StatusCode.Should().Be(HttpStatusCode.OK); + + server.Stop(); + } + + [Fact] + public async Task WireMockServer_WithBodyAsJson_Using_PostAsync_And_WildcardMatcher_ShouldMatch() + { + // Arrange + var server = WireMockServer.Start(); + server.Given( + Request.Create().UsingPost().WithPath("/foo").WithBody(new WildcardMatcher("*Hello*")) + ) + .RespondWith( + Response.Create().WithStatusCode(200) + ); + + // Act + var response = await new HttpClient().PostAsync("http://localhost:" + server.Ports[0] + "/foo", new StringContent("{ Hi = \"Hello World\" }")).ConfigureAwait(false); + + // Assert + response.StatusCode.Should().Be(HttpStatusCode.OK); + + server.Stop(); + } + + [Fact] + public async Task WireMockServer_WithBodyAsFormUrlEncoded_Using_PostAsync_And_WithFunc() + { + // Arrange + var server = WireMockServer.Start(); + server.Given( + Request.Create() + .UsingPost() + .WithPath("/foo") + .WithBody(values => values != null && values["key1"] == "value1") + ) + .RespondWith( + Response.Create() + ); + + // Act + var content = new FormUrlEncodedContent(new[] { new KeyValuePair("key1", "value1") }); + var response = await new HttpClient() + .PostAsync($"{server.Url}/foo", content) + .ConfigureAwait(false); + + // Assert + response.StatusCode.Should().Be(HttpStatusCode.OK); + + server.Stop(); } } + #endif \ No newline at end of file