diff --git a/test/Microsoft.AspNetCore.Routing.Tests/Matchers/BarebonesMatcherConformanceTest.cs b/test/Microsoft.AspNetCore.Routing.Tests/Matchers/BarebonesMatcherConformanceTest.cs index 5d3e7607d9..1e9584ec74 100644 --- a/test/Microsoft.AspNetCore.Routing.Tests/Matchers/BarebonesMatcherConformanceTest.cs +++ b/test/Microsoft.AspNetCore.Routing.Tests/Matchers/BarebonesMatcherConformanceTest.cs @@ -23,6 +23,13 @@ namespace Microsoft.AspNetCore.Routing.Matchers return Task.CompletedTask; } + // Route values not supported + [Fact] + public override Task Match_SingleParameter_WierdNames() + { + return Task.CompletedTask; + } + // Route values not supported [Theory] [InlineData(null, null, null, null)] @@ -32,10 +39,13 @@ namespace Microsoft.AspNetCore.Routing.Matchers return Task.CompletedTask; } - internal override Matcher CreateMatcher(MatcherEndpoint endpoint) + internal override Matcher CreateMatcher(params MatcherEndpoint[] endpoints) { var builder = new BarebonesMatcherBuilder(); - builder.AddEndpoint(endpoint); + for (int i = 0; i < endpoints.Length; i++) + { + builder.AddEndpoint(endpoints[i]); + } return builder.Build(); } } diff --git a/test/Microsoft.AspNetCore.Routing.Tests/Matchers/DfaMatcherConformanceTest.cs b/test/Microsoft.AspNetCore.Routing.Tests/Matchers/DfaMatcherConformanceTest.cs index b04389c5aa..07431ea51d 100644 --- a/test/Microsoft.AspNetCore.Routing.Tests/Matchers/DfaMatcherConformanceTest.cs +++ b/test/Microsoft.AspNetCore.Routing.Tests/Matchers/DfaMatcherConformanceTest.cs @@ -1,34 +1,17 @@ // 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.Threading.Tasks; -using Xunit; - namespace Microsoft.AspNetCore.Routing.Matchers { public class DfaMatcherConformanceTest : MatcherConformanceTest { - // Route values not supported - [Fact] - public override Task Match_SingleParameter_TrailingSlash() - { - return Task.CompletedTask; - } - - // Route values not supported - [Theory] - [InlineData(null, null, null, null)] - public override Task Match_MultipleParameters(string template, string path, string[] keys, string[] values) - { - GC.KeepAlive(new object[] { template, path, keys, values }); - return Task.CompletedTask; - } - - internal override Matcher CreateMatcher(MatcherEndpoint endpoint) + internal override Matcher CreateMatcher(params MatcherEndpoint[] endpoints) { var builder = new DfaMatcherBuilder(); - builder.AddEndpoint(endpoint); + for (int i = 0; i < endpoints.Length; i++) + { + builder.AddEndpoint(endpoints[i]); + } return builder.Build(); } } diff --git a/test/Microsoft.AspNetCore.Routing.Tests/Matchers/DispatcherAssert.cs b/test/Microsoft.AspNetCore.Routing.Tests/Matchers/DispatcherAssert.cs index 9103fd390e..58a5e05ded 100644 --- a/test/Microsoft.AspNetCore.Routing.Tests/Matchers/DispatcherAssert.cs +++ b/test/Microsoft.AspNetCore.Routing.Tests/Matchers/DispatcherAssert.cs @@ -1,6 +1,7 @@ // 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 Xunit.Sdk; @@ -14,8 +15,21 @@ namespace Microsoft.AspNetCore.Routing.Matchers AssertMatch(feature, expected, new RouteValueDictionary()); } + public static void AssertMatch(IEndpointFeature feature, Endpoint expected, bool ignoreValues) + { + AssertMatch(feature, expected, new RouteValueDictionary(), ignoreValues); + } + + public static void AssertMatch(IEndpointFeature feature, Endpoint expected, object values) + { + AssertMatch(feature, expected, new RouteValueDictionary(values)); + } + public static void AssertMatch(IEndpointFeature feature, Endpoint expected, string[] keys, string[] values) { + keys = keys ?? Array.Empty(); + values = values ?? Array.Empty(); + if (keys.Length != values.Length) { throw new XunitException($"Keys and Values must be the same length."); @@ -25,7 +39,11 @@ namespace Microsoft.AspNetCore.Routing.Matchers AssertMatch(feature, expected, new RouteValueDictionary(zipped)); } - public static void AssertMatch(IEndpointFeature feature, Endpoint expected, RouteValueDictionary values) + public static void AssertMatch( + IEndpointFeature feature, + Endpoint expected, + RouteValueDictionary values, + bool ignoreValues = false) { if (feature.Endpoint == null) { @@ -44,14 +62,17 @@ namespace Microsoft.AspNetCore.Routing.Matchers $"'{feature.Endpoint.DisplayName}' with values: {FormatRouteValues(feature.Values)}."); } - // Note: this comparison is intended for unit testing, and is stricter than necessary to make tests - // more precise. Route value comparisons in product code are more flexible than a simple .Equals. - if (values.Count != feature.Values.Count || - !values.OrderBy(kvp => kvp.Key).SequenceEqual(feature.Values.OrderBy(kvp => kvp.Key))) + if (!ignoreValues) { - throw new XunitException( - $"Was expected to match '{expected.DisplayName}' with values {FormatRouteValues(values)} but matched " + - $"values: {FormatRouteValues(feature.Values)}."); + // Note: this comparison is intended for unit testing, and is stricter than necessary to make tests + // more precise. Route value comparisons in product code are more flexible than a simple .Equals. + if (values.Count != feature.Values.Count || + !values.OrderBy(kvp => kvp.Key).SequenceEqual(feature.Values.OrderBy(kvp => kvp.Key))) + { + throw new XunitException( + $"Was expected to match '{expected.DisplayName}' with values {FormatRouteValues(values)} but matched " + + $"values: {FormatRouteValues(feature.Values)}."); + } } } diff --git a/test/Microsoft.AspNetCore.Routing.Tests/Matchers/FullFeaturedMatcherConformanceTest.cs b/test/Microsoft.AspNetCore.Routing.Tests/Matchers/FullFeaturedMatcherConformanceTest.cs new file mode 100644 index 0000000000..a3fa0bee46 --- /dev/null +++ b/test/Microsoft.AspNetCore.Routing.Tests/Matchers/FullFeaturedMatcherConformanceTest.cs @@ -0,0 +1,437 @@ +// 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.Linq; +using System.Threading.Tasks; +using Xunit; + +namespace Microsoft.AspNetCore.Routing.Matchers +{ + // This class includes features that we have not yet implemented in the DFA + // and instruction matchers. + // + // As those matchers add features we can move tests from this class into + // MatcherConformanceTest and delete this. + public abstract class FullFeaturedMatcherConformanceTest : MatcherConformanceTest + { + [Theory] + [InlineData("/a/{b=15}", "/a/b", new string[] { "b", }, new string[] { "b", })] + [InlineData("/a/{b=15}", "/a/", new string[] { "b", }, new string[] { "15", })] + [InlineData("/a/{b=15}", "/a", new string[] { "b", }, new string[] { "15", })] + [InlineData("/{a}/{b=15}", "/54/b", new string[] { "a", "b", }, new string[] { "54", "b", })] + [InlineData("/{a=19}/{b=15}", "/54/b", new string[] { "a", "b", }, new string[] { "54", "b", })] + [InlineData("/{a=19}/{b=15}", "/54/", new string[] { "a", "b", }, new string[] { "54", "15", })] + [InlineData("/{a=19}/{b=15}", "/54", new string[] { "a", "b", }, new string[] { "54", "15", })] + [InlineData("/{a=19}/{b=15}", "/", new string[] { "a", "b", }, new string[] { "19", "15", })] + public virtual async Task Match_DefaultValues(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); + } + + [Fact] + public virtual async Task Match_NonInlineDefaultValues() + { + // Arrange + var endpoint = CreateEndpoint("/a/{b}/{c}", new { b = "17", c = "18", }); + var matcher = CreateMatcher(endpoint); + var (httpContext, feature) = CreateContext("/a"); + + // Act + await matcher.MatchAsync(httpContext, feature); + + // Assert + DispatcherAssert.AssertMatch(feature, endpoint, new { b = "17", c = "18", }); + } + + [Fact] + public virtual async Task Match_ExtraDefaultValues() + { + // Arrange + var endpoint = CreateEndpoint("/a/{b}/{c}", new { b = "17", c = "18", d = "19" }); + var matcher = CreateMatcher(endpoint); + var (httpContext, feature) = CreateContext("/a"); + + // Act + await matcher.MatchAsync(httpContext, feature); + + // Assert + DispatcherAssert.AssertMatch(feature, endpoint, new { b = "17", c = "18", d = "19" }); + } + + [Theory] + [InlineData("/a/{b=15}", "/54/b")] + [InlineData("/a/{b=15}", "/54/")] + [InlineData("/a/{b=15}", "/54")] + [InlineData("/a/{b=15}", "/a//")] + [InlineData("/a/{b=15}", "/54/43/23")] + [InlineData("/{a=19}/{b=15}", "/54/b/c")] + [InlineData("/a/{b=15}/c", "/a/b")] // Intermediate default values don't act like optional segments + [InlineData("a/{b=3}/c/{d?}/e/{*f}", "/a")] + [InlineData("a/{b=3}/c/{d?}/e/{*f}", "/a/b")] + [InlineData("a/{b=3}/c/{d?}/e/{*f}", "/a/b/c")] + [InlineData("a/{b=3}/c/{d?}/e/{*f}", "/a/b/c/d")] + public virtual async Task NotMatch_DefaultValues(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); + } + + [Theory] + [InlineData("/{a?}/{b?}/{c?}", "/", null, null)] + [InlineData("/{a?}/{b?}/{c?}", "/a", new[] { "a", }, new[] { "a", })] + [InlineData("/{a?}/{b?}/{c?}", "/a/", new[] { "a", }, new[] { "a", })] + [InlineData("/{a?}/{b?}/{c?}", "/a/b", new[] { "a", "b", }, new[] { "a", "b", })] + [InlineData("/{a?}/{b?}/{c?}", "/a/b/", new[] { "a", "b", }, new[] { "a", "b", })] + [InlineData("/{a?}/{b?}/{c?}", "/a/b/c", new[] { "a", "b", "c", }, new[] { "a", "b", "c", })] + [InlineData("/{a?}/{b?}/{c?}", "/a/b/c/", new[] { "a", "b", "c", }, new[] { "a", "b", "c", })] + [InlineData("/{c}/{a?}", "/h/i", new[] { "c", "a", }, new[] { "h", "i", })] + [InlineData("/{c}/{a?}", "/h/", new[] { "c", }, new[] { "h", })] + [InlineData("/{c}/{a?}", "/h", new[] { "c", }, new[] { "h", })] + [InlineData("/{c?}/{a?}", "/", null, null)] + [InlineData("/{c}/{a?}/{id?}", "/h/i/18", new[] { "c", "a", "id", }, new[] { "h", "i", "18", })] + [InlineData("/{c}/{a?}/{id?}", "/h/i", new[] { "c", "a", }, new[] { "h", "i", })] + [InlineData("/{c}/{a?}/{id?}", "/h", new[] { "c", }, new[] { "h", })] + [InlineData("template/{p:int?}", "/template/5", new[] { "p", }, new[] { "5", })] + [InlineData("template/{p:int?}", "/template", null, null)] + [InlineData("a/{b=3}/c/{d?}/e/{*f}", "/a/b/c/d/e", new[] { "b", "d", "f" }, new[] { "b", "d", null, })] + [InlineData("a/{b=3}/c/{d?}/e/{*f}", "/a/b/c/d/e/f", new[] { "b", "d", "f", }, new[] { "b", "d", "f", })] + public virtual async Task Match_OptionalParameter(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?}/{c?}", "///")] + [InlineData("/{a?}/{b?}/{c?}", "/a//")] + [InlineData("/{a?}/{b?}/{c?}", "/a/b//")] + [InlineData("/{a?}/{b?}/{c?}", "//b//")] + [InlineData("/{a?}/{b?}/{c?}", "///c")] + [InlineData("/{a?}/{b?}/{c?}", "///c/")] + [InlineData("/{a?}/{b?}/{c?}", "/a/b/c/d")] + [InlineData("/a/{b?}/{c?}", "/")] + [InlineData("template/{parameter:int?}", "/template/qwer")] + public virtual async Task NotMatch_OptionalParameter(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); + } + + [Theory] + [InlineData("/{a}/{*b}", "/a", new[] { "a", "b", }, new[] { "a", null, })] + [InlineData("/{a}/{*b}", "/a/", new[] { "a", "b", }, new[] { "a", null, })] + [InlineData("/{a}/{*b=b}", "/a", new[] { "a", "b", }, new[] { "a", "b", })] + [InlineData("/{a}/{*b=b}", "/a/", new[] { "a", "b", }, new[] { "a", "b", })] + [InlineData("/{a}/{*b=b}", "/a/hello", new[] { "a", "b", }, new[] { "a", "hello", })] + [InlineData("/{a}/{*b=b}", "/a/hello/goodbye", new[] { "a", "b", }, new[] { "a", "hello/goodbye", })] + [InlineData("/{a}/{*b=b}", "/a/b//", new[] { "a", "b", }, new[] { "a", "b//", })] + [InlineData("/{a}/{*b=b}", "/a/b/c/", new[] { "a", "b", }, new[] { "a", "b/c/", })] + [InlineData("/{a=1}/{b=2}/{c=3}/{d=4}", "/a/b/c", new[] { "a", "b", "c", "d", }, new[] { "a", "b", "c", "4", })] + [InlineData("a/{*path:regex(10/20/30)}", "/a/10/20/30", new[] { "path", }, new[] { "10/20/30" })] + public virtual async Task Match_CatchAllParameter(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=b}", "/a///")] + [InlineData("/{a}/{*b=b}", "/a//c/")] + public virtual async Task NotMatch_CatchAllParameter(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); + } + + [Theory] + [InlineData("{p}x{s}", "/xxxxxxxxxx", new[] { "p", "s" }, new[] { "xxxxxxxx", "x", })] + [InlineData("{p}xyz{s}", "/xxxxyzxyzxxxxxxyz", new[] { "p", "s" }, new[] { "xxxxyz", "xxxxxxyz", })] + [InlineData("{p}xyz{s}", "/abcxxxxyzxyzxxxxxxyzxx", new[] { "p", "s" }, new[] { "abcxxxxyzxyzxxxxx", "xx", })] + [InlineData("{p}xyz{s}", "/xyzxyzxyzxyzxyz", new[] { "p", "s" }, new[] { "xyzxyzxyz", "xyz", })] + [InlineData("{p}xyz{s}", "/xyzxyzxyzxyzxyz1", new[] { "p", "s" }, new[] { "xyzxyzxyzxyz", "1", })] + [InlineData("{p}xyz{s}", "/xyzxyzxyz", new[] { "p", "s" }, new[] { "xyz", "xyz", })] + [InlineData("{p}aa{s}", "/aaaaa", new[] { "p", "s" }, new[] { "aa", "a", })] + [InlineData("{p}aaa{s}", "/aaaaa", new[] { "p", "s" }, new[] { "a", "a", })] + [InlineData("language/{lang=en}-{region=US}", "/language/xx-yy", new[] { "lang", "region" }, new[] { "xx", "yy", })] + [InlineData("language/{lang}-{region}", "/language/en-US", new[] { "lang", "region" }, new[] { "en", "US", })] + [InlineData("language/{lang}-{region}a", "/language/en-USa", new[] { "lang", "region" }, new[] { "en", "US", })] + [InlineData("language/a{lang}-{region}", "/language/aen-US", new[] { "lang", "region" }, new[] { "en", "US", })] + [InlineData("language/a{lang}-{region}a", "/language/aen-USa", new[] { "lang", "region" }, new[] { "en", "US", })] + [InlineData("language/{lang}-", "/language/en-", new[] { "lang", }, new[] { "en", })] + [InlineData("language/a{lang}", "/language/aen", new[] { "lang", }, new[] { "en", })] + [InlineData("language/a{lang}a", "/language/aena", new[] { "lang", }, new[] { "en", })] + public virtual async Task Match_ComplexSegment(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("language/a{lang}-{region}a", "/language/a-USa")] + [InlineData("language/a{lang}-{region}a", "/language/aen-a")] + [InlineData("language/{lang=en}-{region=US}", "/language")] + [InlineData("language/{lang=en}-{region=US}", "/language/-")] + [InlineData("language/{lang=en}-{region=US}", "/language/xx-")] + [InlineData("language/{lang=en}-{region=US}", "/language/-xx")] + public virtual async Task NotMatch_ComplexSegment(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); + } + + [Theory] + [InlineData("moo/{p1}.{p2?}", "/moo/foo.bar", new[] { "p1", "p2", }, new[] { "foo", "bar" })] + [InlineData("moo/{p1}.{p2?}", "/moo/foo", new[] { "p1", }, new[] { "foo", })] + [InlineData("moo/{p1}.{p2?}", "/moo/.foo", new[] { "p1", }, new[] { ".foo", })] + [InlineData("moo/{p1}.{p2?}", "/moo/foo..bar", new[] { "p1", "p2", }, new[] { "foo.", "bar" })] + [InlineData("moo/{p1}.{p2?}", "/moo/foo.moo.bar", new[] { "p1", "p2", }, new[] { "foo.moo", "bar" })] + [InlineData("moo/foo.{p1}.{p2?}", "/moo/foo.moo", new[] { "p1", }, new[] { "moo", })] + [InlineData("moo/foo.{p1}.{p2?}", "/moo/foo.foo.bar", new[] { "p1", "p2", }, new[] { "foo", "bar" })] + [InlineData("moo/.{p2?}", "/moo/.foo", new[] { "p2", }, new[] { "foo", })] + [InlineData("moo/{p1}.{p2}.{p3?}", "/moo/foo.moo.bar", new[] { "p1", "p2", "p3" }, new[] { "foo", "moo", "bar" })] + [InlineData("moo/{p1}.{p2}.{p3?}", "/moo/foo.moo", new[] { "p1", "p2", }, new[] { "foo", "moo" })] + [InlineData("moo/{p1}.{p2}.{p3}.{p4?}", "/moo/foo.moo.bar", new[] { "p1", "p2", "p3" }, new[] { "foo", "moo", "bar" })] + [InlineData("{p1}.{p2?}/{p3}", "/foo.moo/bar", new[] { "p1", "p2", "p3" }, new[] { "foo", "moo", "bar" })] + [InlineData("{p1}.{p2?}/{p3}", "/foo/bar", new[] { "p1", "p3" }, new[] { "foo", "bar" })] + [InlineData("{p1}.{p2?}/{p3}", "/.foo/bar", new[] { "p1", "p3" }, new[] { ".foo", "bar" })] + [InlineData("{p1}/{p2}/{p3?}", "/foo/bar/baz", new[] { "p1", "p2", "p3" }, new[] { "foo", "bar", "baz" })] + public virtual async Task Match_OptionalSeparator(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("moo/{p1}.{p2?}", "/moo/foo.")] + [InlineData("moo/{p1}.{p2?}", "/moo/.")] + [InlineData("moo/{p1}.{p2}", "/foo.")] + [InlineData("moo/{p1}.{p2}", "/foo")] + [InlineData("moo/{p1}.{p2}.{p3?}", "/moo/foo.moo.")] + [InlineData("moo/foo.{p2}.{p3?}", "/moo/bar.foo.moo")] + [InlineData("moo/foo.{p2}.{p3?}", "/moo/kungfoo.moo.bar")] + [InlineData("moo/foo.{p2}.{p3?}", "/moo/kungfoo.moo")] + [InlineData("moo/{p1}.{p2}.{p3?}", "/moo/foo")] + [InlineData("{p1}.{p2?}/{p3}", "/foo./bar")] + [InlineData("moo/.{p2?}", "/moo/.")] + [InlineData("{p1}.{p2}/{p3}", "/.foo/bar")] + public virtual async Task NotMatch_OptionalSeparator(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); + } + + // Most of are copied from old routing tests that date back to the VS 2010 era. Enjoy! + [Theory] + [InlineData("{Controller}.mvc/../{action}", "/Home.mvc/../index", new string[] { "Controller", "action" }, new string[] { "Home", "index" })] + [InlineData("{Controller}.mvc/.../{action}", "/Home.mvc/.../index", new string[] { "Controller", "action" }, new string[] { "Home", "index" })] + [InlineData("{Controller}.mvc/../../../{action}", "/Home.mvc/../../../index", new string[] { "Controller", "action" }, new string[] { "Home", "index" })] + [InlineData("{Controller}.mvc!/{action}", "/Home.mvc!/index", new string[] { "Controller", "action" }, new string[] { "Home", "index" })] + [InlineData("../{Controller}.mvc", "/../Home.mvc", new string[] { "Controller", }, new string[] { "Home", })] + [InlineData(@"\{Controller}.mvc", @"/\Home.mvc", new string[] { "Controller", }, new string[] { "Home", })] + [InlineData(@"{Controller}.mvc\{id}\{Param1}", @"/Home.mvc\123\p1", new string[] { "Controller", "id", "Param1" }, new string[] { "Home", "123", "p1" })] + [InlineData("(Controller).mvc", "/(Controller).mvc", new string[] { }, new string[] { })] + [InlineData("Controller.mvc/ ", "/Controller.mvc/ ", new string[] { }, new string[] { })] + [InlineData("Controller.mvc ", "/Controller.mvc ", new string[] { }, new string[] { })] + public virtual async Task Match_WierdCharacterCases(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("template/5", "template/{parameter:int}")] + [InlineData("template/5", "template/{parameter}")] + [InlineData("template/5", "template/{*parameter:int}")] + [InlineData("template/5", "template/{*parameter}")] + [InlineData("template/{parameter}", "template/{parameter:alpha}")] // constraint doesn't match + [InlineData("template/{parameter:int}", "template/{parameter}")] + [InlineData("template/{parameter:int}", "template/{*parameter:int}")] + [InlineData("template/{parameter:int}", "template/{*parameter}")] + [InlineData("template/{parameter}", "template/{*parameter:int}")] + [InlineData("template/{parameter}", "template/{*parameter}")] + [InlineData("template/{*parameter:int}", "template/{*parameter}")] + public virtual async Task Match_SelectEndpoint_BasedOnPrecedence(string template1, string template2) + { + // Arrange + var expected = CreateEndpoint(template1); + var other = CreateEndpoint(template2); + var path = "/template/5"; + + // Arrange + var matcher = CreateMatcher(other, expected); + var (httpContext, feature) = CreateContext(path); + + // Act + await matcher.MatchAsync(httpContext, feature); + + // Assert + DispatcherAssert.AssertMatch(feature, expected, ignoreValues: true); + } + + [Theory] + [InlineData("template/5", "template/{parameter:int}")] + [InlineData("template/5", "template/{parameter}")] + [InlineData("template/5", "template/{*parameter:int}")] + [InlineData("template/5", "template/{*parameter}")] + [InlineData("template/{parameter:int}", "template/{parameter}")] + [InlineData("template/{parameter:int}", "template/{*parameter:int}")] + [InlineData("template/{parameter:int}", "template/{*parameter}")] + [InlineData("template/{parameter}", "template/{*parameter:int}")] + [InlineData("template/{parameter}", "template/{*parameter}")] + [InlineData("template/{*parameter:int}", "template/{*parameter}")] + [InlineData("template/5", "template/5")] + [InlineData("template/{parameter:int}", "template/{parameter:int}")] + [InlineData("template/{parameter}", "template/{parameter}")] + [InlineData("template/{*parameter:int}", "template/{*parameter:int}")] + [InlineData("template/{*parameter}", "template/{*parameter}")] + public virtual async Task Match_SelectEndpoint_BasedOnOrder(string template1, string template2) + { + // Arrange + var expected = CreateEndpoint(template1, order: 0); + var other = CreateEndpoint(template2, order: 1); + var path = "/template/5"; + + // Arrange + var matcher = CreateMatcher(other, expected); + var (httpContext, feature) = CreateContext(path); + + // Act + await matcher.MatchAsync(httpContext, feature); + + // Assert + DispatcherAssert.AssertMatch(feature, expected, ignoreValues: true); + } + + [Theory] + [InlineData("/", "")] + [InlineData("/Literal1", "Literal1")] + [InlineData("/Literal1/Literal2", "Literal1/Literal2")] + [InlineData("/Literal1/Literal2/Literal3", "Literal1/Literal2/Literal3")] + [InlineData("/Literal1/Literal2/Literal3/4", "Literal1/Literal2/Literal3/{*constrainedCatchAll:int}")] + [InlineData("/Literal1/Literal2/Literal3/Literal4", "Literal1/Literal2/Literal3/{*catchAll}")] + [InlineData("/1", "{constrained1:int}")] + [InlineData("/1/2", "{constrained1:int}/{constrained2:int}")] + [InlineData("/1/2/3", "{constrained1:int}/{constrained2:int}/{constrained3:int}")] + [InlineData("/1/2/3/4", "{constrained1:int}/{constrained2:int}/{constrained3:int}/{*constrainedCatchAll:int}")] + [InlineData("/1/2/3/CatchAll4", "{constrained1:int}/{constrained2:int}/{constrained3:int}/{*catchAll}")] + [InlineData("/parameter1", "{parameter1}")] + [InlineData("/parameter1/parameter2", "{parameter1}/{parameter2}")] + [InlineData("/parameter1/parameter2/parameter3", "{parameter1}/{parameter2}/{parameter3}")] + [InlineData("/parameter1/parameter2/parameter3/4", "{parameter1}/{parameter2}/{parameter3}/{*constrainedCatchAll:int}")] + [InlineData("/parameter1/parameter2/parameter3/CatchAll4", "{parameter1}/{parameter2}/{parameter3}/{*catchAll}")] + public virtual async Task Match_IntegrationTest_MultipleEndpoints(string path, string expectedTemplate) + { + // Arrange + var templates = new[] + { + "", + "Literal1", + "Literal1/Literal2", + "Literal1/Literal2/Literal3", + "Literal1/Literal2/Literal3/{*constrainedCatchAll:int}", + "Literal1/Literal2/Literal3/{*catchAll}", + "{constrained1:int}", + "{constrained1:int}/{constrained2:int}", + "{constrained1:int}/{constrained2:int}/{constrained3:int}", + "{constrained1:int}/{constrained2:int}/{constrained3:int}/{*constrainedCatchAll:int}", + "{constrained1:int}/{constrained2:int}/{constrained3:int}/{*catchAll}", + "{parameter1}", + "{parameter1}/{parameter2}", + "{parameter1}/{parameter2}/{parameter3}", + "{parameter1}/{parameter2}/{parameter3}/{*constrainedCatchAll:int}", + "{parameter1}/{parameter2}/{parameter3}/{*catchAll}", + }; + + var endpoints = templates.Select((t) => CreateEndpoint(t)).ToArray(); + var expected = endpoints[Array.IndexOf(templates, expectedTemplate)]; + + var matcher = CreateMatcher(endpoints); + var (httpContext, feature) = CreateContext(path); + + // Act + await matcher.MatchAsync(httpContext, feature); + + // Assert + DispatcherAssert.AssertMatch(feature, expected, ignoreValues: true); + } + } +} diff --git a/test/Microsoft.AspNetCore.Routing.Tests/Matchers/InstructionMatcherConformanceTest.cs b/test/Microsoft.AspNetCore.Routing.Tests/Matchers/InstructionMatcherConformanceTest.cs index 1fe8c45eec..65a902ff58 100644 --- a/test/Microsoft.AspNetCore.Routing.Tests/Matchers/InstructionMatcherConformanceTest.cs +++ b/test/Microsoft.AspNetCore.Routing.Tests/Matchers/InstructionMatcherConformanceTest.cs @@ -1,18 +1,17 @@ // 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.Threading.Tasks; -using Xunit; - namespace Microsoft.AspNetCore.Routing.Matchers { public class InstructionMatcherConformanceTest : MatcherConformanceTest { - internal override Matcher CreateMatcher(MatcherEndpoint endpoint) + internal override Matcher CreateMatcher(params MatcherEndpoint[] endpoints) { var builder = new InstructionMatcherBuilder(); - builder.AddEndpoint(endpoint); + for (int i = 0; i < endpoints.Length; i++) + { + builder.AddEndpoint(endpoints[i]); + } return builder.Build(); } } diff --git a/test/Microsoft.AspNetCore.Routing.Tests/Matchers/MatcherConformanceTest.MultipleEndpoint.cs b/test/Microsoft.AspNetCore.Routing.Tests/Matchers/MatcherConformanceTest.MultipleEndpoint.cs new file mode 100644 index 0000000000..0784a2ebb5 --- /dev/null +++ b/test/Microsoft.AspNetCore.Routing.Tests/Matchers/MatcherConformanceTest.MultipleEndpoint.cs @@ -0,0 +1,9 @@ +// 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. + +namespace Microsoft.AspNetCore.Routing.Matchers +{ + public abstract partial class MatcherConformanceTest + { + } +} diff --git a/test/Microsoft.AspNetCore.Routing.Tests/Matchers/MatcherConformanceTest.SingleEndpoint.cs b/test/Microsoft.AspNetCore.Routing.Tests/Matchers/MatcherConformanceTest.SingleEndpoint.cs new file mode 100644 index 0000000000..58fa2d9282 --- /dev/null +++ b/test/Microsoft.AspNetCore.Routing.Tests/Matchers/MatcherConformanceTest.SingleEndpoint.cs @@ -0,0 +1,314 @@ +// 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.Threading.Tasks; +using Xunit; + +namespace Microsoft.AspNetCore.Routing.Matchers +{ + public abstract partial class MatcherConformanceTest + { + [Fact] + public virtual async Task Match_EmptyRoute() + { + // Arrange + var (matcher, endpoint) = CreateMatcher("/"); + var (httpContext, feature) = CreateContext("/"); + + // Act + await matcher.MatchAsync(httpContext, feature); + + // Assert + DispatcherAssert.AssertMatch(feature, 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); + } + + [Fact] + public virtual async Task Match_SingleParameter_WierdNames() + { + // Arrange + var (matcher, endpoint) = CreateMatcher("/foo/{ }/{.!$%}/{dynamic.data}"); + var (httpContext, feature) = CreateContext("/foo/space/weirdmatch/matcherid"); + var values = new RouteValueDictionary() + { + { " ", "space" }, + { ".!$%", "weirdmatch" }, + { "dynamic.data", "matcherid" }, + }; + + // 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("/{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); + } + } +} diff --git a/test/Microsoft.AspNetCore.Routing.Tests/Matchers/MatcherConformanceTest.cs b/test/Microsoft.AspNetCore.Routing.Tests/Matchers/MatcherConformanceTest.cs index d9de0ddaee..eb331a328a 100644 --- a/test/Microsoft.AspNetCore.Routing.Tests/Matchers/MatcherConformanceTest.cs +++ b/test/Microsoft.AspNetCore.Routing.Tests/Matchers/MatcherConformanceTest.cs @@ -2,289 +2,14 @@ // 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 + public abstract partial 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 abstract Matcher CreateMatcher(params MatcherEndpoint[] endpoints); internal static (HttpContext httpContext, IEndpointFeature feature) CreateContext(string path) { @@ -307,14 +32,17 @@ namespace Microsoft.AspNetCore.Routing.Matchers return services.BuildServiceProvider(); } - internal static MatcherEndpoint CreateEndpoint(string template) + internal static MatcherEndpoint CreateEndpoint( + string template, + object defaults = null, + int? order = null) { return new MatcherEndpoint( MatcherEndpoint.EmptyInvoker, template, - null, - 0, - EndpointMetadataCollection.Empty, + defaults, + order ?? 0, + EndpointMetadataCollection.Empty, "endpoint: " + template, address: null); } diff --git a/test/Microsoft.AspNetCore.Routing.Tests/Matchers/RouteMatcherBuilder.cs b/test/Microsoft.AspNetCore.Routing.Tests/Matchers/RouteMatcherBuilder.cs index 6b4e2e7ae3..e411d69647 100644 --- a/test/Microsoft.AspNetCore.Routing.Tests/Matchers/RouteMatcherBuilder.cs +++ b/test/Microsoft.AspNetCore.Routing.Tests/Matchers/RouteMatcherBuilder.cs @@ -1,19 +1,23 @@ // 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.Threading.Tasks; +using Microsoft.AspNetCore.Routing.Template; using Microsoft.Extensions.Options; namespace Microsoft.AspNetCore.Routing.Matchers { internal class RouteMatcherBuilder : MatcherBuilder { - private readonly RouteCollection _routes = new RouteCollection(); private readonly IInlineConstraintResolver _constraintResolver; + private readonly List _entries; public RouteMatcherBuilder() { _constraintResolver = new DefaultInlineConstraintResolver(Options.Create(new RouteOptions())); + _entries = new List(); } public override void AddEndpoint(MatcherEndpoint endpoint) @@ -23,12 +27,66 @@ namespace Microsoft.AspNetCore.Routing.Matchers c.Features.Get().Endpoint = endpoint; return Task.CompletedTask; }); - _routes.Add(new Route(handler, endpoint.Template, _constraintResolver)); + + // MatcherEndpoint.Values contains the default values parsed from the template + // as well as those specified with a literal. We need to separate those + // for legacy cases. + var defaults = new RouteValueDictionary(endpoint.Values); + for (var i = 0; i < endpoint.ParsedTemlate.Parameters.Count; i++) + { + var parameter = endpoint.ParsedTemlate.Parameters[i]; + if (parameter.DefaultValue != null) + { + defaults.Remove(parameter.Name); + } + } + + _entries.Add(new Entry() + { + Endpoint = endpoint, + Route = new Route( + handler, + endpoint.Template, + defaults, + new Dictionary(), + new RouteValueDictionary(), + _constraintResolver), + }); } public override Matcher Build() { - return new RouteMatcher(_routes); + _entries.Sort(); + var routes = new RouteCollection(); + for (var i = 0; i < _entries.Count; i++) + { + routes.Add(_entries[i].Route); + } + + return new RouteMatcher(routes); + } + + private struct Entry : IComparable + { + public MatcherEndpoint Endpoint; + public Route Route; + + public int CompareTo(Entry other) + { + var comparison = Endpoint.Order.CompareTo(other.Endpoint.Order); + if (comparison != 0) + { + return comparison; + } + + comparison = RoutePrecedence.ComputeInbound(Endpoint.ParsedTemlate).CompareTo(RoutePrecedence.ComputeInbound(other.Endpoint.ParsedTemlate)); + if (comparison != 0) + { + return comparison; + } + + return Endpoint.Template.CompareTo(other.Endpoint.Template); + } } } } diff --git a/test/Microsoft.AspNetCore.Routing.Tests/Matchers/RouteMatcherConformanceTest.cs b/test/Microsoft.AspNetCore.Routing.Tests/Matchers/RouteMatcherConformanceTest.cs index 998261445f..68589c36c4 100644 --- a/test/Microsoft.AspNetCore.Routing.Tests/Matchers/RouteMatcherConformanceTest.cs +++ b/test/Microsoft.AspNetCore.Routing.Tests/Matchers/RouteMatcherConformanceTest.cs @@ -3,12 +3,15 @@ namespace Microsoft.AspNetCore.Routing.Matchers { - public class RouteMatcherConformanceTest : MatcherConformanceTest + public class RouteMatcherConformanceTest : FullFeaturedMatcherConformanceTest { - internal override Matcher CreateMatcher(MatcherEndpoint endpoint) + internal override Matcher CreateMatcher(params MatcherEndpoint[] endpoints) { var builder = new RouteMatcherBuilder(); - builder.AddEndpoint(endpoint); + for (int i = 0; i < endpoints.Length; i++) + { + builder.AddEndpoint(endpoints[i]); + } return builder.Build(); } } diff --git a/test/Microsoft.AspNetCore.Routing.Tests/Matchers/TreeMatcherTests.cs b/test/Microsoft.AspNetCore.Routing.Tests/Matchers/TreeMatcherTests.cs index a08e68ede7..c876adf739 100644 --- a/test/Microsoft.AspNetCore.Routing.Tests/Matchers/TreeMatcherTests.cs +++ b/test/Microsoft.AspNetCore.Routing.Tests/Matchers/TreeMatcherTests.cs @@ -88,6 +88,6 @@ namespace Microsoft.AspNetCore.Routing.Matchers // Assert Assert.Equal(endpointWithConstraint, endpointFeature.Endpoint); + } } } -} diff --git a/test/Microsoft.AspNetCore.Routing.Tests/Matchers/TreeRouterMatcherBuilder.cs b/test/Microsoft.AspNetCore.Routing.Tests/Matchers/TreeRouterMatcherBuilder.cs index 17a70d283a..50f6e5ace3 100644 --- a/test/Microsoft.AspNetCore.Routing.Tests/Matchers/TreeRouterMatcherBuilder.cs +++ b/test/Microsoft.AspNetCore.Routing.Tests/Matchers/TreeRouterMatcherBuilder.cs @@ -1,6 +1,7 @@ // 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.Threading.Tasks; using Microsoft.AspNetCore.Routing.Internal; using Microsoft.AspNetCore.Routing.Template; @@ -34,7 +35,25 @@ namespace Microsoft.AspNetCore.Routing.Matchers return Task.CompletedTask; }); - _inner.MapInbound(handler, TemplateParser.Parse(endpoint.Template), "default", 0); + // MatcherEndpoint.Values contains the default values parsed from the template + // as well as those specified with a literal. We need to separate those + // for legacy cases. + var defaults = new RouteValueDictionary(endpoint.Values); + for (var i = 0; i < endpoint.ParsedTemlate.Parameters.Count; i++) + { + var parameter = endpoint.ParsedTemlate.Parameters[i]; + if (parameter.DefaultValue == null && defaults.ContainsKey(parameter.Name)) + { + throw new InvalidOperationException( + "The TreeRouter does not support non-inline default values."); + } + } + + _inner.MapInbound( + handler, + endpoint.ParsedTemlate, + routeName: null, + order: endpoint.Order); } public override Matcher Build() diff --git a/test/Microsoft.AspNetCore.Routing.Tests/Matchers/TreeRouterMatcherConformanceTest.cs b/test/Microsoft.AspNetCore.Routing.Tests/Matchers/TreeRouterMatcherConformanceTest.cs index 74b0be9f50..1771cf269f 100644 --- a/test/Microsoft.AspNetCore.Routing.Tests/Matchers/TreeRouterMatcherConformanceTest.cs +++ b/test/Microsoft.AspNetCore.Routing.Tests/Matchers/TreeRouterMatcherConformanceTest.cs @@ -1,14 +1,33 @@ // 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.Threading.Tasks; +using Xunit; + namespace Microsoft.AspNetCore.Routing.Matchers { - public class TreeRouterMatcherConformanceTest : MatcherConformanceTest + public class TreeRouterMatcherConformanceTest : FullFeaturedMatcherConformanceTest { - internal override Matcher CreateMatcher(MatcherEndpoint endpoint) + // TreeRouter doesn't support non-inline default values. + [Fact] + public override Task Match_NonInlineDefaultValues() + { + return Task.CompletedTask; + } + + // TreeRouter doesn't support non-inline default values. + [Fact] + public override Task Match_ExtraDefaultValues() + { + return Task.CompletedTask; + } + internal override Matcher CreateMatcher(params MatcherEndpoint[] endpoints) { var builder = new TreeRouterMatcherBuilder(); - builder.AddEndpoint(endpoint); + for (var i = 0; i < endpoints.Length; i++) + { + builder.AddEndpoint(endpoints[i]); + } return builder.Build(); } }