From 09af7bb77b7c2b626292a0a83816f6e969d55861 Mon Sep 17 00:00:00 2001 From: "N. Taylor Mullen" Date: Mon, 20 Oct 2014 13:52:05 -0700 Subject: [PATCH] Change TagBuilder Attributes and HTML Helper dependencies to be case insensitive. - This involved adding the StringComparer.OrdinalIgnoreCase comparer to the TagBuilder's Attributes dictionary. - Added tests to validate that all methods that made use of TagBuilder.Attributes abide by the new ignore case mechanic. - Added two sets of tests to validate the new functionality of Object => Dictionary HTML helper tests. - Modified a functional test that utilizes HTML Helpers to provide same attribute-different case objects. - Fixed existing HTML helper tests to account for new ordering of attrbutes (dictionary no longer adds key value pairs, it sets them). #1328 --- .../Internal/TypeHelper.cs | 2 +- .../Rendering/Html/HtmlHelper.cs | 2 +- .../Rendering/Html/TagBuilder.cs | 5 +- .../Internal/TypeHelperTest.cs | 58 ++++++++++ .../Rendering/HtmlHelperInputTest.cs | 30 ++--- .../Rendering/HtmlHelperTest.cs | 59 ++++++++++ .../Rendering/TagBuilderTest.cs | 104 ++++++++++++++++++ .../Views/Account/Login.cshtml | 4 +- 8 files changed, 243 insertions(+), 21 deletions(-) create mode 100644 test/Microsoft.AspNet.Mvc.Core.Test/Internal/TypeHelperTest.cs create mode 100644 test/Microsoft.AspNet.Mvc.Core.Test/Rendering/HtmlHelperTest.cs create mode 100644 test/Microsoft.AspNet.Mvc.Core.Test/Rendering/TagBuilderTest.cs diff --git a/src/Microsoft.AspNet.Mvc.Core/Internal/TypeHelper.cs b/src/Microsoft.AspNet.Mvc.Core/Internal/TypeHelper.cs index b4160d7464..e479dad114 100644 --- a/src/Microsoft.AspNet.Mvc.Core/Internal/TypeHelper.cs +++ b/src/Microsoft.AspNet.Mvc.Core/Internal/TypeHelper.cs @@ -53,7 +53,7 @@ namespace Microsoft.AspNet.Mvc { foreach (var helper in PropertyHelper.GetProperties(value)) { - dictionary.Add(helper.Name, helper.GetValue(value)); + dictionary[helper.Name] = helper.GetValue(value); } } diff --git a/src/Microsoft.AspNet.Mvc.Core/Rendering/Html/HtmlHelper.cs b/src/Microsoft.AspNet.Mvc.Core/Rendering/Html/HtmlHelper.cs index 8c9b0c59cd..8d17a3172d 100644 --- a/src/Microsoft.AspNet.Mvc.Core/Rendering/Html/HtmlHelper.cs +++ b/src/Microsoft.AspNet.Mvc.Core/Rendering/Html/HtmlHelper.cs @@ -155,7 +155,7 @@ namespace Microsoft.AspNet.Mvc.Rendering { foreach (var helper in HtmlAttributePropertyHelper.GetProperties(htmlAttributes)) { - dictionary.Add(helper.Name, helper.GetValue(htmlAttributes)); + dictionary[helper.Name] = helper.GetValue(htmlAttributes); } } diff --git a/src/Microsoft.AspNet.Mvc.Core/Rendering/Html/TagBuilder.cs b/src/Microsoft.AspNet.Mvc.Core/Rendering/Html/TagBuilder.cs index 0d14fc70dc..330e1fb297 100644 --- a/src/Microsoft.AspNet.Mvc.Core/Rendering/Html/TagBuilder.cs +++ b/src/Microsoft.AspNet.Mvc.Core/Rendering/Html/TagBuilder.cs @@ -22,7 +22,7 @@ namespace Microsoft.AspNet.Mvc.Rendering } TagName = tagName; - Attributes = new SortedDictionary(StringComparer.Ordinal); + Attributes = new SortedDictionary(StringComparer.OrdinalIgnoreCase); } public IDictionary Attributes { get; private set; } @@ -110,7 +110,8 @@ namespace Microsoft.AspNet.Mvc.Rendering foreach (var attribute in Attributes) { var key = attribute.Key; - if (string.Equals(key, "id", StringComparison.Ordinal) && string.IsNullOrEmpty(attribute.Value)) + if (string.Equals(key, "id", StringComparison.OrdinalIgnoreCase) && + string.IsNullOrEmpty(attribute.Value)) { continue; } diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/Internal/TypeHelperTest.cs b/test/Microsoft.AspNet.Mvc.Core.Test/Internal/TypeHelperTest.cs new file mode 100644 index 0000000000..bed09e62b7 --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.Core.Test/Internal/TypeHelperTest.cs @@ -0,0 +1,58 @@ +// 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.Collections.Generic; +using Xunit; + +namespace Microsoft.AspNet.Mvc.Internal +{ + public class TypeHelperTest + { + public static TheoryData> IgnoreCaseTestData + { + get + { + return new TheoryData> + { + { + new + { + selected = true, + SeLeCtEd = false + }, + new KeyValuePair("selected", false) + }, + { + new + { + SeLeCtEd = false, + selected = true + }, + new KeyValuePair("SeLeCtEd", true) + }, + { + new + { + SelECTeD = false, + SeLECTED = true + }, + new KeyValuePair("SelECTeD", true) + } + }; + } + } + + [Theory] + [MemberData(nameof(IgnoreCaseTestData))] + public void ObjectToDictionary_IgnoresPropertyCase(object testObject, + KeyValuePair expectedEntry) + { + // Act + var result = TypeHelper.ObjectToDictionary(testObject); + + // Assert + var entry = Assert.Single(result); + Assert.Equal(expectedEntry, entry); + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/Rendering/HtmlHelperInputTest.cs b/test/Microsoft.AspNet.Mvc.Core.Test/Rendering/HtmlHelperInputTest.cs index 0e4fa572b9..7185c5e409 100644 --- a/test/Microsoft.AspNet.Mvc.Core.Test/Rendering/HtmlHelperInputTest.cs +++ b/test/Microsoft.AspNet.Mvc.Core.Test/Rendering/HtmlHelperInputTest.cs @@ -131,9 +131,9 @@ namespace Microsoft.AspNet.Mvc.Rendering public void CheckBoxReplacesUnderscoresInHtmlAttributesWithDashes() { // Arrange - var expected = @"" + - @""; + var expected = @""; var helper = DefaultTemplatesUtilities.GetHtmlHelper(GetTestModelViewData()); var htmlAttributes = new { Property1_Property3 = "Property3ObjValue" }; @@ -148,8 +148,8 @@ namespace Microsoft.AspNet.Mvc.Rendering public void CheckBoxWithPrefix_ReplaceDotsInIdByDefaultWithUnderscores() { // Arrange - var expected = @""; var dictionary = new RouteValueDictionary(new { Property3 = "Property3Value" }); var helper = DefaultTemplatesUtilities.GetHtmlHelper(); @@ -166,8 +166,8 @@ namespace Microsoft.AspNet.Mvc.Rendering public void CheckBoxWithPrefix_ReplacesDotsInIdWithIdDotReplacement() { // Arrange - var expected = @""; var dictionary = new Dictionary { { "Property3", "Property3Value" } }; var helper = DefaultTemplatesUtilities.GetHtmlHelper(); @@ -185,7 +185,7 @@ namespace Microsoft.AspNet.Mvc.Rendering public void CheckBoxWithPrefixAndEmptyName() { // Arrange - var expected = @""; var helper = DefaultTemplatesUtilities.GetHtmlHelper(model: false); @@ -288,9 +288,9 @@ namespace Microsoft.AspNet.Mvc.Rendering public void CheckBoxForWithObjectAttributeWithUnderscores() { // Arrange - var expected = @"" + - @""; + var expected = @""; var helper = DefaultTemplatesUtilities.GetHtmlHelperForViewData(GetTestModelViewData()); var htmlAttributes = new { Property1_Property3 = "Property3ObjValue" }; @@ -305,9 +305,9 @@ namespace Microsoft.AspNet.Mvc.Rendering public void CheckBoxForWithAttributeDictionary() { // Arrange - var expected = @""; + var expected = @""; var helper = DefaultTemplatesUtilities.GetHtmlHelperForViewData(GetTestModelViewData()); var attributes = new Dictionary { { "Property3", "Property3Value" } }; @@ -322,7 +322,7 @@ namespace Microsoft.AspNet.Mvc.Rendering public void CheckBoxForWithPrefix() { // Arrange - var expected = @""; var helper = DefaultTemplatesUtilities.GetHtmlHelperForViewData(GetTestModelViewData()); diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/Rendering/HtmlHelperTest.cs b/test/Microsoft.AspNet.Mvc.Core.Test/Rendering/HtmlHelperTest.cs new file mode 100644 index 0000000000..0d5aef76cb --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.Core.Test/Rendering/HtmlHelperTest.cs @@ -0,0 +1,59 @@ +// 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.Collections.Generic; +using System.Linq; +using Xunit; + +namespace Microsoft.AspNet.Mvc.Rendering +{ + public class HtmlHelperTest + { + public static TheoryData> IgnoreCaseTestData + { + get + { + return new TheoryData> + { + { + new + { + selected = true, + SeLeCtEd = false + }, + new KeyValuePair("selected", false) + }, + { + new + { + SeLeCtEd = false, + selected = true + }, + new KeyValuePair("SeLeCtEd", true) + }, + { + new + { + SelECTeD = false, + SeLECTED = true + }, + new KeyValuePair("SelECTeD", true) + } + }; + } + } + + [Theory] + [MemberData(nameof(IgnoreCaseTestData))] + public void AnonymousObjectToHtmlAttributes_IgnoresPropertyCase(object htmlAttributeObject, + KeyValuePair expectedEntry) + { + // Act + var result = HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributeObject); + + // Assert + var entry = Assert.Single(result); + Assert.Equal(expectedEntry, entry); + } + } +} diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/Rendering/TagBuilderTest.cs b/test/Microsoft.AspNet.Mvc.Core.Test/Rendering/TagBuilderTest.cs new file mode 100644 index 0000000000..5aea199059 --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.Core.Test/Rendering/TagBuilderTest.cs @@ -0,0 +1,104 @@ +// 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.Collections.Generic; +using Microsoft.AspNet.Mvc.Rendering; +using Xunit; + +namespace Microsoft.AspNet.Mvc.Core.Rendering +{ + public class TagBuilderTest + { + public static TheoryData RenderingTestingData + { + get + { + return new TheoryData + { + { TagRenderMode.StartTag, "

