// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; using Xunit; namespace Microsoft.AspNetCore.Routing.Matchers { public abstract class MatcherConformanceTest { internal abstract Matcher CreateMatcher(MatcherEndpoint endpoint); [Fact] public virtual async Task Match_SingleLiteralSegment() { // Arrange var (matcher, endpoint) = CreateMatcher("/simple"); var (httpContext, feature) = CreateContext("/simple"); // Act await matcher.MatchAsync(httpContext, feature); // Assert DispatcherAssert.AssertMatch(feature, endpoint); } [Fact] public virtual async Task Match_SingleLiteralSegment_TrailingSlash() { // Arrange var (matcher, endpoint) = CreateMatcher("/simple"); var (httpContext, feature) = CreateContext("/simple/"); // Act await matcher.MatchAsync(httpContext, feature); // Assert DispatcherAssert.AssertMatch(feature, endpoint); } [Theory] [InlineData("/simple")] [InlineData("/sImpLe")] [InlineData("/SIMPLE")] public virtual async Task Match_SingleLiteralSegment_CaseInsensitive(string path) { // Arrange var (matcher, endpoint) = CreateMatcher("/Simple"); var (httpContext, feature) = CreateContext(path); // Act await matcher.MatchAsync(httpContext, feature); // Assert DispatcherAssert.AssertMatch(feature, endpoint); } // Some matchers will optimize for the ASCII case [Theory] [InlineData("/SÏmple", "/SÏmple")] [InlineData("/ab\uD834\uDD1Ecd", "/ab\uD834\uDD1Ecd")] // surrogate pair public virtual async Task Match_SingleLiteralSegment_Unicode(string template, string path) { // Arrange var (matcher, endpoint) = CreateMatcher(template); var (httpContext, feature) = CreateContext(path); // Act await matcher.MatchAsync(httpContext, feature); // Assert DispatcherAssert.AssertMatch(feature, endpoint); } // Matchers should operate on the decoded representation - a matcher that calls // `httpContext.Request.Path.ToString()` will break this test. [Theory] [InlineData("/S%mple", "/S%mple")] [InlineData("/S\\imple", "/S\\imple")] // surrogate pair public virtual async Task Match_SingleLiteralSegment_PercentEncoded(string template, string path) { // Arrange var (matcher, endpoint) = CreateMatcher(template); var (httpContext, feature) = CreateContext(path); // Act await matcher.MatchAsync(httpContext, feature); // Assert DispatcherAssert.AssertMatch(feature, endpoint); } [Theory] [InlineData("/")] [InlineData("/imple")] [InlineData("/siple")] [InlineData("/simple1")] [InlineData("/simple/not-simple")] [InlineData("/simple/a/b/c")] public virtual async Task NotMatch_SingleLiteralSegment(string path) { // Arrange var (matcher, endpoint) = CreateMatcher("/simple"); var (httpContext, feature) = CreateContext(path); // Act await matcher.MatchAsync(httpContext, feature); // Assert DispatcherAssert.AssertNotMatch(feature); } [Theory] [InlineData("simple")] [InlineData("/simple")] [InlineData("~/simple")] public virtual async Task Match_Sanitizies_Template(string template) { // Arrange var (matcher, endpoint) = CreateMatcher(template); var (httpContext, feature) = CreateContext("/simple"); // Act await matcher.MatchAsync(httpContext, feature); // Assert DispatcherAssert.AssertMatch(feature, endpoint); } // Matchers do their own 'splitting' of the path into segments, so including // some extra variation here [Theory] [InlineData("/a/b", "/a/b")] [InlineData("/a/b", "/A/B")] [InlineData("/a/b", "/a/b/")] [InlineData("/a/b/c", "/a/b/c")] [InlineData("/a/b/c", "/a/b/c/")] [InlineData("/a/b/c/d", "/a/b/c/d")] [InlineData("/a/b/c/d", "/a/b/c/d/")] public virtual async Task Match_MultipleLiteralSegments(string template, string path) { // Arrange var (matcher, endpoint) = CreateMatcher(template); var (httpContext, feature) = CreateContext(path); // Act await matcher.MatchAsync(httpContext, feature); // Assert DispatcherAssert.AssertMatch(feature, endpoint); } // Matchers do their own 'splitting' of the path into segments, so including // some extra variation here [Theory] [InlineData("/a/b", "/")] [InlineData("/a/b", "/a")] [InlineData("/a/b", "/a/")] [InlineData("/a/b", "/a//")] [InlineData("/a/b", "/aa/")] [InlineData("/a/b", "/a/bb")] [InlineData("/a/b", "/a/bb/")] [InlineData("/a/b/c", "/aa/b/c")] [InlineData("/a/b/c", "/a/bb/c/")] [InlineData("/a/b/c", "/a/b/cab")] [InlineData("/a/b/c", "/d/b/c/")] [InlineData("/a/b/c", "//b/c")] [InlineData("/a/b/c", "/a/b//")] [InlineData("/a/b/c", "/a/b/c/d")] [InlineData("/a/b/c", "/a/b/c/d/e")] public virtual async Task NotMatch_MultipleLiteralSegments(string template, string path) { // Arrange var (matcher, endpoint) = CreateMatcher(template); var (httpContext, feature) = CreateContext(path); // Act await matcher.MatchAsync(httpContext, feature); // Assert DispatcherAssert.AssertNotMatch(feature); } [Fact] public virtual async Task Match_SingleParameter() { // Arrange var (matcher, endpoint) = CreateMatcher("/{p}"); var (httpContext, feature) = CreateContext("/14"); var values = new RouteValueDictionary(new { p = "14", }); // Act await matcher.MatchAsync(httpContext, feature); // Assert DispatcherAssert.AssertMatch(feature, endpoint, values); } [Fact] public virtual async Task Match_SingleParameter_TrailingSlash() { // Arrange var (matcher, endpoint) = CreateMatcher("/{p}"); var (httpContext, feature) = CreateContext("/14/"); var values = new RouteValueDictionary(new { p = "14", }); // Act await matcher.MatchAsync(httpContext, feature); // Assert DispatcherAssert.AssertMatch(feature, endpoint, values); } [Theory] [InlineData("/")] [InlineData("/a/b")] [InlineData("/a/b/c")] [InlineData("//")] public virtual async Task NotMatch_SingleParameter(string path) { // Arrange var (matcher, endpoint) = CreateMatcher("/{p}"); var (httpContext, feature) = CreateContext(path); // Act await matcher.MatchAsync(httpContext, feature); // Assert DispatcherAssert.AssertNotMatch(feature); } [Theory] [InlineData("/subscriptions/{subscriptionId}/providers/Microsoft.Insights/metricAlerts", "/subscriptions/foo/providers/Microsoft.Insights/metricAlerts", new string[] { "subscriptionId", }, new string[] { "foo", })] [InlineData("/{a}/b", "/54/b", new string[] { "a", }, new string[] {"54", })] [InlineData("/{a}/b", "/54/b/", new string[] { "a", }, new string[] { "54", })] [InlineData("/{a}/{b}", "/54/73", new string[] { "a", "b" }, new string[] { "54", "73", })] [InlineData("/a/{b}/c", "/a/b/c", new string[] { "b", }, new string[] { "b", })] [InlineData("/a/{b}/c/", "/a/b/c", new string[] { "b", }, new string[] { "b", })] [InlineData("/{a}/b/{c}", "/54/b/c", new string[] { "a", "c", }, new string[] { "54", "c", })] [InlineData("/{a}/{b}/{c}", "/54/b/c", new string[] { "a", "b", "c", }, new string[] { "54", "b", "c", })] public virtual async Task Match_MultipleParameters(string template, string path, string[] keys, string[] values) { // Arrange var (matcher, endpoint) = CreateMatcher(template); var (httpContext, feature) = CreateContext(path); // Act await matcher.MatchAsync(httpContext, feature); // Assert DispatcherAssert.AssertMatch(feature, endpoint, keys, values); } [Theory] [InlineData("/{a}/b", "/54/bb")] [InlineData("/{a}/b", "/54/b/17")] [InlineData("/{a}/b", "/54/b//")] [InlineData("/{a}/{b}", "//73")] [InlineData("/{a}/{b}", "/54//")] [InlineData("/{a}/{b}", "/54/73/18")] [InlineData("/a/{b}/c", "/aa/b/c")] [InlineData("/a/{b}/c", "/a/b/cc")] [InlineData("/a/{b}/c", "/a/b/c/d")] [InlineData("/{a}/b/{c}", "/54/bb/c")] [InlineData("/{a}/{b}/{c}", "/54/b/c/d")] [InlineData("/{a}/{b}/{c}", "/54/b/c//")] [InlineData("/{a}/{b}/{c}", "//b/c/")] [InlineData("/{a}/{b}/{c}", "/54//c/")] [InlineData("/{a}/{b}/{c}", "/54/b//")] public virtual async Task NotMatch_MultipleParameters(string template, string path) { // Arrange var (matcher, endpoint) = CreateMatcher(template); var (httpContext, feature) = CreateContext(path); // Act await matcher.MatchAsync(httpContext, feature); // Assert DispatcherAssert.AssertNotMatch(feature); } internal static (HttpContext httpContext, IEndpointFeature feature) CreateContext(string path) { var httpContext = new DefaultHttpContext(); httpContext.Request.Method = "TEST"; httpContext.Request.Path = path; httpContext.RequestServices = CreateServices(); var feature = new EndpointFeature(); httpContext.Features.Set(feature); return (httpContext, feature); } // The older routing implementations retrieve services when they first execute. internal static IServiceProvider CreateServices() { var services = new ServiceCollection(); services.AddLogging(); return services.BuildServiceProvider(); } internal static MatcherEndpoint CreateEndpoint(string template) { return new MatcherEndpoint( MatcherEndpoint.EmptyInvoker, template, null, 0, EndpointMetadataCollection.Empty, "endpoint: " + template, address: null); } internal (Matcher matcher, MatcherEndpoint endpoint) CreateMatcher(string template) { var endpoint = CreateEndpoint(template); return (CreateMatcher(endpoint), endpoint); } } }