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));
+ }
+}