// 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.Builder; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.Abstractions; using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.Mvc.ModelBinding.Validation; using Microsoft.AspNetCore.Mvc.Routing; using Microsoft.AspNetCore.Mvc.ViewFeatures; using Microsoft.AspNetCore.Routing; using Microsoft.AspNetCore.Testing; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.ObjectPool; using Microsoft.Extensions.Options; using Moq; using Xunit; namespace Microsoft.AspNetCore.Mvc { public class RemoteAttributeTest { public static TheoryData SomeNames { get { return new TheoryData { string.Empty, "Action", "In a controller", " slightly\t odd\t whitespace\t\r\n", }; } } // Null or empty property names are invalid. (Those containing just whitespace are legal.) public static TheoryData NullOrEmptyNames { get { return new TheoryData { null, string.Empty, }; } } [Fact] public void Constructor_WithNullAction_IgnoresArgument() { // Arrange & Act var attribute = new TestableRemoteAttribute(action: null, controller: "AController"); // Assert var keyValuePair = Assert.Single(attribute.RouteData); Assert.Equal("controller", keyValuePair.Key); } [Fact] public void Constructor_WithNullController_IgnoresArgument() { // Arrange & Act var attribute = new TestableRemoteAttribute("AnAction", controller: null); // Assert var keyValuePair = Assert.Single(attribute.RouteData); Assert.Equal("action", keyValuePair.Key); Assert.Null(attribute.RouteName); } [Theory] [InlineData(null)] [MemberData(nameof(SomeNames))] public void Constructor_WithRouteName_UpdatesProperty(string routeName) { // Arrange & Act var attribute = new TestableRemoteAttribute(routeName); // Assert Assert.Empty(attribute.RouteData); Assert.Equal(routeName, attribute.RouteName); } [Theory] [MemberData(nameof(SomeNames))] public void Constructor_WithActionController_UpdatesActionRouteData(string action) { // Arrange & Act var attribute = new TestableRemoteAttribute(action, "AController"); // Assert Assert.Equal(2, attribute.RouteData.Count); Assert.Contains("controller", attribute.RouteData.Keys); var resultName = Assert.Single( attribute.RouteData, keyValuePair => string.Equals(keyValuePair.Key, "action", StringComparison.Ordinal)) .Value; Assert.Equal(action, resultName); Assert.Null(attribute.RouteName); } [Theory] [MemberData(nameof(SomeNames))] public void Constructor_WithActionController_UpdatesControllerRouteData(string controller) { // Arrange & Act var attribute = new TestableRemoteAttribute("AnAction", controller); // Assert Assert.Equal(2, attribute.RouteData.Count); Assert.Contains("action", attribute.RouteData.Keys); var resultName = Assert.Single( attribute.RouteData, keyValuePair => string.Equals(keyValuePair.Key, "controller", StringComparison.Ordinal)) .Value; Assert.Equal(controller, resultName); Assert.Null(attribute.RouteName); } [Theory] [InlineData(null)] [MemberData(nameof(SomeNames))] public void Constructor_WithActionControllerAreaName_UpdatesAreaRouteData(string areaName) { // Arrange & Act var attribute = new TestableRemoteAttribute("AnAction", "AController", areaName: areaName); // Assert Assert.Equal(3, attribute.RouteData.Count); Assert.Contains("action", attribute.RouteData.Keys); Assert.Contains("controller", attribute.RouteData.Keys); var resultName = Assert.Single( attribute.RouteData, keyValuePair => string.Equals(keyValuePair.Key, "area", StringComparison.Ordinal)) .Value; Assert.Equal(areaName, resultName); Assert.Null(attribute.RouteName); } [Fact] public void GetUrl_WithBadRouteName_Throws() { // Arrange var testableAttribute = new TestableRemoteAttribute("nonexistentRoute"); var context = GetValidationContextWithArea(currentArea: null); // Act & Assert var exception = Assert.Throws(() => testableAttribute.InvokeGetUrl(context)); Assert.Equal("No URL for remote validation could be found.", exception.Message); } [Fact] public void GetUrl_WithRoute_CallsUrlHelperWithExpectedValues() { // Arrange var routeName = "RouteName"; var testableRemoteAttribute = new TestableRemoteAttribute(routeName); var url = "/my/URL"; var urlHelper = new MockUrlHelper(url, routeName); var context = GetValidationContext(urlHelper); // Act var actualUrl = testableRemoteAttribute.InvokeGetUrl(context); // Assert Assert.Equal(url, actualUrl); var routeDictionary = Assert.IsType(urlHelper.RouteValues); Assert.Empty(routeDictionary); } [Fact] public void GetUrl_WithActionController_CallsUrlHelperWithExpectedValues() { // Arrange var testableRemoteAttribute = new TestableRemoteAttribute("Action", "Controller"); var url = "/Controller/Action"; var urlHelper = new MockUrlHelper(url, routeName: null); var context = GetValidationContext(urlHelper); // Act var actualUrl = testableRemoteAttribute.InvokeGetUrl(context); // Assert Assert.Equal(url, actualUrl); var routeDictionary = Assert.IsType(urlHelper.RouteValues); Assert.Equal(2, routeDictionary.Count); Assert.Equal("Action", routeDictionary["action"] as string); Assert.Equal("Controller", routeDictionary["controller"] as string); } [Fact] public void GetUrl_WithActionControllerArea_CallsUrlHelperWithExpectedValues() { // Arrange var testableAttribute = new TestableRemoteAttribute("Action", "Controller", "Test"); var url = "/Test/Controller/Action"; var urlHelper = new MockUrlHelper(url, routeName: null); var context = GetValidationContext(urlHelper); // Act var actualUrl = testableAttribute.InvokeGetUrl(context); // Assert Assert.Equal(url, actualUrl); var routeDictionary = Assert.IsType(urlHelper.RouteValues); Assert.Equal(3, routeDictionary.Count); Assert.Equal("Action", routeDictionary["action"] as string); Assert.Equal("Controller", routeDictionary["controller"] as string); Assert.Equal("Test", routeDictionary["area"] as string); } // Root area is current in this case. [Fact] public void GetUrl_WithActionController_FindsControllerInCurrentArea() { // Arrange var testableAttribute = new TestableRemoteAttribute("Action", "Controller"); var context = GetValidationContextWithArea(currentArea: null); // Act var actualUrl = testableAttribute.InvokeGetUrl(context); // Assert Assert.Equal("/Controller/Action", actualUrl); } // Test area is current in this case. [Fact] public void GetUrl_WithActionControllerInArea_FindsControllerInCurrentArea() { // Arrange var testableAttribute = new TestableRemoteAttribute("Action", "Controller"); var context = GetValidationContextWithArea(currentArea: "Test"); // Act var actualUrl = testableAttribute.InvokeGetUrl(context); // Assert Assert.Equal("/Test/Controller/Action", actualUrl); } // Explicit reference to the (current) root area. [Theory] [MemberData(nameof(NullOrEmptyNames))] public void GetUrl_WithActionControllerArea_FindsControllerInRootArea(string areaName) { // Arrange var testableAttribute = new TestableRemoteAttribute("Action", "Controller", areaName); var context = GetValidationContextWithArea(currentArea: null); // Act var actualUrl = testableAttribute.InvokeGetUrl(context); // Assert Assert.Equal("/Controller/Action", actualUrl); } // Test area is current in this case. [Theory] [MemberData(nameof(NullOrEmptyNames))] public void GetUrl_WithActionControllerAreaInArea_FindsControllerInRootArea(string areaName) { // Arrange var testableAttribute = new TestableRemoteAttribute("Action", "Controller", areaName); var context = GetValidationContextWithArea(currentArea: "Test"); // Act var actualUrl = testableAttribute.InvokeGetUrl(context); // Assert Assert.Equal("/Controller/Action", actualUrl); } // Root area is current in this case. [Fact] public void GetUrl_WithActionControllerArea_FindsControllerInNamedArea() { // Arrange var testableAttribute = new TestableRemoteAttribute("Action", "Controller", "Test"); var context = GetValidationContextWithArea(currentArea: null); // Act var actualUrl = testableAttribute.InvokeGetUrl(context); // Assert Assert.Equal("/Test/Controller/Action", actualUrl); } // Explicit reference to the current (Test) area. [Fact] public void GetUrl_WithActionControllerAreaInArea_FindsControllerInNamedArea() { // Arrange var testableAttribute = new TestableRemoteAttribute("Action", "Controller", "Test"); var context = GetValidationContextWithArea(currentArea: "Test"); // Act var actualUrl = testableAttribute.InvokeGetUrl(context); // Assert Assert.Equal("/Test/Controller/Action", actualUrl); } // Test area is current in this case. [Fact] public void GetUrl_WithActionControllerAreaInArea_FindsControllerInDifferentArea() { // Arrange var testableAttribute = new TestableRemoteAttribute("Action", "Controller", "AnotherArea"); var context = GetValidationContextWithArea(currentArea: "Test"); // Act var actualUrl = testableAttribute.InvokeGetUrl(context); // Assert Assert.Equal("/AnotherArea/Controller/Action", actualUrl); } // Test area is current in this case. [Fact] public void AddValidation_DoesNotTrounceExistingAttributes() { // Arrange var attribute = new RemoteAttribute("Action", "Controller", "AnotherArea") { HttpMethod = "PUT", }; var context = GetValidationContextWithArea(currentArea: "Test"); context.Attributes.Add("data-val", "original"); context.Attributes.Add("data-val-remote", "original"); context.Attributes.Add("data-val-remote-additionalfields", "original"); context.Attributes.Add("data-val-remote-type", "original"); context.Attributes.Add("data-val-remote-url", "original"); // Act attribute.AddValidation(context); // Assert Assert.Collection( context.Attributes, kvp => { Assert.Equal("data-val", kvp.Key); Assert.Equal("original", kvp.Value); }, kvp => { Assert.Equal("data-val-remote", kvp.Key); Assert.Equal("original", kvp.Value); }, kvp => { Assert.Equal("data-val-remote-additionalfields", kvp.Key); Assert.Equal("original", kvp.Value); }, kvp => { Assert.Equal("data-val-remote-type", kvp.Key); Assert.Equal("original", kvp.Value); }, kvp => { Assert.Equal("data-val-remote-url", kvp.Key); Assert.Equal("original", kvp.Value); }); } private static ClientModelValidationContext GetValidationContext(string url) { var urlHelper = new MockUrlHelper(url, routeName: null); return GetValidationContext(urlHelper); } private static ClientModelValidationContext GetValidationContext( IUrlHelper urlHelper) { var serviceCollection = GetServiceCollection(); var factory = new Mock(MockBehavior.Strict); serviceCollection.AddSingleton(factory.Object); var serviceProvider = serviceCollection.BuildServiceProvider(); var actionContext = GetActionContext(serviceProvider, routeData: null); factory .Setup(f => f.GetUrlHelper(actionContext)) .Returns(urlHelper); var metadataProvider = new EmptyModelMetadataProvider(); var metadata = metadataProvider.GetMetadataForProperty( containerType: typeof(string), propertyName: nameof(string.Length)); return new ClientModelValidationContext( actionContext, metadata, metadataProvider, new AttributeDictionary()); } private static ClientModelValidationContext GetValidationContextWithArea(string currentArea) { var serviceCollection = GetServiceCollection(); var serviceProvider = serviceCollection.BuildServiceProvider(); var routeCollection = GetRouteCollectionWithArea(serviceProvider); var routeData = new RouteData { Routers = { routeCollection, }, Values = { { "action", "Index" }, { "controller", "Home" }, }, }; if (!string.IsNullOrEmpty(currentArea)) { routeData.Values["area"] = currentArea; } var actionContext = GetActionContext(serviceProvider, routeData); var urlHelper = new UrlHelper(actionContext); var factory = new Mock(MockBehavior.Strict); factory .Setup(f => f.GetUrlHelper(actionContext)) .Returns(urlHelper); // Make an IUrlHelperFactory available through the ActionContext. serviceCollection.AddSingleton(factory.Object); serviceProvider = serviceCollection.BuildServiceProvider(); actionContext.HttpContext.RequestServices = serviceProvider; var metadataProvider = new EmptyModelMetadataProvider(); var metadata = metadataProvider.GetMetadataForProperty( containerType: typeof(string), propertyName: nameof(string.Length)); return new ClientModelValidationContext( actionContext, metadata, metadataProvider, new AttributeDictionary()); } private static IRouter GetRouteCollectionWithArea(IServiceProvider serviceProvider) { var builder = GetRouteBuilder(serviceProvider); // Setting IsBound to true makes order more important than usual. First try the route that requires the // area value. Skip usual "area:exists" constraint because that isn't relevant for link generation and it // complicates the setup significantly. builder.MapRoute("areaRoute", "{area}/{controller}/{action}"); builder.MapRoute("default", "{controller}/{action}", new { controller = "Home", action = "Index" }); return builder.Build(); } private static RouteBuilder GetRouteBuilder(IServiceProvider serviceProvider) { var app = new Mock(MockBehavior.Strict); app .SetupGet(a => a.ApplicationServices) .Returns(serviceProvider); var builder = new RouteBuilder(app.Object); var handler = new Mock(MockBehavior.Strict); handler .Setup(router => router.GetVirtualPath(It.IsAny())) .Returns((VirtualPathData)null); builder.DefaultHandler = handler.Object; return builder; } private static ActionContext GetActionContext(IServiceProvider serviceProvider, RouteData routeData) { // Set IServiceProvider properties because TemplateRoute gets services (e.g. an ILoggerFactory instance) // through the HttpContext. var httpContext = new DefaultHttpContext { RequestServices = serviceProvider, }; if (routeData == null) { routeData = new RouteData { Routers = { Mock.Of(), }, }; } return new ActionContext(httpContext, routeData, new ActionDescriptor()); } private static ServiceCollection GetServiceCollection() { var serviceCollection = new ServiceCollection(); serviceCollection .AddSingleton() .AddSingleton(new NullLoggerFactory()); serviceCollection.AddOptions(); serviceCollection.AddRouting(); serviceCollection.AddSingleton( provider => new DefaultInlineConstraintResolver(provider.GetRequiredService>(), provider)); return serviceCollection; } private class MockUrlHelper : IUrlHelper { private readonly string _routeName; private readonly string _url; public MockUrlHelper(string url, string routeName) { _routeName = routeName; _url = url; } public ActionContext ActionContext { get; } public object RouteValues { get; private set; } public string Action(UrlActionContext actionContext) { throw new NotImplementedException(); } public string Content(string contentPath) { throw new NotImplementedException(); } public bool IsLocalUrl(string url) { throw new NotImplementedException(); } public string Link(string routeName, object values) { throw new NotImplementedException(); } public string RouteUrl(UrlRouteContext routeContext) { Assert.Equal(_routeName, routeContext.RouteName); Assert.Null(routeContext.Protocol); Assert.Null(routeContext.Host); Assert.Null(routeContext.Fragment); RouteValues = routeContext.Values; return _url; } } private class TestableRemoteAttribute : RemoteAttribute { public TestableRemoteAttribute(string routeName) : base(routeName) { } public TestableRemoteAttribute(string action, string controller) : base(action, controller) { } public TestableRemoteAttribute(string action, string controller, string areaName) : base(action, controller, areaName) { } public new string RouteName { get { return base.RouteName; } } public new RouteValueDictionary RouteData { get { return base.RouteData; } } public string InvokeGetUrl(ClientModelValidationContext context) { return base.GetUrl(context); } } } }