aspnetcore/src/Components/test/E2ETest/ServerExecutionTests/InteropReliabilityTests.cs

539 lines
20 KiB
C#

// 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<ServerStartup>
{
public InteropReliabilityTests(BasicTestAppServerSiteFixture<ServerStartup> 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<object>()));
Assert.Single(DotNetCompletions, c => c == expectedDotNetObjectRef);
await Client.InvokeDotNetMethod(
"1",
null,
"Reverse",
1,
JsonSerializer.Serialize(Array.Empty<object>()));
// Assert
Assert.Single(DotNetCompletions, c => c == "[\"1\",true,\"tnatropmI\"]");
await Client.InvokeDotNetMethod(
"1",
null,
"Reverse",
3, // non existing ref
JsonSerializer.Serialize(Array.Empty<object>()));
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<object>()));
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<TextNode>(),
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<TextNode>(),
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<TextNode>(),
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<TextNode>(),
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<TaskCanceledException>(() => 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<TaskCanceledException>(() => 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<TaskCanceledException>(() => 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<TaskCanceledException>(() => 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<TaskCanceledException>(() => 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<TaskCanceledException>(() => Client.ClickAsync("event-handler-throw-sync", expectRenderBatch: true));
});
}
private Task ValidateClientKeepsWorking(BlazorClient Client, IReadOnlyCollection<CapturedRenderBatch> batches) =>
ValidateClientKeepsWorking(Client, () => batches.Count);
private async Task ValidateClientKeepsWorking(BlazorClient Client, Func<int> countAccessor)
{
var currentBatches = countAccessor();
await Client.ClickAsync("thecounter");
Assert.Equal(currentBatches + 1, countAccessor());
}
}
}