// Copyright (c) Microsoft Corporation. All rights reserved. See License.txt in the project root for license information. using System.Collections.Generic; using System.Globalization; using System.Web.Http.Controllers; using System.Web.Http.Metadata.Providers; using System.Web.Http.ModelBinding.Binders; using System.Web.Http.ValueProviders; using Moq; using Xunit; namespace System.Web.Http.ModelBinding { public class CompositeModelBinderTest { //// REVIEW: remove or activate when PropertyFilter is activated ////[Fact] ////public void BindModel_PropertyFilterIsSet_Throws() ////{ //// // Arrange //// HttpExecutionContext executionContext = GetHttpExecutionContext(); //// ModelBindingContext bindingContext = new ModelBindingContext //// { //// FallbackToEmptyPrefix = true, //// ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(null, typeof(SimpleModel)), //// //PropertyFilter = (new BindAttribute { Include = "FirstName " }).IsPropertyAllowed //// }; //// List binderProviders = new List(); //// CompositeModelBinder shimBinder = new CompositeModelBinder(binderProviders); //// // Act & assert //// Assert.Throws( //// delegate { shimBinder.BindModel(executionContext, bindingContext); }, //// @"The new model binding system cannot be used when a property allow list or disallow list has been specified in [Bind] or via the call to UpdateModel() / TryUpdateModel(). Use the [BindRequired] and [BindNever] attributes on the model type or its properties instead."); ////} [Fact] public void BindModel_SuccessfulBind_RunsValidationAndReturnsModel() { // Arrange HttpActionContext actionContext = ContextUtil.CreateActionContext(GetHttpControllerContext()); bool validationCalled = false; ModelBindingContext bindingContext = new ModelBindingContext { FallbackToEmptyPrefix = true, ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(null, typeof(int)), ModelName = "someName", //ModelState = executionContext.Controller.ViewData.ModelState, //PropertyFilter = _ => true, ValueProvider = new SimpleValueProvider { { "someName", "dummyValue" } } }; Mock mockIntBinder = new Mock(); mockIntBinder .Setup(o => o.BindModel(actionContext, It.IsAny())) .Returns( delegate(HttpActionContext cc, ModelBindingContext mbc) { Assert.Same(bindingContext.ModelMetadata, mbc.ModelMetadata); Assert.Equal("someName", mbc.ModelName); Assert.Same(bindingContext.ValueProvider, mbc.ValueProvider); mbc.Model = 42; mbc.ValidationNode.Validating += delegate { validationCalled = true; }; return true; }); Mock mockBinderProvider = new Mock(); mockBinderProvider.Setup(o => o.GetBinder(actionContext, It.IsAny())).Returns((IModelBinder)mockIntBinder.Object).Verifiable(); List binderProviders = new List() { mockBinderProvider.Object }; //binderProviders.RegisterBinderForType(typeof(int), mockIntBinder.Object, false /* suppressPrefixCheck */); CompositeModelBinder shimBinder = new CompositeModelBinder(binderProviders); // Act bool isBound = shimBinder.BindModel(actionContext, bindingContext); // Assert Assert.True(isBound); Assert.Equal(42, bindingContext.Model); Assert.True(validationCalled); Assert.True(bindingContext.ModelState.IsValid); } [Fact] public void BindModel_SuccessfulBind_ComplexTypeFallback_RunsValidationAndReturnsModel() { // Arrange HttpActionContext actionContext = ContextUtil.CreateActionContext(GetHttpControllerContext()); bool validationCalled = false; List expectedModel = new List { 1, 2, 3, 4, 5 }; ModelBindingContext bindingContext = new ModelBindingContext { FallbackToEmptyPrefix = true, ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(null, typeof(List)), ModelName = "someName", //ModelState = executionContext.Controller.ViewData.ModelState, //PropertyFilter = _ => true, ValueProvider = new SimpleValueProvider { { "someOtherName", "dummyValue" } } }; Mock mockIntBinder = new Mock(); mockIntBinder .Setup(o => o.BindModel(actionContext, It.IsAny())) .Returns( delegate(HttpActionContext cc, ModelBindingContext mbc) { Assert.Same(bindingContext.ModelMetadata, mbc.ModelMetadata); Assert.Equal("", mbc.ModelName); Assert.Same(bindingContext.ValueProvider, mbc.ValueProvider); mbc.Model = expectedModel; mbc.ValidationNode.Validating += delegate { validationCalled = true; }; return true; }); List binderProviders = new List() { new SimpleModelBinderProvider() { Binder = mockIntBinder.Object, OnlyWithEmptyModelName = true } }; //binderProviders.RegisterBinderForType(typeof(List), mockIntBinder.Object, false /* suppressPrefixCheck */); CompositeModelBinder shimBinder = new CompositeModelBinder(binderProviders); // Act bool isBound = shimBinder.BindModel(actionContext, bindingContext); // Assert Assert.True(isBound); Assert.Equal(expectedModel, bindingContext.Model); Assert.True(validationCalled); Assert.True(bindingContext.ModelState.IsValid); } [Fact] public void BindModel_UnsuccessfulBind_BinderFails_ReturnsNull() { // Arrange HttpActionContext actionContext = ContextUtil.CreateActionContext(GetHttpControllerContext()); Mock mockListBinder = new Mock(); mockListBinder.Setup(o => o.BindModel(actionContext, It.IsAny())).Returns(false).Verifiable(); Mock mockBinderProvider = new Mock(); mockBinderProvider.Setup(o => o.GetBinder(actionContext, It.IsAny())).Returns((IModelBinder)mockListBinder.Object).Verifiable(); List binderProviders = new List() { mockBinderProvider.Object }; CompositeModelBinder shimBinder = new CompositeModelBinder(binderProviders); ModelBindingContext bindingContext = new ModelBindingContext { FallbackToEmptyPrefix = false, ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(null, typeof(List)), //ModelState = executionContext.Controller.ViewData.ModelState }; // Act bool isBound = shimBinder.BindModel(actionContext, bindingContext); // Assert Assert.False(isBound); Assert.Null(bindingContext.Model); Assert.True(bindingContext.ModelState.IsValid); mockListBinder.Verify(); } [Fact] public void BindModel_UnsuccessfulBind_SimpleTypeNoFallback_ReturnsNull() { // Arrange HttpActionContext actionContext = ContextUtil.CreateActionContext(GetHttpControllerContext()); Mock mockBinderProvider = new Mock(); mockBinderProvider.Setup(o => o.GetBinder(actionContext, It.IsAny())).Returns((IModelBinder)null).Verifiable(); List binderProviders = new List() { mockBinderProvider.Object }; CompositeModelBinder shimBinder = new CompositeModelBinder(binderProviders); ModelBindingContext bindingContext = new ModelBindingContext { FallbackToEmptyPrefix = true, ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(null, typeof(int)), //ModelState = executionContext.Controller.ViewData.ModelState }; // Act bool isBound = shimBinder.BindModel(actionContext, bindingContext); // Assert Assert.False(isBound); Assert.Null(bindingContext.Model); Assert.True(bindingContext.ModelState.IsValid); mockBinderProvider.Verify(); mockBinderProvider.Verify(o => o.GetBinder(actionContext, It.IsAny()), Times.AtMostOnce()); } private static HttpControllerContext GetHttpControllerContext() { return ContextUtil.CreateControllerContext(); } private class SimpleController : ApiController { } private class SimpleModel { public string FirstName { get; set; } public string LastName { get; set; } } private class SimpleModelBinderProvider : ModelBinderProvider { public IModelBinder Binder { get; set; } public bool OnlyWithEmptyModelName { get; set; } public override IModelBinder GetBinder(HttpActionContext actionContext, ModelBindingContext bindingContext) { if (OnlyWithEmptyModelName && !String.IsNullOrEmpty(bindingContext.ModelName)) { return null; } return Binder; } } private class SimpleValueProvider : Dictionary, IValueProvider { private readonly CultureInfo _culture; public SimpleValueProvider() : this(null) { } public SimpleValueProvider(CultureInfo culture) : base(StringComparer.OrdinalIgnoreCase) { _culture = culture ?? CultureInfo.InvariantCulture; } // copied from ValueProviderUtil public bool ContainsPrefix(string prefix) { foreach (string key in Keys) { if (key != null) { if (prefix.Length == 0) { return true; // shortcut - non-null key matches empty prefix } if (key.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) { if (key.Length == prefix.Length) { return true; // exact match } else { switch (key[prefix.Length]) { case '.': // known separator characters case '[': return true; } } } } } return false; // nothing found } public ValueProviderResult GetValue(string key) { object rawValue; if (TryGetValue(key, out rawValue)) { return new ValueProviderResult(rawValue, Convert.ToString(rawValue, _culture), _culture); } else { // value not found return null; } } } } }