diff --git a/src/Microsoft.AspNet.Routing/Properties/Resources.Designer.cs b/src/Microsoft.AspNet.Routing/Properties/Resources.Designer.cs index e9cca0d601..f1a4eaf192 100644 --- a/src/Microsoft.AspNet.Routing/Properties/Resources.Designer.cs +++ b/src/Microsoft.AspNet.Routing/Properties/Resources.Designer.cs @@ -203,7 +203,7 @@ namespace Microsoft.AspNet.Routing } /// - /// 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 (.). /// internal static string TemplateRoute_CanHaveOnlyLastParameterOptional_IfFollowingOptionalSeperator { @@ -211,7 +211,7 @@ namespace Microsoft.AspNet.Routing } /// - /// 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 (.). /// internal static string FormatTemplateRoute_CanHaveOnlyLastParameterOptional_IfFollowingOptionalSeperator() { @@ -347,7 +347,7 @@ namespace Microsoft.AspNet.Routing } /// - /// 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}'. /// internal static string RouteConstraintBuilder_ValidationMustBeStringOrCustomConstraint { @@ -355,7 +355,7 @@ namespace Microsoft.AspNet.Routing } /// - /// 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}'. /// internal static string FormatRouteConstraintBuilder_ValidationMustBeStringOrCustomConstraint(object p0, object p1, object p2, object p3) { @@ -363,7 +363,7 @@ namespace Microsoft.AspNet.Routing } /// - /// 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}'. /// internal static string RouteConstraintBuilder_CouldNotResolveConstraint { @@ -371,13 +371,29 @@ namespace Microsoft.AspNet.Routing } /// - /// 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}'. /// 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); } + /// + /// In a route parameter, '{' and '}' must be escaped with '{{' and '}}' + /// + internal static string TemplateRoute_UnescapedBrace + { + get { return GetString("TemplateRoute_UnescapedBrace"); } + } + + /// + /// In a route parameter, '{' and '}' must be escaped with '{{' and '}}' + /// + internal static string FormatTemplateRoute_UnescapedBrace() + { + return GetString("TemplateRoute_UnescapedBrace"); + } + private static string GetString(string name, params string[] formatterNames) { var value = _resourceManager.GetString(name); diff --git a/src/Microsoft.AspNet.Routing/Resources.resx b/src/Microsoft.AspNet.Routing/Resources.resx index 4b34eea2d3..072dcf21b2 100644 --- a/src/Microsoft.AspNet.Routing/Resources.resx +++ b/src/Microsoft.AspNet.Routing/Resources.resx @@ -186,4 +186,7 @@ The constraint entry '{0}' - '{1}' on the route '{2}' could not be resolved by the constraint resolver of type '{3}'. + + In a route parameter, '{' and '}' must be escaped with '{{' and '}}' + \ No newline at end of file diff --git a/src/Microsoft.AspNet.Routing/Template/TemplateParser.cs b/src/Microsoft.AspNet.Routing/Template/TemplateParser.cs index 03664f25f5..2c4958b046 100644 --- a/src/Microsoft.AspNet.Routing/Template/TemplateParser.cs +++ b/src/Microsoft.AspNet.Routing/Template/TemplateParser.cs @@ -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)); diff --git a/test/Microsoft.AspNet.Routing.Tests/Constraints/RegexRouteConstraintTests.cs b/test/Microsoft.AspNet.Routing.Tests/Constraints/RegexRouteConstraintTests.cs index f761e2d838..90da38cdb0 100644 --- a/test/Microsoft.AspNet.Routing.Tests/Constraints/RegexRouteConstraintTests.cs +++ b/test/Microsoft.AspNet.Routing.Tests/Constraints/RegexRouteConstraintTests.cs @@ -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) diff --git a/test/Microsoft.AspNet.Routing.Tests/DefaultInlineConstraintResolverTest.cs b/test/Microsoft.AspNet.Routing.Tests/DefaultInlineConstraintResolverTest.cs index 3484f10721..c09f45deab 100644 --- a/test/Microsoft.AspNet.Routing.Tests/DefaultInlineConstraintResolverTest.cs +++ b/test/Microsoft.AspNet.Routing.Tests/DefaultInlineConstraintResolverTest.cs @@ -49,7 +49,7 @@ namespace Microsoft.AspNet.Routing.Tests // Arrange, Act & Assert var ex = Assert.Throws( () => _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(constraint); } + [Fact] + public void ResolveConstraint_RegexInlineConstraint_WithCurlyBraces_Balanced() + { + // Arrange & Act + var constraint = _constraintResolver.ResolveConstraint( + @"regex(\\b(?\\d{1,2})/(?\\d{1,2})/(?\\d{2,4})\\b)"); + + // Assert + Assert.IsType(constraint); + } + [Fact] public void ResolveConstraint_BoolConstraint() { @@ -267,7 +278,7 @@ namespace Microsoft.AspNet.Routing.Tests // Act & Assert var ex = Assert.Throws(() => 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() { diff --git a/test/Microsoft.AspNet.Routing.Tests/InlineRouteParameterParserTests.cs b/test/Microsoft.AspNet.Routing.Tests/InlineRouteParameterParserTests.cs index 5d2346727c..c33278d4c1 100644 --- a/test/Microsoft.AspNet.Routing.Tests/InlineRouteParameterParserTests.cs +++ b/test/Microsoft.AspNet.Routing.Tests/InlineRouteParameterParserTests.cs @@ -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("?", "")] diff --git a/test/Microsoft.AspNet.Routing.Tests/Template/TemplateMatcherTests.cs b/test/Microsoft.AspNet.Routing.Tests/Template/TemplateMatcherTests.cs index 840801bb7d..047f4a44f8 100644 --- a/test/Microsoft.AspNet.Routing.Tests/Template/TemplateMatcherTests.cs +++ b/test/Microsoft.AspNet.Routing.Tests/Template/TemplateMatcherTests.cs @@ -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() { diff --git a/test/Microsoft.AspNet.Routing.Tests/Template/TemplateParserTests.cs b/test/Microsoft.AspNet.Routing.Tests/Template/TemplateParserTests.cs index 181a586b6f..99da06ac17 100644 --- a/test/Microsoft.AspNet.Routing.Tests/Template/TemplateParserTests.cs +++ b/test/Microsoft.AspNet.Routing.Tests/Template/TemplateParserTests.cs @@ -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(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()); + 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 { c })); + expected.Parameters.Add(expected.Segments[0].Parts[0]); + + // Act + var actual = TemplateParser.Parse(template); + + // Assert + Assert.Equal(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( + () => 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( + () => 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( () => 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( () => 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( () => 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( () => 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( () => 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( () => 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( () => 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( () => 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( () => 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( () => 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( () => 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( + c => string.Equals(c.Constraint, xconstraint.Constraint))) + { + return false; + } + } return true; } diff --git a/test/Microsoft.AspNet.Routing.Tests/Template/TemplateRouteTest.cs b/test/Microsoft.AspNet.Routing.Tests/Template/TemplateRouteTest.cs index 4bbdcf7990..6309dcab39 100644 --- a/test/Microsoft.AspNet.Routing.Tests/Template/TemplateRouteTest.cs +++ b/test/Microsoft.AspNet.Routing.Tests/Template/TemplateRouteTest.cs @@ -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(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( + () => 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 routeValues = null; + var mockTarget = new Mock(MockBehavior.Strict); + mockTarget + .Setup(s => s.RouteAsync(It.IsAny())) + .Callback(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(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; } diff --git a/test/Microsoft.AspNet.Routing.Tests/TemplateParserDefaultValuesTests.cs b/test/Microsoft.AspNet.Routing.Tests/TemplateParserDefaultValuesTests.cs index ac3dbea2e9..933e9001d6 100644 --- a/test/Microsoft.AspNet.Routing.Tests/TemplateParserDefaultValuesTests.cs +++ b/test/Microsoft.AspNet.Routing.Tests/TemplateParserDefaultValuesTests.cs @@ -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() {