File: FormTagHelperTest.cs
Web Access
Project: src\src\Mvc\Mvc.TagHelpers\test\Microsoft.AspNetCore.Mvc.TagHelpers.Test.csproj (Microsoft.AspNetCore.Mvc.TagHelpers.Test)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Text.Encodings.Web;
using Microsoft.AspNetCore.Html;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.Routing;
using Microsoft.AspNetCore.Mvc.ViewEngines;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Razor.TagHelpers;
using Microsoft.AspNetCore.Routing;
using Moq;
 
namespace Microsoft.AspNetCore.Mvc.TagHelpers;
 
public class FormTagHelperTest
{
    [Fact]
    public async Task ProcessAsync_InvokesGeneratePageForm_WithOnlyPageHandler()
    {
        // Arrange
        var viewContext = CreateViewContext();
        var context = new TagHelperContext(
            tagName: "form",
            allAttributes: new TagHelperAttributeList()
            {
                    { "asp-handler", "page-handler" },
                    { "method", "get" }
            },
            items: new Dictionary<object, object>(),
            uniqueId: "test");
        var output = new TagHelperOutput(
            "form",
            attributes: new TagHelperAttributeList(),
            getChildContentAsync: (useCachedResult, encoder) =>
            {
                var tagHelperContent = new DefaultTagHelperContent();
                tagHelperContent.SetContent("Something");
                return Task.FromResult<TagHelperContent>(tagHelperContent);
            });
        var generator = new Mock<IHtmlGenerator>(MockBehavior.Strict);
        generator
            .Setup(mock => mock.GeneratePageForm(
                viewContext,
                null,
                "page-handler",
                null,
                null,
                null,
                null))
            .Returns(new TagBuilder("form"))
            .Verifiable();
        var formTagHelper = new FormTagHelper(generator.Object)
        {
            ViewContext = viewContext,
            PageHandler = "page-handler",
            Method = "get"
        };
 
        // Act & Assert
        await formTagHelper.ProcessAsync(context, output);
        generator.Verify();
    }
 
    [Fact]
    public async Task ProcessAsync_ActionAndControllerGenerateAntiforgery()
    {
        // Arrange
        var expectedTagName = "form";
        var metadataProvider = new TestModelMetadataProvider();
        var tagHelperContext = new TagHelperContext(
            tagName: "form",
            allAttributes: new TagHelperAttributeList()
            {
                    { "asp-action", "index" },
                    { "asp-controller", "home" }
            },
            items: new Dictionary<object, object>(),
            uniqueId: "test");
        var output = new TagHelperOutput(
            expectedTagName,
            attributes: new TagHelperAttributeList(),
            getChildContentAsync: (useCachedResult, encoder) =>
            {
                var tagHelperContent = new DefaultTagHelperContent();
                tagHelperContent.SetContent("Something");
                return Task.FromResult<TagHelperContent>(tagHelperContent);
            });
        var urlHelper = new Mock<IUrlHelper>();
        urlHelper
            .Setup(mock => mock.Action(It.IsAny<UrlActionContext>())).Returns("home/index");
        var htmlGenerator = new TestableHtmlGenerator(metadataProvider, urlHelper.Object);
        var viewContext = TestableHtmlGenerator.GetViewContext(
            model: null,
            htmlGenerator: htmlGenerator,
            metadataProvider: metadataProvider);
        var expectedPostContent = HtmlContentUtilities.HtmlContentToString(
            htmlGenerator.GenerateAntiforgery(viewContext),
            HtmlEncoder.Default);
        var formTagHelper = new FormTagHelper(htmlGenerator)
        {
            ViewContext = viewContext,
            Action = "index",
            Controller = "home",
        };
 
        // Act
        await formTagHelper.ProcessAsync(tagHelperContext, output);
 
        // Assert
        Assert.Equal(2, output.Attributes.Count);
        var attribute = Assert.Single(output.Attributes, attr => attr.Name.Equals("method"));
        Assert.Equal("post", attribute.Value);
        attribute = Assert.Single(output.Attributes, attr => attr.Name.Equals("action"));
        Assert.Equal("home/index", attribute.Value);
        Assert.Empty(output.PreContent.GetContent());
        Assert.True(output.Content.GetContent().Length == 0);
        Assert.Equal(expectedPostContent, output.PostContent.GetContent());
        Assert.Equal(expectedTagName, output.TagName);
    }
 
