// 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 System; using System.Collections.Generic; using System.Text.Encodings.Web; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Mvc.Abstractions; using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.ObjectPool; using Moq; using Xunit; namespace Microsoft.AspNetCore.Mvc.Routing { public abstract class UrlHelperTestBase { [Theory] [InlineData(null, null, null)] [InlineData("/myapproot", null, null)] [InlineData("", "/Home/About", "/Home/About")] [InlineData("/myapproot", "/test", "/test")] public void Content_ReturnsContentPath_WhenItDoesNotStartWithToken( string appRoot, string contentPath, string expectedPath) { // Arrange var urlHelper = CreateUrlHelper(appRoot); // Act var path = urlHelper.Content(contentPath); // Assert Assert.Equal(expectedPath, path); } [Theory] [InlineData(null, "~/Home/About", "/Home/About")] [InlineData("/", "~/Home/About", "/Home/About")] [InlineData("/", "~/", "/")] [InlineData("/myapproot", "~/", "/myapproot/")] [InlineData("", "~/Home/About", "/Home/About")] [InlineData("/", "~", "/")] [InlineData("/myapproot", "~/Content/bootstrap.css", "/myapproot/Content/bootstrap.css")] public void Content_ReturnsAppRelativePath_WhenItStartsWithToken( string appRoot, string contentPath, string expectedPath) { // Arrange var urlHelper = CreateUrlHelper(appRoot); // Act var path = urlHelper.Content(contentPath); // Assert Assert.Equal(expectedPath, path); } [Theory] [InlineData(null)] [InlineData("")] [InlineData(" ")] public void IsLocalUrl_ReturnsFalseOnEmpty(string url) { // Arrange var helper = CreateUrlHelper(); // Act var result = helper.IsLocalUrl(url); // Assert Assert.False(result); } [Theory] [InlineData("/foo.html")] [InlineData("/www.example.com")] [InlineData("/")] public void IsLocalUrl_AcceptsRootedUrls(string url) { // Arrange var helper = CreateUrlHelper(); // Act var result = helper.IsLocalUrl(url); // Assert Assert.True(result); } [Theory] [InlineData("~/")] [InlineData("~/foo.html")] public void IsLocalUrl_AcceptsApplicationRelativeUrls(string url) { // Arrange var helper = CreateUrlHelper(); // Act var result = helper.IsLocalUrl(url); // Assert Assert.True(result); } [Theory] [InlineData("foo.html")] [InlineData("../foo.html")] [InlineData("fold/foo.html")] public void IsLocalUrl_RejectsRelativeUrls(string url) { // Arrange var helper = CreateUrlHelper(); // Act var result = helper.IsLocalUrl(url); // Assert Assert.False(result); } [Theory] [InlineData("http:/foo.html")] [InlineData("hTtP:foo.html")] [InlineData("http:/www.example.com")] [InlineData("HtTpS:/www.example.com")] public void IsLocalUrl_RejectValidButUnsafeRelativeUrls(string url) { // Arrange var helper = CreateUrlHelper(); // Act var result = helper.IsLocalUrl(url); // Assert Assert.False(result); } [Theory] [InlineData("http://www.mysite.com/appDir/foo.html")] [InlineData("http://WWW.MYSITE.COM")] public void IsLocalUrl_RejectsUrlsOnTheSameHost(string url) { // Arrange var helper = CreateUrlHelper(appRoot: string.Empty, host: "www.mysite.com", protocol: null); // Act var result = helper.IsLocalUrl(url); // Assert Assert.False(result); } [Theory] [InlineData("http://localhost/foobar.html")] [InlineData("http://127.0.0.1/foobar.html")] public void IsLocalUrl_RejectsUrlsOnLocalHost(string url) { // Arrange var helper = CreateUrlHelper(appRoot: string.Empty, host: "www.mysite.com", protocol: null); // Act var result = helper.IsLocalUrl(url); // Assert Assert.False(result); } [Theory] [InlineData("https://www.mysite.com/")] public void IsLocalUrl_RejectsUrlsOnTheSameHostButDifferentScheme(string url) { // Arrange var helper = CreateUrlHelper(appRoot: string.Empty, host: "www.mysite.com", protocol: null); // Act var result = helper.IsLocalUrl(url); // Assert Assert.False(result); } [Theory] [InlineData("http://www.example.com")] [InlineData("https://www.example.com")] [InlineData("hTtP://www.example.com")] [InlineData("HtTpS://www.example.com")] public void IsLocalUrl_RejectsUrlsOnDifferentHost(string url) { // Arrange var helper = CreateUrlHelper(appRoot: string.Empty, host: "www.mysite.com", protocol: null); // Act var result = helper.IsLocalUrl(url); // Assert Assert.False(result); } [Theory] [InlineData("http://///www.example.com/foo.html")] [InlineData("https://///www.example.com/foo.html")] [InlineData("HtTpS://///www.example.com/foo.html")] [InlineData("http:///www.example.com/foo.html")] [InlineData("http:////www.example.com/foo.html")] public void IsLocalUrl_RejectsUrlsWithTooManySchemeSeparatorCharacters(string url) { // Arrange var helper = CreateUrlHelper(appRoot: string.Empty, host: "www.mysite.com", protocol: null); // Act var result = helper.IsLocalUrl(url); // Assert Assert.False(result); } [Theory] [InlineData("//www.example.com")] [InlineData("//www.example.com?")] [InlineData("//www.example.com:80")] [InlineData("//www.example.com/foobar.html")] [InlineData("///www.example.com")] [InlineData("//////www.example.com")] public void IsLocalUrl_RejectsUrlsWithMissingSchemeName(string url) { // Arrange var helper = CreateUrlHelper(appRoot: string.Empty, host: "www.mysite.com", protocol: null); // Act var result = helper.IsLocalUrl(url); // Assert Assert.False(result); } [Theory] [InlineData("http:\\\\www.example.com")] [InlineData("http:\\\\www.example.com\\")] [InlineData("/\\")] [InlineData("/\\foo")] public void IsLocalUrl_RejectsInvalidUrls(string url) { // Arrange var helper = CreateUrlHelper(appRoot: string.Empty, host: "www.mysite.com", protocol: null); // Act var result = helper.IsLocalUrl(url); // Assert Assert.False(result); } [Theory] [InlineData("~//www.example.com")] [InlineData("~//www.example.com?")] [InlineData("~//www.example.com:80")] [InlineData("~//www.example.com/foobar.html")] [InlineData("~///www.example.com")] [InlineData("~//////www.example.com")] public void IsLocalUrl_RejectsTokenUrlsWithMissingSchemeName(string url) { // Arrange var helper = CreateUrlHelper(appRoot: string.Empty, host: "www.mysite.com", protocol: null); // Act var result = helper.IsLocalUrl(url); // Assert Assert.False(result); } [Theory] [InlineData("~/\\")] [InlineData("~/\\foo")] public void IsLocalUrl_RejectsInvalidTokenUrls(string url) { // Arrange var helper = CreateUrlHelper(appRoot: string.Empty, host: "www.mysite.com", protocol: null); // Act var result = helper.IsLocalUrl(url); // Assert Assert.False(result); } [Fact] public void RouteUrlWithDictionary() { // Arrange var urlHelper = CreateUrlHelperWithDefaultRoutes(); // Act var url = urlHelper.RouteUrl( values: new RouteValueDictionary( new { Action = "newaction", Controller = "home2", id = "someid" })); // Assert Assert.Equal("/app/home2/newaction/someid", url); } [Fact] public void RouteUrlWithEmptyHostName() { // Arrange var urlHelper = CreateUrlHelperWithDefaultRoutes(); // Act var url = urlHelper.RouteUrl( routeName: "namedroute", values: new RouteValueDictionary( new { Action = "newaction", Controller = "home2", id = "someid" }), protocol: "http", host: string.Empty); // Assert Assert.Equal("http://localhost/app/named/home2/newaction/someid", url); } [Fact] public void RouteUrlWithEmptyProtocol() { // Arrange var urlHelper = CreateUrlHelperWithDefaultRoutes(); // Act var url = urlHelper.RouteUrl( routeName: "namedroute", values: new RouteValueDictionary( new { Action = "newaction", Controller = "home2", id = "someid" }), protocol: string.Empty, host: "foo.bar.com"); // Assert Assert.Equal("http://foo.bar.com/app/named/home2/newaction/someid", url); } [Fact] public void RouteUrlWithNullProtocol() { // Arrange var urlHelper = CreateUrlHelperWithDefaultRoutes(); // Act var url = urlHelper.RouteUrl( routeName: "namedroute", values: new RouteValueDictionary( new { Action = "newaction", Controller = "home2", id = "someid" }), protocol: null, host: "foo.bar.com"); // Assert Assert.Equal("http://foo.bar.com/app/named/home2/newaction/someid", url); } [Fact] public void RouteUrlWithNullProtocolAndNullHostName() { // Arrange var urlHelper = CreateUrlHelperWithDefaultRoutes(); // Act var url = urlHelper.RouteUrl( routeName: "namedroute", values: new RouteValueDictionary( new { Action = "newaction", Controller = "home2", id = "someid" }), protocol: null, host: null); // Assert Assert.Equal("/app/named/home2/newaction/someid", url); } [Fact] public void RouteUrlWithObjectProperties() { // Arrange var urlHelper = CreateUrlHelperWithDefaultRoutes(); // Act var url = urlHelper.RouteUrl(new { Action = "newaction", Controller = "home2", id = "someid" }); // Assert Assert.Equal("/app/home2/newaction/someid", url); } [Fact] public void RouteUrlWithProtocol() { // Arrange var urlHelper = CreateUrlHelperWithDefaultRoutes(); // Act var url = urlHelper.RouteUrl( routeName: "namedroute", values: new { Action = "newaction", Controller = "home2", id = "someid" }, protocol: "https"); // Assert Assert.Equal("https://localhost/app/named/home2/newaction/someid", url); } [Fact] public void RouteUrl_WithUnicodeHost_DoesNotPunyEncodeTheHost() { // Arrange var urlHelper = CreateUrlHelperWithDefaultRoutes(); // Act var url = urlHelper.RouteUrl( routeName: "namedroute", values: new { Action = "newaction", Controller = "home2", id = "someid" }, protocol: "https", host: "pingüino"); // Assert Assert.Equal("https://pingüino/app/named/home2/newaction/someid", url); } [Fact] public void RouteUrl_GeneratesUrl_WithRouteName_UsingDefaultValues_WhenExplicitOrAmbientValues_NotPresent() { // Arrange var urlHelper = CreateUrlHelperWithDefaultRoutes(); // Act var url = urlHelper.RouteUrl( routeName: "OrdersApi", values: new { id = "500" }); // Assert Assert.Equal("/app/api/orders/500", url); } [Fact] public void RouteUrl_WithRouteName_DoesNotGenerateUrl_WhenRequiredValueForParameter_NotPresent() { // Arrange var urlHelper = CreateUrlHelperWithDefaultRoutes(); // Act var url = urlHelper.RouteUrl( routeName: "OrdersApi", values: new { }); // Assert Assert.Null(url); } [Fact] public void RouteUrlWithRouteNameAndDictionary() { // Arrange var urlHelper = CreateUrlHelperWithDefaultRoutes(); // Act var url = urlHelper.RouteUrl( routeName: "namedroute", values: new RouteValueDictionary( new { Action = "newaction", Controller = "home2", id = "someid" })); // Assert Assert.Equal("/app/named/home2/newaction/someid", url); } [Fact] public void RouteUrlWithRouteNameAndObjectProperties() { // Arrange var urlHelper = CreateUrlHelperWithDefaultRoutes(); // Act var url = urlHelper.RouteUrl( routeName: "namedroute", values: new { Action = "newaction", Controller = "home2", id = "someid" }); // Assert Assert.Equal("/app/named/home2/newaction/someid", url); } [Fact] public void RouteUrlWithUrlRouteContext_ReturnsExpectedResult() { // Arrange var urlHelper = CreateUrlHelperWithDefaultRoutes(); var routeContext = new UrlRouteContext() { RouteName = "namedroute", Values = new { Action = "newaction", Controller = "home2", id = "someid" }, Fragment = "somefragment", Host = "remotetown", Protocol = "ftp" }; // Act var url = urlHelper.RouteUrl(routeContext); // Assert Assert.Equal("ftp://remotetown/app/named/home2/newaction/someid#somefragment", url); } [Fact] public void RouteUrlWithAllParameters_ReturnsExpectedResult() { // Arrange var urlHelper = CreateUrlHelperWithDefaultRoutes(); // Act var url = urlHelper.RouteUrl( routeName: "namedroute", values: new { Action = "newaction", Controller = "home2", id = "someid" }, fragment: "somefragment", host: "remotetown", protocol: "https"); // Assert Assert.Equal("https://remotetown/app/named/home2/newaction/someid#somefragment", url); } [Fact] public void UrlAction_RouteValuesAsDictionary_CaseSensitive() { // Arrange var urlHelper = CreateUrlHelperWithDefaultRoutes(); // We're using a dictionary with a case-sensitive comparer and loading it with data // using casings differently from the route. This should still successfully generate a link. var dictionary = new Dictionary(); var id = "suppliedid"; var isprint = "true"; dictionary["ID"] = id; dictionary["isprint"] = isprint; // Act var url = urlHelper.Action( action: "contact", controller: "home", values: dictionary); // Assert Assert.Equal(2, dictionary.Count); Assert.Same(id, dictionary["ID"]); Assert.Same(isprint, dictionary["isprint"]); Assert.Equal("/app/home/contact/suppliedid?isprint=true", url); } [Fact] public void UrlAction_WithUnicodeHost_DoesNotPunyEncodeTheHost() { // Arrange var urlHelper = CreateUrlHelperWithDefaultRoutes(); // Act var url = urlHelper.Action( action: "contact", controller: "home", values: null, protocol: "http", host: "pingüino"); // Assert Assert.Equal("http://pingüino/app/home/contact", url); } [Fact] public void UrlRouteUrl_RouteValuesAsDictionary_CaseSensitive() { // Arrange var urlHelper = CreateUrlHelperWithDefaultRoutes(); // We're using a dictionary with a case-sensitive comparer and loading it with data // using casings differently from the route. This should still successfully generate a link. var dict = new Dictionary(); var action = "contact"; var controller = "home"; var id = "suppliedid"; dict["ACTION"] = action; dict["Controller"] = controller; dict["ID"] = id; // Act var url = urlHelper.RouteUrl(routeName: "namedroute", values: dict); // Assert Assert.Equal(3, dict.Count); Assert.Same(action, dict["ACTION"]); Assert.Same(controller, dict["Controller"]); Assert.Same(id, dict["ID"]); Assert.Equal("/app/named/home/contact/suppliedid", url); } [Fact] public void UrlActionWithUrlActionContext_ReturnsExpectedResult() { // Arrange var urlHelper = CreateUrlHelperWithDefaultRoutes(); var actionContext = new UrlActionContext() { Action = "contact", Controller = "home3", Values = new { id = "idone" }, Protocol = "ftp", Host = "remotelyhost", Fragment = "somefragment" }; // Act var url = urlHelper.Action(actionContext); // Assert Assert.Equal("ftp://remotelyhost/app/home3/contact/idone#somefragment", url); } [Fact] public void UrlActionWithAllParameters_ReturnsExpectedResult() { // Arrange var urlHelper = CreateUrlHelperWithDefaultRoutes(); // Act var url = urlHelper.Action( controller: "home3", action: "contact", values: null, protocol: "https", host: "remotelyhost", fragment: "somefragment"); // Assert Assert.Equal("https://remotelyhost/app/home3/contact#somefragment", url); } [Fact] public void LinkWithAllParameters_ReturnsExpectedResult() { // Arrange var urlHelper = CreateUrlHelperWithDefaultRoutes(); // Act var url = urlHelper.Link( "namedroute", new { Action = "newaction", Controller = "home", id = "someid" }); // Assert Assert.Equal("http://localhost/app/named/home/newaction/someid", url); } [Fact] public void LinkWithNullRouteName_ReturnsExpectedResult() { // Arrange var urlHelper = CreateUrlHelperWithDefaultRoutes(); // Act var url = urlHelper.Link( null, new { Action = "newaction", Controller = "home", id = "someid" }); // Assert Assert.Equal("http://localhost/app/home/newaction/someid", url); } [Fact] public void RouteUrlWithRouteNameAndDefaults() { // Arrange var urlHelper = CreateUrlHelperWithDefaultRoutes( "/app", host: null, protocol: null, routeName: "MyRouteName", template: "any/url"); // Act var url = urlHelper.RouteUrl("MyRouteName"); // Assert Assert.Equal("/app/any/url", url); } [Fact] public void LinkWithDefaultsAndNullRouteValues_ReturnsExpectedResult() { // Arrange var urlHelper = CreateUrlHelperWithDefaultRoutes( "/app", host: null, protocol: null, routeName: "MyRouteName", template: "any/url"); // Act var url = urlHelper.Link("MyRouteName", null); // Assert Assert.Equal("http://localhost/app/any/url", url); } [Fact] public void LinkWithCustomHostAndProtocol_ReturnsExpectedResult() { // Arrange var urlHelper = CreateUrlHelperWithDefaultRoutes( string.Empty, "myhost", "https", routeName: "MyRouteName", template: "any/url"); // Act var url = urlHelper.Link( "namedroute", new { Action = "newaction", Controller = "home", id = "someid" }); // Assert Assert.Equal("https://myhost/named/home/newaction/someid", url); } [Fact] public void GetUrlHelper_ReturnsSameInstance_IfAlreadyPresent() { // Arrange var expectedUrlHelper = CreateUrlHelper(); var httpContext = new Mock(); httpContext.SetupGet(h => h.Features).Returns(new FeatureCollection()); var mockItems = new Dictionary { { typeof(IUrlHelper), expectedUrlHelper } }; httpContext.Setup(h => h.Items).Returns(mockItems); var actionContext = CreateActionContext(httpContext.Object); var urlHelperFactory = new UrlHelperFactory(); // Act var urlHelper = urlHelperFactory.GetUrlHelper(actionContext); // Assert Assert.Same(expectedUrlHelper, urlHelper); } [Fact] public void GetUrlHelper_CreatesNewInstance_IfNotAlreadyPresent() { // Arrange var httpContext = new Mock(); httpContext.SetupGet(h => h.Features).Returns(new FeatureCollection()); httpContext.Setup(h => h.Items).Returns(new Dictionary()); var actionContext = CreateActionContext(httpContext.Object); var urlHelperFactory = new UrlHelperFactory(); // Act var urlHelper = urlHelperFactory.GetUrlHelper(actionContext); // Assert Assert.NotNull(urlHelper); Assert.Same(urlHelper, actionContext.HttpContext.Items[typeof(IUrlHelper)] as IUrlHelper); } [Fact] public void GetUrlHelper_CreatesNewInstance_IfExpectedTypeIsNotPresent() { // Arrange var httpContext = new Mock(); httpContext.SetupGet(h => h.Features).Returns(new FeatureCollection()); var mockItems = new Dictionary { { typeof(IUrlHelper), null } }; httpContext.Setup(h => h.Items).Returns(mockItems); var actionContext = CreateActionContext(httpContext.Object); var urlHelperFactory = new UrlHelperFactory(); // Act var urlHelper = urlHelperFactory.GetUrlHelper(actionContext); // Assert Assert.NotNull(urlHelper); Assert.Same(urlHelper, actionContext.HttpContext.Items[typeof(IUrlHelper)] as IUrlHelper); } // Regression test for aspnet/Mvc#2859 [Fact] public void Action_RouteValueInvalidation_DoesNotAffectActionAndController() { // Arrange var urlHelper = CreateUrlHelper( appRoot: "", host: null, protocol: null, "default", "{first}/{controller}/{action}", new { second = "default", controller = "default", action = "default" }); var routeData = urlHelper.ActionContext.RouteData; routeData.Values.Add("first", "a"); routeData.Values.Add("controller", "Store"); routeData.Values.Add("action", "Buy"); // Act // // In this test the 'first' route value has changed, meaning that *normally* the // 'controller' value could not be used. However 'controller' and 'action' are treated // specially by UrlHelper. var url = urlHelper.Action("Checkout", new { first = "b" }); // Assert Assert.NotNull(url); Assert.Equal("/b/Store/Checkout", url); } // Regression test for aspnet/Mvc#2859 [Fact] public void Action_RouteValueInvalidation_AffectsOtherRouteValues() { // Arrange var urlHelper = CreateUrlHelper( appRoot: "", host: null, protocol: null, "default", "{first}/{second}/{controller}/{action}", new { second = "default", controller = "default", action = "default" }); var routeData = urlHelper.ActionContext.RouteData; routeData.Values.Add("first", "a"); routeData.Values.Add("second", "x"); routeData.Values.Add("controller", "Store"); routeData.Values.Add("action", "Buy"); // Act // // In this test the 'first' route value has changed, meaning that *normally* the // 'controller' value could not be used. However 'controller' and 'action' are treated // specially by UrlHelper. // // 'second' gets no special treatment, and picks up its default value instead. var url = urlHelper.Action("Checkout", new { first = "b" }); // Assert Assert.NotNull(url); Assert.Equal("/b/default/Store/Checkout", url); } // Regression test for aspnet/Mvc#2859 [Fact] public void Action_RouteValueInvalidation_DoesNotAffectActionAndController_ActionPassedInRouteValues() { // Arrange var urlHelper = CreateUrlHelper( appRoot: "", host: null, protocol: null, "default", "{first}/{controller}/{action}", new { second = "default", controller = "default", action = "default" }); var routeData = urlHelper.ActionContext.RouteData; routeData.Values.Add("first", "a"); routeData.Values.Add("controller", "Store"); routeData.Values.Add("action", "Buy"); // Act // // In this test the 'first' route value has changed, meaning that *normally* the // 'controller' value could not be used. However 'controller' and 'action' are treated // specially by UrlHelper. var url = urlHelper.Action(action: null, values: new { first = "b", action = "Checkout" }); // Assert Assert.NotNull(url); Assert.Equal("/b/Store/Checkout", url); } protected abstract IServiceProvider CreateServices(); protected abstract IUrlHelper CreateUrlHelper(ActionContext actionContext); protected abstract IUrlHelper CreateUrlHelperWithDefaultRoutes( string appRoot, string host, string protocol); protected abstract IUrlHelper CreateUrlHelperWithDefaultRoutes( string appRoot, string host, string protocol, string routeName, string template); protected abstract IUrlHelper CreateUrlHelper( string appRoot, string host, string protocol, string routeName, string template, object defaults); protected virtual IUrlHelper CreateUrlHelper(string appRoot, string host, string protocol) { appRoot = string.IsNullOrEmpty(appRoot) ? string.Empty : appRoot; host = string.IsNullOrEmpty(host) ? "localhost" : host; var services = CreateServices(); var httpContext = CreateHttpContext(services, appRoot, host, protocol); var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor()); return CreateUrlHelper(actionContext); } protected virtual ActionContext CreateActionContext(HttpContext httpContext, RouteData routeData = null) { routeData = routeData ?? new RouteData(); return new ActionContext(httpContext, routeData, new ActionDescriptor()); } protected virtual HttpContext CreateHttpContext( IServiceProvider services, string appRoot, string host, string protocol) { appRoot = string.IsNullOrEmpty(appRoot) ? string.Empty : appRoot; host = string.IsNullOrEmpty(host) ? "localhost" : host; var context = new DefaultHttpContext(); context.RequestServices = services; context.Request.PathBase = new PathString(appRoot); context.Request.Host = new HostString(host); context.Request.Scheme = protocol; return context; } protected IServiceCollection GetCommonServices() { var services = new ServiceCollection(); services.AddOptions(); services.AddLogging(); services.AddRouting(); services .AddSingleton() .AddSingleton(UrlEncoder.Default); return services; } private IUrlHelper CreateUrlHelper(string appRoot = "") { return CreateUrlHelper(appRoot, host: null, protocol: null); } private IUrlHelper CreateUrlHelperWithDefaultRoutes() { return CreateUrlHelperWithDefaultRoutes(appRoot: "/app", host: null, protocol: null); } } }