// 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.Threading.Channels; using System.Threading.Tasks; using Xunit; namespace Microsoft.AspNetCore.SignalR.Client.Tests { // This includes tests that verify HubConnection conforms to the Hub Protocol, without setting up a full server (even TestServer). // We can also have more control over the messages we send to HubConnection in order to ensure that protocol errors and other quirks // don't cause problems. public partial class HubConnectionTests { public class Protocol { [Fact] public async Task SendAsyncSendsANonBlockingInvocationMessage() { var connection = new TestConnection(); var hubConnection = CreateHubConnection(connection); try { await hubConnection.StartAsync().OrTimeout(); var invokeTask = hubConnection.SendAsync("Foo").OrTimeout(); var invokeMessage = await connection.ReadSentTextMessageAsync().OrTimeout(); // ReadSentTextMessageAsync strips off the record separator (because it has use it as a separator now that we use Pipelines) Assert.Equal("{\"type\":1,\"target\":\"Foo\",\"arguments\":[]}", invokeMessage); } finally { await hubConnection.DisposeAsync().OrTimeout(); await connection.DisposeAsync().OrTimeout(); } } [Fact] public async Task ClientSendsHandshakeMessageWhenStartingConnection() { var connection = new TestConnection(autoHandshake: false); var hubConnection = CreateHubConnection(connection); try { // We can't await StartAsync because it depends on the negotiate process! var startTask = hubConnection.StartAsync().OrTimeout(); var handshakeMessage = await connection.ReadHandshakeAndSendResponseAsync().OrTimeout(); // ReadSentTextMessageAsync strips off the record separator (because it has use it as a separator now that we use Pipelines) Assert.Equal("{\"protocol\":\"json\",\"version\":1}", handshakeMessage); await startTask; } finally { await hubConnection.DisposeAsync().OrTimeout(); await connection.DisposeAsync().OrTimeout(); } } [Fact] public async Task InvokeSendsAnInvocationMessage() { var connection = new TestConnection(); var hubConnection = CreateHubConnection(connection); try { await hubConnection.StartAsync().OrTimeout(); var invokeTask = hubConnection.InvokeAsync("Foo").OrTimeout(); var invokeMessage = await connection.ReadSentTextMessageAsync().OrTimeout(); // ReadSentTextMessageAsync strips off the record separator (because it has use it as a separator now that we use Pipelines) Assert.Equal("{\"type\":1,\"invocationId\":\"1\",\"target\":\"Foo\",\"arguments\":[]}", invokeMessage); } finally { await hubConnection.DisposeAsync().OrTimeout(); await connection.DisposeAsync().OrTimeout(); } } [Fact] public async Task ReceiveCloseMessageWithoutErrorWillCloseHubConnection() { var closedTcs = new TaskCompletionSource(); var connection = new TestConnection(); var hubConnection = CreateHubConnection(connection); hubConnection.Closed += e => { closedTcs.SetResult(e); return Task.CompletedTask; }; try { await hubConnection.StartAsync().OrTimeout(); await connection.ReceiveJsonMessage(new { type = 7 }).OrTimeout(); var closeException = await closedTcs.Task.OrTimeout(); Assert.Null(closeException); } finally { await hubConnection.DisposeAsync().OrTimeout(); await connection.DisposeAsync().OrTimeout(); } } [Fact] public async Task ReceiveCloseMessageWithErrorWillCloseHubConnection() { var closedTcs = new TaskCompletionSource(); var connection = new TestConnection(); var hubConnection = CreateHubConnection(connection); hubConnection.Closed += e => { closedTcs.SetResult(e); return Task.CompletedTask; }; try { await hubConnection.StartAsync().OrTimeout(); await connection.ReceiveJsonMessage(new { type = 7, error = "Error!" }).OrTimeout(); var closeException = await closedTcs.Task.OrTimeout(); Assert.NotNull(closeException); Assert.Equal("The server closed the connection with the following error: Error!", closeException.Message); } finally { await hubConnection.DisposeAsync().OrTimeout(); } } [Fact] public async Task StreamSendsAnInvocationMessage() { var connection = new TestConnection(); var hubConnection = CreateHubConnection(connection); try { await hubConnection.StartAsync().OrTimeout(); var channel = await hubConnection.StreamAsChannelAsync("Foo").OrTimeout(); var invokeMessage = await connection.ReadSentTextMessageAsync().OrTimeout(); // ReadSentTextMessageAsync strips off the record separator (because it has use it as a separator now that we use Pipelines) Assert.Equal("{\"type\":4,\"invocationId\":\"1\",\"target\":\"Foo\",\"arguments\":[]}", invokeMessage); // Complete the channel await connection.ReceiveJsonMessage(new { invocationId = "1", type = 3 }).OrTimeout(); await channel.Completion; } finally { await hubConnection.DisposeAsync().OrTimeout(); await connection.DisposeAsync().OrTimeout(); } } [Fact] public async Task InvokeCompletedWhenCompletionMessageReceived() { var connection = new TestConnection(); var hubConnection = CreateHubConnection(connection); try { await hubConnection.StartAsync().OrTimeout(); var invokeTask = hubConnection.InvokeAsync("Foo").OrTimeout(); await connection.ReceiveJsonMessage(new { invocationId = "1", type = 3 }).OrTimeout(); await invokeTask.OrTimeout(); } finally { await hubConnection.DisposeAsync().OrTimeout(); await connection.DisposeAsync().OrTimeout(); } } [Fact] public async Task StreamCompletesWhenCompletionMessageIsReceived() { var connection = new TestConnection(); var hubConnection = CreateHubConnection(connection); try { await hubConnection.StartAsync().OrTimeout(); var channel = await hubConnection.StreamAsChannelAsync("Foo").OrTimeout(); await connection.ReceiveJsonMessage(new { invocationId = "1", type = 3 }).OrTimeout(); Assert.Empty(await channel.ReadAllAsync()); } finally { await hubConnection.DisposeAsync().OrTimeout(); await connection.DisposeAsync().OrTimeout(); } } [Fact] public async Task InvokeYieldsResultWhenCompletionMessageReceived() { var connection = new TestConnection(); var hubConnection = CreateHubConnection(connection); try { await hubConnection.StartAsync().OrTimeout(); var invokeTask = hubConnection.InvokeAsync("Foo").OrTimeout(); await connection.ReceiveJsonMessage(new { invocationId = "1", type = 3, result = 42 }).OrTimeout(); Assert.Equal(42, await invokeTask.OrTimeout()); } finally { await hubConnection.DisposeAsync().OrTimeout(); await connection.DisposeAsync().OrTimeout(); } } [Fact] public async Task InvokeFailsWithExceptionWhenCompletionWithErrorReceived() { var connection = new TestConnection(); var hubConnection = CreateHubConnection(connection); try { await hubConnection.StartAsync().OrTimeout(); var invokeTask = hubConnection.InvokeAsync("Foo").OrTimeout(); await connection.ReceiveJsonMessage(new { invocationId = "1", type = 3, error = "An error occurred" }).OrTimeout(); var ex = await Assert.ThrowsAsync(() => invokeTask).OrTimeout(); Assert.Equal("An error occurred", ex.Message); } finally { await hubConnection.DisposeAsync().OrTimeout(); await connection.DisposeAsync().OrTimeout(); } } [Fact] public async Task StreamFailsIfCompletionMessageHasPayload() { var connection = new TestConnection(); var hubConnection = CreateHubConnection(connection); try { await hubConnection.StartAsync().OrTimeout(); var channel = await hubConnection.StreamAsChannelAsync("Foo").OrTimeout(); await connection.ReceiveJsonMessage(new { invocationId = "1", type = 3, result = "Oops" }).OrTimeout(); var ex = await Assert.ThrowsAsync(async () => await channel.ReadAllAsync().OrTimeout()); Assert.Equal("Server provided a result in a completion response to a streamed invocation.", ex.Message); } finally { await hubConnection.DisposeAsync().OrTimeout(); await connection.DisposeAsync().OrTimeout(); } } [Fact] public async Task StreamFailsWithExceptionWhenCompletionWithErrorReceived() { var connection = new TestConnection(); var hubConnection = CreateHubConnection(connection); try { await hubConnection.StartAsync().OrTimeout(); var channel = await hubConnection.StreamAsChannelAsync("Foo").OrTimeout(); await connection.ReceiveJsonMessage(new { invocationId = "1", type = 3, error = "An error occurred" }).OrTimeout(); var ex = await Assert.ThrowsAsync(async () => await channel.ReadAllAsync().OrTimeout()); Assert.Equal("An error occurred", ex.Message); } finally { await hubConnection.DisposeAsync().OrTimeout(); await connection.DisposeAsync().OrTimeout(); } } [Fact] public async Task InvokeFailsWithErrorWhenStreamingItemReceived() { var connection = new TestConnection(); var hubConnection = CreateHubConnection(connection); try { await hubConnection.StartAsync().OrTimeout(); var invokeTask = hubConnection.InvokeAsync("Foo").OrTimeout(); await connection.ReceiveJsonMessage(new { invocationId = "1", type = 2, item = 42 }).OrTimeout(); var ex = await Assert.ThrowsAsync(() => invokeTask).OrTimeout(); Assert.Equal("Streaming hub methods must be invoked with the 'HubConnection.StreamAsChannelAsync' method.", ex.Message); } finally { await hubConnection.DisposeAsync().OrTimeout(); await connection.DisposeAsync().OrTimeout(); } } [Fact] public async Task StreamYieldsItemsAsTheyArrive() { var connection = new TestConnection(); var hubConnection = CreateHubConnection(connection); try { await hubConnection.StartAsync().OrTimeout(); var channel = await hubConnection.StreamAsChannelAsync("Foo").OrTimeout(); await connection.ReceiveJsonMessage(new { invocationId = "1", type = 2, item = "1" }).OrTimeout(); await connection.ReceiveJsonMessage(new { invocationId = "1", type = 2, item = "2" }).OrTimeout(); await connection.ReceiveJsonMessage(new { invocationId = "1", type = 2, item = "3" }).OrTimeout(); await connection.ReceiveJsonMessage(new { invocationId = "1", type = 3 }).OrTimeout(); var notifications = await channel.ReadAllAsync().OrTimeout(); Assert.Equal(new[] { "1", "2", "3", }, notifications.ToArray()); } finally { await hubConnection.DisposeAsync().OrTimeout(); await connection.DisposeAsync().OrTimeout(); } } [Fact] public async Task HandlerRegisteredWithOnIsFiredWhenInvocationReceived() { var connection = new TestConnection(); var hubConnection = CreateHubConnection(connection); var handlerCalled = new TaskCompletionSource(); try { await hubConnection.StartAsync().OrTimeout(); hubConnection.On("Foo", (r1, r2, r3) => handlerCalled.TrySetResult(new object[] { r1, r2, r3 })); var args = new object[] { 1, "Foo", 2.0f }; await connection.ReceiveJsonMessage(new { invocationId = "1", type = 1, target = "Foo", arguments = args }).OrTimeout(); Assert.Equal(args, await handlerCalled.Task.OrTimeout()); } finally { await hubConnection.DisposeAsync().OrTimeout(); await connection.DisposeAsync().OrTimeout(); } } [Fact] public async Task HandlerIsRemovedProperlyWithOff() { var connection = new TestConnection(); var hubConnection = CreateHubConnection(connection); var handlerCalled = new TaskCompletionSource(); try { await hubConnection.StartAsync().OrTimeout(); hubConnection.On("Foo", (val) => { handlerCalled.TrySetResult(val); }); hubConnection.Remove("Foo"); await connection.ReceiveJsonMessage(new { invocationId = "1", type = 1, target = "Foo", arguments = 1 }).OrTimeout(); var handlerTask = handlerCalled.Task; // We expect the handler task to timeout since the handler has been removed with the call to Remove("Foo") var ex = Assert.ThrowsAsync(async () => await handlerTask.OrTimeout(2000)); // Ensure that the task from the WhenAny is not the handler task Assert.False(handlerCalled.Task.IsCompleted); } finally { await hubConnection.DisposeAsync().OrTimeout(); await connection.DisposeAsync().OrTimeout(); } } [Fact] public async Task DisposingSubscriptionAfterCallingRemoveHandlerDoesntFail() { var connection = new TestConnection(); var hubConnection = CreateHubConnection(connection); var handlerCalled = new TaskCompletionSource(); try { await hubConnection.StartAsync().OrTimeout(); var subscription = hubConnection.On("Foo", (val) => { handlerCalled.TrySetResult(val); }); hubConnection.Remove("Foo"); await connection.ReceiveJsonMessage(new { invocationId = "1", type = 1, target = "Foo", arguments = 1 }).OrTimeout(); var handlerTask = handlerCalled.Task; subscription.Dispose(); // We expect the handler task to timeout since the handler has been removed with the call to Remove("Foo") var ex = Assert.ThrowsAsync(async () => await handlerTask.OrTimeout(2000)); // Ensure that the task from the WhenAny is not the handler task Assert.False(handlerCalled.Task.IsCompleted); } finally { await hubConnection.DisposeAsync().OrTimeout(); await connection.DisposeAsync().OrTimeout(); } } [Fact] public async Task AcceptsPingMessages() { var connection = new TestConnection(); var hubConnection = CreateHubConnection(connection); try { await hubConnection.StartAsync().OrTimeout(); // Send an invocation var invokeTask = hubConnection.InvokeAsync("Foo").OrTimeout(); // Receive the ping mid-invocation so we can see that the rest of the flow works fine await connection.ReceiveJsonMessage(new { type = 6 }).OrTimeout(); // Receive a completion await connection.ReceiveJsonMessage(new { invocationId = "1", type = 3 }).OrTimeout(); // Ensure the invokeTask completes properly await invokeTask.OrTimeout(); } finally { await hubConnection.DisposeAsync().OrTimeout(); await connection.DisposeAsync().OrTimeout(); } } [Fact] public async Task PartialHandshakeResponseWorks() { var connection = new TestConnection(autoHandshake: false); var hubConnection = CreateHubConnection(connection); try { var task = hubConnection.StartAsync(); await connection.ReceiveTextAsync("{"); Assert.False(task.IsCompleted); await connection.ReceiveTextAsync("}"); Assert.False(task.IsCompleted); await connection.ReceiveTextAsync("\u001e"); await task.OrTimeout(); } finally { await hubConnection.DisposeAsync().OrTimeout(); await connection.DisposeAsync().OrTimeout(); } } [Fact] public async Task HandshakeAndInvocationInSameBufferWorks() { var payload = "{}\u001e{\"type\":1, \"target\": \"Echo\", \"arguments\":[\"hello\"]}\u001e"; var connection = new TestConnection(autoHandshake: false); var hubConnection = CreateHubConnection(connection); try { var tcs = new TaskCompletionSource(); hubConnection.On("Echo", data => { tcs.TrySetResult(data); }); await connection.ReceiveTextAsync(payload); await hubConnection.StartAsync(); var response = await tcs.Task.OrTimeout(); Assert.Equal("hello", response); } finally { await hubConnection.DisposeAsync().OrTimeout(); await connection.DisposeAsync().OrTimeout(); } } [Fact] public async Task PartialInvocationWorks() { var connection = new TestConnection(); var hubConnection = CreateHubConnection(connection); try { var tcs = new TaskCompletionSource(); hubConnection.On("Echo", data => { tcs.TrySetResult(data); }); await hubConnection.StartAsync().OrTimeout(); await connection.ReceiveTextAsync("{\"type\":1, "); Assert.False(tcs.Task.IsCompleted); await connection.ReceiveTextAsync("\"target\": \"Echo\", \"arguments\""); Assert.False(tcs.Task.IsCompleted); await connection.ReceiveTextAsync(":[\"hello\"]}\u001e"); var response = await tcs.Task.OrTimeout(); Assert.Equal("hello", response); } finally { await hubConnection.DisposeAsync().OrTimeout(); await connection.DisposeAsync().OrTimeout(); } } [Fact] public async Task ClientPingsMultipleTimes() { var connection = new TestConnection(); var hubConnection = CreateHubConnection(connection); hubConnection.TickRate = TimeSpan.FromMilliseconds(30); hubConnection.KeepAliveInterval = TimeSpan.FromMilliseconds(80); try { await hubConnection.StartAsync().OrTimeout(); var firstPing = await connection.ReadSentTextMessageAsync(ignorePings: false).OrTimeout(); Assert.Equal("{\"type\":6}", firstPing); var secondPing = await connection.ReadSentTextMessageAsync(ignorePings: false).OrTimeout(); Assert.Equal("{\"type\":6}", secondPing); } finally { await hubConnection.DisposeAsync().OrTimeout(); await connection.DisposeAsync().OrTimeout(); } } } } }