    [Fact]
    public async Task ProcessAsync_AspAntiforgeryAloneGeneratesProperFormTag()
    {
        // Arrange
        var expectedTagName = "form";
        var metadataProvider = new TestModelMetadataProvider();
        var tagHelperContext = new TagHelperContext(
            tagName: "form",
            allAttributes: new TagHelperAttributeList()
            {
                    { "asp-antiforgery", true }
            },
            items: new Dictionary<object, object>(),
            uniqueId: "test");
        var output = new TagHelperOutput(
            expectedTagName,
            attributes: new TagHelperAttributeList(),
            getChildContentAsync: (useCachedResult, encoder) =>
            {
                var tagHelperContent = new DefaultTagHelperContent();
                tagHelperContent.SetContent("Something");
                return Task.FromResult<TagHelperContent>(tagHelperContent);
            });
        var urlHelper = new Mock<IUrlHelper>();
        var htmlGenerator = new TestableHtmlGenerator(metadataProvider, urlHelper.Object);
        var viewContext = TestableHtmlGenerator.GetViewContext(
            model: null,
            htmlGenerator: htmlGenerator,
            metadataProvider: metadataProvider);
        viewContext.HttpContext.Request.Path = "/home/index";
        var expectedPostContent = HtmlContentUtilities.HtmlContentToString(
            htmlGenerator.GenerateAntiforgery(viewContext),
            HtmlEncoder.Default);
        var formTagHelper = new FormTagHelper(htmlGenerator)
        {
            ViewContext = viewContext,
            Antiforgery = true,
        };
 
        // Act
        await formTagHelper.ProcessAsync(tagHelperContext, output);
 
        // Assert
        Assert.Equal(2, output.Attributes.Count);
        var attribute = Assert.Single(output.Attributes, attr => attr.Name.Equals("method"));
        Assert.Equal("post", attribute.Value);
        attribute = Assert.Single(output.Attributes, attr => attr.Name.Equals("action"));
        Assert.Equal("/home/index", attribute.Value);
        Assert.Empty(output.PreContent.GetContent());
        Assert.True(output.Content.GetContent().Length == 0);
        Assert.Equal(expectedPostContent, output.PostContent.GetContent());
        Assert.Equal(expectedTagName, output.TagName);
    }
 
    [Fact]
    public async Task ProcessAsync_EmptyHtmlStringActionGeneratesAntiforgery()
    {
        // Arrange
        var expectedTagName = "form";
        var metadataProvider = new TestModelMetadataProvider();
        var tagHelperContext = new TagHelperContext(
            tagName: "form",
            allAttributes: new TagHelperAttributeList()
            {
                    { "method", new HtmlString("post") }
            },
            items: new Dictionary<object, object>(),
            uniqueId: "test");
        var output = new TagHelperOutput(
            expectedTagName,
            attributes: new TagHelperAttributeList()
            {
                    { "action", HtmlString.Empty },
            },
            getChildContentAsync: (useCachedResult, encoder) =>
            {
                var tagHelperContent = new DefaultTagHelperContent();
                tagHelperContent.SetContent("Something");
                return Task.FromResult<TagHelperContent>(tagHelperContent);
            });
        var urlHelper = new Mock<IUrlHelper>();
        urlHelper
            .Setup(mock => mock.Action(It.IsAny<UrlActionContext>())).Returns("home/index");
 
        var htmlGenerator = new TestableHtmlGenerator(metadataProvider, urlHelper.Object);
        var viewContext = TestableHtmlGenerator.GetViewContext(
            model: null,
            htmlGenerator: htmlGenerator,
            metadataProvider: metadataProvider);
        var expectedPostContent = HtmlContentUtilities.HtmlContentToString(
            htmlGenerator.GenerateAntiforgery(viewContext),
            HtmlEncoder.Default);
        var formTagHelper = new FormTagHelper(htmlGenerator)
        {
            ViewContext = viewContext,
            Method = "post",
        };
 
        // Act
        await formTagHelper.ProcessAsync(tagHelperContext, output);
 
        // Assert
        var attribute = Assert.Single(output.Attributes, attr => attr.Name.Equals("action"));
        Assert.Equal(HtmlString.Empty, attribute.Value);
        Assert.Empty(output.PreElement.GetContent());
        Assert.Empty(output.PreContent.GetContent());
        Assert.Empty(output.Content.GetContent());
        Assert.Equal(expectedPostContent, output.PostContent.GetContent());
        Assert.Empty(output.PostElement.GetContent());
        Assert.Equal(expectedTagName, output.TagName);
    }
 
    [Fact]
    public async Task ProcessAsync_EmptyStringActionGeneratesAntiforgery()
    {
        // Arrange
        var expectedTagName = "form";
        var metadataProvider = new TestModelMetadataProvider();
        var tagHelperContext = new TagHelperContext(
            tagName: "form",
            allAttributes: new TagHelperAttributeList()
            {
                    { "method", new HtmlString("post") }
            },
            items: new Dictionary<object, object>(),
            uniqueId: "test");
        var output = new TagHelperOutput(
            expectedTagName,
            attributes: new TagHelperAttributeList()
            {
                    { "action", string.Empty },
            },
            getChildContentAsync: (useCachedResult, encoder) =>
            {
                var tagHelperContent = new DefaultTagHelperContent();
                tagHelperContent.SetContent("Something");
                return Task.FromResult<TagHelperContent>(tagHelperContent);
            });
        var urlHelper = new Mock<IUrlHelper>();
        urlHelper
            .Setup(mock => mock.Action(It.IsAny<UrlActionContext>())).Returns("home/index");
 
        var htmlGenerator = new TestableHtmlGenerator(metadataProvider, urlHelper.Object);
        var viewContext = TestableHtmlGenerator.GetViewContext(
            model: null,
            htmlGenerator: htmlGenerator,
            metadataProvider: metadataProvider);
        var expectedPostContent = HtmlContentUtilities.HtmlContentToString(
            htmlGenerator.GenerateAntiforgery(viewContext),
            HtmlEncoder.Default);
        var formTagHelper = new FormTagHelper(htmlGenerator)
        {
            ViewContext = viewContext,
            Method = "post",
        };
 
        // Act
        await formTagHelper.ProcessAsync(tagHelperContext, output);
 
        // Assert
        var attribute = Assert.Single(output.Attributes, attr => attr.Name.Equals("action"));
        Assert.Equal(string.Empty, attribute.Value);
        Assert.Empty(output.PreElement.GetContent());
        Assert.Empty(output.PreContent.GetContent());
        Assert.Empty(output.Content.GetContent());
        Assert.Equal(expectedPostContent, output.PostContent.GetContent());
        Assert.Empty(output.PostElement.GetContent());
        Assert.Equal(expectedTagName, output.TagName);
    }
 