" }, + { TagRenderMode.SelfClosing, "

" }, + { TagRenderMode.Normal, "

" } + }; + } + } + + [Theory] + [InlineData(false, "Hello", "World")] + [InlineData(true, "Hello", "something else")] + public void MergeAttribute_IgnoresCase(bool replaceExisting, string expectedKey, string expectedValue) + { + // Arrange + var tagBuilder = new TagBuilder("p"); + tagBuilder.Attributes.Add("Hello", "World"); + + // Act + tagBuilder.MergeAttribute("hello", "something else", replaceExisting); + + // Assert + var attribute = Assert.Single(tagBuilder.Attributes); + Assert.Equal(new KeyValuePair(expectedKey, expectedValue), attribute); + } + + [Fact] + public void AddCssClass_IgnoresCase() + { + // Arrange + var tagBuilder = new TagBuilder("p"); + tagBuilder.Attributes.Add("ClaSs", "btn"); + + // Act + tagBuilder.AddCssClass("success"); + + // Assert + var attribute = Assert.Single(tagBuilder.Attributes); + Assert.Equal(new KeyValuePair("ClaSs", "success btn"), attribute); + } + + [Fact] + public void GenerateId_IgnoresCase() + { + // Arrange + var tagBuilder = new TagBuilder("p"); + tagBuilder.Attributes.Add("ID", "something"); + + // Act + tagBuilder.GenerateId("else", idAttributeDotReplacement: "-"); + + // Assert + var attribute = Assert.Single(tagBuilder.Attributes); + Assert.Equal(new KeyValuePair("ID", "something"), attribute); + } + + [MemberData(nameof(RenderingTestingData))] + public void ToString_IgnoresIdAttributeCase(TagRenderMode renderingMode, string expectedOutput) + { + // Arrange + var tagBuilder = new TagBuilder("p"); + + // An empty value id attribute should not be rendered via ToString. + tagBuilder.Attributes.Add("ID", string.Empty); + + // Act + var value = tagBuilder.ToString(renderingMode); + + // Assert + Assert.Equal(expectedOutput, value); + } + + [MemberData(nameof(RenderingTestingData))] + public void ToHtmlString_IgnoresIdAttributeCase(TagRenderMode renderingMode, string expectedOutput) + { + // Arrange + var tagBuilder = new TagBuilder("p"); + + // An empty value id attribute should not be rendered via ToHtmlString. + tagBuilder.Attributes.Add("ID", string.Empty); + + // Act + var value = tagBuilder.ToHtmlString(renderingMode); + + // Assert + Assert.Equal(expectedOutput, value.ToString()); + } + } +} \ No newline at end of file diff --git a/test/WebSites/AntiForgeryWebSite/Views/Account/Login.cshtml b/test/WebSites/AntiForgeryWebSite/Views/Account/Login.cshtml index 1075ad5735..995d6e6867 100644 --- a/test/WebSites/AntiForgeryWebSite/Views/Account/Login.cshtml +++ b/test/WebSites/AntiForgeryWebSite/Views/Account/Login.cshtml @@ -15,9 +15,9 @@
@Html.ValidationSummary(true)
- @Html.LabelFor(m => m.UserName, new { @class = "col-md-2 control-label" }) + @Html.LabelFor(m => m.UserName, new { @class = "col-md-2", ClAsS = "col-md-2 control-label" })
- @Html.TextBoxFor(m => m.UserName, new { @class = "form-control" }) + @Html.TextBoxFor(m => m.UserName, new { @class = "...", cLass = "form-control" }) @Html.ValidationMessageFor(m => m.UserName)