diff --git a/src/Microsoft.AspNet.Mvc.Core/AreaReference.cs b/src/Microsoft.AspNet.Mvc.Core/AreaReference.cs deleted file mode 100644 index c4fccb132f..0000000000 --- a/src/Microsoft.AspNet.Mvc.Core/AreaReference.cs +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. - -namespace System.Web.Mvc -{ - /// - /// Controls interpretation of a controller name when constructing a . - /// - public enum AreaReference - { - /// - /// Find the controller in the current area. - /// - UseCurrent = 0, - - /// - /// Find the controller in the root area. - /// - UseRoot = 1, - } -} diff --git a/src/Microsoft.AspNet.Mvc.Core/Internal/ModelClientValidationRemoteRule.cs b/src/Microsoft.AspNet.Mvc.Core/Internal/ModelClientValidationRemoteRule.cs index 374da299a4..272ba952d6 100644 --- a/src/Microsoft.AspNet.Mvc.Core/Internal/ModelClientValidationRemoteRule.cs +++ b/src/Microsoft.AspNet.Mvc.Core/Internal/ModelClientValidationRemoteRule.cs @@ -1,26 +1,47 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. -using System.Diagnostics.CodeAnalysis; -using System.Runtime.CompilerServices; +using System; +using Microsoft.AspNet.Mvc.ModelBinding; -namespace System.Web.Mvc +namespace Microsoft.AspNet.Mvc.Internal { - [TypeForwardedFrom("System.Web.Mvc, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35")] + /// + /// containing information for HTML attribute generation in fields a + /// targets. + /// public class ModelClientValidationRemoteRule : ModelClientValidationRule { - [SuppressMessage("Microsoft.Design", "CA1054:UriParametersShouldNotBeStrings", Justification = "The value is a not a regular URL since it may contain ~/ ASP.NET-specific characters")] - public ModelClientValidationRemoteRule(string errorMessage, string url, string httpMethod, string additionalFields) - { - ErrorMessage = errorMessage; - ValidationType = "remote"; - ValidationParameters["url"] = url; + private const string RemoteValidationType = "remote"; + private const string AdditionalFieldsValidationParameter = "additionalfields"; + private const string TypeValidationParameter = "type"; + private const string UrlValidationParameter = "url"; - if (!String.IsNullOrEmpty(httpMethod)) + /// + /// Initializes a new instance of the class. + /// + /// Error message client should display when validation fails. + /// URL where client should send a validation request. + /// + /// HTTP method ("GET" or "POST") client should use when sending a validation request. + /// + /// + /// Comma-separated names of fields the client should include in a validation request. + /// + public ModelClientValidationRemoteRule( + string errorMessage, + string url, + string httpMethod, + string additionalFields) + : base(validationType: RemoteValidationType, errorMessage: errorMessage) + { + ValidationParameters[UrlValidationParameter] = url; + if (!string.IsNullOrEmpty(httpMethod)) { - ValidationParameters["type"] = httpMethod; + ValidationParameters[TypeValidationParameter] = httpMethod; } - ValidationParameters["additionalfields"] = additionalFields; + ValidationParameters[AdditionalFieldsValidationParameter] = additionalFields; } } } diff --git a/src/Microsoft.AspNet.Mvc.Core/Properties/Resources.Designer.cs b/src/Microsoft.AspNet.Mvc.Core/Properties/Resources.Designer.cs index 08263a8077..2ecefcaaee 100644 --- a/src/Microsoft.AspNet.Mvc.Core/Properties/Resources.Designer.cs +++ b/src/Microsoft.AspNet.Mvc.Core/Properties/Resources.Designer.cs @@ -1706,6 +1706,38 @@ namespace Microsoft.AspNet.Mvc.Core return string.Format(CultureInfo.CurrentCulture, GetString("ApiExplorer_UnsupportedAction"), p0); } + /// + /// No URL for remote validation could be found. + /// + internal static string RemoteAttribute_NoUrlFound + { + get { return GetString("RemoteAttribute_NoUrlFound"); } + } + + /// + /// No URL for remote validation could be found. + /// + internal static string FormatRemoteAttribute_NoUrlFound() + { + return GetString("RemoteAttribute_NoUrlFound"); + } + + /// + /// '{0}' is invalid. + /// + internal static string RemoteAttribute_RemoteValidationFailed + { + get { return GetString("RemoteAttribute_RemoteValidationFailed"); } + } + + /// + /// '{0}' is invalid. + /// + internal static string FormatRemoteAttribute_RemoteValidationFailed(object p0) + { + return string.Format(CultureInfo.CurrentCulture, GetString("RemoteAttribute_RemoteValidationFailed"), p0); + } + private static string GetString(string name, params string[] formatterNames) { var value = _resourceManager.GetString(name); diff --git a/src/Microsoft.AspNet.Mvc.Core/RemoteAttribute.cs b/src/Microsoft.AspNet.Mvc.Core/RemoteAttribute.cs index 21868ee211..e8daaf395e 100644 --- a/src/Microsoft.AspNet.Mvc.Core/RemoteAttribute.cs +++ b/src/Microsoft.AspNet.Mvc.Core/RemoteAttribute.cs @@ -1,158 +1,251 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. +// Copyright (c) Microsoft Open Technologies, Inc. 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.ComponentModel.DataAnnotations; -using System.Diagnostics.CodeAnalysis; using System.Globalization; -using System.Web.Mvc.Properties; -using System.Web.Routing; +using System.Linq; +using Microsoft.AspNet.Mvc.Core; +using Microsoft.AspNet.Mvc.Internal; +using Microsoft.AspNet.Mvc.ModelBinding; +using Microsoft.AspNet.Routing; +using Microsoft.Framework.DependencyInjection; -namespace System.Web.Mvc +namespace Microsoft.AspNet.Mvc { - [AttributeUsage(AttributeTargets.Property)] - [SuppressMessage("Microsoft.Design", "CA1019:DefineAccessorsForAttributeArguments", Justification = "The constructor parameters are used to feed RouteData, which is public.")] - [SuppressMessage("Microsoft.Performance", "CA1813:AvoidUnsealedAttributes", Justification = "This attribute is designed to be a base class for other attributes.")] - public class RemoteAttribute : ValidationAttribute, IClientValidatable + /// + /// A which configures Unobtrusive validation to send an Ajax request to the + /// web site. The invoked action should return JSON indicating whether the value is valid. + /// + /// Does no server-side validation of the final form submission. + [AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)] + public class RemoteAttribute : ValidationAttribute, IClientModelValidator { - private string _additionalFields; - private string[] _additonalFieldsSplit = new string[0]; + private string _additionalFields = string.Empty; + private string[] _additionalFieldsSplit = new string[0]; + /// + /// Initializes a new instance of the class. + /// + /// + /// Intended for subclasses that support URL generation with no route, action, or controller names. + /// protected RemoteAttribute() - : base(MvcResources.RemoteAttribute_RemoteValidationFailed) + : base(Resources.RemoteAttribute_RemoteValidationFailed) { RouteData = new RouteValueDictionary(); } + /// + /// Initializes a new instance of the class. + /// + /// + /// The route name used when generating the URL where client should send a validation request. + /// + /// + /// Finds the in any area of the application. + /// public RemoteAttribute(string routeName) : this() { - if (String.IsNullOrWhiteSpace(routeName)) - { - throw new ArgumentException(MvcResources.Common_NullOrEmpty, "routeName"); - } - RouteName = routeName; } + /// + /// Initializes a new instance of the class. + /// + /// + /// The action name used when generating the URL where client should send a validation request. + /// + /// + /// The controller name used when generating the URL where client should send a validation request. + /// + /// + /// + /// If either or is null, uses the corresponding + /// ambient value. + /// + /// Finds the in the current area. + /// public RemoteAttribute(string action, string controller) - : - this(action, controller, null /* areaName */) - { - } - - public RemoteAttribute(string action, string controller, string areaName) : this() { - if (String.IsNullOrWhiteSpace(action)) + if (action != null) { - throw new ArgumentException(MvcResources.Common_NullOrEmpty, "action"); - } - if (String.IsNullOrWhiteSpace(controller)) - { - throw new ArgumentException(MvcResources.Common_NullOrEmpty, "controller"); + RouteData["action"] = action; } - RouteData["controller"] = controller; - RouteData["action"] = action; - - if (!String.IsNullOrWhiteSpace(areaName)) + if (controller != null) { - RouteData["area"] = areaName; + RouteData["controller"] = controller; } } /// /// Initializes a new instance of the class. /// - /// The route name. - /// The name of the controller. - /// - /// Find the controller in the root if . Otherwise look in the current area. + /// + /// The action name used when generating the URL where client should send a validation request. /// - public RemoteAttribute(string action, string controller, AreaReference areaReference) + /// + /// The controller name used when generating the URL where client should send a validation request. + /// + /// The name of the area containing the . + /// + /// + /// If either or is null, uses the corresponding + /// ambient value. + /// + /// If is null, finds the in the root area. + /// Use the overload find the in + /// the current area. Or explicitly pass the current area's name as the argument to + /// this overload. + /// + public RemoteAttribute(string action, string controller, string areaName) : this(action, controller) { - if (areaReference == AreaReference.UseRoot) - { - RouteData["area"] = null; - } + RouteData["area"] = areaName; } + /// + /// Gets or sets the HTTP method ("Get" or "Post") client should use when sending a validation + /// request. + /// public string HttpMethod { get; set; } + /// + /// Gets or sets the comma-separated names of fields the client should include in a validation request. + /// public string AdditionalFields { - get { return _additionalFields ?? String.Empty; } + get { return _additionalFields; } set { - _additionalFields = value; - _additonalFieldsSplit = AuthorizeAttribute.SplitString(value); + _additionalFields = value ?? string.Empty; + _additionalFieldsSplit = SplitAndTrimPropertyNames(value) + .Select(field => FormatPropertyForClientValidation(field)) + .ToArray(); } } - protected RouteValueDictionary RouteData { get; private set; } + /// + /// Gets the used when generating the URL where client should send a + /// validation request. + /// + protected RouteValueDictionary RouteData { get; } + /// + /// Gets or sets the route name used when generating the URL where client should send a validation request. + /// protected string RouteName { get; set; } - protected virtual RouteCollection Routes - { - get { return RouteTable.Routes; } - } - + /// + /// Formats and for use in generated HTML. + /// + /// + /// Name of the property associated with this instance. + /// + /// Comma-separated names of fields the client should include in a validation request. + /// + /// Excludes any whitespace from in the return value. + /// Prefixes each field name in the return value with "*.". + /// public string FormatAdditionalFieldsForClientValidation(string property) { - if (String.IsNullOrEmpty(property)) + if (string.IsNullOrEmpty(property)) { - throw new ArgumentException(MvcResources.Common_NullOrEmpty, "property"); + throw new ArgumentException(Resources.ArgumentCannotBeNullOrEmpty, "property"); } - string delimitedAdditionalFields = FormatPropertyForClientValidation(property); - - foreach (string field in _additonalFieldsSplit) + var delimitedAdditionalFields = string.Join(",", _additionalFieldsSplit); + if (!string.IsNullOrEmpty(delimitedAdditionalFields)) { - delimitedAdditionalFields += "," + FormatPropertyForClientValidation(field); + delimitedAdditionalFields = "," + delimitedAdditionalFields; } - return delimitedAdditionalFields; + var formattedString = FormatPropertyForClientValidation(property) + delimitedAdditionalFields; + + return formattedString; } + /// + /// Formats for use in generated HTML. + /// + /// One field name the client should include in a validation request. + /// Name of a field the client should include in a validation request. + /// Returns with a "*." prefix. public static string FormatPropertyForClientValidation(string property) { - if (String.IsNullOrEmpty(property)) + if (string.IsNullOrEmpty(property)) { - throw new ArgumentException(MvcResources.Common_NullOrEmpty, "property"); + throw new ArgumentException(Resources.ArgumentCannotBeNullOrEmpty, "property"); } + return "*." + property; } - [SuppressMessage("Microsoft.Design", "CA1055:UriReturnValuesShouldNotBeStrings", Justification = "The value is a not a regular URL since it may contain ~/ ASP.NET-specific characters")] - protected virtual string GetUrl(ControllerContext controllerContext) + /// + /// Returns the URL where the client should send a validation request. + /// + /// The used to generate the URL. + /// The URL where the client should send a validation request. + protected virtual string GetUrl([NotNull] ClientModelValidationContext context) { - var pathData = Routes.GetVirtualPathForArea(controllerContext.RequestContext, - RouteName, - RouteData); - - if (pathData == null) + var urlHelper = context.RequestServices.GetRequiredService(); + var url = urlHelper.RouteUrl(RouteName, values: RouteData, protocol: null, host: null, fragment: null); + if (url == null) { - throw new InvalidOperationException(MvcResources.RemoteAttribute_NoUrlFound); + throw new InvalidOperationException(Resources.RemoteAttribute_NoUrlFound); } - return pathData.VirtualPath; + return url; } + /// public override string FormatErrorMessage(string name) { - return String.Format(CultureInfo.CurrentCulture, ErrorMessageString, name); + return string.Format(CultureInfo.CurrentCulture, ErrorMessageString, name); } + /// + /// + /// Always returns true since this does no validation itself. + /// Related validations occur only when the client sends a validation request. + /// public override bool IsValid(object value) { return true; } - public IEnumerable GetClientValidationRules(ModelMetadata metadata, ControllerContext context) + /// + /// + /// Thrown if unable to generate a target URL for a validation request. + /// + public virtual IEnumerable GetClientValidationRules( + [NotNull] ClientModelValidationContext context) { - yield return new ModelClientValidationRemoteRule(FormatErrorMessage(metadata.GetDisplayName()), GetUrl(context), HttpMethod, FormatAdditionalFieldsForClientValidation(metadata.PropertyName)); + var metadata = context.ModelMetadata; + var rule = new ModelClientValidationRemoteRule( + FormatErrorMessage(metadata.GetDisplayName()), + GetUrl(context), + HttpMethod, + FormatAdditionalFieldsForClientValidation(metadata.PropertyName)); + + return new[] { rule }; + } + + private static IEnumerable SplitAndTrimPropertyNames(string original) + { + if (string.IsNullOrEmpty(original)) + { + return new string[0]; + } + + var split = original.Split(',') + .Select(piece => piece.Trim()) + .Where(trimmed => !string.IsNullOrEmpty(trimmed)); + return split; } } } diff --git a/src/Microsoft.AspNet.Mvc.Core/Rendering/Html/DefaultHtmlGenerator.cs b/src/Microsoft.AspNet.Mvc.Core/Rendering/Html/DefaultHtmlGenerator.cs index 82b577eb9e..51c4d30eeb 100644 --- a/src/Microsoft.AspNet.Mvc.Core/Rendering/Html/DefaultHtmlGenerator.cs +++ b/src/Microsoft.AspNet.Mvc.Core/Rendering/Html/DefaultHtmlGenerator.cs @@ -724,16 +724,15 @@ namespace Microsoft.AspNet.Mvc.Rendering string name) { var validatorProvider = _bindingContextAccessor.Value.ValidatorProvider; - metadata = metadata ?? ExpressionMetadataProvider.FromStringExpression(name, viewContext.ViewData, _metadataProvider); + var validationContext = + new ClientModelValidationContext(metadata, _metadataProvider, viewContext.HttpContext.RequestServices); - return - validatorProvider + return validatorProvider .GetValidators(metadata) .OfType() - .SelectMany(v => v.GetClientValidationRules( - new ClientModelValidationContext(metadata, _metadataProvider))); + .SelectMany(v => v.GetClientValidationRules(validationContext)); } internal static string EvalString(ViewContext viewContext, string key, string format) diff --git a/src/Microsoft.AspNet.Mvc.Core/Resources.resx b/src/Microsoft.AspNet.Mvc.Core/Resources.resx index 51c569c1c4..2b17cde37e 100644 --- a/src/Microsoft.AspNet.Mvc.Core/Resources.resx +++ b/src/Microsoft.AspNet.Mvc.Core/Resources.resx @@ -445,4 +445,10 @@ The action '{0}' has ApiExplorer enabled, but is using conventional routing. Only actions which use attribute routing support ApiExplorer. + + No URL for remote validation could be found. + + + '{0}' is invalid. + \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/Metadata/CachedDataAnnotationsModelMetadata.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/Metadata/CachedDataAnnotationsModelMetadata.cs index 8aa42e6555..3448715089 100644 --- a/src/Microsoft.AspNet.Mvc.ModelBinding/Metadata/CachedDataAnnotationsModelMetadata.cs +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/Metadata/CachedDataAnnotationsModelMetadata.cs @@ -351,19 +351,6 @@ namespace Microsoft.AspNet.Mvc.ModelBinding } } - private static IEnumerable SplitString(string original) - { - if (string.IsNullOrEmpty(original)) - { - return new string[0]; - } - - var split = original.Split(',') - .Select(piece => piece.Trim()) - .Where(trimmed => !string.IsNullOrEmpty(trimmed)); - return split; - } - private class CompositePredicateProvider : IPropertyBindingPredicateProvider { private readonly IPropertyBindingPredicateProvider[] _providers; diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/Validation/ClientModelValidationContext.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/Validation/ClientModelValidationContext.cs index 05e47f2249..9e282ef464 100644 --- a/src/Microsoft.AspNet.Mvc.ModelBinding/Validation/ClientModelValidationContext.cs +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/Validation/ClientModelValidationContext.cs @@ -1,19 +1,25 @@ // Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +using System; + namespace Microsoft.AspNet.Mvc.ModelBinding { public class ClientModelValidationContext { public ClientModelValidationContext([NotNull] ModelMetadata metadata, - [NotNull] IModelMetadataProvider metadataProvider) + [NotNull] IModelMetadataProvider metadataProvider, + [NotNull] IServiceProvider requestServices) { ModelMetadata = metadata; MetadataProvider = metadataProvider; + RequestServices = requestServices; } - public ModelMetadata ModelMetadata { get; private set; } + public ModelMetadata ModelMetadata { get; } - public IModelMetadataProvider MetadataProvider { get; private set; } + public IModelMetadataProvider MetadataProvider { get; } + + public IServiceProvider RequestServices { get; } } } diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/Validation/DataAnnotationsModelValidator.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/Validation/DataAnnotationsModelValidator.cs index ab4a8751e1..7bc9d64e14 100644 --- a/src/Microsoft.AspNet.Mvc.ModelBinding/Validation/DataAnnotationsModelValidator.cs +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/Validation/DataAnnotationsModelValidator.cs @@ -61,6 +61,12 @@ namespace Microsoft.AspNet.Mvc.ModelBinding public virtual IEnumerable GetClientValidationRules( [NotNull] ClientModelValidationContext context) { + var customValidator = Attribute as IClientModelValidator; + if (customValidator != null) + { + return customValidator.GetClientValidationRules(context); + } + return Enumerable.Empty(); } diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/RemoteAttributeTest.cs b/test/Microsoft.AspNet.Mvc.Core.Test/RemoteAttributeTest.cs index 9b7ff69921..e460886f6b 100644 --- a/test/Microsoft.AspNet.Mvc.Core.Test/RemoteAttributeTest.cs +++ b/test/Microsoft.AspNet.Mvc.Core.Test/RemoteAttributeTest.cs @@ -1,37 +1,54 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. +// Copyright (c) Microsoft Open Technologies, Inc. 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.Web.Routing; -using Microsoft.TestCommon; +using Microsoft.AspNet.Builder; +using Microsoft.AspNet.Http.Core; +using Microsoft.AspNet.Mvc.ModelBinding; +using Microsoft.AspNet.Routing; +using Microsoft.Framework.DependencyInjection; +using Microsoft.Framework.DependencyInjection.Fallback; +using Microsoft.Framework.Logging; +using Microsoft.Framework.OptionsModel; using Moq; +using Xunit; -namespace System.Web.Mvc.Test +namespace Microsoft.AspNet.Mvc { public class RemoteAttributeTest { - // Good route name, bad route name - // Controller + Action + private static readonly IModelMetadataProvider _metadataProvider = new EmptyModelMetadataProvider(); + private static readonly ModelMetadata _metadata = _metadataProvider.GetMetadataForProperty( + modelAccessor: null, + containerType: typeof(string), + propertyName: "Length"); - [Fact] - public void GuardClauses() + public static TheoryData SomeNames { - // Act & Assert - Assert.ThrowsArgumentNullOrEmpty( - () => new RemoteAttribute(null, "controller"), - "action"); - Assert.ThrowsArgumentNullOrEmpty( - () => new RemoteAttribute("action", null), - "controller"); - Assert.ThrowsArgumentNullOrEmpty( - () => new RemoteAttribute(null), - "routeName"); - Assert.ThrowsArgumentNullOrEmpty( - () => RemoteAttribute.FormatPropertyForClientValidation(String.Empty), - "property"); - Assert.ThrowsArgumentNullOrEmpty( - () => new RemoteAttribute("foo").FormatAdditionalFieldsForClientValidation(String.Empty), - "property"); + 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] @@ -43,408 +60,583 @@ namespace System.Web.Mvc.Test } [Fact] - public void BadRouteNameThrows() + public void Constructor_WithNullAction_IgnoresArgument() { - // Arrange - ControllerContext context = new ControllerContext(); - ModelMetadata metadata = ModelMetadataProviders.Current.GetMetadataForType(null, typeof(object)); - TestableRemoteAttribute attribute = new TestableRemoteAttribute("RouteName"); - - // Act & Assert - Assert.Throws( - () => new List(attribute.GetClientValidationRules(metadata, context)), - "A route named 'RouteName' could not be found in the route collection.\r\nParameter name: name"); - } - - [Fact] - public void NoRouteWithActionControllerThrows() - { - // Arrange - ControllerContext context = new ControllerContext(); - ModelMetadata metadata = ModelMetadataProviders.Current.GetMetadataForProperty(null, typeof(string), "Length"); - TestableRemoteAttribute attribute = new TestableRemoteAttribute("Action", "Controller"); - - // Act & Assert - Assert.Throws( - () => new List(attribute.GetClientValidationRules(metadata, context)), - "No url for remote validation could be found."); - } - - [Fact] - public void GoodRouteNameReturnsCorrectClientData() - { - // Arrange - string url = null; - ModelMetadata metadata = ModelMetadataProviders.Current.GetMetadataForProperty(null, typeof(string), "Length"); - TestableRemoteAttribute attribute = new TestableRemoteAttribute("RouteName"); - attribute.RouteTable.Add("RouteName", new Route("my/url", new MvcRouteHandler())); - - // Act - ModelClientValidationRule rule = attribute.GetClientValidationRules(metadata, GetMockControllerContext(url)).Single(); + // Arrange & Act + var attribute = new TestableRemoteAttribute(action: null, controller: "AController"); // Assert - Assert.Equal("remote", rule.ValidationType); - Assert.Equal("'Length' is invalid.", rule.ErrorMessage); - Assert.Equal(2, rule.ValidationParameters.Count); - Assert.Equal("/my/url", rule.ValidationParameters["url"]); + var keyValuePair = Assert.Single(attribute.RouteData); + Assert.Equal(keyValuePair.Key, "controller"); } [Fact] - public void ActionControllerReturnsCorrectClientDataWithoutNamedParameters() + public void Constructor_WithNullController_IgnoresArgument() { - // Arrange - string url = null; - - ModelMetadata metadata = ModelMetadataProviders.Current.GetMetadataForProperty(null, typeof(string), "Length"); - TestableRemoteAttribute attribute = new TestableRemoteAttribute("Action", "Controller"); - attribute.RouteTable.Add(new Route("{controller}/{action}", new MvcRouteHandler())); - - // Act - ModelClientValidationRule rule = attribute.GetClientValidationRules(metadata, GetMockControllerContext(url)).Single(); + // Arrange & Act + var attribute = new TestableRemoteAttribute("AnAction", controller: null); // Assert + var keyValuePair = Assert.Single(attribute.RouteData); + Assert.Equal(keyValuePair.Key, "action"); + 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); + } + + [Theory] + [MemberData(nameof(NullOrEmptyNames))] + public void FormatAdditionalFieldsForClientValidation_WithInvalidPropertyName_Throws(string property) + { + // Arrange + var attribute = new RemoteAttribute(routeName: "default"); + var expected = "Value cannot be null or empty." + Environment.NewLine + "Parameter name: property"; + + // Act & Assert + var exception = Assert.Throws( + "property", + () => attribute.FormatAdditionalFieldsForClientValidation(property)); + Assert.Equal(expected, exception.Message); + } + + [Theory] + [MemberData(nameof(NullOrEmptyNames))] + public void FormatPropertyForClientValidation_WithInvalidPropertyName_Throws(string property) + { + // Arrange + var expected = "Value cannot be null or empty." + Environment.NewLine + "Parameter name: property"; + + // Act & Assert + var exception = Assert.Throws( + "property", + () => RemoteAttribute.FormatPropertyForClientValidation(property)); + Assert.Equal(expected, exception.Message); + } + + [Fact] + public void GetClientValidationRules_WithBadRouteName_Throws() + { + // Arrange + var attribute = new RemoteAttribute("nonexistentRoute"); + var context = GetValidationContextWithArea(currentArea: null); + + // Act & Assert + var exception = Assert.Throws(() => attribute.GetClientValidationRules(context)); + Assert.Equal("No URL for remote validation could be found.", exception.Message); + } + + [Fact] + public void GetClientValidationRules_WithActionController_NoController_Throws() + { + // Arrange + var attribute = new RemoteAttribute("Action", "Controller"); + var context = GetValidationContextWithNoController(); + + // Act & Assert + var exception = Assert.Throws(() => attribute.GetClientValidationRules(context)); + Assert.Equal("No URL for remote validation could be found.", exception.Message); + } + + [Fact] + public void GetClientValidationRules_WithRoute_CallsUrlHelperWithExpectedValues() + { + // Arrange + var routeName = "RouteName"; + var attribute = new RemoteAttribute(routeName); + var url = "/my/URL"; + var urlHelper = new MockUrlHelper(url, routeName); + var context = GetValidationContext(urlHelper); + + // Act & Assert + var rule = Assert.Single(attribute.GetClientValidationRules(context)); Assert.Equal("remote", rule.ValidationType); Assert.Equal("'Length' is invalid.", rule.ErrorMessage); + Assert.Equal(2, rule.ValidationParameters.Count); - Assert.Equal("/Controller/Action", rule.ValidationParameters["url"]); Assert.Equal("*.Length", rule.ValidationParameters["additionalfields"]); - Assert.Throws( - () => rule.ValidationParameters["type"], - "The given key was not present in the dictionary."); + Assert.Equal(url, rule.ValidationParameters["url"]); + + var routeDictionary = Assert.IsType(urlHelper.RouteValues); + Assert.Empty(routeDictionary); } [Fact] - public void ActionControllerReturnsCorrectClientDataWithNamedParameters() + public void GetClientValidationRules_WithActionController_CallsUrlHelperWithExpectedValues() { // Arrange - string url = null; + var attribute = new RemoteAttribute("Action", "Controller"); + var url = "/Controller/Action"; + var urlHelper = new MockUrlHelper(url, routeName: null); + var context = GetValidationContext(urlHelper); - ModelMetadata metadata = ModelMetadataProviders.Current.GetMetadataForProperty(null, typeof(string), "Length"); - TestableRemoteAttribute attribute = new TestableRemoteAttribute("Action", "Controller"); - attribute.HttpMethod = "POST"; - attribute.AdditionalFields = "Password,ConfirmPassword"; - - attribute.RouteTable.Add(new Route("{controller}/{action}", new MvcRouteHandler())); - - // Act - ModelClientValidationRule rule = attribute.GetClientValidationRules(metadata, GetMockControllerContext(url)).Single(); - - // Assert + // Act & Assert + var rule = Assert.Single(attribute.GetClientValidationRules(context)); Assert.Equal("remote", rule.ValidationType); Assert.Equal("'Length' is invalid.", rule.ErrorMessage); + + Assert.Equal(2, rule.ValidationParameters.Count); + Assert.Equal("*.Length", rule.ValidationParameters["additionalfields"]); + Assert.Equal(url, rule.ValidationParameters["url"]); + + 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 GetClientValidationRules_WithActionController_PropertiesSet_CallsUrlHelperWithExpectedValues() + { + // Arrange + var attribute = new RemoteAttribute("Action", "Controller") + { + HttpMethod = "POST", + AdditionalFields = "Password,ConfirmPassword", + }; + var url = "/Controller/Action"; + var urlHelper = new MockUrlHelper(url, routeName: null); + var context = GetValidationContext(urlHelper); + + // Act & Assert + var rule = Assert.Single(attribute.GetClientValidationRules(context)); + Assert.Equal("remote", rule.ValidationType); + Assert.Equal("'Length' is invalid.", rule.ErrorMessage); + Assert.Equal(3, rule.ValidationParameters.Count); - Assert.Equal("/Controller/Action", rule.ValidationParameters["url"]); Assert.Equal("*.Length,*.Password,*.ConfirmPassword", rule.ValidationParameters["additionalfields"]); Assert.Equal("POST", rule.ValidationParameters["type"]); + Assert.Equal(url, rule.ValidationParameters["url"]); + + 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); } - // Current area is root in this case. [Fact] - public void ActionController_RemoteFindsControllerInCurrentArea() + public void GetClientValidationRules_WithActionControllerArea_CallsUrlHelperWithExpectedValues() { // Arrange - ModelMetadata metadata = ModelMetadataProviders.Current.GetMetadataForProperty(modelAccessor: null, - containerType: typeof(string), propertyName: "Length"); - TestableRemoteAttribute attribute = new TestableRemoteAttribute("Action", "Controller"); - attribute.HttpMethod = "POST"; + var attribute = new RemoteAttribute("Action", "Controller", "Test") + { + HttpMethod = "POST", + }; + var url = "/Test/Controller/Action"; + var urlHelper = new MockUrlHelper(url, routeName: null); + var context = GetValidationContext(urlHelper); - var context = new AreaRegistrationContext("Test", attribute.RouteTable); - context.MapRoute(name: null, url: "Test/{controller}/{action}"); - - attribute.RouteTable.Add(new Route("{controller}/{action}", new MvcRouteHandler())); - - // Act - ModelClientValidationRule rule = - attribute.GetClientValidationRules(metadata, GetMockControllerContext(url: null)).Single(); - - // Assert + // Act & Assert + var rule = Assert.Single(attribute.GetClientValidationRules(context)); Assert.Equal("remote", rule.ValidationType); + Assert.Equal("'Length' is invalid.", rule.ErrorMessage); + + Assert.Equal(3, rule.ValidationParameters.Count); + Assert.Equal("*.Length", rule.ValidationParameters["additionalfields"]); + Assert.Equal("POST", rule.ValidationParameters["type"]); + Assert.Equal(url, rule.ValidationParameters["url"]); + + 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 GetClientValidationRules_WithActionController_FindsControllerInCurrentArea() + { + // Arrange + var attribute = new RemoteAttribute("Action", "Controller"); + var context = GetValidationContextWithArea(currentArea: null); + + // Act & Assert + var rule = Assert.Single(attribute.GetClientValidationRules(context)); + Assert.Equal("remote", rule.ValidationType); + Assert.Equal("'Length' is invalid.", rule.ErrorMessage); + + Assert.Equal(2, rule.ValidationParameters.Count); + Assert.Equal("*.Length", rule.ValidationParameters["additionalfields"]); Assert.Equal("/Controller/Action", rule.ValidationParameters["url"]); } + // Test area is current in this case. [Fact] - public void ActionControllerArea_RemoteFindsControllerInNamedArea() + public void GetClientValidationRules_WithActionControllerInArea_FindsControllerInCurrentArea() { // Arrange - ModelMetadata metadata = ModelMetadataProviders.Current.GetMetadataForProperty(modelAccessor: null, - containerType: typeof(string), propertyName: "Length"); - TestableRemoteAttribute attribute = new TestableRemoteAttribute("Action", "Controller", "Test"); - attribute.HttpMethod = "POST"; + var attribute = new RemoteAttribute("Action", "Controller"); + var context = GetValidationContextWithArea(currentArea: "Test"); - var context = new AreaRegistrationContext("Test", attribute.RouteTable); - context.MapRoute(name: null, url: "Test/{controller}/{action}"); - - attribute.RouteTable.Add(new Route("{controller}/{action}", new MvcRouteHandler())); - - // Act - ModelClientValidationRule rule = - attribute.GetClientValidationRules(metadata, GetMockControllerContext(url: null)).Single(); - - // Assert + // Act & Assert + var rule = Assert.Single(attribute.GetClientValidationRules(context)); Assert.Equal("remote", rule.ValidationType); + Assert.Equal("'Length' is invalid.", rule.ErrorMessage); + + Assert.Equal(2, rule.ValidationParameters.Count); + Assert.Equal("*.Length", rule.ValidationParameters["additionalfields"]); Assert.Equal("/Test/Controller/Action", rule.ValidationParameters["url"]); } - // Current area is root in this case. - [Fact] - public void ActionControllerArea_WithEmptyArea_RemoteFindsControllerInCurrentArea() + // Explicit reference to the (current) root area. + [Theory] + [MemberData(nameof(NullOrEmptyNames))] + public void GetClientValidationRules_WithActionControllerArea_FindsControllerInRootArea(string areaName) { // Arrange - ModelMetadata metadata = ModelMetadataProviders.Current.GetMetadataForProperty(modelAccessor: null, - containerType: typeof(string), propertyName: "Length"); - TestableRemoteAttribute attribute = new TestableRemoteAttribute("Action", "Controller", ""); - attribute.HttpMethod = "POST"; + var attribute = new RemoteAttribute("Action", "Controller", areaName); + var context = GetValidationContextWithArea(currentArea: null); - var context = new AreaRegistrationContext("Test", attribute.RouteTable); - context.MapRoute(name: null, url: "Test/{controller}/{action}"); - - attribute.RouteTable.Add(new Route("{controller}/{action}", new MvcRouteHandler())); - - // Act - ModelClientValidationRule rule = - attribute.GetClientValidationRules(metadata, GetMockControllerContext(url: null)).Single(); - - // Assert + // Act & Assert + var rule = Assert.Single(attribute.GetClientValidationRules(context)); Assert.Equal("remote", rule.ValidationType); + Assert.Equal("'Length' is invalid.", rule.ErrorMessage); + + Assert.Equal(2, rule.ValidationParameters.Count); + Assert.Equal("*.Length", rule.ValidationParameters["additionalfields"]); Assert.Equal("/Controller/Action", rule.ValidationParameters["url"]); } - // Current area is root in this case. - [Fact] - public void ActionControllerAreaReference_WithUseCurrent_RemoteFindsControllerInCurrentArea() + // Test area is current in this case. + [Theory] + [MemberData(nameof(NullOrEmptyNames))] + public void GetClientValidationRules_WithActionControllerAreaInArea_FindsControllerInRootArea(string areaName) { // Arrange - ModelMetadata metadata = ModelMetadataProviders.Current.GetMetadataForProperty(modelAccessor: null, - containerType: typeof(string), propertyName: "Length"); - TestableRemoteAttribute attribute = new TestableRemoteAttribute("Action", "Controller", AreaReference.UseCurrent); - attribute.HttpMethod = "POST"; + var attribute = new RemoteAttribute("Action", "Controller", areaName); + var context = GetValidationContextWithArea(currentArea: "Test"); - var context = new AreaRegistrationContext("Test", attribute.RouteTable); - context.MapRoute(name: null, url: "Test/{controller}/{action}"); - - attribute.RouteTable.Add(new Route("{controller}/{action}", new MvcRouteHandler())); - - // Act - ModelClientValidationRule rule = - attribute.GetClientValidationRules(metadata, GetMockControllerContext(url: null)).Single(); - - // Assert + // Act & Assert + var rule = Assert.Single(attribute.GetClientValidationRules(context)); Assert.Equal("remote", rule.ValidationType); + Assert.Equal("'Length' is invalid.", rule.ErrorMessage); + + Assert.Equal(2, rule.ValidationParameters.Count); + Assert.Equal("*.Length", rule.ValidationParameters["additionalfields"]); Assert.Equal("/Controller/Action", rule.ValidationParameters["url"]); } + // Root area is current in this case. [Fact] - public void ActionControllerAreaReference_WithUseRoot_RemoteFindsControllerInRoot() + public void GetClientValidationRules_WithActionControllerArea_FindsControllerInNamedArea() { // Arrange - ModelMetadata metadata = ModelMetadataProviders.Current.GetMetadataForProperty(modelAccessor: null, - containerType: typeof(string), propertyName: "Length"); - TestableRemoteAttribute attribute = new TestableRemoteAttribute("Action", "Controller", AreaReference.UseRoot); - attribute.HttpMethod = "POST"; + var attribute = new RemoteAttribute("Action", "Controller", "Test"); + var context = GetValidationContextWithArea(currentArea: null); - var context = new AreaRegistrationContext("Test", attribute.RouteTable); - context.MapRoute(name: null, url: "Test/{controller}/{action}"); - - attribute.RouteTable.Add(new Route("{controller}/{action}", new MvcRouteHandler())); - - // Act - ModelClientValidationRule rule = - attribute.GetClientValidationRules(metadata, GetMockControllerContext(url: null)).Single(); - - // Assert + // Act & Assert + var rule = Assert.Single(attribute.GetClientValidationRules(context)); Assert.Equal("remote", rule.ValidationType); - Assert.Equal("/Controller/Action", rule.ValidationParameters["url"]); - } + Assert.Equal("'Length' is invalid.", rule.ErrorMessage); - // Current area is Test in this case. - [Fact] - public void ActionController_InArea_RemoteFindsControllerInCurrentArea() - { - // Arrange - ModelMetadata metadata = ModelMetadataProviders.Current.GetMetadataForProperty(modelAccessor: null, - containerType: typeof(string), propertyName: "Length"); - TestableRemoteAttribute attribute = new TestableRemoteAttribute("Action", "Controller"); - attribute.HttpMethod = "POST"; - - var context = new AreaRegistrationContext("Test", attribute.RouteTable); - context.MapRoute(name: null, url: "Test/{controller}/{action}"); - - attribute.RouteTable.Add(new Route("{controller}/{action}", new MvcRouteHandler())); - - // Act - ModelClientValidationRule rule = - attribute.GetClientValidationRules(metadata, GetMockControllerContextWithArea(url: null, areaName: "Test")) - .Single(); - - // Assert - Assert.Equal("remote", rule.ValidationType); + Assert.Equal(2, rule.ValidationParameters.Count); + Assert.Equal("*.Length", rule.ValidationParameters["additionalfields"]); Assert.Equal("/Test/Controller/Action", rule.ValidationParameters["url"]); } - // Explicit reference to the Test area. + // Explicit reference to the current (Test) area. [Fact] - public void ActionControllerArea_InSameArea_RemoteFindsControllerInNamedArea() + public void GetClientValidationRules_WithActionControllerAreaInArea_FindsControllerInNamedArea() { // Arrange - ModelMetadata metadata = ModelMetadataProviders.Current.GetMetadataForProperty(modelAccessor: null, - containerType: typeof(string), propertyName: "Length"); - TestableRemoteAttribute attribute = new TestableRemoteAttribute("Action", "Controller", "Test"); - attribute.HttpMethod = "POST"; + var attribute = new RemoteAttribute("Action", "Controller", "Test"); + var context = GetValidationContextWithArea(currentArea: "Test"); - var context = new AreaRegistrationContext("Test", attribute.RouteTable); - context.MapRoute(name: null, url: "Test/{controller}/{action}"); - - attribute.RouteTable.Add(new Route("{controller}/{action}", new MvcRouteHandler())); - - // Act - ModelClientValidationRule rule = - attribute.GetClientValidationRules(metadata, GetMockControllerContextWithArea(url: null, areaName: "Test")) - .Single(); - - // Assert + // Act & Assert + var rule = Assert.Single(attribute.GetClientValidationRules(context)); Assert.Equal("remote", rule.ValidationType); + Assert.Equal("'Length' is invalid.", rule.ErrorMessage); + + Assert.Equal(2, rule.ValidationParameters.Count); + Assert.Equal("*.Length", rule.ValidationParameters["additionalfields"]); Assert.Equal("/Test/Controller/Action", rule.ValidationParameters["url"]); } + // Test area is current in this case. [Fact] - public void ActionControllerArea_InArea_RemoteFindsControllerInNamedArea() + public void GetClientValidationRules_WithActionControllerAreaInArea_FindsControllerInDifferentArea() { // Arrange - ModelMetadata metadata = ModelMetadataProviders.Current.GetMetadataForProperty(modelAccessor: null, - containerType: typeof(string), propertyName: "Length"); - TestableRemoteAttribute attribute = new TestableRemoteAttribute("Action", "Controller", "AnotherArea"); - attribute.HttpMethod = "POST"; + var attribute = new RemoteAttribute("Action", "Controller", "AnotherArea"); + var context = GetValidationContextWithArea(currentArea: "Test"); - var context = new AreaRegistrationContext("Test", attribute.RouteTable); - context.MapRoute(name: null, url: "Test/{controller}/{action}"); - context = new AreaRegistrationContext("AnotherArea", attribute.RouteTable); - context.MapRoute(name: null, url: "AnotherArea/{controller}/{action}"); - - attribute.RouteTable.Add(new Route("{controller}/{action}", new MvcRouteHandler())); - - // Act - ModelClientValidationRule rule = - attribute.GetClientValidationRules(metadata, GetMockControllerContextWithArea(url: null, areaName: "Test")) - .Single(); - - // Assert + // Act & Assert + var rule = Assert.Single(attribute.GetClientValidationRules(context)); Assert.Equal("remote", rule.ValidationType); + Assert.Equal("'Length' is invalid.", rule.ErrorMessage); + + Assert.Equal(2, rule.ValidationParameters.Count); + Assert.Equal("*.Length", rule.ValidationParameters["additionalfields"]); Assert.Equal("/AnotherArea/Controller/Action", rule.ValidationParameters["url"]); } - // Current area is Test in this case. - [Fact] - public void ActionControllerArea_WithEmptyAreaInArea_RemoteFindsControllerInCurrentArea() + private static ClientModelValidationContext GetValidationContext(IUrlHelper urlHelper) { - // Arrange - ModelMetadata metadata = ModelMetadataProviders.Current.GetMetadataForProperty(modelAccessor: null, - containerType: typeof(string), propertyName: "Length"); - TestableRemoteAttribute attribute = new TestableRemoteAttribute("Action", "Controller", ""); - attribute.HttpMethod = "POST"; + var serviceCollection = GetServiceCollection(); + serviceCollection.AddInstance(urlHelper); + var serviceProvider = serviceCollection.BuildServiceProvider(); - var context = new AreaRegistrationContext("Test", attribute.RouteTable); - context.MapRoute(name: null, url: "Test/{controller}/{action}"); - - attribute.RouteTable.Add(new Route("{controller}/{action}", new MvcRouteHandler())); - - // Act - ModelClientValidationRule rule = - attribute.GetClientValidationRules(metadata, GetMockControllerContextWithArea(url: null, areaName: "Test")) - .Single(); - - // Assert - Assert.Equal("remote", rule.ValidationType); - Assert.Equal("/Test/Controller/Action", rule.ValidationParameters["url"]); + return new ClientModelValidationContext(_metadata, _metadataProvider, serviceProvider); } - // Current area is Test in this case. - [Fact] - public void ActionControllerAreaReference_WithUseCurrentInArea_RemoteFindsControllerInCurrentArea() + private static ClientModelValidationContext GetValidationContextWithArea(string currentArea) { - // Arrange - ModelMetadata metadata = ModelMetadataProviders.Current.GetMetadataForProperty(modelAccessor: null, - containerType: typeof(string), propertyName: "Length"); - TestableRemoteAttribute attribute = new TestableRemoteAttribute("Action", "Controller", AreaReference.UseCurrent); - attribute.HttpMethod = "POST"; + 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 context = new AreaRegistrationContext("Test", attribute.RouteTable); - context.MapRoute(name: null, url: "Test/{controller}/{action}"); + var contextAccessor = GetContextAccessor(serviceProvider, routeData); + var actionSelector = new Mock(MockBehavior.Strict); + var urlHelper = new UrlHelper(contextAccessor, actionSelector.Object); + serviceCollection.AddInstance(urlHelper); + serviceProvider = serviceCollection.BuildServiceProvider(); - attribute.RouteTable.Add(new Route("{controller}/{action}", new MvcRouteHandler())); - - // Act - ModelClientValidationRule rule = - attribute.GetClientValidationRules(metadata, GetMockControllerContextWithArea(url: null, areaName: "Test")) - .Single(); - - // Assert - Assert.Equal("remote", rule.ValidationType); - Assert.Equal("/Test/Controller/Action", rule.ValidationParameters["url"]); + return new ClientModelValidationContext(_metadata, _metadataProvider, serviceProvider); } - [Fact] - public void ActionControllerAreaReference_WithUseRootInArea_RemoteFindsControllerInRoot() + private static ClientModelValidationContext GetValidationContextWithNoController() { - // Arrange - ModelMetadata metadata = ModelMetadataProviders.Current.GetMetadataForProperty(modelAccessor: null, - containerType: typeof(string), propertyName: "Length"); - TestableRemoteAttribute attribute = new TestableRemoteAttribute("Action", "Controller", AreaReference.UseRoot); - attribute.HttpMethod = "POST"; + var serviceCollection = GetServiceCollection(); + var serviceProvider = serviceCollection.BuildServiceProvider(); + var routeCollection = GetRouteCollectionWithNoController(serviceProvider); + var routeData = new RouteData + { + Routers = + { + routeCollection, + }, + }; - var context = new AreaRegistrationContext("Test", attribute.RouteTable); - context.MapRoute(name: null, url: "Test/{controller}/{action}"); + var contextAccessor = GetContextAccessor(serviceProvider, routeData); + var actionSelector = new Mock(MockBehavior.Strict); + var urlHelper = new UrlHelper(contextAccessor, actionSelector.Object); + serviceCollection.AddInstance(urlHelper); + serviceProvider = serviceCollection.BuildServiceProvider(); - attribute.RouteTable.Add(new Route("{controller}/{action}", new MvcRouteHandler())); - - // Act - ModelClientValidationRule rule = - attribute.GetClientValidationRules(metadata, GetMockControllerContextWithArea(url: null, areaName: "Test")) - .Single(); - - // Assert - Assert.Equal("remote", rule.ValidationType); - Assert.Equal("/Controller/Action", rule.ValidationParameters["url"]); + return new ClientModelValidationContext(_metadata, _metadataProvider, serviceProvider); } - private ControllerContext GetMockControllerContext(string url) + private static IRouter GetRouteCollectionWithArea(IServiceProvider serviceProvider) { - Mock context = new Mock(); - context.Setup(c => c.HttpContext.Request.ApplicationPath) - .Returns("/"); - context.Setup(c => c.HttpContext.Response.ApplyAppPathModifier(It.IsAny())) - .Callback(vpath => url = vpath) - .Returns(() => url); + var builder = GetRouteBuilder(serviceProvider, isBound: true); - return context.Object; + // 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 ControllerContext GetMockControllerContextWithArea(string url, string areaName) + private static IRouter GetRouteCollectionWithNoController(IServiceProvider serviceProvider) { - Mock context = new Mock(); - context.Setup(c => c.HttpContext.Request.ApplicationPath) - .Returns("/"); - context.Setup(c => c.HttpContext.Response.ApplyAppPathModifier(It.IsAny())) - .Callback(vpath => url = vpath) - .Returns(() => url); + var builder = GetRouteBuilder(serviceProvider, isBound: false); + builder.MapRoute("default", "static/route"); - var controllerContext = context.Object; + return builder.Build(); + } - controllerContext.RequestContext.RouteData.DataTokens.Add("area", areaName); + private static RouteBuilder GetRouteBuilder(IServiceProvider serviceProvider, bool isBound) + { + var builder = new RouteBuilder + { + ServiceProvider = serviceProvider, + }; - return controllerContext; + var handler = new Mock(MockBehavior.Strict); + handler + .Setup(router => router.GetVirtualPath(It.IsAny())) + .Callback(context => context.IsBound = isBound) + .Returns((string)null); + builder.DefaultHandler = handler.Object; + + return builder; + } + + private static IScopedInstance GetContextAccessor( + IServiceProvider serviceProvider, + RouteData routeData = null) + { + // Set IServiceProvider properties because TemplateRoute gets services (e.g. an ILoggerFactory instance) + // through the HttpContext. + var httpContext = new DefaultHttpContext + { + ApplicationServices = serviceProvider, + RequestServices = serviceProvider, + }; + + if (routeData == null) + { + routeData = new RouteData + { + Routers = { Mock.Of(), }, + }; + } + + var actionContext = new ActionContext(httpContext, routeData, new ActionDescriptor()); + var contextAccessor = new Mock>(); + contextAccessor + .SetupGet(accessor => accessor.Value) + .Returns(actionContext); + + return contextAccessor.Object; + } + + private static ServiceCollection GetServiceCollection() + { + var serviceCollection = new ServiceCollection(); + serviceCollection.AddInstance(new NullLoggerFactory()); + + var routeOptions = new RouteOptions(); + var accessor = new Mock>(); + accessor + .SetupGet(options => options.Options) + .Returns(routeOptions); + + // DefaultInlineConstraintResolver constructor does not currently check its serviceProvider parameter (e.g. + // for null) and the class does not look up any services. + serviceCollection.AddInstance(new DefaultInlineConstraintResolver( + serviceProvider: null, + routeOptions: accessor.Object)); + + 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 object RouteValues { get; private set; } + + public string Action( + string action, + string controller, + object values, + string protocol, + string host, + string fragment) + { + throw new NotImplementedException(); + } + + public string Content(string contentPath) + { + throw new NotImplementedException(); + } + + public bool IsLocalUrl(string url) + { + throw new NotImplementedException(); + } + + public string RouteUrl(string routeName, object values, string protocol, string host, string fragment) + { + Assert.Equal(_routeName, routeName); + Assert.Null(protocol); + Assert.Null(host); + Assert.Null(fragment); + + RouteValues = values; + + return _url; + } } private class TestableRemoteAttribute : RemoteAttribute { - public RouteCollection RouteTable = new RouteCollection(); - - public TestableRemoteAttribute(string action, string controller, AreaReference areaReference) - : base(action, controller, areaReference) - { - } - - public TestableRemoteAttribute(string action, string controller, string areaName) - : base(action, controller, areaName) + public TestableRemoteAttribute(string routeName) + : base(routeName) { } @@ -453,14 +645,25 @@ namespace System.Web.Mvc.Test { } - public TestableRemoteAttribute(string routeName) - : base(routeName) + public TestableRemoteAttribute(string action, string controller, string areaName) + : base(action, controller, areaName) { } - protected override RouteCollection Routes + public new string RouteName { - get { return RouteTable; } + get + { + return base.RouteName; + } + } + + public new RouteValueDictionary RouteData + { + get + { + return base.RouteData; + } } } } diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/UrlHelperTest.cs b/test/Microsoft.AspNet.Mvc.Core.Test/UrlHelperTest.cs index fc7228709a..0732479ecd 100644 --- a/test/Microsoft.AspNet.Mvc.Core.Test/UrlHelperTest.cs +++ b/test/Microsoft.AspNet.Mvc.Core.Test/UrlHelperTest.cs @@ -13,7 +13,7 @@ using Microsoft.Framework.OptionsModel; using Moq; using Xunit; -namespace Microsoft.AspNet.Mvc.Core.Test +namespace Microsoft.AspNet.Mvc { public class UrlHelperTest { @@ -695,7 +695,7 @@ namespace Microsoft.AspNet.Mvc.Core.Test private static UrlHelper CreateUrlHelperWithRouteCollection(string appPrefix) { var routeCollection = GetRouter(); - return CreateUrlHelper("/app", routeCollection); + return CreateUrlHelper(appPrefix, routeCollection); } private static IRouter GetRouter() @@ -708,13 +708,9 @@ namespace Microsoft.AspNet.Mvc.Core.Test var rt = new RouteBuilder(); var target = new Mock(MockBehavior.Strict); target - .Setup(e => e.GetVirtualPath(It.IsAny())) - .Callback(c => - { - rt.ToString(); - c.IsBound = true; - }) - .Returns(rc => null); + .Setup(router => router.GetVirtualPath(It.IsAny())) + .Callback(context => context.IsBound = true) + .Returns(context => null); rt.DefaultHandler = target.Object; var serviceProviderMock = new Mock(); var accessorMock = new Mock>(); diff --git a/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Validation/CompareAttributeAdapterTest.cs b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Validation/CompareAttributeAdapterTest.cs index 5135f805d2..36ff524623 100644 --- a/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Validation/CompareAttributeAdapterTest.cs +++ b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Validation/CompareAttributeAdapterTest.cs @@ -3,6 +3,8 @@ using System.ComponentModel.DataAnnotations; using Microsoft.AspNet.Testing; +using Microsoft.Framework.DependencyInjection; +using Microsoft.Framework.DependencyInjection.Fallback; using Xunit; namespace Microsoft.AspNet.Mvc.ModelBinding @@ -17,7 +19,9 @@ namespace Microsoft.AspNet.Mvc.ModelBinding var metadataProvider = new DataAnnotationsModelMetadataProvider(); var metadata = metadataProvider.GetMetadataForProperty(() => null, typeof(PropertyDisplayNameModel), "MyProperty"); var attribute = new CompareAttribute("OtherProperty"); - var context = new ClientModelValidationContext(metadata, metadataProvider); + var serviceCollection = new ServiceCollection(); + var requestServices = serviceCollection.BuildServiceProvider(); + var context = new ClientModelValidationContext(metadata, metadataProvider, requestServices); var adapter = new CompareAttributeAdapter(attribute); // Act @@ -36,7 +40,9 @@ namespace Microsoft.AspNet.Mvc.ModelBinding var metadataProvider = new DataAnnotationsModelMetadataProvider(); var metadata = metadataProvider.GetMetadataForProperty(() => null, typeof(PropertyNameModel), "MyProperty"); var attribute = new CompareAttribute("OtherProperty"); - var context = new ClientModelValidationContext(metadata, metadataProvider); + var serviceCollection = new ServiceCollection(); + var requestServices = serviceCollection.BuildServiceProvider(); + var context = new ClientModelValidationContext(metadata, metadataProvider, requestServices); var adapter = new CompareAttributeAdapter(attribute); // Act @@ -57,7 +63,9 @@ namespace Microsoft.AspNet.Mvc.ModelBinding { ErrorMessage = "Hello '{0}', goodbye '{1}'." }; - var context = new ClientModelValidationContext(metadata, metadataProvider); + var serviceCollection = new ServiceCollection(); + var requestServices = serviceCollection.BuildServiceProvider(); + var context = new ClientModelValidationContext(metadata, metadataProvider, requestServices); var adapter = new CompareAttributeAdapter(attribute); // Act @@ -85,7 +93,9 @@ namespace Microsoft.AspNet.Mvc.ModelBinding ErrorMessageResourceName = "CompareAttributeTestResource", ErrorMessageResourceType = typeof(Test.Resources), }; - var context = new ClientModelValidationContext(metadata, metadataProvider); + var serviceCollection = new ServiceCollection(); + var requestServices = serviceCollection.BuildServiceProvider(); + var context = new ClientModelValidationContext(metadata, metadataProvider, requestServices); var adapter = new CompareAttributeAdapter(attribute); // Act diff --git a/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Validation/DataAnnotationsModelValidatorTest.cs b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Validation/DataAnnotationsModelValidatorTest.cs index ade8168826..678190e862 100644 --- a/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Validation/DataAnnotationsModelValidatorTest.cs +++ b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Validation/DataAnnotationsModelValidatorTest.cs @@ -1,10 +1,15 @@ // Copyright (c) Microsoft Open Technologies, Inc. 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.ComponentModel.DataAnnotations; #if ASPNET50 using System.Linq; +#endif +using Microsoft.Framework.DependencyInjection; +using Microsoft.Framework.DependencyInjection.Fallback; +#if ASPNET50 using Moq; using Moq.Protected; #endif @@ -196,6 +201,51 @@ namespace Microsoft.AspNet.Mvc.ModelBinding } #endif + [Fact] + public void GetClientValidationRules_ReturnsEmptyRuleSet() + { + // Arrange + var attribute = new FileExtensionsAttribute(); + var validator = new DataAnnotationsModelValidator(attribute); + var metadata = _metadataProvider.GetMetadataForProperty( + modelAccessor: null, + containerType: typeof(string), + propertyName: nameof(string.Length)); + var serviceCollection = new ServiceCollection(); + var requestServices = serviceCollection.BuildServiceProvider(); + var context = new ClientModelValidationContext(metadata, _metadataProvider, requestServices); + + // Act + var results = validator.GetClientValidationRules(context); + + // Assert + Assert.Empty(results); + } + + [Fact] + public void GetClientValidationRules_WithIClientModelValidator_CallsAttribute() + { + // Arrange + var attribute = new TestableAttribute(); + var validator = new DataAnnotationsModelValidator(attribute); + var metadata = _metadataProvider.GetMetadataForProperty( + modelAccessor: null, + containerType: typeof(string), + propertyName: nameof(string.Length)); + var serviceCollection = new ServiceCollection(); + var requestServices = serviceCollection.BuildServiceProvider(); + var context = new ClientModelValidationContext(metadata, _metadataProvider, requestServices); + + // Act + var results = validator.GetClientValidationRules(context); + + // Assert + var rule = Assert.Single(results); + Assert.Equal("an error", rule.ErrorMessage); + Assert.Empty(rule.ValidationParameters); + Assert.Equal("testable", rule.ValidationType); + } + [Fact] public void IsRequiredTests() { @@ -221,5 +271,13 @@ namespace Microsoft.AspNet.Mvc.ModelBinding { public string Name { get; set; } } + + private class TestableAttribute : ValidationAttribute, IClientModelValidator + { + public IEnumerable GetClientValidationRules(ClientModelValidationContext context) + { + return new[] { new ModelClientValidationRule(validationType: "testable", errorMessage: "an error") }; + } + } } } diff --git a/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Validation/MaxLengthAttributeAdapterTest.cs b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Validation/MaxLengthAttributeAdapterTest.cs index d0878a1bc6..4d78528e21 100644 --- a/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Validation/MaxLengthAttributeAdapterTest.cs +++ b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Validation/MaxLengthAttributeAdapterTest.cs @@ -3,6 +3,8 @@ using System.ComponentModel.DataAnnotations; using Microsoft.AspNet.Testing; +using Microsoft.Framework.DependencyInjection; +using Microsoft.Framework.DependencyInjection.Fallback; using Xunit; namespace Microsoft.AspNet.Mvc.ModelBinding @@ -18,7 +20,9 @@ namespace Microsoft.AspNet.Mvc.ModelBinding var metadata = provider.GetMetadataForProperty(() => null, typeof(string), "Length"); var attribute = new MaxLengthAttribute(10); var adapter = new MaxLengthAttributeAdapter(attribute); - var context = new ClientModelValidationContext(metadata, provider); + var serviceCollection = new ServiceCollection(); + var requestServices = serviceCollection.BuildServiceProvider(); + var context = new ClientModelValidationContext(metadata, provider, requestServices); // Act var rules = adapter.GetClientValidationRules(context); @@ -42,7 +46,9 @@ namespace Microsoft.AspNet.Mvc.ModelBinding var metadata = provider.GetMetadataForProperty(() => null, typeof(string), propertyName); var attribute = new MaxLengthAttribute(5) { ErrorMessage = message }; var adapter = new MaxLengthAttributeAdapter(attribute); - var context = new ClientModelValidationContext(metadata, provider); + var serviceCollection = new ServiceCollection(); + var requestServices = serviceCollection.BuildServiceProvider(); + var context = new ClientModelValidationContext(metadata, provider, requestServices); // Act var rules = adapter.GetClientValidationRules(context); diff --git a/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Validation/MinLengthAttributeAdapterTest.cs b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Validation/MinLengthAttributeAdapterTest.cs index 70eb2a4a57..d8b5b0b0cb 100644 --- a/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Validation/MinLengthAttributeAdapterTest.cs +++ b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Validation/MinLengthAttributeAdapterTest.cs @@ -3,6 +3,8 @@ using System.ComponentModel.DataAnnotations; using Microsoft.AspNet.Testing; +using Microsoft.Framework.DependencyInjection; +using Microsoft.Framework.DependencyInjection.Fallback; using Xunit; namespace Microsoft.AspNet.Mvc.ModelBinding @@ -18,7 +20,9 @@ namespace Microsoft.AspNet.Mvc.ModelBinding var metadata = provider.GetMetadataForProperty(() => null, typeof(string), "Length"); var attribute = new MinLengthAttribute(6); var adapter = new MinLengthAttributeAdapter(attribute); - var context = new ClientModelValidationContext(metadata, provider); + var serviceCollection = new ServiceCollection(); + var requestServices = serviceCollection.BuildServiceProvider(); + var context = new ClientModelValidationContext(metadata, provider, requestServices); // Act var rules = adapter.GetClientValidationRules(context); @@ -42,7 +46,9 @@ namespace Microsoft.AspNet.Mvc.ModelBinding var metadata = provider.GetMetadataForProperty(() => null, typeof(string), propertyName); var attribute = new MinLengthAttribute(2) { ErrorMessage = message }; var adapter = new MinLengthAttributeAdapter(attribute); - var context = new ClientModelValidationContext(metadata, provider); + var serviceCollection = new ServiceCollection(); + var requestServices = serviceCollection.BuildServiceProvider(); + var context = new ClientModelValidationContext(metadata, provider, requestServices); // Act var rules = adapter.GetClientValidationRules(context); diff --git a/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Validation/RangeAttributeAdapterTest.cs b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Validation/RangeAttributeAdapterTest.cs index c739fe7c42..b608739856 100644 --- a/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Validation/RangeAttributeAdapterTest.cs +++ b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Validation/RangeAttributeAdapterTest.cs @@ -3,6 +3,8 @@ using System.ComponentModel.DataAnnotations; using Microsoft.AspNet.Testing; +using Microsoft.Framework.DependencyInjection; +using Microsoft.Framework.DependencyInjection.Fallback; using Xunit; namespace Microsoft.AspNet.Mvc.ModelBinding @@ -18,7 +20,9 @@ namespace Microsoft.AspNet.Mvc.ModelBinding var metadata = provider.GetMetadataForProperty(() => null, typeof(string), "Length"); var attribute = new RangeAttribute(typeof(decimal), "0", "100"); var adapter = new RangeAttributeAdapter(attribute); - var context = new ClientModelValidationContext(metadata, provider); + var serviceCollection = new ServiceCollection(); + var requestServices = serviceCollection.BuildServiceProvider(); + var context = new ClientModelValidationContext(metadata, provider, requestServices); // Act var rules = adapter.GetClientValidationRules(context); diff --git a/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Validation/RequiredAttributeAdapterTest.cs b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Validation/RequiredAttributeAdapterTest.cs index d4fff33eb2..c70ba8d5f6 100644 --- a/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Validation/RequiredAttributeAdapterTest.cs +++ b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Validation/RequiredAttributeAdapterTest.cs @@ -3,6 +3,8 @@ using System.ComponentModel.DataAnnotations; using Microsoft.AspNet.Testing; +using Microsoft.Framework.DependencyInjection; +using Microsoft.Framework.DependencyInjection.Fallback; using Xunit; namespace Microsoft.AspNet.Mvc.ModelBinding @@ -19,7 +21,9 @@ namespace Microsoft.AspNet.Mvc.ModelBinding var metadata = provider.GetMetadataForProperty(() => null, typeof(string), "Length"); var attribute = new RequiredAttribute(); var adapter = new RequiredAttributeAdapter(attribute); - var context = new ClientModelValidationContext(metadata, provider); + var serviceCollection = new ServiceCollection(); + var requestServices = serviceCollection.BuildServiceProvider(); + var context = new ClientModelValidationContext(metadata, provider, requestServices); // Act var rules = adapter.GetClientValidationRules(context); diff --git a/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Validation/StringLengthAttributeAdapterTest.cs b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Validation/StringLengthAttributeAdapterTest.cs index 52c758fe84..644e905b44 100644 --- a/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Validation/StringLengthAttributeAdapterTest.cs +++ b/test/Microsoft.AspNet.Mvc.ModelBinding.Test/Validation/StringLengthAttributeAdapterTest.cs @@ -3,6 +3,8 @@ using System.ComponentModel.DataAnnotations; using Microsoft.AspNet.Testing; +using Microsoft.Framework.DependencyInjection; +using Microsoft.Framework.DependencyInjection.Fallback; using Xunit; namespace Microsoft.AspNet.Mvc.ModelBinding @@ -18,7 +20,9 @@ namespace Microsoft.AspNet.Mvc.ModelBinding var metadata = provider.GetMetadataForProperty(() => null, typeof(string), "Length"); var attribute = new StringLengthAttribute(8); var adapter = new StringLengthAttributeAdapter(attribute); - var context = new ClientModelValidationContext(metadata, provider); + var serviceCollection = new ServiceCollection(); + var requestServices = serviceCollection.BuildServiceProvider(); + var context = new ClientModelValidationContext(metadata, provider, requestServices); // Act var rules = adapter.GetClientValidationRules(context); @@ -40,7 +44,9 @@ namespace Microsoft.AspNet.Mvc.ModelBinding var metadata = provider.GetMetadataForProperty(() => null, typeof(string), "Length"); var attribute = new StringLengthAttribute(10) { MinimumLength = 3 }; var adapter = new StringLengthAttributeAdapter(attribute); - var context = new ClientModelValidationContext(metadata, provider); + var serviceCollection = new ServiceCollection(); + var requestServices = serviceCollection.BuildServiceProvider(); + var context = new ClientModelValidationContext(metadata, provider, requestServices); // Act var rules = adapter.GetClientValidationRules(context);