From 77ef7a5cde5729757815bc2e79b1219f87ed956e Mon Sep 17 00:00:00 2001 From: Yishai Galatzer Date: Fri, 28 Mar 2014 15:46:15 -0700 Subject: [PATCH] RouteConstraints Step II + III Include Url Generation support Add unit tests Clean issues found by unit tests --- .../RouteConstraintBuilder.cs | 4 +- .../RouteConstraintMatcher.cs | 5 +- .../Template/TemplateRoute.cs | 25 +++- .../ConstraintExtensions.cs | 19 +++ .../ConstraintMatcherTests.cs | 102 ++++++++++++++ .../ConstraintsBuilderTests.cs | 132 ++++++++++++++++++ .../RegexConstraintTests.cs | 60 ++++++++ .../Template/TemplateRouteTests.cs | 51 ++++++- 8 files changed, 383 insertions(+), 15 deletions(-) create mode 100644 test/Microsoft.AspNet.Routing.Tests/ConstraintExtensions.cs create mode 100644 test/Microsoft.AspNet.Routing.Tests/ConstraintMatcherTests.cs create mode 100644 test/Microsoft.AspNet.Routing.Tests/ConstraintsBuilderTests.cs create mode 100644 test/Microsoft.AspNet.Routing.Tests/RegexConstraintTests.cs diff --git a/src/Microsoft.AspNet.Routing/RouteConstraintBuilder.cs b/src/Microsoft.AspNet.Routing/RouteConstraintBuilder.cs index c7383b1406..14a69237e7 100644 --- a/src/Microsoft.AspNet.Routing/RouteConstraintBuilder.cs +++ b/src/Microsoft.AspNet.Routing/RouteConstraintBuilder.cs @@ -17,7 +17,7 @@ namespace Microsoft.AspNet.Routing return BuildConstraintsCore(inputConstraints, routeTemplate); } - public static IDictionary + private static IDictionary BuildConstraintsCore(IDictionary inputConstraints, string routeTemplate) { if (inputConstraints == null || inputConstraints.Count == 0) @@ -31,7 +31,7 @@ namespace Microsoft.AspNet.Routing { var constraint = kvp.Value as IRouteConstraint; - if (constraint == null) + if (constraint == null) { var regexPattern = kvp.Value as string; diff --git a/src/Microsoft.AspNet.Routing/RouteConstraintMatcher.cs b/src/Microsoft.AspNet.Routing/RouteConstraintMatcher.cs index 0197f06201..4ff5abdbeb 100644 --- a/src/Microsoft.AspNet.Routing/RouteConstraintMatcher.cs +++ b/src/Microsoft.AspNet.Routing/RouteConstraintMatcher.cs @@ -1,12 +1,11 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using Microsoft.AspNet.Abstractions; namespace Microsoft.AspNet.Routing { public static class RouteConstraintMatcher { - public static bool Match([NotNull] IDictionary constraints, + public static bool Match(IDictionary constraints, [NotNull] IDictionary routeValues, [NotNull] HttpContext httpContext, [NotNull] IRouter route, diff --git a/src/Microsoft.AspNet.Routing/Template/TemplateRoute.cs b/src/Microsoft.AspNet.Routing/Template/TemplateRoute.cs index fa1cd4c88c..873f6d8dbd 100644 --- a/src/Microsoft.AspNet.Routing/Template/TemplateRoute.cs +++ b/src/Microsoft.AspNet.Routing/Template/TemplateRoute.cs @@ -11,7 +11,6 @@ namespace Microsoft.AspNet.Routing.Template private readonly IDictionary _defaults; private readonly IDictionary _constraints; private readonly IRouter _target; - private readonly Template _parsedTemplate; private readonly string _routeTemplate; private readonly TemplateMatcher _matcher; private readonly TemplateBinder _binder; @@ -30,10 +29,10 @@ namespace Microsoft.AspNet.Routing.Template _constraints = RouteConstraintBuilder.BuildConstraints(constraints, _routeTemplate); // The parser will throw for invalid routes. - _parsedTemplate = TemplateParser.Parse(RouteTemplate); + var parsedTemplate = TemplateParser.Parse(RouteTemplate); - _matcher = new TemplateMatcher(_parsedTemplate); - _binder = new TemplateBinder(_parsedTemplate, _defaults); + _matcher = new TemplateMatcher(parsedTemplate); + _binder = new TemplateBinder(parsedTemplate, _defaults); } public IDictionary Defaults @@ -46,6 +45,11 @@ namespace Microsoft.AspNet.Routing.Template get { return _routeTemplate; } } + public IDictionary Constraints + { + get { return _constraints; } + } + public async virtual Task RouteAsync([NotNull] RouteContext context) { var requestPath = context.RequestPath; @@ -54,7 +58,7 @@ namespace Microsoft.AspNet.Routing.Template requestPath = requestPath.Substring(1); } - var values = _matcher.Match(requestPath, _defaults); + var values = _matcher.Match(requestPath, Defaults); if (values == null) { // If we got back a null value set, that means the URI did not match @@ -65,7 +69,7 @@ namespace Microsoft.AspNet.Routing.Template // Not currently doing anything to clean this up if it's not a match. Consider hardening this. context.Values = values; - if (RouteConstraintMatcher.Match(_constraints, + if (RouteConstraintMatcher.Match(Constraints, values, context.HttpContext, this, @@ -85,6 +89,15 @@ namespace Microsoft.AspNet.Routing.Template return null; } + if (!RouteConstraintMatcher.Match(Constraints, + values, + context.Context, + this, + RouteDirection.UrlGeneration)) + { + return null; + } + // Validate that the target can accept these values. var path = _target.GetVirtualPath(context); if (path != null) diff --git a/test/Microsoft.AspNet.Routing.Tests/ConstraintExtensions.cs b/test/Microsoft.AspNet.Routing.Tests/ConstraintExtensions.cs new file mode 100644 index 0000000000..2d90575667 --- /dev/null +++ b/test/Microsoft.AspNet.Routing.Tests/ConstraintExtensions.cs @@ -0,0 +1,19 @@ +using Microsoft.AspNet.Abstractions; +using Moq; + +namespace Microsoft.AspNet.Routing.Tests +{ + public static class RouteConstraintExtensions + { + public static bool EasyMatch(this IRouteConstraint constraint, + string routeKey, + RouteValueDictionary values) + { + return constraint.Match(httpContext: new Mock().Object, + route: new Mock().Object, + routeKey: routeKey, + values: values, + routeDirection: RouteDirection.IncomingRequest); + } + } +} diff --git a/test/Microsoft.AspNet.Routing.Tests/ConstraintMatcherTests.cs b/test/Microsoft.AspNet.Routing.Tests/ConstraintMatcherTests.cs new file mode 100644 index 0000000000..c458f5f721 --- /dev/null +++ b/test/Microsoft.AspNet.Routing.Tests/ConstraintMatcherTests.cs @@ -0,0 +1,102 @@ +using System.Collections.Generic; +using Microsoft.AspNet.Abstractions; +using Moq; +using Xunit; + +namespace Microsoft.AspNet.Routing.Tests +{ + public class ConstraintMatcherTests + { + private class PassConstraint : IRouteConstraint + { + public bool Match(HttpContext httpContext, + IRouter route, + string routeKey, + IDictionary values, + RouteDirection routeDirection) + { + return true; + } + } + + private class FailConstraint : IRouteConstraint + { + public bool Match(HttpContext httpContext, + IRouter route, + string routeKey, + IDictionary values, + RouteDirection routeDirection) + { + return false; + } + } + + [Fact] + public void ReturnsTrueOnValidConstraints() + { + var constraints = new Dictionary + { + {"a", new PassConstraint()}, + {"b", new PassConstraint()} + }; + + var routeValueDictionary = new RouteValueDictionary(new {a = "value", b = "value"}); + + Assert.True(RouteConstraintMatcher.Match( + constraints: constraints, + routeValues: routeValueDictionary, + httpContext: new Mock().Object, + route: new Mock().Object, + routeDirection: RouteDirection.IncomingRequest)); + } + + [Fact] + public void ReturnsFalseOnInvalidConstraintsThatDontMatch() + { + var constraints = new Dictionary + { + {"a", new FailConstraint()}, + {"b", new FailConstraint()} + }; + + var routeValueDictionary = new RouteValueDictionary(new { c = "value", d = "value" }); + + Assert.False(RouteConstraintMatcher.Match( + constraints: constraints, + routeValues: routeValueDictionary, + httpContext: new Mock().Object, + route: new Mock().Object, + routeDirection: RouteDirection.IncomingRequest)); + } + + [Fact] + public void ReturnsFalseOnInvalidConstraintsThatMatch() + { + var constraints = new Dictionary + { + {"a", new FailConstraint()}, + {"b", new FailConstraint()} + }; + + var routeValueDictionary = new RouteValueDictionary(new { a = "value", b = "value" }); + + Assert.False(RouteConstraintMatcher.Match( + constraints: constraints, + routeValues: routeValueDictionary, + httpContext: new Mock().Object, + route: new Mock().Object, + routeDirection: RouteDirection.IncomingRequest)); + } + + [Fact] + public void ReturnsTrueOnNullInput() + { + Assert.True(RouteConstraintMatcher.Match( + constraints: null, + routeValues: new RouteValueDictionary(), + httpContext: new Mock().Object, + route: new Mock().Object, + routeDirection: RouteDirection.IncomingRequest)); + } + } +} diff --git a/test/Microsoft.AspNet.Routing.Tests/ConstraintsBuilderTests.cs b/test/Microsoft.AspNet.Routing.Tests/ConstraintsBuilderTests.cs new file mode 100644 index 0000000000..208dfa23cc --- /dev/null +++ b/test/Microsoft.AspNet.Routing.Tests/ConstraintsBuilderTests.cs @@ -0,0 +1,132 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Microsoft.AspNet.Abstractions; +using Microsoft.AspNet.Testing; +using Moq; +using Xunit; + +namespace Microsoft.AspNet.Routing.Tests +{ + public class ConstraintsBuilderTests + { + public static IEnumerable EmptyAndNullDictionary + { + get + { + return new[] + { + new Object[] + { + null, + }, + + new Object[] + { + new Dictionary(), + }, + }; + } + } + + [Theory] + [MemberData("EmptyAndNullDictionary")] + public void ConstraintBuilderReturnsNull_OnNullOrEmptyInput(IDictionary input) + { + var result = RouteConstraintBuilder.BuildConstraints(input); + + Assert.Null(result); + } + + [Theory] + [MemberData("EmptyAndNullDictionary")] + public void ConstraintBuilderWithTemplateReturnsNull_OnNullOrEmptyInput(IDictionary input) + { + var result = RouteConstraintBuilder.BuildConstraints(input, "{controller}"); + + Assert.Null(result); + } + + [Fact] + public void GetRouteDataWithConstraintsThatIsAStringCreatesARegex() + { + // Arrange + var dictionary = new RouteValueDictionary(new { controller = "abc" }); + var constraintDictionary = RouteConstraintBuilder.BuildConstraints(dictionary); + + // Assert + Assert.Equal(1, constraintDictionary.Count); + Assert.Equal("controller", constraintDictionary.First().Key); + + var constraint = constraintDictionary["controller"]; + + Assert.IsType(constraint); + } + + [Fact] + public void GetRouteDataWithConstraintsThatIsCustomConstraint_IsPassThrough() + { + // Arrange + var originalConstraint = new Mock().Object; + + var dictionary = new RouteValueDictionary(new { controller = originalConstraint }); + var constraintDictionary = RouteConstraintBuilder.BuildConstraints(dictionary); + + // Assert + Assert.Equal(1, constraintDictionary.Count); + Assert.Equal("controller", constraintDictionary.First().Key); + + var constraint = constraintDictionary["controller"]; + + Assert.Equal(originalConstraint, constraint); + } + + [Fact] + public void GetRouteDataWithConstraintsThatIsNotStringOrCustomConstraint_Throws() + { + // Arrange + var dictionary = new RouteValueDictionary(new { controller = new RouteValueDictionary() }); + + ExceptionAssert.Throws( + () => RouteConstraintBuilder.BuildConstraints(dictionary), + "The constraint entry 'controller' must have a string value or be of a type which implements '" + + typeof(IRouteConstraint) + "'."); + } + + [Fact] + public void RouteTemplateGetRouteDataWithConstraintsThatIsNotStringOrCustomConstraint_Throws() + { + // Arrange + var dictionary = new RouteValueDictionary(new { controller = new RouteValueDictionary() }); + + ExceptionAssert.Throws( + () => RouteConstraintBuilder.BuildConstraints(dictionary, "{controller}/{action}"), + "The constraint entry 'controller' on the route with route template " + + "'{controller}/{action}' must have a string value or be of a type which implements '" + + typeof(IRouteConstraint) + "'."); + } + + [Theory] + [InlineData("abc", "abc", true)] + [InlineData("Abc", "abc", true)] + [InlineData("Abc ", "abc", false)] + [InlineData("Abcd", "abc", false)] + [InlineData("Abc", " abc", false)] + public void StringConstraintsMatchesWholeValueCaseInsensitively(string routeValue, + string constraintValue, + bool shouldMatch) + { + // Arrange + var dictionary = new RouteValueDictionary(new { controller = routeValue }); + + var constraintDictionary = RouteConstraintBuilder.BuildConstraints( + new RouteValueDictionary(new { controller = constraintValue })); + var constraint = constraintDictionary["controller"]; + + Assert.Equal(shouldMatch, + constraint.EasyMatch("controller", dictionary)); + } + } +} diff --git a/test/Microsoft.AspNet.Routing.Tests/RegexConstraintTests.cs b/test/Microsoft.AspNet.Routing.Tests/RegexConstraintTests.cs new file mode 100644 index 0000000000..fa16277e37 --- /dev/null +++ b/test/Microsoft.AspNet.Routing.Tests/RegexConstraintTests.cs @@ -0,0 +1,60 @@ +using System.Text.RegularExpressions; +using Xunit; + +namespace Microsoft.AspNet.Routing.Tests +{ + public class RegexConstraintTests + { + [Theory] + [InlineData("abc", "abc", true)] + [InlineData("Abc", "abc", true)] + [InlineData("Abc ", "abc", true)] + [InlineData("Abcd", "abc", true)] + [InlineData("^Abcd", "abc", true)] + [InlineData("Abc", " abc", false)] + public void RegexConstraintDoesNotPrepend(string routeValue, + string constraintValue, + bool shouldMatch) + { + // Arrange + var constraint = new RegexConstraint(constraintValue); + var values = new RouteValueDictionary(new {controller = routeValue}); + + // Assert + Assert.Equal(shouldMatch, constraint.EasyMatch("controller", values)); + } + + [Fact] + public void RegexConstraintCanTakeARegex_SuccessulMatch() + { + // Arrange + var constraint = new RegexConstraint(new Regex("^abc$")); + var values = new RouteValueDictionary(new { controller = "abc"}); + + // Assert + Assert.True(constraint.EasyMatch("controller", values)); + } + + [Fact] + public void RegexConstraintFailsIfKeyIsNotFound() + { + // Arrange + var constraint = new RegexConstraint(new Regex("^abc$")); + var values = new RouteValueDictionary(new { action = "abc" }); + + // Assert + Assert.False(constraint.EasyMatch("controller", values)); + } + + [Fact] + public void RegexConstraintCanTakeARegex_FailedMatch() + { + // Arrange + var constraint = new RegexConstraint(new Regex("^abc$")); + var values = new RouteValueDictionary(new { controller = "Abc" }); + + // Assert + Assert.False(constraint.EasyMatch("controller", values)); + } + } +} diff --git a/test/Microsoft.AspNet.Routing.Tests/Template/TemplateRouteTests.cs b/test/Microsoft.AspNet.Routing.Tests/Template/TemplateRouteTests.cs index 127350ad0b..c6592375aa 100644 --- a/test/Microsoft.AspNet.Routing.Tests/Template/TemplateRouteTests.cs +++ b/test/Microsoft.AspNet.Routing.Tests/Template/TemplateRouteTests.cs @@ -1,8 +1,9 @@ // Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. #if NET45 - -using System.Collections.Generic; +using System; +using Microsoft.AspNet.Testing; + using System.Collections.Generic; using System.Threading.Tasks; using Microsoft.AspNet.Abstractions; using Moq; @@ -115,7 +116,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests { // Arrange var route = CreateRoute("{controller}"); - var context = CreateVirtualPathContext(new {controller = "Home"}); + var context = CreateVirtualPathContext(new { controller = "Home" }); // Act var path = route.GetVirtualPath(context); @@ -160,7 +161,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests { // Arrange var route = CreateRoute("{controller}/{action}"); - var context = CreateVirtualPathContext(new { action = "Index"}, new { controller = "Home" }); + var context = CreateVirtualPathContext(new { action = "Index" }, new { controller = "Home" }); // Act var path = route.GetVirtualPath(context); @@ -189,6 +190,48 @@ namespace Microsoft.AspNet.Routing.Template.Tests #endregion + #region Route Registration + + [Fact] + public void RegisteringRouteWithInvalidConstraints_Throws() + { + // Arrange + var collection = new RouteCollection(); + collection.DefaultHandler = new Mock().Object; + + // Assert + ExceptionAssert.Throws(() => collection.MapRoute("{controller}/{action}", + defaults: null, + constraints: new { controller = "a.*", action = new Object() }), + "The constraint entry 'action' on the route with route template '{controller}/{action}' " + + "must have a string value or be of a type which implements '" + + typeof(IRouteConstraint) + "'."); + } + + [Fact] + public void RegisteringRouteWithTwoConstraints() + { + // Arrange + var collection = new RouteCollection(); + collection.DefaultHandler = new Mock().Object; + + var mockConstraint = new Mock().Object; + + collection.MapRoute("{controller}/{action}", + defaults: null, + constraints: new {controller = "a.*", action = mockConstraint}); + + var constraints = ((TemplateRoute) collection[0]).Constraints; + + // Assert + Assert.Equal(2, constraints.Count); + Assert.IsType(constraints["controller"]); + Assert.Equal(mockConstraint, constraints["action"]); + + } + + #endregion + private static TemplateRoute CreateRoute(string template, bool accept = true) { return new TemplateRoute(CreateTarget(accept), template);