File: RazorComponentEndpointDataSourceTest.cs
Web Access
Project: src\src\Components\Endpoints\test\Microsoft.AspNetCore.Components.Endpoints.Tests.csproj (Microsoft.AspNetCore.Components.Endpoints.Tests)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Reflection;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Components.Discovery;
using Microsoft.AspNetCore.Components.Endpoints.Infrastructure;
using Microsoft.AspNetCore.Components.Endpoints.Tests;
using Microsoft.AspNetCore.Components.Web;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Routing;
using Microsoft.AspNetCore.Routing.Patterns;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.FileProviders;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Testing;
 
namespace Microsoft.AspNetCore.Components.Endpoints;
 
public class RazorComponentEndpointDataSourceTest
{
    [Fact]
    public void RegistersEndpoints()
    {
        var endpointDataSource = CreateDataSource<App>();
 
        var endpoints = endpointDataSource.Endpoints;
 
        Assert.Equal(3, endpoints.Count);
    }
 
    [Fact]
    public void NoDiscoveredModesDefaultsToStatic()
    {
 
        var builder = CreateBuilder();
        var services = CreateServices(typeof(ServerEndpointProvider));
        var endpointDataSource = CreateDataSource<App>(builder, services);
 
        var endpoints = endpointDataSource.Endpoints;
 
        Assert.Single(endpoints);
    }
 
    // renderModes, providers, components, expectedEndpoints
    public static TheoryData<IComponentRenderMode[], Type[], Type[], string[]> ConfiguredAndDiscoveredRenderModes =>
        new()
        {
            // Auto component sets up server and wasm when available
            {
                Array.Empty<IComponentRenderMode>(),
                new []  { typeof(ServerEndpointProvider), typeof(WebassemblyEndpointProvider) },
                new []  { typeof(AutoComponent) },
                new []  { "/server", "/webassembly" }
            },
            // Auto component adds webassembly when available
            {
                new []  { RenderMode.InteractiveServer },
                new []  { typeof(ServerEndpointProvider), typeof(WebassemblyEndpointProvider) },
                new []  { typeof(AutoComponent) },
                new []  { "/server", "/webassembly" }
            },
            // Auto component adds server when available
            {
                new []  { RenderMode.InteractiveWebAssembly },
                new []  { typeof(ServerEndpointProvider), typeof(WebassemblyEndpointProvider) },
                new []  { typeof(AutoComponent) },
                new []  { "/server", "/webassembly" }
            },
            // Auto component does nothing because modes are explicitly configured
            {
                new IComponentRenderMode []  { RenderMode.InteractiveServer, RenderMode.InteractiveWebAssembly },
                new []  { typeof(ServerEndpointProvider), typeof(WebassemblyEndpointProvider) },
                new []  { typeof(AutoComponent) },
                new []  { "/server", "/webassembly" }
            },
            // Webassembly component wires up webassembly endpoints
            {
                Array.Empty<IComponentRenderMode>(),
                new []  { typeof(ServerEndpointProvider), typeof(WebassemblyEndpointProvider) },
                new []  { typeof(WebAssemblyComponent) },
                new []  { "/webassembly" }
            },
            // Webassembly component wires up webassembly endpoints in addition to server endpoints
            // as they were explicitly configured.
            {
                new []  { RenderMode.InteractiveServer },
                new []  { typeof(ServerEndpointProvider), typeof(WebassemblyEndpointProvider) },
                new []  { typeof(WebAssemblyComponent) },
                new []  { "/server", "/webassembly" }
            },
            // Webassembly component does nothing as webassembly is already configured explicitly.
            {
                new []  { RenderMode.InteractiveWebAssembly },
                new []  { typeof(ServerEndpointProvider), typeof(WebassemblyEndpointProvider) },
                new []  { typeof(WebAssemblyComponent) },
                new []  { "/webassembly" }
            },
            // Server and webassembly endpoints are added as they were explicitly configured.
            {
                new IComponentRenderMode [] { RenderMode.InteractiveServer, RenderMode.InteractiveWebAssembly },
                new []  { typeof(ServerEndpointProvider), typeof(WebassemblyEndpointProvider) },
                new []  { typeof(WebAssemblyComponent) },
                new []  { "/server", "/webassembly" }
            },
            // Server component wires up server components
            {
                Array.Empty<IComponentRenderMode>(),
                new []  { typeof(ServerEndpointProvider), typeof(WebassemblyEndpointProvider) },
                new []  { typeof(ServerComponent) },
                new []  { "/server" }
            },
            // Server component does nothing as server is already configured.
            {
                new []  { RenderMode.InteractiveServer },
                new []  { typeof(ServerEndpointProvider), typeof(WebassemblyEndpointProvider) },
                new []  { typeof(ServerComponent) },
                new []  { "/server" }
            },
            // Server component wires up server endpoints in addition to webassembly endpoints
            // that were explicitly configured.
            {
                new []  { RenderMode.InteractiveWebAssembly },
                new []  { typeof(ServerEndpointProvider), typeof(WebassemblyEndpointProvider) },
                new []  { typeof(ServerComponent) },
                new []  { "/server", "/webassembly" }
            },
            // Server component does nothing as server and webassembly endpoints were explicitly configured.
            {
                new IComponentRenderMode [] { RenderMode.InteractiveServer, RenderMode.InteractiveWebAssembly },
                new []  { typeof(ServerEndpointProvider), typeof(WebassemblyEndpointProvider) },
                new []  { typeof(ServerComponent) },
                new []  { "/server", "/webassembly" }
            },
        };
 
