From 98c10b6879ce0718d05c8fcecdecdedc1bef008d Mon Sep 17 00:00:00 2001 From: Alexej Timonin Date: Sun, 18 Nov 2018 18:30:23 +0100 Subject: [PATCH] Add PageRemoteAttribute (#8324) * Add PageRemoteAttribute Fixes https://github.com/aspnet/Mvc/issues/8245 --- .../PageRemoteAttribute.cs | 59 ++ .../RemoteAttribute.cs | 200 +---- .../RemoteAttributeBase.cs | 224 ++++++ .../breakingchanges.netcore.json | 4 + .../PageRemoteAttributeTest.cs | 271 +++++++ .../RemoteAttributeBaseTest.cs | 539 ++++++++++++++ .../RemoteAttributeTest.cs | 695 ++---------------- 7 files changed, 1176 insertions(+), 816 deletions(-) create mode 100644 src/Microsoft.AspNetCore.Mvc.ViewFeatures/PageRemoteAttribute.cs create mode 100644 src/Microsoft.AspNetCore.Mvc.ViewFeatures/RemoteAttributeBase.cs create mode 100644 test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/PageRemoteAttributeTest.cs create mode 100644 test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/RemoteAttributeBaseTest.cs diff --git a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/PageRemoteAttribute.cs b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/PageRemoteAttribute.cs new file mode 100644 index 0000000000..9d8978c9be --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/PageRemoteAttribute.cs @@ -0,0 +1,59 @@ +// 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.Mvc.ModelBinding.Validation; +using Microsoft.AspNetCore.Mvc.Routing; +using Microsoft.Extensions.DependencyInjection; +using Resources = Microsoft.AspNetCore.Mvc.ViewFeatures.Resources; + +namespace Microsoft.AspNetCore.Mvc +{ + /// + /// A for razor page handler which configures Unobtrusive validation + /// to send an Ajax request to the web site. The invoked handler 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 PageRemoteAttribute : RemoteAttributeBase + { + /// + /// The handler name used when generating the URL where client should send a validation request. + /// + /// + /// If not set the ambient value will be used when generating the URL. + /// + public string PageHandler { get; set; } + + /// + /// The page name used when generating the URL where client should send a validation request. + /// + /// + /// If not set the ambient value will be used when generating the URL. + /// + public string PageName { get; set; } + + /// + protected override string GetUrl(ClientModelValidationContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + var services = context.ActionContext.HttpContext.RequestServices; + var factory = services.GetRequiredService(); + var urlHelper = factory.GetUrlHelper(context.ActionContext); + + var url = urlHelper.Page(PageName, PageHandler, RouteData); + + if (url == null) + { + throw new InvalidOperationException(Resources.RemoteAttribute_NoUrlFound); + } + + return url; + } + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/RemoteAttribute.cs b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/RemoteAttribute.cs index 1254a79ed6..574f11cf91 100644 --- a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/RemoteAttribute.cs +++ b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/RemoteAttribute.cs @@ -2,34 +2,21 @@ // 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.Globalization; -using System.Linq; -using Microsoft.AspNetCore.Mvc.DataAnnotations; using Microsoft.AspNetCore.Mvc.ModelBinding.Validation; using Microsoft.AspNetCore.Mvc.Routing; -using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Localization; -using Microsoft.Extensions.Options; using Resources = Microsoft.AspNetCore.Mvc.ViewFeatures.Resources; namespace Microsoft.AspNetCore.Mvc { /// - /// A which configures Unobtrusive validation to send an Ajax request to the + /// A for controllers 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 + public class RemoteAttribute : RemoteAttributeBase { - private string _additionalFields = string.Empty; - private string[] _additionalFieldsSplit = Array.Empty(); - private bool _checkedForLocalizer; - private IStringLocalizer _stringLocalizer; - /// /// Initializes a new instance of the class. /// @@ -37,10 +24,7 @@ namespace Microsoft.AspNetCore.Mvc /// Intended for subclasses that support URL generation with no route, action, or controller names. /// protected RemoteAttribute() - : base(errorMessageAccessor: () => Resources.RemoteAttribute_RemoteValidationFailed) - { - RouteData = new RouteValueDictionary(); - } + { } /// /// Initializes a new instance of the class. @@ -112,90 +96,14 @@ namespace Microsoft.AspNetCore.Mvc { 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 => _additionalFields; - set - { - _additionalFields = value ?? string.Empty; - _additionalFieldsSplit = SplitAndTrimPropertyNames(value) - .Select(field => FormatPropertyForClientValidation(field)) - .ToArray(); - } - } - - /// - /// 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; } - /// - /// 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)) - { - throw new ArgumentException(Resources.ArgumentCannotBeNullOrEmpty, nameof(property)); - } - - var delimitedAdditionalFields = string.Join(",", _additionalFieldsSplit); - if (!string.IsNullOrEmpty(delimitedAdditionalFields)) - { - delimitedAdditionalFields = "," + 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)) - { - throw new ArgumentException(Resources.ArgumentCannotBeNullOrEmpty, nameof(property)); - } - - return "*." + property; - } - - /// - /// 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(ClientModelValidationContext context) + /// + protected override string GetUrl(ClientModelValidationContext context) { if (context == null) { @@ -219,101 +127,5 @@ namespace Microsoft.AspNetCore.Mvc return url; } - - /// - public override string FormatErrorMessage(string 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 virtual void AddValidation(ClientModelValidationContext context) - { - if (context == null) - { - throw new ArgumentNullException(nameof(context)); - } - - MergeAttribute(context.Attributes, "data-val", "true"); - - CheckForLocalizer(context); - var errorMessage = GetErrorMessage(context.ModelMetadata.GetDisplayName()); - MergeAttribute(context.Attributes, "data-val-remote", errorMessage); - - MergeAttribute(context.Attributes, "data-val-remote-url", GetUrl(context)); - - if (!string.IsNullOrEmpty(HttpMethod)) - { - MergeAttribute(context.Attributes, "data-val-remote-type", HttpMethod); - } - - var additionalFields = FormatAdditionalFieldsForClientValidation(context.ModelMetadata.PropertyName); - MergeAttribute(context.Attributes, "data-val-remote-additionalfields", additionalFields); - } - - private static void MergeAttribute(IDictionary attributes, string key, string value) - { - if (!attributes.ContainsKey(key)) - { - attributes.Add(key, value); - } - } - - private static IEnumerable SplitAndTrimPropertyNames(string original) - { - if (string.IsNullOrEmpty(original)) - { - return Array.Empty(); - } - - var split = original - .Split(',') - .Select(piece => piece.Trim()) - .Where(trimmed => !string.IsNullOrEmpty(trimmed)); - - return split; - } - - private void CheckForLocalizer(ClientModelValidationContext context) - { - if (!_checkedForLocalizer) - { - _checkedForLocalizer = true; - - var services = context.ActionContext.HttpContext.RequestServices; - var options = services.GetRequiredService>(); - var factory = services.GetService(); - - var provider = options.Value.DataAnnotationLocalizerProvider; - if (factory != null && provider != null) - { - _stringLocalizer = provider( - context.ModelMetadata.ContainerType ?? context.ModelMetadata.ModelType, - factory); - } - } - } - - private string GetErrorMessage(string displayName) - { - if (_stringLocalizer != null && - !string.IsNullOrEmpty(ErrorMessage) && - string.IsNullOrEmpty(ErrorMessageResourceName) && - ErrorMessageResourceType == null) - { - return _stringLocalizer[ErrorMessage, displayName]; - } - - return FormatErrorMessage(displayName); - } } } diff --git a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/RemoteAttributeBase.cs b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/RemoteAttributeBase.cs new file mode 100644 index 0000000000..46b06b54a0 --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/RemoteAttributeBase.cs @@ -0,0 +1,224 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Globalization; +using System.Linq; +using Microsoft.AspNetCore.Mvc.DataAnnotations; +using Microsoft.AspNetCore.Mvc.ModelBinding.Validation; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Localization; +using Microsoft.Extensions.Options; +using Resources = Microsoft.AspNetCore.Mvc.ViewFeatures.Resources; + + +namespace Microsoft.AspNetCore.Mvc +{ + /// + /// A which configures Unobtrusive validation to send an Ajax request to the + /// web site. The invoked endpoint 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 abstract class RemoteAttributeBase : ValidationAttribute, IClientModelValidator + { + private string _additionalFields = string.Empty; + private string[] _additionalFieldsSplit = Array.Empty(); + private bool _checkedForLocalizer; + private IStringLocalizer _stringLocalizer; + + protected RemoteAttributeBase() + : base(errorMessageAccessor: () => Resources.RemoteAttribute_RemoteValidationFailed) + { + RouteData = new RouteValueDictionary(); + } + + /// + /// Gets the used when generating the URL where client should send a + /// validation request. + /// + protected RouteValueDictionary RouteData { get; } + + /// + /// 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 => _additionalFields; + set + { + _additionalFields = value ?? string.Empty; + _additionalFieldsSplit = SplitAndTrimPropertyNames(value) + .Select(field => FormatPropertyForClientValidation(field)) + .ToArray(); + } + } + + /// + /// 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)) + { + throw new ArgumentException(Resources.ArgumentCannotBeNullOrEmpty, nameof(property)); + } + + var delimitedAdditionalFields = string.Join(",", _additionalFieldsSplit); + if (!string.IsNullOrEmpty(delimitedAdditionalFields)) + { + delimitedAdditionalFields = "," + 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)) + { + throw new ArgumentException(Resources.ArgumentCannotBeNullOrEmpty, nameof(property)); + } + + return "*." + property; + } + + /// + /// 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 abstract string GetUrl(ClientModelValidationContext context); + + /// + public override string FormatErrorMessage(string 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; + } + + /// + /// Adds Unobtrusive validation HTML attributes to . + /// + /// + /// to add Unobtrusive validation HTML attributes to. + /// + /// + /// Calls derived implementation of . + /// + public virtual void AddValidation(ClientModelValidationContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + MergeAttribute(context.Attributes, "data-val", "true"); + + CheckForLocalizer(context); + var errorMessage = GetErrorMessage(context.ModelMetadata.GetDisplayName()); + MergeAttribute(context.Attributes, "data-val-remote", errorMessage); + + MergeAttribute(context.Attributes, "data-val-remote-url", GetUrl(context)); + + if (!string.IsNullOrEmpty(HttpMethod)) + { + MergeAttribute(context.Attributes, "data-val-remote-type", HttpMethod); + } + + var additionalFields = FormatAdditionalFieldsForClientValidation(context.ModelMetadata.PropertyName); + MergeAttribute(context.Attributes, "data-val-remote-additionalfields", additionalFields); + } + + private static void MergeAttribute(IDictionary attributes, string key, string value) + { + if (!attributes.ContainsKey(key)) + { + attributes.Add(key, value); + } + } + + private static IEnumerable SplitAndTrimPropertyNames(string original) + { + if (string.IsNullOrEmpty(original)) + { + return Array.Empty(); + } + + var split = original + .Split(',') + .Select(piece => piece.Trim()) + .Where(trimmed => !string.IsNullOrEmpty(trimmed)); + + return split; + } + + private void CheckForLocalizer(ClientModelValidationContext context) + { + if (!_checkedForLocalizer) + { + _checkedForLocalizer = true; + + var services = context.ActionContext.HttpContext.RequestServices; + var options = services.GetRequiredService>(); + var factory = services.GetService(); + + var provider = options.Value.DataAnnotationLocalizerProvider; + if (factory != null && provider != null) + { + _stringLocalizer = provider( + context.ModelMetadata.ContainerType ?? context.ModelMetadata.ModelType, + factory); + } + } + } + + private string GetErrorMessage(string displayName) + { + if (_stringLocalizer != null && + !string.IsNullOrEmpty(ErrorMessage) && + string.IsNullOrEmpty(ErrorMessageResourceName) && + ErrorMessageResourceType == null) + { + return _stringLocalizer[ErrorMessage, displayName]; + } + + return FormatErrorMessage(displayName); + } + } +} + diff --git a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/breakingchanges.netcore.json b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/breakingchanges.netcore.json index 2535632381..638cfe4f77 100644 --- a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/breakingchanges.netcore.json +++ b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/breakingchanges.netcore.json @@ -42,5 +42,9 @@ "TypeId": "public class Microsoft.AspNetCore.Mvc.ViewFeatures.HtmlHelper : Microsoft.AspNetCore.Mvc.ViewFeatures.HtmlHelper, Microsoft.AspNetCore.Mvc.Rendering.IHtmlHelper", "MemberId": "public .ctor(Microsoft.AspNetCore.Mvc.ViewFeatures.IHtmlGenerator htmlGenerator, Microsoft.AspNetCore.Mvc.ViewEngines.ICompositeViewEngine viewEngine, Microsoft.AspNetCore.Mvc.ModelBinding.IModelMetadataProvider metadataProvider, Microsoft.AspNetCore.Mvc.ViewFeatures.Internal.IViewBufferScope bufferScope, System.Text.Encodings.Web.HtmlEncoder htmlEncoder, System.Text.Encodings.Web.UrlEncoder urlEncoder, Microsoft.AspNetCore.Mvc.ViewFeatures.Internal.ExpressionTextCache expressionTextCache)", "Kind": "Removal" + }, + { + "TypeId": "public class Microsoft.AspNetCore.Mvc.RemoteAttribute : System.ComponentModel.DataAnnotations.ValidationAttribute, Microsoft.AspNetCore.Mvc.ModelBinding.Validation.IClientModelValidator", + "Kind": "Removal" } ] \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/PageRemoteAttributeTest.cs b/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/PageRemoteAttributeTest.cs new file mode 100644 index 0000000000..1cf02d3db5 --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/PageRemoteAttributeTest.cs @@ -0,0 +1,271 @@ +// 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.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; +using Resources = Microsoft.AspNetCore.Mvc.ViewFeatures.Resources; + +namespace Microsoft.AspNetCore.Mvc +{ + public class PageRemoteAttributeTest + { + [Fact] + public void GetUrl_CallsUrlHelperWithExpectedValues() + { + // Arrange + var testableAttribute = new TestablePageRemoteAttribute + { + PageName = "Foo", + PageHandler = "Bar" + }; + + var ambientValues = new RouteValueDictionary() + { + ["page"] = "/Foo" + }; + + var routeData = new RouteData(ambientValues) + { + Routers = { Mock.Of() } + }; + + var urlHelper = new MockUrlHelper(url: "/Foo?handler=Bar") + { + ActionContext = GetActionContext(new ServiceCollection().BuildServiceProvider(), routeData) + }; + + var validationContext = GetValidationContext(urlHelper); + + // Act + testableAttribute.InvokeGetUrl(validationContext); + + // Assert + var routeDictionary = Assert.IsType(urlHelper.RouteValues); + + Assert.Equal(2, routeDictionary.Count); + Assert.Equal("/Foo", routeDictionary["page"] as string); + Assert.Equal("Bar", routeDictionary["handler"] as string); + } + + [Fact] + public void GetUrl_WhenUrlHelperReturnsNull_Throws() + { + // Arrange + var testableAttribute = new TestablePageRemoteAttribute + { + PageName = "Foo", + PageHandler = "Bar" + }; + + var ambientValues = new RouteValueDictionary + { + ["page"] = "/Page" + }; + + var routeData = new RouteData(ambientValues) + { + Routers = { Mock.Of() } + }; + + var urlHelper = new MockUrlHelper(url: null) + { + ActionContext = GetActionContext(new ServiceCollection().BuildServiceProvider(), routeData) + }; + + var validationContext = GetValidationContext(urlHelper); + + // Act && Assert + ExceptionAssert.Throws( + () => testableAttribute.InvokeGetUrl(validationContext), + Resources.RemoteAttribute_NoUrlFound); + } + + [Fact] + public void GetUrl_WhenPageNameIsNotSet_WillUsePageNameFromAmbientValues() + { + // Arrange + var testableAttribute = new TestablePageRemoteAttribute() + { + PageHandler = "Handler" + }; + + var ambientValues = new RouteValueDictionary + { + ["page"] = "/Page" + }; + + var routeData = new RouteData(ambientValues) + { + Routers = { Mock.Of() } + }; + + var urlHelper = new MockUrlHelper(url: "/Page?handler=Handler") + { + ActionContext = GetActionContext(new ServiceCollection().BuildServiceProvider(), routeData) + }; + + var validationContext = GetValidationContext(urlHelper); + + // Act + var actualUrl = testableAttribute.InvokeGetUrl(validationContext); + + // Assert + Assert.Equal("/Page?handler=Handler", actualUrl); + } + + [Fact] + public void GetUrl_WhenPageNameAndPageHandlerIsNotSet_WillUseAmbientValues() + { + // Arrange + var testableAttribute = new TestablePageRemoteAttribute(); + + var ambientValues = new RouteValueDictionary + { + ["page"] = "/Page", + ["handler"] = "Handler" + }; + + var routeData = new RouteData(ambientValues) + { + Routers = { Mock.Of() } + }; + + var urlHelper = new MockUrlHelper(url: "/Page?handler=Handler") + { + ActionContext = GetActionContext(new ServiceCollection().BuildServiceProvider(), routeData) + }; + + var validationContext = GetValidationContext(urlHelper); + + // Act + var actualUrl = testableAttribute.InvokeGetUrl(validationContext); + + // Assert + Assert.Equal("/Page?handler=Handler", actualUrl); + } + + private static ClientModelValidationContext GetValidationContext(IUrlHelper urlHelper, RouteData routeData = null) + { + var serviceCollection = GetServiceCollection(); + var factory = new Mock(MockBehavior.Strict); + serviceCollection.AddSingleton(factory.Object); + + var serviceProvider = serviceCollection.BuildServiceProvider(); + var actionContext = GetActionContext(serviceProvider, routeData); + + 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 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 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 class TestablePageRemoteAttribute : PageRemoteAttribute + { + public string InvokeGetUrl(ClientModelValidationContext context) + { + return base.GetUrl(context); + } + } + + private class MockUrlHelper : IUrlHelper + { + private readonly string _url; + + public MockUrlHelper(string url) + { + _url = url; + } + + public ActionContext ActionContext { get; set; } + + 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) + { + RouteValues = routeContext.Values; + + return _url; + } + } + } +} diff --git a/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/RemoteAttributeBaseTest.cs b/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/RemoteAttributeBaseTest.cs new file mode 100644 index 0000000000..70dd3690f8 --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/RemoteAttributeBaseTest.cs @@ -0,0 +1,539 @@ +// 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 Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc.Abstractions; +using Microsoft.AspNetCore.Mvc.DataAnnotations; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Microsoft.AspNetCore.Mvc.ModelBinding.Validation; +using Microsoft.AspNetCore.Mvc.ViewFeatures; +using Microsoft.AspNetCore.Routing; +using Microsoft.AspNetCore.Testing; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Localization; +using Microsoft.Extensions.Options; +using Moq; +using Xunit; +using Resources = Microsoft.AspNetCore.Mvc.ViewFeatures.Test.Resources; + +namespace Microsoft.AspNetCore.Mvc +{ + public class RemoteAttributeBaseTest + { + // 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 IsValidAlwaysReturnsTrue() + { + // Arrange + var attribute = new TestableRemoteAttributeBase(); + + // Act & Assert + Assert.True(attribute.IsValid(value: null)); + } + + [Fact] + public void ErrorMessageProperties_HaveExpectedDefaultValues() + { + // Arrange & Act + var attribute = new TestableRemoteAttributeBase(); + + // Assert + Assert.Null(attribute.ErrorMessage); + Assert.Null(attribute.ErrorMessageResourceName); + Assert.Null(attribute.ErrorMessageResourceType); + } + + [Fact] + [ReplaceCulture] + public void FormatErrorMessage_ReturnsDefaultErrorMessage() + { + // Arrange + // See ViewFeatures.Resources.RemoteAttribute_RemoteValidationFailed. + var expected = "'Property1' is invalid."; + var attribute = new TestableRemoteAttributeBase(); + + // Act + var message = attribute.FormatErrorMessage("Property1"); + + // Assert + Assert.Equal(expected, message); + } + + [Fact] + public void FormatErrorMessage_UsesOverriddenErrorMessage() + { + // Arrange + var expected = "Error about 'Property1' from override."; + var attribute = new TestableRemoteAttributeBase() + { + ErrorMessage = "Error about '{0}' from override.", + }; + + // Act + var message = attribute.FormatErrorMessage("Property1"); + + // Assert + Assert.Equal(expected, message); + } + + [Fact] + [ReplaceCulture] + public void FormatErrorMessage_UsesErrorMessageFromResource() + { + // Arrange + var expected = "Error about 'Property1' from resources."; + var attribute = new TestableRemoteAttributeBase() + { + ErrorMessageResourceName = nameof(Resources.RemoteAttribute_Error), + ErrorMessageResourceType = typeof(Resources) + }; + + // Act + var message = attribute.FormatErrorMessage("Property1"); + + // Assert + Assert.Equal(expected, message); + } + + [Theory] + [MemberData(nameof(NullOrEmptyNames))] + public void FormatAdditionalFieldsForClientValidation_WithInvalidPropertyName_Throws(string property) + { + // Arrange + var attribute = new TestableRemoteAttributeBase(); + var expectedMessage = "Value cannot be null or empty."; + + // Act & Assert + ExceptionAssert.ThrowsArgument( + () => attribute.FormatAdditionalFieldsForClientValidation(property), + "property", + expectedMessage); + } + + [Fact] + public void FormatAdditionalFieldsForClientValidation_WillFormat_AdditionalFields() + { + // Arrange + var attribute = new TestableRemoteAttributeBase + { + AdditionalFields = "FieldOne, FieldTwo" + }; + + // Act + var actual = attribute.FormatAdditionalFieldsForClientValidation("Property"); + + // Assert + var expected = "*.Property,*.FieldOne,*.FieldTwo"; + Assert.Equal(expected, actual); + } + + [Theory] + [MemberData(nameof(NullOrEmptyNames))] + public void FormatPropertyForClientValidation_WithInvalidPropertyName_Throws(string property) + { + // Arrange + var expected = "Value cannot be null or empty."; + + // Act & Assert + ExceptionAssert.ThrowsArgument( + () => RemoteAttributeBase.FormatPropertyForClientValidation(property), + "property", + expected); + } + + [Fact] + public void AddValidation_WithErrorMessage_SetsAttributesAsExpected() + { + // Arrange + var expected = "Error about 'Length' from override."; + var url = "/Controller/Action"; + var context = GetValidationContext(); + var attribute = new TestableRemoteAttributeBase(dummyGetUrlReturnValue: url) + { + HttpMethod = "POST", + ErrorMessage = "Error about '{0}' from override.", + }; + + // Act + attribute.AddValidation(context); + + // Assert + Assert.Collection( + context.Attributes, + kvp => { Assert.Equal("data-val", kvp.Key); Assert.Equal("true", kvp.Value); }, + kvp => + { + Assert.Equal("data-val-remote", kvp.Key); + Assert.Equal(expected, kvp.Value); + }, + kvp => + { + Assert.Equal("data-val-remote-additionalfields", kvp.Key); + Assert.Equal("*.Length", kvp.Value); + }, + kvp => { Assert.Equal("data-val-remote-type", kvp.Key); Assert.Equal("POST", kvp.Value); }, + kvp => { Assert.Equal("data-val-remote-url", kvp.Key); Assert.Equal(url, kvp.Value); }); + } + + [Fact] + public void AddValidation_WithErrorMessageAndLocalizerFactory_SetsAttributesAsExpected() + { + // Arrange + var expected = "Error about 'Length' from override."; + var url = "/Controller/Action"; + var localizerFactory = new Mock(MockBehavior.Strict).Object; + var context = GetValidationContext(localizerFactory); + var attribute = new TestableRemoteAttributeBase(dummyGetUrlReturnValue: url) + { + HttpMethod = "POST", + ErrorMessage = "Error about '{0}' from override.", + }; + + // Act + attribute.AddValidation(context); + + // Assert + Assert.Collection( + context.Attributes, + kvp => { Assert.Equal("data-val", kvp.Key); Assert.Equal("true", kvp.Value); }, + kvp => + { + // IStringLocalizerFactory existence alone is insufficient to change error message. + Assert.Equal("data-val-remote", kvp.Key); + Assert.Equal(expected, kvp.Value); + }, + kvp => + { + Assert.Equal("data-val-remote-additionalfields", kvp.Key); + Assert.Equal("*.Length", kvp.Value); + }, + kvp => { Assert.Equal("data-val-remote-type", kvp.Key); Assert.Equal("POST", kvp.Value); }, + kvp => { Assert.Equal("data-val-remote-url", kvp.Key); Assert.Equal(url, kvp.Value); }); + } + + [Fact] + public void AddValidation_WithErrorMessageAndLocalizerProvider_SetsAttributesAsExpected() + { + // Arrange + var expected = "Error about 'Length' from override."; + var url = "/Controller/Action"; + var context = GetValidationContext(); + var attribute = new TestableRemoteAttributeBase(url) + { + HttpMethod = "POST", + ErrorMessage = "Error about '{0}' from override.", + }; + + var options = context.ActionContext.HttpContext.RequestServices + .GetRequiredService>(); + var localizer = new Mock(MockBehavior.Strict); + options.Value.DataAnnotationLocalizerProvider = (type, factory) => localizer.Object; + + // Act + attribute.AddValidation(context); + + // Assert + Assert.Collection( + context.Attributes, + kvp => { Assert.Equal("data-val", kvp.Key); Assert.Equal("true", kvp.Value); }, + kvp => + { + // Non-null DataAnnotationLocalizerProvider alone is insufficient to change error message. + Assert.Equal("data-val-remote", kvp.Key); + Assert.Equal(expected, kvp.Value); + }, + kvp => + { + Assert.Equal("data-val-remote-additionalfields", kvp.Key); + Assert.Equal("*.Length", kvp.Value); + }, + kvp => { Assert.Equal("data-val-remote-type", kvp.Key); Assert.Equal("POST", kvp.Value); }, + kvp => { Assert.Equal("data-val-remote-url", kvp.Key); Assert.Equal(url, kvp.Value); }); + } + + [Fact] + public void AddValidation_WithErrorMessageLocalizerFactoryAndLocalizerProvider_SetsAttributesAsExpected() + { + // Arrange + var expected = "Error about 'Length' from localizer."; + var url = "/Controller/Action"; + var localizerFactory = new Mock(MockBehavior.Strict).Object; + var context = GetValidationContext(localizerFactory); + var attribute = new TestableRemoteAttributeBase(dummyGetUrlReturnValue: url) + { + HttpMethod = "POST", + ErrorMessage = "Error about '{0}' from override.", + }; + + var localizedString = new LocalizedString("Fred", expected); + var localizer = new Mock(MockBehavior.Strict); + localizer + .Setup(l => l["Error about '{0}' from override.", "Length"]) + .Returns(localizedString) + .Verifiable(); + var options = context.ActionContext.HttpContext.RequestServices + .GetRequiredService>(); + options.Value.DataAnnotationLocalizerProvider = (type, factory) => localizer.Object; + + // Act + attribute.AddValidation(context); + + // Assert + localizer.VerifyAll(); + + Assert.Collection( + context.Attributes, + kvp => { Assert.Equal("data-val", kvp.Key); Assert.Equal("true", kvp.Value); }, + kvp => + { + Assert.Equal("data-val-remote", kvp.Key); + Assert.Equal(expected, kvp.Value); + }, + kvp => + { + Assert.Equal("data-val-remote-additionalfields", kvp.Key); + Assert.Equal("*.Length", kvp.Value); + }, + kvp => { Assert.Equal("data-val-remote-type", kvp.Key); Assert.Equal("POST", kvp.Value); }, + kvp => { Assert.Equal("data-val-remote-url", kvp.Key); Assert.Equal(url, kvp.Value); }); + } + + [Fact] + [ReplaceCulture] + public void AddValidation_WithErrorResourcesLocalizerFactoryAndLocalizerProvider_SetsAttributesAsExpected() + { + // Arrange + var expected = "Error about 'Length' from resources."; + var url = "/Controller/Action"; + var attribute = new TestableRemoteAttributeBase(dummyGetUrlReturnValue: url) + { + HttpMethod = "POST", + ErrorMessageResourceName = nameof(Resources.RemoteAttribute_Error), + ErrorMessageResourceType = typeof(Resources), + }; + + var localizerFactory = new Mock(MockBehavior.Strict).Object; + var context = GetValidationContext(localizerFactory); + + var localizer = new Mock(MockBehavior.Strict); + var options = context.ActionContext.HttpContext.RequestServices + .GetRequiredService>(); + options.Value.DataAnnotationLocalizerProvider = (type, factory) => localizer.Object; + + // Act + attribute.AddValidation(context); + + // Assert + Assert.Collection( + context.Attributes, + kvp => { Assert.Equal("data-val", kvp.Key); Assert.Equal("true", kvp.Value); }, + kvp => + { + // Configuring the attribute using ErrorMessageResource* trumps available IStringLocalizer etc. + Assert.Equal("data-val-remote", kvp.Key); + Assert.Equal(expected, kvp.Value); + }, + kvp => + { + Assert.Equal("data-val-remote-additionalfields", kvp.Key); + Assert.Equal("*.Length", kvp.Value); + }, + kvp => { Assert.Equal("data-val-remote-type", kvp.Key); Assert.Equal("POST", kvp.Value); }, + kvp => { Assert.Equal("data-val-remote-url", kvp.Key); Assert.Equal(url, kvp.Value); }); + } + + [Fact] + public void AddValidation_WithErrorMessageAndDisplayName_SetsAttributesAsExpected() + { + // Arrange + var expected = "Error about 'Display Length' from override."; + var url = "/Controller/Action"; + + var metadataProvider = new TestModelMetadataProvider(); + metadataProvider + .ForProperty(typeof(string), nameof(string.Length)) + .DisplayDetails(d => d.DisplayName = () => "Display Length"); + var context = GetValidationContext(localizerFactory: null, metadataProvider: metadataProvider); + + var attribute = new TestableRemoteAttributeBase(dummyGetUrlReturnValue: url) + { + HttpMethod = "POST", + ErrorMessage = "Error about '{0}' from override.", + }; + + // Act + attribute.AddValidation(context); + + // Assert + Assert.Collection( + context.Attributes, + kvp => { Assert.Equal("data-val", kvp.Key); Assert.Equal("true", kvp.Value); }, + kvp => + { + Assert.Equal("data-val-remote", kvp.Key); + Assert.Equal(expected, kvp.Value); + }, + kvp => + { + Assert.Equal("data-val-remote-additionalfields", kvp.Key); + Assert.Equal("*.Length", kvp.Value); + }, + kvp => { Assert.Equal("data-val-remote-type", kvp.Key); Assert.Equal("POST", kvp.Value); }, + kvp => { Assert.Equal("data-val-remote-url", kvp.Key); Assert.Equal(url, kvp.Value); }); + } + + [Fact] + public void AddValidation_WithErrorMessageLocalizerFactoryLocalizerProviderAndDisplayName_SetsAttributesAsExpected() + { + // Arrange + var expected = "Error about 'Length' from localizer."; + var url = "/Controller/Action"; + + var metadataProvider = new TestModelMetadataProvider(); + metadataProvider + .ForProperty(typeof(string), nameof(string.Length)) + .DisplayDetails(d => d.DisplayName = () => "Display Length"); + var localizerFactory = new Mock(MockBehavior.Strict).Object; + var context = GetValidationContext(localizerFactory, metadataProvider); + + var attribute = new TestableRemoteAttributeBase(dummyGetUrlReturnValue: url) + { + HttpMethod = "POST", + ErrorMessage = "Error about '{0}' from override.", + }; + + var localizedString = new LocalizedString("Fred", expected); + var localizer = new Mock(MockBehavior.Strict); + localizer + .Setup(l => l["Error about '{0}' from override.", "Display Length"]) + .Returns(localizedString) + .Verifiable(); + var options = context.ActionContext.HttpContext.RequestServices + .GetRequiredService>(); + options.Value.DataAnnotationLocalizerProvider = (type, factory) => localizer.Object; + + // Act + attribute.AddValidation(context); + + // Assert + localizer.VerifyAll(); + + Assert.Collection( + context.Attributes, + kvp => { Assert.Equal("data-val", kvp.Key); Assert.Equal("true", kvp.Value); }, + kvp => + { + Assert.Equal("data-val-remote", kvp.Key); + Assert.Equal(expected, kvp.Value); + }, + kvp => + { + Assert.Equal("data-val-remote-additionalfields", kvp.Key); + Assert.Equal("*.Length", kvp.Value); + }, + kvp => { Assert.Equal("data-val-remote-type", kvp.Key); Assert.Equal("POST", kvp.Value); }, + kvp => { Assert.Equal("data-val-remote-url", kvp.Key); Assert.Equal(url, kvp.Value); }); + } + + [Fact] + public void AddValidation_WillSetAttributes_ToExpectedValues() + { + // Arrange + var url = "/Controller/Action"; + var attribute = new TestableRemoteAttributeBase(dummyGetUrlReturnValue: url) + { + HttpMethod = "POST", + AdditionalFields = "Password,ConfirmPassword", + ErrorMessage = "Error" + }; + var context = GetValidationContext(); + + // Act + attribute.AddValidation(context); + + // Assert + Assert.Collection( + context.Attributes, + kvp => { Assert.Equal("data-val", kvp.Key); Assert.Equal("true", kvp.Value); }, + kvp => { Assert.Equal("data-val-remote", kvp.Key); Assert.Equal("Error", kvp.Value); }, + kvp => + { + Assert.Equal("data-val-remote-additionalfields", kvp.Key); + Assert.Equal("*.Length,*.Password,*.ConfirmPassword", kvp.Value); + }, + kvp => { Assert.Equal("data-val-remote-type", kvp.Key); Assert.Equal("POST", kvp.Value); }, + kvp => { Assert.Equal("data-val-remote-url", kvp.Key); Assert.Equal(url, kvp.Value); }); + } + + private static ClientModelValidationContext GetValidationContext( + IStringLocalizerFactory localizerFactory = null, + IModelMetadataProvider metadataProvider = null) + { + var serviceCollection = new ServiceCollection(); + serviceCollection.AddOptions(); + if (localizerFactory != null) + { + serviceCollection.AddSingleton(localizerFactory); + } + + var serviceProvider = serviceCollection.BuildServiceProvider(); + + var httpContext = new DefaultHttpContext + { + RequestServices = serviceProvider, + }; + + var actionContext = new ActionContext( + httpContext, + routeData: new Mock().Object, + actionDescriptor: new ActionDescriptor()); + + var emptyMetadataProvider = new EmptyModelMetadataProvider(); + + if (metadataProvider == null) + { + metadataProvider = new EmptyModelMetadataProvider(); + } + + var metadata = metadataProvider.GetMetadataForProperty( + containerType: typeof(string), + propertyName: nameof(string.Length)); + + return new ClientModelValidationContext( + actionContext, + metadata, + metadataProvider, + new AttributeDictionary()); + } + + private class TestableRemoteAttributeBase : RemoteAttributeBase + { + private readonly string _dummyGetUrlReturnValue; + + public TestableRemoteAttributeBase() + { } + + public TestableRemoteAttributeBase(string dummyGetUrlReturnValue) + { + _dummyGetUrlReturnValue = dummyGetUrlReturnValue; + } + + protected override string GetUrl(ClientModelValidationContext context) + { + return _dummyGetUrlReturnValue; + } + } + } +} diff --git a/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/RemoteAttributeTest.cs b/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/RemoteAttributeTest.cs index 5742da183b..649b350952 100644 --- a/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/RemoteAttributeTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/RemoteAttributeTest.cs @@ -5,7 +5,6 @@ using System; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.Abstractions; -using Microsoft.AspNetCore.Mvc.DataAnnotations; using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.Mvc.ModelBinding.Validation; using Microsoft.AspNetCore.Mvc.Routing; @@ -13,24 +12,17 @@ using Microsoft.AspNetCore.Mvc.ViewFeatures; using Microsoft.AspNetCore.Routing; using Microsoft.AspNetCore.Testing; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Localization; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.ObjectPool; using Microsoft.Extensions.Options; using Moq; using Xunit; -using Resources = Microsoft.AspNetCore.Mvc.ViewFeatures.Test.Resources; namespace Microsoft.AspNetCore.Mvc { public class RemoteAttributeTest { - private static readonly IModelMetadataProvider _metadataProvider = new EmptyModelMetadataProvider(); - private static readonly ModelMetadata _metadata = _metadataProvider.GetMetadataForProperty( - typeof(string), - nameof(string.Length)); - public static TheoryData SomeNames { get @@ -57,15 +49,7 @@ namespace Microsoft.AspNetCore.Mvc }; } } - - [Fact] - public void IsValidAlwaysReturnsTrue() - { - // Act & Assert - Assert.True(new RemoteAttribute("RouteName", "ParameterName").IsValid(value: null)); - Assert.True(new RemoteAttribute("ActionName", "ControllerName", "ParameterName").IsValid(value: null)); - } - + [Fact] public void Constructor_WithNullAction_IgnoresArgument() { @@ -157,516 +141,74 @@ namespace Microsoft.AspNetCore.Mvc Assert.Equal(areaName, resultName); Assert.Null(attribute.RouteName); } - + [Fact] - public void ErrorMessageProperties_HaveExpectedDefaultValues() - { - // Arrange & Act - var attribute = new RemoteAttribute("Action", "Controller"); - - // Assert - Assert.Null(attribute.ErrorMessage); - Assert.Null(attribute.ErrorMessageResourceName); - Assert.Null(attribute.ErrorMessageResourceType); - } - - [Fact] - [ReplaceCulture] - public void FormatErrorMessage_ReturnsDefaultErrorMessage() + public void GetUrl_WithBadRouteName_Throws() { // Arrange - // See ViewFeatures.Resources.RemoteAttribute_RemoteValidationFailed. - var expected = "'Property1' is invalid."; - var attribute = new RemoteAttribute("Action", "Controller"); - - // Act - var message = attribute.FormatErrorMessage("Property1"); - - // Assert - Assert.Equal(expected, message); - } - - [Fact] - public void FormatErrorMessage_UsesOverriddenErrorMessage() - { - // Arrange - var expected = "Error about 'Property1' from override."; - var attribute = new RemoteAttribute("Action", "Controller") - { - ErrorMessage = "Error about '{0}' from override.", - }; - - // Act - var message = attribute.FormatErrorMessage("Property1"); - - // Assert - Assert.Equal(expected, message); - } - - [Fact] - [ReplaceCulture] - public void FormatErrorMessage_UsesErrorMessageFromResource() - { - // Arrange - var expected = "Error about 'Property1' from resources."; - var attribute = new RemoteAttribute("Action", "Controller") - { - ErrorMessageResourceName = nameof(Resources.RemoteAttribute_Error), - ErrorMessageResourceType = typeof(Resources), - }; - - // Act - var message = attribute.FormatErrorMessage("Property1"); - - // Assert - Assert.Equal(expected, message); - } - - [Theory] - [MemberData(nameof(NullOrEmptyNames))] - public void FormatAdditionalFieldsForClientValidation_WithInvalidPropertyName_Throws(string property) - { - // Arrange - var attribute = new RemoteAttribute(routeName: "default"); - var expectedMessage = "Value cannot be null or empty."; - - // Act & Assert - ExceptionAssert.ThrowsArgument( - () => attribute.FormatAdditionalFieldsForClientValidation(property), - "property", - expectedMessage); - } - - [Theory] - [MemberData(nameof(NullOrEmptyNames))] - public void FormatPropertyForClientValidation_WithInvalidPropertyName_Throws(string property) - { - // Arrange - var expected = "Value cannot be null or empty."; - - // Act & Assert - ExceptionAssert.ThrowsArgument( - () => RemoteAttribute.FormatPropertyForClientValidation(property), - "property", - expected); - } - - [Fact] - public void AddValidation_WithBadRouteName_Throws() - { - // Arrange - var attribute = new RemoteAttribute("nonexistentRoute"); + var testableAttribute = new TestableRemoteAttribute("nonexistentRoute"); var context = GetValidationContextWithArea(currentArea: null); // Act & Assert - var exception = Assert.Throws(() => attribute.AddValidation(context)); + var exception = Assert.Throws(() => testableAttribute.InvokeGetUrl(context)); Assert.Equal("No URL for remote validation could be found.", exception.Message); } [Fact] - [ReplaceCulture] - public void AddValidation_WithRoute_CallsUrlHelperWithExpectedValues() + public void GetUrl_WithRoute_CallsUrlHelperWithExpectedValues() { // Arrange var routeName = "RouteName"; - var attribute = new RemoteAttribute(routeName); + var testableRemoteAttribute = new TestableRemoteAttribute(routeName); var url = "/my/URL"; var urlHelper = new MockUrlHelper(url, routeName); var context = GetValidationContext(urlHelper); // Act - attribute.AddValidation(context); - + var actualUrl = testableRemoteAttribute.InvokeGetUrl(context); + // Assert - Assert.Collection( - context.Attributes, - kvp => { Assert.Equal("data-val", kvp.Key); Assert.Equal("true", kvp.Value); }, - kvp => { Assert.Equal("data-val-remote", kvp.Key); Assert.Equal("'Length' is invalid.", kvp.Value); }, - kvp => { Assert.Equal("data-val-remote-additionalfields", kvp.Key); Assert.Equal("*.Length", kvp.Value); }, - kvp => { Assert.Equal("data-val-remote-url", kvp.Key); Assert.Equal(url, kvp.Value); }); + Assert.Equal(url, actualUrl); var routeDictionary = Assert.IsType(urlHelper.RouteValues); Assert.Empty(routeDictionary); } [Fact] - [ReplaceCulture] - public void AddValidation_WithActionController_CallsUrlHelperWithExpectedValues() + public void GetUrl_WithActionController_CallsUrlHelperWithExpectedValues() { // Arrange - var attribute = new RemoteAttribute("Action", "Controller"); + var testableRemoteAttribute = new TestableRemoteAttribute("Action", "Controller"); var url = "/Controller/Action"; var urlHelper = new MockUrlHelper(url, routeName: null); var context = GetValidationContext(urlHelper); // Act - attribute.AddValidation(context); + var actualUrl = testableRemoteAttribute.InvokeGetUrl(context); // Assert - Assert.Collection( - context.Attributes, - kvp => { Assert.Equal("data-val", kvp.Key); Assert.Equal("true", kvp.Value); }, - kvp => { Assert.Equal("data-val-remote", kvp.Key); Assert.Equal("'Length' is invalid.", kvp.Value); }, - kvp => { Assert.Equal("data-val-remote-additionalfields", kvp.Key); Assert.Equal("*.Length", kvp.Value); }, - kvp => { Assert.Equal("data-val-remote-url", kvp.Key); Assert.Equal(url, kvp.Value); }); + 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] - [ReplaceCulture] - public void AddValidation_WithActionController_PropertiesSet_CallsUrlHelperWithExpectedValues() + public void GetUrl_WithActionControllerArea_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 - attribute.AddValidation(context); - - // Assert - Assert.Collection( - context.Attributes, - kvp => { Assert.Equal("data-val", kvp.Key); Assert.Equal("true", kvp.Value); }, - kvp => { Assert.Equal("data-val-remote", kvp.Key); Assert.Equal("'Length' is invalid.", kvp.Value); }, - kvp => - { - Assert.Equal("data-val-remote-additionalfields", kvp.Key); - Assert.Equal("*.Length,*.Password,*.ConfirmPassword", kvp.Value); - }, - kvp => { Assert.Equal("data-val-remote-type", kvp.Key); Assert.Equal("POST", kvp.Value); }, - kvp => { Assert.Equal("data-val-remote-url", kvp.Key); Assert.Equal(url, kvp.Value); }); - - 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 AddValidation_WithErrorMessage_SetsAttributesAsExpected() - { - // Arrange - var expected = "Error about 'Length' from override."; - var attribute = new RemoteAttribute("Action", "Controller") - { - HttpMethod = "POST", - ErrorMessage = "Error about '{0}' from override.", - }; - var url = "/Controller/Action"; - var context = GetValidationContext(url); - - // Act - attribute.AddValidation(context); - - // Assert - Assert.Collection( - context.Attributes, - kvp => { Assert.Equal("data-val", kvp.Key); Assert.Equal("true", kvp.Value); }, - kvp => - { - Assert.Equal("data-val-remote", kvp.Key); - Assert.Equal(expected, kvp.Value); - }, - kvp => - { - Assert.Equal("data-val-remote-additionalfields", kvp.Key); - Assert.Equal("*.Length", kvp.Value); - }, - kvp => { Assert.Equal("data-val-remote-type", kvp.Key); Assert.Equal("POST", kvp.Value); }, - kvp => { Assert.Equal("data-val-remote-url", kvp.Key); Assert.Equal(url, kvp.Value); }); - } - - [Fact] - public void AddValidation_WithErrorMessageAndLocalizerFactory_SetsAttributesAsExpected() - { - // Arrange - var expected = "Error about 'Length' from override."; - var attribute = new RemoteAttribute("Action", "Controller") - { - HttpMethod = "POST", - ErrorMessage = "Error about '{0}' from override.", - }; - var url = "/Controller/Action"; - var context = GetValidationContextWithLocalizerFactory(url); - - // Act - attribute.AddValidation(context); - - // Assert - Assert.Collection( - context.Attributes, - kvp => { Assert.Equal("data-val", kvp.Key); Assert.Equal("true", kvp.Value); }, - kvp => - { - // IStringLocalizerFactory existence alone is insufficient to change error message. - Assert.Equal("data-val-remote", kvp.Key); - Assert.Equal(expected, kvp.Value); - }, - kvp => - { - Assert.Equal("data-val-remote-additionalfields", kvp.Key); - Assert.Equal("*.Length", kvp.Value); - }, - kvp => { Assert.Equal("data-val-remote-type", kvp.Key); Assert.Equal("POST", kvp.Value); }, - kvp => { Assert.Equal("data-val-remote-url", kvp.Key); Assert.Equal(url, kvp.Value); }); - } - - [Fact] - public void AddValidation_WithErrorMessageAndLocalizerProvider_SetsAttributesAsExpected() - { - // Arrange - var expected = "Error about 'Length' from override."; - var attribute = new RemoteAttribute("Action", "Controller") - { - HttpMethod = "POST", - ErrorMessage = "Error about '{0}' from override.", - }; - var url = "/Controller/Action"; - var context = GetValidationContext(url); - - var options = context.ActionContext.HttpContext.RequestServices - .GetRequiredService>(); - var localizer = new Mock(MockBehavior.Strict); - options.Value.DataAnnotationLocalizerProvider = (type, factory) => localizer.Object; - - // Act - attribute.AddValidation(context); - - // Assert - Assert.Collection( - context.Attributes, - kvp => { Assert.Equal("data-val", kvp.Key); Assert.Equal("true", kvp.Value); }, - kvp => - { - // Non-null DataAnnotationLocalizerProvider alone is insufficient to change error message. - Assert.Equal("data-val-remote", kvp.Key); - Assert.Equal(expected, kvp.Value); - }, - kvp => - { - Assert.Equal("data-val-remote-additionalfields", kvp.Key); - Assert.Equal("*.Length", kvp.Value); - }, - kvp => { Assert.Equal("data-val-remote-type", kvp.Key); Assert.Equal("POST", kvp.Value); }, - kvp => { Assert.Equal("data-val-remote-url", kvp.Key); Assert.Equal(url, kvp.Value); }); - } - - [Fact] - public void AddValidation_WithErrorMessageLocalizerFactoryAndLocalizerProvider_SetsAttributesAsExpected() - { - // Arrange - var expected = "Error about 'Length' from localizer."; - var attribute = new RemoteAttribute("Action", "Controller") - { - HttpMethod = "POST", - ErrorMessage = "Error about '{0}' from override.", - }; - var url = "/Controller/Action"; - var context = GetValidationContextWithLocalizerFactory(url); - - var localizedString = new LocalizedString("Fred", expected); - var localizer = new Mock(MockBehavior.Strict); - localizer - .Setup(l => l["Error about '{0}' from override.", "Length"]) - .Returns(localizedString) - .Verifiable(); - var options = context.ActionContext.HttpContext.RequestServices - .GetRequiredService>(); - options.Value.DataAnnotationLocalizerProvider = (type, factory) => localizer.Object; - - // Act - attribute.AddValidation(context); - - // Assert - localizer.VerifyAll(); - - Assert.Collection( - context.Attributes, - kvp => { Assert.Equal("data-val", kvp.Key); Assert.Equal("true", kvp.Value); }, - kvp => - { - Assert.Equal("data-val-remote", kvp.Key); - Assert.Equal(expected, kvp.Value); - }, - kvp => - { - Assert.Equal("data-val-remote-additionalfields", kvp.Key); - Assert.Equal("*.Length", kvp.Value); - }, - kvp => { Assert.Equal("data-val-remote-type", kvp.Key); Assert.Equal("POST", kvp.Value); }, - kvp => { Assert.Equal("data-val-remote-url", kvp.Key); Assert.Equal(url, kvp.Value); }); - } - - [Fact] - [ReplaceCulture] - public void AddValidation_WithErrorResourcesLocalizerFactoryAndLocalizerProvider_SetsAttributesAsExpected() - { - // Arrange - var expected = "Error about 'Length' from resources."; - var attribute = new RemoteAttribute("Action", "Controller") - { - HttpMethod = "POST", - ErrorMessageResourceName = nameof(Resources.RemoteAttribute_Error), - ErrorMessageResourceType = typeof(Resources), - }; - var url = "/Controller/Action"; - var context = GetValidationContextWithLocalizerFactory(url); - - var localizer = new Mock(MockBehavior.Strict); - var options = context.ActionContext.HttpContext.RequestServices - .GetRequiredService>(); - options.Value.DataAnnotationLocalizerProvider = (type, factory) => localizer.Object; - - // Act - attribute.AddValidation(context); - - // Assert - Assert.Collection( - context.Attributes, - kvp => { Assert.Equal("data-val", kvp.Key); Assert.Equal("true", kvp.Value); }, - kvp => - { - // Configuring the attribute using ErrorMessageResource* trumps available IStringLocalizer etc. - Assert.Equal("data-val-remote", kvp.Key); - Assert.Equal(expected, kvp.Value); - }, - kvp => - { - Assert.Equal("data-val-remote-additionalfields", kvp.Key); - Assert.Equal("*.Length", kvp.Value); - }, - kvp => { Assert.Equal("data-val-remote-type", kvp.Key); Assert.Equal("POST", kvp.Value); }, - kvp => { Assert.Equal("data-val-remote-url", kvp.Key); Assert.Equal(url, kvp.Value); }); - } - - [Fact] - public void AddValidation_WithErrorMessageAndDisplayName_SetsAttributesAsExpected() - { - // Arrange - var expected = "Error about 'Display Length' from override."; - var attribute = new RemoteAttribute("Action", "Controller") - { - HttpMethod = "POST", - ErrorMessage = "Error about '{0}' from override.", - }; - - var url = "/Controller/Action"; - var metadataProvider = new TestModelMetadataProvider(); - metadataProvider - .ForProperty(typeof(string), nameof(string.Length)) - .DisplayDetails(d => d.DisplayName = () => "Display Length"); - var context = GetValidationContext(url, metadataProvider); - - // Act - attribute.AddValidation(context); - - // Assert - Assert.Collection( - context.Attributes, - kvp => { Assert.Equal("data-val", kvp.Key); Assert.Equal("true", kvp.Value); }, - kvp => - { - Assert.Equal("data-val-remote", kvp.Key); - Assert.Equal(expected, kvp.Value); - }, - kvp => - { - Assert.Equal("data-val-remote-additionalfields", kvp.Key); - Assert.Equal("*.Length", kvp.Value); - }, - kvp => { Assert.Equal("data-val-remote-type", kvp.Key); Assert.Equal("POST", kvp.Value); }, - kvp => { Assert.Equal("data-val-remote-url", kvp.Key); Assert.Equal(url, kvp.Value); }); - } - - [Fact] - public void AddValidation_WithErrorMessageLocalizerFactoryLocalizerProviderAndDisplayName_SetsAttributesAsExpected() - { - // Arrange - var expected = "Error about 'Length' from localizer."; - var attribute = new RemoteAttribute("Action", "Controller") - { - HttpMethod = "POST", - ErrorMessage = "Error about '{0}' from override.", - }; - - var url = "/Controller/Action"; - var metadataProvider = new TestModelMetadataProvider(); - metadataProvider - .ForProperty(typeof(string), nameof(string.Length)) - .DisplayDetails(d => d.DisplayName = () => "Display Length"); - var context = GetValidationContextWithLocalizerFactory(url, metadataProvider); - - var localizedString = new LocalizedString("Fred", expected); - var localizer = new Mock(MockBehavior.Strict); - localizer - .Setup(l => l["Error about '{0}' from override.", "Display Length"]) - .Returns(localizedString) - .Verifiable(); - var options = context.ActionContext.HttpContext.RequestServices - .GetRequiredService>(); - options.Value.DataAnnotationLocalizerProvider = (type, factory) => localizer.Object; - - // Act - attribute.AddValidation(context); - - // Assert - localizer.VerifyAll(); - - Assert.Collection( - context.Attributes, - kvp => { Assert.Equal("data-val", kvp.Key); Assert.Equal("true", kvp.Value); }, - kvp => - { - Assert.Equal("data-val-remote", kvp.Key); - Assert.Equal(expected, kvp.Value); - }, - kvp => - { - Assert.Equal("data-val-remote-additionalfields", kvp.Key); - Assert.Equal("*.Length", kvp.Value); - }, - kvp => { Assert.Equal("data-val-remote-type", kvp.Key); Assert.Equal("POST", kvp.Value); }, - kvp => { Assert.Equal("data-val-remote-url", kvp.Key); Assert.Equal(url, kvp.Value); }); - } - - [Fact] - [ReplaceCulture] - public void AddValidation_WithActionControllerArea_CallsUrlHelperWithExpectedValues() - { - // Arrange - var attribute = new RemoteAttribute("Action", "Controller", "Test") - { - HttpMethod = "POST", - }; + var testableAttribute = new TestableRemoteAttribute("Action", "Controller", "Test"); var url = "/Test/Controller/Action"; var urlHelper = new MockUrlHelper(url, routeName: null); var context = GetValidationContext(urlHelper); // Act - attribute.AddValidation(context); + var actualUrl = testableAttribute.InvokeGetUrl(context); // Assert - Assert.Collection( - context.Attributes, - kvp => { Assert.Equal("data-val", kvp.Key); Assert.Equal("true", kvp.Value); }, - kvp => { Assert.Equal("data-val-remote", kvp.Key); Assert.Equal("'Length' is invalid.", kvp.Value); }, - kvp => - { - Assert.Equal("data-val-remote-additionalfields", kvp.Key); - Assert.Equal("*.Length", kvp.Value); - }, - kvp => { Assert.Equal("data-val-remote-type", kvp.Key); Assert.Equal("POST", kvp.Value); }, - kvp => { Assert.Equal("data-val-remote-url", kvp.Key); Assert.Equal(url, kvp.Value); }); + Assert.Equal(url, actualUrl); var routeDictionary = Assert.IsType(urlHelper.RouteValues); Assert.Equal(3, routeDictionary.Count); @@ -677,179 +219,109 @@ namespace Microsoft.AspNetCore.Mvc // Root area is current in this case. [Fact] - [ReplaceCulture] - public void AddValidation_WithActionController_FindsControllerInCurrentArea() + public void GetUrl_WithActionController_FindsControllerInCurrentArea() { // Arrange - var attribute = new RemoteAttribute("Action", "Controller"); + var testableAttribute = new TestableRemoteAttribute("Action", "Controller"); var context = GetValidationContextWithArea(currentArea: null); // Act - attribute.AddValidation(context); + var actualUrl = testableAttribute.InvokeGetUrl(context); // Assert - Assert.Collection( - context.Attributes, - kvp => { Assert.Equal("data-val", kvp.Key); Assert.Equal("true", kvp.Value); }, - kvp => { Assert.Equal("data-val-remote", kvp.Key); Assert.Equal("'Length' is invalid.", kvp.Value); }, - kvp => { Assert.Equal("data-val-remote-additionalfields", kvp.Key); Assert.Equal("*.Length", kvp.Value); }, - kvp => - { - Assert.Equal("data-val-remote-url", kvp.Key); - Assert.Equal("/Controller/Action", kvp.Value); - }); + Assert.Equal("/Controller/Action", actualUrl); } // Test area is current in this case. [Fact] - [ReplaceCulture] - public void AddValidation_WithActionControllerInArea_FindsControllerInCurrentArea() + public void GetUrl_WithActionControllerInArea_FindsControllerInCurrentArea() { // Arrange - var attribute = new RemoteAttribute("Action", "Controller"); + var testableAttribute = new TestableRemoteAttribute("Action", "Controller"); var context = GetValidationContextWithArea(currentArea: "Test"); // Act - attribute.AddValidation(context); + var actualUrl = testableAttribute.InvokeGetUrl(context); // Assert - Assert.Collection( - context.Attributes, - kvp => { Assert.Equal("data-val", kvp.Key); Assert.Equal("true", kvp.Value); }, - kvp => { Assert.Equal("data-val-remote", kvp.Key); Assert.Equal("'Length' is invalid.", kvp.Value); }, - kvp => { Assert.Equal("data-val-remote-additionalfields", kvp.Key); Assert.Equal("*.Length", kvp.Value); }, - kvp => - { - Assert.Equal("data-val-remote-url", kvp.Key); - Assert.Equal("/Test/Controller/Action", kvp.Value); - }); + Assert.Equal("/Test/Controller/Action", actualUrl); } // Explicit reference to the (current) root area. [Theory] [MemberData(nameof(NullOrEmptyNames))] - [ReplaceCulture] - public void AddValidation_WithActionControllerArea_FindsControllerInRootArea(string areaName) + public void GetUrl_WithActionControllerArea_FindsControllerInRootArea(string areaName) { // Arrange - var attribute = new RemoteAttribute("Action", "Controller", areaName); + var testableAttribute = new TestableRemoteAttribute("Action", "Controller", areaName); var context = GetValidationContextWithArea(currentArea: null); // Act - attribute.AddValidation(context); + var actualUrl = testableAttribute.InvokeGetUrl(context); // Assert - Assert.Collection( - context.Attributes, - kvp => { Assert.Equal("data-val", kvp.Key); Assert.Equal("true", kvp.Value); }, - kvp => { Assert.Equal("data-val-remote", kvp.Key); Assert.Equal("'Length' is invalid.", kvp.Value); }, - kvp => { Assert.Equal("data-val-remote-additionalfields", kvp.Key); Assert.Equal("*.Length", kvp.Value); }, - kvp => - { - Assert.Equal("data-val-remote-url", kvp.Key); - Assert.Equal("/Controller/Action", kvp.Value); - }); + Assert.Equal("/Controller/Action", actualUrl); } // Test area is current in this case. [Theory] [MemberData(nameof(NullOrEmptyNames))] - [ReplaceCulture] - public void AddValidation_WithActionControllerAreaInArea_FindsControllerInRootArea(string areaName) + public void GetUrl_WithActionControllerAreaInArea_FindsControllerInRootArea(string areaName) { // Arrange - var attribute = new RemoteAttribute("Action", "Controller", areaName); + var testableAttribute = new TestableRemoteAttribute("Action", "Controller", areaName); var context = GetValidationContextWithArea(currentArea: "Test"); // Act - attribute.AddValidation(context); + var actualUrl = testableAttribute.InvokeGetUrl(context); // Assert - Assert.Collection( - context.Attributes, - kvp => { Assert.Equal("data-val", kvp.Key); Assert.Equal("true", kvp.Value); }, - kvp => { Assert.Equal("data-val-remote", kvp.Key); Assert.Equal("'Length' is invalid.", kvp.Value); }, - kvp => { Assert.Equal("data-val-remote-additionalfields", kvp.Key); Assert.Equal("*.Length", kvp.Value); }, - kvp => - { - Assert.Equal("data-val-remote-url", kvp.Key); - Assert.Equal("/Controller/Action", kvp.Value); - }); + Assert.Equal("/Controller/Action", actualUrl); } // Root area is current in this case. [Fact] - [ReplaceCulture] - public void AddValidation_WithActionControllerArea_FindsControllerInNamedArea() + public void GetUrl_WithActionControllerArea_FindsControllerInNamedArea() { // Arrange - var attribute = new RemoteAttribute("Action", "Controller", "Test"); + var testableAttribute = new TestableRemoteAttribute("Action", "Controller", "Test"); var context = GetValidationContextWithArea(currentArea: null); // Act - attribute.AddValidation(context); + var actualUrl = testableAttribute.InvokeGetUrl(context); // Assert - Assert.Collection( - context.Attributes, - kvp => { Assert.Equal("data-val", kvp.Key); Assert.Equal("true", kvp.Value); }, - kvp => { Assert.Equal("data-val-remote", kvp.Key); Assert.Equal("'Length' is invalid.", kvp.Value); }, - kvp => { Assert.Equal("data-val-remote-additionalfields", kvp.Key); Assert.Equal("*.Length", kvp.Value); }, - kvp => - { - Assert.Equal("data-val-remote-url", kvp.Key); - Assert.Equal("/Test/Controller/Action", kvp.Value); - }); + Assert.Equal("/Test/Controller/Action", actualUrl); } // Explicit reference to the current (Test) area. [Fact] - [ReplaceCulture] - public void AddValidation_WithActionControllerAreaInArea_FindsControllerInNamedArea() + public void GetUrl_WithActionControllerAreaInArea_FindsControllerInNamedArea() { // Arrange - var attribute = new RemoteAttribute("Action", "Controller", "Test"); + var testableAttribute = new TestableRemoteAttribute("Action", "Controller", "Test"); var context = GetValidationContextWithArea(currentArea: "Test"); // Act - attribute.AddValidation(context); + var actualUrl = testableAttribute.InvokeGetUrl(context); // Assert - Assert.Collection( - context.Attributes, - kvp => { Assert.Equal("data-val", kvp.Key); Assert.Equal("true", kvp.Value); }, - kvp => { Assert.Equal("data-val-remote", kvp.Key); Assert.Equal("'Length' is invalid.", kvp.Value); }, - kvp => { Assert.Equal("data-val-remote-additionalfields", kvp.Key); Assert.Equal("*.Length", kvp.Value); }, - kvp => - { - Assert.Equal("data-val-remote-url", kvp.Key); - Assert.Equal("/Test/Controller/Action", kvp.Value); - }); + Assert.Equal("/Test/Controller/Action", actualUrl); } // Test area is current in this case. [Fact] - [ReplaceCulture] - public void AddValidation_WithActionControllerAreaInArea_FindsControllerInDifferentArea() + public void GetUrl_WithActionControllerAreaInArea_FindsControllerInDifferentArea() { // Arrange - var attribute = new RemoteAttribute("Action", "Controller", "AnotherArea"); + var testableAttribute = new TestableRemoteAttribute("Action", "Controller", "AnotherArea"); var context = GetValidationContextWithArea(currentArea: "Test"); // Act - attribute.AddValidation(context); + var actualUrl = testableAttribute.InvokeGetUrl(context); // Assert - Assert.Collection( - context.Attributes, - kvp => { Assert.Equal("data-val", kvp.Key); Assert.Equal("true", kvp.Value); }, - kvp => { Assert.Equal("data-val-remote", kvp.Key); Assert.Equal("'Length' is invalid.", kvp.Value); }, - kvp => { Assert.Equal("data-val-remote-additionalfields", kvp.Key); Assert.Equal("*.Length", kvp.Value); }, - kvp => - { - Assert.Equal("data-val-remote-url", kvp.Key); - Assert.Equal("/AnotherArea/Controller/Action", kvp.Value); - }); + Assert.Equal("/AnotherArea/Controller/Action", actualUrl); } // Test area is current in this case. @@ -883,29 +355,16 @@ namespace Microsoft.AspNetCore.Mvc kvp => { Assert.Equal("data-val-remote-url", kvp.Key); Assert.Equal("original", kvp.Value); }); } - private static ClientModelValidationContext GetValidationContext( - string url, - IModelMetadataProvider metadataProvider = null) + private static ClientModelValidationContext GetValidationContext(string url) { var urlHelper = new MockUrlHelper(url, routeName: null); - return GetValidationContext(urlHelper, localizerFactory: null, metadataProvider: metadataProvider); + return GetValidationContext(urlHelper); } - - private static ClientModelValidationContext GetValidationContextWithLocalizerFactory( - string url, - IModelMetadataProvider metadataProvider = null) - { - var urlHelper = new MockUrlHelper(url, routeName: null); - var localizerFactory = new Mock(MockBehavior.Strict); - return GetValidationContext(urlHelper, localizerFactory.Object, metadataProvider); - } - + private static ClientModelValidationContext GetValidationContext( - IUrlHelper urlHelper, - IStringLocalizerFactory localizerFactory = null, - IModelMetadataProvider metadataProvider = null) + IUrlHelper urlHelper) { - var serviceCollection = GetServiceCollection(localizerFactory); + var serviceCollection = GetServiceCollection(); var factory = new Mock(MockBehavior.Strict); serviceCollection.AddSingleton(factory.Object); @@ -915,17 +374,12 @@ namespace Microsoft.AspNetCore.Mvc factory .Setup(f => f.GetUrlHelper(actionContext)) .Returns(urlHelper); - - var metadata = _metadata; - if (metadataProvider == null) - { - metadataProvider = _metadataProvider; - } - else - { - metadata = metadataProvider.GetMetadataForProperty(typeof(string), nameof(string.Length)); - } - + + var metadataProvider = new EmptyModelMetadataProvider(); + var metadata = metadataProvider.GetMetadataForProperty( + containerType: typeof(string), + propertyName: nameof(string.Length)); + return new ClientModelValidationContext( actionContext, metadata, @@ -935,7 +389,7 @@ namespace Microsoft.AspNetCore.Mvc private static ClientModelValidationContext GetValidationContextWithArea(string currentArea) { - var serviceCollection = GetServiceCollection(localizerFactory: null); + var serviceCollection = GetServiceCollection(); var serviceProvider = serviceCollection.BuildServiceProvider(); var routeCollection = GetRouteCollectionWithArea(serviceProvider); var routeData = new RouteData @@ -968,10 +422,15 @@ namespace Microsoft.AspNetCore.Mvc 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, + metadata, + metadataProvider, new AttributeDictionary()); } @@ -987,15 +446,7 @@ namespace Microsoft.AspNetCore.Mvc return builder.Build(); } - - private static IRouter GetRouteCollectionWithNoController(IServiceProvider serviceProvider) - { - var builder = GetRouteBuilder(serviceProvider); - builder.MapRoute("default", "static/route"); - - return builder.Build(); - } - + private static RouteBuilder GetRouteBuilder(IServiceProvider serviceProvider) { var app = new Mock(MockBehavior.Strict); @@ -1034,7 +485,7 @@ namespace Microsoft.AspNetCore.Mvc return new ActionContext(httpContext, routeData, new ActionDescriptor()); } - private static ServiceCollection GetServiceCollection(IStringLocalizerFactory localizerFactory) + private static ServiceCollection GetServiceCollection() { var serviceCollection = new ServiceCollection(); serviceCollection @@ -1047,11 +498,6 @@ namespace Microsoft.AspNetCore.Mvc serviceCollection.AddSingleton( provider => new DefaultInlineConstraintResolver(provider.GetRequiredService>(), provider)); - if (localizerFactory != null) - { - serviceCollection.AddSingleton(localizerFactory); - } - return serviceCollection; } @@ -1135,6 +581,11 @@ namespace Microsoft.AspNetCore.Mvc return base.RouteData; } } + + public string InvokeGetUrl(ClientModelValidationContext context) + { + return base.GetUrl(context); + } } } }