From 80f396c8e31894d60bae365ab2ea80e67a9758c6 Mon Sep 17 00:00:00 2001 From: skwasjer <11424653+skwasjer@users.noreply.github.com> Date: Sun, 19 Nov 2023 04:59:36 +0100 Subject: [PATCH 1/4] refactor: move Uri normalization to helper --- src/MockHttp/Http/UriExtensions.cs | 34 ++++++++++++++ src/MockHttp/Matchers/RequestUriMatcher.cs | 32 ++++---------- .../MockHttp.Tests/Http/UriExtensionsTests.cs | 44 +++++++++++++++++++ 3 files changed, 86 insertions(+), 24 deletions(-) create mode 100644 src/MockHttp/Http/UriExtensions.cs create mode 100644 test/MockHttp.Tests/Http/UriExtensionsTests.cs diff --git a/src/MockHttp/Http/UriExtensions.cs b/src/MockHttp/Http/UriExtensions.cs new file mode 100644 index 00000000..3c1c74df --- /dev/null +++ b/src/MockHttp/Http/UriExtensions.cs @@ -0,0 +1,34 @@ +namespace MockHttp.Http; + +internal static class UriExtensions +{ + private const char UriSegmentDelimiter = '/'; + internal static readonly UriKind DotNetRelativeOrAbsolute = Type.GetType("Mono.Runtime") == null ? UriKind.RelativeOrAbsolute : (UriKind)300; + + /// + /// If a relative URI, ensures it starts with a forward slash (/). If not, returns the original . + /// + /// + /// + /// + internal static Uri EnsureIsRooted(this Uri uri) + { + if (uri is null) + { + throw new ArgumentNullException(nameof(uri)); + } + + if (uri.IsAbsoluteUri) + { + return uri; + } + + string relUri = uri.ToString(); + if (relUri.Length > 0 && relUri[0] != UriSegmentDelimiter) + { + return new Uri($"{UriSegmentDelimiter}{relUri}", UriKind.Relative); + } + + return uri; + } +} diff --git a/src/MockHttp/Matchers/RequestUriMatcher.cs b/src/MockHttp/Matchers/RequestUriMatcher.cs index b9996dd9..2e36e934 100644 --- a/src/MockHttp/Matchers/RequestUriMatcher.cs +++ b/src/MockHttp/Matchers/RequestUriMatcher.cs @@ -1,6 +1,8 @@ using System.Diagnostics; +using MockHttp.Http; using MockHttp.Matchers.Patterns; using MockHttp.Responses; +using static MockHttp.Http.UriExtensions; namespace MockHttp.Matchers; @@ -9,14 +11,10 @@ namespace MockHttp.Matchers; /// public class RequestUriMatcher : HttpRequestMatcher { - private static readonly UriKind DotNetRelativeOrAbsolute = Type.GetType("Mono.Runtime") == null ? UriKind.RelativeOrAbsolute : (UriKind)300; - - private const char UriSegmentDelimiter = '/'; - [DebuggerBrowsable(DebuggerBrowsableState.Never)] - private Uri _requestUri = default!; + private readonly Uri _requestUri = default!; [DebuggerBrowsable(DebuggerBrowsableState.Never)] - private string _formattedUri = default!; + private readonly string _formattedUri; [DebuggerBrowsable(DebuggerBrowsableState.Never)] private readonly PatternMatcher? _uriPatternMatcher; @@ -26,7 +24,8 @@ public class RequestUriMatcher : HttpRequestMatcher /// The request URI. public RequestUriMatcher(Uri uri) { - SetRequestUri(uri); + _requestUri = uri.EnsureIsRooted(); + _formattedUri = _requestUri.ToString(); } /// @@ -51,26 +50,11 @@ public RequestUriMatcher(string uriString, bool allowWildcards = true) else { // If no wildcards, then must be actual uri. - SetRequestUri(new Uri(uriString, DotNetRelativeOrAbsolute)); + _requestUri = new Uri(uriString, DotNetRelativeOrAbsolute).EnsureIsRooted(); + _formattedUri = _requestUri.ToString(); } } - private void SetRequestUri(Uri uri) - { - _requestUri = uri ?? throw new ArgumentNullException(nameof(uri)); - - if (!_requestUri.IsAbsoluteUri) - { - string relUri = _requestUri.ToString(); - if (relUri.Length > 0 && _requestUri.ToString()[0] != UriSegmentDelimiter) - { - _requestUri = new Uri($"{UriSegmentDelimiter}{_requestUri}", UriKind.Relative); - } - } - - _formattedUri = _requestUri.ToString(); - } - /// public override bool IsMatch(MockHttpRequestContext requestContext) { diff --git a/test/MockHttp.Tests/Http/UriExtensionsTests.cs b/test/MockHttp.Tests/Http/UriExtensionsTests.cs new file mode 100644 index 00000000..2e612736 --- /dev/null +++ b/test/MockHttp.Tests/Http/UriExtensionsTests.cs @@ -0,0 +1,44 @@ +using static MockHttp.Http.UriExtensions; + +namespace MockHttp.Http; + +public abstract class UriExtensionsTests +{ + public class EnsureIsRootedTests : UriExtensionsTests + { + [Theory] + [InlineData("relative/path", "/relative/path")] + [InlineData("/relative/path", "/relative/path")] + [InlineData("http://0.0.0.0/relative/path", "http://0.0.0.0/relative/path")] + public void Given_that_uri_does_not_start_with_forward_slash_when_ensuringIsRooted_it_should_modify_it(string uriStr, string expectedUriStr) + { + var uri = new Uri(uriStr, DotNetRelativeOrAbsolute); + var expectedUri = new Uri(expectedUriStr, DotNetRelativeOrAbsolute); + + // Act + Uri actual = uri.EnsureIsRooted(); + + // Assert + actual.Should().Be(expectedUri); + if (actual == uri) + { + actual.Should().BeSameAs(uri); + } + } + + [Fact] + public void Given_that_uri_is_null_when_ensuringIsRooted_it_should_throw() + { + Uri? uri = null; + + // Act + Func act = () => uri!.EnsureIsRooted(); + + // Assert + act.Should() + .Throw() + .Which.ParamName.Should() + .Be(nameof(uri)); + } + } +} From 89c7c3f03a17def5af448f431e723c57c0ab65ac Mon Sep 17 00:00:00 2001 From: skwasjer <11424653+skwasjer@users.noreply.github.com> Date: Sun, 19 Nov 2023 05:00:02 +0100 Subject: [PATCH 2/4] refactor: extract interface and make pattern matcher generic --- src/MockHttp/Http/HttpHeaderEqualityComparer.cs | 4 ++-- src/MockHttp/Matchers/Patterns/IPatternMatcher.cs | 6 ++++++ src/MockHttp/Matchers/Patterns/PatternMatcher.cs | 2 +- 3 files changed, 9 insertions(+), 3 deletions(-) create mode 100644 src/MockHttp/Matchers/Patterns/IPatternMatcher.cs diff --git a/src/MockHttp/Http/HttpHeaderEqualityComparer.cs b/src/MockHttp/Http/HttpHeaderEqualityComparer.cs index 9ce89c0f..8abed384 100644 --- a/src/MockHttp/Http/HttpHeaderEqualityComparer.cs +++ b/src/MockHttp/Http/HttpHeaderEqualityComparer.cs @@ -22,7 +22,7 @@ internal enum HttpHeaderMatchType internal sealed class HttpHeaderEqualityComparer : IEqualityComparer>> { private readonly HttpHeaderMatchType? _matchType; - private readonly PatternMatcher? _valuePatternMatcher; + private readonly IPatternMatcher? _valuePatternMatcher; public HttpHeaderEqualityComparer(HttpHeaderMatchType matchType) { @@ -34,7 +34,7 @@ public HttpHeaderEqualityComparer(HttpHeaderMatchType matchType) _matchType = matchType; } - public HttpHeaderEqualityComparer(PatternMatcher valuePatternMatcher) + public HttpHeaderEqualityComparer(IPatternMatcher valuePatternMatcher) { _valuePatternMatcher = valuePatternMatcher ?? throw new ArgumentNullException(nameof(valuePatternMatcher)); } diff --git a/src/MockHttp/Matchers/Patterns/IPatternMatcher.cs b/src/MockHttp/Matchers/Patterns/IPatternMatcher.cs new file mode 100644 index 00000000..cd99da36 --- /dev/null +++ b/src/MockHttp/Matchers/Patterns/IPatternMatcher.cs @@ -0,0 +1,6 @@ +namespace MockHttp.Matchers.Patterns; + +internal interface IPatternMatcher +{ + bool IsMatch(T value); +} diff --git a/src/MockHttp/Matchers/Patterns/PatternMatcher.cs b/src/MockHttp/Matchers/Patterns/PatternMatcher.cs index 33bd20e0..c76d89dc 100644 --- a/src/MockHttp/Matchers/Patterns/PatternMatcher.cs +++ b/src/MockHttp/Matchers/Patterns/PatternMatcher.cs @@ -1,6 +1,6 @@ namespace MockHttp.Matchers.Patterns; -internal abstract class PatternMatcher +internal abstract class PatternMatcher : IPatternMatcher { /// /// Tests if the specified matches the pattern. From 542e603810330431bac0c664cef07e2ee99ff71b Mon Sep 17 00:00:00 2001 From: skwasjer <11424653+skwasjer@users.noreply.github.com> Date: Sat, 7 Sep 2024 16:02:29 +0200 Subject: [PATCH 3/4] feat: set up uri matcher for reusability so we can more simply add other types of matchers that check portions of the URI, like path/route values, etc. --- .../Matchers/Patterns/IPatternMatcher.cs | 5 ++ .../Matchers/Patterns/PatternMatcher.cs | 14 ----- .../Matchers/Patterns/RegexPatternMatcher.cs | 4 +- .../RelativeOrAbsoluteUriPatternMatcher.cs | 34 +++++++++++ .../Patterns/UriStringPatternMatcher.cs | 19 ++++++ src/MockHttp/Matchers/RequestUriMatcher.cs | 2 +- src/MockHttp/Matchers/UriMatcher.cs | 59 +++++++++++++++++++ 7 files changed, 120 insertions(+), 17 deletions(-) delete mode 100644 src/MockHttp/Matchers/Patterns/PatternMatcher.cs create mode 100644 src/MockHttp/Matchers/Patterns/RelativeOrAbsoluteUriPatternMatcher.cs create mode 100644 src/MockHttp/Matchers/Patterns/UriStringPatternMatcher.cs create mode 100644 src/MockHttp/Matchers/UriMatcher.cs diff --git a/src/MockHttp/Matchers/Patterns/IPatternMatcher.cs b/src/MockHttp/Matchers/Patterns/IPatternMatcher.cs index cd99da36..b2560b68 100644 --- a/src/MockHttp/Matchers/Patterns/IPatternMatcher.cs +++ b/src/MockHttp/Matchers/Patterns/IPatternMatcher.cs @@ -2,5 +2,10 @@ internal interface IPatternMatcher { + /// + /// Tests if the specified matches the pattern. + /// + /// The value to test. + /// Returns true if the value matches the pattern; otherwise returns false. bool IsMatch(T value); } diff --git a/src/MockHttp/Matchers/Patterns/PatternMatcher.cs b/src/MockHttp/Matchers/Patterns/PatternMatcher.cs deleted file mode 100644 index c76d89dc..00000000 --- a/src/MockHttp/Matchers/Patterns/PatternMatcher.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace MockHttp.Matchers.Patterns; - -internal abstract class PatternMatcher : IPatternMatcher -{ - /// - /// Tests if the specified matches the pattern. - /// - /// The value to test. - /// Returns true if the value matches the pattern; otherwise returns false. - public abstract bool IsMatch(string value); - - /// - public abstract override string ToString(); -} diff --git a/src/MockHttp/Matchers/Patterns/RegexPatternMatcher.cs b/src/MockHttp/Matchers/Patterns/RegexPatternMatcher.cs index 56b93f06..7456a64c 100644 --- a/src/MockHttp/Matchers/Patterns/RegexPatternMatcher.cs +++ b/src/MockHttp/Matchers/Patterns/RegexPatternMatcher.cs @@ -3,7 +3,7 @@ namespace MockHttp.Matchers.Patterns; -internal class RegexPatternMatcher : PatternMatcher +internal class RegexPatternMatcher : IPatternMatcher { public RegexPatternMatcher ( @@ -24,7 +24,7 @@ public RegexPatternMatcher(Regex regex) internal Regex Regex { get; } /// - public override bool IsMatch(string value) + public virtual bool IsMatch(string value) { return Regex.IsMatch(value); } diff --git a/src/MockHttp/Matchers/Patterns/RelativeOrAbsoluteUriPatternMatcher.cs b/src/MockHttp/Matchers/Patterns/RelativeOrAbsoluteUriPatternMatcher.cs new file mode 100644 index 00000000..fb84d4b5 --- /dev/null +++ b/src/MockHttp/Matchers/Patterns/RelativeOrAbsoluteUriPatternMatcher.cs @@ -0,0 +1,34 @@ +using System.Diagnostics; +using MockHttp.Http; + +namespace MockHttp.Matchers.Patterns; + +[DebuggerDisplay($"{{{nameof(_originalUri)}}}")] +internal sealed class RelativeOrAbsoluteUriPatternMatcher : IPatternMatcher +{ + private readonly Uri _originalUri; + private readonly Uri _uri; + + public RelativeOrAbsoluteUriPatternMatcher(Uri uri) + { + _originalUri = uri; + _uri = uri.EnsureIsRooted(); + } + + public bool IsMatch(Uri value) + { + return IsAbsoluteUriMatch(value) || IsRelativeUriMatch(value); + } + + private bool IsAbsoluteUriMatch(Uri uri) + { + return _uri.IsAbsoluteUri && uri.Equals(_uri); + } + + private bool IsRelativeUriMatch(Uri uri) + { + return !_uri.IsAbsoluteUri + && uri.IsBaseOf(_uri) + && uri.ToString().EndsWith(_uri.ToString(), StringComparison.Ordinal); + } +} diff --git a/src/MockHttp/Matchers/Patterns/UriStringPatternMatcher.cs b/src/MockHttp/Matchers/Patterns/UriStringPatternMatcher.cs new file mode 100644 index 00000000..8a677e73 --- /dev/null +++ b/src/MockHttp/Matchers/Patterns/UriStringPatternMatcher.cs @@ -0,0 +1,19 @@ +namespace MockHttp.Matchers.Patterns; + +internal class UriStringPatternMatcher : IPatternMatcher +{ + private readonly Func _selector; + private readonly IPatternMatcher _stringPatternMatcher; + + public UriStringPatternMatcher(Func selector, IPatternMatcher stringPatternMatcher) + { + _selector = selector ?? throw new ArgumentNullException(nameof(selector)); + _stringPatternMatcher = stringPatternMatcher ?? throw new ArgumentNullException(nameof(stringPatternMatcher)); + } + + public bool IsMatch(Uri value) + { + string v = _selector(value); + return _stringPatternMatcher.IsMatch(v); + } +} diff --git a/src/MockHttp/Matchers/RequestUriMatcher.cs b/src/MockHttp/Matchers/RequestUriMatcher.cs index 2e36e934..782e810c 100644 --- a/src/MockHttp/Matchers/RequestUriMatcher.cs +++ b/src/MockHttp/Matchers/RequestUriMatcher.cs @@ -16,7 +16,7 @@ public class RequestUriMatcher : HttpRequestMatcher [DebuggerBrowsable(DebuggerBrowsableState.Never)] private readonly string _formattedUri; [DebuggerBrowsable(DebuggerBrowsableState.Never)] - private readonly PatternMatcher? _uriPatternMatcher; + private readonly WildcardPatternMatcher? _uriPatternMatcher; /// /// Initializes a new instance of the class using specified . diff --git a/src/MockHttp/Matchers/UriMatcher.cs b/src/MockHttp/Matchers/UriMatcher.cs new file mode 100644 index 00000000..866c4ed0 --- /dev/null +++ b/src/MockHttp/Matchers/UriMatcher.cs @@ -0,0 +1,59 @@ +using System.Runtime.CompilerServices; +using MockHttp.Matchers.Patterns; +using MockHttp.Responses; + +namespace MockHttp.Matchers; + +/// +/// Matches a request by the URI. +/// +internal class UriMatcher : HttpRequestMatcher +{ + private readonly IPatternMatcher _patternMatcher; + private readonly string _name; + private readonly string _patternDescription; + + /// + /// Initializes a new instance of the class. + /// + /// A matcher implementation that validates the URI. + /// A description of the pattern. + /// The name of this matcher. + /// Thrown when a required argument is . + internal UriMatcher + ( + IPatternMatcher patternMatcher, + string patternDescription, + [CallerMemberName] string? name = null + ) + { + _patternMatcher = patternMatcher ?? throw new ArgumentNullException(nameof(patternMatcher)); + _patternDescription = patternDescription ?? throw new ArgumentNullException(nameof(patternDescription)); + + name ??= GetType().Name; + if (name.EndsWith("Matcher", StringComparison.Ordinal)) + { + name = name.Remove(name.Length - "Matcher".Length); + } + + _name = name; + } + + /// + public override bool IsMatch(MockHttpRequestContext requestContext) + { + if (requestContext is null) + { + throw new ArgumentNullException(nameof(requestContext)); + } + + Uri? uri = requestContext.Request.RequestUri; + return uri is not null && _patternMatcher.IsMatch(uri); + } + + /// + public override string ToString() + { + return $"{_name}: '{_patternDescription}'"; + } +} From 91a6fbb18526c7bf8b48a345333ff939e88e5801 Mon Sep 17 00:00:00 2001 From: skwasjer <11424653+skwasjer@users.noreply.github.com> Date: Sat, 7 Sep 2024 18:32:28 +0200 Subject: [PATCH 4/4] refactor: replaced RequestUriMatcher with UriMatcher. Additionally, added option to disable wildcards on request URI matching. --- .../Extensions/RequestMatchingExtensions.cs | 27 ++- src/MockHttp/Matchers/RequestUriMatcher.cs | 97 ---------- .../RequestMatchingExtensionsTests.cs | 182 ++++++++++++++++-- .../Matchers/AnyMatcherTests.cs | 8 +- test/MockHttp.Tests/Matchers/BoolMatcher.cs | 23 +++ .../Matchers/RequestUriMatcherTests.cs | 109 ----------- 6 files changed, 219 insertions(+), 227 deletions(-) delete mode 100644 src/MockHttp/Matchers/RequestUriMatcher.cs create mode 100644 test/MockHttp.Tests/Matchers/BoolMatcher.cs delete mode 100644 test/MockHttp.Tests/Matchers/RequestUriMatcherTests.cs diff --git a/src/MockHttp/Extensions/RequestMatchingExtensions.cs b/src/MockHttp/Extensions/RequestMatchingExtensions.cs index 2f4aba6a..7038c877 100644 --- a/src/MockHttp/Extensions/RequestMatchingExtensions.cs +++ b/src/MockHttp/Extensions/RequestMatchingExtensions.cs @@ -6,6 +6,8 @@ using System.Text; using MockHttp.Http; using MockHttp.Matchers; +using MockHttp.Matchers.Patterns; +using static MockHttp.Http.UriExtensions; namespace MockHttp; @@ -14,13 +16,30 @@ namespace MockHttp; /// public static class RequestMatchingExtensions { + private static bool ContainsWildcard(this string value) + { + if (value is null) + { + throw new ArgumentNullException(nameof(value)); + } + +#if NETSTANDARD2_0 || NETFRAMEWORK + return value.Contains("*"); +#else + return value.Contains('*', StringComparison.InvariantCultureIgnoreCase); +#endif + } + /// /// Matches a request by specified . /// /// The request matching builder instance. /// The request URI or a URI wildcard. + /// to allow wildcards, or if exact matching. /// The request matching builder instance. - public static RequestMatching RequestUri(this RequestMatching builder, string requestUri) +#pragma warning disable CA1054 + public static RequestMatching RequestUri(this RequestMatching builder, string requestUri, bool allowWildcards = true) +#pragma warning restore CA1054 { if (builder is null) { @@ -32,7 +51,9 @@ public static RequestMatching RequestUri(this RequestMatching builder, string re throw new ArgumentNullException(nameof(requestUri)); } - return builder.With(new RequestUriMatcher(requestUri, true)); + return allowWildcards && requestUri.ContainsWildcard() + ? builder.With(new UriMatcher(new UriStringPatternMatcher(uri => uri.ToString(), new WildcardPatternMatcher(requestUri)), requestUri)) + : builder.RequestUri(new Uri(requestUri, DotNetRelativeOrAbsolute)); } /// @@ -53,7 +74,7 @@ public static RequestMatching RequestUri(this RequestMatching builder, Uri reque throw new ArgumentNullException(nameof(requestUri)); } - return builder.With(new RequestUriMatcher(requestUri)); + return builder.With(new UriMatcher(new RelativeOrAbsoluteUriPatternMatcher(requestUri), requestUri.ToString())); } /// diff --git a/src/MockHttp/Matchers/RequestUriMatcher.cs b/src/MockHttp/Matchers/RequestUriMatcher.cs deleted file mode 100644 index 782e810c..00000000 --- a/src/MockHttp/Matchers/RequestUriMatcher.cs +++ /dev/null @@ -1,97 +0,0 @@ -using System.Diagnostics; -using MockHttp.Http; -using MockHttp.Matchers.Patterns; -using MockHttp.Responses; -using static MockHttp.Http.UriExtensions; - -namespace MockHttp.Matchers; - -/// -/// Matches a request by the request URI. -/// -public class RequestUriMatcher : HttpRequestMatcher -{ - [DebuggerBrowsable(DebuggerBrowsableState.Never)] - private readonly Uri _requestUri = default!; - [DebuggerBrowsable(DebuggerBrowsableState.Never)] - private readonly string _formattedUri; - [DebuggerBrowsable(DebuggerBrowsableState.Never)] - private readonly WildcardPatternMatcher? _uriPatternMatcher; - - /// - /// Initializes a new instance of the class using specified . - /// - /// The request URI. - public RequestUriMatcher(Uri uri) - { - _requestUri = uri.EnsureIsRooted(); - _formattedUri = _requestUri.ToString(); - } - - /// - /// Initializes a new instance of the class using specified . - /// - /// The request URI or a URI wildcard. - /// to allow wildcards, or if exact matching. - public RequestUriMatcher(string uriString, bool allowWildcards = true) - { - _formattedUri = uriString ?? throw new ArgumentNullException(nameof(uriString)); - - if (allowWildcards -#if NETSTANDARD2_0 || NETFRAMEWORK - && uriString.Contains("*") -#else - && uriString.Contains('*', StringComparison.InvariantCultureIgnoreCase) -#endif - ) - { - _uriPatternMatcher = new WildcardPatternMatcher(uriString); - } - else - { - // If no wildcards, then must be actual uri. - _requestUri = new Uri(uriString, DotNetRelativeOrAbsolute).EnsureIsRooted(); - _formattedUri = _requestUri.ToString(); - } - } - - /// - public override bool IsMatch(MockHttpRequestContext requestContext) - { - if (requestContext is null) - { - throw new ArgumentNullException(nameof(requestContext)); - } - - Uri? requestUri = requestContext.Request.RequestUri; - if (requestUri is null) - { - return false; - } - - if (_uriPatternMatcher is null) - { - return IsAbsoluteUriMatch(requestUri) || IsRelativeUriMatch(requestUri); - } - - return _uriPatternMatcher.IsMatch(requestUri.ToString()); - } - - private bool IsAbsoluteUriMatch(Uri uri) - { - return _requestUri.IsAbsoluteUri && uri.Equals(_requestUri); - } - - private bool IsRelativeUriMatch(Uri uri) - { - return !_requestUri.IsAbsoluteUri - && uri.IsBaseOf(_requestUri) - && uri.ToString().EndsWith(_requestUri.ToString(), StringComparison.Ordinal); - } - - /// - public override string ToString() - { - return $"RequestUri: '{_formattedUri}'"; - } -} diff --git a/test/MockHttp.Tests/Extensions/RequestMatchingExtensionsTests.cs b/test/MockHttp.Tests/Extensions/RequestMatchingExtensionsTests.cs index 4ba231e0..bd876f64 100644 --- a/test/MockHttp.Tests/Extensions/RequestMatchingExtensionsTests.cs +++ b/test/MockHttp.Tests/Extensions/RequestMatchingExtensionsTests.cs @@ -1,4 +1,5 @@ -using System.Collections.Specialized; +using System; +using System.Collections.Specialized; using System.Linq.Expressions; using System.Net.Http.Headers; using System.Text; @@ -6,6 +7,7 @@ using MockHttp.Http; using MockHttp.Matchers; using MockHttp.Responses; +using static MockHttp.Http.UriExtensions; namespace MockHttp.Extensions; @@ -20,33 +22,184 @@ protected RequestMatchingExtensionsTests() public class RequestUri : RequestMatchingExtensionsTests { - [Fact] - public async Task When_configuring_requestUri_as_string_should_match() + [Theory] + [InlineData("relative.htm", true, "http://127.0.0.1/relative.htm", true)] + [InlineData("relative.htm", true, "http://127.0.0.1/relative.htm?query=string", false)] + [InlineData("relative.htm", false, "http://127.0.0.1/relative.htm", true)] + [InlineData("relative.htm", false, "http://127.0.0.1/relative.htm?query=string", false)] + + [InlineData("/relative.htm", true, "http://127.0.0.1/relative.htm", true)] + [InlineData("/relative.htm", true, "http://127.0.0.1/relative.htm?query=string", false)] + [InlineData("/relative.htm", false, "http://127.0.0.1/relative.htm", true)] + [InlineData("/relative.htm", false, "http://127.0.0.1/relative.htm?query=string", false)] + + [InlineData("relative.htm?query=string", true, "http://127.0.0.1/relative.htm?query=string", true)] + [InlineData("relative.htm?query=string", false, "http://127.0.0.1/relative.htm?query=string", true)] + [InlineData("http://127.0.0.1/absolute.htm?query=string", true, "http://127.0.0.1/absolute.htm?query=string", true)] + [InlineData("http://127.0.0.1/absolute.htm?query=string", false, "http://127.0.0.1/absolute.htm?query=string", true)] + + [InlineData("/folder/relative.htm", true, "http://127.0.0.1/relative.htm", false)] + [InlineData("/folder/relative.htm", true, "http://127.0.0.1/relative.htm?query=string", false)] + [InlineData("/folder/relative.htm", false, "http://127.0.0.1/relative.htm", false)] + [InlineData("/folder/relative.htm", false, "http://127.0.0.1/relative.htm?query=string", false)] + + [InlineData("relative.htm", true, "http://127.0.0.1/folder/relative.htm", false)] + [InlineData("relative.htm", true, "http://127.0.0.1/folder/relative.htm?query=string", false)] + [InlineData("relative.htm", false, "http://127.0.0.1/folder/relative.htm", false)] + [InlineData("relative.htm", false, "http://127.0.0.1/folder/relative.htm?query=string", false)] + + [InlineData("folder/relative.htm", true, "http://127.0.0.1/folder/relative.htm", true)] + [InlineData("folder/relative.htm", true, "http://127.0.0.1/folder/relative.htm?query=string", false)] + [InlineData("folder/relative.htm", false, "http://127.0.0.1/folder/relative.htm", true)] + [InlineData("folder/relative.htm", false, "http://127.0.0.1/folder/relative.htm?query=string", false)] + + [InlineData("/folder/relative.htm", true, "http://127.0.0.1/folder/relative.htm", true)] + [InlineData("/folder/relative.htm", true, "http://127.0.0.1/folder/relative.htm?query=string", false)] + [InlineData("/folder/relative.htm", false, "http://127.0.0.1/folder/relative.htm", true)] + [InlineData("/folder/relative.htm", false, "http://127.0.0.1/folder/relative.htm?query=string", false)] + + [InlineData("http://127.0.0.1/absolute.htm", true, "http://127.0.0.1/absolute.htm", true)] + [InlineData("http://127.0.0.1/absolute.htm", true, "http://127.0.0.1/absolute.htm?query=string", false)] + [InlineData("http://127.0.0.1/absolute.htm", false, "http://127.0.0.1/absolute.htm", true)] + [InlineData("http://127.0.0.1/absolute.htm", false, "http://127.0.0.1/absolute.htm?query=string", false)] + + [InlineData("http://127.0.0.1/absolute.htm", true, "http://127.0.0.1/folder/absolute.htm", false)] + [InlineData("http://127.0.0.1/absolute.htm", true, "http://127.0.0.1/folder/absolute.htm?query=string", false)] + [InlineData("http://127.0.0.1/absolute.htm", false, "http://127.0.0.1/folder/absolute.htm", false)] + [InlineData("http://127.0.0.1/absolute.htm", false, "http://127.0.0.1/folder/absolute.htm?query=string", false)] + + [InlineData("http://127.0.0.1/folder/absolute.htm", true, "http://127.0.0.1/folder/absolute.htm", true)] + [InlineData("http://127.0.0.1/folder/absolute.htm", true, "http://127.0.0.1/folder/absolute.htm?query=string", false)] + [InlineData("http://127.0.0.1/folder/absolute.htm", false, "http://127.0.0.1/folder/absolute.htm", true)] + [InlineData("http://127.0.0.1/folder/absolute.htm", false, "http://127.0.0.1/folder/absolute.htm?query=string", false)] + + [InlineData("*.htm", true, "http://127.0.0.1/relative.htm", true)] + [InlineData("*.htm", true, "http://127.0.0.1/relative.htm?query=string", false)] + [InlineData("*.htm", false, "http://127.0.0.1/relative.htm", false)] + + [InlineData("*/relative.htm", true, "http://127.0.0.1/relative.htm", true)] + [InlineData("*/relative.htm", true, "http://127.0.0.1/relative.htm?query=string", false)] + [InlineData("*/relative.htm", false, "http://127.0.0.1/relative.htm", false)] + + [InlineData("/*/relative.htm", true, "http://127.0.0.1/folder/relative.htm", false)] + [InlineData("/*/relative.htm", true, "http://127.0.0.1/folder/relative.htm?query=string", false)] + [InlineData("/*/relative.htm", false, "http://127.0.0.1/folder/relative.htm", false)] + + [InlineData("/*/relative.htm", true, "http://127.0.0.1/relative.htm", false)] + [InlineData("/*/relative.htm", true, "http://127.0.0.1/relative.htm?query=string", false)] + [InlineData("/*/relative.htm", false, "http://127.0.0.1/relative.htm", false)] + + [InlineData("/folder/*.htm", true, "http://127.0.0.1/folder/relative.htm", false)] + [InlineData("/folder/*.htm", true, "http://127.0.0.1/folder/relative.htm?query=string", false)] + [InlineData("/folder/*.htm", false, "http://127.0.0.1/folder/relative.htm", false)] + + [InlineData("*/folder/*.htm", true, "http://127.0.0.1/folder/relative.htm", true)] + [InlineData("*/folder/*.htm", true, "http://127.0.0.1/folder/relative.htm?query=string", false)] + [InlineData("*/folder/*.htm", false, "http://127.0.0.1/folder/relative.htm", false)] + + [InlineData("/folder/*.htm", true, "http://127.0.0.1/relative.htm", false)] + [InlineData("/folder/*.htm", true, "http://127.0.0.1/relative.htm?query=string", false)] + [InlineData("/folder/*.htm", false, "http://127.0.0.1/relative.htm", false)] + + [InlineData("/*/*/relative.*", true, "http://127.0.0.1/folder1/folder2/relative.htm", false)] + [InlineData("/*/*/relative.*", true, "http://127.0.0.1/folder1/folder2/relative.htm?query=string", false)] + [InlineData("/*/*/relative.*", false, "http://127.0.0.1/folder1/folder2/relative.htm", false)] + + [InlineData("*/folder1/*/relative.*", true, "http://127.0.0.1/folder1/folder2/relative.htm", true)] + [InlineData("*/folder1/*/relative.*", true, "http://127.0.0.1/folder1/folder2/relative.htm?query=string", true)] + [InlineData("*/folder1/*/relative.*", false, "http://127.0.0.1/folder1/folder2/relative.htm", false)] + + [InlineData("/*/*/relative.*", true, "http://127.0.0.1/folder1/relative.htm", false)] + [InlineData("/*/*/relative.*", true, "http://127.0.0.1/folder1/relative.htm?query=string", false)] + [InlineData("/*/*/relative.*", false, "http://127.0.0.1/folder1/relative.htm", false)] + + [InlineData("http://127.0.0.1/*.htm", true, "http://127.0.0.1/absolute.htm", true)] + [InlineData("http://127.0.0.1/*.htm", true, "http://127.0.0.1/absolute.htm?query=string", false)] + [InlineData("http://127.0.0.1/*.htm", false, "http://127.0.0.1/absolute.htm", false)] + + [InlineData("http://127.0.0.1/*.htm", true, "http://127.0.0.1/folder/absolute.htm", true)] + [InlineData("http://127.0.0.1/*.htm", true, "http://127.0.0.1/folder/absolute.htm?query=string", false)] + [InlineData("http://127.0.0.1/*.htm", false, "http://127.0.0.1/folder/absolute.htm", false)] + public async Task When_configuring_requestUri_as_string_it_should_match(string uriString, bool allowWildcards, string requestUri, bool isMatch) { - var request = new HttpRequestMessage { RequestUri = new Uri("http://127.0.0.1/") }; + var request = new HttpRequestMessage { RequestUri = new Uri(requestUri, UriKind.Absolute) }; // Act - _sut.RequestUri("http://127.0.0.1/"); + _sut.RequestUri(uriString, allowWildcards); IReadOnlyCollection matchers = _sut.Build(); // Assert - matchers.Should().HaveCount(1).And.AllBeOfType(); - (await matchers.AnyAsync(new MockHttpRequestContext(request))).Should().BeTrue(); + matchers.Should().HaveCount(1).And.AllBeAssignableTo(); + (await matchers.AnyAsync(new MockHttpRequestContext(request))).Should().Be(isMatch); } - [Fact] - public async Task When_configuring_requestUri_as_uri_should_match() + [Theory] + [InlineData("", UriKind.Relative, "http://127.0.0.1/", true)] + [InlineData("relative.htm", UriKind.Relative, "http://127.0.0.1/relative.htm", true)] + [InlineData("/folder/relative.htm", UriKind.Relative, "http://127.0.0.1/relative.htm", false)] + [InlineData("relative.htm", UriKind.Relative, "http://127.0.0.1/folder/relative.htm", false)] + [InlineData("folder/relative.htm", UriKind.Relative, "http://127.0.0.1/folder/relative.htm", true)] + [InlineData("/folder/relative.htm", UriKind.Relative, "http://127.0.0.1/folder/relative.htm", true)] + [InlineData("http://127.0.0.1/absolute.htm", UriKind.Absolute, "http://127.0.0.1/absolute.htm", true)] + [InlineData("http://127.0.0.1/absolute.htm", UriKind.Absolute, "http://127.0.0.1/folder/absolute.htm", false)] + public async Task When_configuring_requestUri_as_uri_it_should_match(string matchUri, UriKind uriKind, string requestUri, bool isMatch) { - var uri = new Uri("http://127.0.0.1/"); - var request = new HttpRequestMessage { RequestUri = new Uri("http://127.0.0.1/") }; + var uri = new Uri(matchUri, uriKind); + var request = new HttpRequestMessage { RequestUri = new Uri(requestUri, UriKind.Absolute) }; // Act _sut.RequestUri(uri); IReadOnlyCollection matchers = _sut.Build(); // Assert - matchers.Should().HaveCount(1).And.AllBeOfType(); - (await matchers.AnyAsync(new MockHttpRequestContext(request))).Should().BeTrue(); + matchers.Should().HaveCount(1).And.AllBeAssignableTo(); + (await matchers.AnyAsync(new MockHttpRequestContext(request))).Should().Be(isMatch); + } + + [Theory] + [InlineData("*/controller/*", false)] + [InlineData("*/controller/*", true)] + [InlineData("file.jpg", true)] + [InlineData("file.jpg", false)] + [InlineData("http://0.0.0.0/path/file.jpg", true)] + [InlineData("http://0.0.0.0/path/file.jpg", false)] + public void When_formatting_uriString_matcher_it_should_return_human_readable_representation(string uriString, bool allowWildcards) + { + string expectedText = $"RequestUri: '{uriString}'"; + + // Act + _sut.RequestUri(uriString, allowWildcards); + IReadOnlyCollection matchers = _sut.Build(); + string displayText = matchers.Should() + .ContainSingle() + .Which.Should() + .BeAssignableTo() + .Which.ToString(); + + // Assert + displayText.Should().Be(expectedText); + } + + [Theory] + [InlineData("*/controller/*")] + [InlineData("file.jpg")] + [InlineData("http://0.0.0.0/path/file.jpg")] + public void When_formatting_uri_matcher_it_should_return_human_readable_representation(string uriString) + { + string expectedText = $"RequestUri: '{uriString}'"; + var uri = new Uri(uriString, DotNetRelativeOrAbsolute); + + // Act + _sut.RequestUri(uri); + IReadOnlyCollection matchers = _sut.Build(); + string displayText = matchers.Should() + .ContainSingle() + .Which.Should() + .BeAssignableTo() + .Which.ToString(); + + // Assert + displayText.Should().Be(expectedText); } } @@ -692,7 +845,8 @@ public void Given_null_argument_when_executing_method_it_should_throw(params obj DelegateTestCase.Create( RequestMatchingExtensions.RequestUri, instance, - uri.ToString()), + uri.ToString(), + true), DelegateTestCase.Create( RequestMatchingExtensions.RequestUri, instance, diff --git a/test/MockHttp.Tests/Matchers/AnyMatcherTests.cs b/test/MockHttp.Tests/Matchers/AnyMatcherTests.cs index 2491861a..2615588b 100644 --- a/test/MockHttp.Tests/Matchers/AnyMatcherTests.cs +++ b/test/MockHttp.Tests/Matchers/AnyMatcherTests.cs @@ -18,8 +18,8 @@ public AnyMatcherTests() [InlineData("url2")] public async Task Given_request_uri_equals_one_of_the_matchers_when_matching_should_match(string requestUrl) { - _matchers.Add(new RequestUriMatcher("*url1")); - _matchers.Add(new RequestUriMatcher("*url2")); + _matchers.Add(new BoolMatcher(requestUrl == "url1")); + _matchers.Add(new BoolMatcher(requestUrl == "url2")); var request = new HttpRequestMessage(HttpMethod.Get, requestUrl); @@ -32,8 +32,8 @@ public async Task Given_request_uri_equals_one_of_the_matchers_when_matching_sho [Fact] public async Task Given_request_uri_matches_none_of_the_matchers_when_matching_should_not_match() { - _matchers.Add(new RequestUriMatcher("http://127.0.0.1/")); - _matchers.Add(new RequestUriMatcher("http://127.0.0.2/")); + _matchers.Add(new BoolMatcher(false)); + _matchers.Add(new BoolMatcher(false)); var request = new HttpRequestMessage(HttpMethod.Get, "http://127.0.0.3/"); diff --git a/test/MockHttp.Tests/Matchers/BoolMatcher.cs b/test/MockHttp.Tests/Matchers/BoolMatcher.cs new file mode 100644 index 00000000..7ff182da --- /dev/null +++ b/test/MockHttp.Tests/Matchers/BoolMatcher.cs @@ -0,0 +1,23 @@ +using MockHttp.Responses; + +namespace MockHttp.Matchers; + +internal sealed class BoolMatcher : HttpRequestMatcher +{ + private readonly bool _shouldMatch; + + public BoolMatcher(bool shouldMatch) + { + _shouldMatch = shouldMatch; + } + + public override bool IsMatch(MockHttpRequestContext requestContext) + { + return _shouldMatch; + } + + public override string ToString() + { + throw new NotImplementedException(); + } +} diff --git a/test/MockHttp.Tests/Matchers/RequestUriMatcherTests.cs b/test/MockHttp.Tests/Matchers/RequestUriMatcherTests.cs deleted file mode 100644 index 0a0943ad..00000000 --- a/test/MockHttp.Tests/Matchers/RequestUriMatcherTests.cs +++ /dev/null @@ -1,109 +0,0 @@ -using MockHttp.Responses; - -namespace MockHttp.Matchers; - -public class RequestUriMatcherTests -{ - [Theory] - [InlineData("", UriKind.Relative, "http://127.0.0.1/", true)] - [InlineData("relative.htm", UriKind.Relative, "http://127.0.0.1/relative.htm", true)] - [InlineData("/folder/relative.htm", UriKind.Relative, "http://127.0.0.1/relative.htm", false)] - [InlineData("relative.htm", UriKind.Relative, "http://127.0.0.1/folder/relative.htm", false)] - [InlineData("folder/relative.htm", UriKind.Relative, "http://127.0.0.1/folder/relative.htm", true)] - [InlineData("/folder/relative.htm", UriKind.Relative, "http://127.0.0.1/folder/relative.htm", true)] - [InlineData("http://127.0.0.1/absolute.htm", UriKind.Absolute, "http://127.0.0.1/absolute.htm", true)] - [InlineData("http://127.0.0.1/absolute.htm", UriKind.Absolute, "http://127.0.0.1/folder/absolute.htm", false)] - public void Given_uri_when_matching_should_match(string matchUri, UriKind uriKind, string requestUri, bool isMatch) - { - var request = new HttpRequestMessage { RequestUri = new Uri(requestUri, UriKind.Absolute) }; - var sut = new RequestUriMatcher(new Uri(matchUri, uriKind)); - - // Act & assert - sut.IsMatch(new MockHttpRequestContext(request)).Should().Be(isMatch); - } - - [Theory] - [InlineData("relative.htm", true, "http://127.0.0.1/relative.htm", true)] - [InlineData("/folder/relative.htm", true, "http://127.0.0.1/relative.htm", false)] - [InlineData("relative.htm", true, "http://127.0.0.1/folder/relative.htm", false)] - [InlineData("folder/relative.htm", true, "http://127.0.0.1/folder/relative.htm", true)] - [InlineData("/folder/relative.htm", true, "http://127.0.0.1/folder/relative.htm", true)] - [InlineData("http://127.0.0.1/absolute.htm", true, "http://127.0.0.1/absolute.htm", true)] - [InlineData("http://127.0.0.1/absolute.htm", true, "http://127.0.0.1/folder/absolute.htm", false)] - [InlineData("*.htm", true, "http://127.0.0.1/relative.htm", true)] - [InlineData("*/relative.htm", true, "http://127.0.0.1/relative.htm", true)] - [InlineData("/*/relative.htm", true, "http://127.0.0.1/folder/relative.htm", false)] - [InlineData("/*/relative.htm", true, "http://127.0.0.1/relative.htm", false)] - [InlineData("/folder/*.htm", true, "http://127.0.0.1/folder/relative.htm", false)] - [InlineData("*/folder/*.htm", true, "http://127.0.0.1/folder/relative.htm", true)] - [InlineData("/folder/*.htm", true, "http://127.0.0.1/relative.htm", false)] - [InlineData("/*/*/relative.*", true, "http://127.0.0.1/folder1/folder2/relative.htm", false)] - [InlineData("*/folder1/*/relative.*", true, "http://127.0.0.1/folder1/folder2/relative.htm", true)] - [InlineData("/*/*/relative.*", true, "http://127.0.0.1/folder1/relative.htm", false)] - [InlineData("http://127.0.0.1/*.htm", true, "http://127.0.0.1/absolute.htm", true)] - [InlineData("http://127.0.0.1/*.htm", true, "http://127.0.0.1/folder/absolute.htm", true)] - public void Given_uriString_when_matching_should_match(string uriString, bool hasWildcard, string requestUri, bool isMatch) - { - var request = new HttpRequestMessage { RequestUri = new Uri(requestUri, UriKind.Absolute) }; - var sut = new RequestUriMatcher(uriString, hasWildcard); - - // Act & assert - sut.IsMatch(new MockHttpRequestContext(request)).Should().Be(isMatch); - } - - [Fact] - public void Given_null_uri_when_creating_matcher_should_throw() - { - Uri? uri = null; - - // Act - Func act = () => new RequestUriMatcher(uri!); - - // Assert - act.Should() - .Throw() - .WithParameterName(nameof(uri)); - } - - [Fact] - public void Given_null_uriString_when_creating_matcher_should_throw() - { - string? uriString = null; - - // Act - Func act = () => new RequestUriMatcher(uriString!, false); - - // Assert - act.Should() - .Throw() - .WithParameterName(nameof(uriString)); - } - - [Fact] - public void When_formatting_should_return_human_readable_representation() - { - const string expectedText = "RequestUri: '*/controller/*'"; - var sut = new RequestUriMatcher("*/controller/*"); - - // Act - string displayText = sut.ToString(); - - // Assert - displayText.Should().Be(expectedText); - } - - [Fact] - public void Given_null_context_when_matching_it_should_throw() - { - var sut = new RequestUriMatcher("*/controller/*"); - MockHttpRequestContext? requestContext = null; - - // Act - Action act = () => sut.IsMatch(requestContext!); - - // Assert - act.Should() - .Throw() - .WithParameterName(nameof(requestContext)); - } -}