From 731767837ab45bf0db26b4679c734a891136cad1 Mon Sep 17 00:00:00 2001 From: Kiran Challa Date: Wed, 15 Aug 2018 10:35:04 -0700 Subject: [PATCH] Introducing new **catchAll parameter to allow generating links with unencoded values --- .../UseEndpointRoutingStartup.cs | 58 ++++++-- .../Internal/UriBuildingContext.cs | 50 ++++++- .../Patterns/RouteParameterParser.cs | 19 ++- .../Patterns/RoutePatternFactory.cs | 24 +++- .../Patterns/RoutePatternParameterPart.cs | 16 +++ .../Template/TemplateBinder.cs | 5 +- .../EndpointRoutingSampleTest.cs | 48 +++++++ .../DefaultLinkGeneratorTest.cs | 70 ++++++++++ .../Internal/UriBuildingContextTest.cs | 69 ++++++++++ .../InlineRouteParameterParserTest.cs | 130 ++++++++++++++++++ .../Patterns/RoutePatternParserTest.cs | 1 - .../Template/TemplateParserTests.cs | 1 - 12 files changed, 465 insertions(+), 26 deletions(-) create mode 100644 test/Microsoft.AspNetCore.Routing.Tests/Internal/UriBuildingContextTest.cs diff --git a/samples/RoutingSample.Web/UseEndpointRoutingStartup.cs b/samples/RoutingSample.Web/UseEndpointRoutingStartup.cs index 7736561627..fb9fb36161 100644 --- a/samples/RoutingSample.Web/UseEndpointRoutingStartup.cs +++ b/samples/RoutingSample.Web/UseEndpointRoutingStartup.cs @@ -81,20 +81,54 @@ namespace RoutingSample.Web EndpointMetadataCollection.Empty, "withoptionalconstraints"), new MatcherEndpoint((next) => (httpContext) => - { - using (var writer = new StreamWriter(httpContext.Response.Body, Encoding.UTF8, 1024, leaveOpen: true)) { - var graphWriter = httpContext.RequestServices.GetRequiredService(); - var dataSource = httpContext.RequestServices.GetRequiredService(); - graphWriter.Write(dataSource, writer); - } + using (var writer = new StreamWriter(httpContext.Response.Body, Encoding.UTF8, 1024, leaveOpen: true)) + { + var graphWriter = httpContext.RequestServices.GetRequiredService(); + var dataSource = httpContext.RequestServices.GetRequiredService(); + graphWriter.Write(dataSource, writer); + } - return Task.CompletedTask; - }, - RoutePatternFactory.Parse("/graph"), - 0, - new EndpointMetadataCollection(new HttpMethodMetadata(new[]{ "GET", })), - "DFA Graph"), + return Task.CompletedTask; + }, + RoutePatternFactory.Parse("/graph"), + 0, + new EndpointMetadataCollection(new HttpMethodMetadata(new[]{ "GET", })), + "DFA Graph"), + new MatcherEndpoint((next) => (httpContext) => + { + var linkGenerator = httpContext.RequestServices.GetRequiredService(); + + var response = httpContext.Response; + response.StatusCode = 200; + response.ContentType = "text/plain"; + return response.WriteAsync( + "Link: " + linkGenerator.GetLink(httpContext, "WithSingleAsteriskCatchAll", new { })); + }, + RoutePatternFactory.Parse("/WithSingleAsteriskCatchAll/{*path}"), + 0, + new EndpointMetadataCollection( + new RouteValuesAddressMetadata( + name: "WithSingleAsteriskCatchAll", + requiredValues: new RouteValueDictionary())), + "WithSingleAsteriskCatchAll"), + new MatcherEndpoint((next) => (httpContext) => + { + var linkGenerator = httpContext.RequestServices.GetRequiredService(); + + var response = httpContext.Response; + response.StatusCode = 200; + response.ContentType = "text/plain"; + return response.WriteAsync( + "Link: " + linkGenerator.GetLink(httpContext, "WithDoubleAsteriskCatchAll", new { })); + }, + RoutePatternFactory.Parse("/WithDoubleAsteriskCatchAll/{**path}"), + 0, + new EndpointMetadataCollection( + new RouteValuesAddressMetadata( + name: "WithDoubleAsteriskCatchAll", + requiredValues: new RouteValueDictionary())), + "WithDoubleAsteriskCatchAll"), }); services.TryAddEnumerable(ServiceDescriptor.Singleton(endpointDataSource)); diff --git a/src/Microsoft.AspNetCore.Routing/Internal/UriBuildingContext.cs b/src/Microsoft.AspNetCore.Routing/Internal/UriBuildingContext.cs index 3b78fe8c78..3b7c3b743d 100644 --- a/src/Microsoft.AspNetCore.Routing/Internal/UriBuildingContext.cs +++ b/src/Microsoft.AspNetCore.Routing/Internal/UriBuildingContext.cs @@ -43,6 +43,11 @@ namespace Microsoft.AspNetCore.Routing.Internal public TextWriter Writer { get; } public bool Accept(string value) + { + return Accept(value, encodeSlashes: true); + } + + public bool Accept(string value, bool encodeSlashes) { if (string.IsNullOrEmpty(value)) { @@ -67,7 +72,7 @@ namespace Microsoft.AspNetCore.Routing.Internal { if (_buffer[i].RequiresEncoding) { - _urlEncoder.Encode(Writer, _buffer[i].Value); + EncodeValue(_buffer[i].Value); } else { @@ -88,16 +93,17 @@ namespace Microsoft.AspNetCore.Routing.Internal UriState = SegmentState.Inside; _lastValueOffset = _uri.Length; + // Allow the first segment to have a leading slash. // This prevents the leading slash from PathString segments from being encoded. if (_uri.Length == 0 && value.Length > 0 && value[0] == '/') { _uri.Append("/"); - _urlEncoder.Encode(Writer, value, 1, value.Length - 1); + EncodeValue(value, 1, value.Length - 1, encodeSlashes); } else { - _urlEncoder.Encode(Writer, value); + EncodeValue(value, encodeSlashes); } return true; @@ -197,6 +203,44 @@ namespace Microsoft.AspNetCore.Routing.Internal return _uri.ToString(); } + private void EncodeValue(string value) + { + EncodeValue(value, encodeSlashes: true); + } + + private void EncodeValue(string value, bool encodeSlashes) + { + EncodeValue(value, start: 0, characterCount: value.Length, encodeSlashes); + } + + // For testing + internal void EncodeValue(string value, int start, int characterCount, bool encodeSlashes) + { + // Just encode everything if its ok to encode slashes + if (encodeSlashes) + { + _urlEncoder.Encode(Writer, value, start, characterCount); + } + else + { + int end; + int length = start + characterCount; + while ((end = value.IndexOf('/', start, characterCount)) >= 0) + { + _urlEncoder.Encode(Writer, value, start, end - start); + _uri.Append("/"); + + start = end + 1; + characterCount = length - start; + } + + if (end < 0 && characterCount >= 0) + { + _urlEncoder.Encode(Writer, value, start, length - start); + } + } + } + private string DebuggerToString() { return string.Format("{{Accepted: '{0}' Buffered: '{1}'}}", _uri, string.Join("", _buffer)); diff --git a/src/Microsoft.AspNetCore.Routing/Patterns/RouteParameterParser.cs b/src/Microsoft.AspNetCore.Routing/Patterns/RouteParameterParser.cs index 885f77481c..8c0132bc06 100644 --- a/src/Microsoft.AspNetCore.Routing/Patterns/RouteParameterParser.cs +++ b/src/Microsoft.AspNetCore.Routing/Patterns/RouteParameterParser.cs @@ -27,9 +27,17 @@ namespace Microsoft.AspNetCore.Routing.Patterns var startIndex = 0; var endIndex = parameter.Length - 1; + var encodeSlashes = true; var parameterKind = RoutePatternParameterKind.Standard; - if (parameter[0] == '*') + + if (parameter.StartsWith("**", StringComparison.Ordinal)) + { + encodeSlashes = false; + parameterKind = RoutePatternParameterKind.CatchAll; + startIndex += 2; + } + else if (parameter[0] == '*') { parameterKind = RoutePatternParameterKind.CatchAll; startIndex++; @@ -79,7 +87,12 @@ namespace Microsoft.AspNetCore.Routing.Patterns defaultValue = parameter.Substring(currentIndex + 1, endIndex - currentIndex); } - return new RoutePatternParameterPart(parameterName, defaultValue, parameterKind, parseResults.Constraints.ToArray()); + return new RoutePatternParameterPart( + parameterName, + defaultValue, + parameterKind, + parseResults.Constraints.ToArray(), + encodeSlashes); } private static ConstraintParseResults ParseConstraints( @@ -237,7 +250,7 @@ namespace Microsoft.AspNetCore.Routing.Patterns public readonly int CurrentIndex; public readonly IReadOnlyList Constraints; - + public ConstraintParseResults(int currentIndex, IReadOnlyList constraints) { CurrentIndex = currentIndex; diff --git a/src/Microsoft.AspNetCore.Routing/Patterns/RoutePatternFactory.cs b/src/Microsoft.AspNetCore.Routing/Patterns/RoutePatternFactory.cs index 54a34c9d4c..44dd675543 100644 --- a/src/Microsoft.AspNetCore.Routing/Patterns/RoutePatternFactory.cs +++ b/src/Microsoft.AspNetCore.Routing/Patterns/RoutePatternFactory.cs @@ -284,7 +284,7 @@ namespace Microsoft.AspNetCore.Routing.Patterns { var segment = VisitSegment(updatedSegments[i]); updatedSegments[i] = segment; - + for (var j = 0; j < segment.Parts.Count; j++) { if (segment.Parts[j] is RoutePatternParameterPart parameter) @@ -339,7 +339,7 @@ namespace Microsoft.AspNetCore.Routing.Patterns @default = newDefault; } - + if (parameter.Default != null) { updatedDefaults.Add(parameter.Name, parameter.Default); @@ -361,7 +361,8 @@ namespace Microsoft.AspNetCore.Routing.Patterns parameter.Name, @default, parameter.ParameterKind, - (IEnumerable)parameterConstraints ?? Array.Empty()); + (IEnumerable)parameterConstraints ?? Array.Empty(), + parameter.EncodeSlashes); } } @@ -624,7 +625,22 @@ namespace Microsoft.AspNetCore.Routing.Patterns RoutePatternParameterKind parameterKind, IEnumerable constraints) { - return new RoutePatternParameterPart(parameterName, @default, parameterKind, constraints.ToArray()); + return ParameterPartCore(parameterName, @default, parameterKind, constraints, encodeSlashes: true); + } + + private static RoutePatternParameterPart ParameterPartCore( + string parameterName, + object @default, + RoutePatternParameterKind parameterKind, + IEnumerable constraints, + bool encodeSlashes) + { + return new RoutePatternParameterPart( + parameterName, + @default, + parameterKind, + constraints.ToArray(), + encodeSlashes); } /// diff --git a/src/Microsoft.AspNetCore.Routing/Patterns/RoutePatternParameterPart.cs b/src/Microsoft.AspNetCore.Routing/Patterns/RoutePatternParameterPart.cs index 04f9056573..df060de9d9 100644 --- a/src/Microsoft.AspNetCore.Routing/Patterns/RoutePatternParameterPart.cs +++ b/src/Microsoft.AspNetCore.Routing/Patterns/RoutePatternParameterPart.cs @@ -19,6 +19,16 @@ namespace Microsoft.AspNetCore.Routing.Patterns object @default, RoutePatternParameterKind parameterKind, RoutePatternConstraintReference[] constraints) + : this(parameterName, @default, parameterKind, constraints, encodeSlashes: true) + { + } + + internal RoutePatternParameterPart( + string parameterName, + object @default, + RoutePatternParameterKind parameterKind, + RoutePatternConstraintReference[] constraints, + bool encodeSlashes) : base(RoutePatternPartKind.Parameter) { // See #475 - this code should have some asserts, but it can't because of the design of RouteParameterParser. @@ -27,6 +37,7 @@ namespace Microsoft.AspNetCore.Routing.Patterns Default = @default; ParameterKind = parameterKind; Constraints = constraints; + EncodeSlashes = encodeSlashes; } /// @@ -34,6 +45,11 @@ namespace Microsoft.AspNetCore.Routing.Patterns /// public IReadOnlyList Constraints { get; } + /// + /// Gets the value indicating if slashes in current parameter's value should be encoded. + /// + public bool EncodeSlashes { get; } + /// /// Gets the default value of this route parameter. May be null. /// diff --git a/src/Microsoft.AspNetCore.Routing/Template/TemplateBinder.cs b/src/Microsoft.AspNetCore.Routing/Template/TemplateBinder.cs index d9df9f30f6..c96f540133 100644 --- a/src/Microsoft.AspNetCore.Routing/Template/TemplateBinder.cs +++ b/src/Microsoft.AspNetCore.Routing/Template/TemplateBinder.cs @@ -7,6 +7,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.Text.Encodings.Web; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing.Internal; using Microsoft.AspNetCore.Routing.Patterns; using Microsoft.Extensions.ObjectPool; @@ -223,12 +224,12 @@ namespace Microsoft.AspNetCore.Routing.Template // If the value is not accepted, it is null or empty value in the // middle of the segment. We accept this if the parameter is an // optional parameter and it is preceded by an optional seperator. - // I this case, we need to remove the optional seperator that we + // In this case, we need to remove the optional seperator that we // have added to the URI // Example: template = {id}.{format?}. parameters: id=5 // In this case after we have generated "5.", we wont find any value // for format, so we remove '.' and generate 5. - if (!context.Accept(converted)) + if (!context.Accept(converted, parameterPart.EncodeSlashes)) { if (j != 0 && parameterPart.IsOptional && (separatorPart = segment.Parts[j - 1] as RoutePatternSeparatorPart) != null) { diff --git a/test/Microsoft.AspNetCore.Routing.FunctionalTests/EndpointRoutingSampleTest.cs b/test/Microsoft.AspNetCore.Routing.FunctionalTests/EndpointRoutingSampleTest.cs index 5f059cae60..6dd2373efe 100644 --- a/test/Microsoft.AspNetCore.Routing.FunctionalTests/EndpointRoutingSampleTest.cs +++ b/test/Microsoft.AspNetCore.Routing.FunctionalTests/EndpointRoutingSampleTest.cs @@ -130,6 +130,54 @@ namespace Microsoft.AspNetCore.Routing.FunctionalTests Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); } + [Theory] + [InlineData("/WithSingleAsteriskCatchAll/a/b/c", "Link: /WithSingleAsteriskCatchAll/a%2Fb%2Fc")] + [InlineData("/WithSingleAsteriskCatchAll/a/b b1/c c1", "Link: /WithSingleAsteriskCatchAll/a%2Fb%20b1%2Fc%20c1")] + public async Task GeneratesLink_ToEndpointWithSingleAsteriskCatchAllParameter_EncodesValue( + string url, + string expected) + { + // Arrange & Act + var response = await _client.GetAsync(url); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.NotNull(response.Content); + var actualContent = await response.Content.ReadAsStringAsync(); + Assert.Equal(expected, actualContent); + } + + [Theory] + [InlineData("/WithDoubleAsteriskCatchAll/a/b/c", "Link: /WithDoubleAsteriskCatchAll/a/b/c")] + [InlineData("/WithDoubleAsteriskCatchAll/a/b/c/", "Link: /WithDoubleAsteriskCatchAll/a/b/c/")] + [InlineData("/WithDoubleAsteriskCatchAll/a//b/c", "Link: /WithDoubleAsteriskCatchAll/a//b/c")] + public async Task GeneratesLink_ToEndpointWithDoubleAsteriskCatchAllParameter_DoesNotEncodeSlashes( + string url, + string expected) + { + // Arrange & Act + var response = await _client.GetAsync(url); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.NotNull(response.Content); + var actualContent = await response.Content.ReadAsStringAsync(); + Assert.Equal(expected, actualContent); + } + + [Fact] + public async Task GeneratesLink_ToEndpointWithDoubleAsteriskCatchAllParameter_EncodesContentOtherThanSlashes() + { + // Arrange & Act + var response = await _client.GetAsync("/WithDoubleAsteriskCatchAll/a/b b1/c c1"); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.NotNull(response.Content); + var actualContent = await response.Content.ReadAsStringAsync(); + Assert.Equal("Link: /WithDoubleAsteriskCatchAll/a/b%20b1/c%20c1", actualContent); + } + public void Dispose() { _testServer.Dispose(); diff --git a/test/Microsoft.AspNetCore.Routing.Tests/DefaultLinkGeneratorTest.cs b/test/Microsoft.AspNetCore.Routing.Tests/DefaultLinkGeneratorTest.cs index 84e9701cd8..1fe785ca8f 100644 --- a/test/Microsoft.AspNetCore.Routing.Tests/DefaultLinkGeneratorTest.cs +++ b/test/Microsoft.AspNetCore.Routing.Tests/DefaultLinkGeneratorTest.cs @@ -98,6 +98,76 @@ namespace Microsoft.AspNetCore.Routing Assert.Equal("/Home/Index", link); } + [Fact] + public void GetLink_EncodesIntermediate_DefaultValues() + { + // Arrange + var endpoint = EndpointFactory.CreateMatcherEndpoint("{p1}/{p2=a b}/{p3=foo}"); + var linkGenerator = CreateLinkGenerator(endpoint); + + // Act + var link = linkGenerator.GetLink(new { p1 = "Home", p3 = "bar" }); + + // Assert + Assert.Equal("/Home/a%20b/bar", link); + } + + [Theory] + [InlineData("a/b/c", "/Home/Index/a%2Fb%2Fc")] + [InlineData("a/b b1/c c1", "/Home/Index/a%2Fb%20b1%2Fc%20c1")] + public void GetLink_EncodesValue_OfSingleAsteriskCatchAllParameter(string routeValue, string expected) + { + // Arrange + var endpoint = EndpointFactory.CreateMatcherEndpoint("{controller}/{action}/{*path}"); + var linkGenerator = CreateLinkGenerator(endpoint); + var httpContext = CreateHttpContext(ambientValues: new { controller = "Home", action = "Index" }); + + // Act + var link = linkGenerator.GetLink(httpContext, new { path = routeValue }); + + // Assert + Assert.Equal(expected, link); + } + + [Theory] + [InlineData("/", "/Home/Index//")] + [InlineData("a", "/Home/Index/a")] + [InlineData("a/", "/Home/Index/a/")] + [InlineData("a/b", "/Home/Index/a/b")] + [InlineData("a/b/c", "/Home/Index/a/b/c")] + [InlineData("a/b/cc", "/Home/Index/a/b/cc")] + [InlineData("a/b/c/", "/Home/Index/a/b/c/")] + [InlineData("a/b/c//", "/Home/Index/a/b/c//")] + [InlineData("a//b//c", "/Home/Index/a//b//c")] + public void GetLink_DoesNotEncodeSlashes_OfDoubleAsteriskCatchAllParameter(string routeValue, string expected) + { + // Arrange + var endpoint = EndpointFactory.CreateMatcherEndpoint("{controller}/{action}/{**path}"); + var linkGenerator = CreateLinkGenerator(endpoint); + var httpContext = CreateHttpContext(ambientValues: new { controller = "Home", action = "Index" }); + + // Act + var link = linkGenerator.GetLink(httpContext, new { path = routeValue }); + + // Assert + Assert.Equal(expected, link); + } + + [Fact] + public void GetLink_EncodesContentOtherThanSlashes_OfDoubleAsteriskCatchAllParameter() + { + // Arrange + var endpoint = EndpointFactory.CreateMatcherEndpoint("{controller}/{action}/{**path}"); + var linkGenerator = CreateLinkGenerator(endpoint); + var httpContext = CreateHttpContext(ambientValues: new { controller = "Home", action = "Index" }); + + // Act + var link = linkGenerator.GetLink(httpContext, new { path = "a/b b1/c c1" }); + + // Assert + Assert.Equal("/Home/Index/a/b%20b1/c%20c1", link); + } + [Fact] public void GetLink_EncodesValues() { diff --git a/test/Microsoft.AspNetCore.Routing.Tests/Internal/UriBuildingContextTest.cs b/test/Microsoft.AspNetCore.Routing.Tests/Internal/UriBuildingContextTest.cs new file mode 100644 index 0000000000..137520051c --- /dev/null +++ b/test/Microsoft.AspNetCore.Routing.Tests/Internal/UriBuildingContextTest.cs @@ -0,0 +1,69 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.Extensions.WebEncoders.Testing; +using Xunit; + +namespace Microsoft.AspNetCore.Routing.Internal +{ + public class UriBuildingContextTest + { + [Fact] + public void EncodeValue_EncodesEntireValue_WhenEncodeSlashes_IsFalse() + { + // Arrange + var urlTestEncoder = new UrlTestEncoder(); + var value = "a/b b1/c"; + var expected = "/UrlEncode[[a/b b1/c]]"; + var uriBuilldingContext = new UriBuildingContext(urlTestEncoder); + + // Act + uriBuilldingContext.EncodeValue(value, 0, value.Length, encodeSlashes: true); + + // Assert + Assert.Equal(expected, uriBuilldingContext.ToString()); + } + + [Fact] + public void EncodeValue_EncodesOnlySlashes_WhenEncodeSlashes_IsFalse() + { + // Arrange + var urlTestEncoder = new UrlTestEncoder(); + var value = "a/b b1/c"; + var expected = "/UrlEncode[[a]]/UrlEncode[[b b1]]/UrlEncode[[c]]"; + var uriBuilldingContext = new UriBuildingContext(urlTestEncoder); + + // Act + uriBuilldingContext.EncodeValue(value, 0, value.Length, encodeSlashes: false); + + // Assert + Assert.Equal(expected, uriBuilldingContext.ToString()); + } + + [Theory] + [InlineData("a/b b1/c", 0, 2, "/UrlEncode[[a]]/")] + [InlineData("a/b b1/c", 3, 4, "/UrlEncode[[ b1]]/")] + [InlineData("a/b b1/c", 3, 5, "/UrlEncode[[ b1]]/UrlEncode[[c]]")] + [InlineData("a/b b1/c/", 8, 1, "/")] + [InlineData("/", 0, 1, "/")] + [InlineData("/a", 0, 2, "/UrlEncode[[a]]")] + [InlineData("a", 0, 1, "/UrlEncode[[a]]")] + [InlineData("a/", 0, 2, "/UrlEncode[[a]]/")] + public void EncodeValue_EncodesOnlySlashes_WithinSubsegment_WhenEncodeSlashes_IsFalse( + string value, + int startIndex, + int characterCount, + string expected) + { + // Arrange + var urlTestEncoder = new UrlTestEncoder(); + var uriBuilldingContext = new UriBuildingContext(urlTestEncoder); + + // Act + uriBuilldingContext.EncodeValue(value, startIndex, characterCount, encodeSlashes: false); + + // Assert + Assert.Equal(expected, uriBuilldingContext.ToString()); + } + } +} diff --git a/test/Microsoft.AspNetCore.Routing.Tests/Patterns/InlineRouteParameterParserTest.cs b/test/Microsoft.AspNetCore.Routing.Tests/Patterns/InlineRouteParameterParserTest.cs index 87ab942b23..0056b1c534 100644 --- a/test/Microsoft.AspNetCore.Routing.Tests/Patterns/InlineRouteParameterParserTest.cs +++ b/test/Microsoft.AspNetCore.Routing.Tests/Patterns/InlineRouteParameterParserTest.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System.Linq; +using Microsoft.AspNetCore.Routing.Constraints; using Xunit; namespace Microsoft.AspNetCore.Routing.Patterns @@ -919,6 +920,7 @@ namespace Microsoft.AspNetCore.Routing.Patterns [InlineData("", "")] [InlineData("?", "")] [InlineData("*", "")] + [InlineData("**", "")] [InlineData(" ", " ")] [InlineData("\t", "\t")] [InlineData("#!@#$%Q@#@%", "#!@#$%Q@#@%")] @@ -936,6 +938,134 @@ namespace Microsoft.AspNetCore.Routing.Patterns Assert.Null(templatePart.Default); } + [Fact] + public void ParseRouteParameter_WithSingleAsteriskCatchAll_IsParsedCorrectly() + { + // Arrange & Act + var parameterPart = ParseParameter("*path"); + + // Assert + Assert.Equal("path", parameterPart.Name); + Assert.True(parameterPart.IsCatchAll); + Assert.Equal(RoutePatternParameterKind.CatchAll, parameterPart.ParameterKind); + Assert.True(parameterPart.EncodeSlashes); + } + + [Fact] + public void ParseRouteParameter_WithSingleAsteriskCatchAll_AndDefaultValue_IsParsedCorrectly() + { + // Arrange & Act + var parameterPart = ParseParameter("*path=a/b/c"); + + // Assert + Assert.Equal("path", parameterPart.Name); + Assert.True(parameterPart.IsCatchAll); + Assert.NotNull(parameterPart.Default); + Assert.Equal("a/b/c", parameterPart.Default.ToString()); + Assert.Equal(RoutePatternParameterKind.CatchAll, parameterPart.ParameterKind); + Assert.True(parameterPart.EncodeSlashes); + } + + [Fact] + public void ParseRouteParameter_WithSingleAsteriskCatchAll_AndConstraints_IsParsedCorrectly() + { + // Arrange + var constraintContent = "regex(^(/[^/ ]*)+/?$)"; + + // Act + var parameterPart = ParseParameter($"*path:{constraintContent}"); + + // Assert + Assert.Equal("path", parameterPart.Name); + Assert.True(parameterPart.IsCatchAll); + Assert.Equal(RoutePatternParameterKind.CatchAll, parameterPart.ParameterKind); + var constraintReference = Assert.Single(parameterPart.Constraints); + Assert.Equal(constraintContent, constraintReference.Content); + Assert.True(parameterPart.EncodeSlashes); + } + + [Fact] + public void ParseRouteParameter_WithSingleAsteriskCatchAll_AndConstraints_AndDefaultValue_IsParsedCorrectly() + { + // Arrange + var constraintContent = "regex(^(/[^/ ]*)+/?$)"; + + // Act + var parameterPart = ParseParameter($"*path:{constraintContent}=a/b/c"); + + // Assert + Assert.Equal("path", parameterPart.Name); + Assert.True(parameterPart.IsCatchAll); + Assert.Equal(RoutePatternParameterKind.CatchAll, parameterPart.ParameterKind); + var constraintReference = Assert.Single(parameterPart.Constraints); + Assert.Equal(constraintContent, constraintReference.Content); + Assert.NotNull(parameterPart.Default); + Assert.Equal("a/b/c", parameterPart.Default.ToString()); + Assert.True(parameterPart.EncodeSlashes); + } + + [Fact] + public void ParseRouteParameter_WithDoubleAsteriskCatchAll_IsParsedCorrectly() + { + // Arrange & Act + var parameterPart = ParseParameter("**path"); + + // Assert + Assert.Equal("path", parameterPart.Name); + Assert.True(parameterPart.IsCatchAll); + Assert.False(parameterPart.EncodeSlashes); + } + + [Fact] + public void ParseRouteParameter_WithDoubleAsteriskCatchAll_AndDefaultValue_IsParsedCorrectly() + { + // Arrange & Act + var parameterPart = ParseParameter("**path=a/b/c"); + + // Assert + Assert.Equal("path", parameterPart.Name); + Assert.True(parameterPart.IsCatchAll); + Assert.NotNull(parameterPart.Default); + Assert.Equal("a/b/c", parameterPart.Default.ToString()); + Assert.False(parameterPart.EncodeSlashes); + } + + [Fact] + public void ParseRouteParameter_WithDoubleAsteriskCatchAll_AndConstraints_IsParsedCorrectly() + { + // Arrange + var constraintContent = "regex(^(/[^/ ]*)+/?$)"; + + // Act + var parameterPart = ParseParameter($"**path:{constraintContent}"); + + // Assert + Assert.Equal("path", parameterPart.Name); + Assert.True(parameterPart.IsCatchAll); + Assert.False(parameterPart.EncodeSlashes); + var constraintReference = Assert.Single(parameterPart.Constraints); + Assert.Equal(constraintContent, constraintReference.Content); + } + + [Fact] + public void ParseRouteParameter_WithDoubleAsteriskCatchAll_AndConstraints_AndDefaultValue_IsParsedCorrectly() + { + // Arrange + var constraintContent = "regex(^(/[^/ ]*)+/?$)"; + + // Act + var parameterPart = ParseParameter($"**path:{constraintContent}=a/b/c"); + + // Assert + Assert.Equal("path", parameterPart.Name); + Assert.True(parameterPart.IsCatchAll); + Assert.False(parameterPart.EncodeSlashes); + var constraintReference = Assert.Single(parameterPart.Constraints); + Assert.Equal(constraintContent, constraintReference.Content); + Assert.NotNull(parameterPart.Default); + Assert.Equal("a/b/c", parameterPart.Default.ToString()); + } + private RoutePatternParameterPart ParseParameter(string routeParameter) { // See: #475 - these tests don't pass the 'whole' text. diff --git a/test/Microsoft.AspNetCore.Routing.Tests/Patterns/RoutePatternParserTest.cs b/test/Microsoft.AspNetCore.Routing.Tests/Patterns/RoutePatternParserTest.cs index 760544801e..0dfc68d072 100644 --- a/test/Microsoft.AspNetCore.Routing.Tests/Patterns/RoutePatternParserTest.cs +++ b/test/Microsoft.AspNetCore.Routing.Tests/Patterns/RoutePatternParserTest.cs @@ -470,7 +470,6 @@ namespace Microsoft.AspNetCore.Routing.Patterns } [Theory] - [InlineData("{**}", "*")] [InlineData("{a*}", "a*")] [InlineData("{*a*}", "a*")] [InlineData("{*a*:int}", "a*")] diff --git a/test/Microsoft.AspNetCore.Routing.Tests/Template/TemplateParserTests.cs b/test/Microsoft.AspNetCore.Routing.Tests/Template/TemplateParserTests.cs index eabc3ec25e..39f2a1159e 100644 --- a/test/Microsoft.AspNetCore.Routing.Tests/Template/TemplateParserTests.cs +++ b/test/Microsoft.AspNetCore.Routing.Tests/Template/TemplateParserTests.cs @@ -626,7 +626,6 @@ namespace Microsoft.AspNetCore.Routing.Template.Tests } [Theory] - [InlineData("{**}", "*")] [InlineData("{a*}", "a*")] [InlineData("{*a*}", "a*")] [InlineData("{*a*:int}", "a*")]