File: AnchorTagHelperTest.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 Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.Routing;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Razor.TagHelpers;
using Microsoft.AspNetCore.Routing;
using Moq;
 
namespace Microsoft.AspNetCore.Mvc.TagHelpers;
 
public class AnchorTagHelperTest
{
    [Fact]
    public async Task ProcessAsync_GeneratesExpectedOutput()
    {
        // Arrange
        var expectedTagName = "not-a";
        var metadataProvider = new TestModelMetadataProvider();
 
        var tagHelperContext = new TagHelperContext(
            tagName: "not-a",
            allAttributes: new TagHelperAttributeList
            {
                    { "id", "myanchor" },
                    { "asp-route-name", "value" },
                    { "asp-action", "index" },
                    { "asp-controller", "home" },
                    { "asp-fragment", "hello=world" },
                    { "asp-host", "contoso.com" },
                    { "asp-protocol", "http" }
            },
            items: new Dictionary<object, object>(),
            uniqueId: "test");
        var output = new TagHelperOutput(
            expectedTagName,
            attributes: new TagHelperAttributeList
            {
                    { "id", "myanchor" },
            },
            getChildContentAsync: (useCachedResult, encoder) =>
            {
                var tagHelperContent = new DefaultTagHelperContent();
                tagHelperContent.SetContent("Something Else");
                return Task.FromResult<TagHelperContent>(tagHelperContent);
            });
        output.Content.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 anchorTagHelper = new AnchorTagHelper(htmlGenerator)
        {
            Action = "index",
            Controller = "home",
            Fragment = "hello=world",
            Host = "contoso.com",
            Protocol = "http",
            RouteValues =
                {
                    {  "name", "value" },
                },
            ViewContext = viewContext,
        };
 
        // Act
        await anchorTagHelper.ProcessAsync(tagHelperContext, output);
 
        // Assert
        Assert.Equal(2, output.Attributes.Count);
        var attribute = Assert.Single(output.Attributes, attr => attr.Name.Equals("id"));
        Assert.Equal("myanchor", attribute.Value);
        attribute = Assert.Single(output.Attributes, attr => attr.Name.Equals("href"));
        Assert.Equal("home/index", attribute.Value);
        Assert.Equal("Something", output.Content.GetContent());
        Assert.Equal(expectedTagName, output.TagName);
    }
 
    [Fact]
    public async Task ProcessAsync_CallsIntoRouteLinkWithExpectedParameters()
    {
        // Arrange
        var context = new TagHelperContext(
            tagName: "a",
            allAttributes: new TagHelperAttributeList(
                Enumerable.Empty<TagHelperAttribute>()),
            items: new Dictionary<object, object>(),
            uniqueId: "test");
        var output = new TagHelperOutput(
            "a",
            attributes: new TagHelperAttributeList(),
            getChildContentAsync: (useCachedResult, encoder) =>
            {
                var tagHelperContent = new DefaultTagHelperContent();
                tagHelperContent.SetContent("Something");
                return Task.FromResult<TagHelperContent>(tagHelperContent);
            });
        output.Content.SetContent(string.Empty);
 
        var generator = new Mock<IHtmlGenerator>(MockBehavior.Strict);
        generator
            .Setup(mock => mock.GenerateRouteLink(
                It.IsAny<ViewContext>(),
                string.Empty,
                "Default",
                "http",
                "contoso.com",
                "hello=world",
                It.IsAny<IDictionary<string, object>>(),
                null))
            .Returns(new TagBuilder("a"))
            .Verifiable();
        var anchorTagHelper = new AnchorTagHelper(generator.Object)
        {
            Fragment = "hello=world",
            Host = "contoso.com",
            Protocol = "http",
            Route = "Default",
        };
 
        // Act & Assert
        await anchorTagHelper.ProcessAsync(context, output);
        generator.Verify();
        Assert.Equal("a", output.TagName);
        Assert.Empty(output.Attributes);
        Assert.Empty(output.Content.GetContent());
    }
 
