From a0a9c2c58555264491860f06d4e75e215d821738 Mon Sep 17 00:00:00 2001 From: Kiran Challa Date: Thu, 14 Jun 2018 17:46:28 -0700 Subject: [PATCH] Integrate Dispatcher's link generator Related to https://github.com/aspnet/Routing/issues/530 --- .../Routing/DispatcherUrlHelper.cs | 127 ++ .../Routing/UrlHelper.cs | 288 +-- .../Routing/UrlHelperBase.cs | 293 +++ .../Routing/UrlHelperFactory.cs | 22 +- .../LocalRedirectResultTest.cs | 11 +- .../RedirectResultTest.cs | 12 +- .../Routing/DispatcherUrlHelperTest.cs | 150 ++ .../Routing/UrlHelperBaseTest.cs | 171 ++ .../Routing/UrlHelperExtensionsTest.cs | 585 ++++++ .../Routing/UrlHelperTest.cs | 1815 +---------------- .../Routing/UrlHelperTestBase.cs | 1007 +++++++++ 11 files changed, 2450 insertions(+), 2031 deletions(-) create mode 100644 src/Microsoft.AspNetCore.Mvc.Core/Routing/DispatcherUrlHelper.cs create mode 100644 src/Microsoft.AspNetCore.Mvc.Core/Routing/UrlHelperBase.cs create mode 100644 test/Microsoft.AspNetCore.Mvc.Core.Test/Routing/DispatcherUrlHelperTest.cs create mode 100644 test/Microsoft.AspNetCore.Mvc.Core.Test/Routing/UrlHelperBaseTest.cs create mode 100644 test/Microsoft.AspNetCore.Mvc.Core.Test/Routing/UrlHelperExtensionsTest.cs create mode 100644 test/Microsoft.AspNetCore.Mvc.Core.Test/Routing/UrlHelperTestBase.cs diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Routing/DispatcherUrlHelper.cs b/src/Microsoft.AspNetCore.Mvc.Core/Routing/DispatcherUrlHelper.cs new file mode 100644 index 0000000000..bb6e1a1bdc --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.Core/Routing/DispatcherUrlHelper.cs @@ -0,0 +1,127 @@ +// 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 Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.Logging; + +namespace Microsoft.AspNetCore.Mvc.Routing +{ + /// + /// An implementation of that uses to build URLs + /// for ASP.NET MVC within an application. + /// + internal class DispatcherUrlHelper : UrlHelperBase + { + private readonly ILogger _logger; + private readonly ILinkGenerator _linkGenerator; + + /// + /// Initializes a new instance of the class using the specified + /// . + /// + /// The for the current request. + /// The used to generate the link. + /// The . + public DispatcherUrlHelper( + ActionContext actionContext, + ILinkGenerator linkGenerator, + ILogger logger) + : base(actionContext) + { + if (linkGenerator == null) + { + throw new ArgumentNullException(nameof(linkGenerator)); + } + + if (logger == null) + { + throw new ArgumentNullException(nameof(logger)); + } + + _linkGenerator = linkGenerator; + _logger = logger; + } + + /// + public override string Action(UrlActionContext urlActionContext) + { + if (urlActionContext == null) + { + throw new ArgumentNullException(nameof(urlActionContext)); + } + + var valuesDictionary = GetValuesDictionary(urlActionContext.Values); + + if (urlActionContext.Action == null) + { + if (!valuesDictionary.ContainsKey("action") && + AmbientValues.TryGetValue("action", out var action)) + { + valuesDictionary["action"] = action; + } + } + else + { + valuesDictionary["action"] = urlActionContext.Action; + } + + if (urlActionContext.Controller == null) + { + if (!valuesDictionary.ContainsKey("controller") && + AmbientValues.TryGetValue("controller", out var controller)) + { + valuesDictionary["controller"] = controller; + } + } + else + { + valuesDictionary["controller"] = urlActionContext.Controller; + } + + var successfullyGeneratedLink = _linkGenerator.TryGetLink( + new LinkGeneratorContext() + { + SuppliedValues = valuesDictionary, + AmbientValues = AmbientValues + }, + out var link); + + if (!successfullyGeneratedLink) + { + //TODO: log here + + return null; + } + + return GenerateUrl(urlActionContext.Protocol, urlActionContext.Host, link, urlActionContext.Fragment); + } + + /// + public override string RouteUrl(UrlRouteContext routeContext) + { + if (routeContext == null) + { + throw new ArgumentNullException(nameof(routeContext)); + } + + var valuesDictionary = routeContext.Values as RouteValueDictionary ?? GetValuesDictionary(routeContext.Values); + + var successfullyGeneratedLink = _linkGenerator.TryGetLink( + new LinkGeneratorContext() + { + Address = new Address(routeContext.RouteName), + SuppliedValues = valuesDictionary, + AmbientValues = AmbientValues + }, + out var link); + + if (!successfullyGeneratedLink) + { + return null; + } + + return GenerateUrl(routeContext.Protocol, routeContext.Host, link, routeContext.Fragment); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Routing/UrlHelper.cs b/src/Microsoft.AspNetCore.Mvc.Core/Routing/UrlHelper.cs index 31a8b4a9fd..8bab2581d4 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/Routing/UrlHelper.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/Routing/UrlHelper.cs @@ -2,9 +2,6 @@ // 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.Diagnostics; -using System.Text; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing; @@ -14,38 +11,18 @@ namespace Microsoft.AspNetCore.Mvc.Routing /// An implementation of that contains methods to /// build URLs for ASP.NET MVC within an application. /// - public class UrlHelper : IUrlHelper + public class UrlHelper : UrlHelperBase { - - // Perf: Share the StringBuilder object across multiple calls of GenerateURL for this UrlHelper - private StringBuilder _stringBuilder; - // Perf: Reuse the RouteValueDictionary across multiple calls of Action for this UrlHelper - private readonly RouteValueDictionary _routeValueDictionary; - /// /// Initializes a new instance of the class using the specified /// . /// /// The for the current request. public UrlHelper(ActionContext actionContext) + : base(actionContext) { - if (actionContext == null) - { - throw new ArgumentNullException(nameof(actionContext)); - } - - ActionContext = actionContext; - _routeValueDictionary = new RouteValueDictionary(); } - /// - public ActionContext ActionContext { get; } - - /// - /// Gets the associated with the current request. - /// - protected RouteValueDictionary AmbientValues => ActionContext.RouteData.Values; - /// /// Gets the associated with the current request. /// @@ -58,7 +35,7 @@ namespace Microsoft.AspNetCore.Mvc.Routing protected IRouter Router => ActionContext.RouteData.Routers[0]; /// - public virtual string Action(UrlActionContext actionContext) + public override string Action(UrlActionContext actionContext) { if (actionContext == null) { @@ -69,9 +46,8 @@ namespace Microsoft.AspNetCore.Mvc.Routing if (actionContext.Action == null) { - object action; if (!valuesDictionary.ContainsKey("action") && - AmbientValues.TryGetValue("action", out action)) + AmbientValues.TryGetValue("action", out var action)) { valuesDictionary["action"] = action; } @@ -83,9 +59,8 @@ namespace Microsoft.AspNetCore.Mvc.Routing if (actionContext.Controller == null) { - object controller; if (!valuesDictionary.ContainsKey("controller") && - AmbientValues.TryGetValue("controller", out controller)) + AmbientValues.TryGetValue("controller", out var controller)) { valuesDictionary["controller"] = controller; } @@ -100,54 +75,7 @@ namespace Microsoft.AspNetCore.Mvc.Routing } /// - public virtual bool IsLocalUrl(string url) - { - if (string.IsNullOrEmpty(url)) - { - return false; - } - - // Allows "/" or "/foo" but not "//" or "/\". - if (url[0] == '/') - { - // url is exactly "/" - if (url.Length == 1) - { - return true; - } - - // url doesn't start with "//" or "/\" - if (url[1] != '/' && url[1] != '\\') - { - return true; - } - - return false; - } - - // Allows "~/" or "~/foo" but not "~//" or "~/\". - if (url[0] == '~' && url.Length > 1 && url[1] == '/') - { - // url is exactly "~/" - if (url.Length == 2) - { - return true; - } - - // url doesn't start with "~//" or "~/\" - if (url[2] != '/' && url[2] != '\\') - { - return true; - } - - return false; - } - - return false; - } - - /// - public virtual string RouteUrl(UrlRouteContext routeContext) + public override string RouteUrl(UrlRouteContext routeContext) { if (routeContext == null) { @@ -167,7 +95,7 @@ namespace Microsoft.AspNetCore.Mvc.Routing /// /// /// The . The uses these values, in combination with - /// , to generate the URL. + /// , to generate the URL. /// /// The . protected virtual VirtualPathData GetVirtualPathData(string routeName, RouteValueDictionary values) @@ -176,128 +104,6 @@ namespace Microsoft.AspNetCore.Mvc.Routing return Router.GetVirtualPath(context); } - // Internal for unit testing. - internal void AppendPathAndFragment(StringBuilder builder, VirtualPathData pathData, string fragment) - { - var pathBase = HttpContext.Request.PathBase; - - if (!pathBase.HasValue) - { - if (pathData.VirtualPath.Length == 0) - { - builder.Append("/"); - } - else - { - if (!pathData.VirtualPath.StartsWith("/", StringComparison.Ordinal)) - { - builder.Append("/"); - } - - builder.Append(pathData.VirtualPath); - } - } - else - { - if (pathData.VirtualPath.Length == 0) - { - builder.Append(pathBase.Value); - } - else - { - builder.Append(pathBase.Value); - - if (pathBase.Value.EndsWith("/", StringComparison.Ordinal)) - { - builder.Length--; - } - - if (!pathData.VirtualPath.StartsWith("/", StringComparison.Ordinal)) - { - builder.Append("/"); - } - - builder.Append(pathData.VirtualPath); - } - } - - if (!string.IsNullOrEmpty(fragment)) - { - builder.Append("#").Append(fragment); - } - } - - /// - public virtual string Content(string contentPath) - { - if (string.IsNullOrEmpty(contentPath)) - { - return null; - } - else if (contentPath[0] == '~') - { - var segment = new PathString(contentPath.Substring(1)); - var applicationPath = HttpContext.Request.PathBase; - - return applicationPath.Add(segment).Value; - } - - return contentPath; - } - - /// - public virtual string Link(string routeName, object values) - { - return RouteUrl(new UrlRouteContext() - { - RouteName = routeName, - Values = values, - Protocol = HttpContext.Request.Scheme, - Host = HttpContext.Request.Host.ToUriComponent() - }); - } - - private RouteValueDictionary GetValuesDictionary(object values) - { - // Perf: RouteValueDictionary can be cast to IDictionary, but it is - // special cased to avoid allocating boxed Enumerator. - var routeValuesDictionary = values as RouteValueDictionary; - if (routeValuesDictionary != null) - { - _routeValueDictionary.Clear(); - foreach (var kvp in routeValuesDictionary) - { - _routeValueDictionary.Add(kvp.Key, kvp.Value); - } - - return _routeValueDictionary; - } - - var dictionaryValues = values as IDictionary; - if (dictionaryValues != null) - { - _routeValueDictionary.Clear(); - foreach (var kvp in dictionaryValues) - { - _routeValueDictionary.Add(kvp.Key, kvp.Value); - } - - return _routeValueDictionary; - } - - return new RouteValueDictionary(values); - } - - private StringBuilder GetStringBuilder() - { - if(_stringBuilder == null) - { - _stringBuilder = new StringBuilder(); - } - - return _stringBuilder; - } - /// /// Generates the URL using the specified components. /// @@ -308,85 +114,7 @@ namespace Microsoft.AspNetCore.Mvc.Routing /// The generated URL. protected virtual string GenerateUrl(string protocol, string host, VirtualPathData pathData, string fragment) { - if (pathData == null) - { - return null; - } - - // VirtualPathData.VirtualPath returns string.Empty instead of null. - Debug.Assert(pathData.VirtualPath != null); - - // Perf: In most of the common cases, GenerateUrl is called with a null protocol, host and fragment. - // In such cases, we might not need to build any URL as the url generated is mostly same as the virtual path available in pathData. - // For such common cases, this FastGenerateUrl method saves a string allocation per GenerateUrl call. - string url; - if (TryFastGenerateUrl(protocol, host, pathData, fragment, out url)) - { - return url; - } - - var builder = GetStringBuilder(); - try - { - if (string.IsNullOrEmpty(protocol) && string.IsNullOrEmpty(host)) - { - AppendPathAndFragment(builder, pathData, fragment); - // We're returning a partial URL (just path + query + fragment), but we still want it to be rooted. - if (builder.Length == 0 || builder[0] != '/') - { - builder.Insert(0, '/'); - } - } - else - { - protocol = string.IsNullOrEmpty(protocol) ? "http" : protocol; - builder.Append(protocol); - - builder.Append("://"); - - host = string.IsNullOrEmpty(host) ? HttpContext.Request.Host.Value : host; - builder.Append(host); - AppendPathAndFragment(builder, pathData, fragment); - } - - var path = builder.ToString(); - return path; - } - finally - { - // Clear the StringBuilder so that it can reused for the next call. - builder.Clear(); - } - } - - private bool TryFastGenerateUrl( - string protocol, - string host, - VirtualPathData pathData, - string fragment, - out string url) - { - var pathBase = HttpContext.Request.PathBase; - url = null; - - if (string.IsNullOrEmpty(protocol) - && string.IsNullOrEmpty(host) - && string.IsNullOrEmpty(fragment) - && !pathBase.HasValue) - { - if (pathData.VirtualPath.Length == 0) - { - url = "/"; - return true; - } - else if (pathData.VirtualPath.StartsWith("/", StringComparison.Ordinal)) - { - url = pathData.VirtualPath; - return true; - } - } - - return false; + return GenerateUrl(protocol, host, pathData?.VirtualPath, fragment); } } } \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Routing/UrlHelperBase.cs b/src/Microsoft.AspNetCore.Mvc.Core/Routing/UrlHelperBase.cs new file mode 100644 index 0000000000..730323f33b --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.Core/Routing/UrlHelperBase.cs @@ -0,0 +1,293 @@ +// 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; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing; + +namespace Microsoft.AspNetCore.Mvc.Routing +{ + public abstract class UrlHelperBase : IUrlHelper + { + // Perf: Share the StringBuilder object across multiple calls of GenerateURL for this UrlHelper + private StringBuilder _stringBuilder; + + // Perf: Reuse the RouteValueDictionary across multiple calls of Action for this UrlHelper + private readonly RouteValueDictionary _routeValueDictionary; + + protected UrlHelperBase(ActionContext actionContext) + { + if (actionContext == null) + { + throw new ArgumentNullException(nameof(actionContext)); + } + + ActionContext = actionContext; + AmbientValues = actionContext.RouteData.Values; + _routeValueDictionary = new RouteValueDictionary(); + } + + /// + /// Gets the associated with the current request. + /// + protected RouteValueDictionary AmbientValues { get; } + + /// + public ActionContext ActionContext { get; } + + /// + public virtual bool IsLocalUrl(string url) + { + if (string.IsNullOrEmpty(url)) + { + return false; + } + + // Allows "/" or "/foo" but not "//" or "/\". + if (url[0] == '/') + { + // url is exactly "/" + if (url.Length == 1) + { + return true; + } + + // url doesn't start with "//" or "/\" + if (url[1] != '/' && url[1] != '\\') + { + return true; + } + + return false; + } + + // Allows "~/" or "~/foo" but not "~//" or "~/\". + if (url[0] == '~' && url.Length > 1 && url[1] == '/') + { + // url is exactly "~/" + if (url.Length == 2) + { + return true; + } + + // url doesn't start with "~//" or "~/\" + if (url[2] != '/' && url[2] != '\\') + { + return true; + } + + return false; + } + + return false; + } + + /// + public virtual string Content(string contentPath) + { + if (string.IsNullOrEmpty(contentPath)) + { + return null; + } + else if (contentPath[0] == '~') + { + var segment = new PathString(contentPath.Substring(1)); + var applicationPath = ActionContext.HttpContext.Request.PathBase; + + return applicationPath.Add(segment).Value; + } + + return contentPath; + } + + /// + public virtual string Link(string routeName, object values) + { + return RouteUrl(new UrlRouteContext() + { + RouteName = routeName, + Values = values, + Protocol = ActionContext.HttpContext.Request.Scheme, + Host = ActionContext.HttpContext.Request.Host.ToUriComponent() + }); + } + + /// + public abstract string Action(UrlActionContext actionContext); + + /// + public abstract string RouteUrl(UrlRouteContext routeContext); + + protected RouteValueDictionary GetValuesDictionary(object values) + { + // Perf: RouteValueDictionary can be cast to IDictionary, but it is + // special cased to avoid allocating boxed Enumerator. + if (values is RouteValueDictionary routeValuesDictionary) + { + _routeValueDictionary.Clear(); + foreach (var kvp in routeValuesDictionary) + { + _routeValueDictionary.Add(kvp.Key, kvp.Value); + } + + return _routeValueDictionary; + } + + if (values is IDictionary dictionaryValues) + { + _routeValueDictionary.Clear(); + foreach (var kvp in dictionaryValues) + { + _routeValueDictionary.Add(kvp.Key, kvp.Value); + } + + return _routeValueDictionary; + } + + return new RouteValueDictionary(values); + } + + protected string GenerateUrl(string protocol, string host, string virtualPath, string fragment) + { + if (virtualPath == null) + { + return null; + } + + // Perf: In most of the common cases, GenerateUrl is called with a null protocol, host and fragment. + // In such cases, we might not need to build any URL as the url generated is mostly same as the virtual path available in pathData. + // For such common cases, this FastGenerateUrl method saves a string allocation per GenerateUrl call. + string url; + if (TryFastGenerateUrl(protocol, host, virtualPath, fragment, out url)) + { + return url; + } + + var builder = GetStringBuilder(); + try + { + var pathBase = ActionContext.HttpContext.Request.PathBase; + + if (string.IsNullOrEmpty(protocol) && string.IsNullOrEmpty(host)) + { + AppendPathAndFragment(builder, pathBase, virtualPath, fragment); + // We're returning a partial URL (just path + query + fragment), but we still want it to be rooted. + if (builder.Length == 0 || builder[0] != '/') + { + builder.Insert(0, '/'); + } + } + else + { + protocol = string.IsNullOrEmpty(protocol) ? "http" : protocol; + builder.Append(protocol); + + builder.Append("://"); + + host = string.IsNullOrEmpty(host) ? ActionContext.HttpContext.Request.Host.Value : host; + builder.Append(host); + AppendPathAndFragment(builder, pathBase, virtualPath, fragment); + } + + var path = builder.ToString(); + return path; + } + finally + { + // Clear the StringBuilder so that it can reused for the next call. + builder.Clear(); + } + } + + // for unit testing + internal static void AppendPathAndFragment(StringBuilder builder, PathString pathBase, string virtualPath, string fragment) + { + if (!pathBase.HasValue) + { + if (virtualPath.Length == 0) + { + builder.Append("/"); + } + else + { + if (!virtualPath.StartsWith("/", StringComparison.Ordinal)) + { + builder.Append("/"); + } + + builder.Append(virtualPath); + } + } + else + { + if (virtualPath.Length == 0) + { + builder.Append(pathBase.Value); + } + else + { + builder.Append(pathBase.Value); + + if (pathBase.Value.EndsWith("/", StringComparison.Ordinal)) + { + builder.Length--; + } + + if (!virtualPath.StartsWith("/", StringComparison.Ordinal)) + { + builder.Append("/"); + } + + builder.Append(virtualPath); + } + } + + if (!string.IsNullOrEmpty(fragment)) + { + builder.Append("#").Append(fragment); + } + } + + private bool TryFastGenerateUrl( + string protocol, + string host, + string virtualPath, + string fragment, + out string url) + { + var pathBase = ActionContext.HttpContext.Request.PathBase; + url = null; + + if (string.IsNullOrEmpty(protocol) + && string.IsNullOrEmpty(host) + && string.IsNullOrEmpty(fragment) + && !pathBase.HasValue) + { + if (virtualPath.Length == 0) + { + url = "/"; + return true; + } + else if (virtualPath.StartsWith("/", StringComparison.Ordinal)) + { + url = virtualPath; + return true; + } + } + + return false; + } + + private StringBuilder GetStringBuilder() + { + if (_stringBuilder == null) + { + _stringBuilder = new StringBuilder(); + } + + return _stringBuilder; + } + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Routing/UrlHelperFactory.cs b/src/Microsoft.AspNetCore.Mvc.Core/Routing/UrlHelperFactory.cs index f9b8006850..ba0f7e9607 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/Routing/UrlHelperFactory.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/Routing/UrlHelperFactory.cs @@ -4,6 +4,9 @@ using System; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.Core; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; namespace Microsoft.AspNetCore.Mvc.Routing { @@ -37,16 +40,27 @@ namespace Microsoft.AspNetCore.Mvc.Routing } // Perf: Create only one UrlHelper per context - object value; - if (httpContext.Items.TryGetValue(typeof(IUrlHelper), out value) && value is IUrlHelper) + if (httpContext.Items.TryGetValue(typeof(IUrlHelper), out var value) && value is IUrlHelper) { return (IUrlHelper)value; } - var urlHelper = new UrlHelper(context); + IUrlHelper urlHelper; + var endpointFeature = httpContext.Features.Get(); + if (endpointFeature?.Endpoint != null) + { + var linkGenerator = httpContext.RequestServices.GetRequiredService(); + var logger = httpContext.RequestServices.GetRequiredService>(); + urlHelper = new DispatcherUrlHelper(context, linkGenerator, logger); + } + else + { + urlHelper = new UrlHelper(context); + } + httpContext.Items[typeof(IUrlHelper)] = urlHelper; return urlHelper; } } -} +} \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/LocalRedirectResultTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/LocalRedirectResultTest.cs index ebbbac82a7..6e78d6f840 100644 --- a/test/Microsoft.AspNetCore.Mvc.Core.Test/LocalRedirectResultTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/LocalRedirectResultTest.cs @@ -4,6 +4,7 @@ using System; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Http.Internal; using Microsoft.AspNetCore.Mvc.Abstractions; using Microsoft.AspNetCore.Mvc.Infrastructure; @@ -168,13 +169,15 @@ namespace Microsoft.AspNetCore.Mvc var serviceProvider = GetServiceProvider(); httpContext.Setup(o => o.Response) - .Returns(response); + .Returns(response); httpContext.SetupGet(o => o.RequestServices) - .Returns(serviceProvider); + .Returns(serviceProvider); httpContext.SetupGet(o => o.Items) - .Returns(new ItemsDictionary()); + .Returns(new ItemsDictionary()); httpContext.Setup(o => o.Request.PathBase) - .Returns(new PathString(appRoot)); + .Returns(new PathString(appRoot)); + httpContext.SetupGet(h => h.Features) + .Returns(new FeatureCollection()); return httpContext.Object; } diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/RedirectResultTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/RedirectResultTest.cs index dc37c52ae7..61d583b998 100644 --- a/test/Microsoft.AspNetCore.Mvc.Core.Test/RedirectResultTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/RedirectResultTest.cs @@ -4,10 +4,10 @@ using System; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Http.Internal; using Microsoft.AspNetCore.Mvc.Abstractions; using Microsoft.AspNetCore.Mvc.Infrastructure; -using Microsoft.AspNetCore.Mvc.Internal; using Microsoft.AspNetCore.Mvc.Routing; using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.DependencyInjection; @@ -146,13 +146,15 @@ namespace Microsoft.AspNetCore.Mvc var serviceProvider = GetServiceProvider(); httpContext.Setup(o => o.Response) - .Returns(response); + .Returns(response); httpContext.SetupGet(o => o.RequestServices) - .Returns(serviceProvider); + .Returns(serviceProvider); httpContext.SetupGet(o => o.Items) - .Returns(new ItemsDictionary()); + .Returns(new ItemsDictionary()); httpContext.Setup(o => o.Request.PathBase) - .Returns(new PathString(appRoot)); + .Returns(new PathString(appRoot)); + httpContext.SetupGet(h => h.Features) + .Returns(new FeatureCollection()); return httpContext.Object; } diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/Routing/DispatcherUrlHelperTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/Routing/DispatcherUrlHelperTest.cs new file mode 100644 index 0000000000..6c42555191 --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/Routing/DispatcherUrlHelperTest.cs @@ -0,0 +1,150 @@ +// 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.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Routing; +using Microsoft.AspNetCore.Routing.Matchers; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Xunit; + +namespace Microsoft.AspNetCore.Mvc.Routing +{ + public class DispatcherUrlHelperTest : UrlHelperTestBase + { + protected override IUrlHelper CreateUrlHelper(string appRoot, string host, string protocol) + { + return CreateUrlHelper(Enumerable.Empty(), appRoot, host, protocol); + } + + protected override IUrlHelper CreateUrlHelperWithDefaultRoutes(string appRoot, string host, string protocol) + { + return CreateUrlHelper(GetDefaultEndpoints(), appRoot, host, protocol); + } + + protected override IUrlHelper CreateUrlHelperWithDefaultRoutes( + string appRoot, + string host, + string protocol, + string routeName, + string template) + { + var endpoints = GetDefaultEndpoints(); + endpoints.Add(new MatcherEndpoint( + next => httpContext => Task.CompletedTask, + template, + null, + 0, + EndpointMetadataCollection.Empty, + null, + new Address(routeName) + )); + return CreateUrlHelper(endpoints, appRoot, host, protocol); + } + + protected override IUrlHelper CreateUrlHelper(ActionContext actionContext) + { + var httpContext = actionContext.HttpContext; + httpContext.Features.Set(new EndpointFeature() + { + Endpoint = new MatcherEndpoint( + next => cntxt => Task.CompletedTask, + "/", + new { }, + 0, + EndpointMetadataCollection.Empty, + null, + null) + }); + + var urlHelperFactory = httpContext.RequestServices.GetRequiredService(); + var urlHelper = urlHelperFactory.GetUrlHelper(actionContext); + Assert.IsType(urlHelper); + return urlHelper; + } + + protected override IServiceProvider CreateServices() + { + return CreateServices(Enumerable.Empty()); + } + + protected override IUrlHelper CreateUrlHelper( + string appRoot, + string host, + string protocol, + string routeName, + string template, + object defaults) + { + var endpoint = GetEndpoint(routeName, template, defaults); + var services = CreateServices(new[] { endpoint }); + var httpContext = CreateHttpContext(services, appRoot: "", host: null, protocol: null); + var actionContext = CreateActionContext(httpContext); + return CreateUrlHelper(actionContext); + } + + private IUrlHelper CreateUrlHelper( + IEnumerable endpoints, + string appRoot, + string host, + string protocol) + { + var serviceProvider = CreateServices(endpoints); + var httpContext = CreateHttpContext(serviceProvider, appRoot, host, protocol); + var actionContext = CreateActionContext(httpContext); + return CreateUrlHelper(actionContext); + } + + private List GetDefaultEndpoints() + { + var endpoints = new List(); + endpoints.Add(new MatcherEndpoint( + next => (httpContext) => Task.CompletedTask, + "{controller}/{action}/{id}", + new { id = "defaultid" }, + 0, + EndpointMetadataCollection.Empty, + "RouteWithNoName", + address: null)); + endpoints.Add(new MatcherEndpoint( + next => (httpContext) => Task.CompletedTask, + "named/{controller}/{action}/{id}", + new { id = "defaultid" }, + 0, + EndpointMetadataCollection.Empty, + "RouteWithNoName", + new Address("namedroute"))); + return endpoints; + } + + private IServiceProvider CreateServices(IEnumerable endpoints) + { + if (endpoints == null) + { + endpoints = Enumerable.Empty(); + } + + var services = GetCommonServices(); + services.AddDispatcher(); + services.TryAddEnumerable( + ServiceDescriptor.Singleton(new DefaultEndpointDataSource(endpoints))); + services.TryAddSingleton(); + return services.BuildServiceProvider(); + } + + private MatcherEndpoint GetEndpoint(string name, string template, object defaults) + { + return new MatcherEndpoint( + next => c => Task.CompletedTask, + template, + defaults, + 0, + EndpointMetadataCollection.Empty, + null, + new Address(name)); + } + } +} diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/Routing/UrlHelperBaseTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/Routing/UrlHelperBaseTest.cs new file mode 100644 index 0000000000..9bb3f54043 --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/Routing/UrlHelperBaseTest.cs @@ -0,0 +1,171 @@ +// 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.Text; +using System.Text.Encodings.Web; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc.Abstractions; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.ObjectPool; +using Xunit; + +namespace Microsoft.AspNetCore.Mvc.Routing +{ + public class UrlHelperBaseTest + { + public static TheoryData GeneratePathFromRoute_HandlesLeadingAndTrailingSlashesData => + new TheoryData + { + { null, "", "/" }, + { null, "/", "/" }, + { null, "Hello", "/Hello" }, + { null, "/Hello", "/Hello" }, + { "/", "", "/" }, + { "/", "hello", "/hello" }, + { "/", "/hello", "/hello" }, + { "/hello", "", "/hello" }, + { "/hello/", "", "/hello/" }, + { "/hello", "/", "/hello/" }, + { "/hello/", "world", "/hello/world" }, + { "/hello/", "/world", "/hello/world" }, + { "/hello/", "/world 123", "/hello/world 123" }, + { "/hello/", "/world%20123", "/hello/world%20123" }, + }; + + [Theory] + [MemberData(nameof(GeneratePathFromRoute_HandlesLeadingAndTrailingSlashesData))] + public void AppendPathAndFragment_HandlesLeadingAndTrailingSlashes( + string appBase, + string virtualPath, + string expected) + { + // Arrange + var services = CreateServices(); + var httpContext = CreateHttpContext(services, appBase, host: null, protocol: null); + var builder = new StringBuilder(); + + // Act + UrlHelperBase.AppendPathAndFragment(builder, httpContext.Request.PathBase, virtualPath, string.Empty); + + // Assert + Assert.Equal(expected, builder.ToString()); + } + + [Theory] + [MemberData(nameof(GeneratePathFromRoute_HandlesLeadingAndTrailingSlashesData))] + public void AppendPathAndFragment_AppendsFragments( + string appBase, + string virtualPath, + string expected) + { + // Arrange + var fragmentValue = "fragment-value"; + expected += $"#{fragmentValue}"; + var services = CreateServices(); + var httpContext = CreateHttpContext(services, appBase, host: null, protocol: null); + var builder = new StringBuilder(); + + // Act + UrlHelperBase.AppendPathAndFragment(builder, httpContext.Request.PathBase, virtualPath, fragmentValue); + + // Assert + Assert.Equal(expected, builder.ToString()); + } + + [Theory] + [InlineData(null, null, null, "/", null, "/")] + [InlineData(null, null, null, "/Hello", null, "/Hello")] + [InlineData(null, null, null, "Hello", null, "/Hello")] + [InlineData("/", null, null, "", null, "/")] + [InlineData("/hello/", null, null, "/world", null, "/hello/world")] + [InlineData("/hello/", "https", "myhost", "/world", "fragment-value", "https://myhost/hello/world#fragment-value")] + public void GenerateUrl_FastAndSlowPathsReturnsExpected( + string appBase, + string protocol, + string host, + string virtualPath, + string fragment, + string expected) + { + // Arrange + var services = CreateServices(); + var httpContext = CreateHttpContext(services, appBase, host, protocol); + var actionContext = CreateActionContext(httpContext); + var urlHelper = new TestUrlHelper(actionContext); + + // Act + var url = urlHelper.GenerateUrl(protocol, host, virtualPath, fragment); + + // Assert + Assert.Equal(expected, url); + } + + private static IServiceProvider CreateServices() + { + var services = new ServiceCollection(); + services.AddOptions(); + services.AddLogging(); + services.AddRouting(); + services + .AddSingleton() + .AddSingleton(UrlEncoder.Default); + + return services.BuildServiceProvider(); + } + + private static 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; + } + + private static ActionContext CreateActionContext(HttpContext context) + { + return new ActionContext(context, new RouteData(), new ActionDescriptor()); + } + + private class TestUrlHelper : UrlHelperBase + { + public TestUrlHelper(ActionContext actionContext) : + base(actionContext) + { + } + + public override string Action(UrlActionContext actionContext) + { + throw new NotImplementedException(); + } + + public override string RouteUrl(UrlRouteContext routeContext) + { + throw new NotImplementedException(); + } + + public new string GenerateUrl( + string protocol, + string host, + string virtualPath, + string fragment) + { + return base.GenerateUrl( + protocol, + host, + virtualPath, + fragment); + } + } + } +} diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/Routing/UrlHelperExtensionsTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/Routing/UrlHelperExtensionsTest.cs new file mode 100644 index 0000000000..c6e00c231e --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/Routing/UrlHelperExtensionsTest.cs @@ -0,0 +1,585 @@ +// 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.Linq; +using Microsoft.AspNetCore.Mvc.Abstractions; +using Microsoft.AspNetCore.Mvc.Routing; +using Microsoft.AspNetCore.Routing; +using Moq; +using Xunit; + +namespace Microsoft.AspNetCore.Mvc.Core.Test.Routing +{ + public class UrlHelperExtensionsTest + { + [Fact] + public void Page_WithName_Works() + { + // Arrange + UrlRouteContext actual = null; + var routeData = new RouteData + { + Values = + { + { "page", "ambient-page" }, + } + }; + var actionContext = new ActionContext + { + RouteData = routeData, + }; + var urlHelper = CreateMockUrlHelper(actionContext); + urlHelper.Setup(h => h.RouteUrl(It.IsAny())) + .Callback((UrlRouteContext context) => actual = context); + + // Act + urlHelper.Object.Page("/TestPage"); + + // Assert + urlHelper.Verify(); + Assert.NotNull(actual); + Assert.Null(actual.RouteName); + Assert.Collection(Assert.IsType(actual.Values), + value => + { + Assert.Equal("page", value.Key); + Assert.Equal("/TestPage", value.Value); + }); + Assert.Null(actual.Host); + Assert.Null(actual.Protocol); + Assert.Null(actual.Fragment); + } + + public static TheoryData Page_WithNameAndRouteValues_WorksData + { + get => new TheoryData + { + { new { id = 10 } }, + { + new Dictionary + { + ["id"] = 10, + } + }, + { + new RouteValueDictionary + { + ["id"] = 10, + } + }, + }; + } + + [Theory] + [MemberData(nameof(Page_WithNameAndRouteValues_WorksData))] + public void Page_WithNameAndRouteValues_Works(object values) + { + // Arrange + UrlRouteContext actual = null; + var urlHelper = CreateMockUrlHelper(); + urlHelper.Setup(h => h.RouteUrl(It.IsAny())) + .Callback((UrlRouteContext context) => actual = context); + + // Act + urlHelper.Object.Page("/TestPage", values); + + // Assert + urlHelper.Verify(); + Assert.NotNull(actual); + Assert.Null(actual.RouteName); + Assert.Collection(Assert.IsType(actual.Values), + value => + { + Assert.Equal("id", value.Key); + Assert.Equal(10, value.Value); + }, + value => + { + Assert.Equal("page", value.Key); + Assert.Equal("/TestPage", value.Value); + }); + Assert.Null(actual.Host); + Assert.Null(actual.Protocol); + Assert.Null(actual.Fragment); + } + + [Fact] + public void Page_WithNameRouteValuesAndProtocol_Works() + { + // Arrange + UrlRouteContext actual = null; + var urlHelper = CreateMockUrlHelper(); + urlHelper.Setup(h => h.RouteUrl(It.IsAny())) + .Callback((UrlRouteContext context) => actual = context); + + // Act + urlHelper.Object.Page("/TestPage", pageHandler: null, values: new { id = 13 }, protocol: "https"); + + // Assert + urlHelper.Verify(); + Assert.NotNull(actual); + Assert.Null(actual.RouteName); + Assert.Collection(Assert.IsType(actual.Values), + value => + { + Assert.Equal("id", value.Key); + Assert.Equal(13, value.Value); + }, + value => + { + Assert.Equal("page", value.Key); + Assert.Equal("/TestPage", value.Value); + }); + Assert.Equal("https", actual.Protocol); + Assert.Null(actual.Host); + Assert.Null(actual.Fragment); + } + + [Fact] + public void Page_WithNameRouteValuesProtocolAndHost_Works() + { + // Arrange + UrlRouteContext actual = null; + var urlHelper = CreateMockUrlHelper(); + urlHelper.Setup(h => h.RouteUrl(It.IsAny())) + .Callback((UrlRouteContext context) => actual = context); + + // Act + urlHelper.Object.Page("/TestPage", pageHandler: null, values: new { id = 13 }, protocol: "https", host: "mytesthost"); + + // Assert + urlHelper.Verify(); + Assert.NotNull(actual); + Assert.Null(actual.RouteName); + Assert.Collection(Assert.IsType(actual.Values), + value => + { + Assert.Equal("id", value.Key); + Assert.Equal(13, value.Value); + }, + value => + { + Assert.Equal("page", value.Key); + Assert.Equal("/TestPage", value.Value); + }); + Assert.Equal("https", actual.Protocol); + Assert.Equal("mytesthost", actual.Host); + Assert.Null(actual.Fragment); + } + + [Fact] + public void Page_WithNameRouteValuesProtocolHostAndFragment_Works() + { + // Arrange + UrlRouteContext actual = null; + var urlHelper = CreateMockUrlHelper(); + urlHelper.Setup(h => h.RouteUrl(It.IsAny())) + .Callback((UrlRouteContext context) => actual = context); + + // Act + urlHelper.Object.Page("/TestPage", "test-handler", new { id = 13 }, "https", "mytesthost", "#toc"); + + // Assert + urlHelper.Verify(); + Assert.NotNull(actual); + Assert.Null(actual.RouteName); + Assert.Collection(Assert.IsType(actual.Values), + value => + { + Assert.Equal("id", value.Key); + Assert.Equal(13, value.Value); + }, + value => + { + Assert.Equal("page", value.Key); + Assert.Equal("/TestPage", value.Value); + }, + value => + { + Assert.Equal("handler", value.Key); + Assert.Equal("test-handler", value.Value); + }); + Assert.Equal("https", actual.Protocol); + Assert.Equal("mytesthost", actual.Host); + Assert.Equal("#toc", actual.Fragment); + } + + [Fact] + public void Page_UsesAmbientRouteValue_WhenPageIsNull() + { + // Arrange + UrlRouteContext actual = null; + var routeData = new RouteData + { + Values = + { + { "page", "ambient-page" }, + } + }; + var actionContext = new ActionContext + { + RouteData = routeData, + }; + + var urlHelper = CreateMockUrlHelper(actionContext); + urlHelper.Setup(h => h.RouteUrl(It.IsAny())) + .Callback((UrlRouteContext context) => actual = context); + + // Act + string page = null; + urlHelper.Object.Page(page, new { id = 13 }); + + // Assert + urlHelper.Verify(); + Assert.NotNull(actual); + Assert.Null(actual.RouteName); + Assert.Collection(Assert.IsType(actual.Values), + value => + { + Assert.Equal("id", value.Key); + Assert.Equal(13, value.Value); + }, + value => + { + Assert.Equal("page", value.Key); + Assert.Equal("ambient-page", value.Value); + }); + } + + [Fact] + public void Page_SetsHandlerToNull_IfValueIsNotSpecifiedInRouteValues() + { + // Arrange + UrlRouteContext actual = null; + var routeData = new RouteData + { + Values = + { + { "page", "ambient-page" }, + { "handler", "ambient-handler" }, + } + }; + var actionContext = new ActionContext + { + RouteData = routeData, + }; + + var urlHelper = CreateMockUrlHelper(actionContext); + urlHelper.Setup(h => h.RouteUrl(It.IsAny())) + .Callback((UrlRouteContext context) => actual = context); + + // Act + string page = null; + urlHelper.Object.Page(page, new { id = 13 }); + + // Assert + urlHelper.Verify(); + Assert.NotNull(actual); + Assert.Null(actual.RouteName); + Assert.Collection(Assert.IsType(actual.Values), + value => + { + Assert.Equal("id", value.Key); + Assert.Equal(13, value.Value); + }, + value => + { + Assert.Equal("page", value.Key); + Assert.Equal("ambient-page", value.Value); + }, + value => + { + Assert.Equal("handler", value.Key); + Assert.Null(value.Value); + }); + } + + [Fact] + public void Page_UsesExplicitlySpecifiedHandlerValue() + { + // Arrange + UrlRouteContext actual = null; + var routeData = new RouteData + { + Values = + { + { "page", "ambient-page" }, + { "handler", "ambient-handler" }, + } + }; + var actionContext = new ActionContext + { + RouteData = routeData, + }; + + var urlHelper = CreateMockUrlHelper(actionContext); + urlHelper.Setup(h => h.RouteUrl(It.IsAny())) + .Callback((UrlRouteContext context) => actual = context); + + // Act + string page = null; + urlHelper.Object.Page(page, "exact-handler", new { handler = "route-value-handler" }); + + // Assert + urlHelper.Verify(); + Assert.NotNull(actual); + Assert.Null(actual.RouteName); + Assert.Collection(Assert.IsType(actual.Values), + value => + { + Assert.Equal("handler", value.Key); + Assert.Equal("exact-handler", value.Value); + }, + value => + { + Assert.Equal("page", value.Key); + Assert.Equal("ambient-page", value.Value); + }); + } + + [Fact] + public void Page_UsesValueFromRouteValueIfPageHandlerIsNotExplicitySpecified() + { + // Arrange + UrlRouteContext actual = null; + var routeData = new RouteData + { + Values = + { + { "page", "ambient-page" }, + { "handler", "ambient-handler" }, + } + }; + var actionContext = new ActionContext + { + RouteData = routeData, + }; + + var urlHelper = CreateMockUrlHelper(actionContext); + urlHelper.Setup(h => h.RouteUrl(It.IsAny())) + .Callback((UrlRouteContext context) => actual = context); + + // Act + string page = null; + urlHelper.Object.Page(page, pageHandler: null, values: new { handler = "route-value-handler" }); + + // Assert + urlHelper.Verify(); + Assert.NotNull(actual); + Assert.Null(actual.RouteName); + Assert.Collection(Assert.IsType(actual.Values), + value => + { + Assert.Equal("handler", value.Key); + Assert.Equal("route-value-handler", value.Value); + }, + value => + { + Assert.Equal("page", value.Key); + Assert.Equal("ambient-page", value.Value); + }); + } + + [Theory] + [InlineData("Sibling", "/Dir1/Dir2/Sibling")] + [InlineData("Dir3/Sibling", "/Dir1/Dir2/Dir3/Sibling")] + [InlineData("Dir4/Dir5/Index", "/Dir1/Dir2/Dir4/Dir5/Index")] + public void Page_CalculatesPathRelativeToViewEnginePath_WhenNotRooted(string pageName, string expected) + { + // Arrange + UrlRouteContext actual = null; + var routeData = new RouteData(); + var actionContext = GetActionContextForPage("/Dir1/Dir2/About"); + + var urlHelper = CreateMockUrlHelper(actionContext); + urlHelper.Setup(h => h.RouteUrl(It.IsAny())) + .Callback((UrlRouteContext context) => actual = context); + + // Act + urlHelper.Object.Page(pageName); + + // Assert + urlHelper.Verify(); + Assert.NotNull(actual); + Assert.Null(actual.RouteName); + Assert.Collection(Assert.IsType(actual.Values), + value => + { + Assert.Equal("page", value.Key); + Assert.Equal(expected, value.Value); + }); + } + + [Fact] + public void Page_CalculatesPathRelativeToViewEnginePath_ForIndexPagePaths() + { + // Arrange + var expected = "/Dir1/Dir2/Sibling"; + UrlRouteContext actual = null; + var actionContext = GetActionContextForPage("/Dir1/Dir2/"); + + var urlHelper = CreateMockUrlHelper(actionContext); + urlHelper.Setup(h => h.RouteUrl(It.IsAny())) + .Callback((UrlRouteContext context) => actual = context); + + // Act + urlHelper.Object.Page("Sibling"); + + // Assert + urlHelper.Verify(); + Assert.NotNull(actual); + Assert.Null(actual.RouteName); + Assert.Collection(Assert.IsType(actual.Values), + value => + { + Assert.Equal("page", value.Key); + Assert.Equal(expected, value.Value); + }); + } + + [Fact] + public void Page_CalculatesPathRelativeToViewEnginePath_WhenNotRooted_ForPageAtRoot() + { + // Arrange + var expected = "/SiblingName"; + UrlRouteContext actual = null; + var routeData = new RouteData(); + var actionContext = new ActionContext + { + ActionDescriptor = new ActionDescriptor + { + RouteValues = new Dictionary + { + { "page", "/Home" }, + }, + }, + RouteData = new RouteData + { + Values = + { + [ "page" ] = "/Home" + }, + }, + }; + + var urlHelper = CreateMockUrlHelper(actionContext); + urlHelper.Setup(h => h.RouteUrl(It.IsAny())) + .Callback((UrlRouteContext context) => actual = context); + + // Act + urlHelper.Object.Page("SiblingName"); + + // Assert + urlHelper.Verify(); + Assert.NotNull(actual); + Assert.Null(actual.RouteName); + Assert.Collection(Assert.IsType(actual.Values), + value => + { + Assert.Equal("page", value.Key); + Assert.Equal(expected, value.Value); + }); + } + + [Fact] + public void Page_Throws_IfRouteValueDoesNotIncludePageKey() + { + // Arrange + var expected = "SiblingName"; + UrlRouteContext actual = null; + var routeData = new RouteData(); + var actionContext = new ActionContext + { + RouteData = new RouteData(), + }; + + var urlHelper = CreateMockUrlHelper(actionContext); + urlHelper.Setup(h => h.RouteUrl(It.IsAny())) + .Callback((UrlRouteContext context) => actual = context); + + // Act & Assert + var ex = Assert.Throws(() => urlHelper.Object.Page(expected)); + Assert.Equal($"The relative page path '{expected}' can only be used while executing a Razor Page. " + + "Specify a root relative path with a leading '/' to generate a URL outside of a Razor Page.", ex.Message); + } + + [Fact] + public void Page_UsesAreaValueFromRouteValueIfSpecified() + { + // Arrange + UrlRouteContext actual = null; + var routeData = new RouteData + { + Values = + { + { "page", "ambient-page" }, + { "area", "ambient-area" }, + } + }; + var actionContext = new ActionContext + { + RouteData = routeData, + }; + + var urlHelper = CreateMockUrlHelper(actionContext); + urlHelper.Setup(h => h.RouteUrl(It.IsAny())) + .Callback((UrlRouteContext context) => actual = context); + + // Act + string page = null; + urlHelper.Object.Page(page, values: new { area = "specified-area" }); + + // Assert + urlHelper.Verify(); + Assert.NotNull(actual); + Assert.Null(actual.RouteName); + Assert.Collection(Assert.IsType(actual.Values).OrderBy(v => v.Key), + value => + { + Assert.Equal("area", value.Key); + Assert.Equal("specified-area", value.Value); + }, + value => + { + Assert.Equal("page", value.Key); + Assert.Equal("ambient-page", value.Value); + }); + } + + private static Mock CreateMockUrlHelper(ActionContext context = null) + { + if (context == null) + { + context = GetActionContextForPage("/Page"); + } + + var urlHelper = new Mock(); + urlHelper.SetupGet(h => h.ActionContext) + .Returns(context); + return urlHelper; + } + + private static ActionContext GetActionContextForPage(string page) + { + return new ActionContext + { + ActionDescriptor = new ActionDescriptor + { + RouteValues = new Dictionary + { + { "page", page }, + }, + }, + RouteData = new RouteData + { + Values = + { + [ "page" ] = page + }, + }, + }; + } + } +} diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/Routing/UrlHelperTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/Routing/UrlHelperTest.cs index b2754f56d3..63c88896ab 100644 --- a/test/Microsoft.AspNetCore.Mvc.Core.Test/Routing/UrlHelperTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/Routing/UrlHelperTest.cs @@ -2,1729 +2,93 @@ // 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.Linq; using System.Text; -using System.Text.Encodings.Web; using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Http; -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 class UrlHelperTest + public class UrlHelperTest : 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) + protected override IServiceProvider CreateServices() { - // Arrange - var httpContext = CreateHttpContext(CreateServices(), appRoot); - var actionContext = CreateActionContext(httpContext); - var urlHelper = CreateUrlHelper(actionContext); - - // 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 httpContext = CreateHttpContext(CreateServices(), appRoot); - var actionContext = CreateActionContext(httpContext); - var urlHelper = CreateUrlHelper(actionContext); - - // 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("www.mysite.com"); - - // 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("www.mysite.com"); - - // 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("www.mysite.com"); - - // 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("www.mysite.com"); - - // 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("www.mysite.com"); - - // 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("www.mysite.com"); - - // 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("www.mysite.com"); - - // 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("www.mysite.com"); - - // Act - var result = helper.IsLocalUrl(url); - - // Assert - Assert.False(result); - } - - [Theory] - [InlineData("~/\\")] - [InlineData("~/\\foo")] - public void IsLocalUrl_RejectsInvalidTokenUrls(string url) - { - // Arrange - var helper = CreateUrlHelper("www.mysite.com"); - - // Act - var result = helper.IsLocalUrl(url); - - // Assert - Assert.False(result); - } - - [Fact] - public void RouteUrlWithDictionary() - { - // Arrange - var services = CreateServices(); - var urlHelper = CreateUrlHelperWithRouteCollection(services, "/app"); - - // 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 services = CreateServices(); - var urlHelper = CreateUrlHelperWithRouteCollection(services, "/app"); - - // 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 services = CreateServices(); - var urlHelper = CreateUrlHelperWithRouteCollection(services, "/app"); - - // 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 services = CreateServices(); - var urlHelper = CreateUrlHelperWithRouteCollection(services, "/app"); - - // 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 services = CreateServices(); - var urlHelper = CreateUrlHelperWithRouteCollection(services, "/app"); - - // 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 services = CreateServices(); - var urlHelper = CreateUrlHelperWithRouteCollection(services, "/app"); - - // 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 services = CreateServices(); - var urlHelper = CreateUrlHelperWithRouteCollection(services, "/app"); - - // 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 services = CreateServices(); - var urlHelper = CreateUrlHelperWithRouteCollection(services, "/app"); - - // 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 RouteUrlWithRouteNameAndDefaults() - { - // Arrange - var services = CreateServices(); - var routeCollection = GetRouter(services, "MyRouteName", "any/url"); - var urlHelper = CreateUrlHelper("/app", routeCollection); - - // Act - var url = urlHelper.RouteUrl("MyRouteName"); - - // Assert - Assert.Equal("/app/any/url", url); - } - - [Fact] - public void RouteUrlWithRouteNameAndDictionary() - { - // Arrange - var services = CreateServices(); - var urlHelper = CreateUrlHelperWithRouteCollection(services, "/app"); - - // 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 services = CreateServices(); - var urlHelper = CreateUrlHelperWithRouteCollection(services, "/app"); - - // 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 services = CreateServices(); - var urlHelper = CreateUrlHelperWithRouteCollection(services, "/app"); - - 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 services = CreateServices(); - var urlHelper = CreateUrlHelperWithRouteCollection(services, "/app"); - - // 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 services = CreateServices(); - var urlHelper = CreateUrlHelperWithRouteCollection(services, "/app"); - - // 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 services = CreateServices(); - var urlHelper = CreateUrlHelperWithRouteCollection(services, "/app"); - - // 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 services = CreateServices(); - var urlHelper = CreateUrlHelperWithRouteCollection(services, "/app"); - - // 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 services = CreateServices(); - var urlHelper = CreateUrlHelperWithRouteCollection(services, "/app"); - - 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 services = CreateServices(); - var urlHelper = CreateUrlHelperWithRouteCollection(services, "/app"); - - // 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 services = CreateServices(); - var urlHelper = CreateUrlHelperWithRouteCollection(services, "/app"); - - // 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 services = CreateServices(); - var urlHelper = CreateUrlHelperWithRouteCollection(services, "/app"); - - // 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 LinkWithDefaultsAndNullRouteValues_ReturnsExpectedResult() - { - // Arrange - var services = CreateServices(); - var routeCollection = GetRouter(services, "MyRouteName", "any/url"); - var urlHelper = CreateUrlHelper("/app", routeCollection); - - // Act - var url = urlHelper.Link("MyRouteName", null); - - // Assert - Assert.Equal("http://localhost/app/any/url", url); - } - - [Fact] - public void LinkWithCustomHostAndProtocol_ReturnsExpectedResult() - { - // Arrange - var services = CreateServices(); - var routeCollection = GetRouter(services, "MyRouteName", "any/url"); - var urlHelper = CreateUrlHelper("myhost", "https", routeCollection); - - // Act - var url = urlHelper.Link( - "namedroute", - new - { - Action = "newaction", - Controller = "home", - id = "someid" - }); - - // Assert - Assert.Equal("https://myhost/named/home/newaction/someid", url); - } - - // Regression test for aspnet/Mvc#2859 - [Fact] - public void Action_RouteValueInvalidation_DoesNotAffectActionAndController() - { - // Arrange - var services = CreateServices(); - var routeBuilder = CreateRouteBuilder(services); - - routeBuilder.MapRoute( - "default", - "{first}/{controller}/{action}", - new { second = "default", controller = "default", action = "default" }); - - var actionContext = new ActionContext() - { - HttpContext = new DefaultHttpContext() - { - RequestServices = services, - }, - }; - - actionContext.RouteData = new RouteData(); - actionContext.RouteData.Values.Add("first", "a"); - actionContext.RouteData.Values.Add("controller", "Store"); - actionContext.RouteData.Values.Add("action", "Buy"); - actionContext.RouteData.Routers.Add(routeBuilder.Build()); - - var urlHelper = CreateUrlHelper(actionContext); - - // 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 services = CreateServices(); - var routeBuilder = CreateRouteBuilder(services); - - routeBuilder.MapRoute( - "default", - "{first}/{second}/{controller}/{action}", - new { second = "default", controller = "default", action = "default" }); - - var actionContext = new ActionContext() - { - HttpContext = new DefaultHttpContext() - { - RequestServices = services, - }, - }; - - actionContext.RouteData = new RouteData(); - actionContext.RouteData.Values.Add("first", "a"); - actionContext.RouteData.Values.Add("second", "x"); - actionContext.RouteData.Values.Add("controller", "Store"); - actionContext.RouteData.Values.Add("action", "Buy"); - actionContext.RouteData.Routers.Add(routeBuilder.Build()); - - var urlHelper = CreateUrlHelper(actionContext); - - // 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 services = CreateServices(); - var routeBuilder = CreateRouteBuilder(services); - - routeBuilder.MapRoute( - "default", - "{first}/{controller}/{action}", - new { second = "default", controller = "default", action = "default" }); - - var actionContext = new ActionContext() - { - HttpContext = new DefaultHttpContext() - { - RequestServices = services, - }, - }; - - actionContext.RouteData = new RouteData(); - actionContext.RouteData.Values.Add("first", "a"); - actionContext.RouteData.Values.Add("controller", "Store"); - actionContext.RouteData.Values.Add("action", "Buy"); - actionContext.RouteData.Routers.Add(routeBuilder.Build()); - - var urlHelper = CreateUrlHelper(actionContext); - - // 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); - } - - public static TheoryData GeneratePathFromRoute_HandlesLeadingAndTrailingSlashesData => - new TheoryData - { - { null, "", "/" }, - { null, "/", "/" }, - { null, "Hello", "/Hello" }, - { null, "/Hello", "/Hello" }, - { "/", "", "/" }, - { "/", "hello", "/hello" }, - { "/", "/hello", "/hello" }, - { "/hello", "", "/hello" }, - { "/hello/", "", "/hello/" }, - { "/hello", "/", "/hello/" }, - { "/hello/", "world", "/hello/world" }, - { "/hello/", "/world", "/hello/world" }, - { "/hello/", "/world 123", "/hello/world 123" }, - { "/hello/", "/world%20123", "/hello/world%20123" }, - }; - - [Theory] - [MemberData(nameof(GeneratePathFromRoute_HandlesLeadingAndTrailingSlashesData))] - public void AppendPathAndFragment_HandlesLeadingAndTrailingSlashes( - string appBase, - string virtualPath, - string expected) - { - // Arrange - var router = Mock.Of(); - var pathData = new VirtualPathData(router, virtualPath) - { - VirtualPath = virtualPath - }; - var urlHelper = CreateUrlHelper(appBase, router); - var builder = new StringBuilder(); - - // Act - urlHelper.AppendPathAndFragment(builder, pathData, string.Empty); - - // Assert - Assert.Equal(expected, builder.ToString()); - } - - [Theory] - [MemberData(nameof(GeneratePathFromRoute_HandlesLeadingAndTrailingSlashesData))] - public void AppendPathAndFragment_AppendsFragments( - string appBase, - string virtualPath, - string expected) - { - // Arrange - var fragmentValue = "fragment-value"; - expected += $"#{fragmentValue}"; - var router = Mock.Of(); - var pathData = new VirtualPathData(router, virtualPath) - { - VirtualPath = virtualPath - }; - var urlHelper = CreateUrlHelper(appBase, router); - var builder = new StringBuilder(); - - // Act - urlHelper.AppendPathAndFragment(builder, pathData, fragmentValue); - - // Assert - Assert.Equal(expected, builder.ToString()); - } - - [Theory] - [InlineData(null, null, null, "/", null, "/")] - [InlineData(null, null, null, "/Hello", null, "/Hello")] - [InlineData(null, null, null, "Hello", null, "/Hello")] - [InlineData("/", null, null, "", null, "/")] - [InlineData("/hello/", null, null, "/world", null, "/hello/world")] - [InlineData("/hello/", "https", "myhost", "/world", "fragment-value", "https://myhost/hello/world#fragment-value")] - public void GenerateUrl_FastAndSlowPathsReturnsExpected( - string appBase, - string protocol, - string host, - string virtualPath, - string fragment, - string expected) - { - // Arrange - var router = Mock.Of(); - var pathData = new VirtualPathData(router, virtualPath) - { - VirtualPath = virtualPath - }; - var urlHelper = CreateUrlHelper(appBase, router); - - // Act - var url = urlHelper.GenerateUrl(protocol, host, pathData, fragment); - - // Assert - Assert.Equal(expected, url); - } - - [Fact] - public void GetUrlHelper_ReturnsSameInstance_IfAlreadyPresent() - { - // Arrange - var expectedUrlHelper = CreateUrlHelper(); - var httpContext = new Mock(); - var mockItems = new Dictionary - { - { typeof(IUrlHelper), expectedUrlHelper } - }; - httpContext.Setup(h => h.Items).Returns(mockItems); - - var actionContext = CreateActionContext(httpContext.Object, Mock.Of()); - 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.Setup(h => h.Items).Returns(new Dictionary()); - - var actionContext = CreateActionContext(httpContext.Object, Mock.Of()); - 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(); - var mockItems = new Dictionary - { - { typeof(IUrlHelper), null } - }; - httpContext.Setup(h => h.Items).Returns(mockItems); - - var actionContext = CreateActionContext(httpContext.Object, Mock.Of()); - 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 Page_WithName_Works() - { - // Arrange - UrlRouteContext actual = null; - var routeData = new RouteData - { - Values = - { - { "page", "ambient-page" }, - } - }; - var actionContext = new ActionContext - { - RouteData = routeData, - }; - var urlHelper = CreateMockUrlHelper(actionContext); - urlHelper.Setup(h => h.RouteUrl(It.IsAny())) - .Callback((UrlRouteContext context) => actual = context); - - // Act - urlHelper.Object.Page("/TestPage"); - - // Assert - urlHelper.Verify(); - Assert.NotNull(actual); - Assert.Null(actual.RouteName); - Assert.Collection(Assert.IsType(actual.Values), - value => - { - Assert.Equal("page", value.Key); - Assert.Equal("/TestPage", value.Value); - }); - Assert.Null(actual.Host); - Assert.Null(actual.Protocol); - Assert.Null(actual.Fragment); - } - - public static TheoryData Page_WithNameAndRouteValues_WorksData - { - get => new TheoryData - { - { new { id = 10 } }, - { - new Dictionary - { - ["id"] = 10, - } - }, - { - new RouteValueDictionary - { - ["id"] = 10, - } - }, - }; - } - - [Theory] - [MemberData(nameof(Page_WithNameAndRouteValues_WorksData))] - public void Page_WithNameAndRouteValues_Works(object values) - { - // Arrange - UrlRouteContext actual = null; - var urlHelper = CreateMockUrlHelper(); - urlHelper.Setup(h => h.RouteUrl(It.IsAny())) - .Callback((UrlRouteContext context) => actual = context); - - // Act - urlHelper.Object.Page("/TestPage", values); - - // Assert - urlHelper.Verify(); - Assert.NotNull(actual); - Assert.Null(actual.RouteName); - Assert.Collection(Assert.IsType(actual.Values), - value => - { - Assert.Equal("id", value.Key); - Assert.Equal(10, value.Value); - }, - value => - { - Assert.Equal("page", value.Key); - Assert.Equal("/TestPage", value.Value); - }); - Assert.Null(actual.Host); - Assert.Null(actual.Protocol); - Assert.Null(actual.Fragment); - } - - [Fact] - public void Page_WithNameRouteValuesAndProtocol_Works() - { - // Arrange - UrlRouteContext actual = null; - var urlHelper = CreateMockUrlHelper(); - urlHelper.Setup(h => h.RouteUrl(It.IsAny())) - .Callback((UrlRouteContext context) => actual = context); - - // Act - urlHelper.Object.Page("/TestPage", pageHandler: null, values: new { id = 13 }, protocol: "https"); - - // Assert - urlHelper.Verify(); - Assert.NotNull(actual); - Assert.Null(actual.RouteName); - Assert.Collection(Assert.IsType(actual.Values), - value => - { - Assert.Equal("id", value.Key); - Assert.Equal(13, value.Value); - }, - value => - { - Assert.Equal("page", value.Key); - Assert.Equal("/TestPage", value.Value); - }); - Assert.Equal("https", actual.Protocol); - Assert.Null(actual.Host); - Assert.Null(actual.Fragment); - } - - [Fact] - public void Page_WithNameRouteValuesProtocolAndHost_Works() - { - // Arrange - UrlRouteContext actual = null; - var urlHelper = CreateMockUrlHelper(); - urlHelper.Setup(h => h.RouteUrl(It.IsAny())) - .Callback((UrlRouteContext context) => actual = context); - - // Act - urlHelper.Object.Page("/TestPage", pageHandler: null, values: new { id = 13 }, protocol: "https", host: "mytesthost"); - - // Assert - urlHelper.Verify(); - Assert.NotNull(actual); - Assert.Null(actual.RouteName); - Assert.Collection(Assert.IsType(actual.Values), - value => - { - Assert.Equal("id", value.Key); - Assert.Equal(13, value.Value); - }, - value => - { - Assert.Equal("page", value.Key); - Assert.Equal("/TestPage", value.Value); - }); - Assert.Equal("https", actual.Protocol); - Assert.Equal("mytesthost", actual.Host); - Assert.Null(actual.Fragment); - } - - [Fact] - public void Page_WithNameRouteValuesProtocolHostAndFragment_Works() - { - // Arrange - UrlRouteContext actual = null; - var urlHelper = CreateMockUrlHelper(); - urlHelper.Setup(h => h.RouteUrl(It.IsAny())) - .Callback((UrlRouteContext context) => actual = context); - - // Act - urlHelper.Object.Page("/TestPage", "test-handler", new { id = 13 }, "https", "mytesthost", "#toc"); - - // Assert - urlHelper.Verify(); - Assert.NotNull(actual); - Assert.Null(actual.RouteName); - Assert.Collection(Assert.IsType(actual.Values), - value => - { - Assert.Equal("id", value.Key); - Assert.Equal(13, value.Value); - }, - value => - { - Assert.Equal("page", value.Key); - Assert.Equal("/TestPage", value.Value); - }, - value => - { - Assert.Equal("handler", value.Key); - Assert.Equal("test-handler", value.Value); - }); - Assert.Equal("https", actual.Protocol); - Assert.Equal("mytesthost", actual.Host); - Assert.Equal("#toc", actual.Fragment); - } - - [Fact] - public void Page_UsesAmbientRouteValue_WhenPageIsNull() - { - // Arrange - UrlRouteContext actual = null; - var routeData = new RouteData - { - Values = - { - { "page", "ambient-page" }, - } - }; - var actionContext = new ActionContext - { - RouteData = routeData, - }; - - var urlHelper = CreateMockUrlHelper(actionContext); - urlHelper.Setup(h => h.RouteUrl(It.IsAny())) - .Callback((UrlRouteContext context) => actual = context); - - // Act - string page = null; - urlHelper.Object.Page(page, new { id = 13 }); - - // Assert - urlHelper.Verify(); - Assert.NotNull(actual); - Assert.Null(actual.RouteName); - Assert.Collection(Assert.IsType(actual.Values), - value => - { - Assert.Equal("id", value.Key); - Assert.Equal(13, value.Value); - }, - value => - { - Assert.Equal("page", value.Key); - Assert.Equal("ambient-page", value.Value); - }); - } - - [Fact] - public void Page_SetsHandlerToNull_IfValueIsNotSpecifiedInRouteValues() - { - // Arrange - UrlRouteContext actual = null; - var routeData = new RouteData - { - Values = - { - { "page", "ambient-page" }, - { "handler", "ambient-handler" }, - } - }; - var actionContext = new ActionContext - { - RouteData = routeData, - }; - - var urlHelper = CreateMockUrlHelper(actionContext); - urlHelper.Setup(h => h.RouteUrl(It.IsAny())) - .Callback((UrlRouteContext context) => actual = context); - - // Act - string page = null; - urlHelper.Object.Page(page, new { id = 13 }); - - // Assert - urlHelper.Verify(); - Assert.NotNull(actual); - Assert.Null(actual.RouteName); - Assert.Collection(Assert.IsType(actual.Values), - value => - { - Assert.Equal("id", value.Key); - Assert.Equal(13, value.Value); - }, - value => - { - Assert.Equal("page", value.Key); - Assert.Equal("ambient-page", value.Value); - }, - value => - { - Assert.Equal("handler", value.Key); - Assert.Null(value.Value); - }); - } - - [Fact] - public void Page_UsesExplicitlySpecifiedHandlerValue() - { - // Arrange - UrlRouteContext actual = null; - var routeData = new RouteData - { - Values = - { - { "page", "ambient-page" }, - { "handler", "ambient-handler" }, - } - }; - var actionContext = new ActionContext - { - RouteData = routeData, - }; - - var urlHelper = CreateMockUrlHelper(actionContext); - urlHelper.Setup(h => h.RouteUrl(It.IsAny())) - .Callback((UrlRouteContext context) => actual = context); - - // Act - string page = null; - urlHelper.Object.Page(page, "exact-handler", new { handler = "route-value-handler" }); - - // Assert - urlHelper.Verify(); - Assert.NotNull(actual); - Assert.Null(actual.RouteName); - Assert.Collection(Assert.IsType(actual.Values), - value => - { - Assert.Equal("handler", value.Key); - Assert.Equal("exact-handler", value.Value); - }, - value => - { - Assert.Equal("page", value.Key); - Assert.Equal("ambient-page", value.Value); - }); - } - - [Fact] - public void Page_UsesValueFromRouteValueIfPageHandlerIsNotExplicitySpecified() - { - // Arrange - UrlRouteContext actual = null; - var routeData = new RouteData - { - Values = - { - { "page", "ambient-page" }, - { "handler", "ambient-handler" }, - } - }; - var actionContext = new ActionContext - { - RouteData = routeData, - }; - - var urlHelper = CreateMockUrlHelper(actionContext); - urlHelper.Setup(h => h.RouteUrl(It.IsAny())) - .Callback((UrlRouteContext context) => actual = context); - - // Act - string page = null; - urlHelper.Object.Page(page, pageHandler: null, values: new { handler = "route-value-handler" }); - - // Assert - urlHelper.Verify(); - Assert.NotNull(actual); - Assert.Null(actual.RouteName); - Assert.Collection(Assert.IsType(actual.Values), - value => - { - Assert.Equal("handler", value.Key); - Assert.Equal("route-value-handler", value.Value); - }, - value => - { - Assert.Equal("page", value.Key); - Assert.Equal("ambient-page", value.Value); - }); - } - - [Theory] - [InlineData("Sibling", "/Dir1/Dir2/Sibling")] - [InlineData("Dir3/Sibling", "/Dir1/Dir2/Dir3/Sibling")] - [InlineData("Dir4/Dir5/Index", "/Dir1/Dir2/Dir4/Dir5/Index")] - public void Page_CalculatesPathRelativeToViewEnginePath_WhenNotRooted(string pageName, string expected) - { - // Arrange - UrlRouteContext actual = null; - var routeData = new RouteData(); - var actionContext = GetActionContextForPage("/Dir1/Dir2/About"); - - var urlHelper = CreateMockUrlHelper(actionContext); - urlHelper.Setup(h => h.RouteUrl(It.IsAny())) - .Callback((UrlRouteContext context) => actual = context); - - // Act - urlHelper.Object.Page(pageName); - - // Assert - urlHelper.Verify(); - Assert.NotNull(actual); - Assert.Null(actual.RouteName); - Assert.Collection(Assert.IsType(actual.Values), - value => - { - Assert.Equal("page", value.Key); - Assert.Equal(expected, value.Value); - }); - } - - [Fact] - public void Page_CalculatesPathRelativeToViewEnginePath_ForIndexPagePaths() - { - // Arrange - var expected = "/Dir1/Dir2/Sibling"; - UrlRouteContext actual = null; - var actionContext = GetActionContextForPage("/Dir1/Dir2/"); - - var urlHelper = CreateMockUrlHelper(actionContext); - urlHelper.Setup(h => h.RouteUrl(It.IsAny())) - .Callback((UrlRouteContext context) => actual = context); - - // Act - urlHelper.Object.Page("Sibling"); - - // Assert - urlHelper.Verify(); - Assert.NotNull(actual); - Assert.Null(actual.RouteName); - Assert.Collection(Assert.IsType(actual.Values), - value => - { - Assert.Equal("page", value.Key); - Assert.Equal(expected, value.Value); - }); - } - - [Fact] - public void Page_CalculatesPathRelativeToViewEnginePath_WhenNotRooted_ForPageAtRoot() - { - // Arrange - var expected = "/SiblingName"; - UrlRouteContext actual = null; - var routeData = new RouteData(); - var actionContext = new ActionContext - { - ActionDescriptor = new ActionDescriptor - { - RouteValues = new Dictionary - { - { "page", "/Home" }, - }, - }, - RouteData = new RouteData - { - Values = - { - [ "page" ] = "/Home" - }, - }, - }; - - var urlHelper = CreateMockUrlHelper(actionContext); - urlHelper.Setup(h => h.RouteUrl(It.IsAny())) - .Callback((UrlRouteContext context) => actual = context); - - // Act - urlHelper.Object.Page("SiblingName"); - - // Assert - urlHelper.Verify(); - Assert.NotNull(actual); - Assert.Null(actual.RouteName); - Assert.Collection(Assert.IsType(actual.Values), - value => - { - Assert.Equal("page", value.Key); - Assert.Equal(expected, value.Value); - }); - } - - [Fact] - public void Page_Throws_IfRouteValueDoesNotIncludePageKey() - { - // Arrange - var expected = "SiblingName"; - UrlRouteContext actual = null; - var routeData = new RouteData(); - var actionContext = new ActionContext - { - RouteData = new RouteData(), - }; - - var urlHelper = CreateMockUrlHelper(actionContext); - urlHelper.Setup(h => h.RouteUrl(It.IsAny())) - .Callback((UrlRouteContext context) => actual = context); - - // Act & Assert - var ex = Assert.Throws(() => urlHelper.Object.Page(expected)); - Assert.Equal($"The relative page path '{expected}' can only be used while executing a Razor Page. " + - "Specify a root relative path with a leading '/' to generate a URL outside of a Razor Page.", ex.Message); - } - - [Fact] - public void Page_UsesAreaValueFromRouteValueIfSpecified() - { - // Arrange - UrlRouteContext actual = null; - var routeData = new RouteData - { - Values = - { - { "page", "ambient-page" }, - { "area", "ambient-area" }, - } - }; - var actionContext = new ActionContext - { - RouteData = routeData, - }; - - var urlHelper = CreateMockUrlHelper(actionContext); - urlHelper.Setup(h => h.RouteUrl(It.IsAny())) - .Callback((UrlRouteContext context) => actual = context); - - // Act - string page = null; - urlHelper.Object.Page(page, values: new { area = "specified-area" }); - - // Assert - urlHelper.Verify(); - Assert.NotNull(actual); - Assert.Null(actual.RouteName); - Assert.Collection(Assert.IsType(actual.Values).OrderBy(v => v.Key), - value => - { - Assert.Equal("area", value.Key); - Assert.Equal("specified-area", value.Value); - }, - value => - { - Assert.Equal("page", value.Key); - Assert.Equal("ambient-page", value.Value); - }); - } - - private static Mock CreateMockUrlHelper(ActionContext context = null) - { - if (context == null) - { - context = GetActionContextForPage("/Page"); - } - - var urlHelper = new Mock(); - urlHelper.SetupGet(h => h.ActionContext) - .Returns(context); - return urlHelper; - } - - private static HttpContext CreateHttpContext( - IServiceProvider services, - string appRoot) - { - var context = new DefaultHttpContext(); - context.RequestServices = services; - - context.Request.PathBase = new PathString(appRoot); - context.Request.Host = new HostString("localhost"); - - return context; - } - - private static ActionContext CreateActionContext(HttpContext context) - { - return CreateActionContext(context, (new Mock()).Object); - } - - private static ActionContext CreateActionContext(HttpContext context, IRouter router) - { - var routeData = new RouteData(); - routeData.Routers.Add(router); - - return new ActionContext(context, routeData, new ActionDescriptor()); - } - - private static UrlHelper CreateUrlHelper() - { - var services = CreateServices(); - var context = CreateHttpContext(services, string.Empty); - var actionContext = CreateActionContext(context); - - return new UrlHelper(actionContext); - } - - private static UrlHelper CreateUrlHelper(ActionContext context) - { - return new UrlHelper(context); - } - - private static UrlHelper CreateUrlHelper(string host) - { - var services = CreateServices(); - var context = CreateHttpContext(services, string.Empty); - context.Request.Host = new HostString(host); - - var actionContext = CreateActionContext(context); - - return new UrlHelper(actionContext); - } - - private static UrlHelper CreateUrlHelper(string host, string protocol, IRouter router) - { - var services = CreateServices(); - var context = CreateHttpContext(services, string.Empty); - context.Request.Host = new HostString(host); - context.Request.Scheme = protocol; - - var actionContext = CreateActionContext(context, router); - - return new UrlHelper(actionContext); - } - - private static TestUrlHelper CreateUrlHelper(string appBase, IRouter router) - { - var services = CreateServices(); - var context = CreateHttpContext(services, appBase); - var actionContext = CreateActionContext(context, router); - - return new TestUrlHelper(actionContext); - } - - private static UrlHelper CreateUrlHelperWithRouteCollection( - IServiceProvider services, - string appPrefix) - { - var routeCollection = GetRouter(services); - return CreateUrlHelper(appPrefix, routeCollection); - } - - private static IRouter GetRouter(IServiceProvider services) - { - return GetRouter(services, "mockRoute", "/mockTemplate"); - } - - private static IServiceProvider CreateServices() - { - var services = new ServiceCollection(); - services.AddOptions(); - services.AddLogging(); - services.AddRouting(); - services - .AddSingleton() - .AddSingleton(UrlEncoder.Default); - + var services = GetCommonServices(); return services.BuildServiceProvider(); } - private static IRouteBuilder CreateRouteBuilder(IServiceProvider services) + protected override IUrlHelper CreateUrlHelper(string appRoot, string host, string protocol) { - var app = new Mock(); - app - .SetupGet(a => a.ApplicationServices) - .Returns(services); - - return new RouteBuilder(app.Object) - { - DefaultHandler = new PassThroughRouter(), - }; + var services = CreateServices(); + var httpContext = CreateHttpContext(services, appRoot, host, protocol); + var actionContext = CreateActionContext(httpContext); + var defaultRoutes = GetDefaultRoutes(services); + actionContext.RouteData.Routers.Add(defaultRoutes); + return new UrlHelper(actionContext); } - private static IRouter GetRouter( + protected override IUrlHelper CreateUrlHelperWithDefaultRoutes( + string appRoot, + string host, + string protocol, + string routeName, + string template) + { + var services = CreateServices(); + var httpContext = CreateHttpContext(services, appRoot, host, protocol); + var actionContext = CreateActionContext(httpContext); + var router = GetDefaultRoutes(services, routeName, template); + actionContext.RouteData.Routers.Add(router); + return CreateUrlHelper(actionContext); + } + + protected override IUrlHelper CreateUrlHelper(ActionContext actionContext) + { + return new UrlHelper(actionContext); + } + + protected override IUrlHelper CreateUrlHelperWithDefaultRoutes(string appRoot, string host, string protocol) + { + var services = CreateServices(); + var context = CreateHttpContext(services, appRoot, host, protocol); + + var router = GetDefaultRoutes(services); + var actionContext = CreateActionContext(context); + actionContext.RouteData.Routers.Add(router); + + return CreateUrlHelper(actionContext); + } + + protected override IUrlHelper CreateUrlHelper( + string appRoot, + string host, + string protocol, + string routeName, + string template, + object defaults) + { + var services = CreateServices(); + var routeBuilder = CreateRouteBuilder(services); + routeBuilder.MapRoute( + routeName, + template, + defaults); + var router = routeBuilder.Build(); + var httpContext = CreateHttpContext(services, appRoot, host, protocol); + var actionContext = CreateActionContext(httpContext); + actionContext.RouteData.Routers.Add(router); + return CreateUrlHelper(actionContext); + } + + private static IRouter GetDefaultRoutes(IServiceProvider services) + { + return GetDefaultRoutes(services, "mockRoute", "/mockTemplate"); + } + + private static IRouter GetDefaultRoutes( IServiceProvider services, string mockRouteName, string mockTemplateValue) @@ -1756,24 +120,16 @@ namespace Microsoft.AspNetCore.Mvc.Routing return routeBuilder.Build(); } - private static ActionContext GetActionContextForPage(string page) + private static IRouteBuilder CreateRouteBuilder(IServiceProvider services) { - return new ActionContext + var app = new Mock(); + app + .SetupGet(a => a.ApplicationServices) + .Returns(services); + + return new RouteBuilder(app.Object) { - ActionDescriptor = new ActionDescriptor - { - RouteValues = new Dictionary - { - { "page", page }, - }, - }, - RouteData = new RouteData - { - Values = - { - [ "page" ] = page - }, - }, + DefaultHandler = new PassThroughRouter(), }; } @@ -1790,22 +146,5 @@ namespace Microsoft.AspNetCore.Mvc.Routing return Task.FromResult(false); } } - - private class TestUrlHelper : UrlHelper - { - public TestUrlHelper(ActionContext actionContext) : - base(actionContext) - { - - } - public new string GenerateUrl(string protocol, string host, VirtualPathData pathData, string fragment) - { - return base.GenerateUrl( - protocol, - host, - pathData, - fragment); - } - } } } \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/Routing/UrlHelperTestBase.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/Routing/UrlHelperTestBase.cs new file mode 100644 index 0000000000..300efe8c4a --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/Routing/UrlHelperTestBase.cs @@ -0,0 +1,1007 @@ +// 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 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); + } + } +}