aspnetcore/test/Microsoft.AspNetCore.Mvc.Co.../ModelBinding/ParameterBinderTest.cs

441 lines
18 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 System;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.DataAnnotations;
using Microsoft.AspNetCore.Mvc.DataAnnotations.Internal;
using Microsoft.AspNetCore.Mvc.Internal;
using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Moq;
using Xunit;
namespace Microsoft.AspNetCore.Mvc.ModelBinding
{
public class ParameterBinderTest
{
public static TheoryData BindModelAsyncData
{
get
{
var emptyBindingInfo = new BindingInfo();
var bindingInfoWithName = new BindingInfo
{
BinderModelName = "bindingInfoName",
BinderType = typeof(Person),
};
// parameterBindingInfo, metadataBinderModelName, parameterName, expectedBinderModelName
return new TheoryData<BindingInfo, string, string, string>
{
// If the parameter name is not a prefix match, it is ignored. But name is required to create a
// ModelBindingContext.
{ null, null, "parameterName", string.Empty },
{ emptyBindingInfo, null, "parameterName", string.Empty },
{ bindingInfoWithName, null, "parameterName", "bindingInfoName" },
{ null, "modelBinderName", "parameterName", "modelBinderName" },
{ null, null, "parameterName", string.Empty },
// Parameter's BindingInfo has highest precedence
{ bindingInfoWithName, "modelBinderName", "parameterName", "bindingInfoName" },
};
}
}
[Theory]
[MemberData(nameof(BindModelAsyncData))]
public async Task BindModelAsync_PassesExpectedBindingInfoAndMetadata_IfPrefixDoesNotMatch(
BindingInfo parameterBindingInfo,
string metadataBinderModelName,
string parameterName,
string expectedModelName)
{
// Arrange
var binderExecuted = false;
var metadataProvider = new TestModelMetadataProvider();
metadataProvider.ForType<Person>().BindingDetails(binding =>
{
binding.BinderModelName = metadataBinderModelName;
});
var metadata = metadataProvider.GetMetadataForType(typeof(Person));
var modelBinder = new Mock<IModelBinder>();
modelBinder
.Setup(b => b.BindModelAsync(It.IsAny<ModelBindingContext>()))
.Callback((ModelBindingContext context) =>
{
Assert.Equal(expectedModelName, context.ModelName, StringComparer.Ordinal);
})
.Returns(Task.CompletedTask);
var parameterDescriptor = new ParameterDescriptor
{
BindingInfo = parameterBindingInfo,
Name = parameterName,
ParameterType = typeof(Person),
};
var factory = new Mock<IModelBinderFactory>(MockBehavior.Strict);
factory
.Setup(f => f.CreateBinder(It.IsAny<ModelBinderFactoryContext>()))
.Callback((ModelBinderFactoryContext context) =>
{
binderExecuted = true;
// Confirm expected data is passed through to ModelBindingFactory.
Assert.Same(parameterDescriptor.BindingInfo, context.BindingInfo);
Assert.Same(parameterDescriptor, context.CacheToken);
Assert.Equal(metadata, context.Metadata);
})
.Returns(modelBinder.Object);
var parameterBinder = new ParameterBinder(
metadataProvider,
factory.Object,
CreateMockValidatorProvider(),
NullLoggerFactory.Instance);
var controllerContext = GetControllerContext();
// Act & Assert
await parameterBinder.BindModelAsync(controllerContext, new SimpleValueProvider(), parameterDescriptor);
Assert.True(binderExecuted);
}
[Fact]
public async Task BindModelAsync_PassesExpectedBindingInfoAndMetadata_IfPrefixMatches()
{
// Arrange
var expectedModelName = "expectedName";
var binderExecuted = false;
var metadataProvider = new TestModelMetadataProvider();
var metadata = metadataProvider.GetMetadataForType(typeof(Person));
var modelBinder = new Mock<IModelBinder>();
modelBinder
.Setup(b => b.BindModelAsync(It.IsAny<ModelBindingContext>()))
.Callback((ModelBindingContext context) =>
{
Assert.Equal(expectedModelName, context.ModelName, StringComparer.Ordinal);
})
.Returns(Task.CompletedTask);
var parameterDescriptor = new ParameterDescriptor
{
Name = expectedModelName,
ParameterType = typeof(Person),
};
var factory = new Mock<IModelBinderFactory>(MockBehavior.Strict);
factory
.Setup(f => f.CreateBinder(It.IsAny<ModelBinderFactoryContext>()))
.Callback((ModelBinderFactoryContext context) =>
{
binderExecuted = true;
// Confirm expected data is passed through to ModelBindingFactory.
Assert.Null(context.BindingInfo);
Assert.Same(parameterDescriptor, context.CacheToken);
Assert.Equal(metadata, context.Metadata);
})
.Returns(modelBinder.Object);
var argumentBinder = new ParameterBinder(
metadataProvider,
factory.Object,
CreateMockValidatorProvider(),
NullLoggerFactory.Instance);
var valueProvider = new SimpleValueProvider
{
{ expectedModelName, new object() },
};
var valueProviderFactory = new SimpleValueProviderFactory(valueProvider);
var controllerContext = GetControllerContext();
// Act & Assert
await argumentBinder.BindModelAsync(controllerContext, valueProvider, parameterDescriptor);
Assert.True(binderExecuted);
}
[Fact]
public async Task BindModelAsync_EnforcesTopLevelBindRequired()
{
// Arrange
var actionContext = GetControllerContext();
var mockModelMetadata = CreateMockModelMetadata();
mockModelMetadata.Setup(o => o.IsBindingRequired).Returns(true);
mockModelMetadata.Setup(o => o.DisplayName).Returns("Ignored Display Name"); // Bind attribute errors are phrased in terms of the model name, not display name
var parameterBinder = CreateParameterBinder(mockModelMetadata.Object, validator: null);
var modelBindingResult = ModelBindingResult.Failed();
// Act
var result = await parameterBinder.BindModelAsync(
actionContext,
CreateMockModelBinder(modelBindingResult),
CreateMockValueProvider(),
new ParameterDescriptor { Name = "myParam", ParameterType = typeof(Person) },
mockModelMetadata.Object,
"ignoredvalue");
// Assert
Assert.False(actionContext.ModelState.IsValid);
Assert.Equal("myParam", actionContext.ModelState.Single().Key);
Assert.Equal(
new DefaultModelBindingMessageProvider().MissingBindRequiredValueAccessor("myParam"),
actionContext.ModelState.Single().Value.Errors.Single().ErrorMessage);
}
[Fact]
public async Task BindModelAsync_EnforcesTopLevelRequired()
{
// Arrange
var actionContext = GetControllerContext();
var mockModelMetadata = CreateMockModelMetadata();
mockModelMetadata.Setup(o => o.IsRequired).Returns(true);
mockModelMetadata.Setup(o => o.DisplayName).Returns("My Display Name");
mockModelMetadata.Setup(o => o.ValidatorMetadata).Returns(new[]
{
new RequiredAttribute()
});
var validator = new DataAnnotationsModelValidator(
new ValidationAttributeAdapterProvider(),
new RequiredAttribute(),
stringLocalizer: null);
var parameterBinder = CreateParameterBinder(
mockModelMetadata.Object,
validator);
var modelBindingResult = ModelBindingResult.Success(null);
// Act
var result = await parameterBinder.BindModelAsync(
actionContext,
CreateMockModelBinder(modelBindingResult),
CreateMockValueProvider(),
new ParameterDescriptor { Name = "myParam", ParameterType = typeof(Person) },
mockModelMetadata.Object,
"ignoredvalue");
// Assert
Assert.False(actionContext.ModelState.IsValid);
Assert.Equal("myParam", actionContext.ModelState.Single().Key);
Assert.Equal(
new RequiredAttribute().FormatErrorMessage("My Display Name"),
actionContext.ModelState.Single().Value.Errors.Single().ErrorMessage);
}
[Fact]
public async Task BindModelAsync_EnforcesTopLevelDataAnnotationsAttribute()
{
// Arrange
var actionContext = GetControllerContext();
var mockModelMetadata = CreateMockModelMetadata();
var validationAttribute = new RangeAttribute(1, 100);
mockModelMetadata.Setup(o => o.DisplayName).Returns("My Display Name");
mockModelMetadata.Setup(o => o.ValidatorMetadata).Returns(new[] {
validationAttribute
});
var validator = new DataAnnotationsModelValidator(
new ValidationAttributeAdapterProvider(),
validationAttribute,
stringLocalizer: null);
var parameterBinder = CreateParameterBinder(
mockModelMetadata.Object,
validator);
var modelBindingResult = ModelBindingResult.Success(123);
// Act
var result = await parameterBinder.BindModelAsync(
actionContext,
CreateMockModelBinder(modelBindingResult),
CreateMockValueProvider(),
new ParameterDescriptor { Name = "myParam", ParameterType = typeof(Person) },
mockModelMetadata.Object,
50); // This value is ignored, because test explicitly set the ModelBindingResult
// Assert
Assert.False(actionContext.ModelState.IsValid);
Assert.Equal("myParam", actionContext.ModelState.Single().Key);
Assert.Equal(
validationAttribute.FormatErrorMessage("My Display Name"),
actionContext.ModelState.Single().Value.Errors.Single().ErrorMessage);
}
[Fact]
public async Task BindModelAsync_SupportsIObjectModelValidatorForBackCompat()
{
// Arrange
var actionContext = GetControllerContext();
var mockValidator = new Mock<IObjectModelValidator>(MockBehavior.Strict);
mockValidator
.Setup(o => o.Validate(
It.IsAny<ActionContext>(),
It.IsAny<ValidationStateDictionary>(),
It.IsAny<string>(),
It.IsAny<object>()))
.Callback((ActionContext context, ValidationStateDictionary validationState, string prefix, object model) =>
{
context.ModelState.AddModelError(prefix, "Test validation message");
});
var modelMetadata = CreateMockModelMetadata().Object;
var parameterBinder = CreateBackCompatParameterBinder(
modelMetadata,
mockValidator.Object);
var modelBindingResult = ModelBindingResult.Success(123);
// Act
var result = await parameterBinder.BindModelAsync(
actionContext,
CreateMockModelBinder(modelBindingResult),
CreateMockValueProvider(),
new ParameterDescriptor { Name = "myParam", ParameterType = typeof(Person) },
modelMetadata,
"ignored");
// Assert
Assert.False(actionContext.ModelState.IsValid);
Assert.Equal("myParam", actionContext.ModelState.Single().Key);
Assert.Equal(
"Test validation message",
actionContext.ModelState.Single().Value.Errors.Single().ErrorMessage);
}
private static ControllerContext GetControllerContext()
{
var services = new ServiceCollection();
services.AddSingleton<ILoggerFactory>(NullLoggerFactory.Instance);
return new ControllerContext()
{
HttpContext = new DefaultHttpContext()
{
RequestServices = services.BuildServiceProvider()
}
};
}
private static Mock<FakeModelMetadata> CreateMockModelMetadata()
{
var mockModelMetadata = new Mock<FakeModelMetadata>();
mockModelMetadata
.Setup(o => o.ModelBindingMessageProvider)
.Returns(new DefaultModelBindingMessageProvider());
return mockModelMetadata;
}
private static IModelBinder CreateMockModelBinder(ModelBindingResult modelBinderResult)
{
var mockBinder = new Mock<IModelBinder>(MockBehavior.Strict);
mockBinder
.Setup(o => o.BindModelAsync(It.IsAny<ModelBindingContext>()))
.Returns<ModelBindingContext>(context =>
{
context.Result = modelBinderResult;
return Task.CompletedTask;
});
return mockBinder.Object;
}
private static ParameterBinder CreateParameterBinder(
ModelMetadata modelMetadata,
IModelValidator validator)
{
var mockModelMetadataProvider = new Mock<IModelMetadataProvider>(MockBehavior.Strict);
mockModelMetadataProvider
.Setup(o => o.GetMetadataForType(typeof(Person)))
.Returns(modelMetadata);
var mockModelBinderFactory = new Mock<IModelBinderFactory>(MockBehavior.Strict);
return new ParameterBinder(
mockModelMetadataProvider.Object,
mockModelBinderFactory.Object,
CreateMockValidatorProvider(validator),
NullLoggerFactory.Instance);
}
private static ParameterBinder CreateBackCompatParameterBinder(
ModelMetadata modelMetadata,
IObjectModelValidator validator)
{
var mockModelMetadataProvider = new Mock<IModelMetadataProvider>(MockBehavior.Strict);
mockModelMetadataProvider
.Setup(o => o.GetMetadataForType(typeof(Person)))
.Returns(modelMetadata);
var mockModelBinderFactory = new Mock<IModelBinderFactory>(MockBehavior.Strict);
#pragma warning disable CS0618 // Type or member is obsolete
return new ParameterBinder(
mockModelMetadataProvider.Object,
mockModelBinderFactory.Object,
validator);
#pragma warning restore CS0618 // Type or member is obsolete
}
private static IValueProvider CreateMockValueProvider()
{
var mockValueProvider = new Mock<IValueProvider>(MockBehavior.Strict);
mockValueProvider
.Setup(o => o.ContainsPrefix(It.IsAny<string>()))
.Returns(true);
return mockValueProvider.Object;
}
private static IModelValidatorProvider CreateMockValidatorProvider(IModelValidator validator = null)
{
var mockValidator = new Mock<IModelValidatorProvider>();
mockValidator
.Setup(o => o.CreateValidators(
It.IsAny<ModelValidatorProviderContext>()))
.Callback<ModelValidatorProviderContext>(context =>
{
if (validator != null)
{
foreach (var result in context.Results)
{
result.Validator = validator;
}
}
});
return mockValidator.Object;
}
private class Person : IEquatable<Person>, IEquatable<object>
{
public string Name { get; set; }
public bool Equals(Person other)
{
return other != null && string.Equals(Name, other.Name, StringComparison.Ordinal);
}
bool IEquatable<object>.Equals(object obj)
{
return Equals(obj as Person);
}
}
public abstract class FakeModelMetadata : ModelMetadata
{
public FakeModelMetadata()
: base(ModelMetadataIdentity.ForType(typeof(string)))
{
}
}
}
}