// 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.Collections; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; using Moq; using Xunit; namespace Microsoft.AspNet.Mvc.ModelBinding.Metadata { public class DefaultModelMetadataTest { [Fact] public void DefaultValues() { // Arrange var provider = new EmptyModelMetadataProvider(); var detailsProvider = new DefaultCompositeMetadataDetailsProvider( Enumerable.Empty()); var key = ModelMetadataIdentity.ForType(typeof(string)); var cache = new DefaultMetadataDetails(key, new ModelAttributes(new object[0])); // Act var metadata = new DefaultModelMetadata(provider, detailsProvider, cache); // Assert Assert.NotNull(metadata.AdditionalValues); Assert.Empty(metadata.AdditionalValues); Assert.Equal(typeof(string), metadata.ModelType); Assert.True(metadata.ConvertEmptyStringToNull); Assert.False(metadata.HasNonDefaultEditFormat); Assert.False(metadata.HideSurroundingHtml); Assert.True(metadata.HtmlEncode); Assert.True(metadata.IsBindingAllowed); Assert.False(metadata.IsBindingRequired); Assert.False(metadata.IsCollectionType); Assert.False(metadata.IsComplexType); Assert.False(metadata.IsEnumerableType); Assert.False(metadata.IsEnum); Assert.False(metadata.IsFlagsEnum); Assert.False(metadata.IsNullableValueType); Assert.False(metadata.IsReadOnly); Assert.False(metadata.IsRequired); // Defaults to false for reference types Assert.True(metadata.ShowForDisplay); Assert.True(metadata.ShowForEdit); Assert.Null(metadata.DataTypeName); Assert.Null(metadata.Description); Assert.Null(metadata.DisplayFormatString); Assert.Null(metadata.DisplayName); Assert.Null(metadata.EditFormatString); Assert.Null(metadata.ElementMetadata); Assert.Null(metadata.EnumGroupedDisplayNamesAndValues); Assert.Null(metadata.EnumNamesAndValues); Assert.Null(metadata.NullDisplayText); Assert.Null(metadata.TemplateHint); Assert.Null(metadata.SimpleDisplayProperty); Assert.Equal(10000, ModelMetadata.DefaultOrder); Assert.Equal(ModelMetadata.DefaultOrder, metadata.Order); Assert.Null(metadata.BinderModelName); Assert.Null(metadata.BinderType); Assert.Null(metadata.BindingSource); Assert.Null(metadata.PropertyBindingPredicateProvider); } [Fact] public void CreateMetadataForType() { // Arrange var provider = new EmptyModelMetadataProvider(); var detailsProvider = new DefaultCompositeMetadataDetailsProvider( Enumerable.Empty()); var key = ModelMetadataIdentity.ForType(typeof(Exception)); var cache = new DefaultMetadataDetails(key, new ModelAttributes(new object[0])); // Act var metadata = new DefaultModelMetadata(provider, detailsProvider, cache); // Assert Assert.Equal(typeof(Exception), metadata.ModelType); Assert.Null(metadata.PropertyName); Assert.Null(metadata.ContainerType); } [Fact] public void CreateMetadataForProperty() { // Arrange var provider = new EmptyModelMetadataProvider(); var detailsProvider = new EmptyCompositeMetadataDetailsProvider(); var key = ModelMetadataIdentity.ForProperty(typeof(string), "Message", typeof(Exception)); var cache = new DefaultMetadataDetails(key, new ModelAttributes(new object[0], new object[0])); // Act var metadata = new DefaultModelMetadata(provider, detailsProvider, cache); // Assert Assert.Equal(typeof(string), metadata.ModelType); Assert.Equal("Message", metadata.PropertyName); Assert.Equal(typeof(Exception), metadata.ContainerType); } [Theory] [InlineData(typeof(object))] [InlineData(typeof(int))] [InlineData(typeof(NonCollectionType))] [InlineData(typeof(string))] public void ElementMetadata_ReturnsNull_ForNonCollections(Type modelType) { // Arrange var provider = new EmptyModelMetadataProvider(); var detailsProvider = new EmptyCompositeMetadataDetailsProvider(); var key = ModelMetadataIdentity.ForType(modelType); var cache = new DefaultMetadataDetails(key, new ModelAttributes(new object[0])); var metadata = new DefaultModelMetadata(provider, detailsProvider, cache); // Act var elementMetadata = metadata.ElementMetadata; // Assert Assert.Null(elementMetadata); } [Theory] [InlineData(typeof(int[]), typeof(int))] [InlineData(typeof(List), typeof(string))] [InlineData(typeof(DerivedList), typeof(int))] [InlineData(typeof(IEnumerable), typeof(object))] [InlineData(typeof(IEnumerable), typeof(string))] [InlineData(typeof(Collection), typeof(int))] [InlineData(typeof(Dictionary), typeof(KeyValuePair))] [InlineData(typeof(DerivedDictionary), typeof(KeyValuePair))] public void ElementMetadata_ReturnsExpectedMetadata(Type modelType, Type elementType) { // Arrange var provider = new EmptyModelMetadataProvider(); var detailsProvider = new EmptyCompositeMetadataDetailsProvider(); var key = ModelMetadataIdentity.ForType(modelType); var cache = new DefaultMetadataDetails(key, new ModelAttributes(new object[0])); var metadata = new DefaultModelMetadata(provider, detailsProvider, cache); // Act var elementMetadata = metadata.ElementMetadata; // Assert Assert.NotNull(elementMetadata); Assert.Equal(elementType, elementMetadata.ModelType); } private class NonCollectionType { } private class DerivedList : List { } private class DerivedDictionary : Dictionary { } [Theory] [InlineData(typeof(string))] [InlineData(typeof(int))] public void IsBindingAllowed_ReturnsTrue_ForTypes(Type modelType) { // Arrange var provider = new EmptyModelMetadataProvider(); var detailsProvider = new EmptyCompositeMetadataDetailsProvider(); var key = ModelMetadataIdentity.ForType(modelType); var cache = new DefaultMetadataDetails(key, new ModelAttributes(new object[0])); cache.BindingMetadata = new BindingMetadata() { IsBindingAllowed = false, // Will be ignored. }; var metadata = new DefaultModelMetadata(provider, detailsProvider, cache); // Act var isBindingAllowed = metadata.IsBindingAllowed; // Assert Assert.True(isBindingAllowed); } [Theory] [InlineData(typeof(string))] [InlineData(typeof(int))] public void IsBindingRequired_ReturnsFalse_ForTypes(Type modelType) { // Arrange var provider = new EmptyModelMetadataProvider(); var detailsProvider = new EmptyCompositeMetadataDetailsProvider(); var key = ModelMetadataIdentity.ForType(modelType); var cache = new DefaultMetadataDetails(key, new ModelAttributes(new object[0])); cache.BindingMetadata = new BindingMetadata() { IsBindingRequired = true, // Will be ignored. }; var metadata = new DefaultModelMetadata(provider, detailsProvider, cache); // Act var isBindingRequired = metadata.IsBindingRequired; // Assert Assert.False(isBindingRequired); } [Theory] [InlineData(typeof(string))] [InlineData(typeof(IDisposable))] [InlineData(typeof(Nullable))] public void IsRequired_ReturnsFalse_ForNullableTypes(Type modelType) { // Arrange var provider = new EmptyModelMetadataProvider(); var detailsProvider = new EmptyCompositeMetadataDetailsProvider(); var key = ModelMetadataIdentity.ForType(modelType); var cache = new DefaultMetadataDetails(key, new ModelAttributes(new object[0])); var metadata = new DefaultModelMetadata(provider, detailsProvider, cache); // Act var isRequired = metadata.IsRequired; // Assert Assert.False(isRequired); } [Theory] [InlineData(typeof(int))] [InlineData(typeof(DayOfWeek))] public void IsRequired_ReturnsTrue_ForNonNullableTypes(Type modelType) { // Arrange var provider = new EmptyModelMetadataProvider(); var detailsProvider = new EmptyCompositeMetadataDetailsProvider(); var key = ModelMetadataIdentity.ForType(modelType); var cache = new DefaultMetadataDetails(key, new ModelAttributes(new object[0])); var metadata = new DefaultModelMetadata(provider, detailsProvider, cache); // Act var isRequired = metadata.IsRequired; // Assert Assert.True(isRequired); } [Fact] public void PropertiesProperty_CallsProvider() { // Arrange var provider = new Mock(MockBehavior.Strict); var detailsProvider = new EmptyCompositeMetadataDetailsProvider(); var expectedProperties = new DefaultModelMetadata[] { new DefaultModelMetadata( provider.Object, detailsProvider, new DefaultMetadataDetails( ModelMetadataIdentity.ForProperty(typeof(int), "Prop1", typeof(string)), attributes: new ModelAttributes(new object[0], new object[0]))), new DefaultModelMetadata( provider.Object, detailsProvider, new DefaultMetadataDetails( ModelMetadataIdentity.ForProperty(typeof(int), "Prop2", typeof(string)), attributes: new ModelAttributes(new object[0], new object[0]))), }; provider .Setup(p => p.GetMetadataForProperties(typeof(string))) .Returns(expectedProperties); var key = ModelMetadataIdentity.ForType(typeof(string)); var cache = new DefaultMetadataDetails(key, new ModelAttributes(new object[0])); var metadata = new DefaultModelMetadata(provider.Object, detailsProvider, cache); // Act var properties = metadata.Properties; // Assert Assert.Equal(expectedProperties.Length, properties.Count); for (var i = 0; i < expectedProperties.Length; i++) { Assert.Same(expectedProperties[i], properties[i]); } } // Input (original) property names and expected (ordered) property names. public static TheoryData, IEnumerable> PropertyNamesTheoryData { get { // ModelMetadata does not reorder properties the provider returns without an Order override. return new TheoryData, IEnumerable> { { new List { "Property1", "Property2", "Property3", "Property4", }, new List { "Property1", "Property2", "Property3", "Property4", } }, { new List { "Property4", "Property3", "Property2", "Property1", }, new List { "Property4", "Property3", "Property2", "Property1", } }, { new List { "Delta", "Bravo", "Charlie", "Alpha", }, new List { "Delta", "Bravo", "Charlie", "Alpha", } }, { new List { "John", "Jonathan", "Jon", "Joan", }, new List { "John", "Jonathan", "Jon", "Joan", } }, }; } } [Theory] [MemberData(nameof(PropertyNamesTheoryData))] public void PropertiesProperty_WithDefaultOrder_OrdersPropertyNamesAsProvided( IEnumerable originalNames, IEnumerable expectedNames) { // Arrange var provider = new Mock(MockBehavior.Strict); var detailsProvider = new EmptyCompositeMetadataDetailsProvider(); var expectedProperties = new List(); foreach (var originalName in originalNames) { expectedProperties.Add(new DefaultModelMetadata( provider.Object, detailsProvider, new DefaultMetadataDetails( ModelMetadataIdentity.ForProperty(typeof(int), originalName, typeof(string)), attributes: new ModelAttributes(new object[0], new object[0])))); } provider .Setup(p => p.GetMetadataForProperties(typeof(string))) .Returns(expectedProperties); var key = ModelMetadataIdentity.ForType(typeof(string)); var cache = new DefaultMetadataDetails(key, new ModelAttributes(new object[0])); var metadata = new DefaultModelMetadata(provider.Object, detailsProvider, cache); // Act var properties = metadata.Properties; // Assert Assert.Equal(expectedNames.Count(), properties.Count); Assert.Equal(expectedNames.ToArray(), properties.Select(p => p.PropertyName).ToArray()); } // Input (original) property names, Order values, and expected (ordered) property names. public static TheoryData>, IEnumerable> PropertyNamesAndOrdersTheoryData { get { return new TheoryData>, IEnumerable> { { new List> { new KeyValuePair("Property1", 23), new KeyValuePair("Property2", 23), new KeyValuePair("Property3", 23), new KeyValuePair("Property4", 23), }, new List { "Property1", "Property2", "Property3", "Property4", } }, // Same order if already ordered using Order. { new List> { new KeyValuePair("Property4", 23), new KeyValuePair("Property3", 24), new KeyValuePair("Property2", 25), new KeyValuePair("Property1", 26), }, new List { "Property4", "Property3", "Property2", "Property1", } }, // Rest of the orderings get updated within ModelMetadata. { new List> { new KeyValuePair("Property1", 26), new KeyValuePair("Property2", 25), new KeyValuePair("Property3", 24), new KeyValuePair("Property4", 23), }, new List { "Property4", "Property3", "Property2", "Property1", } }, { new List> { new KeyValuePair("Alpha", 26), new KeyValuePair("Bravo", 24), new KeyValuePair("Charlie", 23), new KeyValuePair("Delta", 25), }, new List { "Charlie", "Bravo", "Delta", "Alpha", } }, // Jonathan and Jon will not be reordered. { new List> { new KeyValuePair("Joan", 1), new KeyValuePair("Jonathan", 0), new KeyValuePair("Jon", 0), new KeyValuePair("John", -1), }, new List { "John", "Jonathan", "Jon", "Joan", } }, }; } } [Theory] [MemberData(nameof(PropertyNamesAndOrdersTheoryData))] public void PropertiesProperty_OrdersPropertyNamesUsingOrder_ThenAsProvided( IEnumerable> originalNamesAndOrders, IEnumerable expectedNames) { // Arrange var provider = new Mock(MockBehavior.Strict); var detailsProvider = new EmptyCompositeMetadataDetailsProvider(); var expectedProperties = new List(); foreach (var kvp in originalNamesAndOrders) { var propertyCache = new DefaultMetadataDetails( ModelMetadataIdentity.ForProperty(typeof(int), kvp.Key, typeof(string)), attributes: new ModelAttributes(new object[0], new object[0])); propertyCache.DisplayMetadata = new DisplayMetadata(); propertyCache.DisplayMetadata.Order = kvp.Value; expectedProperties.Add(new DefaultModelMetadata( provider.Object, detailsProvider, propertyCache)); } provider .Setup(p => p.GetMetadataForProperties(typeof(string))) .Returns(expectedProperties); var key = ModelMetadataIdentity.ForType(typeof(string)); var cache = new DefaultMetadataDetails(key, new ModelAttributes(new object[0])); var metadata = new DefaultModelMetadata(provider.Object, detailsProvider, cache); // Act var properties = metadata.Properties; // Assert Assert.Equal(expectedNames.Count(), properties.Count); Assert.Equal(expectedNames.ToArray(), properties.Select(p => p.PropertyName).ToArray()); } [Fact] public void PropertiesSetOnce() { // Arrange var provider = new EmptyModelMetadataProvider(); var detailsProvider = new EmptyCompositeMetadataDetailsProvider(); var key = ModelMetadataIdentity.ForType(typeof(string)); var cache = new DefaultMetadataDetails(key, new ModelAttributes(new object[0])); var metadata = new DefaultModelMetadata(provider, detailsProvider, cache); // Act var firstPropertiesEvaluation = metadata.Properties; var secondPropertiesEvaluation = metadata.Properties; // Assert // Same IEnumerable object. Assert.Same(firstPropertiesEvaluation, secondPropertiesEvaluation); } [Fact] public void PropertiesEnumerationEvaluatedOnce() { // Arrange var provider = new EmptyModelMetadataProvider(); var detailsProvider = new EmptyCompositeMetadataDetailsProvider(); var key = ModelMetadataIdentity.ForType(typeof(string)); var cache = new DefaultMetadataDetails(key, new ModelAttributes(new object[0])); var metadata = new DefaultModelMetadata(provider, detailsProvider, cache); // Act var firstPropertiesEvaluation = metadata.Properties.ToList(); var secondPropertiesEvaluation = metadata.Properties.ToList(); // Assert // Identical ModelMetadata objects every time we run through the Properties collection. Assert.Equal(firstPropertiesEvaluation, secondPropertiesEvaluation); } [Fact] public void IsReadOnly_ReturnsFalse_ForType() { // Arrange var detailsProvider = new EmptyCompositeMetadataDetailsProvider(); var provider = new DefaultModelMetadataProvider(detailsProvider); var key = ModelMetadataIdentity.ForType(typeof(int[])); var cache = new DefaultMetadataDetails(key, new ModelAttributes(new object[0])); cache.BindingMetadata = new BindingMetadata() { IsReadOnly = true, // Will be ignored. }; var metadata = new DefaultModelMetadata(provider, detailsProvider, cache); // Act var isReadOnly = metadata.IsReadOnly; // Assert Assert.False(isReadOnly); } [Fact] public void IsReadOnly_ReturnsTrue_ForPrivateSetProperty() { // Arrange var detailsProvider = new EmptyCompositeMetadataDetailsProvider(); var provider = new DefaultModelMetadataProvider(detailsProvider); var key = ModelMetadataIdentity.ForType(typeof(TypeWithProperties)); var cache = new DefaultMetadataDetails(key, new ModelAttributes(new object[0])); var metadata = new DefaultModelMetadata(provider, detailsProvider, cache); // Act var isReadOnly = metadata.Properties["PublicGetPrivateSetProperty"].IsReadOnly; // Assert Assert.True(isReadOnly); } [Fact] public void IsReadOnly_ReturnsTrue_ForProtectedSetProperty() { // Arrange var detailsProvider = new EmptyCompositeMetadataDetailsProvider(); var provider = new DefaultModelMetadataProvider(detailsProvider); var key = ModelMetadataIdentity.ForType(typeof(TypeWithProperties)); var cache = new DefaultMetadataDetails(key, new ModelAttributes(new object[0])); var metadata = new DefaultModelMetadata(provider, detailsProvider, cache); // Act var isReadOnly = metadata.Properties["PublicGetProtectedSetProperty"].IsReadOnly; // Assert Assert.True(isReadOnly); } [Fact] public void IsReadOnly_ReturnsFalse_ForPublicSetProperty() { // Arrange var detailsProvider = new EmptyCompositeMetadataDetailsProvider(); var provider = new DefaultModelMetadataProvider(detailsProvider); var key = ModelMetadataIdentity.ForType(typeof(TypeWithProperties)); var cache = new DefaultMetadataDetails(key, new ModelAttributes(new object[0])); var metadata = new DefaultModelMetadata(provider, detailsProvider, cache); // Act var isReadOnly = metadata.Properties["PublicGetPublicSetProperty"].IsReadOnly; // Assert Assert.False(isReadOnly); } private void ActionMethod(string input) { } private class TypeWithProperties { public string PublicGetPrivateSetProperty { get; private set; } public int PublicGetProtectedSetProperty { get; protected set; } public int PublicGetPublicSetProperty { get; set; } } } }