From 81a17300b228ad6847ad50524d8482c748f53668 Mon Sep 17 00:00:00 2001 From: Mugdha Kulkarni Date: Wed, 11 Feb 2015 14:27:59 -0800 Subject: [PATCH] Issue 140: Support lower case routes --- .../RouteCollection.cs | 37 +++- src/Microsoft.AspNet.Routing/RouteOptions.cs | 5 + .../RouteCollectionTest.cs | 187 +++++++++++++++++- 3 files changed, 214 insertions(+), 15 deletions(-) diff --git a/src/Microsoft.AspNet.Routing/RouteCollection.cs b/src/Microsoft.AspNet.Routing/RouteCollection.cs index c6f3ab1dbb..2a85f426de 100644 --- a/src/Microsoft.AspNet.Routing/RouteCollection.cs +++ b/src/Microsoft.AspNet.Routing/RouteCollection.cs @@ -9,8 +9,9 @@ using Microsoft.AspNet.Http; using Microsoft.AspNet.Routing.Logging; using Microsoft.AspNet.Routing.Logging.Internal; using Microsoft.Framework.DependencyInjection; -using Microsoft.Framework.OptionsModel; using Microsoft.Framework.Logging; +using Microsoft.Framework.OptionsModel; + namespace Microsoft.AspNet.Routing { @@ -154,7 +155,7 @@ namespace Microsoft.AspNet.Routing if (isValidated || useBestEffort) { context.IsBound = isValidated; - return bestPath; + return NormalizeVirtualPath(bestPath); } else { @@ -177,7 +178,7 @@ namespace Microsoft.AspNet.Routing if (context.IsBound) { // This route has validated route values, short circuit. - return path; + return NormalizeVirtualPath(path); } else if (bestPath == null) { @@ -188,7 +189,7 @@ namespace Microsoft.AspNet.Routing if (useBestEffort) { - return bestPath; + return NormalizeVirtualPath(bestPath); } else { @@ -197,7 +198,33 @@ namespace Microsoft.AspNet.Routing } } - private void EnsureLogger(HttpContext context) + private string NormalizeVirtualPath(String url) + { + if (string.IsNullOrEmpty(url)) return url; + + if (_options.LowercaseUrls) + { + var indexOfSeparator = url.IndexOfAny(new char[] { '?', '#' }); + + // No query string, lowercase the url + if (indexOfSeparator == -1) + { + return url.ToLowerInvariant(); + } + else + { + var lowercaseUrl = url.Substring(0, indexOfSeparator).ToLowerInvariant(); + var queryString = url.Substring(indexOfSeparator); + + // queryString will contain the delimiter ? or # as the first character, so it's safe to append. + return lowercaseUrl + queryString; + } + } + + return url; + } + + private void EnsureLogger(HttpContext context) { if (_logger == null) { diff --git a/src/Microsoft.AspNet.Routing/RouteOptions.cs b/src/Microsoft.AspNet.Routing/RouteOptions.cs index a17f762a93..b8785a4cf1 100644 --- a/src/Microsoft.AspNet.Routing/RouteOptions.cs +++ b/src/Microsoft.AspNet.Routing/RouteOptions.cs @@ -9,6 +9,11 @@ namespace Microsoft.AspNet.Routing { public class RouteOptions { + /// + /// Gets or sets a value indicating whether all generated URLs are lower-case. + /// + public bool LowercaseUrls { get; set; } + private IDictionary _constraintTypeMap = GetDefaultConstraintMap(); public IDictionary ConstraintMap diff --git a/test/Microsoft.AspNet.Routing.Tests/RouteCollectionTest.cs b/test/Microsoft.AspNet.Routing.Tests/RouteCollectionTest.cs index 081885a8f5..fc64702a99 100644 --- a/test/Microsoft.AspNet.Routing.Tests/RouteCollectionTest.cs +++ b/test/Microsoft.AspNet.Routing.Tests/RouteCollectionTest.cs @@ -8,6 +8,7 @@ using System.Linq; using System.Threading.Tasks; using Microsoft.AspNet.Http; using Microsoft.AspNet.Routing.Logging; +using Microsoft.AspNet.Routing.Template; using Microsoft.Framework.Logging; using Microsoft.Framework.OptionsModel; using Moq; @@ -17,6 +18,61 @@ namespace Microsoft.AspNet.Routing { public class RouteCollectionTest { + [Theory] + [InlineData(@"Home/Index/23", "home/index/23", true)] + [InlineData(@"Home/Index/23", "Home/Index/23", false)] + [InlineData(@"Home/Index/23?Param1=ABC&Param2=Xyz", "Home/Index/23?Param1=ABC&Param2=Xyz", false)] + [InlineData(@"Home/Index/23?Param1=ABC&Param2=Xyz", "home/index/23?Param1=ABC&Param2=Xyz", true)] + [InlineData(@"Home/Index/23#Param1=ABC&Param2=Xyz", "Home/Index/23#Param1=ABC&Param2=Xyz", false)] + [InlineData(@"Home/Index/23#Param1=ABC&Param2=Xyz", "home/index/23#Param1=ABC&Param2=Xyz", true)] + public void GetVirtualPath_CanLowerCaseUrls_BasedOnOptions( + string returnUrl, + string lowercaseUrl, + bool lowercaseUrls) + { + // Arrange + var target = new Mock(MockBehavior.Strict); + target + .Setup(e => e.GetVirtualPath(It.IsAny())) + .Returns(returnUrl); + + var routeCollection = new RouteCollection(); + routeCollection.Add(target.Object); + var virtualPathContext = CreateVirtualPathContext(options: GetRouteOptions(lowercaseUrls)); + + // Act + var stringVirtualPath = routeCollection.GetVirtualPath(virtualPathContext); + + // Assert + Assert.Equal(lowercaseUrl, stringVirtualPath); + } + + [Theory] + [InlineData(@"\u0130", @"\u0130", true)] + [InlineData(@"\u0049", @"\u0049", true)] + [InlineData(@"üino", @"üino", true)] + public void GetVirtualPath_DoesntLowerCaseUrls_Invariant( + string returnUrl, + string lowercaseUrl, + bool lowercaseUrls) + { + // Arrange + var target = new Mock(MockBehavior.Strict); + target + .Setup(e => e.GetVirtualPath(It.IsAny())) + .Returns(returnUrl); + + var routeCollection = new RouteCollection(); + routeCollection.Add(target.Object); + var virtualPathContext = CreateVirtualPathContext(options: GetRouteOptions(lowercaseUrls)); + + // Act + var stringVirtualPath = routeCollection.GetVirtualPath(virtualPathContext); + + // Assert + Assert.Equal(lowercaseUrl, stringVirtualPath); + } + [Fact] public async Task RouteAsync_LogsCorrectValuesWhenHandled() { @@ -124,8 +180,8 @@ namespace Microsoft.AspNet.Routing public async Task RouteAsync_SecondMatches() { // Arrange - var routes = new RouteCollection(); + var routes = new RouteCollection(); var route1 = CreateRoute(accept: false); routes.Add(route1.Object); @@ -151,7 +207,6 @@ namespace Microsoft.AspNet.Routing { // Arrange var routes = new RouteCollection(); - var route1 = CreateRoute(accept: false); routes.Add(route1.Object); @@ -171,18 +226,22 @@ namespace Microsoft.AspNet.Routing Assert.Empty(context.RouteData.Routers); } - [Fact] - public void NamedRouteTests_GetNamedRoute_ReturnsValue() + [Theory] + [InlineData(false, "RouteName")] + [InlineData(true, "routename")] + public void NamedRouteTests_GetNamedRoute_ReturnsValue(bool lowercaseUrls, string expectedUrl) { // Arrange var routeCollection = GetNestedRouteCollection(new string[] { "Route1", "Route2", "RouteName", "Route3" }); - var virtualPathContext = CreateVirtualPathContext("RouteName"); + var virtualPathContext = CreateVirtualPathContext( + routeName: "RouteName", + options: GetRouteOptions(lowercaseUrls)); // Act var stringVirtualPath = routeCollection.GetVirtualPath(virtualPathContext); // Assert - Assert.Equal("RouteName", stringVirtualPath); + Assert.Equal(expectedUrl, stringVirtualPath); } [Fact] @@ -207,13 +266,13 @@ namespace Microsoft.AspNet.Routing // Add Duplicate route. routeCollection.Add(CreateNamedRoute("Route3")); - var virtualPathContext = CreateVirtualPathContext("Route1"); + var virtualPathContext = CreateVirtualPathContext(routeName: "Route1", options: GetRouteOptions(true)); // Act var stringVirtualPath = routeCollection.GetVirtualPath(virtualPathContext); // Assert - Assert.Equal("Route1", stringVirtualPath); + Assert.Equal("route1", stringVirtualPath); } [Fact] @@ -225,11 +284,13 @@ namespace Microsoft.AspNet.Routing // Add Duplicate route. routeCollection.Add(CreateNamedRoute(ambiguousRoute)); - var virtualPathContext = CreateVirtualPathContext(ambiguousRoute); + var virtualPathContext = CreateVirtualPathContext(routeName: ambiguousRoute, options: GetRouteOptions()); // Act & Assert var ex = Assert.Throws(() => routeCollection.GetVirtualPath(virtualPathContext)); - Assert.Equal("The supplied route name 'ambiguousRoute' is ambiguous and matched more than one route.", ex.Message); + Assert.Equal( + "The supplied route name 'ambiguousRoute' is ambiguous and matched more than one route.", + ex.Message); } [Fact] @@ -526,6 +587,75 @@ namespace Microsoft.AspNet.Routing route3.Verify(r => r.GetVirtualPath(It.IsAny()), Times.Once()); } + // "Integration" tests for RouteCollection + + public static IEnumerable IntegrationTestData + { + get + { + yield return new object[] { + "{controller}/{action}", + new RouteValueDictionary { { "controller", "Home" }, { "action", "Index" } }, + "home/index", + true }; + + yield return new object[] { + "{controller}/{action}/", + new RouteValueDictionary { { "controller", "Home" }, { "action", "Index" } }, + "Home/Index", + false }; + + yield return new object[] { + "api/{action}/", + new RouteValueDictionary { { "action", "Create" } }, + "api/create", + true }; + + yield return new object[] { + "api/{action}/{id}", + new RouteValueDictionary { + { "action", "Create" }, + { "id", "23" }, + { "Param1", "Value1" }, + { "Param2", "Value2" } }, + "api/create/23?Param1=Value1&Param2=Value2", + true }; + + yield return new object[] { + "api/{action}/{id}", + new RouteValueDictionary { + { "action", "Create" }, + { "id", "23" }, + { "Param1", "Value1" }, + { "Param2", "Value2" } }, + "api/Create/23?Param1=Value1&Param2=Value2", + false }; + } + } + + [Theory] + [MemberData("IntegrationTestData")] + public void GetVirtualPath_Success( + string template, + RouteValueDictionary values, + string expectedUrl, + bool lowercaseUrls + ) + { + // Arrange + var routeCollection = new RouteCollection(); + var route = CreateTemplateRoute(template); + routeCollection.Add(route); + var context = CreateVirtualPathContext(values, options: GetRouteOptions(lowercaseUrls)); + + // Act + var path = routeCollection.GetVirtualPath(context); + + // Assert + Assert.True(context.IsBound); + Assert.Equal(expectedUrl, path); + } + private static async Task SetUp(bool enabled, bool handled) { // Arrange @@ -615,6 +745,18 @@ namespace Microsoft.AspNet.Routing return target.Object; } + private static TemplateRoute CreateTemplateRoute(string template) + { + var target = new Mock(MockBehavior.Strict); + target + .Setup(e => e.GetVirtualPath(It.IsAny())) + .Callback(c => c.IsBound = true) + .Returns(rc => null); + + var resolverMock = new Mock(); + return new TemplateRoute(target.Object, template, resolverMock.Object); + } + private static VirtualPathContext CreateVirtualPathContext( string routeName = null, ILoggerFactory loggerFactory = null, @@ -646,6 +788,23 @@ namespace Microsoft.AspNet.Routing return new VirtualPathContext(context.Object, null, null, routeName); } + private static VirtualPathContext CreateVirtualPathContext( + RouteValueDictionary values, + RouteOptions options = null) + { + var optionsAccessor = new Mock>(MockBehavior.Strict); + optionsAccessor.SetupGet(o => o.Options).Returns(options); + + var context = new Mock(MockBehavior.Strict); + context.Setup(m => m.RequestServices.GetService(typeof(IOptions))) + .Returns(optionsAccessor.Object); + context.Setup(m => m.RequestServices.GetService(typeof(ILoggerFactory))) + .Returns(NullLoggerFactory.Instance); + + + return new VirtualPathContext(context.Object, null, values); + } + private static RouteContext CreateRouteContext( string requestPath, ILoggerFactory loggerFactory = null, @@ -697,6 +856,14 @@ namespace Microsoft.AspNet.Routing return target; } + + private static RouteOptions GetRouteOptions(bool lowerCaseUrls = false, bool useBestEffortLinkGeneration = true) + { + var routeOptions = new RouteOptions(); + routeOptions.LowercaseUrls = lowerCaseUrls; + routeOptions.UseBestEffortLinkGeneration = useBestEffortLinkGeneration; + return routeOptions; + } } } #endif