File: Routing\ActionEndpointFactoryTest.cs
Web Access
Project: src\src\Mvc\Mvc.Core\test\Microsoft.AspNetCore.Mvc.Core.Test.csproj (Microsoft.AspNetCore.Mvc.Core.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.Builder;
using Microsoft.AspNetCore.Components.Routing;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Routing;
using Microsoft.AspNetCore.Routing.Patterns;
using Microsoft.Extensions.DependencyInjection;
using Moq;
 
namespace Microsoft.AspNetCore.Mvc.Routing;
 
public class ActionEndpointFactoryTest
{
    public ActionEndpointFactoryTest()
    {
        var serviceCollection = new ServiceCollection();
 
        var routeOptionsSetup = new MvcCoreRouteOptionsSetup();
        serviceCollection.Configure<RouteOptions>(routeOptionsSetup.Configure);
        serviceCollection.AddRouting(options =>
        {
            options.ConstraintMap["upper-case"] = typeof(UpperCaseParameterTransform);
        });
 
        Services = serviceCollection.BuildServiceProvider();
        Factory = new ActionEndpointFactory(Services.GetRequiredService<RoutePatternTransformer>(), Enumerable.Empty<IRequestDelegateFactory>(), Services);
    }
 
    internal ActionEndpointFactory Factory { get; }
 
    internal IServiceProvider Services { get; }
 
    [Fact]
    public void AddEndpoints_ConventionalRouted_WithEmptyRouteName_CreatesMetadataWithEmptyRouteName()
    {
        // Arrange
        var values = new { controller = "TestController", action = "TestAction", page = (string)null };
        var action = CreateActionDescriptor(values);
        var route = CreateRoute(routeName: string.Empty, pattern: "{controller}/{action}");
 
        // Act
        var endpoint = CreateConventionalRoutedEndpoint(action, route);
 
        // Assert
        var routeNameMetadata = endpoint.Metadata.GetMetadata<IRouteNameMetadata>();
        Assert.NotNull(routeNameMetadata);
        Assert.Equal(string.Empty, routeNameMetadata.RouteName);
    }
 
    [Fact]
    public void AddEndpoints_ConventionalRouted_ContainsParameterWithNullRequiredRouteValue_NoEndpointCreated()
    {
        // Arrange
        var values = new { controller = "TestController", action = "TestAction", page = (string)null };
        var action = CreateActionDescriptor(values);
        var route = CreateRoute(
            routeName: "Test",
            pattern: "{controller}/{action}/{page}",
            defaults: new RouteValueDictionary(new { action = "TestAction" }));
 
        // Act
        var endpoints = CreateConventionalRoutedEndpoints(action, route);
 
        // Assert
        Assert.Empty(endpoints);
    }
 
    // area, controller, action and page are special, but not hardcoded. Actions can define custom required
    // route values. This has been used successfully for localization, versioning and similar schemes. We should
    // be able to replace custom route values too.
    [Fact]
    public void AddEndpoints_ConventionalRouted_NonReservedRequiredValue_WithNoCorresponding_TemplateParameter_DoesNotProduceEndpoint()
    {
        // Arrange
        var values = new { controller = "home", action = "index", locale = "en-NZ" };
        var action = CreateActionDescriptor(values);
        var route = CreateRoute(routeName: "test", pattern: "{controller}/{action}");
 
        // Act
        var endpoints = CreateConventionalRoutedEndpoints(action, route);
 
        // Assert
        Assert.Empty(endpoints);
    }
 
    [Fact]
    public void AddEndpoints_ConventionalRouted_NonReservedRequiredValue_WithCorresponding_TemplateParameter_ProducesEndpoint()
    {
        // Arrange
        var values = new { controller = "home", action = "index", locale = "en-NZ" };
        var action = CreateActionDescriptor(values);
        var route = CreateRoute(routeName: "test", pattern: "{locale}/{controller}/{action}");
 
        // Act
        var endpoints = CreateConventionalRoutedEndpoints(action, route);
 
        // Assert
        Assert.Single(endpoints);
    }
 
    [Fact]
    public void AddEndpoints_ConventionalRouted_NonAreaRouteForAreaAction_DoesNotProduceEndpoint()
    {
        // Arrange
        var values = new { controller = "TestController", action = "TestAction", area = "admin", page = (string)null };
        var action = CreateActionDescriptor(values);
        var route = CreateRoute(routeName: "test", pattern: "{controller}/{action}");
 
        // Act
        var endpoints = CreateConventionalRoutedEndpoints(action, route);
 
        // Assert
        Assert.Empty(endpoints);
    }
 
    [Fact]
    public void AddEndpoints_ConventionalRouted_AreaRouteForNonAreaAction_DoesNotProduceEndpoint()
    {
        // Arrange
        var values = new { controller = "TestController", action = "TestAction", area = (string)null, page = (string)null };
        var action = CreateActionDescriptor(values);
        var route = CreateRoute(routeName: "test", pattern: "{area}/{controller}/{action}");
 
        // Act
        var endpoints = CreateConventionalRoutedEndpoints(action, route);
 
        // Assert
        Assert.Empty(endpoints);
    }
 
    [Fact]
    public void AddEndpoints_ConventionalRouted_RequiredValues_DoesNotMatchParameterDefaults_CreatesEndpoint()
    {
        // Arrange
        var values = new { controller = "TestController", action = "TestAction", page = (string)null };
        var action = CreateActionDescriptor(values);
        var route = CreateRoute(
            routeName: "test",
            pattern: "{controller}/{action}/{id?}",
            defaults: new RouteValueDictionary(new { controller = "TestController", action = "TestAction1" }));
 
        // Act
        var endpoint = CreateConventionalRoutedEndpoint(action, route);
 
        // Assert
        Assert.Equal("{controller}/{action}/{id?}", endpoint.RoutePattern.RawText);
        Assert.Equal("TestController", endpoint.RoutePattern.RequiredValues["controller"]);
        Assert.Equal("TestAction", endpoint.RoutePattern.RequiredValues["action"]);
        Assert.Equal("TestController", endpoint.RoutePattern.Defaults["controller"]);
        Assert.False(endpoint.RoutePattern.Defaults.ContainsKey("action"));
    }
 
    [Fact]
    public void AddEndpoints_ConventionalRouted_RequiredValues_DoesNotMatchNonParameterDefaults_DoesNotProduceEndpoint()
    {
        // Arrange
        var values = new { controller = "TestController", action = "TestAction", page = (string)null };
        var action = CreateActionDescriptor(values);
        var route = CreateRoute(
            routeName: "test",
            pattern: "/Blog/{*slug}",
            defaults: new RouteValueDictionary(new { controller = "TestController", action = "TestAction1" }));
 
        // Act
        var endpoints = CreateConventionalRoutedEndpoints(action, route);
 
        // Assert
        Assert.Empty(endpoints);
    }
 
    [Fact]
    public void AddEndpoints_ConventionalRouted_AttributeRoutes_DefaultDifferentCaseFromRouteValue_UseDefaultCase()
    {
        // Arrange
        var values = new { controller = "TestController", action = "TestAction", page = (string)null };
        var action = CreateActionDescriptor(values, "{controller}/{action=TESTACTION}/{id?}");
        // Act
        var endpoint = CreateAttributeRoutedEndpoint(action);
 
        // Assert
        Assert.Equal("{controller}/{action=TESTACTION}/{id?}", endpoint.RoutePattern.RawText);
        Assert.Equal("TESTACTION", endpoint.RoutePattern.Defaults["action"]);
        Assert.Equal(0, endpoint.Order);
        Assert.Equal("TestAction", endpoint.RoutePattern.RequiredValues["action"]);
    }
 
    [Fact]
    public void AddEndpoints_ConventionalRouted_RequiredValueWithNoCorrespondingParameter_DoesNotProduceEndpoint()
    {
        // Arrange
        var values = new { area = "admin", controller = "home", action = "index" };
        var action = CreateActionDescriptor(values);
        var route = CreateRoute(routeName: "test", pattern: "{controller}/{action}");
 
        // Act
        var endpoints = CreateConventionalRoutedEndpoints(action, route);
 
        // Assert
        Assert.Empty(endpoints);
    }
 
    [Fact]
    public void AddEndpoints_AttributeRouted_ContainsParameterUsingReservedNameWithConstraint_ExceptionThrown()
    {
        // Arrange
        var values = new { controller = "TestController", action = "TestAction", page = (string)null };
        var action = CreateActionDescriptor(values, "Products/{action:int}");
 
        // Act & Assert
        var exception = Assert.Throws<InvalidOperationException>(() => CreateAttributeRoutedEndpoint(action));
        Assert.Equal(
            "Failed to update the route pattern 'Products/{action:int}' with required route values. " +
            "This can occur when the route pattern contains parameters with reserved names such as: 'controller', 'action', 'page' and also uses route constraints such as '{action:int}'. " +
            "To fix this error, choose a different parameter name.",
            exception.Message);
    }
 
    [Fact]
    public void AddEndpoints_AttributeRouted_ContainsParameterWithNullRequiredRouteValue_EndpointCreated()
    {
        // Arrange
        var values = new { controller = "TestController", action = "TestAction", page = (string)null };
        var action = CreateActionDescriptor(values, "{controller}/{action}/{page}");
 
        // Act
        var endpoint = CreateAttributeRoutedEndpoint(action);
 
        // Assert
        Assert.Equal("{controller}/{action}/{page}", endpoint.RoutePattern.RawText);
        Assert.Equal("TestController", endpoint.RoutePattern.RequiredValues["controller"]);
        Assert.Equal("TestAction", endpoint.RoutePattern.RequiredValues["action"]);
        Assert.False(endpoint.RoutePattern.RequiredValues.ContainsKey("page"));
    }
 
    [Fact]
    public void AddEndpoints_AttributeRouted_WithRouteName_EndpointCreated()
    {
        // Arrange
        var values = new { controller = "TestController", action = "TestAction", page = (string)null };
        var action = CreateActionDescriptor(values, "{controller}/{action}/{page}");
        action.AttributeRouteInfo.Name = "Test";
 
        // Act
        var endpoint = CreateAttributeRoutedEndpoint(action);
 
        // Assert
        Assert.Equal("{controller}/{action}/{page}", endpoint.RoutePattern.RawText);
        Assert.Equal("TestController", endpoint.RoutePattern.RequiredValues["controller"]);
        Assert.Equal("TestAction", endpoint.RoutePattern.RequiredValues["action"]);
        Assert.False(endpoint.RoutePattern.RequiredValues.ContainsKey("page"));
        Assert.Equal("Test", endpoint.Metadata.GetMetadata<IRouteNameMetadata>().RouteName);
        Assert.Equal("Test", endpoint.Metadata.GetMetadata<IEndpointNameMetadata>().EndpointName);
    }
 
    [Fact]
    public void RequestDelegateFactoryWorks()
    {
        // Arrange
        var values = new { controller = "TestController", action = "TestAction", page = (string)null };
        var action = CreateActionDescriptor(values, "{controller}/{action}/{page}");
        action.AttributeRouteInfo.Name = "Test";
        RequestDelegate del = context => Task.CompletedTask;
        var requestDelegateFactory = new Mock<IRequestDelegateFactory>();
        requestDelegateFactory.Setup(m => m.CreateRequestDelegate(action, It.IsAny<RouteValueDictionary>())).Returns(del);
 
        // Act
        var factory = new ActionEndpointFactory(Services.GetRequiredService<RoutePatternTransformer>(), new[] { requestDelegateFactory.Object }, Services);
 
        var endpoints = new List<Endpoint>();
        factory.AddEndpoints(
            endpoints,
            new HashSet<string>(),
            action,
            Array.Empty<ConventionalRouteEntry>(),
            conventions: Array.Empty<Action<EndpointBuilder>>(),
            groupConventions: Array.Empty<Action<EndpointBuilder>>(),
            finallyConventions: Array.Empty<Action<EndpointBuilder>>(),
            groupFinallyConventions: Array.Empty<Action<EndpointBuilder>>(),
            createInertEndpoints: false);
 
        var endpoint = Assert.IsType<RouteEndpoint>(Assert.Single(endpoints));
 
        // Assert
        Assert.Same(del, endpoint.RequestDelegate);
    }
 
    [Fact]
    public void AddEndpoints_ConventionalRouted_WithMatchingConstraint_CreatesEndpoint()
    {
        // Arrange
        var values = new { controller = "TestController", action = "TestAction1", page = (string)null };
        var action = CreateActionDescriptor(values);
        var route = CreateRoute(
            routeName: "test",
            pattern: "{controller}/{action}",
            constraints: new RouteValueDictionary(new { action = "(TestAction1|TestAction2)" }));
 
        // Act
        var endpoints = CreateConventionalRoutedEndpoints(action, route);
 
        // Assert
        Assert.Single(endpoints);
    }
 
    [Fact]
    public void AddEndpoints_ConventionalRouted_WithNotMatchingConstraint_DoesNotCreateEndpoint()
    {
        // Arrange
        var values = new { controller = "TestController", action = "TestAction", page = (string)null };
        var action = CreateActionDescriptor(values);
        var route = CreateRoute(
            routeName: "test",
            pattern: "{controller}/{action}",
            constraints: new RouteValueDictionary(new { action = "(TestAction1|TestAction2)" }));
 
        // Act
        var endpoints = CreateConventionalRoutedEndpoints(action, route);
 
        // Assert
        Assert.Empty(endpoints);
    }
 
    [Fact]
    public void AddEndpoints_ConventionalRouted_StaticallyDefinedOrder_IsMaintained()
    {
        // Arrange
        var values = new { controller = "Home", action = "Index", page = (string)null };
        var action = CreateActionDescriptor(values);
        var routes = new[]
        {
                CreateRoute(routeName: "test1", pattern: "{controller}/{action}/{id?}", order: 1),
                CreateRoute(routeName: "test2", pattern: "named/{controller}/{action}/{id?}", order: 2),
            };
 
        // Act
        var endpoints = CreateConventionalRoutedEndpoints(action, routes);
 
        // Assert
        Assert.Collection(
            endpoints,
            (ep) =>
            {
                var matcherEndpoint = Assert.IsType<RouteEndpoint>(ep);
                Assert.Equal("{controller}/{action}/{id?}", matcherEndpoint.RoutePattern.RawText);
                Assert.Equal("Index", matcherEndpoint.RoutePattern.RequiredValues["action"]);
                Assert.Equal("Home", matcherEndpoint.RoutePattern.RequiredValues["controller"]);
                Assert.Equal(1, matcherEndpoint.Order);
            },
            (ep) =>
            {
                var matcherEndpoint = Assert.IsType<RouteEndpoint>(ep);
                Assert.Equal("named/{controller}/{action}/{id?}", matcherEndpoint.RoutePattern.RawText);
                Assert.Equal("Index", matcherEndpoint.RoutePattern.RequiredValues["action"]);
                Assert.Equal("Home", matcherEndpoint.RoutePattern.RequiredValues["controller"]);
                Assert.Equal(2, matcherEndpoint.Order);
            });
    }
 
    [Fact]
    public void AddEndpoints_CreatesInertEndpoint()
    {
        // Arrange
        var values = new { controller = "TestController", action = "TestAction", page = (string)null };
        var action = CreateActionDescriptor(values);
 
        // Act
        var endpoints = CreateConventionalRoutedEndpoints(action, Array.Empty<ConventionalRouteEntry>(), createInertEndpoints: true);
 
        // Assert
        Assert.IsType<Endpoint>(Assert.Single(endpoints));
    }
 
    private RouteEndpoint CreateAttributeRoutedEndpoint(ActionDescriptor action)
    {
        var endpoints = new List<Endpoint>();
        Factory.AddEndpoints(
            endpoints,
            new HashSet<string>(),
            action,
            Array.Empty<ConventionalRouteEntry>(),
            conventions: Array.Empty<Action<EndpointBuilder>>(),
            groupConventions: Array.Empty<Action<EndpointBuilder>>(),
            finallyConventions: Array.Empty<Action<EndpointBuilder>>(),
            groupFinallyConventions: Array.Empty<Action<EndpointBuilder>>(),
            createInertEndpoints: false);
        return Assert.IsType<RouteEndpoint>(Assert.Single(endpoints));
    }
 
    private RouteEndpoint CreateConventionalRoutedEndpoint(ActionDescriptor action, string template)
    {
        return CreateConventionalRoutedEndpoint(action, new ConventionalRouteEntry(routeName: null, template, null, null, null, order: 0, new List<Action<EndpointBuilder>>(), new List<Action<EndpointBuilder>>()));
    }
 
    private RouteEndpoint CreateConventionalRoutedEndpoint(ActionDescriptor action, ConventionalRouteEntry route)
    {
        Assert.NotNull(action.RouteValues);
 
        var endpoints = new List<Endpoint>();
        Factory.AddEndpoints(
            endpoints,
            new HashSet<string>(),
            action,
            new[] { route, },
            conventions: Array.Empty<Action<EndpointBuilder>>(),
            groupConventions: Array.Empty<Action<EndpointBuilder>>(),
            finallyConventions: Array.Empty<Action<EndpointBuilder>>(),
            groupFinallyConventions: Array.Empty<Action<EndpointBuilder>>(),
            createInertEndpoints: false);
        var endpoint = Assert.IsType<RouteEndpoint>(Assert.Single(endpoints));
 
        // This should be true for all conventional-routed actions.
        AssertIsSubset(new RouteValueDictionary(action.RouteValues), endpoint.RoutePattern.RequiredValues);
 
        return endpoint;
    }
 
    private IReadOnlyList<Endpoint> CreateConventionalRoutedEndpoints(ActionDescriptor action, ConventionalRouteEntry route)
    {
        return CreateConventionalRoutedEndpoints(action, new[] { route, });
    }
 
    private IReadOnlyList<Endpoint> CreateConventionalRoutedEndpoints(ActionDescriptor action, IReadOnlyList<ConventionalRouteEntry> routes, bool createInertEndpoints = false)
    {
        var endpoints = new List<Endpoint>();
        Factory.AddEndpoints(
            endpoints,
            new HashSet<string>(),
            action,
            routes,
            conventions: Array.Empty<Action<EndpointBuilder>>(),
            groupConventions: Array.Empty<Action<EndpointBuilder>>(),
            finallyConventions: Array.Empty<Action<EndpointBuilder>>(),
            groupFinallyConventions: Array.Empty<Action<EndpointBuilder>>(),
            createInertEndpoints);
        return endpoints.ToList();
    }
 
    private ConventionalRouteEntry CreateRoute(
        string routeName,
        string pattern,
        RouteValueDictionary defaults = null,
        IDictionary<string, object> constraints = null,
        RouteValueDictionary dataTokens = null,
        int order = 0,
        List<Action<EndpointBuilder>> conventions = null,
        List<Action<EndpointBuilder>> finallyConventions = null)
    {
        conventions ??= new List<Action<EndpointBuilder>>();
        finallyConventions ??= new List<Action<EndpointBuilder>>();
        return new ConventionalRouteEntry(routeName, pattern, defaults, constraints, dataTokens, order, conventions, finallyConventions);
    }
 
    private ActionDescriptor CreateActionDescriptor(
        object requiredValues,
        string pattern = null,
        IList<object> metadata = null)
    {
        var actionDescriptor = new ActionDescriptor();
        var routeValues = new RouteValueDictionary(requiredValues);
        foreach (var kvp in routeValues)
        {
            actionDescriptor.RouteValues[kvp.Key] = kvp.Value?.ToString();
        }
 
        if (!string.IsNullOrEmpty(pattern))
        {
            actionDescriptor.AttributeRouteInfo = new AttributeRouteInfo
            {
                Name = pattern,
                Template = pattern
            };
        }
 
        actionDescriptor.EndpointMetadata = metadata;
        return actionDescriptor;
    }
 
    private void AssertIsSubset(
        IReadOnlyDictionary<string, object> subset,
        IReadOnlyDictionary<string, object> fullSet)
    {
        foreach (var subsetPair in subset)
        {
            var isPresent = fullSet.TryGetValue(subsetPair.Key, out var fullSetPairValue);
            Assert.True(isPresent);
            Assert.Equal(subsetPair.Value, fullSetPairValue);
        }
    }
 
    private void AssertMatchingSuppressed(Endpoint endpoint, bool suppressed)
    {
        var isEndpointSuppressed = endpoint.Metadata.GetMetadata<ISuppressMatchingMetadata>()?.SuppressMatching ?? false;
        Assert.Equal(suppressed, isEndpointSuppressed);
    }
 
    private class UpperCaseParameterTransform : IOutboundParameterTransformer
    {
        public string TransformOutbound(object value)
        {
            return value?.ToString().ToUpperInvariant();
        }
    }
}