Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add WithBody with IDictionary (form-urlencoded values) #903

Merged
merged 8 commits into from
Mar 17, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions src/WireMock.Net.Abstractions/Models/IBodyData.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.Collections.Generic;
using System.Text;
using WireMock.Types;

Expand Down Expand Up @@ -38,6 +39,11 @@ public interface IBodyData
/// </summary>
string? BodyAsString { get; set; }

/// <summary>
/// The body as Form UrlEncoded dictionary.
/// </summary>
IDictionary<string, string>? BodyAsFormUrlEncoded { get; set; }

/// <summary>
/// The detected body type (detection based on body content).
/// </summary>
Expand Down
62 changes: 33 additions & 29 deletions src/WireMock.Net.Abstractions/Types/BodyType.cs
Original file line number Diff line number Diff line change
@@ -1,38 +1,42 @@
namespace WireMock.Types
namespace WireMock.Types;

/// <summary>
/// The BodyType
/// </summary>
public enum BodyType
{
/// <summary>
/// The BodyType
/// No body present
/// </summary>
public enum BodyType
{
/// <summary>
/// No body present
/// </summary>
None,
None,

/// <summary>
/// Body is a String
/// </summary>
String,
/// <summary>
/// Body is a String
/// </summary>
String,

/// <summary>
/// Body is a Json object
/// </summary>
Json,
/// <summary>
/// Body is a Json object
/// </summary>
Json,

/// <summary>
/// Body is a Byte array
/// </summary>
Bytes,

/// <summary>
/// Body is a Byte array
/// </summary>
Bytes,
/// <summary>
/// Body is a File
/// </summary>
File,

/// <summary>
/// Body is a File
/// </summary>
File,
/// <summary>
/// Body is a MultiPart
/// </summary>
MultiPart,

/// <summary>
/// Body is a MultiPart
/// </summary>
MultiPart
}
/// <summary>
/// Body is a String which is x-www-form-urlencoded.
/// </summary>
FormUrlEncoded
}
24 changes: 21 additions & 3 deletions src/WireMock.Net/Matchers/Request/RequestMessageBodyMatcher.cs
Original file line number Diff line number Diff line change
@@ -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;

Expand Down Expand Up @@ -33,6 +32,11 @@ public class RequestMessageBodyMatcher : IRequestMatcher
/// </summary>
public Func<IBodyData?, bool>? BodyDataFunc { get; }

/// <summary>
/// The body data function for FormUrlEncoded
/// </summary>
public Func<IDictionary<string, string>?, bool>? FormUrlEncodedFunc { get; }

/// <summary>
/// The matchers.
/// </summary>
Expand Down Expand Up @@ -109,6 +113,15 @@ public RequestMessageBodyMatcher(Func<IBodyData?, bool> func)
BodyDataFunc = Guard.NotNull(func);
}

/// <summary>
/// Initializes a new instance of the <see cref="RequestMessageBodyMatcher"/> class.
/// </summary>
/// <param name="func">The function.</param>
public RequestMessageBodyMatcher(Func<IDictionary<string, string>?, bool> func)
{
FormUrlEncodedFunc = Guard.NotNull(func);
}

/// <summary>
/// Initializes a new instance of the <see cref="RequestMessageBodyMatcher"/> class.
/// </summary>
Expand Down Expand Up @@ -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);
}
Expand All @@ -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));
Expand Down
4 changes: 4 additions & 0 deletions src/WireMock.Net/Models/BodyData.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.Collections.Generic;
using System.Text;
using WireMock.Types;

Expand All @@ -14,6 +15,9 @@ public class BodyData : IBodyData
/// <inheritdoc />
public string? BodyAsString { get; set; }

/// <inheritdoc />
public IDictionary<string, string>? BodyAsFormUrlEncoded { get; set; }

/// <inheritdoc cref="IBodyData.BodyAsJson" />
public object? BodyAsJson { get; set; }

Expand Down
16 changes: 12 additions & 4 deletions src/WireMock.Net/RequestBuilders/IBodyRequestBuilder.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using WireMock.Matchers;
using WireMock.Matchers.Request;
using WireMock.Util;
Expand Down Expand Up @@ -54,26 +55,33 @@ public interface IBodyRequestBuilder : IRequestMatcher
/// </summary>
/// <param name="func">The function.</param>
/// <returns>The <see cref="IRequestBuilder"/>.</returns>
IRequestBuilder WithBody(Func<string, bool> func);
IRequestBuilder WithBody(Func<string?, bool> func);

/// <summary>
/// WithBody: func (byte[])
/// </summary>
/// <param name="func">The function.</param>
/// <returns>The <see cref="IRequestBuilder"/>.</returns>
IRequestBuilder WithBody(Func<byte[], bool> func);
IRequestBuilder WithBody(Func<byte[]?, bool> func);

/// <summary>
/// WithBody: func (json object)
/// </summary>
/// <param name="func">The function.</param>
/// <returns>The <see cref="IRequestBuilder"/>.</returns>
IRequestBuilder WithBody(Func<object, bool> func);
IRequestBuilder WithBody(Func<object?, bool> func);

