// 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.Json; using System.Threading.Tasks; using Ignitor; using Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures; using Microsoft.AspNetCore.Components.RenderTree; using Microsoft.AspNetCore.Components.Web; using Microsoft.AspNetCore.SignalR.Client; using Microsoft.Extensions.Logging; using TestServer; using Xunit; using Xunit.Abstractions; namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests { public class InteropReliabilityTests : IgnitorTest { public InteropReliabilityTests(BasicTestAppServerSiteFixture serverFixture, ITestOutputHelper output) : base(serverFixture, output) { } protected async override Task InitializeAsync() { var rootUri = ServerFixture.RootUri; Assert.True(await Client.ConnectAsync(new Uri(rootUri, "/subdir")), "Couldn't connect to the app"); Assert.Single(Batches); await Client.SelectAsync("test-selector-select", "BasicTestApp.ReliabilityComponent"); Assert.Equal(2, Batches.Count); } [Fact] public async Task CannotInvokeNonJSInvokableMethods() { // Arrange var expectedError = "[\"1\"," + "false," + "\"There was an exception invoking \\u0027WriteAllText\\u0027 on assembly \\u0027System.IO.FileSystem\\u0027. For more details turn on detailed exceptions in \\u0027CircuitOptions.DetailedErrors\\u0027\"]"; // Act await Client.InvokeDotNetMethod( "1", "System.IO.FileSystem", "WriteAllText", null, JsonSerializer.Serialize(new[] { ".\\log.txt", "log" })); // Assert Assert.Single(DotNetCompletions, c => c == expectedError); await ValidateClientKeepsWorking(Client, Batches); } [Fact] public async Task CannotInvokeNonExistingMethods() { // Arrange var expectedError = "[\"1\"," + "false," + "\"There was an exception invoking \\u0027MadeUpMethod\\u0027 on assembly \\u0027BasicTestApp\\u0027. For more details turn on detailed exceptions in \\u0027CircuitOptions.DetailedErrors\\u0027\"]"; // Act await Client.InvokeDotNetMethod( "1", "BasicTestApp", "MadeUpMethod", null, JsonSerializer.Serialize(new[] { ".\\log.txt", "log" })); // Assert Assert.Single(DotNetCompletions, c => c == expectedError); await ValidateClientKeepsWorking(Client, Batches); } [Fact] public async Task CannotInvokeJSInvokableMethodsWithWrongNumberOfArguments() { // Arrange var expectedError = "[\"1\"," + "false," + "\"There was an exception invoking \\u0027NotifyLocationChanged\\u0027 on assembly \\u0027Microsoft.AspNetCore.Components.Server\\u0027. For more details turn on detailed exceptions in \\u0027CircuitOptions.DetailedErrors\\u0027\"]"; // Act await Client.InvokeDotNetMethod( "1", "Microsoft.AspNetCore.Components.Server", "NotifyLocationChanged", null, JsonSerializer.Serialize(new[] { ServerFixture.RootUri })); // Assert Assert.Single(DotNetCompletions, c => c == expectedError); await ValidateClientKeepsWorking(Client, Batches); } [Fact] public async Task CannotInvokeJSInvokableMethodsEmptyAssemblyName() { // Arrange var expectedError = "[\"1\"," + "false," + "\"There was an exception invoking \\u0027NotifyLocationChanged\\u0027 on assembly \\u0027\\u0027. For more details turn on detailed exceptions in \\u0027CircuitOptions.DetailedErrors\\u0027\"]"; // Act await Client.InvokeDotNetMethod( "1", "", "NotifyLocationChanged", null, JsonSerializer.Serialize(new object[] { ServerFixture.RootUri + "counter", false })); // Assert Assert.Single(DotNetCompletions, c => c == expectedError); await ValidateClientKeepsWorking(Client, Batches); } [Fact] public async Task CannotInvokeJSInvokableMethodsEmptyMethodName() { // Arrange var expectedError = "[\"1\"," + "false," + "\"There was an exception invoking \\u0027\\u0027 on assembly \\u0027Microsoft.AspNetCore.Components.Server\\u0027. For more details turn on detailed exceptions in \\u0027CircuitOptions.DetailedErrors\\u0027\"]"; // Act await Client.InvokeDotNetMethod( "1", "Microsoft.AspNetCore.Components.Server", "", null, JsonSerializer.Serialize(new object[] { ServerFixture.RootUri + "counter", false })); // Assert Assert.Single(DotNetCompletions, c => c == expectedError); await ValidateClientKeepsWorking(Client, Batches); } [Fact] public async Task CannotInvokeJSInvokableMethodsWithWrongReferenceId() { // Arrange var expectedDotNetObjectRef = "[\"1\",true,{\"__dotNetObject\":1}]"; var expectedError = "[\"1\"," + "false," + "\"There was an exception invoking \\u0027Reverse\\u0027. For more details turn on detailed exceptions in \\u0027CircuitOptions.DetailedErrors\\u0027\"]"; // Act await Client.InvokeDotNetMethod( "1", "BasicTestApp", "CreateImportant", null, JsonSerializer.Serialize(Array.Empty())); Assert.Single(DotNetCompletions, c => c == expectedDotNetObjectRef); await Client.InvokeDotNetMethod( "1", null, "Reverse", 1, JsonSerializer.Serialize(Array.Empty())); // Assert Assert.Single(DotNetCompletions, c => c == "[\"1\",true,\"tnatropmI\"]"); await Client.InvokeDotNetMethod( "1", null, "Reverse", 3, // non existing ref JsonSerializer.Serialize(Array.Empty())); Assert.Single(DotNetCompletions, c => c == expectedError); await ValidateClientKeepsWorking(Client, Batches); } [Fact] public async Task CannotInvokeJSInvokableMethodsWrongReferenceIdType() { // Arrange var expectedImportantDotNetObjectRef = "[\"1\",true,{\"__dotNetObject\":1}]"; var expectedError = "[\"1\"," + "false," + "\"There was an exception invoking \\u0027ReceiveTrivial\\u0027 on assembly \\u0027BasicTestApp\\u0027. For more details turn on detailed exceptions in \\u0027CircuitOptions.DetailedErrors\\u0027\"]"; await Client.InvokeDotNetMethod( "1", "BasicTestApp", "CreateImportant", null, JsonSerializer.Serialize(Array.Empty())); Assert.Single(DotNetCompletions, c => c == expectedImportantDotNetObjectRef); // Act await Client.InvokeDotNetMethod( "1", "BasicTestApp", "ReceiveTrivial", null, JsonSerializer.Serialize(new object[] { new { __dotNetObject = 1 } })); // Assert Assert.Single(DotNetCompletions, c => c == expectedError); await ValidateClientKeepsWorking(Client, Batches); } [Fact] public async Task ContinuesWorkingAfterInvalidAsyncReturnCallback() { // Arrange var expectedError = "An exception occurred executing JS interop: The JSON value could not be converted to System.Int32. Path: $ | LineNumber: 0 | BytePositionInLine: 3.. See InnerException for more details."; // Act await Client.ClickAsync("triggerjsinterop-malformed"); var call = JSInteropCalls.FirstOrDefault(call => call.Identifier == "sendMalformedCallbackReturn"); Assert.NotEqual(default, call); var id = call.AsyncHandle; await Client.ExpectRenderBatch(async () => { await Client.HubConnection.InvokeAsync( "EndInvokeJSFromDotNet", id, true, $"[{id}, true, \"{{\"]"); }); var text = Assert.Single( Client.FindElementById("errormessage-malformed").Children.OfType(), e => expectedError == e.TextContent); await ValidateClientKeepsWorking(Client, Batches); } [Fact] public async Task JSInteropCompletionSuccess() { // Arrange // Act await Client.ClickAsync("triggerjsinterop-success"); var call = JSInteropCalls.FirstOrDefault(call => call.Identifier == "sendSuccessCallbackReturn"); Assert.NotEqual(default, call); var id = call.AsyncHandle; await Client.ExpectRenderBatch(async () => { await Client.HubConnection.InvokeAsync( "EndInvokeJSFromDotNet", id, true, $"[{id}, true, null]"); }); Assert.Single( Client.FindElementById("errormessage-success").Children.OfType(), e => "" == e.TextContent); var entry = Assert.Single(Logs, l => l.EventId.Name == "EndInvokeJSSucceeded"); Assert.Equal(LogLevel.Debug, entry.LogLevel); } [Fact] public async Task JSInteropThrowsInUserCode() { // Arrange // Act await Client.ClickAsync("triggerjsinterop-failure"); var call = JSInteropCalls.FirstOrDefault(call => call.Identifier == "sendFailureCallbackReturn"); Assert.NotEqual(default, call); var id = call.AsyncHandle; await Client.ExpectRenderBatch(async () => { await Client.HubConnection.InvokeAsync( "EndInvokeJSFromDotNet", id, false, $"[{id}, false, \"There was an error invoking sendFailureCallbackReturn\"]"); }); Assert.Single( Client.FindElementById("errormessage-failure").Children.OfType(), e => "There was an error invoking sendFailureCallbackReturn" == e.TextContent); var entry = Assert.Single(Logs, l => l.EventId.Name == "EndInvokeJSFailed"); Assert.Equal(LogLevel.Debug, entry.LogLevel); Assert.DoesNotContain(Logs, m => m.LogLevel > LogLevel.Information); await ValidateClientKeepsWorking(Client, Batches); } [Fact] public async Task MalformedJSInteropCallbackDisposesCircuit() { // Arrange // Act await Client.ClickAsync("triggerjsinterop-malformed"); var call = JSInteropCalls.FirstOrDefault(call => call.Identifier == "sendMalformedCallbackReturn"); Assert.NotEqual(default, call); var id = call.AsyncHandle; await Client.ExpectCircuitError(async () => { await Client.HubConnection.InvokeAsync( "EndInvokeJSFromDotNet", id, true, $"[{id}, true, }}"); }); // A completely malformed payload like the one above never gets to the application. Assert.Single( Client.FindElementById("errormessage-malformed").Children.OfType(), e => "" == e.TextContent); var entry = Assert.Single(Logs, l => l.EventId.Name == "EndInvokeDispatchException"); Assert.Equal(LogLevel.Debug, entry.LogLevel); await Client.ExpectCircuitErrorAndDisconnect(async () => { await Assert.ThrowsAsync(() => Client.ClickAsync("event-handler-throw-sync", expectRenderBatch: true)); }); } [Fact] public async Task CannotInvokeJSInvokableMethodsWithInvalidArgumentsPayload() { // Arrange var expectedError = "[\"1\"," + "false," + "\"There was an exception invoking \\u0027NotifyLocationChanged\\u0027 on assembly \\u0027Microsoft.AspNetCore.Components.Server\\u0027. For more details turn on detailed exceptions in \\u0027CircuitOptions.DetailedErrors\\u0027\"]"; // Act await Client.InvokeDotNetMethod( "1", "Microsoft.AspNetCore.Components.Server", "NotifyLocationChanged", null, "[ \"invalidPayload\"}"); // Assert Assert.Single(DotNetCompletions, c => c == expectedError); await ValidateClientKeepsWorking(Client, Batches); } [Fact] public async Task CannotInvokeJSInvokableMethodsWithMalformedArgumentPayload() { // Arrange var expectedError = "[\"1\"," + "false," + "\"There was an exception invoking \\u0027ReceiveTrivial\\u0027 on assembly \\u0027BasicTestApp\\u0027. For more details turn on detailed exceptions in \\u0027CircuitOptions.DetailedErrors\\u0027\"]"; // Act await Client.InvokeDotNetMethod( "1", "BasicTestApp", "ReceiveTrivial", null, "[ { \"data\": {\"}} ]"); // Assert Assert.Single(DotNetCompletions, c => c == expectedError); await ValidateClientKeepsWorking(Client, Batches); } [Fact] public async Task DispatchingEventsWithInvalidPayloadsShutsDownCircuitGracefully() { // Arrange // Act await Client.ExpectCircuitError(async () => { await Client.HubConnection.InvokeAsync( "DispatchBrowserEvent", null, null); }); var entry = Assert.Single(Logs, l => l.EventId.Name == "DispatchEventFailedToParseEventData"); Assert.Equal(LogLevel.Debug, entry.LogLevel); // Taking any other action will fail because the circuit is disposed. await Client.ExpectCircuitErrorAndDisconnect(async () => { await Assert.ThrowsAsync(() => Client.ClickAsync("event-handler-throw-sync", expectRenderBatch: true)); }); } [Fact] public async Task DispatchingEventsWithInvalidEventDescriptor() { // Arrange // Act await Client.ExpectCircuitError(async () => { await Client.HubConnection.InvokeAsync( "DispatchBrowserEvent", "{Invalid:{\"payload}", "{}"); }); var entry = Assert.Single(Logs, l => l.EventId.Name == "DispatchEventFailedToParseEventData"); Assert.Equal(LogLevel.Debug, entry.LogLevel); // Taking any other action will fail because the circuit is disposed. await Client.ExpectCircuitErrorAndDisconnect(async () => { await Assert.ThrowsAsync(() => Client.ClickAsync("event-handler-throw-sync", expectRenderBatch: true)); }); } [Fact] public async Task DispatchingEventsWithInvalidEventArgs() { // Arrange // Act var browserDescriptor = new WebEventDescriptor() { BrowserRendererId = 0, EventHandlerId = 6, EventArgsType = "mouse", }; await Client.ExpectCircuitError(async () => { await Client.HubConnection.InvokeAsync( "DispatchBrowserEvent", JsonSerializer.Serialize(browserDescriptor, TestJsonSerializerOptionsProvider.Options), "{Invalid:{\"payload}"); }); Assert.Contains( Logs, e => e.EventId.Name == "DispatchEventFailedToParseEventData" && e.LogLevel == LogLevel.Debug && e.Exception.Message == "There was an error parsing the event arguments. EventId: '6'."); // Taking any other action will fail because the circuit is disposed. await Client.ExpectCircuitErrorAndDisconnect(async () => { await Assert.ThrowsAsync(() => Client.ClickAsync("event-handler-throw-sync", expectRenderBatch: true)); }); } [Fact] public async Task DispatchingEventsWithInvalidEventHandlerId() { // Arrange // Act var mouseEventArgs = new MouseEventArgs() { Type = "click", Detail = 1 }; var browserDescriptor = new WebEventDescriptor() { BrowserRendererId = 0, EventHandlerId = 1, EventArgsType = "mouse", }; await Client.ExpectCircuitError(async () => { await Client.HubConnection.InvokeAsync( "DispatchBrowserEvent", JsonSerializer.Serialize(browserDescriptor, TestJsonSerializerOptionsProvider.Options), JsonSerializer.Serialize(mouseEventArgs, TestJsonSerializerOptionsProvider.Options)); }); Assert.Contains( Logs, e => e.EventId.Name == "DispatchEventFailedToDispatchEvent" && e.LogLevel == LogLevel.Debug && e.Exception is ArgumentException ae && ae.Message.Contains("There is no event handler associated with this event. EventId: '1'.")); // Taking any other action will fail because the circuit is disposed. await Client.ExpectCircuitErrorAndDisconnect(async () => { await Assert.ThrowsAsync(() => Client.ClickAsync("event-handler-throw-sync", expectRenderBatch: true)); }); } [Fact] public async Task EventHandlerThrowsSyncExceptionTerminatesTheCircuit() { // Arrange // Act await Client.ExpectCircuitError(async () => { await Client.ClickAsync("event-handler-throw-sync", expectRenderBatch: false); }); Assert.Contains( Logs, e => LogLevel.Error == e.LogLevel && "CircuitUnhandledException" == e.EventId.Name && "Handler threw an exception" == e.Exception.Message); // Now if you try to click again, you will get *forcibly* disconnected for trying to talk to // a circuit that's gone. await Client.ExpectCircuitErrorAndDisconnect(async () => { await Assert.ThrowsAsync(() => Client.ClickAsync("event-handler-throw-sync", expectRenderBatch: true)); }); } private Task ValidateClientKeepsWorking(BlazorClient Client, IReadOnlyCollection batches) => ValidateClientKeepsWorking(Client, () => batches.Count); private async Task ValidateClientKeepsWorking(BlazorClient Client, Func countAccessor) { var currentBatches = countAccessor(); await Client.ClickAsync("thecounter"); Assert.Equal(currentBatches + 1, countAccessor()); } } }