// 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.Collections.Generic; using System.Linq; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing.Internal; using Microsoft.AspNetCore.Routing.Patterns; using Microsoft.AspNetCore.Routing.TestObjects; using Microsoft.AspNetCore.Routing.Tree; using Xunit; namespace Microsoft.AspNetCore.Routing { public class RouteValuesAddressSchemeTest { [Fact] public void GetOutboundMatches_GetsNamedMatchesFor_EndpointsHaving_IRouteNameMetadata() { // Arrange var endpoint1 = CreateEndpoint("/a", routeName: "other"); var endpoint2 = CreateEndpoint("/a", routeName: "named"); // Act var addressScheme = CreateAddressScheme(endpoint1, endpoint2); // Assert Assert.NotNull(addressScheme.AllMatches); Assert.Equal(2, addressScheme.AllMatches.Count()); Assert.NotNull(addressScheme.NamedMatches); Assert.True(addressScheme.NamedMatches.TryGetValue("named", out var namedMatches)); var namedMatch = Assert.Single(namedMatches); var actual = Assert.IsType(namedMatch.Match.Entry.Data); Assert.Same(endpoint2, actual); } [Fact] public void GetOutboundMatches_GroupsMultipleEndpoints_WithSameName() { // Arrange var endpoint1 = CreateEndpoint("/a", routeName: "other"); var endpoint2 = CreateEndpoint("/a", routeName: "named"); var endpoint3 = CreateEndpoint("/b", routeName: "named"); // Act var addressScheme = CreateAddressScheme(endpoint1, endpoint2, endpoint3); // Assert Assert.NotNull(addressScheme.AllMatches); Assert.Equal(3, addressScheme.AllMatches.Count()); Assert.NotNull(addressScheme.NamedMatches); Assert.True(addressScheme.NamedMatches.TryGetValue("named", out var namedMatches)); Assert.Equal(2, namedMatches.Count); Assert.Same(endpoint2, Assert.IsType(namedMatches[0].Match.Entry.Data)); Assert.Same(endpoint3, Assert.IsType(namedMatches[1].Match.Entry.Data)); } [Fact] public void GetOutboundMatches_GroupsMultipleEndpoints_WithSameName_IgnoringCase() { // Arrange var endpoint1 = CreateEndpoint("/a", routeName: "other"); var endpoint2 = CreateEndpoint("/a", routeName: "named"); var endpoint3 = CreateEndpoint("/b", routeName: "NaMed"); // Act var addressScheme = CreateAddressScheme(endpoint1, endpoint2, endpoint3); // Assert Assert.NotNull(addressScheme.AllMatches); Assert.Equal(3, addressScheme.AllMatches.Count()); Assert.NotNull(addressScheme.NamedMatches); Assert.True(addressScheme.NamedMatches.TryGetValue("named", out var namedMatches)); Assert.Equal(2, namedMatches.Count); Assert.Same(endpoint2, Assert.IsType(namedMatches[0].Match.Entry.Data)); Assert.Same(endpoint3, Assert.IsType(namedMatches[1].Match.Entry.Data)); } [Fact] public void EndpointDataSource_ChangeCallback_Refreshes_OutboundMatches() { // Arrange 1 var endpoint1 = CreateEndpoint("/a", metadataRequiredValues: new { }); var dynamicDataSource = new DynamicEndpointDataSource(new[] { endpoint1 }); // Act 1 var addressScheme = new CustomRouteValuesBasedAddressScheme(new CompositeEndpointDataSource(new[] { dynamicDataSource })); // Assert 1 Assert.NotNull(addressScheme.AllMatches); var match = Assert.Single(addressScheme.AllMatches); var actual = Assert.IsType(match.Entry.Data); Assert.Same(endpoint1, actual); // Arrange 2 var endpoint2 = CreateEndpoint("/b", metadataRequiredValues: new { }); // Act 2 // Trigger change dynamicDataSource.AddEndpoint(endpoint2); // Arrange 2 var endpoint3 = CreateEndpoint("/c", metadataRequiredValues: new { }); // Act 2 // Trigger change dynamicDataSource.AddEndpoint(endpoint3); // Arrange 3 var endpoint4 = CreateEndpoint("/d", metadataRequiredValues: new { }); // Act 3 // Trigger change dynamicDataSource.AddEndpoint(endpoint4); // Assert 3 Assert.NotNull(addressScheme.AllMatches); Assert.Collection( addressScheme.AllMatches, (m) => { actual = Assert.IsType(m.Entry.Data); Assert.Same(endpoint1, actual); }, (m) => { actual = Assert.IsType(m.Entry.Data); Assert.Same(endpoint2, actual); }, (m) => { actual = Assert.IsType(m.Entry.Data); Assert.Same(endpoint3, actual); }, (m) => { actual = Assert.IsType(m.Entry.Data); Assert.Same(endpoint4, actual); }); } [Fact] public void FindEndpoints_LookedUpByCriteria_NoMatch() { // Arrange var endpoint1 = CreateEndpoint( "api/orders/{id}/{name?}/{urgent=true}/{zipCode}", defaults: new { zipCode = 3510 }, metadataRequiredValues: new { id = 7 }); var endpoint2 = CreateEndpoint( "api/orders/{id}/{name?}/{urgent=true}/{zipCode}", defaults: new { id = 12 }, metadataRequiredValues: new { zipCode = 3510 }); var addressScheme = CreateAddressScheme(endpoint1, endpoint2); // Act var foundEndpoints = addressScheme.FindEndpoints( new RouteValuesAddress { ExplicitValues = new RouteValueDictionary(new { id = 8 }), AmbientValues = new RouteValueDictionary(new { urgent = false }), }); // Assert Assert.Empty(foundEndpoints); } [Fact] public void FindEndpoints_LookedUpByCriteria_OneMatch() { // Arrange var endpoint1 = CreateEndpoint( "api/orders/{id}/{name?}/{urgent=true}/{zipCode}", defaults: new { zipCode = 3510 }, metadataRequiredValues: new { id = 7 }); var endpoint2 = CreateEndpoint( "api/orders/{id}/{name?}/{urgent=true}/{zipCode}", defaults: new { id = 12 }); var addressScheme = CreateAddressScheme(endpoint1, endpoint2); // Act var foundEndpoints = addressScheme.FindEndpoints( new RouteValuesAddress { ExplicitValues = new RouteValueDictionary(new { id = 7 }), AmbientValues = new RouteValueDictionary(new { zipCode = 3500 }), }); // Assert var actual = Assert.Single(foundEndpoints); Assert.Same(endpoint1, actual); } [Fact] public void FindEndpoints_LookedUpByCriteria_MultipleMatches() { // Arrange var endpoint1 = CreateEndpoint( "api/orders/{id}/{name?}/{urgent=true}/{zipCode}", defaults: new { zipCode = 3510 }, metadataRequiredValues: new { id = 7 }); var endpoint2 = CreateEndpoint( "api/orders/{id}/{name?}/{urgent}/{zipCode}", defaults: new { id = 12 }, metadataRequiredValues: new { id = 12 }); var endpoint3 = CreateEndpoint( "api/orders/{id}/{name?}/{urgent=true}/{zipCode}", defaults: new { id = 12 }, metadataRequiredValues: new { id = 12 }); var addressScheme = CreateAddressScheme(endpoint1, endpoint2, endpoint3); // Act var foundEndpoints = addressScheme.FindEndpoints( new RouteValuesAddress { ExplicitValues = new RouteValueDictionary(new { id = 12 }), AmbientValues = new RouteValueDictionary(new { zipCode = 3500 }), }); // Assert Assert.Collection(foundEndpoints, e => Assert.Equal(endpoint3, e), e => Assert.Equal(endpoint2, e)); } [Fact] public void FindEndpoints_LookedUpByCriteria_ExcludeEndpointWithoutRouteValuesAddressMetadata() { // Arrange var endpoint1 = CreateEndpoint( "api/orders/{id}/{name?}/{urgent=true}/{zipCode}", defaults: new { zipCode = 3510 }, metadataRequiredValues: new { id = 7 }); var endpoint2 = CreateEndpoint("test"); var addressScheme = CreateAddressScheme(endpoint1, endpoint2); // Act var foundEndpoints = addressScheme.FindEndpoints( new RouteValuesAddress { ExplicitValues = new RouteValueDictionary(new { id = 7 }), AmbientValues = new RouteValueDictionary(new { zipCode = 3500 }), }).ToList(); // Assert Assert.DoesNotContain(endpoint2, foundEndpoints); Assert.Contains(endpoint1, foundEndpoints); } [Fact] public void FindEndpoints_ReturnsEndpoint_WhenLookedUpByRouteName() { // Arrange var expected = CreateEndpoint( "api/orders/{id}", defaults: new { controller = "Orders", action = "GetById" }, metadataRequiredValues: new { controller = "Orders", action = "GetById" }, routeName: "OrdersApi"); var addressScheme = CreateAddressScheme(expected); // Act var foundEndpoints = addressScheme.FindEndpoints( new RouteValuesAddress { ExplicitValues = new RouteValueDictionary(new { id = 10 }), AmbientValues = new RouteValueDictionary(new { controller = "Home", action = "Index" }), RouteName = "OrdersApi" }); // Assert var actual = Assert.Single(foundEndpoints); Assert.Same(expected, actual); } [Fact] public void FindEndpoints_ReturnsEndpoint_UsingRoutePatternRequiredValues() { // Arrange var expected = CreateEndpoint( "api/orders/{id}", defaults: new { controller = "Orders", action = "GetById" }, routePatternRequiredValues: new { controller = "Orders", action = "GetById" }); var addressScheme = CreateAddressScheme(expected); // Act var foundEndpoints = addressScheme.FindEndpoints( new RouteValuesAddress { ExplicitValues = new RouteValueDictionary(new { id = 10 }), AmbientValues = new RouteValueDictionary(new { controller = "Orders", action = "GetById" }), }); // Assert var actual = Assert.Single(foundEndpoints); Assert.Same(expected, actual); } [Fact] public void FindEndpoints_AlwaysReturnsEndpointsByRouteName_IgnoringMissingRequiredParameterValues() { // Here 'id' is the required value. The endpoint addressScheme would always return an endpoint by looking up // name only. Its the link generator which uses these endpoints finally to generate a link or not // based on the required parameter values being present or not. // Arrange var expected = CreateEndpoint( "api/orders/{id}", defaults: new { controller = "Orders", action = "GetById" }, metadataRequiredValues: new { controller = "Orders", action = "GetById" }, routeName: "OrdersApi"); var addressScheme = CreateAddressScheme(expected); // Act var foundEndpoints = addressScheme.FindEndpoints( new RouteValuesAddress { ExplicitValues = new RouteValueDictionary(), AmbientValues = new RouteValueDictionary(), RouteName = "OrdersApi" }); // Assert var actual = Assert.Single(foundEndpoints); Assert.Same(expected, actual); } [Fact] public void GetOutboundMatches_DoesNotInclude_EndpointsWithSuppressLinkGenerationMetadata() { // Arrange var endpoint = CreateEndpoint( "/a", metadataCollection: new EndpointMetadataCollection(new[] { new SuppressLinkGenerationMetadata() })); // Act var addressScheme = CreateAddressScheme(endpoint); // Assert Assert.Empty(addressScheme.AllMatches); } [Fact] public void AddressScheme_UnsuppressedEndpoint_IsUsed() { // Arrange var endpoint = EndpointFactory.CreateRouteEndpoint( "/a", metadata: new object[] { new SuppressLinkGenerationMetadata(), new EncourageLinkGenerationMetadata(), new RouteValuesAddressMetadata(string.Empty), }); // Act var addressScheme = CreateAddressScheme(endpoint); // Assert Assert.Same(endpoint, Assert.Single(addressScheme.AllMatches).Entry.Data); } private CustomRouteValuesBasedAddressScheme CreateAddressScheme(params Endpoint[] endpoints) { return CreateAddressScheme(new DefaultEndpointDataSource(endpoints)); } private CustomRouteValuesBasedAddressScheme CreateAddressScheme(params EndpointDataSource[] dataSources) { return new CustomRouteValuesBasedAddressScheme(new CompositeEndpointDataSource(dataSources)); } private RouteEndpoint CreateEndpoint( string template, object defaults = null, object metadataRequiredValues = null, object routePatternRequiredValues = null, int order = 0, string routeName = null, EndpointMetadataCollection metadataCollection = null) { if (metadataCollection == null) { var metadata = new List(); if (!string.IsNullOrEmpty(routeName) || metadataRequiredValues != null) { metadata.Add(new RouteValuesAddressMetadata(routeName, new RouteValueDictionary(metadataRequiredValues))); } metadataCollection = new EndpointMetadataCollection(metadata); } return new RouteEndpoint( TestConstants.EmptyRequestDelegate, RoutePatternFactory.Parse(template, defaults, parameterPolicies: null, requiredValues: routePatternRequiredValues), order, metadataCollection, null); } private class CustomRouteValuesBasedAddressScheme : RouteValuesAddressScheme { public CustomRouteValuesBasedAddressScheme(CompositeEndpointDataSource dataSource) : base(dataSource) { } public IEnumerable AllMatches { get; private set; } public IDictionary> NamedMatches { get; private set; } protected override (List, Dictionary>) GetOutboundMatches() { var matches = base.GetOutboundMatches(); AllMatches = matches.Item1; NamedMatches = matches.Item2; return matches; } } private class EncourageLinkGenerationMetadata : ISuppressLinkGenerationMetadata { public bool SuppressLinkGeneration => false; } } }