diff --git a/src/Microsoft.AspNet.Mvc.Core/Rendering/Html/HtmlHelper.cs b/src/Microsoft.AspNet.Mvc.Core/Rendering/Html/HtmlHelper.cs index c8e610a5d9..ae2858ba32 100644 --- a/src/Microsoft.AspNet.Mvc.Core/Rendering/Html/HtmlHelper.cs +++ b/src/Microsoft.AspNet.Mvc.Core/Rendering/Html/HtmlHelper.cs @@ -124,7 +124,7 @@ namespace Microsoft.AspNet.Mvc.Rendering object routeValues, object htmlAttributes) { - var url = _urlHelper.Action(actionName, controllerName, routeValues); + var url = _urlHelper.Action(actionName, controllerName, routeValues, protocol, hostname, fragment); return GenerateLink(linkText, url, GetHtmlAttributeDictionaryOrNull(htmlAttributes)); } diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/Rendering/DefaultTemplatesUtilities.cs b/test/Microsoft.AspNet.Mvc.Core.Test/Rendering/DefaultTemplatesUtilities.cs index c995d88788..c02f465f89 100644 --- a/test/Microsoft.AspNet.Mvc.Core.Test/Rendering/DefaultTemplatesUtilities.cs +++ b/test/Microsoft.AspNet.Mvc.Core.Test/Rendering/DefaultTemplatesUtilities.cs @@ -48,6 +48,11 @@ namespace Microsoft.AspNet.Mvc.Core return GetHtmlHelper(null); } + public static HtmlHelper GetHtmlHelper(IUrlHelper urlHelper) + { + return GetHtmlHelper(null, urlHelper, CreateViewEngine(), CreateModelMetadataProvider()); + } + public static HtmlHelper GetHtmlHelper(TModel model) { return GetHtmlHelper(model, CreateViewEngine()); @@ -60,16 +65,17 @@ namespace Microsoft.AspNet.Mvc.Core public static HtmlHelper GetHtmlHelper(TModel model, IModelMetadataProvider provider) { - return GetHtmlHelper(model, CreateViewEngine(), provider); + return GetHtmlHelper(model, CreateUrlHelper(), CreateViewEngine(), provider); } public static HtmlHelper GetHtmlHelper(TModel model, ICompositeViewEngine viewEngine) { - return GetHtmlHelper(model, viewEngine, new DataAnnotationsModelMetadataProvider()); + return GetHtmlHelper(model, CreateUrlHelper(), viewEngine, CreateModelMetadataProvider()); } public static HtmlHelper GetHtmlHelper( TModel model, + IUrlHelper urlHelper, ICompositeViewEngine viewEngine, IModelMetadataProvider provider) { @@ -94,7 +100,6 @@ namespace Microsoft.AspNet.Mvc.Core Mock.Of(), Mock.Of(), Mock.Of()); - var urlHelper = Mock.Of(); var actionBindingContextProvider = new Mock(); actionBindingContextProvider .Setup(c => c.GetActionBindingContextAsync(It.IsAny())) @@ -180,5 +185,15 @@ namespace Microsoft.AspNet.Mvc.Core metadata.PropertyName ?? "(null)", metadata.SimpleDisplayText ?? "(null)"); } + + private static IUrlHelper CreateUrlHelper() + { + return Mock.Of(); + } + + private static IModelMetadataProvider CreateModelMetadataProvider() + { + return new DataAnnotationsModelMetadataProvider(); + } } } \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/Rendering/HtmlHelperLinkGenerationTest.cs b/test/Microsoft.AspNet.Mvc.Core.Test/Rendering/HtmlHelperLinkGenerationTest.cs new file mode 100644 index 0000000000..3e33b33bfc --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.Core.Test/Rendering/HtmlHelperLinkGenerationTest.cs @@ -0,0 +1,172 @@ +using System.Linq; +using System.Collections.Generic; +using Microsoft.AspNet.Mvc.Core; +using Moq; +using Xunit; + +namespace Microsoft.AspNet.Mvc.Rendering +{ + /// + /// Tests the 's link generation methods. + /// + public class HtmlHelperLinkGenerationTest + { + public static IEnumerable ActionLinkGenerationData + { + get + { + yield return new object[] { + "Details", "Product", new { isprint = "true", showreviews = "true" }, "https", "www.contoso.com", "h1", + new { p1 = "p1-value" } }; + yield return new object[] { + "Details", "Product", new { isprint = "true", showreviews = "true" }, "https", "www.contoso.com", null, null }; + yield return new object[] { + "Details", "Product", new { isprint = "true", showreviews = "true" }, "https", null, null, null }; + yield return new object[] { + "Details", "Product", new { isprint = "true", showreviews = "true" }, null, null, null, null }; + yield return new object[] { + "Details", "Product", null, null, null, null, null }; + yield return new object[] { + null, null, null, null, null, null, null }; + } + } + + [Theory] + [MemberData(nameof(ActionLinkGenerationData))] + public void ActionLink_GeneratesLink_WithExpectedValues( + string action, + string controller, + object routeValues, + string protocol, + string hostname, + string fragment, + object htmlAttributes) + { + //Arrange + string expectedLink = string.Format(@"Details", + protocol, + hostname, + controller, + action, + GetRouteValuesAsString(routeValues), + fragment, + GetHtmlAttributesAsString(htmlAttributes)); + + var urlHelper = new Mock(); + urlHelper.Setup( + h => h.Action( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny())) + .Returns( + (actn, cntrlr, rvalues, prtcl, hname, frgmt) => + string.Format("{0}{1}{2}{3}{4}{5}", + prtcl, + hname, + cntrlr, + actn, + GetRouteValuesAsString(rvalues), + frgmt)); + + var htmlHelper = DefaultTemplatesUtilities.GetHtmlHelper(urlHelper.Object); + + // Act + var actualLink = htmlHelper.ActionLink( + linkText: "Details", + actionName: action, + controllerName: controller, + protocol: protocol, + hostname: hostname, + fragment: fragment, + routeValues: routeValues, + htmlAttributes: htmlAttributes).ToString(); + + // Assert + Assert.Equal(expectedLink, actualLink); + } + + public static IEnumerable RouteLinkGenerationData + { + get + { + yield return new object[] { + "default", new { isprint = "true", showreviews = "true" }, "https", "www.contoso.com", "h1", + new { p1 = "p1-value" } }; + yield return new object[] { + "default", new { isprint = "true", showreviews = "true" }, "https", "www.contoso.com", null, null }; + yield return new object[] { + "default", new { isprint = "true", showreviews = "true" }, "https", null, null, null }; + yield return new object[] { + "default", new { isprint = "true", showreviews = "true" }, null, null, null, null }; + yield return new object[] { + "default", null, null, null, null, null }; + } + } + + [Theory] + [MemberData(nameof(RouteLinkGenerationData))] + public void RouteLink_GeneratesLink_WithExpectedValues( + string routeName, + object routeValues, + string protocol, + string hostname, + string fragment, + object htmlAttributes) + { + //Arrange + string expectedLink = string.Format(@"Details", + protocol, + hostname, + GetRouteValuesAsString(routeValues), + fragment, + GetHtmlAttributesAsString(htmlAttributes)); + + var urlHelper = new Mock(); + urlHelper + .Setup( + h => h.RouteUrl( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny())) + .Returns( + (rname, rvalues, prtcl, hname, frgmt) => + string.Format("{0}{1}{2}{3}", + prtcl, + hname, + GetRouteValuesAsString(rvalues), + frgmt)); + + var htmlHelper = DefaultTemplatesUtilities.GetHtmlHelper(urlHelper.Object); + + // Act + var actualLink = htmlHelper.RouteLink( + linkText: "Details", + routeName: routeName, + protocol: protocol, + hostName: hostname, + fragment: fragment, + routeValues: routeValues, + htmlAttributes: htmlAttributes).ToString(); + + // Assert + Assert.Equal(expectedLink, actualLink); + } + + private string GetRouteValuesAsString(object routeValues) + { + var dict = TypeHelper.ObjectToDictionary(routeValues); + return string.Join(string.Empty, dict.Select(kvp => string.Format("{0}={1}", kvp.Key, kvp.Value.ToString()))); + } + + private string GetHtmlAttributesAsString(object routeValues) + { + var dict = TypeHelper.ObjectToDictionary(routeValues); + return string.Join(string.Empty, dict.Select(kvp => string.Format(" {0}=\"{1}\"", kvp.Key, kvp.Value.ToString()))); + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.FunctionalTests/BasicTests.cs b/test/Microsoft.AspNet.Mvc.FunctionalTests/BasicTests.cs index 83aa20f97b..655fae1346 100644 --- a/test/Microsoft.AspNet.Mvc.FunctionalTests/BasicTests.cs +++ b/test/Microsoft.AspNet.Mvc.FunctionalTests/BasicTests.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.Collections.Generic; using System.Net; using System.Net.Http; using System.Net.Http.Headers; @@ -184,5 +185,57 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests // Assert Assert.Equal(HttpStatusCode.OK, response.StatusCode); } + + public static IEnumerable HtmlHelperLinkGenerationData + { + get + { + yield return new[] { + "ActionLink_ActionOnSameController", + @"linktext" }; + yield return new[] { + "ActionLink_ActionOnOtherController", + @"linktext" + }; + yield return new[] { + "ActionLink_SecurePage_ImplicitHostName", + @"linktext" + }; + yield return new[] { + "ActionLink_HostNameFragmentAttributes", + // note: attributes are alphabetically ordered + @"linktext" + }; + yield return new[] { + "RouteLink_RestLinkToOtherController", + @"linktext" + }; + yield return new[] { + "RouteLink_SecureApi_ImplicitHostName", + @"linktext" + }; + yield return new[] { + "RouteLink_HostNameFragmentAttributes", + @"linktext" + }; + } + } + + [Theory] + [MemberData(nameof(HtmlHelperLinkGenerationData))] + public async Task HtmlHelperLinkGeneration(string viewName, string expectedLink) + { + // Arrange + var server = TestServer.Create(_provider, _app); + var client = new HttpClient(server.CreateHandler(), false); + + // Act + var response = await client.GetAsync("http://localhost/Links/Index?view=" + viewName); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var responseData = await response.Content.ReadAsStringAsync(); + Assert.Contains(expectedLink, responseData, StringComparison.OrdinalIgnoreCase); + } } } \ No newline at end of file diff --git a/test/WebSites/BasicWebSite/Controllers/LinkGeneration/LinksController.cs b/test/WebSites/BasicWebSite/Controllers/LinkGeneration/LinksController.cs new file mode 100644 index 0000000000..6a68a3d5b5 --- /dev/null +++ b/test/WebSites/BasicWebSite/Controllers/LinkGeneration/LinksController.cs @@ -0,0 +1,18 @@ +using System; +using Microsoft.AspNet.Mvc; + +namespace BasicWebSite.Controllers.LinkGeneration +{ + public class LinksController : Controller + { + public IActionResult Index(string view) + { + return View(viewName: view); + } + + public string Details() + { + throw new NotImplementedException(); + } + } +} diff --git a/test/WebSites/BasicWebSite/Controllers/LinkGeneration/OrdersController.cs b/test/WebSites/BasicWebSite/Controllers/LinkGeneration/OrdersController.cs new file mode 100644 index 0000000000..bcb58ed43b --- /dev/null +++ b/test/WebSites/BasicWebSite/Controllers/LinkGeneration/OrdersController.cs @@ -0,0 +1,21 @@ +using System; +using Microsoft.AspNet.Mvc; + +namespace BasicWebSite.Controllers.LinkGeneration +{ + [Route("api/orders/{id?}", Name = "OrdersApi")] + public class OrdersController : Controller + { + [HttpGet] + public IActionResult GetAll() + { + throw new NotImplementedException(); + } + + [HttpGet] + public IActionResult GetById(int id) + { + throw new NotImplementedException(); + } + } +} \ No newline at end of file diff --git a/test/WebSites/BasicWebSite/Controllers/LinkGeneration/ProductsController.cs b/test/WebSites/BasicWebSite/Controllers/LinkGeneration/ProductsController.cs new file mode 100644 index 0000000000..448bd9c17c --- /dev/null +++ b/test/WebSites/BasicWebSite/Controllers/LinkGeneration/ProductsController.cs @@ -0,0 +1,18 @@ +using System; +using Microsoft.AspNet.Mvc; + +namespace BasicWebSite.Controllers.LinkGeneration +{ + public class ProductsController : Controller + { + public IActionResult Index() + { + throw new NotImplementedException(); + } + + public IActionResult Details() + { + throw new NotImplementedException(); + } + } +} \ No newline at end of file diff --git a/test/WebSites/BasicWebSite/Views/Links/ActionLink_ActionOnOtherController.cshtml b/test/WebSites/BasicWebSite/Views/Links/ActionLink_ActionOnOtherController.cshtml new file mode 100644 index 0000000000..66046197ca --- /dev/null +++ b/test/WebSites/BasicWebSite/Views/Links/ActionLink_ActionOnOtherController.cshtml @@ -0,0 +1,2 @@ +@* Generate link to a different controller and with non-route parameters *@ +@Html.ActionLink("linktext", "Details", new { controller = "Products", print = "true" }) \ No newline at end of file diff --git a/test/WebSites/BasicWebSite/Views/Links/ActionLink_ActionOnSameController.cshtml b/test/WebSites/BasicWebSite/Views/Links/ActionLink_ActionOnSameController.cshtml new file mode 100644 index 0000000000..8e23c4a0a3 --- /dev/null +++ b/test/WebSites/BasicWebSite/Views/Links/ActionLink_ActionOnSameController.cshtml @@ -0,0 +1,2 @@ +@* Generate link to action on current controller *@ +@Html.ActionLink("linktext", "Details") \ No newline at end of file diff --git a/test/WebSites/BasicWebSite/Views/Links/ActionLink_HostNameFragmentAttributes.cshtml b/test/WebSites/BasicWebSite/Views/Links/ActionLink_HostNameFragmentAttributes.cshtml new file mode 100644 index 0000000000..b9dc82aeff --- /dev/null +++ b/test/WebSites/BasicWebSite/Views/Links/ActionLink_HostNameFragmentAttributes.cshtml @@ -0,0 +1,9 @@ +@Html.ActionLink( + linkText: "linktext", + actionName: "Details", + controllerName: "Products", + protocol: "https", + hostname: "www.contoso.com:9000", + fragment: "details", + routeValues: new { print = "true" }, + htmlAttributes: new { p1 = "p1-value" }) \ No newline at end of file diff --git a/test/WebSites/BasicWebSite/Views/Links/ActionLink_SecurePage_ImplicitHostName.cshtml b/test/WebSites/BasicWebSite/Views/Links/ActionLink_SecurePage_ImplicitHostName.cshtml new file mode 100644 index 0000000000..b0254536db --- /dev/null +++ b/test/WebSites/BasicWebSite/Views/Links/ActionLink_SecurePage_ImplicitHostName.cshtml @@ -0,0 +1,10 @@ +@* Notice that the 'hostname' here is null but the protocol is 'https'. Its a link to a secure page on this application *@ +@Html.ActionLink( + linkText: "linktext", + actionName: "Details", + controllerName: "Products", + protocol: "https", + hostname: null, + fragment:null, + routeValues: new { print = "true" }, + htmlAttributes: null) \ No newline at end of file diff --git a/test/WebSites/BasicWebSite/Views/Links/RouteLink_HostNameFragmentAttributes.cshtml b/test/WebSites/BasicWebSite/Views/Links/RouteLink_HostNameFragmentAttributes.cshtml new file mode 100644 index 0000000000..f40397e6bc --- /dev/null +++ b/test/WebSites/BasicWebSite/Views/Links/RouteLink_HostNameFragmentAttributes.cshtml @@ -0,0 +1,8 @@ +@Html.RouteLink( + linkText: "linktext", + routeName:"OrdersApi", + protocol: "https", + hostName:"www.contoso.com:9000", + fragment: "details", + routeValues: new { controller = "Orders", id = 10, print = true }, + htmlAttributes: new { p1 = "p1-value" }) diff --git a/test/WebSites/BasicWebSite/Views/Links/RouteLink_RestLinkToOtherController.cshtml b/test/WebSites/BasicWebSite/Views/Links/RouteLink_RestLinkToOtherController.cshtml new file mode 100644 index 0000000000..36b7f9b64d --- /dev/null +++ b/test/WebSites/BasicWebSite/Views/Links/RouteLink_RestLinkToOtherController.cshtml @@ -0,0 +1 @@ +@Html.RouteLink("linktext", "OrdersApi", new { controller = "Orders", id = 10 }) \ No newline at end of file diff --git a/test/WebSites/BasicWebSite/Views/Links/RouteLink_SecureApi_ImplicitHostName.cshtml b/test/WebSites/BasicWebSite/Views/Links/RouteLink_SecureApi_ImplicitHostName.cshtml new file mode 100644 index 0000000000..bf74c1cab1 --- /dev/null +++ b/test/WebSites/BasicWebSite/Views/Links/RouteLink_SecureApi_ImplicitHostName.cshtml @@ -0,0 +1,8 @@ +@Html.RouteLink( + linkText: "linktext", + routeName: "OrdersApi", + protocol: "https", + hostName:null, + fragment: null, + routeValues: new { controller = "Orders", id = 10 }, + htmlAttributes: null)