// 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.Text.Encodings.Web; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Components.Browser; using Microsoft.AspNetCore.Components.Browser.Rendering; using Microsoft.AspNetCore.Components.Rendering; using Microsoft.AspNetCore.SignalR; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.JSInterop; using Moq; using Xunit; namespace Microsoft.AspNetCore.Components.Server.Circuits { public class CircuitHostTest { [Fact] public async Task DisposeAsync_DisposesResources() { // Arrange var serviceScope = new Mock(); var remoteRenderer = GetRemoteRenderer(Renderer.CreateDefaultDispatcher()); var circuitHost = TestCircuitHost.Create( Guid.NewGuid().ToString(), serviceScope.Object, remoteRenderer); // Act await circuitHost.DisposeAsync(); // Assert serviceScope.Verify(s => s.Dispose(), Times.Once()); Assert.True(remoteRenderer.Disposed); } [Fact] public async Task DisposeAsync_DisposesRendererWithinSynchronizationContext() { // Arrange var serviceScope = new Mock(); var remoteRenderer = GetRemoteRenderer(Renderer.CreateDefaultDispatcher()); var circuitHost = TestCircuitHost.Create( Guid.NewGuid().ToString(), serviceScope.Object, remoteRenderer); var component = new DispatcherComponent(circuitHost.Dispatcher); circuitHost.Renderer.AssignRootComponentId(component); var original = SynchronizationContext.Current; SynchronizationContext.SetSynchronizationContext(null); // Act & Assert try { Assert.Null(SynchronizationContext.Current); await circuitHost.DisposeAsync(); Assert.True(component.Called); Assert.Null(SynchronizationContext.Current); } finally { // Not sure if the line above messes up the xunit sync context, so just being cautious here. SynchronizationContext.SetSynchronizationContext(original); } } [Fact] public async Task InitializeAsync_InvokesHandlers() { // Arrange var cancellationToken = new CancellationToken(); var handler1 = new Mock(MockBehavior.Strict); var handler2 = new Mock(MockBehavior.Strict); var sequence = new MockSequence(); handler1 .InSequence(sequence) .Setup(h => h.OnCircuitOpenedAsync(It.IsAny(), cancellationToken)) .Returns(Task.CompletedTask) .Verifiable(); handler2 .InSequence(sequence) .Setup(h => h.OnCircuitOpenedAsync(It.IsAny(), cancellationToken)) .Returns(Task.CompletedTask) .Verifiable(); handler1 .InSequence(sequence) .Setup(h => h.OnConnectionUpAsync(It.IsAny(), cancellationToken)) .Returns(Task.CompletedTask) .Verifiable(); handler2 .InSequence(sequence) .Setup(h => h.OnConnectionUpAsync(It.IsAny(), cancellationToken)) .Returns(Task.CompletedTask) .Verifiable(); var circuitHost = TestCircuitHost.Create(handlers: new[] { handler1.Object, handler2.Object }); // Act await circuitHost.InitializeAsync(cancellationToken); // Assert handler1.VerifyAll(); handler2.VerifyAll(); } [Fact] public async Task InitializeAsync_ReportsOwnAsyncExceptions() { // Arrange var handler = new Mock(MockBehavior.Strict); var tcs = new TaskCompletionSource(); var reportedErrors = new List(); handler .Setup(h => h.OnCircuitOpenedAsync(It.IsAny(), It.IsAny())) .Returns(tcs.Task) .Verifiable(); var circuitHost = TestCircuitHost.Create(handlers: new[] { handler.Object }); circuitHost.UnhandledException += (sender, errorInfo) => { Assert.Same(circuitHost, sender); reportedErrors.Add(errorInfo); }; // Act var initializeAsyncTask = circuitHost.InitializeAsync(new CancellationToken()); // Assert: No synchronous exceptions handler.VerifyAll(); Assert.Empty(reportedErrors); // Act: Trigger async exception var ex = new InvalidTimeZoneException(); tcs.SetException(ex); // Assert: The top-level task still succeeds, because the intended usage // pattern is fire-and-forget. await initializeAsyncTask; // Assert: The async exception was reported via the side-channel Assert.Same(ex, reportedErrors.Single().ExceptionObject); Assert.False(reportedErrors.Single().IsTerminating); } [Fact] public async Task DisposeAsync_InvokesCircuitHandler() { // Arrange var cancellationToken = new CancellationToken(); var handler1 = new Mock(MockBehavior.Strict); var handler2 = new Mock(MockBehavior.Strict); var sequence = new MockSequence(); handler1 .InSequence(sequence) .Setup(h => h.OnConnectionDownAsync(It.IsAny(), cancellationToken)) .Returns(Task.CompletedTask) .Verifiable(); handler2 .InSequence(sequence) .Setup(h => h.OnConnectionDownAsync(It.IsAny(), cancellationToken)) .Returns(Task.CompletedTask) .Verifiable(); handler1 .InSequence(sequence) .Setup(h => h.OnCircuitClosedAsync(It.IsAny(), cancellationToken)) .Returns(Task.CompletedTask) .Verifiable(); handler2 .InSequence(sequence) .Setup(h => h.OnCircuitClosedAsync(It.IsAny(), cancellationToken)) .Returns(Task.CompletedTask) .Verifiable(); var circuitHost = TestCircuitHost.Create(handlers: new[] { handler1.Object, handler2.Object }); // Act await circuitHost.DisposeAsync(); // Assert handler1.VerifyAll(); handler2.VerifyAll(); } private static TestRemoteRenderer GetRemoteRenderer(IDispatcher dispatcher) { return new TestRemoteRenderer( Mock.Of(), new RendererRegistry(), dispatcher, Mock.Of(), Mock.Of()); } private class TestRemoteRenderer : RemoteRenderer { public TestRemoteRenderer(IServiceProvider serviceProvider, RendererRegistry rendererRegistry, IDispatcher dispatcher, IJSRuntime jsRuntime, IClientProxy client) : base(serviceProvider, rendererRegistry, jsRuntime, new CircuitClientProxy(client, "connection"), dispatcher, HtmlEncoder.Default, NullLogger.Instance) { } public bool Disposed { get; set; } protected override void Dispose(bool disposing) { base.Dispose(disposing); Disposed = true; } } private class DispatcherComponent : ComponentBase, IDisposable { public DispatcherComponent(IDispatcher dispatcher) { Dispatcher = dispatcher; } public IDispatcher Dispatcher { get; } public bool Called { get; private set; } public void Dispose() { Called = true; Assert.Same(Dispatcher, SynchronizationContext.Current); } } } }