Fixing Routing Issue: 136

The RegEx inline constraint doesn't take care of Escape characters.
This commit is contained in:
Mugdha Kulkarni 2014-12-21 16:22:50 -08:00
parent 67dcdbf8a1
commit 4e5fc2e2dd
10 changed files with 336 additions and 60 deletions

View File

@ -203,7 +203,7 @@ namespace Microsoft.AspNet.Routing
} }
/// <summary> /// <summary>
/// A path segment that contains more than one section, such as a literal section or a parameter, cannot contain an optional parameter. /// In a path segment that contains more than one section, such as a literal section or a parameter, there can only be one optional parameter. The optional parameter must be the last parameter in the segment and must be preceded by one single period (.).
/// </summary> /// </summary>
internal static string TemplateRoute_CanHaveOnlyLastParameterOptional_IfFollowingOptionalSeperator internal static string TemplateRoute_CanHaveOnlyLastParameterOptional_IfFollowingOptionalSeperator
{ {
@ -211,7 +211,7 @@ namespace Microsoft.AspNet.Routing
} }
/// <summary> /// <summary>
/// A path segment that contains more than one section, such as a literal section or a parameter, cannot contain an optional parameter. /// In a path segment that contains more than one section, such as a literal section or a parameter, there can only be one optional parameter. The optional parameter must be the last parameter in the segment and must be preceded by one single period (.).
/// </summary> /// </summary>
internal static string FormatTemplateRoute_CanHaveOnlyLastParameterOptional_IfFollowingOptionalSeperator() internal static string FormatTemplateRoute_CanHaveOnlyLastParameterOptional_IfFollowingOptionalSeperator()
{ {
@ -347,7 +347,7 @@ namespace Microsoft.AspNet.Routing
} }
/// <summary> /// <summary>
/// The constraint entry '{0}' - '{1}' on route '{2}' must have a string value or be of a type which implements '{3}'. /// The constraint entry '{0}' - '{1}' on the route '{2}' must have a string value or be of a type which implements '{3}'.
/// </summary> /// </summary>
internal static string RouteConstraintBuilder_ValidationMustBeStringOrCustomConstraint internal static string RouteConstraintBuilder_ValidationMustBeStringOrCustomConstraint
{ {
@ -355,7 +355,7 @@ namespace Microsoft.AspNet.Routing
} }
/// <summary> /// <summary>
/// The constraint entry '{0}' - '{1}' on route '{2}' must have a string value or be of a type which implements '{3}'. /// The constraint entry '{0}' - '{1}' on the route '{2}' must have a string value or be of a type which implements '{3}'.
/// </summary> /// </summary>
internal static string FormatRouteConstraintBuilder_ValidationMustBeStringOrCustomConstraint(object p0, object p1, object p2, object p3) internal static string FormatRouteConstraintBuilder_ValidationMustBeStringOrCustomConstraint(object p0, object p1, object p2, object p3)
{ {
@ -363,7 +363,7 @@ namespace Microsoft.AspNet.Routing
} }
/// <summary> /// <summary>
/// The constraint entry '{0}' - '{1}' on route '{2}' could not be resolved by the constraint resolver of type '{3}'. /// The constraint entry '{0}' - '{1}' on the route '{2}' could not be resolved by the constraint resolver of type '{3}'.
/// </summary> /// </summary>
internal static string RouteConstraintBuilder_CouldNotResolveConstraint internal static string RouteConstraintBuilder_CouldNotResolveConstraint
{ {
@ -371,13 +371,29 @@ namespace Microsoft.AspNet.Routing
} }
/// <summary> /// <summary>
/// The constraint entry '{0}' - '{1}' on route '{2}' could not be resolved by the constraint resolver of type '{3}'. /// The constraint entry '{0}' - '{1}' on the route '{2}' could not be resolved by the constraint resolver of type '{3}'.
/// </summary> /// </summary>
internal static string FormatRouteConstraintBuilder_CouldNotResolveConstraint(object p0, object p1, object p2, object p3) internal static string FormatRouteConstraintBuilder_CouldNotResolveConstraint(object p0, object p1, object p2, object p3)
{ {
return string.Format(CultureInfo.CurrentCulture, GetString("RouteConstraintBuilder_CouldNotResolveConstraint"), p0, p1, p2, p3); return string.Format(CultureInfo.CurrentCulture, GetString("RouteConstraintBuilder_CouldNotResolveConstraint"), p0, p1, p2, p3);
} }
/// <summary>
/// In a route parameter, '{' and '}' must be escaped with '{{' and '}}'
/// </summary>
internal static string TemplateRoute_UnescapedBrace
{
get { return GetString("TemplateRoute_UnescapedBrace"); }
}
/// <summary>
/// In a route parameter, '{' and '}' must be escaped with '{{' and '}}'
/// </summary>
internal static string FormatTemplateRoute_UnescapedBrace()
{
return GetString("TemplateRoute_UnescapedBrace");
}
private static string GetString(string name, params string[] formatterNames) private static string GetString(string name, params string[] formatterNames)
{ {
var value = _resourceManager.GetString(name); var value = _resourceManager.GetString(name);

View File

@ -186,4 +186,7 @@
<data name="RouteConstraintBuilder_CouldNotResolveConstraint" xml:space="preserve"> <data name="RouteConstraintBuilder_CouldNotResolveConstraint" xml:space="preserve">
<value>The constraint entry '{0}' - '{1}' on the route '{2}' could not be resolved by the constraint resolver of type '{3}'.</value> <value>The constraint entry '{0}' - '{1}' on the route '{2}' could not be resolved by the constraint resolver of type '{3}'.</value>
</data> </data>
<data name="TemplateRoute_UnescapedBrace" xml:space="preserve">
<value>In a route parameter, '{' and '}' must be escaped with '{{' and '}}'</value>
</data>
</root> </root>

View File

@ -5,6 +5,7 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.Globalization; using System.Globalization;
using System.Text.RegularExpressions;
namespace Microsoft.AspNet.Routing.Template namespace Microsoft.AspNet.Routing.Template
{ {
@ -134,30 +135,41 @@ namespace Microsoft.AspNet.Routing.Template
while (true) while (true)
{ {
if (context.Current == Separator) if (context.Current == OpenBrace)
{
// This is an open brace inside of a parameter, it has to be escaped
if (context.Next())
{
if (context.Current != OpenBrace)
{
// If we see something like "{p1:regex(^\d{3", we will come here.
context.Error = Resources.TemplateRoute_UnescapedBrace;
return false;
}
}
else
{ {
// This is a dangling open-brace, which is not allowed // This is a dangling open-brace, which is not allowed
// Example: "{p1:regex(^\d{"
context.Error = Resources.TemplateRoute_MismatchedParameter; context.Error = Resources.TemplateRoute_MismatchedParameter;
return false; return false;
} }
else if (context.Current == OpenBrace)
{
// If we see a '{' while parsing a parameter name it's invalid. We'll just accept it for now
// and let the validation code for the name find it.
} }
else if (context.Current == CloseBrace) else if (context.Current == CloseBrace)
{ {
// When we encounter Closed brace here, it either means end of the parameter or it is a closed
// brace in the parameter, in that case it needs to be escaped.
// Example: {p1:regex(([}}])\w+}. First pair is escaped one and last marks end of the parameter
if (!context.Next()) if (!context.Next())
{ {
// This is the end of the string - and we have a valid parameter // This is the end of the string -and we have a valid parameter
context.Back(); context.Back();
break; break;
} }
if (context.Current == CloseBrace) if (context.Current == CloseBrace)
{ {
// This is an 'escaped' brace in a parameter name, which is not allowed. // This is an 'escaped' brace in a parameter name
// We'll just accept it for now and let the validation code for the name find it.
} }
else else
{ {
@ -176,10 +188,11 @@ namespace Microsoft.AspNet.Routing.Template
} }
var rawParameter = context.Capture(); var rawParameter = context.Capture();
var decoded = rawParameter.Replace("}}", "}").Replace("{{", "{");
// At this point, we need to parse the raw name for inline constraint, // At this point, we need to parse the raw name for inline constraint,
// default values and optional parameters. // default values and optional parameters.
var templatePart = InlineRouteParameterParser.ParseRouteParameter(rawParameter); var templatePart = InlineRouteParameterParser.ParseRouteParameter(decoded);
if (templatePart.IsCatchAll && templatePart.IsOptional) if (templatePart.IsCatchAll && templatePart.IsOptional)
{ {
@ -272,7 +285,7 @@ namespace Microsoft.AspNet.Routing.Template
} }
} }
var decoded = encoded.Replace("}}", "}").Replace("{{", "}"); var decoded = encoded.Replace("}}", "}").Replace("{{", "{");
if (IsValidLiteral(context, decoded)) if (IsValidLiteral(context, decoded))
{ {
segment.Parts.Add(TemplatePart.CreateLiteral(decoded)); segment.Parts.Add(TemplatePart.CreateLiteral(decoded));

View File

@ -23,6 +23,9 @@ namespace Microsoft.AspNet.Routing.Tests
[InlineData("Abcd", "abc", true)] // Extra char [InlineData("Abcd", "abc", true)] // Extra char
[InlineData("^Abcd", "abc", true)] // Extra special char [InlineData("^Abcd", "abc", true)] // Extra special char
[InlineData("Abc", " abc", false)] // Missing char [InlineData("Abc", " abc", false)] // Missing char
[InlineData("123-456-2334", @"^\d{3}-\d{3}-\d{4}$", true)] // ssn
[InlineData(@"12/4/2013", @"^\d{1,2}\/\d{1,2}\/\d{4}$", true)] // date
[InlineData(@"abc@def.com", @"^\w+[\w\.]*\@\w+((-\w+)|(\w*))\.[a-z]{2,3}$", true)] // email
public void RegexConstraintBuildRegexVerbatimFromInput(string routeValue, public void RegexConstraintBuildRegexVerbatimFromInput(string routeValue,
string constraintValue, string constraintValue,
bool shouldMatch) bool shouldMatch)

View File

@ -49,7 +49,7 @@ namespace Microsoft.AspNet.Routing.Tests
// Arrange, Act & Assert // Arrange, Act & Assert
var ex = Assert.Throws<InvalidOperationException>( var ex = Assert.Throws<InvalidOperationException>(
() => _constraintResolver.ResolveConstraint("int(5)")); () => _constraintResolver.ResolveConstraint("int(5)"));
Assert.Equal("Could not find a constructor for constraint type 'IntRouteConstraint'"+ Assert.Equal("Could not find a constructor for constraint type 'IntRouteConstraint'" +
" with the following number of parameters: 1.", " with the following number of parameters: 1.",
ex.Message); ex.Message);
} }
@ -74,6 +74,17 @@ namespace Microsoft.AspNet.Routing.Tests
Assert.IsType<RegexInlineRouteConstraint>(constraint); Assert.IsType<RegexInlineRouteConstraint>(constraint);
} }
[Fact]
public void ResolveConstraint_RegexInlineConstraint_WithCurlyBraces_Balanced()
{
// Arrange & Act
var constraint = _constraintResolver.ResolveConstraint(
@"regex(\\b(?<month>\\d{1,2})/(?<day>\\d{1,2})/(?<year>\\d{2,4})\\b)");
// Assert
Assert.IsType<RegexInlineRouteConstraint>(constraint);
}
[Fact] [Fact]
public void ResolveConstraint_BoolConstraint() public void ResolveConstraint_BoolConstraint()
{ {
@ -267,7 +278,7 @@ namespace Microsoft.AspNet.Routing.Tests
// Act & Assert // Act & Assert
var ex = Assert.Throws<InvalidOperationException>(() => resolver.ResolveConstraint("custom")); var ex = Assert.Throws<InvalidOperationException>(() => resolver.ResolveConstraint("custom"));
Assert.Equal("The constraint type 'System.String' which is mapped to constraint key 'custom'"+ Assert.Equal("The constraint type 'System.String' which is mapped to constraint key 'custom'" +
" must implement the 'IRouteConstraint' interface.", " must implement the 'IRouteConstraint' interface.",
ex.Message); ex.Message);
} }
@ -287,6 +298,21 @@ namespace Microsoft.AspNet.Routing.Tests
ex.Message); ex.Message);
} }
// These are cases which parsing does not catch and we'll end up here
[Theory]
[InlineData("regex(abc")]
[InlineData("int/")]
[InlineData("in{t")]
public void ResolveConstraint_Invalid_Throws(string constraint)
{
// Arrange
var routeOptions = new RouteOptions();
var resolver = GetInlineConstraintResolver(routeOptions);
// Act & Assert
Assert.Null(resolver.ResolveConstraint(constraint));
}
[Fact] [Fact]
public void ResolveConstraint_NoMatchingConstructor_Throws() public void ResolveConstraint_NoMatchingConstructor_Throws()
{ {

View File

@ -256,6 +256,36 @@ namespace Microsoft.AspNet.Routing.Tests
Assert.Single(templatePart.InlineConstraints, c => c.Constraint == @"test(\?)"); Assert.Single(templatePart.InlineConstraints, c => c.Constraint == @"test(\?)");
} }
[Fact]
public void ParseRouteParameter_ConstraintWithBraces_PatternIsParsedCorrectly()
{
// Arrange & Act
var templatePart = ParseParameter(@"p1:regex(^\d{{3}}-\d{{3}}-\d{{4}}$)"); // ssn
// Assert
Assert.Equal("p1", templatePart.Name);
Assert.Null(templatePart.DefaultValue);
Assert.False(templatePart.IsOptional);
Assert.Single(templatePart.InlineConstraints);
Assert.Single(templatePart.InlineConstraints, c => c.Constraint == @"regex(^\d{{3}}-\d{{3}}-\d{{4}}$)");
}
[Fact]
public void ParseRouteParameter_ConstraintWithBraces_WithDefaultValue()
{
// Arrange & Act
var templatePart = ParseParameter(@"p1:regex(^\d{{3}}-\d{{3}}-\d{{4}}$)=123-456-7890"); // ssn
// Assert
Assert.Equal("p1", templatePart.Name);
Assert.Equal(templatePart.DefaultValue, "123-456-7890");
Assert.False(templatePart.IsOptional);
Assert.Single(templatePart.InlineConstraints);
Assert.Single(templatePart.InlineConstraints, c => c.Constraint == @"regex(^\d{{3}}-\d{{3}}-\d{{4}}$)");
}
[Theory] [Theory]
[InlineData("", "")] [InlineData("", "")]
[InlineData("?", "")] [InlineData("?", "")]

