Issue 140: Support lower case routes

This commit is contained in:
Mugdha Kulkarni 2015-02-11 14:27:59 -08:00
parent f41b6e4d10
commit 81a17300b2
3 changed files with 214 additions and 15 deletions

View File

@ -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)
{

View File

@ -9,6 +9,11 @@ namespace Microsoft.AspNet.Routing
{
public class RouteOptions
{
/// <summary>
/// Gets or sets a value indicating whether all generated URLs are lower-case.
/// </summary>
public bool LowercaseUrls { get; set; }
private IDictionary<string, Type> _constraintTypeMap = GetDefaultConstraintMap();
public IDictionary<string, Type> ConstraintMap

View File

@ -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<IRouter>(MockBehavior.Strict);
target
.Setup(e => e.GetVirtualPath(It.IsAny<VirtualPathContext>()))
.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<IRouter>(MockBehavior.Strict);
target
.Setup(e => e.GetVirtualPath(It.IsAny<VirtualPathContext>()))
.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<InvalidOperationException>(() => 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<VirtualPathContext>()), Times.Once());
}
// "Integration" tests for RouteCollection
public static IEnumerable<object[]> 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<TestSink> 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<IRouter>(MockBehavior.Strict);
target
.Setup(e => e.GetVirtualPath(It.IsAny<VirtualPathContext>()))
.Callback<VirtualPathContext>(c => c.IsBound = true)
.Returns<VirtualPathContext>(rc => null);
var resolverMock = new Mock<IInlineConstraintResolver>();
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<IOptions<RouteOptions>>(MockBehavior.Strict);
optionsAccessor.SetupGet(o => o.Options).Returns(options);
var context = new Mock<HttpContext>(MockBehavior.Strict);
context.Setup(m => m.RequestServices.GetService(typeof(IOptions<RouteOptions>)))
.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