Skip to content
This repository was archived by the owner on Dec 14, 2018. It is now read-only.

Commit 14cf681

Browse files
author
NTaylorMullen
committed
Add tests for FormTagHelper
- Modified FormTagHelper to utilize the AntiForgeryToken when the Action is URL based (different behavior).
1 parent b9250c5 commit 14cf681

File tree

2 files changed

+314
-28
lines changed

2 files changed

+314
-28
lines changed

src/Microsoft.AspNet.Mvc.TagHelpers/FormTagHelper.cs

Lines changed: 31 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
33

44
using System;
5+
using System.Collections.Generic;
56
using System.Linq;
67
using Microsoft.AspNet.Mvc.Rendering;
78
using Microsoft.AspNet.Razor.Runtime.TagHelpers;
@@ -16,7 +17,6 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
1617
public class FormTagHelper : TagHelper
1718
{
1819
private const string RouteAttributePrefix = "route-";
19-
private static readonly bool DefaultAntiForgeryBehavior = true;
2020

2121
[Activate]
2222
private ViewContext ViewContext { get; set; }
@@ -52,6 +52,8 @@ public class FormTagHelper : TagHelper
5252
/// <remarks>Does nothing if <see cref="Action"/> contains a '/'.</remarks>
5353
public override void Process(TagHelperContext context, TagHelperOutput output)
5454
{
55+
bool defaultAntiForgeryBehavior = true;
56+
5557
// If Action contains a '/' it means the user is attempting to use the FormTagHelper as a normal form.
5658
if (Action != null && Action.Contains('/'))
5759
{
@@ -65,16 +67,36 @@ public override void Process(TagHelperContext context, TagHelperOutput output)
6567
nameof(Controller)));
6668
}
6769

