diff --git a/src/WireMock.Net/Matchers/ContentTypeMatcher.cs b/src/WireMock.Net/Matchers/ContentTypeMatcher.cs new file mode 100644 index 000000000..3f0f1cfcb --- /dev/null +++ b/src/WireMock.Net/Matchers/ContentTypeMatcher.cs @@ -0,0 +1,73 @@ +using System.Net.Http.Headers; +using JetBrains.Annotations; + +namespace WireMock.Matchers +{ + /// + /// ContentTypeMatcher which accepts also all charsets + /// + /// + public class ContentTypeMatcher : WildcardMatcher + { + private readonly string[] _patterns; + + /// + /// Initializes a new instance of the class. + /// + /// The pattern. + /// IgnoreCase (default false) + public ContentTypeMatcher([NotNull] string pattern, bool ignoreCase = false) : this(new[] { pattern }, ignoreCase) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The match behaviour. + /// The pattern. + /// IgnoreCase (default false) + public ContentTypeMatcher(MatchBehaviour matchBehaviour, [NotNull] string pattern, bool ignoreCase = false) : this(matchBehaviour, new[] { pattern }, ignoreCase) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The patterns. + /// IgnoreCase (default false) + public ContentTypeMatcher([NotNull] string[] patterns, bool ignoreCase = false) : this(MatchBehaviour.AcceptOnMatch, patterns, ignoreCase) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The match behaviour. + /// The patterns. + /// IgnoreCase (default false) + public ContentTypeMatcher(MatchBehaviour matchBehaviour, [NotNull] string[] patterns, bool ignoreCase = false) : base(matchBehaviour, patterns, ignoreCase) + { + _patterns = patterns; + } + + /// + 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); + } + + /// + public override string[] GetPatterns() + { + return _patterns; + } + + /// + public override string Name => "ContentTypeMatcher"; + } +} \ No newline at end of file diff --git a/src/WireMock.Net/Matchers/RegexMatcher.cs b/src/WireMock.Net/Matchers/RegexMatcher.cs index 4947cdc4b..9101a112c 100644 --- a/src/WireMock.Net/Matchers/RegexMatcher.cs +++ b/src/WireMock.Net/Matchers/RegexMatcher.cs @@ -71,7 +71,7 @@ public RegexMatcher(MatchBehaviour matchBehaviour, [NotNull, RegexPattern] strin } /// - public double IsMatch(string input) + public virtual double IsMatch(string input) { double match = MatchScores.Mismatch; if (input != null) diff --git a/src/WireMock.Net/Serialization/MatcherMapper.cs b/src/WireMock.Net/Serialization/MatcherMapper.cs index 4b63aff08..2cab19b61 100644 --- a/src/WireMock.Net/Serialization/MatcherMapper.cs +++ b/src/WireMock.Net/Serialization/MatcherMapper.cs @@ -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)) diff --git a/src/WireMock.Net/Server/FluentMockServer.Admin.cs b/src/WireMock.Net/Server/FluentMockServer.Admin.cs index 9cc2a399f..8ee0a65b3 100644 --- a/src/WireMock.Net/Server/FluentMockServer.Admin.cs +++ b/src/WireMock.Net/Server/FluentMockServer.Admin.cs @@ -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 { @@ -60,11 +61,11 @@ 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 @@ -72,7 +73,7 @@ private void InitAdmin() // __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 diff --git a/test/WireMock.Net.Tests/FluentMockServerTests.cs b/test/WireMock.Net.Tests/FluentMockServerTests.cs index 22ddc1d79..8d7ecf2e4 100644 --- a/test/WireMock.Net.Tests/FluentMockServerTests.cs +++ b/test/WireMock.Net.Tests/FluentMockServerTests.cs @@ -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; @@ -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 @@ -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")] @@ -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"); + } } } \ No newline at end of file diff --git a/test/WireMock.Net.Tests/Matchers/ContentTypeMatcherTests.cs b/test/WireMock.Net.Tests/Matchers/ContentTypeMatcherTests.cs new file mode 100644 index 000000000..890a05e79 --- /dev/null +++ b/test/WireMock.Net.Tests/Matchers/ContentTypeMatcherTests.cs @@ -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"); + } + } +} \ No newline at end of file