RouteConstraints Step II + III

Include Url Generation support
Add unit tests
Clean issues found by unit tests
This commit is contained in:
Yishai Galatzer 2014-03-28 15:46:15 -07:00
parent 2b87a625d9
commit 77ef7a5cde
8 changed files with 383 additions and 15 deletions

View File

@ -17,7 +17,7 @@ namespace Microsoft.AspNet.Routing
return BuildConstraintsCore(inputConstraints, routeTemplate);
}
public static IDictionary<string, IRouteConstraint>
private static IDictionary<string, IRouteConstraint>
BuildConstraintsCore(IDictionary<string, object> 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;

View File

@ -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<string, IRouteConstraint> constraints,
public static bool Match(IDictionary<string, IRouteConstraint> constraints,
[NotNull] IDictionary<string, object> routeValues,
[NotNull] HttpContext httpContext,
[NotNull] IRouter route,

View File

@ -11,7 +11,6 @@ namespace Microsoft.AspNet.Routing.Template
private readonly IDictionary<string, object> _defaults;
private readonly IDictionary<string, IRouteConstraint> _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<string, object> Defaults
@ -46,6 +45,11 @@ namespace Microsoft.AspNet.Routing.Template
get { return _routeTemplate; }
}
public IDictionary<string, IRouteConstraint> 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)

View File

@ -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<HttpContext>().Object,
route: new Mock<IRouter>().Object,
routeKey: routeKey,
values: values,
routeDirection: RouteDirection.IncomingRequest);
}
}
}

View File

@ -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<string, object> values,
RouteDirection routeDirection)
{
return true;
}
}
private class FailConstraint : IRouteConstraint
{
public bool Match(HttpContext httpContext,
IRouter route,
string routeKey,
IDictionary<string, object> values,
RouteDirection routeDirection)
{
return false;
}
}
[Fact]
public void ReturnsTrueOnValidConstraints()
{
var constraints = new Dictionary<string, IRouteConstraint>
{
{"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<HttpContext>().Object,
route: new Mock<IRouter>().Object,
routeDirection: RouteDirection.IncomingRequest));
}
[Fact]
public void ReturnsFalseOnInvalidConstraintsThatDontMatch()
{
var constraints = new Dictionary<string, IRouteConstraint>
{
{"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<HttpContext>().Object,
route: new Mock<IRouter>().Object,
routeDirection: RouteDirection.IncomingRequest));
}
[Fact]
public void ReturnsFalseOnInvalidConstraintsThatMatch()
{
var constraints = new Dictionary<string, IRouteConstraint>
{
{"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<HttpContext>().Object,
route: new Mock<IRouter>().Object,
routeDirection: RouteDirection.IncomingRequest));
}
[Fact]
public void ReturnsTrueOnNullInput()
{
Assert.True(RouteConstraintMatcher.Match(
constraints: null,
routeValues: new RouteValueDictionary(),
httpContext: new Mock<HttpContext>().Object,
route: new Mock<IRouter>().Object,
routeDirection: RouteDirection.IncomingRequest));
}
}
}

View File

@ -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<object> EmptyAndNullDictionary
{
get
{
return new[]
{
new Object[]
{
null,
},
new Object[]
{
new Dictionary<string, object>(),
},
};
}
}
[Theory]
[MemberData("EmptyAndNullDictionary")]
public void ConstraintBuilderReturnsNull_OnNullOrEmptyInput(IDictionary<string, object> input)
{
var result = RouteConstraintBuilder.BuildConstraints(input);
Assert.Null(result);
}
[Theory]
[MemberData("EmptyAndNullDictionary")]
public void ConstraintBuilderWithTemplateReturnsNull_OnNullOrEmptyInput(IDictionary<string, object> 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<RegexConstraint>(constraint);
}
[Fact]
public void GetRouteDataWithConstraintsThatIsCustomConstraint_IsPassThrough()
{
// Arrange
var originalConstraint = new Mock<IRouteConstraint>().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<InvalidOperationException>(
() => 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<InvalidOperationException>(
() => 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));
}
}
}

View File

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

View File

@ -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<IRouter>().Object;
// Assert
ExceptionAssert.Throws<InvalidOperationException>(() => 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<IRouter>().Object;
var mockConstraint = new Mock<IRouteConstraint>().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<RegexConstraint>(constraints["controller"]);
Assert.Equal(mockConstraint, constraints["action"]);
}
#endregion
private static TemplateRoute CreateRoute(string template, bool accept = true)
{
return new TemplateRoute(CreateTarget(accept), template);