Skip to content

Commit

Permalink
Fixed failing admin requests when content type includes a charset (ba…
Browse files Browse the repository at this point in the history
…sed on idea from Paul Roub) (#353)

* .

* #350

* fix

* .
  • Loading branch information
StefH authored Oct 5, 2019
1 parent 0a9214e commit 3cc361e
Show file tree
Hide file tree
Showing 6 changed files with 179 additions and 10 deletions.
73 changes: 73 additions & 0 deletions src/WireMock.Net/Matchers/ContentTypeMatcher.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
using System.Net.Http.Headers;
using JetBrains.Annotations;

namespace WireMock.Matchers
{
/// <summary>
/// ContentTypeMatcher which accepts also all charsets
/// </summary>
/// <seealso cref="RegexMatcher" />
public class ContentTypeMatcher : WildcardMatcher
{
private readonly string[] _patterns;

/// <summary>
/// Initializes a new instance of the <see cref="ContentTypeMatcher"/> class.
/// </summary>
/// <param name="pattern">The pattern.</param>
/// <param name="ignoreCase">IgnoreCase (default false)</param>
public ContentTypeMatcher([NotNull] string pattern, bool ignoreCase = false) : this(new[] { pattern }, ignoreCase)
{
}

/// <summary>
/// Initializes a new instance of the <see cref="ContentTypeMatcher"/> class.
/// </summary>
/// <param name="matchBehaviour">The match behaviour.</param>
/// <param name="pattern">The pattern.</param>
/// <param name="ignoreCase">IgnoreCase (default false)</param>
public ContentTypeMatcher(MatchBehaviour matchBehaviour, [NotNull] string pattern, bool ignoreCase = false) : this(matchBehaviour, new[] { pattern }, ignoreCase)
{
}

/// <summary>
/// Initializes a new instance of the <see cref="ContentTypeMatcher"/> class.
/// </summary>
/// <param name="patterns">The patterns.</param>
/// <param name="ignoreCase">IgnoreCase (default false)</param>
public ContentTypeMatcher([NotNull] string[] patterns, bool ignoreCase = false) : this(MatchBehaviour.AcceptOnMatch, patterns, ignoreCase)
{
}

/// <summary>
/// Initializes a new instance of the <see cref="ContentTypeMatcher"/> class.
/// </summary>
/// <param name="matchBehaviour">The match behaviour.</param>
/// <param name="patterns">The patterns.</param>
/// <param name="ignoreCase">IgnoreCase (default false)</param>
public ContentTypeMatcher(MatchBehaviour matchBehaviour, [NotNull] string[] patterns, bool ignoreCase = false) : base(matchBehaviour, patterns, ignoreCase)
{
_patterns = patterns;
}

/// <inheritdoc cref="RegexMatcher.IsMatch"/>
public override double IsMatch(string input)
{
if (string.IsNullOrEmpty(input) || !MediaTypeHeaderValue.TryParse(input, out MediaTypeHeaderValue contentType))
{
return MatchBehaviourHelper.Convert(MatchBehaviour, MatchScores.Mismatch);
}

return base.IsMatch(contentType.MediaType);
}

/// <inheritdoc cref="IStringMatcher.GetPatterns"/>
public override string[] GetPatterns()
{
return _patterns;
}

/// <inheritdoc cref="IMatcher.Name"/>
public override string Name => "ContentTypeMatcher";
}
}
2 changes: 1 addition & 1 deletion src/WireMock.Net/Matchers/RegexMatcher.cs
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ public RegexMatcher(MatchBehaviour matchBehaviour, [NotNull, RegexPattern] strin
}

/// <inheritdoc cref="IStringMatcher.IsMatch"/>
public double IsMatch(string input)
public virtual double IsMatch(string input)
{
double match = MatchScores.Mismatch;
if (input != null)
Expand Down
3 changes: 3 additions & 0 deletions src/WireMock.Net/Serialization/MatcherMapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,9 @@ public IMatcher Map([CanBeNull] MatcherModel matcher)
case "WildcardMatcher":
return new WildcardMatcher(matchBehaviour, stringPatterns, matcher.IgnoreCase == true);

case "ContentTypeMatcher":
return new ContentTypeMatcher(matchBehaviour, stringPatterns, matcher.IgnoreCase == true);

case "SimMetricsMatcher":
SimMetricType type = SimMetricType.Levenstein;
if (!string.IsNullOrEmpty(matcherType) && !Enum.TryParse(matcherType, out type))
Expand Down
11 changes: 6 additions & 5 deletions src/WireMock.Net/Server/FluentMockServer.Admin.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,9 @@ public partial class FluentMockServer
private const string AdminSettings = "/__admin/settings";
private const string AdminScenarios = "/__admin/scenarios";

private readonly RegexMatcher _adminMappingsGuidPathMatcher = new RegexMatcher(MatchBehaviour.AcceptOnMatch, @"^\/__admin\/mappings\/([0-9A-Fa-f]{8}[-][0-9A-Fa-f]{4}[-][0-9A-Fa-f]{4}[-][0-9A-Fa-f]{4}[-][0-9A-Fa-f]{12})$");
private readonly RegexMatcher _adminRequestsGuidPathMatcher = new RegexMatcher(MatchBehaviour.AcceptOnMatch, @"^\/__admin\/requests\/([0-9A-Fa-f]{8}[-][0-9A-Fa-f]{4}[-][0-9A-Fa-f]{4}[-][0-9A-Fa-f]{4}[-][0-9A-Fa-f]{12})$");
private readonly RegexMatcher _adminRequestContentTypeJson = new ContentTypeMatcher(ContentTypeJson, true);
private readonly RegexMatcher _adminMappingsGuidPathMatcher = new RegexMatcher(@"^\/__admin\/mappings\/([0-9A-Fa-f]{8}[-][0-9A-Fa-f]{4}[-][0-9A-Fa-f]{4}[-][0-9A-Fa-f]{4}[-][0-9A-Fa-f]{12})$");
private readonly RegexMatcher _adminRequestsGuidPathMatcher = new RegexMatcher(@"^\/__admin\/requests\/([0-9A-Fa-f]{8}[-][0-9A-Fa-f]{4}[-][0-9A-Fa-f]{4}[-][0-9A-Fa-f]{4}[-][0-9A-Fa-f]{12})$");

private readonly JsonSerializerSettings _jsonSerializerSettings = new JsonSerializerSettings
{
Expand All @@ -60,19 +61,19 @@ private void InitAdmin()
{
// __admin/settings
Given(Request.Create().WithPath(AdminSettings).UsingGet()).AtPriority(AdminPriority).RespondWith(new DynamicResponseProvider(SettingsGet));
Given(Request.Create().WithPath(AdminSettings).UsingMethod("PUT", "POST").WithHeader(HttpKnownHeaderNames.ContentType, ContentTypeJson)).AtPriority(AdminPriority).RespondWith(new DynamicResponseProvider(SettingsUpdate));
Given(Request.Create().WithPath(AdminSettings).UsingMethod("PUT", "POST").WithHeader(HttpKnownHeaderNames.ContentType, _adminRequestContentTypeJson)).AtPriority(AdminPriority).RespondWith(new DynamicResponseProvider(SettingsUpdate));

// __admin/mappings
Given(Request.Create().WithPath(AdminMappings).UsingGet()).AtPriority(AdminPriority).RespondWith(new DynamicResponseProvider(MappingsGet));
Given(Request.Create().WithPath(AdminMappings).UsingPost().WithHeader(HttpKnownHeaderNames.ContentType, ContentTypeJson)).AtPriority(AdminPriority).RespondWith(new DynamicResponseProvider(MappingsPost));
Given(Request.Create().WithPath(AdminMappings).UsingPost().WithHeader(HttpKnownHeaderNames.ContentType, _adminRequestContentTypeJson)).AtPriority(AdminPriority).RespondWith(new DynamicResponseProvider(MappingsPost));
Given(Request.Create().WithPath(AdminMappings).UsingDelete()).AtPriority(AdminPriority).RespondWith(new DynamicResponseProvider(MappingsDelete));

// __admin/mappings/reset
Given(Request.Create().WithPath(AdminMappings + "/reset").UsingPost()).AtPriority(AdminPriority).RespondWith(new DynamicResponseProvider(MappingsDelete));

// __admin/mappings/{guid}
Given(Request.Create().WithPath(_adminMappingsGuidPathMatcher).UsingGet()).AtPriority(AdminPriority).RespondWith(new DynamicResponseProvider(MappingGet));
Given(Request.Create().WithPath(_adminMappingsGuidPathMatcher).UsingPut().WithHeader(HttpKnownHeaderNames.ContentType, ContentTypeJson)).AtPriority(AdminPriority).RespondWith(new DynamicResponseProvider(MappingPut));
Given(Request.Create().WithPath(_adminMappingsGuidPathMatcher).UsingPut().WithHeader(HttpKnownHeaderNames.ContentType, _adminRequestContentTypeJson)).AtPriority(AdminPriority).RespondWith(new DynamicResponseProvider(MappingPut));
Given(Request.Create().WithPath(_adminMappingsGuidPathMatcher).UsingDelete()).AtPriority(AdminPriority).RespondWith(new DynamicResponseProvider(MappingDelete));

// __admin/mappings/save
Expand Down
42 changes: 38 additions & 4 deletions test/WireMock.Net.Tests/FluentMockServerTests.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
using NFluent;
using System;
using System;
using System.Diagnostics;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading.Tasks;
using NFluent;
using WireMock.RequestBuilders;
using WireMock.ResponseBuilders;
using WireMock.Server;
Expand Down Expand Up @@ -187,7 +188,7 @@ public async Task FluentMockServer_Should_exclude_body_for_methods_where_body_is
var server = FluentMockServer.Start();

server
.Given(Request.Create().WithBody((byte[] bodyBytes) => bodyBytes != null))
.Given(Request.Create().WithBody((byte[] bodyBytes) => bodyBytes != null))
.AtPriority(0)
.RespondWith(Response.Create().WithStatusCode(400));
server
Expand All @@ -203,7 +204,7 @@ public async Task FluentMockServer_Should_exclude_body_for_methods_where_body_is
// Assert
Check.That(response.StatusCode).Equals(HttpStatusCode.OK);
}

[Theory]
[InlineData("POST")]
[InlineData("PUT")]
Expand Down Expand Up @@ -233,5 +234,38 @@ public async Task FluentMockServer_Should_not_exclude_body_for_supported_methods
// Assert
Check.That(response.StatusCode).Equals(HttpStatusCode.OK);
}

[Theory]
[InlineData("application/json")]
[InlineData("application/json; charset=ascii")]
[InlineData("application/json; charset=utf-8")]
[InlineData("application/json; charset=UTF-8")]
public async Task WireMockServer_Should_AcceptPostMappingsWithContentTypeJsonAndAnyCharset(string contentType)
{
// Arrange
string message = @"{
""request"": {
""method"": ""GET"",
""url"": ""/some/thing""
},
""response"": {
""status"": 200,
""body"": ""Hello world!"",
""headers"": {
""Content-Type"": ""text/plain""
}
}
}";
var stringContent = new StringContent(message);
stringContent.Headers.ContentType = MediaTypeHeaderValue.Parse(contentType);
var server = FluentMockServer.StartWithAdminInterface();

// Act
var response = await new HttpClient().PostAsync($"{server.Urls[0]}/__admin/mappings", stringContent);

// Assert
Check.That(response.StatusCode).Equals(HttpStatusCode.Created);
Check.That(await response.Content.ReadAsStringAsync()).Contains("Mapping added");
}
}
}
58 changes: 58 additions & 0 deletions test/WireMock.Net.Tests/Matchers/ContentTypeMatcherTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
using NFluent;
using WireMock.Matchers;
using Xunit;

namespace WireMock.Net.Tests.Matchers
{
public class ContentTypeMatcherTests
{
[Theory]
[InlineData("application/json")]
[InlineData("application/json; charset=ascii")]
[InlineData("application/json; charset=utf-8")]
[InlineData("application/json; charset=UTF-8")]
public void ContentTypeMatcher_IsMatchWithIgnoreCaseFalse_Positive(string contentType)
{
var matcher = new ContentTypeMatcher("application/json");
Check.That(matcher.IsMatch(contentType)).IsEqualTo(1.0d);
}

[Theory]
[InlineData("application/json")]
[InlineData("application/JSON")]
[InlineData("application/json; CharSet=ascii")]
[InlineData("application/json; charset=utf-8")]
[InlineData("application/json; charset=UTF-8")]
public void ContentTypeMatcher_IsMatchWithIgnoreCaseTrue_Positive(string contentType)
{
var matcher = new ContentTypeMatcher("application/json", true);
Check.That(matcher.IsMatch(contentType)).IsEqualTo(1.0d);
}

[Fact]
public void ContentTypeMatcher_GetName()
{
// Assign
var matcher = new ContentTypeMatcher("x");

// Act
string name = matcher.Name;

// Assert
Check.That(name).Equals("ContentTypeMatcher");
}

[Fact]
public void ContentTypeMatcher_GetPatterns()
{
// Assign
var matcher = new ContentTypeMatcher("x");

// Act
string[] patterns = matcher.GetPatterns();

// Assert
Check.That(patterns).ContainsExactly("x");
}
}
}

0 comments on commit 3cc361e

Please sign in to comment.