Add FormTagHelper.
- Added the FormTagHelper. - Utilized the IHtmlGenerator to share base functionality with the HTMLHelper counterparts. - Added tests to validate FormTagHelper functionality. #1246
This commit is contained in:
parent
63f8f7de6d
commit
4c98c8fcb9
|
|
@ -0,0 +1,132 @@
|
|||
// 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;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Microsoft.AspNet.Mvc.Rendering;
|
||||
using Microsoft.AspNet.Razor.Runtime.TagHelpers;
|
||||
using Microsoft.AspNet.Razor.TagHelpers;
|
||||
|
||||
namespace Microsoft.AspNet.Mvc.TagHelpers
|
||||
{
|
||||
/// <summary>
|
||||
/// <see cref="ITagHelper"/> implementation targeting <form> elements.
|
||||
/// </summary>
|
||||
[ContentBehavior(ContentBehavior.Append)]
|
||||
public class FormTagHelper : TagHelper
|
||||
{
|
||||
private const string RouteAttributePrefix = "route-";
|
||||
|
||||
[Activate]
|
||||
private ViewContext ViewContext { get; set; }
|
||||
|
||||
[Activate]
|
||||
private IHtmlGenerator Generator { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The name of the action method.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// If value contains a '/' this <see cref="ITagHelper"/> will do nothing.
|
||||
/// </remarks>
|
||||
public string Action { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The name of the controller.
|
||||
/// </summary>
|
||||
public string Controller { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The HTTP method for processing the form, either GET or POST.
|
||||
/// </summary>
|
||||
public string Method { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether the anti-forgery token should be generated. Defaults to <c>true</c> if <see cref="Action"/> is not
|
||||
/// a URL, <c>false</c> otherwise.
|
||||
/// </summary>
|
||||
[HtmlAttributeName("anti-forgery")]
|
||||
public bool? AntiForgery { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
/// <remarks>Does nothing if <see cref="Action"/> contains a '/'.</remarks>
|
||||
public override void Process(TagHelperContext context, TagHelperOutput output)
|
||||
{
|
||||
bool antiForgeryDefault = true;
|
||||
|
||||
var routePrefixedAttributes = output.FindPrefixedAttributes(RouteAttributePrefix);
|
||||
|
||||
// If Action contains a '/' it means the user is attempting to use the FormTagHelper as a normal form.
|
||||
if (Action != null && Action.Contains('/'))
|
||||
{
|
||||
if (Controller != null || routePrefixedAttributes.Any())
|
||||
{
|
||||
// We don't know how to generate a form action since a Controller attribute was also provided.
|
||||
throw new InvalidOperationException(
|
||||
Resources.FormatFormTagHelper_CannotDetermineAction(
|
||||
"<form>",
|
||||
nameof(Action).ToLowerInvariant(),
|
||||
nameof(Controller).ToLowerInvariant(),
|
||||
RouteAttributePrefix));
|
||||
}
|
||||
|
||||
// User is using the FormTagHelper like a normal <form> tag, anti-forgery default should be false to
|
||||
// not force the anti-forgery token onto the user.
|
||||
antiForgeryDefault = false;
|
||||
|
||||
// Restore Action, Method and Route HTML attributes if they were provided, user wants non-TagHelper <form>.
|
||||
output.CopyHtmlAttribute(nameof(Action), context);
|
||||
|
||||
if (Method != null)
|
||||
{
|
||||
output.CopyHtmlAttribute(nameof(Method), context);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var routeValues = GetRouteValues(output, routePrefixedAttributes);
|
||||
var tagBuilder = Generator.GenerateForm(ViewContext,
|
||||
Action,
|
||||
Controller,
|
||||
routeValues,
|
||||
Method,
|
||||
htmlAttributes: null);
|
||||
|
||||
if (tagBuilder != null)
|
||||
{
|
||||
// We don't want to do a full merge because we want the TagHelper content to take precedence.
|
||||
output.Merge(tagBuilder);
|
||||
}
|
||||
}
|
||||
|
||||
if (AntiForgery ?? antiForgeryDefault)
|
||||
{
|
||||
var antiForgeryTagBuilder = Generator.GenerateAntiForgery(ViewContext);
|
||||
if (antiForgeryTagBuilder != null)
|
||||
{
|
||||
output.Content += antiForgeryTagBuilder.ToString(TagRenderMode.SelfClosing);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: We will not need this method once https://github.com/aspnet/Razor/issues/89 is completed.
|
||||
private static Dictionary<string, object> GetRouteValues(
|
||||
TagHelperOutput output, IEnumerable<KeyValuePair<string, string>> routePrefixedAttributes)
|
||||
{
|
||||
Dictionary<string, object> routeValues = null;
|
||||
if (routePrefixedAttributes.Any())
|
||||
{
|
||||
// Prefixed values should be treated as bound attributes, remove them from the output.
|
||||
output.RemoveRange(routePrefixedAttributes);
|
||||
|
||||
// Generator.GenerateForm does not accept a Dictionary<string, string> for route values.
|
||||
routeValues = routePrefixedAttributes.ToDictionary(
|
||||
attribute => attribute.Key.Substring(RouteAttributePrefix.Length),
|
||||
attribute => (object)attribute.Value);
|
||||
}
|
||||
|
||||
return routeValues;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
// <auto-generated />
|
||||
namespace Microsoft.AspNet.Mvc.TagHelpers
|
||||
{
|
||||
using System.Globalization;
|
||||
using System.Reflection;
|
||||
using System.Resources;
|
||||
|
||||
internal static class Resources
|
||||
{
|
||||
private static readonly ResourceManager _resourceManager
|
||||
= new ResourceManager("Microsoft.AspNet.Mvc.TagHelpers.Resources", typeof(Resources).GetTypeInfo().Assembly);
|
||||
|
||||
/// <summary>
|
||||
/// Cannot determine an {1} for {0}. A {0} with a URL-based {1} must not have attributes starting with {3} or a {2} attribute.
|
||||
/// </summary>
|
||||
internal static string FormTagHelper_CannotDetermineAction
|
||||
{
|
||||
get { return GetString("FormTagHelper_CannotDetermineAction"); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Cannot determine an {1} for {0}. A {0} with a URL-based {1} must not have attributes starting with {3} or a {2} attribute.
|
||||
/// </summary>
|
||||
internal static string FormatFormTagHelper_CannotDetermineAction(object p0, object p1, object p2, object p3)
|
||||
{
|
||||
return string.Format(CultureInfo.CurrentCulture, GetString("FormTagHelper_CannotDetermineAction"), p0, p1, p2, p3);
|
||||
}
|
||||
|
||||
private static string GetString(string name, params string[] formatterNames)
|
||||
{
|
||||
var value = _resourceManager.GetString(name);
|
||||
|
||||
System.Diagnostics.Debug.Assert(value != null);
|
||||
|
||||
if (formatterNames != null)
|
||||
{
|
||||
for (var i = 0; i < formatterNames.Length; i++)
|
||||
{
|
||||
value = value.Replace("{" + formatterNames[i] + "}", "{" + i + "}");
|
||||
}
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,123 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<!--
|
||||
Microsoft ResX Schema
|
||||
|
||||
Version 2.0
|
||||
|
||||
The primary goals of this format is to allow a simple XML format
|
||||
that is mostly human readable. The generation and parsing of the
|
||||
various data types are done through the TypeConverter classes
|
||||
associated with the data types.
|
||||
|
||||
Example:
|
||||
|
||||
... ado.net/XML headers & schema ...
|
||||
<resheader name="resmimetype">text/microsoft-resx</resheader>
|
||||
<resheader name="version">2.0</resheader>
|
||||
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
|
||||
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
|
||||
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
|
||||
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
|
||||
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
|
||||
<value>[base64 mime encoded serialized .NET Framework object]</value>
|
||||
</data>
|
||||
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
|
||||
<comment>This is a comment</comment>
|
||||
</data>
|
||||
|
||||
There are any number of "resheader" rows that contain simple
|
||||
name/value pairs.
|
||||
|
||||
Each data row contains a name, and value. The row also contains a
|
||||
type or mimetype. Type corresponds to a .NET class that support
|
||||
text/value conversion through the TypeConverter architecture.
|
||||
Classes that don't support this are serialized and stored with the
|
||||
mimetype set.
|
||||
|
||||
The mimetype is used for serialized objects, and tells the
|
||||
ResXResourceReader how to depersist the object. This is currently not
|
||||
extensible. For a given mimetype the value must be set accordingly:
|
||||
|
||||
Note - application/x-microsoft.net.object.binary.base64 is the format
|
||||
that the ResXResourceWriter will generate, however the reader can
|
||||
read any of the formats listed below.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.binary.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.soap.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.bytearray.base64
|
||||
value : The object must be serialized into a byte array
|
||||
: using a System.ComponentModel.TypeConverter
|
||||
: and then encoded with base64 encoding.
|
||||
-->
|
||||
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
|
||||
<xsd:element name="root" msdata:IsDataSet="true">
|
||||
<xsd:complexType>
|
||||
<xsd:choice maxOccurs="unbounded">
|
||||
<xsd:element name="metadata">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" use="required" type="xsd:string" />
|
||||
<xsd:attribute name="type" type="xsd:string" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="assembly">
|
||||
<xsd:complexType>
|
||||
<xsd:attribute name="alias" type="xsd:string" />
|
||||
<xsd:attribute name="name" type="xsd:string" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="data">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
|
||||
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="resheader">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:choice>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:schema>
|
||||
<resheader name="resmimetype">
|
||||
<value>text/microsoft-resx</value>
|
||||
</resheader>
|
||||
<resheader name="version">
|
||||
<value>2.0</value>
|
||||
</resheader>
|
||||
<resheader name="reader">
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<data name="FormTagHelper_CannotDetermineAction" xml:space="preserve">
|
||||
<value>Cannot determine an {1} for {0}. A {0} with a URL-based {1} must not have attributes starting with {3} or a {2} attribute.</value>
|
||||
</data>
|
||||
</root>
|
||||
|
|
@ -0,0 +1,379 @@
|
|||
// 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;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Reflection;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNet.Http;
|
||||
using Microsoft.AspNet.Mvc.ModelBinding;
|
||||
using Microsoft.AspNet.Mvc.Razor;
|
||||
using Microsoft.AspNet.Mvc.Rendering;
|
||||
using Microsoft.AspNet.Razor.Runtime.TagHelpers;
|
||||
using Microsoft.AspNet.Routing;
|
||||
using Moq;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNet.Mvc.TagHelpers
|
||||
{
|
||||
public class FormTagHelperTest
|
||||
{
|
||||
[Fact]
|
||||
public async Task ProcessAsync_GeneratesExpectedOutput()
|
||||
{
|
||||
// Arrange
|
||||
var metadataProvider = new DataAnnotationsModelMetadataProvider();
|
||||
var formTagHelper = new FormTagHelper
|
||||
{
|
||||
Action = "index",
|
||||
Controller = "home",
|
||||
Method = "post",
|
||||
AntiForgery = true
|
||||
};
|
||||
var tagHelperContext = new TagHelperContext(
|
||||
allAttributes: new Dictionary<string, object>
|
||||
{
|
||||
{ "id", "myform" },
|
||||
{ "route-foo", "bar" },
|
||||
{ "action", "index" },
|
||||
{ "controller", "home" },
|
||||
{ "method", "post" },
|
||||
{ "anti-forgery", true }
|
||||
});
|
||||
var output = new TagHelperOutput(
|
||||
"form",
|
||||
attributes: new Dictionary<string, string>
|
||||
{
|
||||
{ "id", "myform" },
|
||||
{ "route-foo", "bar" },
|
||||
},
|
||||
content: "Something");
|
||||
var urlHelper = new Mock<IUrlHelper>();
|
||||
urlHelper
|
||||
.Setup(mock => mock.Action(It.IsAny<string>(),
|
||||
It.IsAny<string>(),
|
||||
It.IsAny<object>(),
|
||||
It.IsAny<string>(),
|
||||
It.IsAny<string>(),
|
||||
It.IsAny<string>()))
|
||||
.Returns("home/index");
|
||||
|
||||
var htmlGenerator = new TestableHtmlGenerator(metadataProvider, urlHelper.Object);
|
||||
var viewContext = TestableHtmlGenerator.GetViewContext(model: null,
|
||||
htmlGenerator: htmlGenerator,
|
||||
metadataProvider: metadataProvider);
|
||||
var expectedContent = "Something" + htmlGenerator.GenerateAntiForgery(viewContext)
|
||||
.ToString(TagRenderMode.SelfClosing);
|
||||
var activator = new DefaultTagHelperActivator();
|
||||
activator.Activate(formTagHelper, viewContext);
|
||||
|
||||
// Act
|
||||
await formTagHelper.ProcessAsync(tagHelperContext, output);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(3, output.Attributes.Count);
|
||||
var attribute = Assert.Single(output.Attributes, kvp => kvp.Key.Equals("id"));
|
||||
Assert.Equal("myform", attribute.Value);
|
||||
attribute = Assert.Single(output.Attributes, kvp => kvp.Key.Equals("method"));
|
||||
Assert.Equal("post", attribute.Value);
|
||||
attribute = Assert.Single(output.Attributes, kvp => kvp.Key.Equals("action"));
|
||||
Assert.Equal("home/index", attribute.Value);
|
||||
Assert.Equal(expectedContent, output.Content);
|
||||
Assert.Equal("form", output.TagName);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(true, "<input />")]
|
||||
[InlineData(false, "")]
|
||||
[InlineData(null, "<input />")]
|
||||
public async Task ProcessAsync_GeneratesAntiForgeryCorrectly(bool? antiForgery, string expectedContent)
|
||||
{
|
||||
// Arrange
|
||||
var viewContext = CreateViewContext();
|
||||
var formTagHelper = new FormTagHelper
|
||||
{
|
||||
Action = "Index",
|
||||
AntiForgery = antiForgery
|
||||
};
|
||||
var context = new TagHelperContext(
|
||||
allAttributes: new Dictionary<string, object>());
|
||||
var output = new TagHelperOutput(
|
||||
"form",
|
||||
attributes: new Dictionary<string, string>(),
|
||||
content: string.Empty);
|
||||
var generator = new Mock<IHtmlGenerator>(MockBehavior.Strict);
|
||||
generator
|
||||
.Setup(mock => mock.GenerateForm(
|
||||
It.IsAny<ViewContext>(),
|
||||
It.IsAny<string>(),
|
||||
It.IsAny<string>(),
|
||||
It.IsAny<object>(),
|
||||
It.IsAny<string>(),
|
||||
It.IsAny<object>()))
|
||||
.Returns(new TagBuilder("form"));
|
||||
|
||||
generator.Setup(mock => mock.GenerateAntiForgery(viewContext))
|
||||
.Returns(new TagBuilder("input"));
|
||||
|
||||
SetViewContextAndGenerator(formTagHelper, viewContext, generator.Object);
|
||||
|
||||
// Act
|
||||
await formTagHelper.ProcessAsync(context, output);
|
||||
|
||||
// Assert
|
||||
Assert.Equal("form", output.TagName);
|
||||
Assert.Empty(output.Attributes);
|
||||
Assert.Equal(expectedContent, output.Content);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ProcessAsync_BindsRouteValuesFromTagHelperOutput()
|
||||
{
|
||||
// Arrange
|
||||
var testViewContext = CreateViewContext();
|
||||
var formTagHelper = new FormTagHelper
|
||||
{
|
||||
Action = "Index",
|
||||
AntiForgery = false
|
||||
};
|
||||
var context = new TagHelperContext(
|
||||
allAttributes: new Dictionary<string, object>());
|
||||
var expectedAttribute = new KeyValuePair<string, string>("ROUTEE-NotRoute", "something");
|
||||
var output = new TagHelperOutput(
|
||||
"form",
|
||||
attributes: new Dictionary<string, string>()
|
||||
{
|
||||
{ "route-val", "hello" },
|
||||
{ "roUte--Foo", "bar" }
|
||||
},
|
||||
content: string.Empty);
|
||||
output.Attributes.Add(expectedAttribute);
|
||||
|
||||
var generator = new Mock<IHtmlGenerator>(MockBehavior.Strict);
|
||||
generator
|
||||
.Setup(mock => mock.GenerateForm(
|
||||
It.IsAny<ViewContext>(),
|
||||
It.IsAny<string>(),
|
||||
It.IsAny<string>(),
|
||||
It.IsAny<object>(),
|
||||
It.IsAny<string>(),
|
||||
It.IsAny<object>()))
|
||||
.Callback<ViewContext, string, string, object, string, object>(
|
||||
(viewContext, actionName, controllerName, routeValues, method, htmlAttributes) =>
|
||||
{
|
||||
// Fixes Roslyn bug with lambdas
|
||||
generator.ToString();
|
||||
|
||||
var routeValueDictionary = (Dictionary<string, object>)routeValues;
|
||||
|
||||
Assert.Equal(2, routeValueDictionary.Count);
|
||||
var routeValue = Assert.Single(routeValueDictionary, kvp => kvp.Key.Equals("val"));
|
||||
Assert.Equal("hello", routeValue.Value);
|
||||
routeValue = Assert.Single(routeValueDictionary, kvp => kvp.Key.Equals("-Foo"));
|
||||
Assert.Equal("bar", routeValue.Value);
|
||||
})
|
||||
.Returns(new TagBuilder("form"))
|
||||
.Verifiable();
|
||||
|
||||
SetViewContextAndGenerator(formTagHelper, testViewContext, generator.Object);
|
||||
|
||||
// Act & Assert
|
||||
await formTagHelper.ProcessAsync(context, output);
|
||||
|
||||
Assert.Equal("form", output.TagName);
|
||||
var attribute = Assert.Single(output.Attributes);
|
||||
Assert.Equal(expectedAttribute, attribute);
|
||||
Assert.Empty(output.Content);
|
||||
generator.Verify();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ProcessAsync_CallsIntoGenerateFormWithExpectedParameters()
|
||||
{
|
||||
// Arrange
|
||||
var viewContext = CreateViewContext();
|
||||
var formTagHelper = new FormTagHelper
|
||||
{
|
||||
Action = "Index",
|
||||
Controller = "Home",
|
||||
Method = "POST",
|
||||
AntiForgery = false
|
||||
};
|
||||
var context = new TagHelperContext(
|
||||
allAttributes: new Dictionary<string, object>());
|
||||
var output = new TagHelperOutput(
|
||||
"form",
|
||||
attributes: new Dictionary<string, string>(),
|
||||
content: string.Empty);
|
||||
var generator = new Mock<IHtmlGenerator>(MockBehavior.Strict);
|
||||
generator
|
||||
.Setup(mock => mock.GenerateForm(viewContext, "Index", "Home", null, "POST", null))
|
||||
.Returns(new TagBuilder("form"))
|
||||
.Verifiable();
|
||||
|
||||
SetViewContextAndGenerator(formTagHelper,
|
||||
viewContext,
|
||||
generator.Object);
|
||||
|
||||
// Act & Assert
|
||||
await formTagHelper.ProcessAsync(context, output);
|
||||
generator.Verify();
|
||||
|
||||
Assert.Equal("form", output.TagName);
|
||||
Assert.Empty(output.Attributes);
|
||||
Assert.Empty(output.Content);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ProcessAsync_RestoresBoundAttributesIfActionIsURL()
|
||||
{
|
||||
// Arrange
|
||||
var formTagHelper = new FormTagHelper
|
||||
{
|
||||
Action = "http://www.contoso.com",
|
||||
Method = "POST"
|
||||
};
|
||||
var output = new TagHelperOutput("form",
|
||||
attributes: new Dictionary<string, string>(),
|
||||
content: string.Empty);
|
||||
var context = new TagHelperContext(
|
||||
allAttributes: new Dictionary<string, object>()
|
||||
{
|
||||
{ "aCTiON", "http://www.contoso.com" },
|
||||
{ "METhod", "POST" }
|
||||
});
|
||||
|
||||
// Act
|
||||
await formTagHelper.ProcessAsync(context, output);
|
||||
|
||||
// Assert
|
||||
Assert.Equal("form", output.TagName);
|
||||
Assert.Equal(2, output.Attributes.Count);
|
||||
var attribute = Assert.Single(output.Attributes, kvp => kvp.Key.Equals("aCTiON"));
|
||||
Assert.Equal("http://www.contoso.com", attribute.Value);
|
||||
attribute = Assert.Single(output.Attributes, kvp => kvp.Key.Equals("METhod"));
|
||||
Assert.Equal("POST", attribute.Value);
|
||||
Assert.Empty(output.Content);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(true, "<input />")]
|
||||
[InlineData(false, "")]
|
||||
[InlineData(null, "")]
|
||||
public async Task ProcessAsync_SupportsAntiForgeryIfActionIsURL(bool? antiForgery, string expectedContent)
|
||||
{
|
||||
// Arrange
|
||||
var viewContext = CreateViewContext();
|
||||
var generator = new Mock<IHtmlGenerator>();
|
||||
generator.Setup(mock => mock.GenerateAntiForgery(It.IsAny<ViewContext>()))
|
||||
.Returns(new TagBuilder("input"));
|
||||
var formTagHelper = new FormTagHelper
|
||||
{
|
||||
Action = "http://www.contoso.com",
|
||||
AntiForgery = antiForgery,
|
||||
};
|
||||
SetViewContextAndGenerator(formTagHelper,
|
||||
viewContext,
|
||||
generator.Object);
|
||||
var output = new TagHelperOutput("form",
|
||||
attributes: new Dictionary<string, string>(),
|
||||
content: string.Empty);
|
||||
var context = new TagHelperContext(
|
||||
allAttributes: new Dictionary<string, object>()
|
||||
{
|
||||
{ "aCTiON", "http://www.contoso.com" }
|
||||
});
|
||||
|
||||
// Act
|
||||
await formTagHelper.ProcessAsync(context, output);
|
||||
|
||||
// Assert
|
||||
Assert.Equal("form", output.TagName);
|
||||
var attribute = Assert.Single(output.Attributes);
|
||||
Assert.Equal(new KeyValuePair<string, string>("aCTiON", "http://www.contoso.com"), attribute);
|
||||
Assert.Equal(expectedContent, output.Content);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ProcessAsync_ThrowsIfActionIsUrlWithSpecifiedController()
|
||||
{
|
||||
// Arrange
|
||||
var formTagHelper = new FormTagHelper
|
||||
{
|
||||
Action = "http://www.contoso.com",
|
||||
Controller = "Home",
|
||||
Method = "POST"
|
||||
};
|
||||
var expectedErrorMessage = "Cannot determine an action for <form>. A <form> with a URL-based action " +
|
||||
"must not have attributes starting with route- or a controller attribute.";
|
||||
var tagHelperOutput = new TagHelperOutput(
|
||||
"form",
|
||||
attributes: new Dictionary<string, string>(),
|
||||
content: string.Empty);
|
||||
|
||||
// Act & Assert
|
||||
var ex = await Assert.ThrowsAsync<InvalidOperationException>(async () =>
|
||||
{
|
||||
await formTagHelper.ProcessAsync(context: null, output: tagHelperOutput);
|
||||
});
|
||||
|
||||
Assert.Equal(expectedErrorMessage, ex.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ProcessAsync_ThrowsIfActionIsUrlWithSpecifiedRoutes()
|
||||
{
|
||||
// Arrange
|
||||
var formTagHelper = new FormTagHelper
|
||||
{
|
||||
Action = "http://www.contoso.com",
|
||||
Method = "POST"
|
||||
};
|
||||
var expectedErrorMessage = "Cannot determine an action for <form>. A <form> with a URL-based action " +
|
||||
"must not have attributes starting with route- or a controller attribute.";
|
||||
var tagHelperOutput = new TagHelperOutput(
|
||||
"form",
|
||||
attributes: new Dictionary<string, string>
|
||||
{
|
||||
{ "route-foo", "bar" }
|
||||
},
|
||||
content: string.Empty);
|
||||
|
||||
// Act & Assert
|
||||
var ex = await Assert.ThrowsAsync<InvalidOperationException>(async () =>
|
||||
{
|
||||
await formTagHelper.ProcessAsync(context: null, output: tagHelperOutput);
|
||||
});
|
||||
|
||||
Assert.Equal(expectedErrorMessage, ex.Message);
|
||||
}
|
||||
|
||||
private static ViewContext CreateViewContext()
|
||||
{
|
||||
var actionContext = new ActionContext(
|
||||
new Mock<HttpContext>().Object,
|
||||
new RouteData(),
|
||||
new ActionDescriptor());
|
||||
|
||||
return new ViewContext(
|
||||
actionContext,
|
||||
Mock.Of<IView>(),
|
||||
new ViewDataDictionary(
|
||||
new DataAnnotationsModelMetadataProvider()),
|
||||
new StringWriter());
|
||||
}
|
||||
|
||||
private static void SetViewContextAndGenerator(ITagHelper tagHelper,
|
||||
ViewContext viewContext,
|
||||
IHtmlGenerator generator)
|
||||
{
|
||||
var tagHelperType = tagHelper.GetType();
|
||||
|
||||
tagHelperType.GetProperty("ViewContext", BindingFlags.NonPublic | BindingFlags.Instance)
|
||||
.SetValue(tagHelper, viewContext);
|
||||
tagHelperType.GetProperty("Generator", BindingFlags.NonPublic | BindingFlags.Instance)
|
||||
.SetValue(tagHelper, generator);
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue