540 lines
21 KiB
C#
540 lines
21 KiB
C#
// 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<string> NullOrEmptyNames
|
|
{
|
|
get
|
|
{
|
|
return new TheoryData<string>
|
|
{
|
|
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<IStringLocalizerFactory>(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<IOptions<MvcDataAnnotationsLocalizationOptions>>();
|
|
var localizer = new Mock<IStringLocalizer>(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<IStringLocalizerFactory>(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<IStringLocalizer>(MockBehavior.Strict);
|
|
localizer
|
|
.Setup(l => l["Error about '{0}' from override.", "Length"])
|
|
.Returns(localizedString)
|
|
.Verifiable();
|
|
var options = context.ActionContext.HttpContext.RequestServices
|
|
.GetRequiredService<IOptions<MvcDataAnnotationsLocalizationOptions>>();
|
|
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<IStringLocalizerFactory>(MockBehavior.Strict).Object;
|
|
var context = GetValidationContext(localizerFactory);
|
|
|
|
var localizer = new Mock<IStringLocalizer>(MockBehavior.Strict);
|
|
var options = context.ActionContext.HttpContext.RequestServices
|
|
.GetRequiredService<IOptions<MvcDataAnnotationsLocalizationOptions>>();
|
|
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<IStringLocalizerFactory>(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<IStringLocalizer>(MockBehavior.Strict);
|
|
localizer
|
|
.Setup(l => l["Error about '{0}' from override.", "Display Length"])
|
|
.Returns(localizedString)
|
|
.Verifiable();
|
|
var options = context.ActionContext.HttpContext.RequestServices
|
|
.GetRequiredService<IOptions<MvcDataAnnotationsLocalizationOptions>>();
|
|
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<IStringLocalizerFactory>(localizerFactory);
|
|
}
|
|
|
|
var serviceProvider = serviceCollection.BuildServiceProvider();
|
|
|
|
var httpContext = new DefaultHttpContext
|
|
{
|
|
RequestServices = serviceProvider,
|
|
};
|
|
|
|
var actionContext = new ActionContext(
|
|
httpContext,
|
|
routeData: new Mock<RouteData>().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;
|
|
}
|
|
}
|
|
}
|
|
}
|