/// <summary>
/// WithBody: func (BodyData object)
/// </summary>
/// <param name="func">The function.</param>
/// <returns>The <see cref="IRequestBuilder"/>.</returns>
IRequestBuilder WithBody(Func<IBodyData, bool> func);
IRequestBuilder WithBody(Func<IBodyData?, bool> func);

/// <summary>
/// WithBody: Body as form-urlencoded values.
/// </summary>
/// <param name="func">The form-urlencoded values.</param>
/// <returns>The <see cref="IRequestBuilder"/>.</returns>
IRequestBuilder WithBody(Func<IDictionary<string, string>?, bool> func);
}
38 changes: 23 additions & 15 deletions src/WireMock.Net/RequestBuilders/Request.WithBody.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -10,21 +11,21 @@ namespace WireMock.RequestBuilders;

public partial class Request
{
/// <inheritdoc cref="IBodyRequestBuilder.WithBody(string, MatchBehaviour)"/>
/// <inheritdoc />
public IRequestBuilder WithBody(string body, MatchBehaviour matchBehaviour = MatchBehaviour.AcceptOnMatch)
{
_requestMatchers.Add(new RequestMessageBodyMatcher(matchBehaviour, body));
return this;
}

/// <inheritdoc cref="IBodyRequestBuilder.WithBody(byte[], MatchBehaviour)"/>
/// <inheritdoc />
public IRequestBuilder WithBody(byte[] body, MatchBehaviour matchBehaviour = MatchBehaviour.AcceptOnMatch)
{
_requestMatchers.Add(new RequestMessageBodyMatcher(matchBehaviour, body));
return this;
}

/// <inheritdoc cref="IBodyRequestBuilder.WithBody(object, MatchBehaviour)"/>
/// <inheritdoc />
public IRequestBuilder WithBody(object body, MatchBehaviour matchBehaviour = MatchBehaviour.AcceptOnMatch)
{
_requestMatchers.Add(new RequestMessageBodyMatcher(matchBehaviour, body));
Expand All @@ -46,39 +47,46 @@ public IRequestBuilder WithBody(IMatcher[] matchers, MatchOperator matchOperator
return this;
}

/// <inheritdoc cref="IBodyRequestBuilder.WithBody(Func{string, bool})"/>
public IRequestBuilder WithBody(Func<string, bool> func)
/// <inheritdoc />
public IRequestBuilder WithBody(Func<string?, bool> func)
{
Guard.NotNull(func, nameof(func));
Guard.NotNull(func);

_requestMatchers.Add(new RequestMessageBodyMatcher(func));
return this;
}

/// <inheritdoc cref="IBodyRequestBuilder.WithBody(Func{byte[], bool})"/>
public IRequestBuilder WithBody(Func<byte[], bool> func)
/// <inheritdoc />
public IRequestBuilder WithBody(Func<byte[]?, bool> func)
{
Guard.NotNull(func, nameof(func));
Guard.NotNull(func);

_requestMatchers.Add(new RequestMessageBodyMatcher(func));
return this;
}

/// <inheritdoc cref="IBodyRequestBuilder.WithBody(Func{object, bool})"/>
public IRequestBuilder WithBody(Func<object, bool> func)
/// <inheritdoc />
public IRequestBuilder WithBody(Func<object?, bool> func)
{
Guard.NotNull(func, nameof(func));
Guard.NotNull(func);

_requestMatchers.Add(new RequestMessageBodyMatcher(func));
return this;
}

/// <inheritdoc cref="IBodyRequestBuilder.WithBody(Func{IBodyData, bool})"/>
public IRequestBuilder WithBody(Func<IBodyData, bool> func)
/// <inheritdoc />
public IRequestBuilder WithBody(Func<IBodyData?, bool> func)
{
Guard.NotNull(func, nameof(func));
Guard.NotNull(func);

_requestMatchers.Add(new RequestMessageBodyMatcher(func));
return this;
}

/// <inheritdoc />
public IRequestBuilder WithBody(Func<IDictionary<string, string>?, bool> func)
{
_requestMatchers.Add(new RequestMessageBodyMatcher(Guard.NotNull(func)));
return this;
}
}
30 changes: 27 additions & 3 deletions src/WireMock.Net/Util/BodyParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -49,12 +49,14 @@ 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)
Expand All @@ -69,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;
}
Expand All @@ -88,6 +90,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;
Expand Down Expand Up @@ -133,13 +140,30 @@ public static async Task<BodyData> 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<string, string>
if (settings.DeserializeFormUrlEncoded &&
data.DetectedBodyTypeFromContentType == BodyType.FormUrlEncoded &&
QueryStringParser.TryParse(data.BodyAsString, false, out var nameValueCollection)
)
{
try
{
data.BodyAsFormUrlEncoded = nameValueCollection;
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))
{
Expand Down
2 changes: 2 additions & 0 deletions src/WireMock.Net/Util/BodyParserSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Loading