    [Fact]
    public async Task ProcessAsync_GeneratesExpectedOutput()
    {
        // Arrange
        var expectedTagName = "not-form";
        var metadataProvider = new TestModelMetadataProvider();
        var tagHelperContext = new TagHelperContext(
            tagName: "form",
            allAttributes: new TagHelperAttributeList
            {
                    { "id", "myform" },
                    { "asp-route-name", "value" },
                    { "asp-action", "index" },
                    { "asp-controller", "home" },
                    { "method", "post" },
                    { "asp-antiforgery", true }
            },
            items: new Dictionary<object, object>(),
            uniqueId: "test");
        var output = new TagHelperOutput(
            expectedTagName,
            attributes: new TagHelperAttributeList
            {
                    { "id", "myform" },
            },
            getChildContentAsync: (useCachedResult, encoder) =>
            {
                var tagHelperContent = new DefaultTagHelperContent();
                tagHelperContent.SetContent("Something Else");
                return Task.FromResult<TagHelperContent>(tagHelperContent);
            });
        output.PostContent.SetContent("Something");
        var urlHelper = new Mock<IUrlHelper>();
        urlHelper
            .Setup(mock => mock.Action(It.IsAny<UrlActionContext>())).Returns("home/index");
 
        var htmlGenerator = new TestableHtmlGenerator(metadataProvider, urlHelper.Object);
        var viewContext = TestableHtmlGenerator.GetViewContext(
            model: null,
            htmlGenerator: htmlGenerator,
            metadataProvider: metadataProvider);
        var expectedPostContent = "Something" +
            HtmlContentUtilities.HtmlContentToString(
                htmlGenerator.GenerateAntiforgery(viewContext),
                HtmlEncoder.Default);
        var formTagHelper = new FormTagHelper(htmlGenerator)
        {
            Action = "index",
            Antiforgery = true,
            Controller = "home",
            ViewContext = viewContext,
            RouteValues =
                {
                    { "name", "value" },
                },
        };
 
        // Act
        await formTagHelper.ProcessAsync(tagHelperContext, output);
 
        // Assert
        Assert.Equal(3, output.Attributes.Count);
        var attribute = Assert.Single(output.Attributes, attr => attr.Name.Equals("id"));
        Assert.Equal("myform", attribute.Value);
        attribute = Assert.Single(output.Attributes, attr => attr.Name.Equals("method"));
        Assert.Equal("post", attribute.Value);
        attribute = Assert.Single(output.Attributes, attr => attr.Name.Equals("action"));
        Assert.Equal("home/index", attribute.Value);
        Assert.Empty(output.PreContent.GetContent());
        Assert.True(output.Content.GetContent().Length == 0);
        Assert.Equal(expectedPostContent, output.PostContent.GetContent());
        Assert.Equal(expectedTagName, output.TagName);
    }
 
    [Theory]
    [InlineData(null, FormMethod.Post, "<input />")]
    [InlineData(true, FormMethod.Post, "<input />")]
    [InlineData(false, FormMethod.Post, "")]
    [InlineData(null, FormMethod.Get, "")]
    [InlineData(true, FormMethod.Get, "<input />")]
    [InlineData(false, FormMethod.Get, "")]
    public async Task ProcessAsync_GeneratesAntiforgeryCorrectly(
        bool? antiforgery,
        FormMethod method,
        string expectedPostContent)
    {
        // Arrange
        var viewContext = CreateViewContext();
        var expectedAttribute = new TagHelperAttribute("method", method.ToString().ToLowerInvariant());
        var context = new TagHelperContext(
            tagName: "form",
            allAttributes: new TagHelperAttributeList(new[] { expectedAttribute }),
            items: new Dictionary<object, object>(),
            uniqueId: "test");
        var output = new TagHelperOutput(
            "form",
            attributes: new TagHelperAttributeList(),
            getChildContentAsync: (useCachedResult, encoder) =>
            {
                var tagHelperContent = new DefaultTagHelperContent();
                tagHelperContent.SetContent("Something");
                return Task.FromResult<TagHelperContent>(tagHelperContent);
            });
        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 HtmlString("<input />"));
        var formTagHelper = new FormTagHelper(generator.Object)
        {
            Action = "Index",
            Antiforgery = antiforgery,
            ViewContext = viewContext,
            Method = method.ToString().ToLowerInvariant()
        };
 