68-
RestoreBoundHtmlAttributes(context, output);
70+
// If the anti-forgery token was not specified then don't assume that it's on (user is using the
71+
// FormTagHelper like a normal <form> tag).
72+
defaultAntiForgeryBehavior = false;
73+
74+
// Restore Action and Method HTML attributes if they were provided, user wants the FormTagHelper to
75+
// behave like a normal <form> tag.
76+
77+
if (Action != null)
78+
{
79+
output.RestoreBoundHtmlAttribute(nameof(Action), context);
80+
}
81+
82+
if (Method != null)
83+
{
84+
output.RestoreBoundHtmlAttribute(nameof(Method), context);
85+
}
6986
}
7087
else
7188
{
7289
var prefixedValues = output.PullPrefixedAttributes(RouteAttributePrefix);
7390

74-
// Generator.GenerateForm does not accept a Dictionary<string, string> for route values.
75-
var routeValues = prefixedValues.ToDictionary(
76-
attribute => attribute.Key.Substring(RouteAttributePrefix.Length),
77-
attribute => (object)attribute.Value);
91+
Dictionary<string, object> routeValues = null;
92+
93+
if (prefixedValues.Any())
94+
{
95+
// Generator.GenerateForm does not accept a Dictionary<string, string> for route values.
96+
routeValues = prefixedValues.ToDictionary(
97+
attribute => attribute.Key.Substring(RouteAttributePrefix.Length),
98+
attribute => (object)attribute.Value);
99+
}
78100

79101
var tagBuilder = Generator.GenerateForm(ViewContext,
80102
Action,
@@ -84,31 +106,12 @@ public override void Process(TagHelperContext context, TagHelperOutput output)
84106
htmlAttributes: null);
85107

86108
output.Merge(tagBuilder);
87-
88-
if (AntiForgery ?? DefaultAntiForgeryBehavior)
89-
{
90-
var antiForgeryTag = Generator.GenerateAntiForgery(ViewContext);
91-
output.Content += antiForgeryTag.ToString(TagRenderMode.SelfClosing);
92-
}
93-
}
94-
}
95-
96-
// Restores bound HTML attributes when we detect that a user is using the AnchorTagHelper as a normal <a> tag.
97-
private void RestoreBoundHtmlAttributes(TagHelperContext context, TagHelperOutput output)
98-
{
99-
if (Action != null)
100-
{
101-
output.RestoreBoundHtmlAttribute(nameof(Action), context);
102-
}
103-
104-
if (Controller != null)
105-
{
106-
output.RestoreBoundHtmlAttribute(nameof(Controller), context);
107109
}
108110

109-
if (Method != null)
111+
if (AntiForgery ?? defaultAntiForgeryBehavior)
110112
{
111-
output.RestoreBoundHtmlAttribute(nameof(Method), context);
113+
var antiForgeryTag = Generator.GenerateAntiForgery(ViewContext);
114+
output.Content += antiForgeryTag.ToString(TagRenderMode.SelfClosing);
112115
}
113116
}
114117
}
Lines changed: 283 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,283 @@
1+
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System;
5+
using System.Collections.Generic;
6+
using System.IO;
7+
using System.Reflection;
8+
using Microsoft.AspNet.Http;
9+
using Microsoft.AspNet.Mvc.ModelBinding;
10+
using Microsoft.AspNet.Mvc.Rendering;
11+
using Microsoft.AspNet.Razor.Runtime.TagHelpers;
12+
using Microsoft.AspNet.Routing;
13+
using Moq;
14+
using Xunit;
15+
16+
namespace Microsoft.AspNet.Mvc.TagHelpers
17+
{
18+
public class FormTagHelperTest
19+
{
20+
[Theory]
21+
[InlineData(true, "<input />")]
22+
[InlineData(false, "")]
23+
[InlineData(null, "<input />")]
24+
public void Process_GeneratesAntiForgeryCorrectly(bool? antiForgery, string expectedContent)
25+
{
26+
// Arrange
27+
var expectedViewContext = CreateViewContext();
28+
var formTagHelper = new FormTagHelper
29+
{
30+
Action = "Index",
31+
AntiForgery = antiForgery
32+
};
33+
var context = new TagHelperContext(
34+
allAttributes: new Dictionary<string, object>());
35+
var output = new TagHelperOutput(
36+
"form",
37+
attributes: new Dictionary<string, string>(),
38+
content: string.Empty);
39+
40+
var generator = new Mock<IHtmlGenerator>();
41+
generator.Setup(mock =>
42+
mock.GenerateForm(It.IsAny<ViewContext>(),
43+
It.IsAny<string>(),
44+
It.IsAny<string>(),
45+
It.IsAny<object>(),
46+
It.IsAny<string>(),
47+
It.IsAny<object>()))
48+
.Returns(new TagBuilder("form"));
49+
generator.Setup(mock => mock.GenerateAntiForgery(It.IsAny<ViewContext>()))
50+
.Returns(new TagBuilder("input"));
51+
52+
SetViewContextAndGenerator(formTagHelper,
53+
expectedViewContext,
54+
generator.Object);
55+
56+
// Act
57+
formTagHelper.Process(context, output);
58+
59+
// Assert
60+
Assert.Equal(expectedContent, output.Content);
61+
}
62+
63+
[Fact]
64+
public void Process_UnderstandsRouteValues()
65+
{
66+
// Arrange
67+
var testViewContext = CreateViewContext();
68+
var formTagHelper = new FormTagHelper
69+
{
70+
Action = "Index",
71+
AntiForgery = false
72+
};
73+
var context = new TagHelperContext(
74+
allAttributes: new Dictionary<string, object>());
75+
var output = new TagHelperOutput(
76+
"form",
77+
attributes: new Dictionary<string, string>()
78+
{
79+
{ "route-val", "hello" },
80+
{ "roUte--Foo", "bar" },
81+
{ "ROUTEE-NotRoute", "something" }
82+
},
83+
content: string.Empty);
84+
var called = false;
85+
var generator = new Mock<IHtmlGenerator>();
86+
generator.Setup(mock =>
87+
mock.GenerateForm(It.IsAny<ViewContext>(),
88+
It.IsAny<string>(),
89+
It.IsAny<string>(),
90+
It.IsAny<object>(),
91+
It.IsAny<string>(),
92+
It.IsAny<object>()))
93+
.Callback<ViewContext, string, string, object, string, object>(
94+
(viewContext, actionName, controllerName, routeValues, method, htmlAttributes) =>
95+
{
96+
called = true;
97+
98+
var routeValueDictionary = (Dictionary<string, object>)routeValues;
99+
var routeValue = Assert.Single(routeValueDictionary, kvp => kvp.Key.Equals("val"));
100+
Assert.Equal("hello", routeValue.Value);
101+
routeValue = Assert.Single(routeValueDictionary, kvp => kvp.Key.Equals("-Foo"));
102+
Assert.Equal("bar", routeValue.Value);
103+
})
104+
.Returns(new TagBuilder("form"));
105+
106+
SetViewContextAndGenerator(formTagHelper,
107+
testViewContext,
108+
generator.Object);
109+
110+
// Act & Assert
111+
formTagHelper.Process(context, output);
112+
113+
var attribute = Assert.Single(output.Attributes, kvp => kvp.Key.Equals("ROUTEE-NotRoute"));
114+
Assert.Equal("something", attribute.Value);
115+
Assert.True(called);
116+
}
117+
118+
[Fact]
119+
public void Process_CallsIntoGenerateFormWithExpectedParameters()
120+
{
121+
// Arrange
122+
var expectedViewContext = CreateViewContext();
123+
var formTagHelper = new FormTagHelper
124+
{
125+
Action = "Index",
126+
Controller = "Home",
127+
Method = "POST",
128+
AntiForgery = false
129+
};
130+
var called = false;
131+
var context = new TagHelperContext(
132+
allAttributes: new Dictionary<string, object>());
133+
var output = new TagHelperOutput(
134+
"form",
135+
attributes: new Dictionary<string, string>(),
136+
content: string.Empty);
137+
138+
var generator = new Mock<IHtmlGenerator>();
139+
generator.Setup(mock =>
140+
mock.GenerateForm(It.IsAny<ViewContext>(),
141+
It.IsAny<string>(),
142+
It.IsAny<string>(),
143+
It.IsAny<object>(),
144+
It.IsAny<string>(),
145+
It.IsAny<object>()))
146+
.Callback<ViewContext, string, string, object, string, object>(
147+
(viewContext, actionName, controllerName, routeValues, method, htmlAttributes) =>
148+
{
149+
called = true;
150+
151+
Assert.Same(expectedViewContext, viewContext);
152+
Assert.Equal("Index", actionName);
153+
Assert.Equal("Home", controllerName);
154+
Assert.Null(routeValues);
155+
Assert.Equal("POST", method);
156+
Assert.Null(htmlAttributes);
157+
})
158+
.Returns(new TagBuilder("form"));
159+
160+
SetViewContextAndGenerator(formTagHelper,
161+
expectedViewContext,
162+
generator.Object);
163+
164+
// Act & Assert
165+
formTagHelper.Process(context, output);
166+
167+
Assert.True(called);
168+
}
169+
170+
[Fact]
171+
public void Process_RestoresBoundAttributesIfActionIsURL()
172+
{
173+
// Arrange
174+
var formTagHelper = new FormTagHelper
175+
{
176+
Action = "http://www.contoso.com",
177+
Method = "POST"
178+
};
179+
var output = new TagHelperOutput("form",
180+
attributes: new Dictionary<string, string>(),
181+
content: string.Empty);
182+
var context = new TagHelperContext(
183+
allAttributes: new Dictionary<string, object>()
184+
{
185+
{ "aCTiON", "http://www.contoso.com" },
186+
{ "METhod", "POST" }
187+
});
188+
189+
// Act
190+
formTagHelper.Process(context, output);
191+
192+
// Assert
193+
var attribute = Assert.Single(output.Attributes, kvp => kvp.Key.Equals("aCTiON"));
194+
Assert.Equal("http://www.contoso.com", attribute.Value);
195+
attribute = Assert.Single(output.Attributes, kvp => kvp.Key.Equals("METhod"));
196+
Assert.Equal("POST", attribute.Value);
197+
}
198+
199+
[Theory]
200+
[InlineData(true, "<input />")]
201+
[InlineData(false, "")]
202+
[InlineData(null, "")]
203+
public void Process_AllowsAntiForgeryIfActionIsURL(bool? antiForgery, string expectedContent)
204+
{
205+
// Arrange
206+
var expectedViewContext = CreateViewContext();
207+
var generator = new Mock<IHtmlGenerator>();
208+
generator.Setup(mock => mock.GenerateAntiForgery(It.IsAny<ViewContext>()))
209+
.Returns(new TagBuilder("input"));
210+
var formTagHelper = new FormTagHelper
211+
{
212+
Action = "http://www.contoso.com",
213+
AntiForgery = antiForgery,
214+
};
215+
SetViewContextAndGenerator(formTagHelper,
216+
expectedViewContext,
217+
generator.Object);
218+
var output = new TagHelperOutput("form",
219+
attributes: new Dictionary<string, string>(),
220+
content: string.Empty);
221+
var context = new TagHelperContext(
222+
allAttributes: new Dictionary<string, object>()
223+
{
224+
{ "aCTiON", "http://www.contoso.com" }
225+
});
226+
227+
// Act
228+
formTagHelper.Process(context, output);
229+
230+
// Assert
231+
Assert.Equal(expectedContent, output.Content);
232+
}
233+
234+
[Fact]
235+
public void Process_ThrowsIfActionIsUrlWithSpecifiedController()
236+
{
237+
// Arrange
238+
var formTagHelper = new FormTagHelper
239+
{
240+
Action = "http://www.contoso.com",
241+
Controller = "Home",
242+
Method = "POST"
243+
};
244+
var expectedErrorMessage = "Cannot determine an Action for FormTagHelper. A FormTagHelper with a " +
245+
"speicified URL based Action must not have a Controller attribute.";
246+
247+
// Act & Assert
248+
var ex = Assert.Throws<InvalidOperationException>(() =>
249+
{
250+
formTagHelper.Process(context: null, output: null);
251+
});
252+
253+
Assert.Equal(expectedErrorMessage, ex.Message);
254+
}
255+
256+
private ViewContext CreateViewContext()
257+
{
258+
var actionContext = new ActionContext(
259+
new Mock<HttpContext>().Object,
260+
new RouteData(),
261+
new ActionDescriptor());
262+
263+
return new ViewContext(
264+
actionContext,
265+
Mock.Of<IView>(),
266+
new ViewDataDictionary(
267+
new DataAnnotationsModelMetadataProvider()),
268+
new StringWriter());
269+
}
270+
271+
private void SetViewContextAndGenerator(ITagHelper tagHelper,
272+
ViewContext viewContext,
273+
IHtmlGenerator generator)
274+
{
275+
var tagHelperType = tagHelper.GetType();
276+
277+
tagHelperType.GetProperty("ViewContext", BindingFlags.NonPublic | BindingFlags.Instance)
278+
.SetValue(tagHelper, viewContext);
279+
tagHelperType.GetProperty("Generator", BindingFlags.NonPublic | BindingFlags.Instance)
280+
.SetValue(tagHelper, generator);
281+
}
282+
}
283+
}

0 commit comments

Comments
 (0)