    [Fact]
    public async Task ProcessAsync_CallsIntoActionLinkWithExpectedParameters()
    {
        // Arrange
        var context = new TagHelperContext(
            tagName: "a",
            allAttributes: new TagHelperAttributeList(
                Enumerable.Empty<TagHelperAttribute>()),
            items: new Dictionary<object, object>(),
            uniqueId: "test");
        var output = new TagHelperOutput(
            "a",
            attributes: new TagHelperAttributeList(),
            getChildContentAsync: (useCachedResult, encoder) =>
            {
                var tagHelperContent = new DefaultTagHelperContent();
                tagHelperContent.SetContent("Something");
                return Task.FromResult<TagHelperContent>(tagHelperContent);
            });
        output.Content.SetContent(string.Empty);
 
        var generator = new Mock<IHtmlGenerator>();
        generator
            .Setup(mock => mock.GenerateActionLink(
                It.IsAny<ViewContext>(),
                string.Empty,
                "Index",
                "Home",
                "http",
                "contoso.com",
                "hello=world",
                It.IsAny<IDictionary<string, object>>(),
                null))
            .Returns(new TagBuilder("a"))
            .Verifiable();
        var anchorTagHelper = new AnchorTagHelper(generator.Object)
        {
            Action = "Index",
            Controller = "Home",
            Fragment = "hello=world",
            Host = "contoso.com",
            Protocol = "http",
        };
 
        // Act & Assert
        await anchorTagHelper.ProcessAsync(context, output);
        generator.Verify();
        Assert.Equal("a", output.TagName);
        Assert.Empty(output.Attributes);
        Assert.Empty(output.Content.GetContent());
    }
 
    [Fact]
    public async Task ProcessAsync_AddsAreaToRouteValuesAndCallsIntoActionLinkWithExpectedParameters()
    {
        // Arrange
        var context = new TagHelperContext(
            tagName: "a",
            allAttributes: new TagHelperAttributeList(
                Enumerable.Empty<TagHelperAttribute>()),
            items: new Dictionary<object, object>(),
            uniqueId: "test");
        var output = new TagHelperOutput(
            "a",
            attributes: new TagHelperAttributeList(),
            getChildContentAsync: (useCachedResult, encoder) =>
            {
                var tagHelperContent = new DefaultTagHelperContent();
                tagHelperContent.SetContent("Something");
                return Task.FromResult<TagHelperContent>(tagHelperContent);
            });
        output.Content.SetContent(string.Empty);
 
        var generator = new Mock<IHtmlGenerator>();
        var expectedRouteValues = new Dictionary<string, object> { { "area", "Admin" } };
 
        generator
            .Setup(mock => mock.GenerateActionLink(
                It.IsAny<ViewContext>(),
                string.Empty,
                "Index",
                "Home",
                "http",
                "contoso.com",
                "hello=world",
                expectedRouteValues,
                null))
            .Returns(new TagBuilder("a"))
            .Verifiable();
        var anchorTagHelper = new AnchorTagHelper(generator.Object)
        {
            Action = "Index",
            Controller = "Home",
            Area = "Admin",
            Fragment = "hello=world",
            Host = "contoso.com",
            Protocol = "http",
        };
 
        // Act
        await anchorTagHelper.ProcessAsync(context, output);
 
        // Assert
        generator.Verify();
 
        Assert.Equal("a", output.TagName);
        Assert.Empty(output.Attributes);
        Assert.Empty(output.Content.GetContent());
    }
 
