// 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.Generic; using System.Globalization; using System.IO; using System.Linq; using Microsoft.AspNet.DataProtection; using Microsoft.AspNet.Mvc.ModelBinding; using Microsoft.Framework.OptionsModel; using Microsoft.Framework.WebEncoders; using Moq; using Xunit; namespace Microsoft.AspNet.Mvc.Rendering { public class DefaultHtmlGeneratorTest { [Theory] [InlineData(false)] [InlineData(true)] public void GetCurrentValues_WithEmptyViewData_ReturnsNull(bool allowMultiple) { // Arrange var metadataProvider = new TestModelMetadataProvider(); var htmlGenerator = GetGenerator(metadataProvider); var viewContext = GetViewContext(model: null, metadataProvider: metadataProvider); // Act var result = htmlGenerator.GetCurrentValues( viewContext, modelExplorer: null, expression: nameof(Model.Name), allowMultiple: allowMultiple); // Assert Assert.Null(result); } [Theory] [InlineData(false)] [InlineData(true)] public void GetCurrentValues_WithNullExpressionResult_ReturnsNull(bool allowMultiple) { // Arrange var metadataProvider = new TestModelMetadataProvider(); var htmlGenerator = GetGenerator(metadataProvider); var viewContext = GetViewContext(model: null, metadataProvider: metadataProvider); var modelExplorer = metadataProvider.GetModelExplorerForType(typeof(string), model: null); // Act var result = htmlGenerator.GetCurrentValues( viewContext, modelExplorer, expression: nameof(Model.Name), allowMultiple: allowMultiple); // Assert Assert.Null(result); } [Theory] [InlineData(false)] [InlineData(true)] public void GetCurrentValues_WithSelectListInViewData_ReturnsNull(bool allowMultiple) { // Arrange var metadataProvider = new TestModelMetadataProvider(); var htmlGenerator = GetGenerator(metadataProvider); var viewContext = GetViewContext(model: null, metadataProvider: metadataProvider); viewContext.ViewData[nameof(Model.Name)] = Enumerable.Empty(); // Act var result = htmlGenerator.GetCurrentValues( viewContext, modelExplorer: null, expression: nameof(Model.Name), allowMultiple: allowMultiple); // Assert Assert.Null(result); } [Theory] [InlineData("some string")] // treated as if it were not IEnumerable [InlineData(23)] [InlineData(RegularEnum.Three)] public void GetCurrentValues_AllowMultipleWithNonEnumerableInViewData_Throws(object value) { // Arrange var metadataProvider = new TestModelMetadataProvider(); var htmlGenerator = GetGenerator(metadataProvider); var viewContext = GetViewContext(model: null, metadataProvider: metadataProvider); viewContext.ViewData[nameof(Model.Name)] = value; // Act & Assert var exception = Assert.Throws(() => htmlGenerator.GetCurrentValues( viewContext, modelExplorer: null, expression: nameof(Model.Name), allowMultiple: true)); Assert.Equal( "The parameter 'expression' must evaluate to an IEnumerable when multiple selection is allowed.", exception.Message); } // rawValue, allowMultiple -> expected current values public static TheoryData> GetCurrentValues_StringAndCollectionData { get { return new TheoryData> { // ModelStateDictionary converts single values to arrays and visa-versa. { string.Empty, false, new [] { string.Empty } }, { string.Empty, true, new [] { string.Empty } }, { "some string", false, new [] { "some string" } }, { "some string", true, new [] { "some string" } }, { new [] { "some string" }, false, new [] { "some string" } }, { new [] { "some string" }, true, new [] { "some string" } }, { new [] { "some string", "some other string" }, false, new [] { "some string" } }, { new [] { "some string", "some other string" }, true, new [] { "some string", "some other string" } }, // { new string[] { null }, false, null } would fall back to other sources. { new string[] { null }, true, new [] { string.Empty } }, { new [] { string.Empty }, false, new [] { string.Empty } }, { new [] { string.Empty }, true, new [] { string.Empty } }, { new [] { null, "some string", "some other string" }, true, new [] { string.Empty, "some string", "some other string" } }, // ignores duplicates { new [] { null, "some string", null, "some other string", null, "some string", null }, true, new [] { string.Empty, "some string", "some other string" } }, // ignores case of duplicates { new [] { "some string", "SoMe StriNg", "Some String", "soME STRing", "SOME STRING" }, true, new [] { "some string" } }, }; } } [Theory] [MemberData(nameof(GetCurrentValues_StringAndCollectionData))] public void GetCurrentValues_WithModelStateEntryAndViewData_ReturnsModelStateEntry( object rawValue, bool allowMultiple, IReadOnlyCollection expected) { // Arrange var metadataProvider = new TestModelMetadataProvider(); var htmlGenerator = GetGenerator(metadataProvider); var model = new Model { Name = "ignored property value" }; var viewContext = GetViewContext(model, metadataProvider); viewContext.ViewData[nameof(Model.Name)] = "ignored ViewData value"; var valueProviderResult = new ValueProviderResult(rawValue); viewContext.ModelState.SetModelValue(nameof(Model.Name), valueProviderResult); // Act var result = htmlGenerator.GetCurrentValues( viewContext, modelExplorer: null, expression: nameof(Model.Name), allowMultiple: allowMultiple); // Assert Assert.NotNull(result); Assert.Equal(expected, result); } [Theory] [MemberData(nameof(GetCurrentValues_StringAndCollectionData))] public void GetCurrentValues_WithModelStateEntryModelExplorerAndViewData_ReturnsModelStateEntry( object rawValue, bool allowMultiple, IReadOnlyCollection expected) { // Arrange var metadataProvider = new TestModelMetadataProvider(); var htmlGenerator = GetGenerator(metadataProvider); var model = new Model { Name = "ignored property value" }; var viewContext = GetViewContext(model, metadataProvider); viewContext.ViewData[nameof(Model.Name)] = "ignored ViewData value"; var modelExplorer = metadataProvider.GetModelExplorerForType(typeof(string), "ignored model value"); var valueProviderResult = new ValueProviderResult(rawValue); viewContext.ModelState.SetModelValue(nameof(Model.Name), valueProviderResult); // Act var result = htmlGenerator.GetCurrentValues( viewContext, modelExplorer, expression: nameof(Model.Name), allowMultiple: allowMultiple); // Assert Assert.NotNull(result); Assert.Equal(expected, result); } // rawValue -> expected current values public static TheoryData GetCurrentValues_StringData { get { return new TheoryData { // 1. If given a ModelExplorer, GetCurrentValues does not use ViewData even if expression result is // null. // 2. Otherwise if ViewData entry exists, GetCurrentValue does not fall back to ViewData.Model even // if entry is null. // 3. Otherwise, GetCurrentValue does not fall back anywhere else even if ViewData.Model is null. { null, null }, { string.Empty, new [] { string.Empty } }, { "some string", new [] { "some string" } }, }; } } [Theory] [MemberData(nameof(GetCurrentValues_StringData))] public void GetCurrentValues_WithModelExplorerAndViewData_ReturnsExpressionResult( string rawValue, IReadOnlyCollection expected) { // Arrange var metadataProvider = new TestModelMetadataProvider(); var htmlGenerator = GetGenerator(metadataProvider); var model = new Model { Name = "ignored property value" }; var viewContext = GetViewContext(model, metadataProvider); viewContext.ViewData[nameof(Model.Name)] = "ignored ViewData value"; var modelExplorer = metadataProvider.GetModelExplorerForType(typeof(string), rawValue); var valueProviderResult = new ValueProviderResult(rawValue: null); viewContext.ModelState.SetModelValue(nameof(Model.Name), valueProviderResult); // Act var result = htmlGenerator.GetCurrentValues( viewContext, modelExplorer, expression: nameof(Model.Name), allowMultiple: false); // Assert Assert.Equal(expected, result); } [Theory] [MemberData(nameof(GetCurrentValues_StringData))] public void GetCurrentValues_WithViewData_ReturnsViewDataEntry( object rawValue, IReadOnlyCollection expected) { // Arrange var metadataProvider = new TestModelMetadataProvider(); var htmlGenerator = GetGenerator(metadataProvider); var model = new Model { Name = "ignored property value" }; var viewContext = GetViewContext(model, metadataProvider); viewContext.ViewData[nameof(Model.Name)] = rawValue; var valueProviderResult = new ValueProviderResult(rawValue: null); viewContext.ModelState.SetModelValue(nameof(Model.Name), valueProviderResult); // Act var result = htmlGenerator.GetCurrentValues( viewContext, modelExplorer: null, expression: nameof(Model.Name), allowMultiple: false); // Assert Assert.Equal(expected, result); } [Theory] [MemberData(nameof(GetCurrentValues_StringData))] public void GetCurrentValues_WithModel_ReturnsModel(string rawValue, IReadOnlyCollection expected) { // Arrange var metadataProvider = new TestModelMetadataProvider(); var htmlGenerator = GetGenerator(metadataProvider); var model = new Model { Name = rawValue }; var viewContext = GetViewContext(model, metadataProvider); var valueProviderResult = new ValueProviderResult(rawValue: null); viewContext.ModelState.SetModelValue(nameof(Model.Name), valueProviderResult); // Act var result = htmlGenerator.GetCurrentValues( viewContext, modelExplorer: null, expression: nameof(Model.Name), allowMultiple: false); // Assert Assert.Equal(expected, result); } // rawValue -> expected current values public static TheoryData GetCurrentValues_StringCollectionData { get { return new TheoryData { { new string[] { null }, new [] { string.Empty } }, { new [] { string.Empty }, new [] { string.Empty } }, { new [] { "some string" }, new [] { "some string" } }, { new [] { "some string", "some other string" }, new [] { "some string", "some other string" } }, { new [] { null, "some string", "some other string" }, new [] { string.Empty, "some string", "some other string" } }, // ignores duplicates { new [] { null, "some string", null, "some other string", null, "some string", null }, new [] { string.Empty, "some string", "some other string" } }, // ignores case of duplicates { new [] { "some string", "SoMe StriNg", "Some String", "soME STRing", "SOME STRING" }, new [] { "some string" } }, }; } } [Theory] [MemberData(nameof(GetCurrentValues_StringCollectionData))] public void GetCurrentValues_CollectionWithModelExplorerAndViewData_ReturnsExpressionResult( string[] rawValue, IReadOnlyCollection expected) { // Arrange var metadataProvider = new TestModelMetadataProvider(); var htmlGenerator = GetGenerator(metadataProvider); var model = new Model { Collection = { "ignored property value" } }; var viewContext = GetViewContext(model, metadataProvider); viewContext.ViewData[nameof(Model.Collection)] = new[] { "ignored ViewData value" }; var modelExplorer = metadataProvider.GetModelExplorerForType(typeof(List), new List(rawValue)); var valueProviderResult = new ValueProviderResult(rawValue: null); viewContext.ModelState.SetModelValue(nameof(Model.Collection), valueProviderResult); // Act var result = htmlGenerator.GetCurrentValues( viewContext, modelExplorer, expression: nameof(Model.Collection), allowMultiple: true); // Assert Assert.Equal(expected, result); } [Theory] [MemberData(nameof(GetCurrentValues_StringCollectionData))] public void GetCurrentValues_CollectionWithViewData_ReturnsViewDataEntry( object[] rawValue, IReadOnlyCollection expected) { // Arrange var metadataProvider = new TestModelMetadataProvider(); var htmlGenerator = GetGenerator(metadataProvider); var model = new Model { Collection = { "ignored property value" } }; var viewContext = GetViewContext(model, metadataProvider); viewContext.ViewData[nameof(Model.Collection)] = rawValue; var valueProviderResult = new ValueProviderResult(rawValue: null); viewContext.ModelState.SetModelValue(nameof(Model.Collection), valueProviderResult); // Act var result = htmlGenerator.GetCurrentValues( viewContext, modelExplorer: null, expression: nameof(Model.Collection), allowMultiple: true); // Assert Assert.Equal(expected, result); } [Theory] [MemberData(nameof(GetCurrentValues_StringCollectionData))] public void GetCurrentValues_CollectionWithModel_ReturnsModel( string[] rawValue, IReadOnlyCollection expected) { // Arrange var metadataProvider = new TestModelMetadataProvider(); var htmlGenerator = GetGenerator(metadataProvider); var model = new Model(); model.Collection.AddRange(rawValue); var viewContext = GetViewContext(model, metadataProvider); var valueProviderResult = new ValueProviderResult(rawValue: null); viewContext.ModelState.SetModelValue(nameof(Model.Collection), valueProviderResult); // Act var result = htmlGenerator.GetCurrentValues( viewContext, modelExplorer: null, expression: nameof(Model.Collection), allowMultiple: true); // Assert Assert.Equal(expected, result); } // property name, rawValue -> expected current values public static TheoryData GetCurrentValues_ValueToConvertData { get { return new TheoryData { { nameof(Model.FlagsEnum), FlagsEnum.All, new [] { "-1", "All" } }, { nameof(Model.FlagsEnum), FlagsEnum.FortyTwo, new [] { "42", "FortyTwo" } }, { nameof(Model.FlagsEnum), FlagsEnum.None, new [] { "0", "None" } }, { nameof(Model.FlagsEnum), FlagsEnum.Two, new [] { "2", "Two" } }, { nameof(Model.FlagsEnum), string.Empty, new [] { string.Empty } }, { nameof(Model.FlagsEnum), "All", new [] { "-1", "All" } }, { nameof(Model.FlagsEnum), "FortyTwo", new [] { "42", "FortyTwo" } }, { nameof(Model.FlagsEnum), "None", new [] { "0", "None" } }, { nameof(Model.FlagsEnum), "Two", new [] { "2", "Two" } }, { nameof(Model.FlagsEnum), "Two, Four", new [] { "Two, Four", "6" } }, { nameof(Model.FlagsEnum), "garbage", new [] { "garbage" } }, { nameof(Model.FlagsEnum), "0", new [] { "0", "None" } }, { nameof(Model.FlagsEnum), " 43", new [] { " 43", "43" } }, { nameof(Model.FlagsEnum), "-5 ", new [] { "-5 ", "-5" } }, { nameof(Model.FlagsEnum), 0, new [] { "0", "None" } }, { nameof(Model.FlagsEnum), 1, new [] { "1", "One" } }, { nameof(Model.FlagsEnum), 43, new [] { "43" } }, { nameof(Model.FlagsEnum), -5, new [] { "-5" } }, { nameof(Model.FlagsEnum), int.MaxValue, new [] { "2147483647" } }, { nameof(Model.FlagsEnum), (uint)int.MaxValue + 1, new [] { "2147483648" } }, { nameof(Model.FlagsEnum), uint.MaxValue, new [] { "4294967295" } }, // converted to string & used { nameof(Model.Id), string.Empty, new [] { string.Empty } }, { nameof(Model.Id), "garbage", new [] { "garbage" } }, // no compatibility checks { nameof(Model.Id), "0", new [] { "0" } }, { nameof(Model.Id), " 43", new [] { " 43" } }, { nameof(Model.Id), "-5 ", new [] { "-5 " } }, { nameof(Model.Id), 0, new [] { "0" } }, { nameof(Model.Id), 1, new [] { "1" } }, { nameof(Model.Id), 43, new [] { "43" } }, { nameof(Model.Id), -5, new [] { "-5" } }, { nameof(Model.Id), int.MaxValue, new [] { "2147483647" } }, { nameof(Model.Id), (uint)int.MaxValue + 1, new [] { "2147483648" } }, // no limit checks { nameof(Model.Id), uint.MaxValue, new [] { "4294967295" } }, // no limit checks { nameof(Model.NullableEnum), RegularEnum.Zero, new [] { "0", "Zero" } }, { nameof(Model.NullableEnum), RegularEnum.One, new [] { "1", "One" } }, { nameof(Model.NullableEnum), RegularEnum.Two, new [] { "2", "Two" } }, { nameof(Model.NullableEnum), RegularEnum.Three, new [] { "3", "Three" } }, { nameof(Model.NullableEnum), string.Empty, new [] { string.Empty } }, { nameof(Model.NullableEnum), "Zero", new [] { "0", "Zero" } }, { nameof(Model.NullableEnum), "Two", new [] { "2", "Two" } }, { nameof(Model.NullableEnum), "One, Two", new [] { "One, Two", "3", "Three" } }, { nameof(Model.NullableEnum), "garbage", new [] { "garbage" } }, { nameof(Model.NullableEnum), "0", new [] { "0", "Zero" } }, { nameof(Model.NullableEnum), " 43", new [] { " 43", "43" } }, { nameof(Model.NullableEnum), "-5 ", new [] { "-5 ", "-5" } }, { nameof(Model.NullableEnum), 0, new [] { "0", "Zero" } }, { nameof(Model.NullableEnum), 1, new [] { "1", "One" } }, { nameof(Model.NullableEnum), 43, new [] { "43" } }, { nameof(Model.NullableEnum), -5, new [] { "-5" } }, { nameof(Model.NullableEnum), int.MaxValue, new [] { "2147483647" } }, { nameof(Model.NullableEnum), (uint)int.MaxValue + 1, new [] { "2147483648" } }, { nameof(Model.NullableEnum), uint.MaxValue, new [] { "4294967295" } }, { nameof(Model.RegularEnum), RegularEnum.Zero, new [] { "0", "Zero" } }, { nameof(Model.RegularEnum), RegularEnum.One, new [] { "1", "One" } }, { nameof(Model.RegularEnum), RegularEnum.Two, new [] { "2", "Two" } }, { nameof(Model.RegularEnum), RegularEnum.Three, new [] { "3", "Three" } }, { nameof(Model.RegularEnum), string.Empty, new [] { string.Empty } }, { nameof(Model.RegularEnum), "Zero", new [] { "0", "Zero" } }, { nameof(Model.RegularEnum), "Two", new [] { "2", "Two" } }, { nameof(Model.RegularEnum), "One, Two", new [] { "One, Two", "3", "Three" } }, { nameof(Model.RegularEnum), "garbage", new [] { "garbage" } }, { nameof(Model.RegularEnum), "0", new [] { "0", "Zero" } }, { nameof(Model.RegularEnum), " 43", new [] { " 43", "43" } }, { nameof(Model.RegularEnum), "-5 ", new [] { "-5 ", "-5" } }, { nameof(Model.RegularEnum), 0, new [] { "0", "Zero" } }, { nameof(Model.RegularEnum), 1, new [] { "1", "One" } }, { nameof(Model.RegularEnum), 43, new [] { "43" } }, { nameof(Model.RegularEnum), -5, new [] { "-5" } }, { nameof(Model.RegularEnum), int.MaxValue, new [] { "2147483647" } }, { nameof(Model.RegularEnum), (uint)int.MaxValue + 1, new [] { "2147483648" } }, { nameof(Model.RegularEnum), uint.MaxValue, new [] { "4294967295" } }, }; } } [Theory] [MemberData(nameof(GetCurrentValues_ValueToConvertData))] public void GetCurrentValues_ValueConvertedAsExpected( string propertyName, object rawValue, IReadOnlyCollection expected) { // Arrange var metadataProvider = TestModelMetadataProvider.CreateDefaultProvider(); var htmlGenerator = GetGenerator(metadataProvider); var viewContext = GetViewContext(model: null, metadataProvider: metadataProvider); var valueProviderResult = new ValueProviderResult(rawValue); viewContext.ModelState.SetModelValue(propertyName, valueProviderResult); // Act var result = htmlGenerator.GetCurrentValues( viewContext, modelExplorer: null, expression: propertyName, allowMultiple: false); // Assert Assert.Equal(expected, result); } // GetCurrentValues uses only the IModelMetadataProvider passed to the DefaultHtmlGenerator constructor. private static IHtmlGenerator GetGenerator(IModelMetadataProvider metadataProvider) { var antiforgeryOptionsAccessor = new Mock>(); antiforgeryOptionsAccessor.SetupGet(accessor => accessor.Options).Returns(new AntiForgeryOptions()); var mvcViewOptionsAccessor = new Mock>(); mvcViewOptionsAccessor.SetupGet(accessor => accessor.Options).Returns(new MvcViewOptions()); var htmlEncoder = Mock.Of(); var dataOptionsAccessor = new Mock>(); dataOptionsAccessor.SetupGet(accessor => accessor.Options).Returns(new DataProtectionOptions()); var antiForgery = new AntiForgery( Mock.Of(), Mock.Of(), Mock.Of(), antiforgeryOptionsAccessor.Object, htmlEncoder, dataOptionsAccessor.Object); var optionsAccessor = new Mock>(); optionsAccessor .SetupGet(o => o.Options) .Returns(new MvcOptions()); return new DefaultHtmlGenerator( antiForgery, mvcViewOptionsAccessor.Object, metadataProvider, Mock.Of(), htmlEncoder); } // GetCurrentValues uses only the ModelStateDictionary and ViewDataDictionary from the passed ViewContext. private static ViewContext GetViewContext(TModel model, IModelMetadataProvider metadataProvider) { var actionContext = new ActionContext(); var viewData = new ViewDataDictionary(metadataProvider, actionContext.ModelState) { Model = model, }; return new ViewContext( actionContext, Mock.Of(), viewData, Mock.Of(), TextWriter.Null, new HtmlHelperOptions()); } public enum RegularEnum { Zero, One, Two, Three, } public enum FlagsEnum { None = 0, One = 1, Two = 2, Four = 4, FortyTwo = 42, All = -1, } private class Model { public int Id { get; set; } public string Name { get; set; } public RegularEnum RegularEnum { get; set; } public FlagsEnum FlagsEnum { get; set; } public RegularEnum? NullableEnum { get; set; } public List Collection { get; } = new List(); } } }