diff --git a/src/MockHttp/Extensions/RequestMatchingExtensions.cs b/src/MockHttp/Extensions/RequestMatchingExtensions.cs index c78883fe..41bf02e2 100644 --- a/src/MockHttp/Extensions/RequestMatchingExtensions.cs +++ b/src/MockHttp/Extensions/RequestMatchingExtensions.cs @@ -30,6 +30,19 @@ private static bool ContainsWildcard(this string value) #endif } + /// + /// Matches a request by specified . + /// + /// The request matching builder instance. + /// The request URI or a URI wildcard. + /// The request matching builder instance. +#pragma warning disable CA1054 + public static RequestMatching RequestUri(this RequestMatching builder, string requestUri) +#pragma warning restore CA1054 + { + return builder.RequestUri(requestUri, true); + } + /// /// Matches a request by specified . /// @@ -38,7 +51,8 @@ private static bool ContainsWildcard(this string value) /// to allow wildcards, or if exact matching. /// The request matching builder instance. #pragma warning disable CA1054 - public static RequestMatching RequestUri(this RequestMatching builder, string requestUri, bool allowWildcards = true) + // For now, keep this internal. For coverage, and most likely, the API will change so then we'd have more to deprecate (using patterns). + internal static RequestMatching RequestUri(this RequestMatching builder, string requestUri, bool allowWildcards) #pragma warning restore CA1054 { if (requestUri is null) diff --git a/src/MockHttp/Matchers/RequestUriMatcher.cs b/src/MockHttp/Matchers/RequestUriMatcher.cs new file mode 100644 index 00000000..38510a71 --- /dev/null +++ b/src/MockHttp/Matchers/RequestUriMatcher.cs @@ -0,0 +1,98 @@ +using System.Diagnostics; +using MockHttp.Http; +using MockHttp.Patterns; +using MockHttp.Responses; +using static MockHttp.Http.UriExtensions; + +namespace MockHttp.Matchers; + +/// +/// Matches a request by the request URI. +/// +[Obsolete($"Replaced with {nameof(UriMatcher)}. Will be removed in next major release.")] +public class RequestUriMatcher : HttpRequestMatcher +{ + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + private readonly Uri _requestUri = default!; + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + private readonly string _formattedUri; + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + private readonly WildcardPattern? _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 = WildcardPattern.Create(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.Value.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/Matchers/RequestUriMatcherTests.cs b/test/MockHttp.Tests/Matchers/RequestUriMatcherTests.cs new file mode 100644 index 00000000..0a0943ad --- /dev/null +++ b/test/MockHttp.Tests/Matchers/RequestUriMatcherTests.cs @@ -0,0 +1,109 @@ +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)); + } +}