    [Fact]
    public async Task ProcessAsync_AspAreaOverridesAspRouteArea()
    {
        // Arrange
        var context = new TagHelperContext(
            tagName: "a",
            allAttributes: new TagHelperAttributeList(
                Enumerable.Empty<TagHelperAttribute>()),
            items: new Dictionary<object, object>(),
            uniqueId: "test");
        var output = new TagHelperOutput(
            "a",
            attributes: new TagHelperAttributeList(),
            getChildContentAsync: (useCachedResult, encoder) =>
            {
                var tagHelperContent = new DefaultTagHelperContent();
                tagHelperContent.SetContent("Something");
                return Task.FromResult<TagHelperContent>(tagHelperContent);
            });
        output.Content.SetContent(string.Empty);
 
        var generator = new Mock<IHtmlGenerator>();
        var expectedRouteValues = new Dictionary<string, object> { { "area", "Admin" } };
 
        generator
            .Setup(mock => mock.GenerateActionLink(
                It.IsAny<ViewContext>(),
                string.Empty,
                "Index",
                "Home",
                "http",
                "contoso.com",
                "hello=world",
                expectedRouteValues,
                null))
            .Returns(new TagBuilder("a"))
            .Verifiable();
        var anchorTagHelper = new AnchorTagHelper(generator.Object)
        {
            Action = "Index",
            Controller = "Home",
            Area = "Admin",
            Fragment = "hello=world",
            Host = "contoso.com",
            Protocol = "http",
            RouteValues = new Dictionary<string, string> { { "area", "Home" } }
        };
 
        // Act
        await anchorTagHelper.ProcessAsync(context, output);
 
        // Assert
        generator.Verify();
 
        Assert.Equal("a", output.TagName);
        Assert.Empty(output.Attributes);
        Assert.Empty(output.Content.GetContent());
    }
 
    [Fact]
    public async Task ProcessAsync_EmptyStringOnAspAreaIsPassedThroughToRouteValues()
    {
        // Arrange
        var context = new TagHelperContext(
            tagName: "a",
            allAttributes: new TagHelperAttributeList(
                Enumerable.Empty<TagHelperAttribute>()),
            items: new Dictionary<object, object>(),
            uniqueId: "test");
        var output = new TagHelperOutput(
            "a",
            attributes: new TagHelperAttributeList(),
            getChildContentAsync: (useCachedResult, encoder) =>
            {
                var tagHelperContent = new DefaultTagHelperContent();
                tagHelperContent.SetContent("Something");
                return Task.FromResult<TagHelperContent>(tagHelperContent);
            });
        output.Content.SetContent(string.Empty);
 
        var generator = new Mock<IHtmlGenerator>();
        var expectedRouteValues = new Dictionary<string, object> { { "area", string.Empty } };
 
        generator
            .Setup(mock => mock.GenerateActionLink(
                It.IsAny<ViewContext>(),
                string.Empty,
                "Index",
                "Home",
                "http",
                "contoso.com",
                "hello=world",
                expectedRouteValues,
                null))
            .Returns(new TagBuilder("a"))
            .Verifiable();
        var anchorTagHelper = new AnchorTagHelper(generator.Object)
        {
            Action = "Index",
            Controller = "Home",
            Area = string.Empty,
            Fragment = "hello=world",
            Host = "contoso.com",
            Protocol = "http"
        };
 
        // Act
        await anchorTagHelper.ProcessAsync(context, output);
 
        // Assert
        generator.Verify();
 
        Assert.Equal("a", output.TagName);
        Assert.Empty(output.Attributes);
        Assert.Empty(output.Content.GetContent());
    }
 
    [Fact]
    public async Task ProcessAsync_AddsPageToRouteValuesAndCallsPageLinkWithExpectedParameters()
    {
        // Arrange
        var context = new TagHelperContext(
            tagName: "a",
            allAttributes: new TagHelperAttributeList(
                Enumerable.Empty<TagHelperAttribute>()),
            items: new Dictionary<object, object>(),
            uniqueId: "test");
        var output = new TagHelperOutput(
            "a",
            attributes: new TagHelperAttributeList(),
            getChildContentAsync: (useCachedResult, encoder) =>
            {
                var tagHelperContent = new DefaultTagHelperContent();
                tagHelperContent.SetContent("Something");
                return Task.FromResult<TagHelperContent>(tagHelperContent);
            });
        output.Content.SetContent(string.Empty);
 
        var generator = new Mock<IHtmlGenerator>();
        generator
            .Setup(mock => mock.GeneratePageLink(
                It.IsAny<ViewContext>(),
                string.Empty,
                "/User/Home/Index",
                "page-handler",
                "http",
                "contoso.com",
                "hello=world",
                It.IsAny<object>(),
                null))
            .Returns(new TagBuilder("a"))
            .Verifiable();
        var anchorTagHelper = new AnchorTagHelper(generator.Object)
        {
            Page = "/User/Home/Index",
            PageHandler = "page-handler",
            Fragment = "hello=world",
            Host = "contoso.com",
            Protocol = "http",
        };
 
        // Act
        await anchorTagHelper.ProcessAsync(context, output);
 
        // Assert
        generator.Verify();
    }
 
