// 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. #if DNX451 using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Threading.Tasks; using Microsoft.AspNet.Mvc.ModelBinding.Validation; using Moq; using Xunit; namespace Microsoft.AspNet.Mvc.ModelBinding.Test { public class CompositeModelBinderTest { [Fact] public async Task BindModel_SuccessfulBind_ReturnsModel() { // Arrange var bindingContext = new ModelBindingContext { FallbackToEmptyPrefix = true, ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(typeof(int)), ModelName = "someName", ModelState = new ModelStateDictionary(), ValueProvider = new SimpleValueProvider { { "someName", "dummyValue" } }, OperationBindingContext = new OperationBindingContext { ValidatorProvider = GetValidatorProvider() } }; var mockIntBinder = new Mock(); mockIntBinder .Setup(o => o.BindModelAsync(It.IsAny())) .Returns( delegate (ModelBindingContext context) { Assert.Same(bindingContext.ModelMetadata, context.ModelMetadata); Assert.Equal("someName", context.ModelName); Assert.Same(bindingContext.ValueProvider, context.ValueProvider); return Task.FromResult( new ModelBindingResult(model: 42, key: "someName", isModelSet: true)); }); var shimBinder = CreateCompositeBinder(mockIntBinder.Object); // Act var result = await shimBinder.BindModelAsync(bindingContext); // Assert Assert.NotNull(result); Assert.True(result.IsModelSet); Assert.Equal(42, result.Model); } [Fact] public async Task BindModel_SuccessfulBind_ComplexTypeFallback_ReturnsModel() { // Arrange var expectedModel = new List { 1, 2, 3, 4, 5 }; var bindingContext = new ModelBindingContext { FallbackToEmptyPrefix = true, ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(typeof(List)), ModelName = "someName", ModelState = new ModelStateDictionary(), ValueProvider = new SimpleValueProvider { { "someOtherName", "dummyValue" } }, OperationBindingContext = new OperationBindingContext { ValidatorProvider = GetValidatorProvider() } }; var mockIntBinder = new Mock(); mockIntBinder .Setup(o => o.BindModelAsync(It.IsAny())) .Returns( delegate (ModelBindingContext mbc) { if (!string.IsNullOrEmpty(mbc.ModelName)) { return Task.FromResult(null); } Assert.Same(bindingContext.ModelMetadata, mbc.ModelMetadata); Assert.Equal("", mbc.ModelName); Assert.Same(bindingContext.ValueProvider, mbc.ValueProvider); return Task.FromResult(new ModelBindingResult(expectedModel, string.Empty, true)); }); var shimBinder = CreateCompositeBinder(mockIntBinder.Object); // Act var result = await shimBinder.BindModelAsync(bindingContext); // Assert Assert.NotNull(result); Assert.True(result.IsModelSet); Assert.Equal(string.Empty, result.Key); Assert.Equal(expectedModel, result.Model); } [Fact] public async Task ModelBinder_ReturnsNull_IfBinderMatchesButDoesNotSetModel() { // Arrange var bindingContext = new ModelBindingContext { FallbackToEmptyPrefix = true, ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(typeof(List)), ModelName = "someName", ModelState = new ModelStateDictionary(), ValueProvider = new SimpleValueProvider { { "someOtherName", "dummyValue" } }, OperationBindingContext = new OperationBindingContext { ValidatorProvider = GetValidatorProvider() } }; var modelBinder = new Mock(); modelBinder .Setup(mb => mb.BindModelAsync(It.IsAny())) .Returns(Task.FromResult(new ModelBindingResult(model: null, key: "someName", isModelSet: false))); var composite = CreateCompositeBinder(modelBinder.Object); // Act var result = await composite.BindModelAsync(bindingContext); // Assert Assert.Null(result); } [Fact] public async Task ModelBinder_FallsBackToEmpty_IfBinderMatchesButDoesNotSetModel() { // Arrange var bindingContext = new ModelBindingContext { FallbackToEmptyPrefix = true, ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(typeof(List)), ModelName = "someName", ModelState = new ModelStateDictionary(), ValueProvider = new SimpleValueProvider { { "someOtherName", "dummyValue" } }, OperationBindingContext = new OperationBindingContext { ValidatorProvider = GetValidatorProvider() } }; var count = 0; var modelBinder = new Mock(); modelBinder .Setup(mb => mb.BindModelAsync(It.IsAny())) .Callback(context => { // Expect two calls; the second with empty ModelName. Assert.InRange(count, 0, 1); count++; if (count == 1) { Assert.Equal("someName", context.ModelName); } else { Assert.Empty(context.ModelName); } }) .Returns(Task.FromResult(new ModelBindingResult(model: null, key: "someName", isModelSet: false))); var composite = CreateCompositeBinder(modelBinder.Object); // Act & Assert var result = await composite.BindModelAsync(bindingContext); } [Fact] public async Task ModelBinder_DoesNotFallBackToEmpty_IfFallbackToEmptyPrefixFalse() { // Arrange var bindingContext = new ModelBindingContext { FallbackToEmptyPrefix = false, ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(typeof(List)), ModelName = "someName", ModelState = new ModelStateDictionary(), ValueProvider = new SimpleValueProvider { { "someOtherName", "dummyValue" } }, OperationBindingContext = new OperationBindingContext { ValidatorProvider = GetValidatorProvider() } }; var modelBinder = new Mock(); modelBinder .Setup(mb => mb.BindModelAsync(It.IsAny())) .Callback(context => { Assert.Equal("someName", context.ModelName); }) .Returns(Task.FromResult(new ModelBindingResult(model: null, key: "someName", isModelSet: false))) .Verifiable(); var composite = CreateCompositeBinder(modelBinder.Object); // Act & Assert var result = await composite.BindModelAsync(bindingContext); modelBinder.Verify(mb => mb.BindModelAsync(It.IsAny()), Times.Once); } [Fact] public async Task ModelBinder_DoesNotFallBackToEmpty_IfErrorsAreAdded() { // Arrange var bindingContext = new ModelBindingContext { FallbackToEmptyPrefix = false, ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(typeof(List)), ModelName = "someName", ModelState = new ModelStateDictionary(), ValueProvider = new SimpleValueProvider { { "someOtherName", "dummyValue" } }, OperationBindingContext = new OperationBindingContext { ValidatorProvider = GetValidatorProvider() } }; var modelBinder = new Mock(); modelBinder .Setup(mb => mb.BindModelAsync(It.IsAny())) .Callback(context => { Assert.Equal("someName", context.ModelName); context.ModelState.AddModelError(context.ModelName, "this is an error message"); }) .Returns(Task.FromResult(new ModelBindingResult(model: null, key: "someName", isModelSet: false))) .Verifiable(); var composite = CreateCompositeBinder(modelBinder.Object); // Act & Assert var result = await composite.BindModelAsync(bindingContext); modelBinder.Verify(mb => mb.BindModelAsync(It.IsAny()), Times.Once); } [Fact] public async Task ModelBinder_ReturnsNotNull_SetsNullValue_SetsModelStateKey() { // Arrange var bindingContext = new ModelBindingContext { FallbackToEmptyPrefix = true, ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(typeof(List)), ModelName = "someName", ModelState = new ModelStateDictionary(), ValueProvider = new SimpleValueProvider { { "someOtherName", "dummyValue" } }, OperationBindingContext = new OperationBindingContext { ValidatorProvider = GetValidatorProvider() } }; var modelBinder = new Mock(); modelBinder .Setup(mb => mb.BindModelAsync(It.IsAny())) .Returns(Task.FromResult(new ModelBindingResult(model: null, key: "someName", isModelSet: true))); var composite = CreateCompositeBinder(modelBinder.Object); // Act var result = await composite.BindModelAsync(bindingContext); // Assert Assert.NotNull(result); Assert.True(result.IsModelSet); Assert.Equal("someName", result.Key); Assert.Null(result.Model); } [Fact] public async Task BindModel_UnsuccessfulBind_BinderFails_ReturnsNull() { // Arrange var mockListBinder = new Mock(); mockListBinder.Setup(o => o.BindModelAsync(It.IsAny())) .Returns(Task.FromResult(null)) .Verifiable(); var shimBinder = mockListBinder.Object; var bindingContext = new ModelBindingContext { FallbackToEmptyPrefix = false, ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(typeof(List)), }; // Act var result = await shimBinder.BindModelAsync(bindingContext); // Assert Assert.Null(result); Assert.True(bindingContext.ModelState.IsValid); mockListBinder.Verify(); } [Fact] public async Task BindModel_UnsuccessfulBind_SimpleTypeNoFallback_ReturnsNull() { // Arrange var innerBinder = Mock.Of(); var shimBinder = CreateCompositeBinder(innerBinder); var bindingContext = new ModelBindingContext { FallbackToEmptyPrefix = true, ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(typeof(int)), ModelState = new ModelStateDictionary(), OperationBindingContext = Mock.Of(), }; // Act var result = await shimBinder.BindModelAsync(bindingContext); // Assert Assert.Null(result); } [Fact] public async Task BindModel_WithDefaultBinders_BindsSimpleType() { // Arrange var binder = CreateBinderWithDefaults(); var valueProvider = new SimpleValueProvider { { "firstName", "firstName-value"}, { "lastName", "lastName-value"} }; var bindingContext = CreateBindingContext(binder, valueProvider, typeof(SimplePropertiesModel)); // Act var result = await binder.BindModelAsync(bindingContext); // Assert Assert.NotNull(result); var model = Assert.IsType(result.Model); Assert.Equal("firstName-value", model.FirstName); Assert.Equal("lastName-value", model.LastName); Assert.NotNull(result.ValidationNode); Assert.Equal(2, result.ValidationNode.ChildNodes.Count); Assert.Equal("", result.ValidationNode.Key); Assert.Equal(bindingContext.ModelMetadata, result.ValidationNode.ModelMetadata); model = Assert.IsType(result.ValidationNode.Model); Assert.Equal("firstName-value", model.FirstName); Assert.Equal("lastName-value", model.LastName); Assert.Equal(2, result.ValidationNode.ChildNodes.Count); var validationNode = result.ValidationNode.ChildNodes[0]; Assert.Equal("FirstName", validationNode.Key); Assert.Equal("firstName-value", validationNode.Model); Assert.Empty(validationNode.ChildNodes); validationNode = result.ValidationNode.ChildNodes[1]; Assert.Equal("LastName", validationNode.Key); Assert.Equal("lastName-value", validationNode.Model); Assert.Empty(validationNode.ChildNodes); } [Fact] public async Task BindModel_WithDefaultBinders_BindsComplexType() { // Arrange var binder = CreateBinderWithDefaults(); var valueProvider = new SimpleValueProvider { { "firstName", "firstName-value"}, { "lastName", "lastName-value"}, { "friends[0].firstName", "first-friend"}, { "friends[0].age", "40"}, { "friends[0].friends[0].firstname", "nested friend"}, { "friends[1].firstName", "some other"}, { "friends[1].lastName", "name"}, { "resume", "4+mFeTp3tPF=" } }; var bindingContext = CreateBindingContext(binder, valueProvider, typeof(Person)); // Act var result = await binder.BindModelAsync(bindingContext); // Assert Assert.NotNull(result); var model = Assert.IsType(result.Model); Assert.Equal("firstName-value", model.FirstName); Assert.Equal("lastName-value", model.LastName); Assert.Equal(2, model.Friends.Count); Assert.Equal("first-friend", model.Friends[0].FirstName); Assert.Equal(40, model.Friends[0].Age); var nestedFriend = Assert.Single(model.Friends[0].Friends); Assert.Equal("nested friend", nestedFriend.FirstName); Assert.Equal("some other", model.Friends[1].FirstName); Assert.Equal("name", model.Friends[1].LastName); Assert.Equal(new byte[] { 227, 233, 133, 121, 58, 119, 180, 241 }, model.Resume); } [Fact] public async Task BindModel_DoesNotAddAValidationNode_IfModelIsNotSet() { // Arrange var valueProvider = new SimpleValueProvider(); var mockBinder = new Mock(); mockBinder .Setup(o => o.BindModelAsync(It.IsAny())) .Returns( delegate (ModelBindingContext context) { return Task.FromResult( new ModelBindingResult(model: 42, key: "someName", isModelSet: false)); }); var binder = CreateCompositeBinder(mockBinder.Object); var bindingContext = CreateBindingContext(binder, valueProvider, typeof(SimplePropertiesModel)); // Act var result = await binder.BindModelAsync(bindingContext); // Assert Assert.Null(result); } [Fact] public async Task BindModel_DoesNotAddAValidationNode_IfModelBindingResultIsNull() { // Arrange var mockBinder = new Mock(); mockBinder .Setup(o => o.BindModelAsync(It.IsAny())) .Returns(Task.FromResult(null)); var binder = CreateCompositeBinder(mockBinder.Object); var valueProvider = new SimpleValueProvider(); var bindingContext = CreateBindingContext(binder, valueProvider, typeof(SimplePropertiesModel)); // Act var result = await binder.BindModelAsync(bindingContext); // Assert Assert.Null(result); } [Fact] public async Task BindModel_UsesTheValidationNodeOnModelBindingResult_IfPresent() { // Arrange var valueProvider = new SimpleValueProvider(); ModelValidationNode validationNode = null; var mockBinder = new Mock(); mockBinder .Setup(o => o.BindModelAsync(It.IsAny())) .Returns( delegate (ModelBindingContext context) { validationNode = new ModelValidationNode("someName", context.ModelMetadata, 42); return Task.FromResult( new ModelBindingResult(42, "someName", isModelSet: true, validationNode: validationNode)); }); var binder = CreateCompositeBinder(mockBinder.Object); var bindingContext = CreateBindingContext(binder, valueProvider, typeof(SimplePropertiesModel)); // Act var result = await binder.BindModelAsync(bindingContext); // Assert Assert.NotNull(result); Assert.True(result.IsModelSet); Assert.Same(validationNode, result.ValidationNode); } private static ModelBindingContext CreateBindingContext(IModelBinder binder, IValueProvider valueProvider, Type type, IModelValidatorProvider validatorProvider = null) { validatorProvider = validatorProvider ?? GetValidatorProvider(); var metadataProvider = TestModelMetadataProvider.CreateDefaultProvider(); var bindingContext = new ModelBindingContext { FallbackToEmptyPrefix = true, ModelMetadata = metadataProvider.GetMetadataForType(type), ModelState = new ModelStateDictionary(), ValueProvider = valueProvider, OperationBindingContext = new OperationBindingContext { MetadataProvider = metadataProvider, ModelBinder = binder, ValidatorProvider = validatorProvider } }; return bindingContext; } private static CompositeModelBinder CreateBinderWithDefaults() { var binders = new IModelBinder[] { new ByteArrayModelBinder(), new GenericModelBinder(), new SimpleTypeModelBinder(), new MutableObjectModelBinder() }; var binder = new CompositeModelBinder(binders); return binder; } private static CompositeModelBinder CreateCompositeBinder(IModelBinder mockIntBinder) { var shimBinder = new CompositeModelBinder(new[] { mockIntBinder }); return shimBinder; } private static IModelValidatorProvider GetValidatorProvider(params IModelValidator[] validators) { var provider = new Mock(); provider .Setup(v => v.GetValidators(It.IsAny())) .Callback(c => { if (validators == null) { return; } foreach (var validator in validators) { c.Validators.Add(validator); } }); return provider.Object; } private class SimplePropertiesModel { public string FirstName { get; set; } public string LastName { get; set; } } private sealed class Person { public string FirstName { get; set; } public string LastName { get; set; } public int Age { get; set; } public List Friends { get; set; } public byte[] Resume { get; set; } } private class User : IValidatableObject { public string Password { get; set; } [Compare("Password")] public string ConfirmPassword { get; set; } public IEnumerable Validate(ValidationContext validationContext) { if (Password == "password") { yield return new ValidationResult("Password does not meet complexity requirements."); } } } } } #endif