From d628ce2270fc491a82c40f8c96cb4f8637efa964 Mon Sep 17 00:00:00 2001 From: Stef Heyenrath Date: Wed, 21 May 2025 17:28:29 +0200 Subject: [PATCH] Fix BodyParser to correctly check for json (#1297) * Fix BodyParser to correctly check for json * JsonUtils --- .../Models/IBodyDataExtensions.cs | 14 +---- .../Http/HttpRequestMessageHelper.cs | 2 +- .../Owin/Mappers/OwinResponseMapper.cs | 4 +- src/WireMock.Net/Util/BodyParser.cs | 4 +- src/WireMock.Net/Util/JsonUtils.cs | 20 ++++--- .../Http/HttpRequestMessageHelperTests.cs | 54 +++++++++++++------ .../Util/BodyParserTests.cs | 37 ++++++------- 7 files changed, 76 insertions(+), 59 deletions(-) diff --git a/src/WireMock.Net.Abstractions/Models/IBodyDataExtensions.cs b/src/WireMock.Net.Abstractions/Models/IBodyDataExtensions.cs index eabfce47..624bc2ec 100644 --- a/src/WireMock.Net.Abstractions/Models/IBodyDataExtensions.cs +++ b/src/WireMock.Net.Abstractions/Models/IBodyDataExtensions.cs @@ -8,18 +8,8 @@ namespace WireMock.Util; // ReSharper disable once InconsistentNaming public static class IBodyDataExtensions { - public static BodyType GetBodyType(this IBodyData bodyData) + public static BodyType GetDetectedBodyType(this IBodyData bodyData) { - if (bodyData.DetectedBodyTypeFromContentType is not null and not BodyType.None) - { - return bodyData.DetectedBodyTypeFromContentType.Value; - } - - if (bodyData.DetectedBodyType is not null and not BodyType.None) - { - return bodyData.DetectedBodyType.Value; - } - - return BodyType.None; + return bodyData.DetectedBodyType ?? BodyType.None; } } \ No newline at end of file diff --git a/src/WireMock.Net/Http/HttpRequestMessageHelper.cs b/src/WireMock.Net/Http/HttpRequestMessageHelper.cs index e73e1f4c..7c8f051a 100644 --- a/src/WireMock.Net/Http/HttpRequestMessageHelper.cs +++ b/src/WireMock.Net/Http/HttpRequestMessageHelper.cs @@ -35,7 +35,7 @@ internal static class HttpRequestMessageHelper } var bodyData = requestMessage.BodyData; - httpRequestMessage.Content = bodyData?.GetBodyType() switch + httpRequestMessage.Content = bodyData?.DetectedBodyType switch { BodyType.Bytes => ByteArrayContentHelper.Create(bodyData.BodyAsBytes!, contentType), BodyType.Json => StringContentHelper.Create(JsonConvert.SerializeObject(bodyData.BodyAsJson), contentType), diff --git a/src/WireMock.Net/Owin/Mappers/OwinResponseMapper.cs b/src/WireMock.Net/Owin/Mappers/OwinResponseMapper.cs index 91d13ef4..7a5025c6 100644 --- a/src/WireMock.Net/Owin/Mappers/OwinResponseMapper.cs +++ b/src/WireMock.Net/Owin/Mappers/OwinResponseMapper.cs @@ -70,7 +70,7 @@ namespace WireMock.Owin.Mappers } var bodyData = responseMessage.BodyData; - if (bodyData?.GetBodyType() == BodyType.SseString) + if (bodyData?.GetDetectedBodyType() == BodyType.SseString) { await HandleSseStringAsync(responseMessage, response, bodyData); return; @@ -166,7 +166,7 @@ namespace WireMock.Owin.Mappers private async Task GetNormalBodyAsync(IResponseMessage responseMessage) { var bodyData = responseMessage.BodyData; - switch (bodyData?.GetBodyType()) + switch (bodyData?.GetDetectedBodyType()) { case BodyType.String: case BodyType.FormUrlEncoded: diff --git a/src/WireMock.Net/Util/BodyParser.cs b/src/WireMock.Net/Util/BodyParser.cs index 12715c76..c5b49829 100644 --- a/src/WireMock.Net/Util/BodyParser.cs +++ b/src/WireMock.Net/Util/BodyParser.cs @@ -177,7 +177,7 @@ internal static class BodyParser } // If string is not null or empty, try to deserialize the string to a JObject - if (settings.DeserializeJson && !string.IsNullOrEmpty(data.BodyAsString)) + if (settings.DeserializeJson && JsonUtils.IsJson(data.BodyAsString)) { try { @@ -197,7 +197,7 @@ internal static class BodyParser return data; } - + private static async Task<(string? ContentType, byte[] Bytes)> ReadBytesAsync(Stream stream, string? contentEncoding = null, bool decompressGZipAndDeflate = true) { using var memoryStream = new MemoryStream(); diff --git a/src/WireMock.Net/Util/JsonUtils.cs b/src/WireMock.Net/Util/JsonUtils.cs index e47bee0c..0c5e9571 100644 --- a/src/WireMock.Net/Util/JsonUtils.cs +++ b/src/WireMock.Net/Util/JsonUtils.cs @@ -12,17 +12,23 @@ namespace WireMock.Util; internal static class JsonUtils { - public static bool TryParseAsJObject(string? strInput, [NotNullWhen(true)] out JObject? value) + public static bool IsJson(string? value) { - value = null; - - if (strInput == null || string.IsNullOrWhiteSpace(strInput)) + if (string.IsNullOrWhiteSpace(value)) { return false; } - strInput = strInput.Trim(); - if ((!strInput.StartsWith("{") || !strInput.EndsWith("}")) && (!strInput.StartsWith("[") || !strInput.EndsWith("]"))) + value = value!.Trim(); + + return (value.StartsWith("{") && value.EndsWith("}")) || (value.StartsWith("[") && value.EndsWith("]")); + } + + public static bool TryParseAsJObject(string? strInput, [NotNullWhen(true)] out JObject? value) + { + value = null; + + if (!IsJson(strInput)) { return false; } @@ -30,7 +36,7 @@ internal static class JsonUtils try { // Try to convert this string into a JToken - value = JObject.Parse(strInput); + value = JObject.Parse(strInput!); return true; } catch diff --git a/test/WireMock.Net.Tests/Http/HttpRequestMessageHelperTests.cs b/test/WireMock.Net.Tests/Http/HttpRequestMessageHelperTests.cs index be2c4eb3..25470cb4 100644 --- a/test/WireMock.Net.Tests/Http/HttpRequestMessageHelperTests.cs +++ b/test/WireMock.Net.Tests/Http/HttpRequestMessageHelperTests.cs @@ -32,7 +32,7 @@ public class HttpRequestMessageHelperTests } [Fact] - public async Task HttpRequestMessageHelper_Create_Bytes() + public async Task HttpRequestMessageHelper_Create_Bytes_Without_ContentTypeHeader() { // Assign var body = new BodyData @@ -40,27 +40,25 @@ public class HttpRequestMessageHelperTests BodyAsBytes = Encoding.UTF8.GetBytes("hi"), DetectedBodyType = BodyType.Bytes }; - var request = new RequestMessage(new UrlDetails("http://localhost/foo"), "GET", ClientIp, body); + var request = new RequestMessage(new UrlDetails("http://localhost/foo"), "POST", ClientIp, body); // Act var message = HttpRequestMessageHelper.Create(request, "http://url"); // Assert - Check.That(await message.Content.ReadAsByteArrayAsync().ConfigureAwait(false)).ContainsExactly(Encoding.UTF8.GetBytes("hi")); + Check.That(await message.Content!.ReadAsByteArrayAsync().ConfigureAwait(false)).ContainsExactly(Encoding.UTF8.GetBytes("hi")); } [Fact] - public async Task HttpRequestMessageHelper_Create_TextPlain() + public async Task HttpRequestMessageHelper_Create_TextPlain_Without_ContentTypeHeader() { // Assign var body = new BodyData { BodyAsString = "0123", // or 83 in decimal - BodyAsJson = 83, - DetectedBodyType = BodyType.Json, - DetectedBodyTypeFromContentType = BodyType.String + DetectedBodyType = BodyType.String }; - var request = new RequestMessage(new UrlDetails("http://localhost/foo"), "GET", ClientIp, body); + var request = new RequestMessage(new UrlDetails("http://localhost/foo"), "POST", ClientIp, body); // Act var message = HttpRequestMessageHelper.Create(request, "http://url"); @@ -78,7 +76,7 @@ public class HttpRequestMessageHelperTests BodyAsJson = new { x = 42 }, DetectedBodyType = BodyType.Json }; - var request = new RequestMessage(new UrlDetails("http://localhost/foo"), "GET", ClientIp, body); + var request = new RequestMessage(new UrlDetails("http://localhost/foo"), "POST", ClientIp, body); // Act var message = HttpRequestMessageHelper.Create(request, "http://url"); @@ -97,13 +95,13 @@ public class HttpRequestMessageHelperTests BodyAsJson = new { x = 42 }, DetectedBodyType = BodyType.Json }; - var request = new RequestMessage(new UrlDetails("http://localhost/foo"), "GET", ClientIp, body, headers); + var request = new RequestMessage(new UrlDetails("http://localhost/foo"), "POST", ClientIp, body, headers); // Act var message = HttpRequestMessageHelper.Create(request, "http://url"); // Assert - Check.That(await message.Content.ReadAsStringAsync().ConfigureAwait(false)).Equals("{\"x\":42}"); + Check.That(await message.Content!.ReadAsStringAsync().ConfigureAwait(false)).Equals("{\"x\":42}"); Check.That(message.Content.Headers.GetValues("Content-Type")).ContainsExactly("application/json"); } @@ -117,16 +115,38 @@ public class HttpRequestMessageHelperTests BodyAsJson = new { x = 42 }, DetectedBodyType = BodyType.Json }; - var request = new RequestMessage(new UrlDetails("http://localhost/foo"), "GET", ClientIp, body, headers); + var request = new RequestMessage(new UrlDetails("http://localhost/foo"), "POST", ClientIp, body, headers); // Act var message = HttpRequestMessageHelper.Create(request, "http://url"); // Assert - Check.That(await message.Content.ReadAsStringAsync().ConfigureAwait(false)).Equals("{\"x\":42}"); + Check.That(await message.Content!.ReadAsStringAsync().ConfigureAwait(false)).Equals("{\"x\":42}"); Check.That(message.Content.Headers.GetValues("Content-Type")).ContainsExactly("application/json; charset=utf-8"); } + [Fact] + public async Task HttpRequestMessageHelper_Create_Json_With_ContentType_MultiPartFormData() + { + // Assign + var headers = new Dictionary { { "Content-Type", ["multipart/form-data"] } }; + var body = new BodyData + { + BodyAsJson = new { x = 42 }, + DetectedBodyTypeFromContentType = BodyType.MultiPart, + DetectedBodyType = BodyType.Json + }; + var request = new RequestMessage(new UrlDetails("http://localhost/foo"), "POST", ClientIp, body, headers); + + // Act + var message = HttpRequestMessageHelper.Create(request, "http://url"); + + // Assert + Check.That(await message.Content!.ReadAsStringAsync().ConfigureAwait(false)).Equals("{\"x\":42}"); + Check.That(message.Content.Headers.GetValues("Content-Type")).ContainsExactly("multipart/form-data"); + } + + [Fact] public void HttpRequestMessageHelper_Create_String_With_ContentType_ApplicationXml() { @@ -143,7 +163,7 @@ public class HttpRequestMessageHelperTests var message = HttpRequestMessageHelper.Create(request, "http://url"); // Assert - Check.That(message.Content.Headers.GetValues("Content-Type")).ContainsExactly("application/xml"); + Check.That(message.Content!.Headers.GetValues("Content-Type")).ContainsExactly("application/xml"); } [Fact] @@ -162,7 +182,7 @@ public class HttpRequestMessageHelperTests var message = HttpRequestMessageHelper.Create(request, "http://url"); // Assert - Check.That(message.Content.Headers.GetValues("Content-Type")).ContainsExactly("application/xml; charset=UTF-8"); + Check.That(message.Content!.Headers.GetValues("Content-Type")).ContainsExactly("application/xml; charset=UTF-8"); } [Fact] @@ -181,7 +201,7 @@ public class HttpRequestMessageHelperTests var message = HttpRequestMessageHelper.Create(request, "http://url"); // Assert - Check.That(message.Content.Headers.GetValues("Content-Type")).ContainsExactly("application/xml; charset=Ascii"); + Check.That(message.Content!.Headers.GetValues("Content-Type")).ContainsExactly("application/xml; charset=Ascii"); } [Fact] @@ -222,7 +242,7 @@ public class HttpRequestMessageHelperTests var message = HttpRequestMessageHelper.Create(request, "http://url"); // Assert - Check.That(await message.Content.ReadAsStringAsync().ConfigureAwait(false)).Equals(body); + Check.That(await message.Content!.ReadAsStringAsync().ConfigureAwait(false)).Equals(body); Check.That(message.Content.Headers.GetValues("Content-Type")).ContainsExactly("multipart/form-data"); } diff --git a/test/WireMock.Net.Tests/Util/BodyParserTests.cs b/test/WireMock.Net.Tests/Util/BodyParserTests.cs index c2046f35..af5ae34a 100644 --- a/test/WireMock.Net.Tests/Util/BodyParserTests.cs +++ b/test/WireMock.Net.Tests/Util/BodyParserTests.cs @@ -1,12 +1,11 @@ // Copyright © WireMock.Net using System; -using NFluent; using System.IO; -using System.IO.Compression; using System.Text; using System.Threading.Tasks; using FluentAssertions; +using NFluent; using WireMock.Types; using WireMock.Util; using Xunit; @@ -67,7 +66,7 @@ public class BodyParserTests } [Theory] - [InlineData(new byte[] { 34, 97, 34 }, BodyType.Json)] + [InlineData(new byte[] { 123, 32, 34, 120, 34, 58, 32, 49, 50, 51, 32, 125 }, BodyType.Json)] [InlineData(new byte[] { 97 }, BodyType.String)] [InlineData(new byte[] { 0xFF, 0xD8, 0xFF, 0xE0 }, BodyType.Bytes)] public async Task BodyParser_Parse_DetectedBodyType(byte[] content, BodyType detectedBodyType) @@ -88,7 +87,7 @@ public class BodyParserTests } [Theory] - [InlineData(new byte[] { 34, 97, 34 }, BodyType.String)] + [InlineData(new byte[] { 123, 32, 34, 120, 34, 58, 32, 49, 50, 51, 32, 125 }, BodyType.String)] [InlineData(new byte[] { 97 }, BodyType.String)] [InlineData(new byte[] { 0xFF, 0xD8, 0xFF, 0xE0 }, BodyType.Bytes)] public async Task BodyParser_Parse_DetectedBodyTypeNoJsonParsing(byte[] content, BodyType detectedBodyType) @@ -112,26 +111,28 @@ public class BodyParserTests public async Task BodyParser_Parse_WithUTF8EncodingAndContentTypeMultipart_DetectedBodyTypeEqualsString() { // Arrange - string contentType = "multipart/form-data"; - string body = @" + var contentType = "multipart/form-data"; + var body = + """ ------------------------------9051914041544843365972754266 -Content-Disposition: form-data; name=""text"" + -----------------------------9051914041544843365972754266 + Content-Disposition: form-data; name="text" -text default ------------------------------9051914041544843365972754266 -Content-Disposition: form-data; name=""file1""; filename=""a.txt"" -Content-Type: text/plain + text default + -----------------------------9051914041544843365972754266 + Content-Disposition: form-data; name="file1"; filename="a.txt" + Content-Type: text/plain -Content of a txt + Content of a txt ------------------------------9051914041544843365972754266 -Content-Disposition: form-data; name=""file2""; filename=""a.html"" -Content-Type: text/html + -----------------------------9051914041544843365972754266 + Content-Disposition: form-data; name="file2"; filename="a.html" + Content-Type: text/html -Content of a.html. + Content of a.html. ------------------------------9051914041544843365972754266--"; + -----------------------------9051914041544843365972754266-- + """; var bodyParserSettings = new BodyParserSettings {