    [Fact]
    public async Task ProcessAsync_AddsAreaToRouteValuesAndCallsPageLinkWithExpectedParameters()
    {
        // Arrange
        var context = new TagHelperContext(
            tagName: "a",
            allAttributes: new TagHelperAttributeList(
                Enumerable.Empty<TagHelperAttribute>()),
            items: new Dictionary<object, object>(),
            uniqueId: "test");
        var output = new TagHelperOutput(
            "a",
            attributes: new TagHelperAttributeList(),
            getChildContentAsync: (useCachedResult, encoder) =>
            {
                var tagHelperContent = new DefaultTagHelperContent();
                tagHelperContent.SetContent("Something");
                return Task.FromResult<TagHelperContent>(tagHelperContent);
            });
        output.Content.SetContent(string.Empty);
 
        var generator = new Mock<IHtmlGenerator>();
        generator
            .Setup(mock => mock.GeneratePageLink(
                It.IsAny<ViewContext>(),
                string.Empty,
                "/User/Home/Index",
                "page-handler",
                "http",
                "contoso.com",
                "hello=world",
                It.IsAny<object>(),
                null))
            .Callback((ViewContext v, string linkText, string pageName, string pageHandler, string protocol, string hostname, string fragment, object routeValues, 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("a"))
            .Verifiable();
        var anchorTagHelper = new AnchorTagHelper(generator.Object)
        {
            Page = "/User/Home/Index",
            Area = "test-area",
            PageHandler = "page-handler",
            Fragment = "hello=world",
            Host = "contoso.com",
            Protocol = "http",
        };
 
        // Act
        await anchorTagHelper.ProcessAsync(context, output);
 
        // Assert
        generator.Verify();
    }
 
    [Theory]
    [InlineData("Action")]
    [InlineData("Controller")]
    [InlineData("Route")]
    [InlineData("Protocol")]
    [InlineData("Host")]
    [InlineData("Fragment")]
    [InlineData("asp-route-")]
    [InlineData("Page")]
    [InlineData("PageHandler")]
    public async Task ProcessAsync_ThrowsIfHrefConflictsWithBoundAttributes(string propertyName)
    {
        // Arrange
        var metadataProvider = new EmptyModelMetadataProvider();
        var htmlGenerator = new TestableHtmlGenerator(metadataProvider);
 
        var anchorTagHelper = new AnchorTagHelper(htmlGenerator);
 
        var output = new TagHelperOutput(
            "a",
            attributes: new TagHelperAttributeList
            {
                    { "href", "http://www.contoso.com" }
            },
            getChildContentAsync: (useCachedResult, encoder) => Task.FromResult<TagHelperContent>(null));
        if (propertyName == "asp-route-")
        {
            anchorTagHelper.RouteValues.Add("name", "value");
        }
        else
        {
            typeof(AnchorTagHelper).GetProperty(propertyName).SetValue(anchorTagHelper, "Home");
        }
 
        var expectedErrorMessage = "Cannot override the 'href' attribute for <a>. An <a> with a specified " +
            "'href' must not have attributes starting with 'asp-route-' or an " +
            "'asp-action', 'asp-controller', 'asp-area', 'asp-route', 'asp-protocol', 'asp-host', " +
            "'asp-fragment', 'asp-page' or 'asp-page-handler' attribute.";
 
        var context = new TagHelperContext(
            tagName: "test",
            allAttributes: new TagHelperAttributeList(
                Enumerable.Empty<TagHelperAttribute>()),
            items: new Dictionary<object, object>(),
            uniqueId: "test");
 
        // Act & Assert
        var ex = await Assert.ThrowsAsync<InvalidOperationException>(
            () => anchorTagHelper.ProcessAsync(context, output));
 
        Assert.Equal(expectedErrorMessage, ex.Message);
    }
 
    [Theory]
    [InlineData("Action")]
    [InlineData("Controller")]
    public async Task ProcessAsync_ThrowsIfRouteAndActionOrControllerProvided(string propertyName)
    {
        // Arrange
        var metadataProvider = new EmptyModelMetadataProvider();
        var htmlGenerator = new TestableHtmlGenerator(metadataProvider);
 
        var anchorTagHelper = new AnchorTagHelper(htmlGenerator)
        {
            Route = "Default",
        };
 
        typeof(AnchorTagHelper).GetProperty(propertyName).SetValue(anchorTagHelper, "Home");
        var output = new TagHelperOutput(
            "a",
            attributes: new TagHelperAttributeList(),
            getChildContentAsync: (useCachedResult, encoder) => Task.FromResult<TagHelperContent>(null));
        var expectedErrorMessage = string.Join(
            Environment.NewLine,
            "Cannot determine the 'href' attribute for <a>. The following attributes are mutually exclusive:",
            "asp-route",
            "asp-controller, asp-action",
            "asp-page, asp-page-handler");
 
        var context = new TagHelperContext(
            tagName: "test",
            allAttributes: new TagHelperAttributeList(
                Enumerable.Empty<TagHelperAttribute>()),
            items: new Dictionary<object, object>(),
            uniqueId: "test");
 
        // Act & Assert
        var ex = await Assert.ThrowsAsync<InvalidOperationException>(
            () => anchorTagHelper.ProcessAsync(context, output));
 
        Assert.Equal(expectedErrorMessage, ex.Message);
    }
 
    [Fact]
    public async Task ProcessAsync_ThrowsIfRouteAndPageProvided()
    {
        // Arrange
        var metadataProvider = new EmptyModelMetadataProvider();
        var htmlGenerator = new TestableHtmlGenerator(metadataProvider);
 
        var anchorTagHelper = new AnchorTagHelper(htmlGenerator)
        {
            Route = "Default",
            Page = "Page",
        };
 
        var output = new TagHelperOutput(
            "a",
            attributes: new TagHelperAttributeList(),
            getChildContentAsync: (useCachedResult, encoder) => Task.FromResult<TagHelperContent>(null));
        var expectedErrorMessage = string.Join(
            Environment.NewLine,
            "Cannot determine the 'href' attribute for <a>. The following attributes are mutually exclusive:",
            "asp-route",
            "asp-controller, asp-action",
            "asp-page, asp-page-handler");
 
        var context = new TagHelperContext(
            tagName: "test",
            allAttributes: new TagHelperAttributeList(
                Enumerable.Empty<TagHelperAttribute>()),
            items: new Dictionary<object, object>(),
            uniqueId: "test");
 
        // Act & Assert
        var ex = await Assert.ThrowsAsync<InvalidOperationException>(
            () => anchorTagHelper.ProcessAsync(context, output));
 
        Assert.Equal(expectedErrorMessage, ex.Message);
    }
 
    [Fact]
    public async Task ProcessAsync_ThrowsIfActionAndPageProvided()
    {
        // Arrange
        var metadataProvider = new EmptyModelMetadataProvider();
        var htmlGenerator = new TestableHtmlGenerator(metadataProvider);
 
        var anchorTagHelper = new AnchorTagHelper(htmlGenerator)
        {
            Action = "Action",
            Page = "Page",
        };
 
        var output = new TagHelperOutput(
            "a",
            attributes: new TagHelperAttributeList(),
            getChildContentAsync: (useCachedResult, encoder) => Task.FromResult<TagHelperContent>(null));
        var expectedErrorMessage = string.Join(
            Environment.NewLine,
            "Cannot determine the 'href' attribute for <a>. The following attributes are mutually exclusive:",
            "asp-route",
            "asp-controller, asp-action",
            "asp-page, asp-page-handler");
 
        var context = new TagHelperContext(
            tagName: "test",
            allAttributes: new TagHelperAttributeList(
                Enumerable.Empty<TagHelperAttribute>()),
            items: new Dictionary<object, object>(),
            uniqueId: "test");
 
        // Act & Assert
        var ex = await Assert.ThrowsAsync<InvalidOperationException>(
            () => anchorTagHelper.ProcessAsync(context, output));
 
        Assert.Equal(expectedErrorMessage, ex.Message);
    }
}