    // renderModes, providers, components, expectedEndpoints
    public static TheoryData<IComponentRenderMode[], Type[], Type[]> SetRenderModesFailures =>
        new()
        {
            // Missing server and webassembly, auto requires both.
            {
                Array.Empty<IComponentRenderMode>(),
                Array.Empty<Type>(),
                new []  { typeof(AutoComponent) }
            },
            // Missing webassembly, auto requires it.
            {
                Array.Empty<IComponentRenderMode>(),
                new []  { typeof(ServerEndpointProvider) },
                new []  { typeof(AutoComponent) }
            },
            // Missing server, auto requires it.
            {
                Array.Empty<IComponentRenderMode>(),
                new []  { typeof(WebassemblyEndpointProvider) },
                new []  { typeof(AutoComponent) }
            },
 
            // Missing server.
            {
                Array.Empty<IComponentRenderMode>(),
                Array.Empty<Type>(),
                new []  { typeof(ServerComponent) }
            },
            // Missing server.
            {
                Array.Empty<IComponentRenderMode>(),
                new []  { typeof(WebassemblyEndpointProvider) },
                new []  { typeof(ServerComponent) }
            },
            // Server explicitly configured and missing
            {
                new IComponentRenderMode[] { RenderMode.InteractiveServer },
                new []  { typeof(WebassemblyEndpointProvider) },
                Array.Empty<Type>()
            },
 
            // Missing webassembly
            {
                Array.Empty<IComponentRenderMode>(),
                Array.Empty<Type>(),
                new []  { typeof(WebAssemblyComponent) }
            },
 
            // Missing webassembly
            {
                Array.Empty<IComponentRenderMode>(),
                new []  { typeof(ServerEndpointProvider) },
                new []  { typeof(WebAssemblyComponent) }
            },
 
            // Webassembly explicitly configured and missing
            {
                new IComponentRenderMode[] { RenderMode.InteractiveWebAssembly },
                new []  { typeof(ServerEndpointProvider) },
                Array.Empty<Type>()
            },
        };
 
    private ComponentApplicationBuilder CreateBuilder(params Type[] types)
    {
        var builder = new ComponentApplicationBuilder();
        builder.AddLibrary(new AssemblyComponentLibraryDescriptor(
            "TestAssembly",
            Array.Empty<PageComponentBuilder>(),
            types.Select(t => new ComponentBuilder
            {
                AssemblyName = "TestAssembly",
                ComponentType = t,
                RenderMode = t.GetCustomAttribute<RenderModeAttribute>()
            }).ToArray()));
 
        return builder;
    }
 
    private IServiceProvider CreateServices(params Type[] types)
    {
        var services = new ServiceCollection();
        foreach (var type in types)
        {
            services.TryAddEnumerable(ServiceDescriptor.Singleton(typeof(RenderModeEndpointProvider), type));
        }
 
        services.AddSingleton<IWebHostEnvironment, TestWebHostEnvironment>();
        services.AddLogging();
 
        return services.BuildServiceProvider();
    }
 
