// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Mvc.Abstractions; using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.AspNetCore.Mvc.Infrastructure; using Microsoft.AspNetCore.Mvc.Routing; using Microsoft.AspNetCore.Routing; using Microsoft.AspNetCore.Routing.Matching; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using Microsoft.Extensions.Primitives; using Moq; using Xunit; namespace Microsoft.AspNetCore.Mvc.Internal { public class MvcEndpointDataSourceTests { [Fact] public void Endpoints_AccessParameters_InitializedFromProvider() { // Arrange var routeValue = "Value"; var requiredValues = new Dictionary { ["Name"] = routeValue }; var displayName = "DisplayName!"; var order = 1; var template = "/Template!"; var filterDescriptor = new FilterDescriptor(new ControllerActionFilter(), 1); var mockDescriptorProvider = new Mock(); mockDescriptorProvider.Setup(m => m.ActionDescriptors).Returns(new ActionDescriptorCollection(new List { new ActionDescriptor { RouteValues = requiredValues, DisplayName = displayName, AttributeRouteInfo = new AttributeRouteInfo { Order = order, Template = template }, FilterDescriptors = new List { filterDescriptor } } }, 0)); var dataSource = CreateMvcEndpointDataSource(mockDescriptorProvider.Object); // Act var endpoints = dataSource.Endpoints; // Assert var endpoint = Assert.Single(endpoints); var matcherEndpoint = Assert.IsType(endpoint); var routeValuesAddressMetadata = matcherEndpoint.Metadata.GetMetadata(); Assert.NotNull(routeValuesAddressMetadata); var endpointValue = routeValuesAddressMetadata.RequiredValues["Name"]; Assert.Equal(routeValue, endpointValue); Assert.Equal(displayName, matcherEndpoint.DisplayName); Assert.Equal(order, matcherEndpoint.Order); Assert.Equal(template, matcherEndpoint.RoutePattern.RawText); } [Fact] public async Task Endpoints_InvokeReturnedEndpoint_ActionInvokerProviderCalled() { // Arrange var endpointFeature = new EndpointFeature { RouteValues = new RouteValueDictionary() }; var featureCollection = new FeatureCollection(); featureCollection.Set(endpointFeature); featureCollection.Set(endpointFeature); var httpContextMock = new Mock(); httpContextMock.Setup(m => m.Features).Returns(featureCollection); var descriptorProviderMock = new Mock(); descriptorProviderMock.Setup(m => m.ActionDescriptors).Returns(new ActionDescriptorCollection(new List { new ActionDescriptor { AttributeRouteInfo = new AttributeRouteInfo { Template = string.Empty }, FilterDescriptors = new List() } }, 0)); var actionInvokerCalled = false; var actionInvokerMock = new Mock(); actionInvokerMock.Setup(m => m.InvokeAsync()).Returns(() => { actionInvokerCalled = true; return Task.CompletedTask; }); var actionInvokerProviderMock = new Mock(); actionInvokerProviderMock.Setup(m => m.CreateInvoker(It.IsAny())).Returns(actionInvokerMock.Object); var dataSource = CreateMvcEndpointDataSource( descriptorProviderMock.Object, new MvcEndpointInvokerFactory(actionInvokerProviderMock.Object)); // Act var endpoints = dataSource.Endpoints; // Assert var endpoint = Assert.Single(endpoints); var matcherEndpoint = Assert.IsType(endpoint); await matcherEndpoint.RequestDelegate(httpContextMock.Object); Assert.True(actionInvokerCalled); } [Fact(Skip = "https://github.com/aspnet/Routing/issues/722")] public void GetChangeToken_MultipleChangeTokenProviders_ComposedResult() { // Arrange var featureCollection = new FeatureCollection(); featureCollection.Set(new EndpointFeature { RouteValues = new RouteValueDictionary() }); var httpContextMock = new Mock(); httpContextMock.Setup(m => m.Features).Returns(featureCollection); var descriptorProviderMock = new Mock(); descriptorProviderMock.Setup(m => m.ActionDescriptors).Returns(new ActionDescriptorCollection(new List(), 0)); var actionInvokerMock = new Mock(); var actionInvokerProviderMock = new Mock(); actionInvokerProviderMock.Setup(m => m.CreateInvoker(It.IsAny())).Returns(actionInvokerMock.Object); var changeTokenMock = new Mock(); var changeProvider1Mock = new Mock(); changeProvider1Mock.Setup(m => m.GetChangeToken()).Returns(changeTokenMock.Object); var changeProvider2Mock = new Mock(); changeProvider2Mock.Setup(m => m.GetChangeToken()).Returns(changeTokenMock.Object); var dataSource = CreateMvcEndpointDataSource( descriptorProviderMock.Object, new MvcEndpointInvokerFactory(actionInvokerProviderMock.Object), new[] { changeProvider1Mock.Object, changeProvider2Mock.Object }); // Act var changeToken = dataSource.GetChangeToken(); // Assert var compositeChangeToken = Assert.IsType(changeToken); Assert.Equal(2, compositeChangeToken.ChangeTokens.Count); } [Theory] [InlineData("{controller}/{action}/{id?}", new[] { "TestController/TestAction/{id?}" })] [InlineData("{controller}/{id?}", new string[] { })] [InlineData("{action}/{id?}", new string[] { })] [InlineData("{Controller}/{Action}/{id?}", new[] { "TestController/TestAction/{id?}" })] [InlineData("{CONTROLLER}/{ACTION}/{id?}", new[] { "TestController/TestAction/{id?}" })] [InlineData("{controller}/{action=TestAction}", new[] { "TestController", "TestController/TestAction" })] [InlineData("{controller}/{action=TestAction}/{id?}", new[] { "TestController", "TestController/TestAction/{id?}" })] [InlineData("{controller=TestController}/{action=TestAction}/{id?}", new[] { "", "TestController", "TestController/TestAction/{id?}" })] [InlineData("{controller}/{action}/{*catchAll}", new[] { "TestController/TestAction/{*catchAll}" })] [InlineData("{controller}/{action=TestAction}/{*catchAll}", new[] { "TestController", "TestController/TestAction/{*catchAll}" })] [InlineData("{controller}/{action=TestAction}/{id?}/{*catchAll}", new[] { "TestController", "TestController/TestAction/{id?}/{*catchAll}" })] [InlineData("{controller}/{action}.{ext?}", new[] { "TestController/TestAction.{ext?}" })] [InlineData("{controller}/{action=TestAction}.{ext?}", new[] { "TestController", "TestController/TestAction.{ext?}" })] public void Endpoints_SingleAction(string endpointInfoRoute, string[] finalEndpointPatterns) { // Arrange var actionDescriptorCollection = GetActionDescriptorCollection( new { controller = "TestController", action = "TestAction" }); var dataSource = CreateMvcEndpointDataSource(actionDescriptorCollection); dataSource.ConventionalEndpointInfos.Add(CreateEndpointInfo(string.Empty, endpointInfoRoute)); // Act var endpoints = dataSource.Endpoints; // Assert var inspectors = finalEndpointPatterns .Select(t => new Action(e => Assert.Equal(t, Assert.IsType(e).RoutePattern.RawText))) .ToArray(); // Assert Assert.Collection(endpoints, inspectors); } [Theory] [InlineData("{area}/{controller}/{action}/{id?}", new[] { "TestArea/TestController/TestAction/{id?}" })] [InlineData("{controller}/{action}/{id?}", new string[] { })] [InlineData("{area=TestArea}/{controller}/{action}/{id?}", new[] { "TestArea/TestController/TestAction/{id?}" })] [InlineData("{area=TestArea}/{controller}/{action=TestAction}/{id?}", new[] { "TestArea/TestController", "TestArea/TestController/TestAction/{id?}" })] [InlineData("{area=TestArea}/{controller=TestController}/{action=TestAction}/{id?}", new[] { "", "TestArea", "TestArea/TestController", "TestArea/TestController/TestAction/{id?}" })] [InlineData("{area:exists}/{controller}/{action}/{id?}", new[] { "TestArea/TestController/TestAction/{id?}" })] public void Endpoints_AreaSingleAction(string endpointInfoRoute, string[] finalEndpointTemplates) { // Arrange var actionDescriptorCollection = GetActionDescriptorCollection( new { controller = "TestController", action = "TestAction", area = "TestArea" }); var dataSource = CreateMvcEndpointDataSource(actionDescriptorCollection); dataSource.ConventionalEndpointInfos.Add(CreateEndpointInfo(string.Empty, endpointInfoRoute)); // Act var endpoints = dataSource.Endpoints; // Assert var inspectors = finalEndpointTemplates .Select(t => new Action(e => Assert.Equal(t, Assert.IsType(e).RoutePattern.RawText))) .ToArray(); // Assert Assert.Collection(endpoints, inspectors); } [Fact] public void Endpoints_SingleAction_WithActionDefault() { // Arrange var actionDescriptorCollection = GetActionDescriptorCollection( new { controller = "TestController", action = "TestAction" }); var dataSource = CreateMvcEndpointDataSource(actionDescriptorCollection); dataSource.ConventionalEndpointInfos.Add(CreateEndpointInfo( string.Empty, "{controller}/{action}", new RouteValueDictionary(new { action = "TestAction" }))); // Act var endpoints = dataSource.Endpoints; // Assert Assert.Collection(endpoints, (e) => Assert.Equal("TestController", Assert.IsType(e).RoutePattern.RawText), (e) => Assert.Equal("TestController/TestAction", Assert.IsType(e).RoutePattern.RawText)); } [Fact] public void Endpoints_CalledMultipleTimes_ReturnsSameInstance() { // Arrange var actionDescriptorCollectionProviderMock = new Mock(); actionDescriptorCollectionProviderMock .Setup(m => m.ActionDescriptors) .Returns(new ActionDescriptorCollection(new[] { CreateActionDescriptor(new { controller = "TestController", action = "TestAction" }) }, version: 0)); var dataSource = CreateMvcEndpointDataSource(actionDescriptorCollectionProviderMock.Object); dataSource.ConventionalEndpointInfos.Add(CreateEndpointInfo( string.Empty, "{controller}/{action}", new RouteValueDictionary(new { action = "TestAction" }))); // Act var endpoints1 = dataSource.Endpoints; var endpoints2 = dataSource.Endpoints; // Assert Assert.Collection(endpoints1, (e) => Assert.Equal("TestController", Assert.IsType(e).RoutePattern.RawText), (e) => Assert.Equal("TestController/TestAction", Assert.IsType(e).RoutePattern.RawText)); Assert.Same(endpoints1, endpoints2); actionDescriptorCollectionProviderMock.VerifyGet(m => m.ActionDescriptors, Times.Once); } [Fact(Skip = "https://github.com/aspnet/Routing/issues/722")] public void Endpoints_ChangeTokenTriggered_EndpointsRecreated() { // Arrange var actionDescriptorCollectionProviderMock = new Mock(); actionDescriptorCollectionProviderMock .Setup(m => m.ActionDescriptors) .Returns(new ActionDescriptorCollection(new[] { CreateActionDescriptor(new { controller = "TestController", action = "TestAction" }) }, version: 0)); CancellationTokenSource cts = null; var changeProviderMock = new Mock(); changeProviderMock.Setup(m => m.GetChangeToken()).Returns(() => { cts = new CancellationTokenSource(); var changeToken = new CancellationChangeToken(cts.Token); return changeToken; }); var dataSource = CreateMvcEndpointDataSource( actionDescriptorCollectionProviderMock.Object, actionDescriptorChangeProviders: new[] { changeProviderMock.Object }); dataSource.ConventionalEndpointInfos.Add(CreateEndpointInfo( string.Empty, "{controller}/{action}", new RouteValueDictionary(new { action = "TestAction" }))); // Act var endpoints = dataSource.Endpoints; Assert.Collection(endpoints, (e) => Assert.Equal("TestController", Assert.IsType(e).RoutePattern.RawText), (e) => Assert.Equal("TestController/TestAction", Assert.IsType(e).RoutePattern.RawText)); actionDescriptorCollectionProviderMock .Setup(m => m.ActionDescriptors) .Returns(new ActionDescriptorCollection(new[] { CreateActionDescriptor(new { controller = "NewTestController", action = "NewTestAction" }) }, version: 1)); cts.Cancel(); // Assert var newEndpoints = dataSource.Endpoints; Assert.NotSame(endpoints, newEndpoints); Assert.Collection(newEndpoints, (e) => Assert.Equal("NewTestController/NewTestAction", Assert.IsType(e).RoutePattern.RawText)); } [Fact] public void Endpoints_MultipleActions_WithActionConstraint() { // Arrange var actionDescriptorCollection = GetActionDescriptorCollection( new { controller = "TestController", action = "TestAction" }, new { controller = "TestController", action = "TestAction1" }, new { controller = "TestController", action = "TestAction2" }); var dataSource = CreateMvcEndpointDataSource(actionDescriptorCollection); dataSource.ConventionalEndpointInfos.Add(CreateEndpointInfo( string.Empty, "{controller}/{action}", constraints: new RouteValueDictionary(new { action = "(TestAction1|TestAction2)" }))); // Act var endpoints = dataSource.Endpoints; // Assert Assert.Collection(endpoints, (e) => Assert.Equal("TestController/TestAction1", Assert.IsType(e).RoutePattern.RawText), (e) => Assert.Equal("TestController/TestAction2", Assert.IsType(e).RoutePattern.RawText)); } [Theory] [InlineData("{controller}/{action}", new[] { "TestController1/TestAction1", "TestController1/TestAction2", "TestController1/TestAction3", "TestController2/TestAction1" })] [InlineData("{controller}/{action:regex((TestAction1|TestAction2))}", new[] { "TestController1/TestAction1", "TestController1/TestAction2", "TestController2/TestAction1" })] public void Endpoints_MultipleActions(string endpointInfoRoute, string[] finalEndpointTemplates) { // Arrange var actionDescriptorCollection = GetActionDescriptorCollection( new { controller = "TestController1", action = "TestAction1" }, new { controller = "TestController1", action = "TestAction2" }, new { controller = "TestController1", action = "TestAction3" }, new { controller = "TestController2", action = "TestAction1" }); var dataSource = CreateMvcEndpointDataSource(actionDescriptorCollection); dataSource.ConventionalEndpointInfos.Add(CreateEndpointInfo( string.Empty, endpointInfoRoute)); // Act var endpoints = dataSource.Endpoints; var inspectors = finalEndpointTemplates .Select(t => new Action(e => Assert.Equal(t, Assert.IsType(e).RoutePattern.RawText))) .ToArray(); // Assert Assert.Collection(endpoints, inspectors); } [Fact] public void Endpoints_ConventionalRoute_WithEmptyRouteName_CreatesMetadataWithEmptyRouteName() { // Arrange var actionDescriptorCollection = GetActionDescriptorCollection( new { controller = "Home", action = "Index" }); var dataSource = CreateMvcEndpointDataSource(actionDescriptorCollection); dataSource.ConventionalEndpointInfos.Add( CreateEndpointInfo(string.Empty, "named/{controller}/{action}/{id?}")); // Act var endpoints = dataSource.Endpoints; // Assert var endpoint = Assert.Single(endpoints); var matcherEndpoint = Assert.IsType(endpoint); var routeValuesAddressNameMetadata = matcherEndpoint.Metadata.GetMetadata(); Assert.NotNull(routeValuesAddressNameMetadata); Assert.Equal(string.Empty, routeValuesAddressNameMetadata.Name); } [Fact] public void Endpoints_CanCreateMultipleEndpoints_WithSameRouteName() { // Arrange var actionDescriptorCollection = GetActionDescriptorCollection( new { controller = "Home", action = "Index" }, new { controller = "Products", action = "Details" }); var dataSource = CreateMvcEndpointDataSource(actionDescriptorCollection); dataSource.ConventionalEndpointInfos.Add( CreateEndpointInfo("namedRoute", "named/{controller}/{action}/{id?}")); // Act var endpoints = dataSource.Endpoints; // Assert Assert.Collection( endpoints, (ep) => { var matcherEndpoint = Assert.IsType(ep); var routeValuesAddressMetadata = matcherEndpoint.Metadata.GetMetadata(); Assert.NotNull(routeValuesAddressMetadata); Assert.Equal("namedRoute", routeValuesAddressMetadata.Name); Assert.Equal("named/Home/Index/{id?}", matcherEndpoint.RoutePattern.RawText); }, (ep) => { var matcherEndpoint = Assert.IsType(ep); var routeValuesAddressMetadata = matcherEndpoint.Metadata.GetMetadata(); Assert.NotNull(routeValuesAddressMetadata); Assert.Equal("namedRoute", routeValuesAddressMetadata.Name); Assert.Equal("named/Products/Details/{id?}", matcherEndpoint.RoutePattern.RawText); }); } [Fact] public void Endpoints_ConventionalRoutes_StaticallyDefinedOrder_IsMaintained() { // Arrange var actionDescriptorCollection = GetActionDescriptorCollection( new { controller = "Home", action = "Index" }, new { controller = "Products", action = "Details" }); var dataSource = CreateMvcEndpointDataSource(actionDescriptorCollection); dataSource.ConventionalEndpointInfos.Add(CreateEndpointInfo( name: string.Empty, template: "{controller}/{action}/{id?}")); dataSource.ConventionalEndpointInfos.Add(CreateEndpointInfo( name: "namedRoute", "named/{controller}/{action}/{id?}")); // Act var endpoints = dataSource.Endpoints; // Assert Assert.Collection( endpoints, (ep) => { var matcherEndpoint = Assert.IsType(ep); Assert.Equal("Home/Index/{id?}", matcherEndpoint.RoutePattern.RawText); Assert.Equal(1, matcherEndpoint.Order); }, (ep) => { var matcherEndpoint = Assert.IsType(ep); Assert.Equal("named/Home/Index/{id?}", matcherEndpoint.RoutePattern.RawText); Assert.Equal(2, matcherEndpoint.Order); }, (ep) => { var matcherEndpoint = Assert.IsType(ep); Assert.Equal("Products/Details/{id?}", matcherEndpoint.RoutePattern.RawText); Assert.Equal(1, matcherEndpoint.Order); }, (ep) => { var matcherEndpoint = Assert.IsType(ep); Assert.Equal("named/Products/Details/{id?}", matcherEndpoint.RoutePattern.RawText); Assert.Equal(2, matcherEndpoint.Order); }); } [Fact] public void RequiredValue_WithNoCorresponding_TemplateParameter_DoesNotProduceEndpoint() { // Arrange var requiredValues = new RouteValueDictionary(new { area = "admin", controller = "home", action = "index" }); var actionDescriptorCollection = GetActionDescriptorCollection(requiredValues); var dataSource = CreateMvcEndpointDataSource(actionDescriptorCollection); dataSource.ConventionalEndpointInfos.Add(CreateEndpointInfo(string.Empty, "{controller}/{action}")); // Act var endpoints = dataSource.Endpoints; // 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 NonReservedRequiredValue_WithNoCorresponding_TemplateParameter_DoesNotProduceEndpoint() { // Arrange var action1 = new RouteValueDictionary(new { controller = "home", action = "index", locale = "en-NZ" }); var action2 = new RouteValueDictionary(new { controller = "home", action = "about", locale = "en-CA" }); var action3 = new RouteValueDictionary(new { controller = "home", action = "index", locale = (string)null }); var actionDescriptorCollection = GetActionDescriptorCollection(action1, action2, action3); var dataSource = CreateMvcEndpointDataSource(actionDescriptorCollection); // Adding a localized route a non-localized route dataSource.ConventionalEndpointInfos.Add(CreateEndpointInfo(string.Empty, "{locale}/{controller}/{action}")); dataSource.ConventionalEndpointInfos.Add(CreateEndpointInfo(string.Empty, "{controller}/{action}")); // Act var endpoints = dataSource.Endpoints; // Assert Assert.Collection( endpoints.Cast().OrderBy(e => e.RoutePattern.RawText), e => Assert.Equal("en-CA/home/about", e.RoutePattern.RawText), e => Assert.Equal("en-NZ/home/index", e.RoutePattern.RawText), e => Assert.Equal("home/index", e.RoutePattern.RawText)); } [Fact] public void TemplateParameter_WithNoDefaultOrRequiredValue_DoesNotProduceEndpoint() { // Arrange var requiredValues = new RouteValueDictionary(new { controller = "home", action = "index", area = (string)null }); var actionDescriptorCollection = GetActionDescriptorCollection(requiredValues); var dataSource = CreateMvcEndpointDataSource(actionDescriptorCollection); dataSource.ConventionalEndpointInfos.Add(CreateEndpointInfo(string.Empty, "{area}/{controller}/{action}")); // Act var endpoints = dataSource.Endpoints; // Assert Assert.Empty(endpoints); } [Fact] public void TemplateParameter_WithDefaultValue_AndNullRequiredValue_DoesNotProduceEndpoint() { // Arrange var requiredValues = new RouteValueDictionary(new { area = (string)null, controller = "home", action = "index" }); var actionDescriptorCollection = GetActionDescriptorCollection(requiredValues); var dataSource = CreateMvcEndpointDataSource(actionDescriptorCollection); dataSource.ConventionalEndpointInfos.Add(CreateEndpointInfo(string.Empty, "{area=admin}/{controller}/{action}")); // Act var endpoints = dataSource.Endpoints; // Assert Assert.Empty(endpoints); } [Fact] public void TemplateParameter_WithNullRequiredValue_DoesNotProduceEndpoint() { // Arrange var requiredValues = new RouteValueDictionary(new { area = (string)null, controller = "home", action = "index" }); var actionDescriptorCollection = GetActionDescriptorCollection(requiredValues); var dataSource = CreateMvcEndpointDataSource(actionDescriptorCollection); dataSource.ConventionalEndpointInfos.Add(CreateEndpointInfo(string.Empty, "{area}/{controller}/{action}")); // Act var endpoints = dataSource.Endpoints; // Assert Assert.Empty(endpoints); } [Fact] public void NoDefaultValues_RequiredValues_UsedToCreateDefaultValues() { // Arrange var expectedDefaults = new RouteValueDictionary(new { controller = "Foo", action = "Bar" }); var actionDescriptorCollection = GetActionDescriptorCollection(requiredValues: expectedDefaults); var dataSource = CreateMvcEndpointDataSource(actionDescriptorCollection); dataSource.ConventionalEndpointInfos.Add(CreateEndpointInfo(string.Empty, "{controller}/{action}")); // Act var endpoints = dataSource.Endpoints; // Assert var endpoint = Assert.Single(endpoints); var matcherEndpoint = Assert.IsType(endpoint); Assert.Equal("Foo/Bar", matcherEndpoint.RoutePattern.RawText); AssertIsSubset(expectedDefaults, matcherEndpoint.RoutePattern.Defaults); } [Fact] public void RequiredValues_NotPresent_InDefaultValues_IsAddedToDefaultValues() { // Arrange var requiredValues = new RouteValueDictionary( new { controller = "Foo", action = "Bar", subarea = "test" }); var expectedDefaults = requiredValues; var actionDescriptorCollection = GetActionDescriptorCollection(requiredValues: requiredValues); var dataSource = CreateMvcEndpointDataSource(actionDescriptorCollection); dataSource.ConventionalEndpointInfos.Add( CreateEndpointInfo(string.Empty, "{subarea}/{controller=Home}/{action=Index}")); // Act var endpoints = dataSource.Endpoints; // Assert var endpoint = Assert.Single(endpoints); var matcherEndpoint = Assert.IsType(endpoint); Assert.Equal("test/Foo/Bar", matcherEndpoint.RoutePattern.RawText); AssertIsSubset(expectedDefaults, matcherEndpoint.RoutePattern.Defaults); } [Fact] public void RequiredValues_NotPresent_InDefaultValuesOrParameter_EndpointNotCreated() { // Arrange var requiredValues = new RouteValueDictionary( new { controller = "Foo", action = "Bar", subarea = "test" }); var expectedDefaults = requiredValues; var actionDescriptorCollection = GetActionDescriptorCollection(requiredValues: requiredValues); var dataSource = CreateMvcEndpointDataSource(actionDescriptorCollection); dataSource.ConventionalEndpointInfos.Add( CreateEndpointInfo(string.Empty, "{controller=Home}/{action=Index}")); // Act var endpoints = dataSource.Endpoints; // Assert Assert.Empty(endpoints); } [Fact] public void RequiredValues_IsSubsetOf_DefaultValues() { // Arrange var requiredValues = new RouteValueDictionary( new { controller = "Foo", action = "Bar", subarea = "test" }); var expectedDefaults = new RouteValueDictionary( new { controller = "Foo", action = "Bar", subarea = "test", subscription = "general" }); var actionDescriptorCollection = GetActionDescriptorCollection(requiredValues); var dataSource = CreateMvcEndpointDataSource(actionDescriptorCollection); dataSource.ConventionalEndpointInfos.Add( CreateEndpointInfo( string.Empty, "{controller=Home}/{action=Index}/{subscription=general}", defaults: new RouteValueDictionary(new { subarea = "test", }))); // Act var endpoints = dataSource.Endpoints; // Assert var endpoint = Assert.Single(endpoints); var matcherEndpoint = Assert.IsType(endpoint); Assert.Equal("Foo/Bar/{subscription=general}", matcherEndpoint.RoutePattern.RawText); AssertIsSubset(expectedDefaults, matcherEndpoint.RoutePattern.Defaults); } [Fact] public void RequiredValues_DoesNotMatchParameterDefaults_Included() { // Arrange var action = new RouteValueDictionary( new { controller = "Foo", action = "Baz", }); // Doesn't match default var expectedDefaults = new RouteValueDictionary( new { controller = "Foo", action = "Baz", }); var actionDescriptorCollection = GetActionDescriptorCollection(action); var dataSource = CreateMvcEndpointDataSource(actionDescriptorCollection); dataSource.ConventionalEndpointInfos.Add( CreateEndpointInfo( string.Empty, "{controller}/{action}/{id?}", defaults: new RouteValueDictionary(new { controller = "Foo", action = "Bar" }))); // Act var endpoints = dataSource.Endpoints; // Assert var endpoint = Assert.Single(endpoints); var matcherEndpoint = Assert.IsType(endpoint); Assert.Equal("Foo/Baz/{id?}", matcherEndpoint.RoutePattern.RawText); AssertIsSubset(expectedDefaults, matcherEndpoint.RoutePattern.Defaults); } [Fact] public void RequiredValues_DoesNotMatchNonParameterDefaults_FilteredOut() { // Arrange var action1 = new RouteValueDictionary( new { controller = "Foo", action = "Bar", }); var action2 = new RouteValueDictionary( new { controller = "Foo", action = "Baz", }); // Doesn't match default var expectedDefaults = new RouteValueDictionary( new { controller = "Foo", action = "Bar", }); var actionDescriptorCollection = GetActionDescriptorCollection(action1, action2); var dataSource = CreateMvcEndpointDataSource(actionDescriptorCollection); dataSource.ConventionalEndpointInfos.Add( CreateEndpointInfo( string.Empty, "Blog/{*slug}", defaults: new RouteValueDictionary(new { controller = "Foo", action = "Bar" }))); // Act var endpoints = dataSource.Endpoints; // Assert var endpoint = Assert.Single(endpoints); var matcherEndpoint = Assert.IsType(endpoint); Assert.Equal("Blog/{*slug}", matcherEndpoint.RoutePattern.RawText); AssertIsSubset(expectedDefaults, matcherEndpoint.RoutePattern.Defaults); } [Fact] public void RequiredValues_HavingNull_AndNotPresentInDefaultValues_IsAddedToDefaultValues() { // Arrange var requiredValues = new RouteValueDictionary( new { area = (string)null, controller = "Foo", action = "Bar", page = (string)null }); var expectedDefaults = requiredValues; var actionDescriptorCollection = GetActionDescriptorCollection(requiredValues: requiredValues); var dataSource = CreateMvcEndpointDataSource(actionDescriptorCollection); dataSource.ConventionalEndpointInfos.Add( CreateEndpointInfo(string.Empty, "{controller=Home}/{action=Index}")); // Act var endpoints = dataSource.Endpoints; // Assert var endpoint = Assert.Single(endpoints); var matcherEndpoint = Assert.IsType(endpoint); Assert.Equal("Foo/Bar", matcherEndpoint.RoutePattern.RawText); AssertIsSubset(expectedDefaults, matcherEndpoint.RoutePattern.Defaults); } private MvcEndpointDataSource CreateMvcEndpointDataSource( IActionDescriptorCollectionProvider actionDescriptorCollectionProvider = null, MvcEndpointInvokerFactory mvcEndpointInvokerFactory = null, IEnumerable actionDescriptorChangeProviders = null) { if (actionDescriptorCollectionProvider == null) { var mockDescriptorProvider = new Mock(); mockDescriptorProvider.Setup(m => m.ActionDescriptors).Returns(new ActionDescriptorCollection(new List(), 0)); actionDescriptorCollectionProvider = mockDescriptorProvider.Object; } var serviceProviderMock = new Mock(); serviceProviderMock.Setup(m => m.GetService(typeof(IActionDescriptorCollectionProvider))).Returns(actionDescriptorCollectionProvider); var dataSource = new MvcEndpointDataSource( actionDescriptorCollectionProvider, mvcEndpointInvokerFactory ?? new MvcEndpointInvokerFactory(new ActionInvokerFactory(Array.Empty())), actionDescriptorChangeProviders ?? Array.Empty(), serviceProviderMock.Object); return dataSource; } private MvcEndpointInfo CreateEndpointInfo( string name, string template, RouteValueDictionary defaults = null, IDictionary constraints = null, RouteValueDictionary dataTokens = null) { var serviceCollection = new ServiceCollection(); serviceCollection.AddRouting(); var routeOptionsSetup = new MvcCoreRouteOptionsSetup(); serviceCollection.Configure(routeOptionsSetup.Configure); var serviceProvider = serviceCollection.BuildServiceProvider(); var parameterPolicyFactory = serviceProvider.GetRequiredService(); return new MvcEndpointInfo(name, template, defaults, constraints, dataTokens, parameterPolicyFactory); } private IActionDescriptorCollectionProvider GetActionDescriptorCollection(params object[] requiredValues) { var actionDescriptors = new List(); foreach (var requiredValue in requiredValues) { actionDescriptors.Add(CreateActionDescriptor(requiredValue)); } var actionDescriptorCollectionProviderMock = new Mock(); actionDescriptorCollectionProviderMock .Setup(m => m.ActionDescriptors) .Returns(new ActionDescriptorCollection(actionDescriptors, version: 0)); return actionDescriptorCollectionProviderMock.Object; } private ActionDescriptor CreateActionDescriptor(string controller, string action, string area = null) { return CreateActionDescriptor(new { controller = controller, action = action, area = area }); } private ActionDescriptor CreateActionDescriptor(object requiredValues) { var actionDescriptor = new ActionDescriptor(); var routeValues = new RouteValueDictionary(requiredValues); foreach (var kvp in routeValues) { actionDescriptor.RouteValues[kvp.Key] = kvp.Value?.ToString(); } return actionDescriptor; } private void AssertIsSubset( IReadOnlyDictionary subset, IReadOnlyDictionary fullSet) { foreach (var subsetPair in subset) { var isPresent = fullSet.TryGetValue(subsetPair.Key, out var fullSetPairValue); Assert.True(isPresent); Assert.Equal(subsetPair.Value, fullSetPairValue); } } } }