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>
/// 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>
internal static string TemplateRoute_CanHaveOnlyLastParameterOptional_IfFollowingOptionalSeperator
{
@ -211,7 +211,7 @@ namespace Microsoft.AspNet.Routing
}
/// <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>
internal static string FormatTemplateRoute_CanHaveOnlyLastParameterOptional_IfFollowingOptionalSeperator()
{
@ -347,7 +347,7 @@ namespace Microsoft.AspNet.Routing
}
/// <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>
internal static string RouteConstraintBuilder_ValidationMustBeStringOrCustomConstraint
{
@ -355,7 +355,7 @@ namespace Microsoft.AspNet.Routing
}
/// <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>
internal static string FormatRouteConstraintBuilder_ValidationMustBeStringOrCustomConstraint(object p0, object p1, object p2, object p3)
{
@ -363,7 +363,7 @@ namespace Microsoft.AspNet.Routing
}
/// <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>
internal static string RouteConstraintBuilder_CouldNotResolveConstraint
{
@ -371,13 +371,29 @@ namespace Microsoft.AspNet.Routing
}
/// <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>
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);
}
/// <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)
{
var value = _resourceManager.GetString(name);

View File

@ -186,4 +186,7 @@
<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>
</data>
<data name="TemplateRoute_UnescapedBrace" xml:space="preserve">
<value>In a route parameter, '{' and '}' must be escaped with '{{' and '}}'</value>
</data>
</root>

View File

@ -5,6 +5,7 @@ using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.Text.RegularExpressions;
namespace Microsoft.AspNet.Routing.Template
{
@ -134,30 +135,41 @@ namespace Microsoft.AspNet.Routing.Template
while (true)
{
if (context.Current == Separator)
if (context.Current == OpenBrace)
{
// This is a dangling open-brace, which is not allowed
context.Error = Resources.TemplateRoute_MismatchedParameter;
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.
// 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
// Example: "{p1:regex(^\d{"
context.Error = Resources.TemplateRoute_MismatchedParameter;
return false;
}
}
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())
{
// 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();
break;
}
if (context.Current == CloseBrace)
{
// This is an 'escaped' brace in a parameter name, which is not allowed.
// We'll just accept it for now and let the validation code for the name find it.
// This is an 'escaped' brace in a parameter name
}
else
{
@ -176,10 +188,11 @@ namespace Microsoft.AspNet.Routing.Template
}
var rawParameter = context.Capture();
var decoded = rawParameter.Replace("}}", "}").Replace("{{", "{");
// At this point, we need to parse the raw name for inline constraint,
// default values and optional parameters.
var templatePart = InlineRouteParameterParser.ParseRouteParameter(rawParameter);
var templatePart = InlineRouteParameterParser.ParseRouteParameter(decoded);
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))
{
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 special 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,
string constraintValue,
bool shouldMatch)

View File

@ -49,7 +49,7 @@ namespace Microsoft.AspNet.Routing.Tests
// Arrange, Act & Assert
var ex = Assert.Throws<InvalidOperationException>(
() => _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.",
ex.Message);
}
@ -74,6 +74,17 @@ namespace Microsoft.AspNet.Routing.Tests
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]
public void ResolveConstraint_BoolConstraint()
{
@ -267,7 +278,7 @@ namespace Microsoft.AspNet.Routing.Tests
// Act & Assert
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.",
ex.Message);
}
@ -287,6 +298,21 @@ namespace Microsoft.AspNet.Routing.Tests
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]
public void ResolveConstraint_NoMatchingConstructor_Throws()
{

View File

@ -256,6 +256,36 @@ namespace Microsoft.AspNet.Routing.Tests
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]
[InlineData("", "")]
[InlineData("?", "")]

View File

