// 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.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Linq; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.Mvc.ModelBinding.Validation; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Localization; using Moq; using Xunit; namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal { public class DataAnnotationsModelValidatorTest { private static readonly ModelMetadataProvider _metadataProvider = TestModelMetadataProvider.CreateDefaultProvider(); [Fact] public void Constructor_SetsAttribute() { // Arrange var attribute = new RequiredAttribute(); // Act var validator = new DataAnnotationsModelValidator( new ValidationAttributeAdapterProvider(), attribute, stringLocalizer: null); // Assert Assert.Same(attribute, validator.Attribute); } public static TheoryData Validate_SetsMemberName_AsExpectedData { get { var array = new[] { new SampleModel { Name = "one" }, new SampleModel { Name = "two" } }; var method = typeof(ModelValidationResultComparer).GetMethod( nameof(ModelValidationResultComparer.GetHashCode), new[] { typeof(ModelValidationResult) }); var parameter = method.GetParameters()[0]; // GetHashCode(ModelValidationResult obj) // metadata, container, model, expected MemberName return new TheoryData { { _metadataProvider.GetMetadataForProperty(typeof(string), nameof(string.Length)), "Hello", "Hello".Length, nameof(string.Length) }, { // Validating a top-level property. _metadataProvider.GetMetadataForProperty(typeof(SampleModel), nameof(SampleModel.Name)), null, "Fred", nameof(SampleModel.Name) }, { // Validating a parameter. _metadataProvider.GetMetadataForParameter(parameter), null, new ModelValidationResult(memberName: string.Empty, message: string.Empty), "obj" }, { // Validating a top-level parameter as if using old-fashioned metadata provider. _metadataProvider.GetMetadataForType(typeof(SampleModel)), null, 15, null }, { // Validating an element in a collection. _metadataProvider.GetMetadataForType(typeof(SampleModel)), array, array[1], null }, }; } } [Theory] [MemberData(nameof(Validate_SetsMemberName_AsExpectedData))] public void Validate_SetsMemberName_AsExpected( ModelMetadata metadata, object container, object model, string expectedMemberName) { // Arrange var attribute = new Mock { CallBase = true }; attribute .Setup(p => p.IsValidPublic(It.IsAny(), It.IsAny())) .Callback((object o, ValidationContext context) => { Assert.Equal(expectedMemberName, context.MemberName); }) .Returns(ValidationResult.Success) .Verifiable(); var validator = new DataAnnotationsModelValidator( new ValidationAttributeAdapterProvider(), attribute.Object, stringLocalizer: null); var validationContext = new ModelValidationContext( actionContext: new ActionContext(), modelMetadata: metadata, metadataProvider: _metadataProvider, container: container, model: model); // Act var results = validator.Validate(validationContext); // Assert Assert.Empty(results); attribute.VerifyAll(); } [Fact] public void Validate_Valid() { // Arrange var metadata = _metadataProvider.GetMetadataForType(typeof(string)); var container = "Hello"; var model = container.Length; var attribute = new Mock { CallBase = true }; attribute.Setup(a => a.IsValid(model)).Returns(true); var validator = new DataAnnotationsModelValidator( new ValidationAttributeAdapterProvider(), attribute.Object, stringLocalizer: null); var validationContext = new ModelValidationContext( actionContext: new ActionContext(), modelMetadata: metadata, metadataProvider: _metadataProvider, container: container, model: model); // Act var result = validator.Validate(validationContext); // Assert Assert.Empty(result); } [Fact] public void Validate_Invalid() { // Arrange var metadata = _metadataProvider.GetMetadataForProperty(typeof(string), "Length"); var container = "Hello"; var model = container.Length; var attribute = new Mock { CallBase = true }; attribute.Setup(a => a.IsValid(model)).Returns(false); var validator = new DataAnnotationsModelValidator( new ValidationAttributeAdapterProvider(), attribute.Object, stringLocalizer: null); var validationContext = new ModelValidationContext( actionContext: new ActionContext(), modelMetadata: metadata, metadataProvider: _metadataProvider, container: container, model: model); // Act var result = validator.Validate(validationContext); // Assert var validationResult = result.Single(); Assert.Empty(validationResult.MemberName); Assert.Equal(attribute.Object.FormatErrorMessage("Length"), validationResult.Message); } [Fact] public void Validate_ValidationResultSuccess() { // Arrange var metadata = _metadataProvider.GetMetadataForType(typeof(string)); var container = "Hello"; var model = container.Length; var attribute = new Mock { CallBase = true }; attribute .Setup(p => p.IsValidPublic(It.IsAny(), It.IsAny())) .Returns(ValidationResult.Success); var validator = new DataAnnotationsModelValidator( new ValidationAttributeAdapterProvider(), attribute.Object, stringLocalizer: null); var validationContext = new ModelValidationContext( actionContext: new ActionContext(), modelMetadata: metadata, metadataProvider: _metadataProvider, container: container, model: model); // Act var result = validator.Validate(validationContext); // Assert Assert.Empty(result); } [Fact] public void Validate_RequiredButNullAtTopLevel_Invalid() { // Arrange var metadata = _metadataProvider.GetMetadataForProperty(typeof(string), "Length"); var validator = new DataAnnotationsModelValidator( new ValidationAttributeAdapterProvider(), new RequiredAttribute(), stringLocalizer: null); var validationContext = new ModelValidationContext( actionContext: new ActionContext(), modelMetadata: metadata, metadataProvider: _metadataProvider, container: null, model: null); // Act var result = validator.Validate(validationContext); // Assert var validationResult = result.Single(); Assert.Empty(validationResult.MemberName); Assert.Equal(new RequiredAttribute().FormatErrorMessage("Length"), validationResult.Message); } [Fact] public void Validate_RequiredAndNotNullAtTopLevel_Valid() { // Arrange var metadata = _metadataProvider.GetMetadataForProperty(typeof(string), "Length"); var validator = new DataAnnotationsModelValidator( new ValidationAttributeAdapterProvider(), new RequiredAttribute(), stringLocalizer: null); var validationContext = new ModelValidationContext( actionContext: new ActionContext(), modelMetadata: metadata, metadataProvider: _metadataProvider, container: null, model: 123); // Act var result = validator.Validate(validationContext); // Assert Assert.Empty(result); } public static TheoryData, IEnumerable> Validate_ReturnsExpectedResults_Data { get { var errorMessage = "Some error message"; return new TheoryData, IEnumerable> { { errorMessage, null, new[] { new ModelValidationResult(memberName: string.Empty, message: errorMessage) } }, { errorMessage, Enumerable.Empty(), new[] { new ModelValidationResult(memberName: string.Empty, message: errorMessage) } }, { errorMessage, new[] { (string)null }, new[] { new ModelValidationResult(memberName: string.Empty, message: errorMessage) } }, { errorMessage, new[] { string.Empty }, new[] { new ModelValidationResult(memberName: string.Empty, message: errorMessage) } }, { errorMessage, // Name matches ValidationContext.MemberName. new[] { nameof(string.Length) }, new[] { new ModelValidationResult(memberName: string.Empty, message: errorMessage) } }, { errorMessage, new[] { "AnotherName" }, new[] { new ModelValidationResult(memberName: "AnotherName", message: errorMessage) } }, { errorMessage, new[] { "[1]" }, new[] { new ModelValidationResult(memberName: "[1]", message: errorMessage) } }, { errorMessage, new[] { "Name1", "Name2" }, new[] { new ModelValidationResult(memberName: "Name1", message: errorMessage), new ModelValidationResult(memberName: "Name2", message: errorMessage), } }, { errorMessage, new[] { "[0]", "[2]" }, new[] { new ModelValidationResult(memberName: "[0]", message: errorMessage), new ModelValidationResult(memberName: "[2]", message: errorMessage), } }, }; } } [Theory] [MemberData(nameof(Validate_ReturnsExpectedResults_Data))] public void Validate_ReturnsExpectedResults( string errorMessage, IEnumerable memberNames, IEnumerable expectedResults) { // Arrange var metadata = _metadataProvider.GetMetadataForProperty(typeof(string), nameof(string.Length)); var container = "Hello"; var model = container.Length; var attribute = new Mock { CallBase = true }; attribute .Setup(p => p.IsValidPublic(It.IsAny(), It.IsAny())) .Returns(new ValidationResult(errorMessage, memberNames)); var validator = new DataAnnotationsModelValidator( new ValidationAttributeAdapterProvider(), attribute.Object, stringLocalizer: null); var validationContext = new ModelValidationContext( actionContext: new ActionContext(), modelMetadata: metadata, metadataProvider: _metadataProvider, container: container, model: model); // Act var results = validator.Validate(validationContext); // Assert Assert.Equal(expectedResults, results, ModelValidationResultComparer.Instance); } [Fact] public void Validate_IsValidFalse_StringLocalizerReturnsLocalizerErrorMessage() { // Arrange var metadata = _metadataProvider.GetMetadataForType(typeof(string)); var container = "Hello"; var attribute = new MaxLengthAttribute(4); attribute.ErrorMessage = "{0} should have no more than {1} characters."; var localizedString = new LocalizedString(attribute.ErrorMessage, "Longueur est invalide : 4"); var stringLocalizer = new Mock(); stringLocalizer.Setup(s => s[attribute.ErrorMessage, It.IsAny()]).Returns(localizedString); var validator = new DataAnnotationsModelValidator( new ValidationAttributeAdapterProvider(), attribute, stringLocalizer.Object); var validationContext = new ModelValidationContext( actionContext: new ActionContext(), modelMetadata: metadata, metadataProvider: _metadataProvider, container: container, model: "abcde"); // Act var result = validator.Validate(validationContext); // Assert var validationResult = result.Single(); Assert.Empty(validationResult.MemberName); Assert.Equal("Longueur est invalide : 4", validationResult.Message); } [Fact] public void Validate_CanUseRequestServices_WithinValidationAttribute() { // Arrange var service = new Mock(); service.Setup(x => x.DoSomething()).Verifiable(); var provider = new ServiceCollection().AddSingleton(service.Object).BuildServiceProvider(); var httpContext = new Mock(); httpContext.SetupGet(x => x.RequestServices).Returns(provider); var attribute = new Mock { CallBase = true }; attribute .Setup(p => p.IsValidPublic(It.IsAny(), It.IsAny())) .Callback((object o, ValidationContext context) => { var receivedService = context.GetService(); Assert.Equal(service.Object, receivedService); receivedService.DoSomething(); }); var validator = new DataAnnotationsModelValidator( new ValidationAttributeAdapterProvider(), attribute.Object, stringLocalizer: null); var validationContext = new ModelValidationContext( actionContext: new ActionContext { HttpContext = httpContext.Object }, modelMetadata: _metadataProvider.GetMetadataForType(typeof(object)), metadataProvider: _metadataProvider, container: null, model: new object()); // Act var results = validator.Validate(validationContext); // Assert service.Verify(); } private const string LocalizationKey = "LocalizeIt"; public static TheoryData Validate_AttributesIncludeValues { get { var pattern = "apattern"; var length = 5; var regex = "^((?!" + pattern + ").)*$"; return new TheoryData { { new RegularExpressionAttribute(regex) { ErrorMessage = LocalizationKey }, pattern, new object[] { nameof(SampleModel), regex } }, { new MaxLengthAttribute(length) { ErrorMessage = LocalizationKey }, pattern, new object[] { nameof(SampleModel), length }}, { new MaxLengthAttribute(length) { ErrorMessage = LocalizationKey }, pattern, new object[] { nameof(SampleModel), length } }, { new CompareAttribute(pattern) { ErrorMessage = LocalizationKey }, pattern, new object[] { nameof(SampleModel), pattern }}, { new MinLengthAttribute(length) { ErrorMessage = LocalizationKey }, "a", new object[] { nameof(SampleModel), length } }, { new CreditCardAttribute() { ErrorMessage = LocalizationKey }, pattern, new object[] { nameof(SampleModel), "CreditCard" } }, { new StringLengthAttribute(length) { ErrorMessage = LocalizationKey, MinimumLength = 1}, string.Empty, new object[] { nameof(SampleModel), length, 1 } }, { new RangeAttribute(0, length) { ErrorMessage = LocalizationKey }, pattern, new object[] { nameof(SampleModel), 0, length} }, { new EmailAddressAttribute() { ErrorMessage = LocalizationKey }, pattern, new object[] { nameof(SampleModel), "EmailAddress" } }, { new PhoneAttribute() { ErrorMessage = LocalizationKey }, pattern, new object[] { nameof(SampleModel), "PhoneNumber" } }, { new UrlAttribute() { ErrorMessage = LocalizationKey }, pattern, new object[] { nameof(SampleModel), "Url" } } }; } } [Theory] [MemberData(nameof(Validate_AttributesIncludeValues))] public void Validate_IsValidFalse_StringLocalizerGetsArguments( ValidationAttribute attribute, string model, object[] values) { // Arrange var stringLocalizer = new Mock(); var validator = new DataAnnotationsModelValidator( new ValidationAttributeAdapterProvider(), attribute, stringLocalizer.Object); var metadata = _metadataProvider.GetMetadataForType(typeof(SampleModel)); var validationContext = new ModelValidationContext( actionContext: new ActionContext(), modelMetadata: metadata, metadataProvider: _metadataProvider, container: null, model: model); // Act validator.Validate(validationContext); // Assert var json = Newtonsoft.Json.JsonConvert.SerializeObject(values) + " " + attribute.GetType().Name; stringLocalizer.Verify(l => l[LocalizationKey, values], json); } public abstract class TestableValidationAttribute : ValidationAttribute { protected override ValidationResult IsValid(object value, ValidationContext validationContext) { return IsValidPublic(value, validationContext); } public abstract ValidationResult IsValidPublic(object value, ValidationContext validationContext); } private class SampleModel { public string Name { get; set; } } public interface IExampleService { void DoSomething(); } } }