File: Circuits\ComponentHubTest.cs
Web Access
Project: src\src\Components\Server\test\Microsoft.AspNetCore.Components.Server.Tests.csproj (Microsoft.AspNetCore.Components.Server.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.Diagnostics.CodeAnalysis;
using System.Security.Claims;
using System.Text.RegularExpressions;
using Microsoft.AspNetCore.Components.Server.Circuits;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.AspNetCore.SignalR;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using Moq;
 
namespace Microsoft.AspNetCore.Components.Server;
 
public class ComponentHubTest
{
    [Fact]
    public async Task CannotStartMultipleCircuits()
    {
        var (mockClientProxy, hub) = InitializeComponentHub();
        var circuitSecret = await hub.StartCircuit("https://localhost:5000", "https://localhost:5000/subdir", "{}", null);
        Assert.NotNull(circuitSecret);
 
        var circuit2Secret = await hub.StartCircuit("https://localhost:5000", "https://localhost:5000/subdir", "{}", null);
        Assert.Null(circuit2Secret);
 
        var errorMessage = "The circuit host '.*?' has already been initialized.";
        mockClientProxy.Verify(m => m.SendCoreAsync("JS.Error", It.Is<object[]>(s => Regex.Match((string)s[0], errorMessage).Success), It.IsAny<CancellationToken>()), Times.Once());
    }
 
    [Fact]
    public async Task StartCircuitFailsWithNullData()
    {
        var (mockClientProxy, hub) = InitializeComponentHub();
        var circuitSecret = await hub.StartCircuit(null, null, "foo", null);
 
        Assert.Null(circuitSecret);
        var errorMessage = "The uris provided are invalid.";
        mockClientProxy.Verify(m => m.SendCoreAsync("JS.Error", new[] { errorMessage }, It.IsAny<CancellationToken>()), Times.Once());
    }
 
    [Fact]
    public async Task CannotInvokeJSInteropBeforeInitialization()
    {
        var (mockClientProxy, hub) = InitializeComponentHub();
 
        await hub.BeginInvokeDotNetFromJS("", "", "", 0, "");
 
        var errorMessage = "Circuit not initialized.";
        mockClientProxy.Verify(m => m.SendCoreAsync("JS.Error", new[] { errorMessage }, It.IsAny<CancellationToken>()), Times.Once());
    }
 
    [Fact]
    public async Task CannotInvokeJSInteropCallbackCompletionsBeforeInitialization()
    {
        var (mockClientProxy, hub) = InitializeComponentHub();
 
        await hub.EndInvokeJSFromDotNet(3, true, "[]");
 
        var errorMessage = "Circuit not initialized.";
        mockClientProxy.Verify(m => m.SendCoreAsync("JS.Error", new[] { errorMessage }, It.IsAny<CancellationToken>()), Times.Once());
    }
 
    [Fact]
    public async Task CannotInvokeOnRenderCompletedBeforeInitialization()
    {
        var (mockClientProxy, hub) = InitializeComponentHub();
 
        await hub.OnRenderCompleted(5, null);
 
        var errorMessage = "Circuit not initialized.";
        mockClientProxy.Verify(m => m.SendCoreAsync("JS.Error", new[] { errorMessage }, It.IsAny<CancellationToken>()), Times.Once());
    }
 
    [Fact]
    public async Task CannotInvokeOnLocationChangedBeforeInitialization()
    {
        var (mockClientProxy, hub) = InitializeComponentHub();
 
        await hub.OnLocationChanged("https://localhost:5000/subdir/page", null, false);
 
        var errorMessage = "Circuit not initialized.";
        mockClientProxy.Verify(m => m.SendCoreAsync("JS.Error", new[] { errorMessage }, It.IsAny<CancellationToken>()), Times.Once());
    }
 
    [Fact]
    public async Task CannotInvokeOnLocationChangingBeforeInitialization()
    {
        var (mockClientProxy, hub) = InitializeComponentHub();
 
        await hub.OnLocationChanging(0, "https://localhost:5000/subdir/page", null, false);
 
        var errorMessage = "Circuit not initialized.";
        mockClientProxy.Verify(m => m.SendCoreAsync("JS.Error", new[] { errorMessage }, It.IsAny<CancellationToken>()), Times.Once());
    }
 
    private static (Mock<ISingleClientProxy>, ComponentHub) InitializeComponentHub()
    {
        var ephemeralDataProtectionProvider = new EphemeralDataProtectionProvider();
        var circuitIdFactory = new CircuitIdFactory(ephemeralDataProtectionProvider);
        var circuitFactory = new TestCircuitFactory(
            new Mock<IServiceScopeFactory>().Object,
            NullLoggerFactory.Instance,
            circuitIdFactory,
            Options.Create(new CircuitOptions()));
        var circuitRegistry = new CircuitRegistry(
            Options.Create(new CircuitOptions()),
            NullLogger<CircuitRegistry>.Instance,
            circuitIdFactory);
        var serializer = new TestServerComponentDeserializer();
        var circuitHandleRegistry = new TestCircuitHandleRegistry();
        var hub = new ComponentHub(
            serializer: serializer,
            dataProtectionProvider: ephemeralDataProtectionProvider,
            circuitFactory: circuitFactory,
            circuitIdFactory: circuitIdFactory,
            circuitRegistry: circuitRegistry,
            circuitHandleRegistry: circuitHandleRegistry,
            logger: NullLogger<ComponentHub>.Instance);
 
        // Here we mock out elements of the Hub that are typically configured
        // by SignalR as clients connect to the hub.
        var mockCaller = new Mock<IHubCallerClients>();
        var mockClientProxy = new Mock<ISingleClientProxy>();
        mockCaller.Setup(x => x.Caller).Returns(mockClientProxy.Object);
        hub.Clients = mockCaller.Object;
        var mockContext = new Mock<HubCallerContext>();
        mockContext.Setup(x => x.ConnectionId).Returns("123");
        hub.Context = mockContext.Object;
 
        return (mockClientProxy, hub);
    }
 
    private class TestCircuitHandleRegistry : ICircuitHandleRegistry
    {
        private bool circuitSet = false;
 
        public CircuitHandle GetCircuitHandle(IDictionary<object, object> circuitHandles, object circuitKey)
        {
            return null;
        }
 
        public CircuitHost GetCircuit(IDictionary<object, object> circuitHandles, object circuitKey)
        {
            if (circuitSet)
            {
                var serviceScope = new Mock<IServiceScope>();
                var circuitHost = TestCircuitHost.Create(
                    serviceScope: new AsyncServiceScope(serviceScope.Object));
                return circuitHost;
            }
            return null;
        }
 
        public void SetCircuit(IDictionary<object, object> circuitHandles, object circuitKey, CircuitHost circuitHost)
        {
            circuitSet = true;
            return;
        }
    }
 
    private class TestServerComponentDeserializer : IServerComponentDeserializer
    {
        public bool TryDeserializeComponentDescriptorCollection(string serializedComponentRecords, out List<ComponentDescriptor> descriptors)
        {
            descriptors = default;
            return true;
        }
 
        public bool TryDeserializeRootComponentOperations(string serializedComponentOperations, out RootComponentOperationBatch operationsWithDescriptors)
        {
            operationsWithDescriptors = default;
            return true;
        }
    }
 
    private class TestCircuitFactory : ICircuitFactory
    {
        public TestCircuitFactory(
        IServiceScopeFactory scopeFactory,
        ILoggerFactory loggerFactory,
        CircuitIdFactory circuitIdFactory,
        IOptions<CircuitOptions> options)
        { }
 
        // Implement a `CreateCircuitHostAsync` that mocks the construction
        // of the CircuitHost.
        public ValueTask<CircuitHost> CreateCircuitHostAsync(
            IReadOnlyList<ComponentDescriptor> components,
            CircuitClientProxy client,
            string baseUri,
            string uri,
            ClaimsPrincipal user,
            IPersistentComponentStateStore store)
        {
            var serviceScope = new Mock<IServiceScope>();
            var circuitHost = TestCircuitHost.Create(serviceScope: new AsyncServiceScope(serviceScope.Object));
            return ValueTask.FromResult(circuitHost);
        }
    }
}