// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Microsoft.AspNet.Http.Core; using Microsoft.AspNet.Mvc.ModelBinding; using Microsoft.AspNet.Mvc.ModelBinding.Metadata; using Microsoft.AspNet.Routing; using Moq; using Xunit; namespace Microsoft.AspNet.Mvc.Core.Test { public class ControllerActionArgumentBinderTests { public class MySimpleModel { } [Bind(Prefix = "TypePrefix")] public class MySimpleModelWithTypeBasedBind { } public void ParameterWithNoBindAttribute(MySimpleModelWithTypeBasedBind parameter) { } public void ParameterHasFieldPrefix([Bind(Prefix = "simpleModelPrefix")] string parameter) { } public void ParameterHasEmptyFieldPrefix([Bind(Prefix = "")] MySimpleModel parameter, [Bind(Prefix = "")] MySimpleModelWithTypeBasedBind parameter1) { } public void ParameterHasPrefixAndComplexType( [Bind(Prefix = "simpleModelPrefix")] MySimpleModel parameter, [Bind(Prefix = "simpleModelPrefix")] MySimpleModelWithTypeBasedBind parameter1) { } public void ParameterHasEmptyBindAttribute([Bind] MySimpleModel parameter, [Bind] MySimpleModelWithTypeBasedBind parameter1) { } [Fact] public async Task GetActionArgumentsAsync_DoesNotAddActionArguments_IfBinderReturnsFalse() { // Arrange Func method = foo => 1; var actionDescriptor = new ControllerActionDescriptor { MethodInfo = method.Method, Parameters = new List { new ParameterDescriptor { Name = "foo", ParameterType = typeof(object), BindingInfo = new BindingInfo(), } } }; var binder = new Mock(); binder .Setup(b => b.BindModelAsync(It.IsAny())) .Returns(Task.FromResult(result: null)); var actionContext = new ActionContext( new DefaultHttpContext(), new RouteData(), actionDescriptor); var actionBindingContext = new ActionBindingContext() { ModelBinder = binder.Object, }; var modelMetadataProvider = TestModelMetadataProvider.CreateDefaultProvider(); var inputFormattersProvider = new Mock(); inputFormattersProvider .SetupGet(o => o.InputFormatters) .Returns(new List()); var invoker = new DefaultControllerActionArgumentBinder( modelMetadataProvider, new DefaultObjectValidator(Mock.Of(), modelMetadataProvider), new MockMvcOptionsAccessor()); // Act var result = await invoker.GetActionArgumentsAsync(actionContext, actionBindingContext); // Assert Assert.Empty(result); } [Fact] public async Task GetActionArgumentsAsync_DoesNotAddActionArguments_IfBinderDoesNotSetModel() { // Arrange Func method = foo => 1; var actionDescriptor = new ControllerActionDescriptor { MethodInfo = method.Method, Parameters = new List { new ParameterDescriptor { Name = "foo", ParameterType = typeof(object), BindingInfo = new BindingInfo(), } } }; var binder = new Mock(); binder .Setup(b => b.BindModelAsync(It.IsAny())) .Returns(Task.FromResult(new ModelBindingResult(null, "", false))); var actionContext = new ActionContext( new DefaultHttpContext(), new RouteData(), actionDescriptor); var actionBindingContext = new ActionBindingContext() { ModelBinder = binder.Object, }; var inputFormattersProvider = new Mock(); inputFormattersProvider .SetupGet(o => o.InputFormatters) .Returns(new List()); var modelMetadataProvider = TestModelMetadataProvider.CreateDefaultProvider(); var invoker = new DefaultControllerActionArgumentBinder( modelMetadataProvider, new DefaultObjectValidator(Mock.Of(), modelMetadataProvider), new MockMvcOptionsAccessor()); // Act var result = await invoker.GetActionArgumentsAsync(actionContext, actionBindingContext); // Assert Assert.Empty(result); } [Fact] public async Task GetActionArgumentsAsync_AddsActionArguments_IfBinderReturnsTrue() { // Arrange Func method = foo => 1; var actionDescriptor = new ControllerActionDescriptor { MethodInfo = method.Method, Parameters = new List { new ParameterDescriptor { Name = "foo", ParameterType = typeof(string), BindingInfo = new BindingInfo(), } }, }; var value = "Hello world"; var metadataProvider = new EmptyModelMetadataProvider(); var binder = new Mock(); binder .Setup(b => b.BindModelAsync(It.IsAny())) .Callback((ModelBindingContext context) => { context.ModelMetadata = metadataProvider.GetMetadataForType(typeof(string)); }) .Returns(Task.FromResult(result: new ModelBindingResult(value, "", true))); var actionContext = new ActionContext( new DefaultHttpContext(), new RouteData(), actionDescriptor); var actionBindingContext = new ActionBindingContext() { ModelBinder = binder.Object, }; var mockValidatorProvider = new Mock(MockBehavior.Strict); mockValidatorProvider.Setup(o => o.Validate(It.IsAny())); var invoker = new DefaultControllerActionArgumentBinder( metadataProvider, mockValidatorProvider.Object, new MockMvcOptionsAccessor()); // Act var result = await invoker.GetActionArgumentsAsync(actionContext, actionBindingContext); // Assert Assert.Equal(1, result.Count); Assert.Equal(value, result["foo"]); } [Fact] public async Task GetActionArgumentsAsync_CallsValidator_IfModelBinderSucceeds() { // Arrange Func method = foo => 1; var actionDescriptor = new ControllerActionDescriptor { MethodInfo = method.Method, Parameters = new List { new ParameterDescriptor { Name = "foo", ParameterType = typeof(object), BindingInfo = new BindingInfo(), } } }; var actionContext = new ActionContext( new DefaultHttpContext(), new RouteData(), actionDescriptor); var binder = new Mock(); binder .Setup(b => b.BindModelAsync(It.IsAny())) .Returns(Task.FromResult(result: new ModelBindingResult( model: null, key: string.Empty, isModelSet: true))); var actionBindingContext = new ActionBindingContext() { ModelBinder = binder.Object, }; var mockValidatorProvider = new Mock(MockBehavior.Strict); mockValidatorProvider .Setup(o => o.Validate(It.IsAny())) .Verifiable(); var invoker = new DefaultControllerActionArgumentBinder( TestModelMetadataProvider.CreateDefaultProvider(), mockValidatorProvider.Object, new MockMvcOptionsAccessor()); // Act var result = await invoker.GetActionArgumentsAsync(actionContext, actionBindingContext); // Assert mockValidatorProvider.Verify( o => o.Validate(It.IsAny()), Times.Once()); } [Fact] public async Task GetActionArgumentsAsync_DoesNotCallValidator_IfModelBinderFails() { // Arrange Func method = foo => 1; var actionDescriptor = new ControllerActionDescriptor { MethodInfo = method.Method, Parameters = new List { new ParameterDescriptor { Name = "foo", ParameterType = typeof(object), BindingInfo = new BindingInfo(), } } }; var actionContext = new ActionContext( new DefaultHttpContext(), new RouteData(), actionDescriptor); var binder = new Mock(); binder .Setup(b => b.BindModelAsync(It.IsAny())) .Returns(Task.FromResult(null)); var actionBindingContext = new ActionBindingContext() { ModelBinder = binder.Object, }; var mockValidatorProvider = new Mock(MockBehavior.Strict); mockValidatorProvider.Setup(o => o.Validate(It.IsAny())) .Verifiable(); var invoker = new DefaultControllerActionArgumentBinder( TestModelMetadataProvider.CreateDefaultProvider(), mockValidatorProvider.Object, new MockMvcOptionsAccessor()); // Act var result = await invoker.GetActionArgumentsAsync(actionContext, actionBindingContext); // Assert mockValidatorProvider.Verify(o => o.Validate(It.IsAny()), Times.Never()); } [Fact] public async Task GetActionArgumentsAsync_SetsMaxModelErrors() { // Arrange Func method = foo => 1; var actionDescriptor = new ControllerActionDescriptor { MethodInfo = method.Method, Parameters = new List { new ParameterDescriptor { Name = "foo", ParameterType = typeof(object), BindingInfo = new BindingInfo(), } } }; var binder = new Mock(); binder .Setup(b => b.BindModelAsync(It.IsAny())) .Returns(Task.FromResult( result: new ModelBindingResult(model: "Hello", key: string.Empty, isModelSet: true))); var actionContext = new ActionContext( new DefaultHttpContext(), new RouteData(), actionDescriptor); var actionBindingContext = new ActionBindingContext() { ModelBinder = binder.Object, }; var inputFormattersProvider = new Mock(); inputFormattersProvider .SetupGet(o => o.InputFormatters) .Returns(new List()); var options = new MockMvcOptionsAccessor(); options.Options.MaxModelValidationErrors = 5; var mockValidatorProvider = new Mock(MockBehavior.Strict); mockValidatorProvider.Setup(o => o.Validate(It.IsAny())); var invoker = new DefaultControllerActionArgumentBinder( TestModelMetadataProvider.CreateDefaultProvider(), mockValidatorProvider.Object, options); // Act var result = await invoker.GetActionArgumentsAsync(actionContext, actionBindingContext); // Assert Assert.Equal(5, actionContext.ModelState.MaxAllowedErrors); } private class TestController { public string UnmarkedProperty { get; set; } [NonValueProviderBinderMetadata] public string NonValueBinderMarkedProperty { get; set; } [ValueProviderMetadata] public string ValueBinderMarkedProperty { get; set; } public Person ActionWithBodyParam([FromBody] Person bodyParam) { return bodyParam; } public Person ActionWithTwoBodyParam([FromBody] Person bodyParam, [FromBody] Person bodyParam1) { return bodyParam; } } private class Person { public string Name { get; set; } } private class NonValueProviderBinderMetadataAttribute : Attribute, IBindingSourceMetadata { public BindingSource BindingSource { get { return BindingSource.Body; } } } private class ValueProviderMetadataAttribute : Attribute, IBindingSourceMetadata { public BindingSource BindingSource { get { return BindingSource.Query; } } } [Bind(new string[] { nameof(IncludedExplicitly1), nameof(IncludedExplicitly2) })] private class TypeWithIncludedPropertiesUsingBindAttribute { public int ExcludedByDefault1 { get; set; } public int ExcludedByDefault2 { get; set; } public int IncludedExplicitly1 { get; set; } public int IncludedExplicitly2 { get; set; } } } }