| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312 |
- // Licensed to the .NET Foundation under one or more agreements.
- // The .NET Foundation licenses this file to you under the MIT license.
- using Microsoft.AspNetCore.SignalR;
- using Microsoft.AspNetCore.InternalTesting;
- using Microsoft.Extensions.Logging;
- using Microsoft.Extensions.Options;
- using Microsoft.JSInterop;
- using Moq;
- namespace Microsoft.AspNetCore.Components.Server.Circuits;
- public class RemoteJSDataStreamTest
- {
- private static readonly TestRemoteJSRuntime _jsRuntime = new(Options.Create(new CircuitOptions()), Options.Create(new HubOptions<ComponentHub>()), Mock.Of<ILogger<RemoteJSRuntime>>());
- [Fact]
- public async Task CreateRemoteJSDataStreamAsync_CreatesStream()
- {
- // Arrange
- var jsStreamReference = Mock.Of<IJSStreamReference>();
- // Act
- var remoteJSDataStream = await RemoteJSDataStream.CreateRemoteJSDataStreamAsync(_jsRuntime, jsStreamReference, totalLength: 100, signalRMaximumIncomingBytes: 10_000, jsInteropDefaultCallTimeout: TimeSpan.FromMinutes(1), cancellationToken: CancellationToken.None).DefaultTimeout();
- // Assert
- Assert.NotNull(remoteJSDataStream);
- }
- [Fact]
- public async Task ReceiveData_DoesNotFindStream()
- {
- // Arrange
- var chunk = new byte[] { 3, 5, 6, 7 };
- var unrecognizedGuid = 10;
- // Act
- var success = await RemoteJSDataStream.ReceiveData(_jsRuntime, streamId: unrecognizedGuid, chunkId: 0, chunk, error: null).DefaultTimeout();
- // Assert
- Assert.False(success);
- }
- [Fact]
- public async Task ReceiveData_SuccessReadsBackStream()
- {
- // Arrange
- var jsRuntime = new TestRemoteJSRuntime(Options.Create(new CircuitOptions()), Options.Create(new HubOptions<ComponentHub>()), Mock.Of<ILogger<RemoteJSRuntime>>());
- var remoteJSDataStream = await CreateRemoteJSDataStreamAsync(jsRuntime);
- var streamId = GetStreamId(remoteJSDataStream, jsRuntime);
- var chunk = new byte[100];
- var random = new Random();
- random.NextBytes(chunk);
- var sendDataTask = Task.Run(async () =>
- {
- // Act 1
- var success = await RemoteJSDataStream.ReceiveData(jsRuntime, streamId, chunkId: 0, chunk, error: null).DefaultTimeout();
- return success;
- });
- // Act & Assert 2
- using var memoryStream = new MemoryStream();
- await remoteJSDataStream.CopyToAsync(memoryStream).DefaultTimeout();
- Assert.Equal(chunk, memoryStream.ToArray());
- // Act & Assert 3
- var sendDataCompleted = await sendDataTask.DefaultTimeout();
- Assert.True(sendDataCompleted);
- }
- [Fact]
- public async Task ReceiveData_SuccessReadsBackPipeReader()
- {
- // Arrange
- var jsRuntime = new TestRemoteJSRuntime(Options.Create(new CircuitOptions()), Options.Create(new HubOptions<ComponentHub>()), Mock.Of<ILogger<RemoteJSRuntime>>());
- var remoteJSDataStream = await CreateRemoteJSDataStreamAsync(jsRuntime);
- var streamId = GetStreamId(remoteJSDataStream, jsRuntime);
- var chunk = new byte[100];
- var random = new Random();
- random.NextBytes(chunk);
- var sendDataTask = Task.Run(async () =>
- {
- // Act 1
- var success = await RemoteJSDataStream.ReceiveData(jsRuntime, streamId, chunkId: 0, chunk, error: null).DefaultTimeout();
- return success;
- });
- // Act & Assert 2
- using var memoryStream = new MemoryStream();
- await remoteJSDataStream.PipeReader.CopyToAsync(memoryStream).DefaultTimeout();
- Assert.Equal(chunk, memoryStream.ToArray());
- // Act & Assert 3
- var sendDataCompleted = await sendDataTask.DefaultTimeout();
- Assert.True(sendDataCompleted);
- }
- [Fact]
- public async Task ReceiveData_WithError()
- {
- // Arrange
- var jsRuntime = new TestRemoteJSRuntime(Options.Create(new CircuitOptions()), Options.Create(new HubOptions<ComponentHub>()), Mock.Of<ILogger<RemoteJSRuntime>>());
- var remoteJSDataStream = await CreateRemoteJSDataStreamAsync(jsRuntime);
- var streamId = GetStreamId(remoteJSDataStream, jsRuntime);
- // Act & Assert 1
- var success = await RemoteJSDataStream.ReceiveData(jsRuntime, streamId, chunkId: 0, chunk: null, error: "some error").DefaultTimeout();
- Assert.False(success);
- // Act & Assert 2
- using var mem = new MemoryStream();
- var ex = await Assert.ThrowsAsync<InvalidOperationException>(async () => await remoteJSDataStream.CopyToAsync(mem).DefaultTimeout());
- Assert.Equal("An error occurred while reading the remote stream: some error", ex.Message);
- }
- [Fact]
- public async Task ReceiveData_WithZeroLengthChunk()
- {
- // Arrange
- var jsRuntime = new TestRemoteJSRuntime(Options.Create(new CircuitOptions()), Options.Create(new HubOptions<ComponentHub>()), Mock.Of<ILogger<RemoteJSRuntime>>());
- var remoteJSDataStream = await CreateRemoteJSDataStreamAsync(jsRuntime);
- var streamId = GetStreamId(remoteJSDataStream, jsRuntime);
- var chunk = Array.Empty<byte>();
- // Act & Assert 1
- var ex = await Assert.ThrowsAsync<EndOfStreamException>(async () => await RemoteJSDataStream.ReceiveData(jsRuntime, streamId, chunkId: 0, chunk, error: null).DefaultTimeout());
- Assert.Equal("The incoming data chunk cannot be empty.", ex.Message);
- // Act & Assert 2
- using var mem = new MemoryStream();
- ex = await Assert.ThrowsAsync<EndOfStreamException>(async () => await remoteJSDataStream.CopyToAsync(mem).DefaultTimeout());
- Assert.Equal("The incoming data chunk cannot be empty.", ex.Message);
- }
- [Fact]
- public async Task ReceiveData_WithLargerChunksThanPermitted()
- {
- // Arrange
- var jsRuntime = new TestRemoteJSRuntime(Options.Create(new CircuitOptions()), Options.Create(new HubOptions<ComponentHub>()), Mock.Of<ILogger<RemoteJSRuntime>>());
- var remoteJSDataStream = await CreateRemoteJSDataStreamAsync(jsRuntime);
- var streamId = GetStreamId(remoteJSDataStream, jsRuntime);
- var chunk = new byte[50_000]; // more than the 32k maximum chunk size
- // Act & Assert 1
- var ex = await Assert.ThrowsAsync<EndOfStreamException>(async () => await RemoteJSDataStream.ReceiveData(jsRuntime, streamId, chunkId: 0, chunk, error: null).DefaultTimeout());
- Assert.Equal("The incoming data chunk exceeded the permitted length.", ex.Message);
- // Act & Assert 2
- using var mem = new MemoryStream();
- ex = await Assert.ThrowsAsync<EndOfStreamException>(async () => await remoteJSDataStream.CopyToAsync(mem).DefaultTimeout());
- Assert.Equal("The incoming data chunk exceeded the permitted length.", ex.Message);
- }
- [Fact]
- public async Task ReceiveData_ProvidedWithMoreBytesThanRemaining()
- {
- // Arrange
- var jsRuntime = new TestRemoteJSRuntime(Options.Create(new CircuitOptions()), Options.Create(new HubOptions<ComponentHub>()), Mock.Of<ILogger<RemoteJSRuntime>>());
- var jsStreamReference = Mock.Of<IJSStreamReference>();
- var remoteJSDataStream = await RemoteJSDataStream.CreateRemoteJSDataStreamAsync(jsRuntime, jsStreamReference, totalLength: 100, signalRMaximumIncomingBytes: 10_000, jsInteropDefaultCallTimeout: TimeSpan.FromMinutes(1), cancellationToken: CancellationToken.None);
- var streamId = GetStreamId(remoteJSDataStream, jsRuntime);
- var chunk = new byte[110]; // 100 byte totalLength for stream
- // Act & Assert 1
- var ex = await Assert.ThrowsAsync<EndOfStreamException>(async () => await RemoteJSDataStream.ReceiveData(jsRuntime, streamId, chunkId: 0, chunk, error: null).DefaultTimeout());
- Assert.Equal("The incoming data stream declared a length 100, but 110 bytes were sent.", ex.Message);
- // Act & Assert 2
- using var mem = new MemoryStream();
- ex = await Assert.ThrowsAsync<EndOfStreamException>(async () => await remoteJSDataStream.CopyToAsync(mem).DefaultTimeout());
- Assert.Equal("The incoming data stream declared a length 100, but 110 bytes were sent.", ex.Message);
- }
- [Fact]
- public async Task ReceiveData_ProvidedWithOutOfOrderChunk_SimulatesSignalRDisconnect()
- {
- // Arrange
- var jsRuntime = new TestRemoteJSRuntime(Options.Create(new CircuitOptions()), Options.Create(new HubOptions<ComponentHub>()), Mock.Of<ILogger<RemoteJSRuntime>>());
- var jsStreamReference = Mock.Of<IJSStreamReference>();
- var remoteJSDataStream = await RemoteJSDataStream.CreateRemoteJSDataStreamAsync(jsRuntime, jsStreamReference, totalLength: 100, signalRMaximumIncomingBytes: 10_000, jsInteropDefaultCallTimeout: TimeSpan.FromMinutes(1), cancellationToken: CancellationToken.None);
- var streamId = GetStreamId(remoteJSDataStream, jsRuntime);
- var chunk = new byte[5];
- // Act & Assert 1
- for (var i = 0; i < 5; i++)
- {
- await RemoteJSDataStream.ReceiveData(jsRuntime, streamId, chunkId: i, chunk, error: null);
- }
- var ex = await Assert.ThrowsAsync<EndOfStreamException>(async () => await RemoteJSDataStream.ReceiveData(jsRuntime, streamId, chunkId: 7, chunk, error: null).DefaultTimeout());
- Assert.Equal("Out of sequence chunk received, expected 5, but received 7.", ex.Message);
- // Act & Assert 2
- using var mem = new MemoryStream();
- ex = await Assert.ThrowsAsync<EndOfStreamException>(async () => await remoteJSDataStream.CopyToAsync(mem).DefaultTimeout());
- Assert.Equal("Out of sequence chunk received, expected 5, but received 7.", ex.Message);
- }
- [Fact]
- public async Task ReceiveData_NoDataProvidedBeforeTimeout_StreamDisposed()
- {
- // Arrange
- var unhandledExceptionRaisedTask = new TaskCompletionSource<bool>();
- var jsRuntime = new TestRemoteJSRuntime(Options.Create(new CircuitOptions()), Options.Create(new HubOptions<ComponentHub>()), Mock.Of<ILogger<RemoteJSRuntime>>());
- jsRuntime.UnhandledException += (_, ex) =>
- {
- Assert.Equal("Did not receive any data in the allotted time.", ex.Message);
- unhandledExceptionRaisedTask.SetResult(ex is TimeoutException);
- };
- var jsStreamReference = Mock.Of<IJSStreamReference>();
- var remoteJSDataStream = await RemoteJSDataStream.CreateRemoteJSDataStreamAsync(
- jsRuntime,
- jsStreamReference,
- totalLength: 15,
- signalRMaximumIncomingBytes: 10_000,
- jsInteropDefaultCallTimeout: TimeSpan.FromSeconds(2),
- cancellationToken: CancellationToken.None);
- var streamId = GetStreamId(remoteJSDataStream, jsRuntime);
- var chunk = new byte[] { 3, 5, 7 };
- // Act & Assert 1
- // Trigger timeout and ensure unhandled exception raised to crush circuit
- remoteJSDataStream.InvalidateLastDataReceivedTimeForTimeout();
- var unhandledExceptionResult = await unhandledExceptionRaisedTask.Task.DefaultTimeout();
- Assert.True(unhandledExceptionResult);
- // Act & Assert 2
- // Confirm exception also raised on pipe reader
- using var mem = new MemoryStream();
- var ex = await Assert.ThrowsAsync<TimeoutException>(async () => await remoteJSDataStream.CopyToAsync(mem).DefaultTimeout());
- Assert.Equal("Did not receive any data in the allotted time.", ex.Message);
- // Act & Assert 3
- // Ensures stream is disposed after the timeout and any additional chunks aren't accepted
- var success = await RemoteJSDataStream.ReceiveData(jsRuntime, streamId, chunkId: 0, chunk, error: null).DefaultTimeout();
- Assert.False(success);
- }
- [Fact]
- public async Task ReceiveData_ReceivesDataThenTimesout_StreamDisposed()
- {
- // Arrange
- var unhandledExceptionRaisedTask = new TaskCompletionSource<bool>();
- var jsRuntime = new TestRemoteJSRuntime(Options.Create(new CircuitOptions()), Options.Create(new HubOptions<ComponentHub>()), Mock.Of<ILogger<RemoteJSRuntime>>());
- jsRuntime.UnhandledException += (_, ex) =>
- {
- Assert.Equal("Did not receive any data in the allotted time.", ex.Message);
- unhandledExceptionRaisedTask.SetResult(ex is TimeoutException);
- };
- var jsStreamReference = Mock.Of<IJSStreamReference>();
- var remoteJSDataStream = await RemoteJSDataStream.CreateRemoteJSDataStreamAsync(
- jsRuntime,
- jsStreamReference,
- totalLength: 15,
- signalRMaximumIncomingBytes: 10_000,
- jsInteropDefaultCallTimeout: TimeSpan.FromSeconds(3),
- cancellationToken: CancellationToken.None);
- var streamId = GetStreamId(remoteJSDataStream, jsRuntime);
- var chunk = new byte[] { 3, 5, 7 };
- // Act & Assert 1
- var success = await RemoteJSDataStream.ReceiveData(jsRuntime, streamId, chunkId: 0, chunk, error: null).DefaultTimeout();
- Assert.True(success);
- // Act & Assert 2
- success = await RemoteJSDataStream.ReceiveData(jsRuntime, streamId, chunkId: 1, chunk, error: null).DefaultTimeout();
- Assert.True(success);
- // Act & Assert 3
- // Trigger timeout and ensure unhandled exception raised to crush circuit
- remoteJSDataStream.InvalidateLastDataReceivedTimeForTimeout();
- var unhandledExceptionResult = await unhandledExceptionRaisedTask.Task.DefaultTimeout();
- Assert.True(unhandledExceptionResult);
- // Act & Assert 4
- // Confirm exception also raised on pipe reader
- using var mem = new MemoryStream();
- var ex = await Assert.ThrowsAsync<TimeoutException>(async () => await remoteJSDataStream.CopyToAsync(mem).DefaultTimeout());
- Assert.Equal("Did not receive any data in the allotted time.", ex.Message);
- // Act & Assert 5
- // Ensures stream is disposed after the timeout and any additional chunks aren't accepted
- success = await RemoteJSDataStream.ReceiveData(jsRuntime, streamId, chunkId: 2, chunk, error: null).DefaultTimeout();
- Assert.False(success);
- }
- private static async Task<RemoteJSDataStream> CreateRemoteJSDataStreamAsync(TestRemoteJSRuntime jsRuntime = null)
- {
- var jsStreamReference = Mock.Of<IJSStreamReference>();
- var remoteJSDataStream = await RemoteJSDataStream.CreateRemoteJSDataStreamAsync(jsRuntime ?? _jsRuntime, jsStreamReference, totalLength: 100, signalRMaximumIncomingBytes: 10_000, jsInteropDefaultCallTimeout: TimeSpan.FromMinutes(1), cancellationToken: CancellationToken.None);
- return remoteJSDataStream;
- }
- private static long GetStreamId(RemoteJSDataStream stream, RemoteJSRuntime runtime) =>
- runtime.RemoteJSDataStreamInstances.FirstOrDefault(kvp => kvp.Value == stream).Key;
- class TestRemoteJSRuntime : RemoteJSRuntime, IJSRuntime
- {
- public TestRemoteJSRuntime(IOptions<CircuitOptions> circuitOptions, IOptions<HubOptions<ComponentHub>> hubOptions, ILogger<RemoteJSRuntime> logger) : base(circuitOptions, hubOptions, logger)
- {
- }
- public new ValueTask<TValue> InvokeAsync<TValue>(string identifier, object[] args)
- {
- Assert.Equal("Blazor._internal.sendJSDataStream", identifier);
- return ValueTask.FromResult<TValue>(default);
- }
- }
- }
|