    private RazorComponentEndpointDataSource<TComponent> CreateDataSource<TComponent>(
        ComponentApplicationBuilder builder = null,
        IServiceProvider services = null,
        IComponentRenderMode[] renderModes = null)
    {
        var result = new RazorComponentEndpointDataSource<TComponent>(
            builder ?? DefaultRazorComponentApplication<TComponent>.Instance.GetBuilder(),
            services?.GetService<IEnumerable<RenderModeEndpointProvider>>() ?? Enumerable.Empty<RenderModeEndpointProvider>(),
            new TestEndpointRouteBuilder(services ?? CreateServices()),
            new RazorComponentEndpointFactory(),
            new HotReloadService() { MetadataUpdateSupported = true });
 
        if (renderModes != null)
        {
            foreach (var mode in renderModes)
            {
                result.Options.ConfiguredRenderModes.Add(mode);
            }
        }
 
        return result;
    }
 
    [TestRenderMode<InteractiveServerRenderMode>]
    private class ServerComponent : ComponentBase { }
 
    [TestRenderMode<InteractiveAutoRenderMode>]
    private class AutoComponent : ComponentBase { }
 
    [TestRenderMode<InteractiveWebAssemblyRenderMode>]
    private class WebAssemblyComponent : ComponentBase { }
 
    private class ServerEndpointProvider : RenderModeEndpointProvider
    {
        public override IEnumerable<RouteEndpointBuilder> GetEndpointBuilders(IComponentRenderMode renderMode, IApplicationBuilder applicationBuilder)
        {
            yield return new RouteEndpointBuilder(
                (context) => Task.CompletedTask,
                RoutePatternFactory.Parse("/server"),
                0);
        }
 
        public override bool Supports(IComponentRenderMode renderMode) => renderMode is InteractiveServerRenderMode or InteractiveAutoRenderMode;
    }
 
    private class WebassemblyEndpointProvider : RenderModeEndpointProvider
    {
        public override IEnumerable<RouteEndpointBuilder> GetEndpointBuilders(IComponentRenderMode renderMode, IApplicationBuilder applicationBuilder)
        {
            yield return new RouteEndpointBuilder(
                (context) => Task.CompletedTask,
                RoutePatternFactory.Parse("/webassembly"),
                0);
        }
 
        public override bool Supports(IComponentRenderMode renderMode) => renderMode is InteractiveWebAssemblyRenderMode or InteractiveAutoRenderMode;
    }
 
    private class TestWebHostEnvironment : IWebHostEnvironment
    {
        public string ApplicationName { get; set; } = "TestApplication";
        public string EnvironmentName { get; set; } = "TestEnvironment";
        public string WebRootPath { get; set; } = "";
        public IFileProvider WebRootFileProvider { get => ContentRootFileProvider; set { } }
        public string ContentRootPath { get; set; } = Directory.GetCurrentDirectory();
        public IFileProvider ContentRootFileProvider { get; set; } = CreateTestFileProvider();
 
        private static TestFileProvider CreateTestFileProvider()
        {
            var provider = new TestFileProvider();
            provider.AddFile("site.css", "body { color: red; }");
            return provider;
        }
    }
 
    private class TestEndpointRouteBuilder : IEndpointRouteBuilder
    {
        private IServiceProvider _serviceProvider;
        private List<EndpointDataSource> _dataSources = new();
 
        public TestEndpointRouteBuilder(IServiceProvider serviceProvider) => _serviceProvider = serviceProvider;
 
        public IServiceProvider ServiceProvider => _serviceProvider;
 
        public ICollection<EndpointDataSource> DataSources => _dataSources;
 
        public IApplicationBuilder CreateApplicationBuilder() => new ApplicationBuilder(_serviceProvider);
    }
}
 
public class App : IComponent
{
    public void Attach(RenderHandle renderHandle)
    {
        throw new NotImplementedException();
    }
 
    public Task SetParametersAsync(ParameterView parameters)
    {
        throw new NotImplementedException();
    }
}
 
[Route("/")]
public class Index : IComponent
{
    public void Attach(RenderHandle renderHandle)
    {
        throw new NotImplementedException();
    }
 
    public Task SetParametersAsync(ParameterView parameters)
    {
        throw new NotImplementedException();
    }
}
 
[Route("/counter")]
public class Counter : IComponent
{
    public void Attach(RenderHandle renderHandle)
    {
        throw new NotImplementedException();
    }
 
    public Task SetParametersAsync(ParameterView parameters)
    {
        throw new NotImplementedException();
    }
}