View File

@ -101,6 +101,25 @@ namespace Microsoft.AspNet.Routing.Template.Tests
Assert.Equal("default p2", rd["p2"]); Assert.Equal("default p2", rd["p2"]);
} }
[Theory]
[InlineData(@"{p1:regex(^\d{{3}}-\d{{3}}-\d{{4}}$)}", "123-456-7890")] // ssn
[InlineData(@"{p1:regex(^\w+\@\w+\.\w+)}", "asd@assds.com")] // email
[InlineData(@"{p1:regex(([}}])\w+)}", "}sda")] // Not balanced }
[InlineData(@"{p1:regex(([{{)])\w+)}", "})sda")] // Not balanced {
public void MatchRoute_RegularExpression_Valid(
string template,
string path)
{
// Arrange
var matcher = CreateMatcher(template);
// Act
var rd = matcher.Match(path);
// Assert
Assert.NotNull(rd);
}
[Theory] [Theory]
[InlineData("moo/{p1}.{p2?}", "moo/foo.bar", "foo", "bar")] [InlineData("moo/{p1}.{p2?}", "moo/foo.bar", "foo", "bar")]
[InlineData("moo/{p1?}", "moo/foo", "foo", null)] [InlineData("moo/{p1?}", "moo/foo", "foo", null)]
@ -145,6 +164,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests
[InlineData("{p1}.{p2?}/{p3}", "foo.moo/bar", "foo", "moo", "bar")] [InlineData("{p1}.{p2?}/{p3}", "foo.moo/bar", "foo", "moo", "bar")]
[InlineData("{p1}.{p2?}/{p3}", "foo/bar", "foo", null, "bar")] [InlineData("{p1}.{p2?}/{p3}", "foo/bar", "foo", null, "bar")]
[InlineData("{p1}.{p2?}/{p3}", ".foo/bar", ".foo", null, "bar")] [InlineData("{p1}.{p2?}/{p3}", ".foo/bar", ".foo", null, "bar")]
[InlineData("{p1}/{p2}/{p3?}", "foo/bar/baz", "foo", "bar", "baz")]
public void MatchRoute_OptionalParameter_FollowedByPeriod_3Parameters_Valid( public void MatchRoute_OptionalParameter_FollowedByPeriod_3Parameters_Valid(
string template, string template,
string path, string path,
@ -159,7 +179,6 @@ namespace Microsoft.AspNet.Routing.Template.Tests
var rd = matcher.Match(path); var rd = matcher.Match(path);
// Assert // Assert
Assert.Equal(p1, rd["p1"]); Assert.Equal(p1, rd["p1"]);
if (p2 != null) if (p2 != null)

View File

@ -4,6 +4,7 @@
#if ASPNET50 #if ASPNET50
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNet.Testing; using Microsoft.AspNet.Testing;
using Microsoft.Framework.DependencyInjection; using Microsoft.Framework.DependencyInjection;
using Microsoft.Framework.DependencyInjection.Fallback; using Microsoft.Framework.DependencyInjection.Fallback;
@ -471,6 +472,61 @@ namespace Microsoft.AspNet.Routing.Template.Tests
Assert.Equal<RouteTemplate>(expected, actual, new TemplateEqualityComparer()); Assert.Equal<RouteTemplate>(expected, actual, new TemplateEqualityComparer());
} }
[Theory]
[InlineData(@"{p1:regex(^\d{{3}}-\d{{3}}-\d{{4}}$)}", @"regex(^\d{3}-\d{3}-\d{4}$)")] // ssn
[InlineData(@"{p1:regex(^\d{{1,2}}\/\d{{1,2}}\/\d{{4}}$)}", @"regex(^\d{1,2}\/\d{1,2}\/\d{4}$)")] // date
[InlineData(@"{p1:regex(^\w+\@\w+\.\w+)}", @"regex(^\w+\@\w+\.\w+)")] // email
[InlineData(@"{p1:regex(([}}])\w+)}", @"regex(([}])\w+)")] // Not balanced }
[InlineData(@"{p1:regex(([{{(])\w+)}", @"regex(([{(])\w+)")] // Not balanced {
public void Parse_RegularExpressions(string template, string constraint)
{
// Arrange
var expected = new RouteTemplate(new List<TemplateSegment>());
expected.Segments.Add(new TemplateSegment());
var c = new InlineConstraint(constraint);
expected.Segments[0].Parts.Add(
TemplatePart.CreateParameter("p1",
false,
false,
defaultValue: null,
inlineConstraints: new List<InlineConstraint> { c }));
expected.Parameters.Add(expected.Segments[0].Parts[0]);
// Act
var actual = TemplateParser.Parse(template);
// Assert
Assert.Equal<RouteTemplate>(expected, actual, new TemplateEqualityComparer());
}
[Theory]
[InlineData(@"{p1:regex(^\d{{3}}-\d{{3}}-\d{{4}}}$)}")] // extra }
[InlineData(@"{p1:regex(^\d{{3}}-\d{{3}}-\d{{4}}$)}}")] // extra } at the end
[InlineData(@"{{p1:regex(^\d{{3}}-\d{{3}}-\d{{4}}$)}")] // extra { at the begining
[InlineData(@"{p1:regex(([}])\w+}")] // Not escaped }
[InlineData(@"{p1:regex(^\d{{3}}-\d{{3}}-\d{{4}$)}")] // Not escaped }
[InlineData(@"{p1:regex(abc)")]
public void Parse_RegularExpressions_Invalid(string template)
{
// Act and Assert
ExceptionAssert.Throws<ArgumentException>(
() => TemplateParser.Parse(template),
"There is an incomplete parameter in the route template. Check that each '{' character has a matching " +
"'}' character." + Environment.NewLine + "Parameter name: routeTemplate");
}
[Theory]
[InlineData(@"{p1:regex(^\d{{3}}-\d{{3}}-\d{{{4}}$)}")] // extra {
[InlineData(@"{p1:regex(^\d{{3}}-\d{{3}}-\d{4}}$)}")] // Not escaped {
public void Parse_RegularExpressions_Unescaped(string template)
{
// Act and Assert
ExceptionAssert.Throws<ArgumentException>(
() => TemplateParser.Parse(template),
"In a route parameter, '{' and '}' must be escaped with '{{' and '}}'" + Environment.NewLine +
"Parameter name: routeTemplate");
}
[Theory] [Theory]
[InlineData("{p1?}.{p2?}")] [InlineData("{p1?}.{p2?}")]
[InlineData("{p1}.{p2?}.{p3}")] [InlineData("{p1}.{p2?}.{p3}")]
@ -565,7 +621,13 @@ namespace Microsoft.AspNet.Routing.Template.Tests
[InlineData("{*a*:int}", "a*")] [InlineData("{*a*:int}", "a*")]
[InlineData("{*a*=5}", "a*")] [InlineData("{*a*=5}", "a*")]
[InlineData("{*a*b=5}", "a*b")] [InlineData("{*a*b=5}", "a*b")]
public void ParseRouteParameter_ThrowsIf_ParameterContainsAsterisk(string template, string parameterName) [InlineData("{p1?}.{p2/}/{p3}", "p2/")]
[InlineData("{p{{}", "p{")]
[InlineData("{p}}}", "p}")]
[InlineData("{p/}", "p/")]
public void ParseRouteParameter_ThrowsIf_ParameterContainsSpecialCharacters(
string template,
string parameterName)
{ {
// Arrange // Arrange
var expectedMessage = "The route parameter name '" + parameterName + "' is invalid. Route parameter " + var expectedMessage = "The route parameter name '" + parameterName + "' is invalid. Route parameter " +
@ -634,10 +696,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests
{ {
ExceptionAssert.Throws<ArgumentException>( ExceptionAssert.Throws<ArgumentException>(
() => TemplateParser.Parse("{a}/{a{aa}/{z}"), () => TemplateParser.Parse("{a}/{a{aa}/{z}"),
"The route parameter name 'a{aa' is invalid. Route parameter names must be non-empty and cannot" + "In a route parameter, '{' and '}' must be escaped with '{{' and '}}'" + Environment.NewLine +
" contain these characters: '{', '}', '/'. The '?' character marks a parameter as optional, and" +
" can occur only at the end of the parameter. The '*' character marks a parameter as catch-all," +
" and can occur only at the start of the parameter." + Environment.NewLine +
"Parameter name: routeTemplate"); "Parameter name: routeTemplate");
} }
@ -814,11 +873,31 @@ namespace Microsoft.AspNet.Routing.Template.Tests
x.IsCatchAll != y.IsCatchAll || x.IsCatchAll != y.IsCatchAll ||
x.IsOptional != y.IsOptional || x.IsOptional != y.IsOptional ||
!String.Equals(x.Name, y.Name, StringComparison.Ordinal) || !String.Equals(x.Name, y.Name, StringComparison.Ordinal) ||
!String.Equals(x.Name, y.Name, StringComparison.Ordinal)) !String.Equals(x.Name, y.Name, StringComparison.Ordinal) ||
(x.InlineConstraints == null && y.InlineConstraints != null) ||
(x.InlineConstraints != null && y.InlineConstraints == null))
{ {
return false; return false;
} }
if (x.InlineConstraints == null && y.InlineConstraints == null)
{
return true;
}
if (x.InlineConstraints.Count() != y.InlineConstraints.Count())
{
return false;
}
foreach (var xconstraint in x.InlineConstraints)
{
if (!y.InlineConstraints.Any<InlineConstraint>(
c => string.Equals(c.Constraint, xconstraint.Constraint)))
{
return false;
}
}
return true; return true;
} }

