|
// 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();
}
}
}
|