File: Infrastructure\DynamicPageEndpointMatcherPolicyTest.cs
Web Access
Project: src\src\Mvc\Mvc.RazorPages\test\Microsoft.AspNetCore.Mvc.RazorPages.Test.csproj (Microsoft.AspNetCore.Mvc.RazorPages.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.Http;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.Routing;
using Microsoft.AspNetCore.Routing;
using Microsoft.AspNetCore.Routing.Matching;
using Microsoft.Extensions.DependencyInjection;
using Moq;
 
namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure;
 
public class DynamicPageEndpointMatcherPolicyTest
{
    public DynamicPageEndpointMatcherPolicyTest()
    {
        var actions = new ActionDescriptor[]
        {
                new PageActionDescriptor()
                {
                    RouteValues = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
                    {
                        ["page"] = "/Index",
                    },
                    DisplayName = "/Index",
                },
                new PageActionDescriptor()
                {
                    RouteValues = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
                    {
                        ["page"] = "/About",
                    },
                    DisplayName = "/About"
                },
        };
 
        PageEndpoints = new[]
        {
                new Endpoint(_ => Task.CompletedTask, new EndpointMetadataCollection(actions[0]), "Test1"),
                new Endpoint(_ => Task.CompletedTask, new EndpointMetadataCollection(actions[1]), "Test2"),
            };
 
        DynamicEndpoint = new Endpoint(
            _ => Task.CompletedTask,
            new EndpointMetadataCollection(new object[]
            {
                    new DynamicPageRouteValueTransformerMetadata(typeof(CustomTransformer), State),
                    new PageEndpointDataSourceIdMetadata(1),
            }),
            "dynamic");
 
        DataSource = new DefaultEndpointDataSource(PageEndpoints);
 
        SelectorCache = new TestDynamicPageEndpointSelectorCache(DataSource);
 
        var services = new ServiceCollection();
        services.AddRouting();
        services.AddTransient<CustomTransformer>(s =>
        {
            var transformer = new CustomTransformer();
            transformer.Transform = (c, values, state) => Transform(c, values, state);
            transformer.Filter = (c, values, state, endpoints) => Filter(c, values, state, endpoints);
            return transformer;
        });
        Services = services.BuildServiceProvider();
 
        Comparer = Services.GetRequiredService<EndpointMetadataComparer>();
 
        LoadedEndpoints = new[]
        {
                new Endpoint(_ => Task.CompletedTask, EndpointMetadataCollection.Empty, "Test1"),
                new Endpoint(_ => Task.CompletedTask, EndpointMetadataCollection.Empty, "Test2"),
                new Endpoint(_ => Task.CompletedTask, EndpointMetadataCollection.Empty, "ReplacedLoaded")
            };
 
        var loader = new Mock<PageLoader>();
        loader
            .Setup(l => l.LoadAsync(It.IsAny<PageActionDescriptor>(), It.IsAny<EndpointMetadataCollection>()))
            .Returns((PageActionDescriptor descriptor, EndpointMetadataCollection endpoint) => Task.FromResult(new CompiledPageActionDescriptor
            {
                Endpoint = descriptor.DisplayName switch
                {
                    "/Index" => LoadedEndpoints[0],
                    "/About" => LoadedEndpoints[1],
                    "/ReplacedEndpoint" => LoadedEndpoints[2],
                    _ => throw new InvalidOperationException($"Invalid endpoint '{descriptor.DisplayName}'.")
                }
            }));
        Loader = loader.Object;
    }
 
    private EndpointMetadataComparer Comparer { get; }
 
    private DefaultEndpointDataSource DataSource { get; }
 
    private Endpoint[] PageEndpoints { get; }
 
    private Endpoint DynamicEndpoint { get; }
 
    private Endpoint[] LoadedEndpoints { get; }
 
    private PageLoader Loader { get; }
 
    private DynamicPageEndpointSelectorCache SelectorCache { get; }
 
    private object State { get; }
 
    private IServiceProvider Services { get; }
 
    private Func<HttpContext, RouteValueDictionary, object, ValueTask<RouteValueDictionary>> Transform { get; set; }
 
    private Func<HttpContext, RouteValueDictionary, object, IReadOnlyList<Endpoint>, ValueTask<IReadOnlyList<Endpoint>>> Filter { get; set; } = (_, __, ___, e) => new ValueTask<IReadOnlyList<Endpoint>>(e);
 
    [Fact]
    public async Task ApplyAsync_NoMatch()
    {
        // Arrange
        var policy = new DynamicPageEndpointMatcherPolicy(SelectorCache, Loader, Comparer);
 
        var endpoints = new[] { DynamicEndpoint, };
        var values = new RouteValueDictionary[] { null, };
        var scores = new[] { 0, };
 
        var candidates = new CandidateSet(endpoints, values, scores);
        candidates.SetValidity(0, false);
 
        Transform = (c, values, state) =>
        {
            throw new InvalidOperationException();
        };
 
        var httpContext = new DefaultHttpContext()
        {
            RequestServices = Services,
        };
 
        // Act
        await policy.ApplyAsync(httpContext, candidates);
 
        // Assert
        Assert.False(candidates.IsValidCandidate(0));
    }
 
    [Fact]
    public async Task ApplyAsync_HasMatchNoEndpointFound()
    {
        // Arrange
        var policy = new DynamicPageEndpointMatcherPolicy(SelectorCache, Loader, Comparer);
 
        var endpoints = new[] { DynamicEndpoint, };
        var values = new RouteValueDictionary[] { null, };
        var scores = new[] { 0, };
 
        var candidates = new CandidateSet(endpoints, values, scores);
 
        Transform = (c, values, state) =>
        {
            return new ValueTask<RouteValueDictionary>(new RouteValueDictionary());
        };
 
        var httpContext = new DefaultHttpContext()
        {
            RequestServices = Services,
        };
 
        // Act
        await policy.ApplyAsync(httpContext, candidates);
 
        // Assert
        Assert.Null(candidates[0].Endpoint);
        Assert.Null(candidates[0].Values);
        Assert.False(candidates.IsValidCandidate(0));
    }
 
    [Fact]
    public async Task ApplyAsync_HasMatchFindsEndpoint_WithoutRouteValues()
    {
        // Arrange
        var policy = new DynamicPageEndpointMatcherPolicy(SelectorCache, Loader, Comparer);
 
        var endpoints = new[] { DynamicEndpoint, };
        var values = new RouteValueDictionary[] { null, };
        var scores = new[] { 0, };
 
        var candidates = new CandidateSet(endpoints, values, scores);
 
        Transform = (c, values, state) =>
        {
            return new ValueTask<RouteValueDictionary>(new RouteValueDictionary(new
            {
                page = "/Index",
            }));
        };
 
        var httpContext = new DefaultHttpContext()
        {
            RequestServices = Services,
        };
 
        // Act
        await policy.ApplyAsync(httpContext, candidates);
 
        // Assert
        Assert.Same(LoadedEndpoints[0], candidates[0].Endpoint);
        Assert.Collection(
            candidates[0].Values.OrderBy(kvp => kvp.Key),
            kvp =>
            {
                Assert.Equal("page", kvp.Key);
                Assert.Equal("/Index", kvp.Value);
            });
        Assert.True(candidates.IsValidCandidate(0));
    }
 
    [Fact]
    public async Task ApplyAsync_HasMatchFindsEndpoint_WithRouteValues()
    {
        // Arrange
        var policy = new DynamicPageEndpointMatcherPolicy(SelectorCache, Loader, Comparer);
 
        var endpoints = new[] { DynamicEndpoint, };
        var values = new RouteValueDictionary[] { new RouteValueDictionary(new { slug = "test", }), };
        var scores = new[] { 0, };
 
        var candidates = new CandidateSet(endpoints, values, scores);
 
        Transform = (c, values, state) =>
        {
            return new ValueTask<RouteValueDictionary>(new RouteValueDictionary(new
            {
                page = "/Index",
                state
            }));
        };
 
        var httpContext = new DefaultHttpContext()
        {
            RequestServices = Services,
        };
 
        // Act
        await policy.ApplyAsync(httpContext, candidates);
 
        // Assert
        Assert.Same(LoadedEndpoints[0], candidates[0].Endpoint);
        Assert.Collection(
            candidates[0].Values.OrderBy(kvp => kvp.Key),
            kvp =>
            {
                Assert.Equal("page", kvp.Key);
                Assert.Equal("/Index", kvp.Value);
            },
            kvp =>
            {
                Assert.Equal("slug", kvp.Key);
                Assert.Equal("test", kvp.Value);
            },
            kvp =>
            {
                Assert.Equal("state", kvp.Key);
                Assert.Same(State, kvp.Value);
            });
        Assert.True(candidates.IsValidCandidate(0));
    }
 
    [Fact]
    public async Task ApplyAsync_Throws_ForTransformersWithInvalidLifetime()
    {
        // Arrange
        var policy = new DynamicPageEndpointMatcherPolicy(SelectorCache, Loader, Comparer);
 
        var endpoints = new[] { DynamicEndpoint, };
        var values = new RouteValueDictionary[] { new RouteValueDictionary(new { slug = "test", }), };
        var scores = new[] { 0, };
 
        var candidates = new CandidateSet(endpoints, values, scores);
 
        Transform = (c, values, state) =>
        {
            return new ValueTask<RouteValueDictionary>(new RouteValueDictionary(new
            {
                page = "/Index",
                state
            }));
        };
 
        var httpContext = new DefaultHttpContext()
        {
            RequestServices = new ServiceCollection().AddScoped(sp => new CustomTransformer() { State = "Invalid" }).BuildServiceProvider()
        };
 
        // Act & Assert
        await Assert.ThrowsAsync<InvalidOperationException>(() => policy.ApplyAsync(httpContext, candidates));
    }
 
    [Fact]
    public async Task ApplyAsync_CanDiscardFoundEndpoints()
    {
        // Arrange
        var policy = new DynamicPageEndpointMatcherPolicy(SelectorCache, Loader, Comparer);
 
        var endpoints = new[] { DynamicEndpoint, };
        var values = new RouteValueDictionary[] { new RouteValueDictionary(new { slug = "test", }), };
        var scores = new[] { 0, };
 
        var candidates = new CandidateSet(endpoints, values, scores);
 
        Transform = (c, values, state) =>
        {
            return new ValueTask<RouteValueDictionary>(new RouteValueDictionary(new
            {
                page = "/Index",
                state
            }));
        };
 
        Filter = (c, values, state, endpoints) =>
        {
            return new ValueTask<IReadOnlyList<Endpoint>>(Array.Empty<Endpoint>());
        };
 
        var httpContext = new DefaultHttpContext()
        {
            RequestServices = Services,
        };
 
        // Act
        await policy.ApplyAsync(httpContext, candidates);
 
        // Assert
        Assert.False(candidates.IsValidCandidate(0));
    }
 
    [Fact]
    public async Task ApplyAsync_CanReplaceFoundEndpoints()
    {
        // Arrange
        var policy = new DynamicPageEndpointMatcherPolicy(SelectorCache, Loader, Comparer);
 
        var endpoints = new[] { DynamicEndpoint, };
        var values = new RouteValueDictionary[] { new RouteValueDictionary(new { slug = "test", }), };
        var scores = new[] { 0, };
 
        var candidates = new CandidateSet(endpoints, values, scores);
 
        Transform = (c, values, state) =>
        {
            return new ValueTask<RouteValueDictionary>(new RouteValueDictionary(new
            {
                page = "/Index",
                state
            }));
        };
 
        Filter = (c, values, state, endpoints) => new ValueTask<IReadOnlyList<Endpoint>>(new[]
        {
                new Endpoint((ctx) => Task.CompletedTask, new EndpointMetadataCollection(new PageActionDescriptor()
                {
                    DisplayName = "/ReplacedEndpoint",
                }), "ReplacedEndpoint")
            });
 
        var httpContext = new DefaultHttpContext()
        {
            RequestServices = Services,
        };
 
        // Act
        await policy.ApplyAsync(httpContext, candidates);
 
        // Assert
        Assert.Equal(1, candidates.Count);
        Assert.Collection(
            candidates[0].Values.OrderBy(kvp => kvp.Key),
            kvp =>
            {
                Assert.Equal("page", kvp.Key);
                Assert.Equal("/Index", kvp.Value);
            },
            kvp =>
            {
                Assert.Equal("slug", kvp.Key);
                Assert.Equal("test", kvp.Value);
            },
            kvp =>
            {
                Assert.Equal("state", kvp.Key);
                Assert.Same(State, kvp.Value);
            });
        Assert.Equal("ReplacedLoaded", candidates[0].Endpoint.DisplayName);
        Assert.True(candidates.IsValidCandidate(0));
    }
 
    [Fact]
    public async Task ApplyAsync_CanExpandTheListOfFoundEndpoints()
    {
        // Arrange
        var policy = new DynamicPageEndpointMatcherPolicy(SelectorCache, Loader, Comparer);
 
        var endpoints = new[] { DynamicEndpoint, };
        var values = new RouteValueDictionary[] { new RouteValueDictionary(new { slug = "test", }), };
        var scores = new[] { 0, };
 
        var candidates = new CandidateSet(endpoints, values, scores);
 
        Transform = (c, values, state) =>
        {
            return new ValueTask<RouteValueDictionary>(new RouteValueDictionary(new
            {
                page = "/Index",
                state
            }));
        };
 
        Filter = (c, values, state, endpoints) => new ValueTask<IReadOnlyList<Endpoint>>(new[]
        {
                PageEndpoints[0], PageEndpoints[1]
            });
 
        var httpContext = new DefaultHttpContext()
        {
            RequestServices = Services,
        };
 
        // Act
        await policy.ApplyAsync(httpContext, candidates);
 
        // Assert
        Assert.Equal(2, candidates.Count);
        Assert.True(candidates.IsValidCandidate(0));
        Assert.True(candidates.IsValidCandidate(1));
        Assert.Same(LoadedEndpoints[0], candidates[0].Endpoint);
        Assert.Same(LoadedEndpoints[1], candidates[1].Endpoint);
    }
 
    private class TestDynamicPageEndpointSelectorCache : DynamicPageEndpointSelectorCache
    {
        public TestDynamicPageEndpointSelectorCache(EndpointDataSource dataSource)
        {
            AddDataSource(dataSource, 1);
        }
    }
 
    private class CustomTransformer : DynamicRouteValueTransformer
    {
        public Func<HttpContext, RouteValueDictionary, object, ValueTask<RouteValueDictionary>> Transform { get; set; }
 
        public Func<HttpContext, RouteValueDictionary, object, IReadOnlyList<Endpoint>, ValueTask<IReadOnlyList<Endpoint>>> Filter { get; set; }
 
        public override ValueTask<IReadOnlyList<Endpoint>> FilterAsync(HttpContext httpContext, RouteValueDictionary values, IReadOnlyList<Endpoint> endpoints)
        {
            return Filter(httpContext, values, State, endpoints);
        }
 
        public override ValueTask<RouteValueDictionary> TransformAsync(HttpContext httpContext, RouteValueDictionary values)
        {
            return Transform(httpContext, values, State);
        }
    }
}