View File

@ -12,6 +12,7 @@ using Microsoft.AspNet.Routing.Constraints;
using Microsoft.AspNet.Routing.Logging; using Microsoft.AspNet.Routing.Logging;
using Microsoft.AspNet.Testing; using Microsoft.AspNet.Testing;
using Microsoft.Framework.Logging; using Microsoft.Framework.Logging;
using Microsoft.AspNet.WebUtilities;
using Moq; using Moq;
using Xunit; using Xunit;
@ -37,6 +38,28 @@ namespace Microsoft.AspNet.Routing.Template
return Tuple.Create(sink, context); return Tuple.Create(sink, context);
} }
[Fact]
public void CreateTemplate_InlineConstraint_Regex_Malformed()
{
// Arrange
var template = @"{controller}/{action}/ {p1:regex(abc} ";
var mockTarget = new Mock<IRouter>(MockBehavior.Strict);
var expected = "The constraint entry 'p1' - 'regex(abc' on the route " +
"'{controller}/{action}/ {p1:regex(abc} ' could not be resolved by the constraint resolver of type " +
"'IInlineConstraintResolverProxy'.";
var exception = Assert.Throws<InvalidOperationException>(
() => new TemplateRoute(
mockTarget.Object,
template,
defaults: null,
constraints: null,
dataTokens: null,
inlineConstraintResolver: _inlineConstraintResolver));
Assert.Equal(expected, exception.Message);
}
[Fact] [Fact]
public async Task RouteAsync_MatchSuccess_LogsCorrectValues() public async Task RouteAsync_MatchSuccess_LogsCorrectValues()
{ {
@ -506,6 +529,48 @@ namespace Microsoft.AspNet.Routing.Template
Assert.Equal("5", context.RouteData.Values["id"]); Assert.Equal("5", context.RouteData.Values["id"]);
} }
[Fact]
public async Task RouteAsync_InlineConstraint_Regex()
{
// Arrange
var template = @"{controller}/{action}/{ssn:regex(^\d{{3}}-\d{{3}}-\d{{4}}$)}";
var context = CreateRouteContext("/Home/Index/123-456-7890");
IDictionary<string, object> routeValues = null;
var mockTarget = new Mock<IRouter>(MockBehavior.Strict);
mockTarget
.Setup(s => s.RouteAsync(It.IsAny<RouteContext>()))
.Callback<RouteContext>(ctx =>
{
routeValues = ctx.RouteData.Values;
ctx.IsHandled = true;
})
.Returns(Task.FromResult(true));
var route = new TemplateRoute(
mockTarget.Object,
template,
defaults: null,
constraints: null,
dataTokens: null,
inlineConstraintResolver: _inlineConstraintResolver);
Assert.NotEmpty(route.Constraints);
Assert.IsType<RegexInlineRouteConstraint>(route.Constraints["ssn"]);
// Act
await route.RouteAsync(context);
// Assert
Assert.True(context.IsHandled);
Assert.True(routeValues.ContainsKey("ssn"));
Assert.Equal("123-456-7890", routeValues["ssn"]);
Assert.True(context.RouteData.Values.ContainsKey("ssn"));
Assert.Equal("123-456-7890", context.RouteData.Values["ssn"]);
}
[Fact] [Fact]
public async Task RouteAsync_InlineConstraint_OptionalParameter_NotPresent() public async Task RouteAsync_InlineConstraint_OptionalParameter_NotPresent()
{ {
@ -1782,7 +1847,10 @@ namespace Microsoft.AspNet.Routing.Template
resolverMock.Setup(o => o.ResolveConstraint("int")).Returns(new IntRouteConstraint()); resolverMock.Setup(o => o.ResolveConstraint("int")).Returns(new IntRouteConstraint());
resolverMock.Setup(o => o.ResolveConstraint("range(1,20)")).Returns(new RangeRouteConstraint(1, 20)); resolverMock.Setup(o => o.ResolveConstraint("range(1,20)")).Returns(new RangeRouteConstraint(1, 20));
resolverMock.Setup(o => o.ResolveConstraint("alpha")).Returns(new AlphaRouteConstraint()); resolverMock.Setup(o => o.ResolveConstraint("alpha")).Returns(new AlphaRouteConstraint());
resolverMock.Setup(o => o.ResolveConstraint(@"regex(^\d{3}-\d{3}-\d{4}$)")).Returns(
new RegexInlineRouteConstraint(@"^\d{3}-\d{3}-\d{4}$"));
resolverMock.Setup(o => o.ResolveConstraint(@"regex(^\d{1,2}\/\d{1,2}\/\d{4}$)")).Returns(
new RegexInlineRouteConstraint(@"^\d{1,2}\/\d{1,2}\/\d{4}$"));
return resolverMock.Object; return resolverMock.Object;
} }

View File

@ -34,6 +34,25 @@ namespace Microsoft.AspNet.Routing.Tests
Assert.Equal("12", defaults["id"]); Assert.Equal("12", defaults["id"]);
} }
[Theory]
[InlineData(@"{controller}/{action}/{p1:regex(([}}])\w+)=}}asd}", "}asd")]
[InlineData(@"{p1:regex(^\d{{1,2}}\/\d{{1,2}}\/\d{{4}}$)=12/12/1234}", @"12/12/1234")]
public void InlineDefaultValueSpecified_WithSpecialCharacters(string template, string value)
{
// Arrange & Act
var routeBuilder = CreateRouteBuilder();
// Act
routeBuilder.MapRoute("mockName",
template,
defaults: null,
constraints: null);
// Assert
var defaults = ((Template.TemplateRoute)routeBuilder.Routes[0]).Defaults;
Assert.Equal(value, defaults["p1"]);
}
[Fact] [Fact]
public void ExplicitDefaultValueSpecified_WithInlineDefaultValue_Throws() public void ExplicitDefaultValueSpecified_WithInlineDefaultValue_Throws()
{ {