File: TagHelpers\UrlResolutionTagHelperTest.cs
Web Access
Project: src\src\Mvc\Mvc.Razor\test\Microsoft.AspNetCore.Mvc.Razor.Test.csproj (Microsoft.AspNetCore.Mvc.Razor.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.Components;
using Microsoft.AspNetCore.Html;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Routing;
using Microsoft.AspNetCore.Razor.TagHelpers;
using Microsoft.Extensions.WebEncoders.Testing;
using Moq;
 
namespace Microsoft.AspNetCore.Mvc.Razor.TagHelpers;
 
public class UrlResolutionTagHelperTest
{
    public static TheoryData<string, string> ResolvableUrlData
    {
        get
        {
            // url, expectedHref
            return new TheoryData<string, string>
                {
                   { "~/home/index.html", "/approot/home/index.html" },
                   { "~/home/index.html\r\n", "/approot/home/index.html" },
                   { "  ~/home/index.html", "/approot/home/index.html" },
                   { "\u000C~/home/index.html\r\n", "/approot/home/index.html" },
                   { "\t ~/home/index.html\n", "/approot/home/index.html" },
                   { "\r\n~/home/index.html\u000C\t", "/approot/home/index.html" },
                   { "\r~/home/index.html\t", "/approot/home/index.html" },
                   { "\n~/home/index.html\u202F", "/approot/home/index.html\u202F" },
                   {
                        "~/home/index.html ~/secondValue/index.html",
                        "/approot/home/index.html ~/secondValue/index.html"
                   },
                   { "  ~/   ", "/approot/" },
                   { "  ~/", "/approot/" },
                };
        }
    }
 
    public static TheoryData<string, string> ResolvableUrlVersionData
    {
        get
        {
            // url, expectedHref
            return new TheoryData<string, string>
                {
                   { "~/home/index.html", "/approot/home/index.fingerprint.html" },
                   { "~/home/index.html\r\n", "/approot/home/index.fingerprint.html" },
                   { "  ~/home/index.html", "/approot/home/index.fingerprint.html" },
                   { "\u000C~/home/index.html\r\n", "/approot/home/index.fingerprint.html" },
                   { "\t ~/home/index.html\n", "/approot/home/index.fingerprint.html" },
                   { "\r\n~/home/index.html\u000C\t", "/approot/home/index.fingerprint.html" },
                   { "\r~/home/index.html\t", "/approot/home/index.fingerprint.html" },
                   { "\n~/home/index.html\u202F", "/approot/home/index.fingerprint.html\u202F" },
                   {
                        "~/home/index.html ~/secondValue/index.html",
                        "/approot/home/index.html ~/secondValue/index.html"
                   },
                };
        }
    }
 
    [Fact]
    public void Process_DoesNothingIfTagNameIsNull()
    {
        // Arrange
        var tagHelperOutput = new TagHelperOutput(
            tagName: null,
            attributes: new TagHelperAttributeList
            {
                    { "href", "~/home/index.html" }
            },
            getChildContentAsync: (useCachedResult, encoder) => Task.FromResult<TagHelperContent>(null));
 
        var tagHelper = new UrlResolutionTagHelper(Mock.Of<IUrlHelperFactory>(), new HtmlTestEncoder());
        var context = new TagHelperContext(
            allAttributes: new TagHelperAttributeList(
                Enumerable.Empty<TagHelperAttribute>()),
            items: new Dictionary<object, object>(),
            uniqueId: "test");
 
        // Act
        tagHelper.Process(context, tagHelperOutput);
 
        // Assert
        var attribute = Assert.Single(tagHelperOutput.Attributes);
        Assert.Equal("href", attribute.Name, StringComparer.Ordinal);
        var attributeValue = Assert.IsType<string>(attribute.Value);
        Assert.Equal("~/home/index.html", attributeValue, StringComparer.Ordinal);
    }
 
    [Theory]
    [MemberData(nameof(ResolvableUrlData))]
    public void Process_ResolvesTildeSlashValues(string url, string expectedHref)
    {
        // Arrange
        var tagHelperOutput = new TagHelperOutput(
            tagName: "a",
            attributes: new TagHelperAttributeList
            {
                    { "href", url }
            },
            getChildContentAsync: (useCachedResult, encoder) => Task.FromResult<TagHelperContent>(null));
        var urlHelperMock = new Mock<IUrlHelper>();
        urlHelperMock
            .Setup(urlHelper => urlHelper.Content(It.IsAny<string>()))
            .Returns(new Func<string, string>(value => "/approot" + value.Substring(1)));
        var urlHelperFactory = new Mock<IUrlHelperFactory>();
        urlHelperFactory
            .Setup(f => f.GetUrlHelper(It.IsAny<ActionContext>()))
            .Returns(urlHelperMock.Object);
        var tagHelper = new UrlResolutionTagHelper(urlHelperFactory.Object, new HtmlTestEncoder())
        {
            ViewContext = new Rendering.ViewContext { HttpContext = new DefaultHttpContext() }
        };
 
        var context = new TagHelperContext(
            tagName: "a",
            allAttributes: new TagHelperAttributeList(
                Enumerable.Empty<TagHelperAttribute>()),
            items: new Dictionary<object, object>(),
            uniqueId: "test");
 
        // Act
        tagHelper.Process(context, tagHelperOutput);
 
        // Assert
        var attribute = Assert.Single(tagHelperOutput.Attributes);
        Assert.Equal("href", attribute.Name, StringComparer.Ordinal);
        var attributeValue = Assert.IsType<string>(attribute.Value);
        Assert.Equal(expectedHref, attributeValue, StringComparer.Ordinal);
        Assert.Equal(HtmlAttributeValueStyle.DoubleQuotes, attribute.ValueStyle);
    }
 
    public static TheoryData<string, string> ResolvableUrlHtmlStringData
    {
        get
        {
            // url, expectedHref
            return new TheoryData<string, string>
                {
                   { "~/home/index.html", "HtmlEncode[[/approot/]]home/index.html" },
                   { "  ~/home/index.html", "HtmlEncode[[/approot/]]home/index.html" },
                   {
                        "~/home/index.html ~/secondValue/index.html",
                        "HtmlEncode[[/approot/]]home/index.html ~/secondValue/index.html"
                   },
                };
        }
    }
 
    [Theory]
    [MemberData(nameof(ResolvableUrlHtmlStringData))]
    public void Process_ResolvesTildeSlashValues_InHtmlString(string url, string expectedHref)
    {
        // Arrange
        var tagHelperOutput = new TagHelperOutput(
            tagName: "a",
            attributes: new TagHelperAttributeList
            {
                    { "href", new HtmlString(url) }
            },
            getChildContentAsync: (useCachedResult, encoder) => Task.FromResult<TagHelperContent>(null));
        var urlHelperMock = new Mock<IUrlHelper>();
        urlHelperMock
            .Setup(urlHelper => urlHelper.Content(It.IsAny<string>()))
            .Returns(new Func<string, string>(value => "/approot" + value.Substring(1)));
        var urlHelperFactory = new Mock<IUrlHelperFactory>();
        urlHelperFactory
            .Setup(f => f.GetUrlHelper(It.IsAny<ActionContext>()))
            .Returns(urlHelperMock.Object);
        var tagHelper = new UrlResolutionTagHelper(urlHelperFactory.Object, new HtmlTestEncoder())
        {
            ViewContext = new Rendering.ViewContext { HttpContext = new DefaultHttpContext() }
        };
 
        var context = new TagHelperContext(
            tagName: "a",
            allAttributes: new TagHelperAttributeList(
                Enumerable.Empty<TagHelperAttribute>()),
            items: new Dictionary<object, object>(),
            uniqueId: "test");
 
        // Act
        tagHelper.Process(context, tagHelperOutput);
 
        // Assert
        var attribute = Assert.Single(tagHelperOutput.Attributes);
        Assert.Equal("href", attribute.Name, StringComparer.Ordinal);
        var htmlContent = Assert.IsAssignableFrom<IHtmlContent>(attribute.Value);
        Assert.Equal(expectedHref, HtmlContentUtilities.HtmlContentToString(htmlContent), StringComparer.Ordinal);
        Assert.Equal(HtmlAttributeValueStyle.DoubleQuotes, attribute.ValueStyle);
    }
 
    public static TheoryData<string> UnresolvableUrlData
    {
        get
        {
            // url
            return new TheoryData<string>
                {
                   { "/home/index.html" },
                   { "~ /home/index.html" },
                   { "/home/index.html ~/second/wontresolve.html" },
                   { "  ~\\home\\index.html" },
                   { "~\\/home/index.html" },
                   { "  ~" },
                   { "   " },
                };
        }
    }
 
    [Theory]
    [MemberData(nameof(UnresolvableUrlData))]
    public void Process_DoesNotResolveNonTildeSlashValues(string url)
    {
        // Arrange
        var tagHelperOutput = new TagHelperOutput(
            tagName: "a",
            attributes: new TagHelperAttributeList
            {
                    { "href", url }
            },
            getChildContentAsync: (useCachedResult, encoder) => Task.FromResult<TagHelperContent>(null));
        var urlHelperMock = new Mock<IUrlHelper>();
        urlHelperMock
            .Setup(urlHelper => urlHelper.Content(It.IsAny<string>()))
            .Returns("approot/home/index.html");
        var urlHelperFactory = new Mock<IUrlHelperFactory>();
        urlHelperFactory
            .Setup(f => f.GetUrlHelper(It.IsAny<ActionContext>()))
            .Returns(urlHelperMock.Object);
        var tagHelper = new UrlResolutionTagHelper(urlHelperFactory.Object, new HtmlTestEncoder());
 
        var context = new TagHelperContext(
            tagName: "a",
            allAttributes: new TagHelperAttributeList(
                Enumerable.Empty<TagHelperAttribute>()),
            items: new Dictionary<object, object>(),
            uniqueId: "test");
 
        // Act
        tagHelper.Process(context, tagHelperOutput);
 
        // Assert
        var attribute = Assert.Single(tagHelperOutput.Attributes);
        Assert.Equal("href", attribute.Name, StringComparer.Ordinal);
        var attributeValue = Assert.IsType<string>(attribute.Value);
        Assert.Equal(url, attributeValue, StringComparer.Ordinal);
        Assert.Equal(HtmlAttributeValueStyle.DoubleQuotes, attribute.ValueStyle);
    }
 
    public static TheoryData<string> UnresolvableUrlHtmlStringData
    {
        get
        {
            // url
            return new TheoryData<string>
                {
                   { "/home/index.html" },
                   { "~ /home/index.html" },
                   { "/home/index.html ~/second/wontresolve.html" },
                   { "~\\home\\index.html" },
                   { "~\\/home/index.html" },
                };
        }
    }
 
    [Theory]
    [MemberData(nameof(UnresolvableUrlHtmlStringData))]
    public void Process_DoesNotResolveNonTildeSlashValues_InHtmlString(string url)
    {
        // Arrange
        var tagHelperOutput = new TagHelperOutput(
            tagName: "a",
            attributes: new TagHelperAttributeList
            {
                    { "href", new HtmlString(url) }
            },
            getChildContentAsync: (useCachedResult, encoder) => Task.FromResult<TagHelperContent>(null));
        var urlHelperMock = new Mock<IUrlHelper>();
        urlHelperMock
            .Setup(urlHelper => urlHelper.Content(It.IsAny<string>()))
            .Returns("approot/home/index.html");
        var urlHelperFactory = new Mock<IUrlHelperFactory>();
        urlHelperFactory
            .Setup(f => f.GetUrlHelper(It.IsAny<ActionContext>()))
            .Returns(urlHelperMock.Object);
        var tagHelper = new UrlResolutionTagHelper(urlHelperFactory.Object, new HtmlTestEncoder());
 
        var context = new TagHelperContext(
            tagName: "a",
            allAttributes: new TagHelperAttributeList(
                Enumerable.Empty<TagHelperAttribute>()),
            items: new Dictionary<object, object>(),
            uniqueId: "test");
 
        // Act
        tagHelper.Process(context, tagHelperOutput);
 
        // Assert
        var attribute = Assert.Single(tagHelperOutput.Attributes);
        Assert.Equal("href", attribute.Name, StringComparer.Ordinal);
        var attributeValue = Assert.IsType<HtmlString>(attribute.Value);
        Assert.Equal(url.ToString(), attributeValue.ToString(), StringComparer.Ordinal);
        Assert.Equal(HtmlAttributeValueStyle.DoubleQuotes, attribute.ValueStyle);
    }
 
    [Fact]
    public void Process_IgnoresNonHtmlStringOrStringValues()
    {
        // Arrange
        var tagHelperOutput = new TagHelperOutput(
            tagName: "a",
            attributes: new TagHelperAttributeList
            {
                    { "href", true }
            },
            getChildContentAsync: (useCachedResult, encoder) => Task.FromResult<TagHelperContent>(null));
        var tagHelper = new UrlResolutionTagHelper(urlHelperFactory: null, htmlEncoder: null);
 
        var context = new TagHelperContext(
            tagName: "a",
            allAttributes: new TagHelperAttributeList(
                Enumerable.Empty<TagHelperAttribute>()),
            items: new Dictionary<object, object>(),
            uniqueId: "test");
 
        // Act
        tagHelper.Process(context, tagHelperOutput);
 
        // Assert
        var attribute = Assert.Single(tagHelperOutput.Attributes);
        Assert.Equal("href", attribute.Name, StringComparer.Ordinal);
        Assert.True(Assert.IsType<bool>(attribute.Value));
        Assert.Equal(HtmlAttributeValueStyle.DoubleQuotes, attribute.ValueStyle);
    }
 
    [Fact]
    public void Process_ThrowsWhenEncodingNeededAndIUrlHelperActsUnexpectedly()
    {
        // Arrange
        var relativeUrl = "~/home/index.html";
        var expectedExceptionMessage = Resources.FormatCouldNotResolveApplicationRelativeUrl_TagHelper(
            relativeUrl,
            nameof(IUrlHelper),
            nameof(IUrlHelper.Content),
            "removeTagHelper",
            typeof(UrlResolutionTagHelper).FullName,
            typeof(UrlResolutionTagHelper).Assembly.GetName().Name);
        var tagHelperOutput = new TagHelperOutput(
            tagName: "a",
            attributes: new TagHelperAttributeList
            {
                    { "href", new HtmlString(relativeUrl) }
            },
            getChildContentAsync: (useCachedResult, encoder) => Task.FromResult<TagHelperContent>(null));
        var urlHelperMock = new Mock<IUrlHelper>();
        urlHelperMock
            .Setup(urlHelper => urlHelper.Content(It.IsAny<string>()))
            .Returns("UnexpectedResult");
        var urlHelperFactory = new Mock<IUrlHelperFactory>();
        urlHelperFactory
            .Setup(f => f.GetUrlHelper(It.IsAny<ActionContext>()))
            .Returns(urlHelperMock.Object);
        var tagHelper = new UrlResolutionTagHelper(urlHelperFactory.Object, new HtmlTestEncoder())
        {
            ViewContext = new Rendering.ViewContext { HttpContext = new DefaultHttpContext() }
        };
 
        var context = new TagHelperContext(
            tagName: "a",
            allAttributes: new TagHelperAttributeList(
                Enumerable.Empty<TagHelperAttribute>()),
            items: new Dictionary<object, object>(),
            uniqueId: "test");
 
        // Act & Assert
        var exception = Assert.Throws<InvalidOperationException>(
            () => tagHelper.Process(context, tagHelperOutput));
        Assert.Equal(expectedExceptionMessage, exception.Message, StringComparer.Ordinal);
    }
 
    [Theory]
    [MemberData(nameof(ResolvableUrlVersionData))]
    public void Process_ResolvesVersionedUrls_WhenResourceCollectionIsAvailable(string url, string expectedHref)
    {
        // Arrange
        var tagHelperOutput = new TagHelperOutput(
            tagName: "a",
            attributes: new TagHelperAttributeList
            {
                    { "href", url }
            },
            getChildContentAsync: (useCachedResult, encoder) => Task.FromResult<TagHelperContent>(null));
        var urlHelperMock = new Mock<IUrlHelper>();
        urlHelperMock
            .Setup(urlHelper => urlHelper.Content(It.IsAny<string>()))
            .Returns(new Func<string, string>(value => "/approot" + value.Substring(1)));
        var urlHelperFactory = new Mock<IUrlHelperFactory>();
        urlHelperFactory
            .Setup(f => f.GetUrlHelper(It.IsAny<ActionContext>()))
            .Returns(urlHelperMock.Object);
 
        var httpContext = new DefaultHttpContext();
        httpContext.SetEndpoint(new Endpoint(
            (context) => Task.CompletedTask,
            new EndpointMetadataCollection(
                [new ResourceAssetCollection([new("home/index.fingerprint.html", [new ResourceAssetProperty("label", "home/index.html")])])]),
            "Test"));
 
        var tagHelper = new UrlResolutionTagHelper(urlHelperFactory.Object, new HtmlTestEncoder())
        {
            ViewContext = new Rendering.ViewContext { HttpContext = httpContext }
        };
 
        var context = new TagHelperContext(
            tagName: "a",
            allAttributes: new TagHelperAttributeList(
                Enumerable.Empty<TagHelperAttribute>()),
            items: new Dictionary<object, object>(),
            uniqueId: "test");
 
        // Act
        tagHelper.Process(context, tagHelperOutput);
 
        // Assert
        var attribute = Assert.Single(tagHelperOutput.Attributes);
        Assert.Equal("href", attribute.Name, StringComparer.Ordinal);
        var attributeValue = Assert.IsType<string>(attribute.Value);
        Assert.Equal(expectedHref, attributeValue, StringComparer.Ordinal);
        Assert.Equal(HtmlAttributeValueStyle.DoubleQuotes, attribute.ValueStyle);
    }
}