        // Act
        await formTagHelper.ProcessAsync(context, output);
 
        // Assert
        Assert.Equal("form", output.TagName);
        Assert.Equal(TagMode.StartTagAndEndTag, output.TagMode);
        var attribute = Assert.Single(output.Attributes);
        Assert.Equal(expectedAttribute, attribute);
        Assert.Empty(output.PreContent.GetContent());
        Assert.True(output.Content.GetContent().Length == 0);
        Assert.Equal(expectedPostContent, output.PostContent.GetContent());
    }
 
    [Fact]
    public async Task ProcessAsync_BindsRouteValues()
    {
        // Arrange
        var testViewContext = CreateViewContext();
        var context = new TagHelperContext(
            tagName: "form",
            allAttributes: new TagHelperAttributeList(
                Enumerable.Empty<TagHelperAttribute>()),
            items: new Dictionary<object, object>(),
            uniqueId: "test");
        var expectedAttribute = new TagHelperAttribute("asp-ROUTEE-NotRoute", "something");
        var output = new TagHelperOutput(
            "form",
            attributes: new TagHelperAttributeList(),
            getChildContentAsync: (useCachedResult, encoder) =>
            {
                var tagHelperContent = new DefaultTagHelperContent();
                tagHelperContent.SetContent("Something");
                return Task.FromResult<TagHelperContent>(tagHelperContent);
            });
        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 = Assert.IsType<RouteValueDictionary>(routeValues);
                    Assert.Equal(2, routeValueDictionary.Count);
                    var routeValue = Assert.Single(routeValueDictionary, attr => attr.Key.Equals("val"));
                    Assert.Equal("hello", routeValue.Value);
                    routeValue = Assert.Single(routeValueDictionary, attr => attr.Key.Equals("-Name"));
                    Assert.Equal("Value", routeValue.Value);
                })
            .Returns(new TagBuilder("form"))
            .Verifiable();
        var formTagHelper = new FormTagHelper(generator.Object)
        {
            Action = "Index",
            Antiforgery = false,
            ViewContext = testViewContext,
            RouteValues =
                {
                    { "val", "hello" },
                    { "-Name", "Value" },
                },
        };
 
        // Act & Assert
        await formTagHelper.ProcessAsync(context, output);
 
        Assert.Equal("form", output.TagName);
        Assert.Equal(TagMode.StartTagAndEndTag, output.TagMode);
        var attribute = Assert.Single(output.Attributes);
        Assert.Equal(expectedAttribute, attribute);
        Assert.Empty(output.PreContent.GetContent());
        Assert.True(output.Content.GetContent().Length == 0);
        Assert.Empty(output.PostContent.GetContent());
        generator.Verify();
    }
 
    [Fact]
    public async Task ProcessAsync_CallsIntoGenerateFormWithExpectedParameters()
    {
        // Arrange
        var viewContext = CreateViewContext();
        var context = new TagHelperContext(
            tagName: "form",
            allAttributes: new TagHelperAttributeList(
                Enumerable.Empty<TagHelperAttribute>()),
            items: new Dictionary<object, object>(),
            uniqueId: "test");
        var output = new TagHelperOutput(
            "form",
            attributes: new TagHelperAttributeList(),
            getChildContentAsync: (useCachedResult, encoder) =>
            {
                var tagHelperContent = new DefaultTagHelperContent();
                tagHelperContent.SetContent("Something");
                return Task.FromResult<TagHelperContent>(tagHelperContent);
            });
        var generator = new Mock<IHtmlGenerator>(MockBehavior.Strict);
        generator
            .Setup(mock => mock.GenerateForm(
                viewContext,
                "Index",
                "Home",
                It.IsAny<IDictionary<string, object>>(),
                null,
                null))
            .Returns(new TagBuilder("form"))
            .Verifiable();
        var formTagHelper = new FormTagHelper(generator.Object)
        {
            Action = "Index",
            Antiforgery = false,
            Controller = "Home",
            ViewContext = viewContext,
        };
 
        // Act & Assert
        await formTagHelper.ProcessAsync(context, output);
        generator.Verify();
 
        Assert.Equal("form", output.TagName);
        Assert.Equal(TagMode.StartTagAndEndTag, output.TagMode);
        Assert.Empty(output.Attributes);
        Assert.Empty(output.PreElement.GetContent());
        Assert.Empty(output.PreContent.GetContent());
        Assert.True(output.Content.GetContent().Length == 0);
        Assert.Empty(output.PostContent.GetContent());
    }
 
    [Fact]
    public async Task ProcessAsync_AspFragmentAddsFragmentToAction()
    {
        // Arrange
        var expectedTagName = "form";
        var metadataProvider = new TestModelMetadataProvider();
        var tagHelperContext = new TagHelperContext(
            tagName: "form",
            allAttributes: new TagHelperAttributeList
            {
                    { "id", "myform" },
                    { "asp-route-name", "value" },
                    { "asp-action", "index" },
                    { "asp-controller", "home" },
                    { "asp-fragment", "test" },
                    { "method", "post" },
                    { "asp-antiforgery", true }
            },
            items: new Dictionary<object, object>(),
            uniqueId: "test");
 
        var output = new TagHelperOutput(
            expectedTagName,
            attributes: new TagHelperAttributeList
            {
                    { "id", "myform" },
            },
            getChildContentAsync: (useCachedResult, encoder) =>
            {
                var tagHelperContent = new DefaultTagHelperContent();
                return Task.FromResult<TagHelperContent>(tagHelperContent);
            });
 
        var urlHelper = new Mock<IUrlHelper>();
        urlHelper
            .Setup(mock => mock.Action(It.IsAny<UrlActionContext>())).Returns("home/index");
 
        var htmlGenerator = new TestableHtmlGenerator(metadataProvider, urlHelper.Object);
        var viewContext = TestableHtmlGenerator.GetViewContext(
            model: null,
            htmlGenerator: htmlGenerator,
            metadataProvider: metadataProvider);
 
        var formTagHelper = new FormTagHelper(htmlGenerator)
        {
            Action = "index",
            Antiforgery = true,
            Controller = "home",
            Fragment = "test",
            ViewContext = viewContext,
            RouteValues =
                {
                    { "name", "value" },
                },
        };
 
        // Act
        await formTagHelper.ProcessAsync(tagHelperContext, output);
 
        // Assert
 
        Assert.Equal("form", output.TagName);
        Assert.Equal(TagMode.StartTagAndEndTag, output.TagMode);
        var attribute = Assert.Single(output.Attributes, attr => attr.Name.Equals("action"));
        Assert.Equal("home/index#test", attribute.Value);
    }
 
    [Fact]
    public async Task ProcessAsync_AspAreaAddsAreaToRouteValues()
    {
        // Arrange
        var viewContext = CreateViewContext();
        var context = new TagHelperContext(
            tagName: "form",
            allAttributes: new TagHelperAttributeList(
                Enumerable.Empty<TagHelperAttribute>()),
            items: new Dictionary<object, object>(),
            uniqueId: "test");
        var output = new TagHelperOutput(
            "form",
            attributes: new TagHelperAttributeList(),
            getChildContentAsync: (useCachedResult, encoder) =>
            {
                var tagHelperContent = new DefaultTagHelperContent();
                tagHelperContent.SetContent("Something");
                return Task.FromResult<TagHelperContent>(tagHelperContent);
            });
 
        var expectedRouteValues = new Dictionary<string, object> { { "area", "Admin" } };
        var generator = new Mock<IHtmlGenerator>(MockBehavior.Strict);
        generator
            .Setup(mock => mock.GenerateForm(
                viewContext,
                "Index",
                "Home",
                expectedRouteValues,
                null,
                null))
            .Returns(new TagBuilder("form"))
            .Verifiable();
        var formTagHelper = new FormTagHelper(generator.Object)
        {
            Action = "Index",
            Antiforgery = false,
            Controller = "Home",
            Area = "Admin",
            ViewContext = viewContext,
        };
 
        // Act
        await formTagHelper.ProcessAsync(context, output);
 
        // Assert
        generator.Verify();
 
        Assert.Equal("form", output.TagName);
        Assert.Equal(TagMode.StartTagAndEndTag, output.TagMode);
        Assert.Empty(output.Attributes);
        Assert.Empty(output.PreElement.GetContent());
        Assert.Empty(output.PreContent.GetContent());
        Assert.True(output.Content.GetContent().Length == 0);
        Assert.Empty(output.PostContent.GetContent());
    }
 
    [Fact]
    public async Task ProcessAsync_EmptyStringOnAspAreaIsPassedThroughToRouteValues()
    {
        // Arrange
        var viewContext = CreateViewContext();
        var context = new TagHelperContext(
            tagName: "form",
            allAttributes: new TagHelperAttributeList(
                Enumerable.Empty<TagHelperAttribute>()),
            items: new Dictionary<object, object>(),
            uniqueId: "test");
        var output = new TagHelperOutput(
            "form",
            attributes: new TagHelperAttributeList(),
            getChildContentAsync: (useCachedResult, encoder) =>
            {
                var tagHelperContent = new DefaultTagHelperContent();
                tagHelperContent.SetContent("Something");
                return Task.FromResult<TagHelperContent>(tagHelperContent);
            });
 
        var expectedRouteValues = new Dictionary<string, object> { { "area", string.Empty } };
        var generator = new Mock<IHtmlGenerator>(MockBehavior.Strict);
        generator
            .Setup(mock => mock.GenerateForm(
                viewContext,
                "Index",
                "Home",
                expectedRouteValues,
                null,
                null))
            .Returns(new TagBuilder("form"))
            .Verifiable();
        var formTagHelper = new FormTagHelper(generator.Object)
        {
            Action = "Index",
            Antiforgery = false,
            Controller = "Home",
            Area = string.Empty,
            ViewContext = viewContext,
        };
 
        // Act
        await formTagHelper.ProcessAsync(context, output);
 
        // Assert
        generator.Verify();
 
        Assert.Equal("form", output.TagName);
        Assert.Equal(TagMode.StartTagAndEndTag, output.TagMode);
        Assert.Empty(output.Attributes);
        Assert.Empty(output.PreElement.GetContent());
        Assert.Empty(output.PreContent.GetContent());
        Assert.True(output.Content.GetContent().Length == 0);
        Assert.Empty(output.PostContent.GetContent());
    }
 
    [Fact]
    public async Task ProcessAsync_AspAreaOverridesAspRouteArea()
    {
        // Arrange
        var viewContext = CreateViewContext();
        var context = new TagHelperContext(
            tagName: "form",
            allAttributes: new TagHelperAttributeList(
                Enumerable.Empty<TagHelperAttribute>()),
            items: new Dictionary<object, object>(),
            uniqueId: "test");
        var output = new TagHelperOutput(
            "form",
            attributes: new TagHelperAttributeList(),
            getChildContentAsync: (useCachedResult, encoder) =>
            {
                var tagHelperContent = new DefaultTagHelperContent();
                tagHelperContent.SetContent("Something");
                return Task.FromResult<TagHelperContent>(tagHelperContent);
            });
 
        var expectedRouteValues = new Dictionary<string, object> { { "area", "Admin" } };
        var generator = new Mock<IHtmlGenerator>(MockBehavior.Strict);
        generator
            .Setup(mock => mock.GenerateForm(
                viewContext,
                "Index",
                "Home",
                expectedRouteValues,
                null,
                null))
            .Returns(new TagBuilder("form"))
            .Verifiable();
        var formTagHelper = new FormTagHelper(generator.Object)
        {
            Action = "Index",
            Antiforgery = false,
            Controller = "Home",
            Area = "Admin",
            RouteValues = new Dictionary<string, string> { { "area", "Client" } },
            ViewContext = viewContext,
        };
 
        // Act
        await formTagHelper.ProcessAsync(context, output);
 
        // Assert
        generator.Verify();
 
        Assert.Equal("form", output.TagName);
        Assert.Equal(TagMode.StartTagAndEndTag, output.TagMode);
        Assert.Empty(output.Attributes);
        Assert.Empty(output.PreElement.GetContent());
        Assert.Empty(output.PreContent.GetContent());
        Assert.True(output.Content.GetContent().Length == 0);
        Assert.Empty(output.PostContent.GetContent());
    }
 
    [Fact]
    public async Task ProcessAsync_CallsIntoGenerateRouteFormWithExpectedParameters()
    {
        // Arrange
        var viewContext = CreateViewContext();
        var context = new TagHelperContext(
            tagName: "form",
            allAttributes: new TagHelperAttributeList(
                Enumerable.Empty<TagHelperAttribute>()),
            items: new Dictionary<object, object>(),
            uniqueId: "test");
        var output = new TagHelperOutput(
            "form",
            attributes: new TagHelperAttributeList(),
            getChildContentAsync: (useCachedResult, encoder) =>
            {
                var tagHelperContent = new DefaultTagHelperContent();
                tagHelperContent.SetContent("Something");
                return Task.FromResult<TagHelperContent>(tagHelperContent);
            });
        var generator = new Mock<IHtmlGenerator>(MockBehavior.Strict);
        generator
            .Setup(mock => mock.GenerateRouteForm(
                viewContext,
                "Default",
                It.Is<RouteValueDictionary>(m => string.Equals(m["name"], "value")),
                null,
                null))
            .Returns(new TagBuilder("form"))
            .Verifiable();
        var formTagHelper = new FormTagHelper(generator.Object)
        {
            Antiforgery = false,
            Route = "Default",
            ViewContext = viewContext,
            RouteValues =
                {
                    { "name", "value" },
                },
        };
 
        // Act & Assert
        await formTagHelper.ProcessAsync(context, output);
        generator.Verify();
 
        Assert.Equal("form", output.TagName);
        Assert.Equal(TagMode.StartTagAndEndTag, output.TagMode);
        Assert.Empty(output.Attributes);
        Assert.Empty(output.PreElement.GetContent());
        Assert.Empty(output.PreContent.GetContent());
        Assert.True(output.Content.GetContent().Length == 0);
        Assert.Empty(output.PostContent.GetContent());
        Assert.Empty(output.PostElement.GetContent());
    }
 
    [Fact]
    public async Task ProcessAsync_InvokesGeneratePageForm()
    {
        // Arrange
        var viewContext = CreateViewContext();
        var context = new TagHelperContext(
            tagName: "form",
            allAttributes: new TagHelperAttributeList(
                Enumerable.Empty<TagHelperAttribute>()),
            items: new Dictionary<object, object>(),
            uniqueId: "test");
        var output = new TagHelperOutput(
            "form",
            attributes: new TagHelperAttributeList(),
            getChildContentAsync: (useCachedResult, encoder) =>
            {
                var tagHelperContent = new DefaultTagHelperContent();
                tagHelperContent.SetContent("Something");
                return Task.FromResult<TagHelperContent>(tagHelperContent);
            });
        var generator = new Mock<IHtmlGenerator>(MockBehavior.Strict);
        generator
            .Setup(mock => mock.GeneratePageForm(
                viewContext,
                "/Home/Admin/Post",
                "page-handler",
                null,
                "hello-world",
                null,
                null))
            .Returns(new TagBuilder("form"))
            .Verifiable();
        var formTagHelper = new FormTagHelper(generator.Object)
        {
            Antiforgery = false,
            ViewContext = viewContext,
            Page = "/Home/Admin/Post",
            PageHandler = "page-handler",
            Fragment = "hello-world",
        };
 
        // Act & Assert
        await formTagHelper.ProcessAsync(context, output);
        generator.Verify();
    }
 
    [Fact]
    public async Task ProcessAsync_WithPageAndArea_InvokesGeneratePageForm()
    {
        // Arrange
        var viewContext = CreateViewContext();
        var context = new TagHelperContext(
            tagName: "form",
            allAttributes: new TagHelperAttributeList(
                Enumerable.Empty<TagHelperAttribute>()),
            items: new Dictionary<object, object>(),
            uniqueId: "test");
        var output = new TagHelperOutput(
            "form",
            attributes: new TagHelperAttributeList(),
            getChildContentAsync: (useCachedResult, encoder) =>
            {
                var tagHelperContent = new DefaultTagHelperContent();
                tagHelperContent.SetContent("Something");
                return Task.FromResult<TagHelperContent>(tagHelperContent);
            });
        var generator = new Mock<IHtmlGenerator>(MockBehavior.Strict);
        generator
            .Setup(mock => mock.GeneratePageForm(
                viewContext,
                "/Home/Admin/Post",
                "page-handler",
                It.IsAny<object>(),
                "hello-world",
                null,
                null))
            .Callback((ViewContext _, string pageName, string pageHandler, object routeValues, string fragment, string method, object htmlAttributes) =>
            {
                var rvd = Assert.IsType<RouteValueDictionary>(routeValues);
                Assert.Collection(
                    rvd.OrderBy(item => item.Key),
                    item =>
                    {
                        Assert.Equal("area", item.Key);
                        Assert.Equal("test-area", item.Value);
                    });
            })
            .Returns(new TagBuilder("form"))
            .Verifiable();
        var formTagHelper = new FormTagHelper(generator.Object)
        {
            Antiforgery = false,
            ViewContext = viewContext,
            Page = "/Home/Admin/Post",
            PageHandler = "page-handler",
            Fragment = "hello-world",
            Area = "test-area",
        };
 
        // Act & Assert
        await formTagHelper.ProcessAsync(context, output);
        generator.Verify();
    }
 
    [Theory]
    [InlineData(true, "<input />")]
    [InlineData(false, "")]
    [InlineData(null, "")]
    public async Task ProcessAsync_SupportsAntiforgeryIfActionIsSpecified(
        bool? antiforgery,
        string expectedPostContent)
    {
        // Arrange
        var viewContext = CreateViewContext();
        var generator = new Mock<IHtmlGenerator>();
 
        generator.Setup(mock => mock.GenerateAntiforgery(It.IsAny<ViewContext>()))
                 .Returns(new HtmlString("<input />"));
        var formTagHelper = new FormTagHelper(generator.Object)
        {
            Antiforgery = antiforgery,
            ViewContext = viewContext,
        };
 
        var output = new TagHelperOutput(
            tagName: "form",
            attributes: new TagHelperAttributeList
            {
                    { "aCTiON", "my-action" },
            },
            getChildContentAsync: (useCachedResult, encoder) =>
            {
                var tagHelperContent = new DefaultTagHelperContent();
                tagHelperContent.SetContent("Something");
                return Task.FromResult<TagHelperContent>(tagHelperContent);
            });
        var context = new TagHelperContext(
            tagName: "form",
            allAttributes: new TagHelperAttributeList(
                Enumerable.Empty<TagHelperAttribute>()),
            items: new Dictionary<object, object>(),
            uniqueId: "test");
 
        // Act
        await formTagHelper.ProcessAsync(context, output);
 
        // Assert
        Assert.Equal("form", output.TagName);
        Assert.Equal(TagMode.StartTagAndEndTag, output.TagMode);
        var attribute = Assert.Single(output.Attributes);
        Assert.Equal(new TagHelperAttribute("aCTiON", "my-action"), attribute);
        Assert.Empty(output.PreContent.GetContent());
        Assert.True(output.Content.GetContent().Length == 0);
        Assert.Equal(expectedPostContent, output.PostContent.GetContent());
    }
 
    [Theory]
    [InlineData("Action")]
    [InlineData("Controller")]
    [InlineData("Page")]
    [InlineData("PageHandler")]
    [InlineData("asp-route-")]
    public async Task ProcessAsync_ThrowsIfActionConflictsWithBoundAttributes(string propertyName)
    {
        // Arrange
        var formTagHelper = new FormTagHelper(new TestableHtmlGenerator(new EmptyModelMetadataProvider()));
        var tagHelperOutput = new TagHelperOutput(
            "form",
            attributes: new TagHelperAttributeList
            {
                    { "action", "my-action" },
            },
            getChildContentAsync: (useCachedResult, encoder) => Task.FromResult<TagHelperContent>(null));
        if (propertyName == "asp-route-")
        {
            formTagHelper.RouteValues.Add("name", "value");
        }
        else
        {
            typeof(FormTagHelper).GetProperty(propertyName).SetValue(formTagHelper, "Home");
        }
 
        var expectedErrorMessage = "Cannot override the 'action' attribute for <form>. A <form> with a specified " +
            "'action' must not have attributes starting with 'asp-route-' or an " +
            "'asp-action', 'asp-controller', 'asp-fragment', 'asp-area', 'asp-route', 'asp-page' or 'asp-page-handler' attribute.";
 
        var context = new TagHelperContext(
            tagName: "form",
            allAttributes: new TagHelperAttributeList(
                Enumerable.Empty<TagHelperAttribute>()),
            items: new Dictionary<object, object>(),
            uniqueId: "test");
 
        // Act & Assert
        var ex = await Assert.ThrowsAsync<InvalidOperationException>(
            () => formTagHelper.ProcessAsync(context, tagHelperOutput));
 
        Assert.Equal(expectedErrorMessage, ex.Message);
    }
 
    [Theory]
    [InlineData("Action")]
    [InlineData("Controller")]
    public async Task ProcessAsync_ThrowsIfRouteAndActionOrControllerProvided(string propertyName)
    {
        // Arrange
        var formTagHelper = new FormTagHelper(new TestableHtmlGenerator(new EmptyModelMetadataProvider()))
        {
            Route = "Default",
        };
        typeof(FormTagHelper).GetProperty(propertyName).SetValue(formTagHelper, "Home");
        var output = new TagHelperOutput(
            "form",
            attributes: new TagHelperAttributeList(),
            getChildContentAsync: (useCachedResult, encoder) => Task.FromResult<TagHelperContent>(null));
        var expectedErrorMessage = string.Join(
            Environment.NewLine,
            "Cannot determine the 'action' attribute for <form>. The following attributes are mutually exclusive:",
            "asp-route",
            "asp-controller, asp-action",
            "asp-page");
 
        var context = new TagHelperContext(
            tagName: "form",
            allAttributes: new TagHelperAttributeList(
                Enumerable.Empty<TagHelperAttribute>()),
            items: new Dictionary<object, object>(),
            uniqueId: "test");
 
        // Act & Assert
        var ex = await Assert.ThrowsAsync<InvalidOperationException>(
            () => formTagHelper.ProcessAsync(context, output));
 
        Assert.Equal(expectedErrorMessage, ex.Message);
    }
 
    [Fact]
    public async Task ProcessAsync_ThrowsIfRouteAndPageProvided()
    {
        // Arrange
        var formTagHelper = new FormTagHelper(new TestableHtmlGenerator(new EmptyModelMetadataProvider()))
        {
            Route = "Default",
            Page = "Page",
        };
        var output = new TagHelperOutput(
            "form",
            attributes: new TagHelperAttributeList(),
            getChildContentAsync: (useCachedResult, encoder) => Task.FromResult<TagHelperContent>(null));
        var expectedErrorMessage = string.Join(
            Environment.NewLine,
            "Cannot determine the 'action' attribute for <form>. The following attributes are mutually exclusive:",
            "asp-route",
            "asp-controller, asp-action",
            "asp-page");
 
        var context = new TagHelperContext(
            tagName: "form",
            allAttributes: new TagHelperAttributeList(
                Enumerable.Empty<TagHelperAttribute>()),
            items: new Dictionary<object, object>(),
            uniqueId: "test");
 
        // Act & Assert
        var ex = await Assert.ThrowsAsync<InvalidOperationException>(
            () => formTagHelper.ProcessAsync(context, output));
 
        Assert.Equal(expectedErrorMessage, ex.Message);
    }
 
    [Fact]
    public async Task ProcessAsync_ThrowsIfActionAndPageProvided()
    {
        // Arrange
        var formTagHelper = new FormTagHelper(new TestableHtmlGenerator(new EmptyModelMetadataProvider()))
        {
            Action = "Default",
            Page = "Page",
        };
        var output = new TagHelperOutput(
            "form",
            attributes: new TagHelperAttributeList(),
            getChildContentAsync: (useCachedResult, encoder) => Task.FromResult<TagHelperContent>(null));
        var expectedErrorMessage = string.Join(
            Environment.NewLine,
            "Cannot determine the 'action' attribute for <form>. The following attributes are mutually exclusive:",
            "asp-route",
            "asp-controller, asp-action",
            "asp-page");
 
        var context = new TagHelperContext(
            tagName: "form",
            allAttributes: new TagHelperAttributeList(
                Enumerable.Empty<TagHelperAttribute>()),
            items: new Dictionary<object, object>(),
            uniqueId: "test");
 
        // Act & Assert
        var ex = await Assert.ThrowsAsync<InvalidOperationException>(
            () => formTagHelper.ProcessAsync(context, output));
 
        Assert.Equal(expectedErrorMessage, ex.Message);
    }
 
    private static ViewContext CreateViewContext()
    {
        var actionContext = new ActionContext(
            new DefaultHttpContext(),
            new RouteData(),
            new ActionDescriptor());
 
        return new ViewContext(
            actionContext,
            Mock.Of<IView>(),
            new ViewDataDictionary(new TestModelMetadataProvider(), new ModelStateDictionary()),
            Mock.Of<ITempDataDictionary>(),
            TextWriter.Null,
            new HtmlHelperOptions());
    }
}