@ -102,7 +102,26 @@ namespace Microsoft.AspNet.Routing.Template.Tests
}
[Theory]
[InlineData("moo/{p1}.{p2?}", "moo/foo.bar", "foo", "bar")]
[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]
[InlineData("moo/{p1}.{p2?}", "moo/foo.bar", "foo", "bar")]
[InlineData("moo/{p1?}", "moo/foo", "foo", null)]
[InlineData("moo/{p1?}", "moo", null, null)]
[InlineData("moo/{p1}.{p2?}", "moo/foo", "foo", null)]
@ -116,9 +135,9 @@ namespace Microsoft.AspNet.Routing.Template.Tests
[InlineData("moo/{p1}.{p2?}", "moo/....", "..", ".")]
[InlineData("moo/{p1}.{p2?}", "moo/.bar", ".bar", null)]
public void MatchRoute_OptionalParameter_FollowedByPeriod_Valid(
string template,
string path,
string p1,
string template,
string path,
string p1,
string p2)
{
// Arrange
@ -143,13 +162,14 @@ namespace Microsoft.AspNet.Routing.Template.Tests
[InlineData("moo/{p1}.{p2}.{p3?}", "moo/foo.moo", "foo", "moo", null)]
[InlineData("moo/{p1}.{p2}.{p3}.{p4?}", "moo/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/baz", "foo", "bar", "baz")]
public void MatchRoute_OptionalParameter_FollowedByPeriod_3Parameters_Valid(
string template,
string path,
string p1,
string p2,
string template,
string path,
string p1,
string p2,
string p3)
{
// Arrange
@ -159,9 +179,8 @@ namespace Microsoft.AspNet.Routing.Template.Tests
var rd = matcher.Match(path);
// Assert
Assert.Equal(p1, rd["p1"]);
if (p2 != null)
{
Assert.Equal(p2, rd["p2"]);
@ -173,9 +192,9 @@ namespace Microsoft.AspNet.Routing.Template.Tests
}
}
[Theory]
[Theory]
[InlineData("moo/{p1}.{p2?}", "moo/foo.")]
[InlineData("moo/{p1}.{p2?}", "moo/.")]
[InlineData("moo/{p1}.{p2?}", "moo/.")]
[InlineData("moo/{p1}.{p2}", "foo.")]
[InlineData("moo/{p1}.{p2}", "foo")]
[InlineData("moo/{p1}.{p2}.{p3?}", "moo/foo.moo.")]
@ -183,7 +202,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests
[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("{p1}.{p2?}/{p3}", "foo./bar")]
[InlineData("moo/.{p2?}", "moo/.")]
[InlineData("{p1}.{p2}/{p3}", ".foo/bar")]
public void MatchRoute_OptionalParameter_FollowedByPeriod_Invalid(string template, string path)
@ -280,7 +299,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests
// Assert
Assert.Null(rd);
}
[Fact]
public void NoMatchLongerUrl()
{

View File

@ -4,6 +4,7 @@
#if ASPNET50
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNet.Testing;
using Microsoft.Framework.DependencyInjection;
using Microsoft.Framework.DependencyInjection.Fallback;
@ -417,7 +418,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests
defaultValue: null,
inlineConstraints: null));
expected.Segments.Add(new TemplateSegment());
expected.Segments.Add(new TemplateSegment());
expected.Segments[1].Parts.Add(TemplatePart.CreateParameter("p2",
false,
true,
@ -432,7 +433,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests
expected.Parameters.Add(expected.Segments[0].Parts[0]);
expected.Parameters.Add(expected.Segments[1].Parts[0]);
expected.Parameters.Add(expected.Segments[1].Parts[2]);
// Act
var actual = TemplateParser.Parse(template);
@ -462,7 +463,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests
null,
null));
expected.Parameters.Add(expected.Segments[0].Parts[0]);
expected.Parameters.Add(expected.Segments[1].Parts[1]);
expected.Parameters.Add(expected.Segments[1].Parts[1]);
// Act
var actual = TemplateParser.Parse(template);
@ -471,7 +472,62 @@ namespace Microsoft.AspNet.Routing.Template.Tests
Assert.Equal<RouteTemplate>(expected, actual, new TemplateEqualityComparer());
}
[Theory]
[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]
[InlineData("{p1?}.{p2?}")]
[InlineData("{p1}.{p2?}.{p3}")]
[InlineData("{p1?}.{p2}/{p3}")]
@ -485,18 +541,18 @@ namespace Microsoft.AspNet.Routing.Template.Tests
// Act and Assert
ExceptionAssert.Throws<ArgumentException>(
() => TemplateParser.Parse(template),
"In a path segment that contains more than one section, such as a literal section or a 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 (.)." + Environment.NewLine +
"Parameter name: routeTemplate");
}
[Fact]
public void InvalidTemplate_WithRepeatedParameter()
{
var ex = ExceptionAssert.Throws<ArgumentException>(
() => TemplateParser.Parse("{Controller}.mvc/{id}/{controller}"),
"The route parameter name 'controller' appears more than one time in the route template." +
"The route parameter name 'controller' appears more than one time in the route template." +
Environment.NewLine + "Parameter name: routeTemplate");
}
@ -521,7 +577,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests
{
ExceptionAssert.Throws<ArgumentException>(
() => TemplateParser.Parse("123{a}abc{*moo}"),
"A path segment that contains more than one section, such as a literal section or a parameter, " +
"A path segment that contains more than one section, such as a literal section or a parameter, " +
"cannot contain a catch-all parameter." + Environment.NewLine +
"Parameter name: routeTemplate");
}
@ -531,7 +587,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests
{
ExceptionAssert.Throws<ArgumentException>(
() => TemplateParser.Parse("{*p1}/{*p2}"),
"A catch-all parameter can only appear as the last segment of the route template." +
"A catch-all parameter can only appear as the last segment of the route template." +
Environment.NewLine +
"Parameter name: routeTemplate");
}
@ -565,7 +621,13 @@ namespace Microsoft.AspNet.Routing.Template.Tests
[InlineData("{*a*:int}", "a*")]
[InlineData("{*a*=5}", "a*")]
[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
var expectedMessage = "The route parameter name '" + parameterName + "' is invalid. Route parameter " +
@ -604,7 +666,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests
{
ExceptionAssert.Throws<ArgumentException>(
() => TemplateParser.Parse("{aaa}/{AAA}"),
"The route parameter name 'AAA' appears more than one time in the route template." +
"The route parameter name 'AAA' appears more than one time in the route template." +
Environment.NewLine +
"Parameter name: routeTemplate");
}
@ -614,7 +676,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests
{
ExceptionAssert.Throws<ArgumentException>(
() => TemplateParser.Parse("{aaa}/{*AAA}"),
"The route parameter name 'AAA' appears more than one time in the route template." +
"The route parameter name 'AAA' appears more than one time in the route template." +
Environment.NewLine +
"Parameter name: routeTemplate");
}
@ -624,7 +686,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests
{
ExceptionAssert.Throws<ArgumentException>(
() => TemplateParser.Parse("{a}/{aa}a}/{z}"),
"There is an incomplete parameter in the route template. Check that each '{' character has a " +
"There is an incomplete parameter in the route template. Check that each '{' character has a " +
"matching '}' character." + Environment.NewLine +
"Parameter name: routeTemplate");
}
@ -634,10 +696,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests
{
ExceptionAssert.Throws<ArgumentException>(
() => TemplateParser.Parse("{a}/{a{aa}/{z}"),
"The route parameter name 'a{aa' is invalid. Route parameter names must be non-empty and cannot" +
" 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 +
"In a route parameter, '{' and '}' must be escaped with '{{' and '}}'" + Environment.NewLine +
"Parameter name: routeTemplate");
}
@ -680,7 +739,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests
{
ExceptionAssert.Throws<ArgumentException>(
() => TemplateParser.Parse("foo/{p1}/{*p2}/{p3}"),
"A catch-all parameter can only appear as the last segment of the route template." +
"A catch-all parameter can only appear as the last segment of the route template." +
Environment.NewLine +
"Parameter name: routeTemplate");
}
@ -718,7 +777,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests
{
ExceptionAssert.Throws<ArgumentException>(
() => TemplateParser.Parse("foor?bar"),
"The literal section 'foor?bar' is invalid. Literal sections cannot contain the '?' character." +
"The literal section 'foor?bar' is invalid. Literal sections cannot contain the '?' character." +
Environment.NewLine +
"Parameter name: routeTemplate");
}
@ -741,7 +800,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests
ExceptionAssert.Throws<ArgumentException>(
() => TemplateParser.Parse("{foorb?}-bar-{z}"),
"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 " +
"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 (.)." + Environment.NewLine +
"Parameter name: routeTemplate");
}
@ -814,11 +873,31 @@ namespace Microsoft.AspNet.Routing.Template.Tests
x.IsCatchAll != y.IsCatchAll ||
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) ||
(x.InlineConstraints == null && y.InlineConstraints != null) ||
(x.InlineConstraints != null && y.InlineConstraints == null))
{
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;
}

View File

@ -12,6 +12,7 @@ using Microsoft.AspNet.Routing.Constraints;
using Microsoft.AspNet.Routing.Logging;
using Microsoft.AspNet.Testing;
using Microsoft.Framework.Logging;
using Microsoft.AspNet.WebUtilities;
using Moq;
using Xunit;
@ -37,6 +38,28 @@ namespace Microsoft.AspNet.Routing.Template
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]
public async Task RouteAsync_MatchSuccess_LogsCorrectValues()
{
@ -497,7 +520,7 @@ namespace Microsoft.AspNet.Routing.Template
// Act
await route.RouteAsync(context);
// Assert
// Assert
Assert.True(context.IsHandled);
Assert.True(routeValues.ContainsKey("id"));
Assert.Equal("5", routeValues["id"]);
@ -506,6 +529,48 @@ namespace Microsoft.AspNet.Routing.Template
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]
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("range(1,20)")).Returns(new RangeRouteConstraint(1, 20));
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;
}

View File

@ -34,6 +34,25 @@ namespace Microsoft.AspNet.Routing.Tests
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]
public void ExplicitDefaultValueSpecified_WithInlineDefaultValue_Throws()
{