RemoteJSDataStreamTest.cs 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312
  1. // Licensed to the .NET Foundation under one or more agreements.
  2. // The .NET Foundation licenses this file to you under the MIT license.
  3. using Microsoft.AspNetCore.SignalR;
  4. using Microsoft.AspNetCore.InternalTesting;
  5. using Microsoft.Extensions.Logging;
  6. using Microsoft.Extensions.Options;
  7. using Microsoft.JSInterop;
  8. using Moq;
  9. namespace Microsoft.AspNetCore.Components.Server.Circuits;
  10. public class RemoteJSDataStreamTest
  11. {
  12. private static readonly TestRemoteJSRuntime _jsRuntime = new(Options.Create(new CircuitOptions()), Options.Create(new HubOptions<ComponentHub>()), Mock.Of<ILogger<RemoteJSRuntime>>());
  13. [Fact]
  14. public async Task CreateRemoteJSDataStreamAsync_CreatesStream()
  15. {
  16. // Arrange
  17. var jsStreamReference = Mock.Of<IJSStreamReference>();
  18. // Act
  19. var remoteJSDataStream = await RemoteJSDataStream.CreateRemoteJSDataStreamAsync(_jsRuntime, jsStreamReference, totalLength: 100, signalRMaximumIncomingBytes: 10_000, jsInteropDefaultCallTimeout: TimeSpan.FromMinutes(1), cancellationToken: CancellationToken.None).DefaultTimeout();
  20. // Assert
  21. Assert.NotNull(remoteJSDataStream);
  22. }
  23. [Fact]
  24. public async Task ReceiveData_DoesNotFindStream()
  25. {
  26. // Arrange
  27. var chunk = new byte[] { 3, 5, 6, 7 };
  28. var unrecognizedGuid = 10;
  29. // Act
  30. var success = await RemoteJSDataStream.ReceiveData(_jsRuntime, streamId: unrecognizedGuid, chunkId: 0, chunk, error: null).DefaultTimeout();
  31. // Assert
  32. Assert.False(success);
  33. }
  34. [Fact]
  35. public async Task ReceiveData_SuccessReadsBackStream()
  36. {
  37. // Arrange
  38. var jsRuntime = new TestRemoteJSRuntime(Options.Create(new CircuitOptions()), Options.Create(new HubOptions<ComponentHub>()), Mock.Of<ILogger<RemoteJSRuntime>>());
  39. var remoteJSDataStream = await CreateRemoteJSDataStreamAsync(jsRuntime);
  40. var streamId = GetStreamId(remoteJSDataStream, jsRuntime);
  41. var chunk = new byte[100];
  42. var random = new Random();
  43. random.NextBytes(chunk);
  44. var sendDataTask = Task.Run(async () =>
  45. {
  46. // Act 1
  47. var success = await RemoteJSDataStream.ReceiveData(jsRuntime, streamId, chunkId: 0, chunk, error: null).DefaultTimeout();
  48. return success;
  49. });
  50. // Act & Assert 2
  51. using var memoryStream = new MemoryStream();
  52. await remoteJSDataStream.CopyToAsync(memoryStream).DefaultTimeout();
  53. Assert.Equal(chunk, memoryStream.ToArray());
  54. // Act & Assert 3
  55. var sendDataCompleted = await sendDataTask.DefaultTimeout();
  56. Assert.True(sendDataCompleted);
  57. }
  58. [Fact]
  59. public async Task ReceiveData_SuccessReadsBackPipeReader()
  60. {
  61. // Arrange
  62. var jsRuntime = new TestRemoteJSRuntime(Options.Create(new CircuitOptions()), Options.Create(new HubOptions<ComponentHub>()), Mock.Of<ILogger<RemoteJSRuntime>>());
  63. var remoteJSDataStream = await CreateRemoteJSDataStreamAsync(jsRuntime);
  64. var streamId = GetStreamId(remoteJSDataStream, jsRuntime);
  65. var chunk = new byte[100];
  66. var random = new Random();
  67. random.NextBytes(chunk);
  68. var sendDataTask = Task.Run(async () =>
  69. {
  70. // Act 1
  71. var success = await RemoteJSDataStream.ReceiveData(jsRuntime, streamId, chunkId: 0, chunk, error: null).DefaultTimeout();
  72. return success;
  73. });
  74. // Act & Assert 2
  75. using var memoryStream = new MemoryStream();
  76. await remoteJSDataStream.PipeReader.CopyToAsync(memoryStream).DefaultTimeout();
  77. Assert.Equal(chunk, memoryStream.ToArray());
  78. // Act & Assert 3
  79. var sendDataCompleted = await sendDataTask.DefaultTimeout();
  80. Assert.True(sendDataCompleted);
  81. }
  82. [Fact]
  83. public async Task ReceiveData_WithError()
  84. {
  85. // Arrange
  86. var jsRuntime = new TestRemoteJSRuntime(Options.Create(new CircuitOptions()), Options.Create(new HubOptions<ComponentHub>()), Mock.Of<ILogger<RemoteJSRuntime>>());
  87. var remoteJSDataStream = await CreateRemoteJSDataStreamAsync(jsRuntime);
  88. var streamId = GetStreamId(remoteJSDataStream, jsRuntime);
  89. // Act & Assert 1
  90. var success = await RemoteJSDataStream.ReceiveData(jsRuntime, streamId, chunkId: 0, chunk: null, error: "some error").DefaultTimeout();
  91. Assert.False(success);
  92. // Act & Assert 2
  93. using var mem = new MemoryStream();
  94. var ex = await Assert.ThrowsAsync<InvalidOperationException>(async () => await remoteJSDataStream.CopyToAsync(mem).DefaultTimeout());
  95. Assert.Equal("An error occurred while reading the remote stream: some error", ex.Message);
  96. }
  97. [Fact]
  98. public async Task ReceiveData_WithZeroLengthChunk()
  99. {
  100. // Arrange
  101. var jsRuntime = new TestRemoteJSRuntime(Options.Create(new CircuitOptions()), Options.Create(new HubOptions<ComponentHub>()), Mock.Of<ILogger<RemoteJSRuntime>>());
  102. var remoteJSDataStream = await CreateRemoteJSDataStreamAsync(jsRuntime);
  103. var streamId = GetStreamId(remoteJSDataStream, jsRuntime);
  104. var chunk = Array.Empty<byte>();
  105. // Act & Assert 1
  106. var ex = await Assert.ThrowsAsync<EndOfStreamException>(async () => await RemoteJSDataStream.ReceiveData(jsRuntime, streamId, chunkId: 0, chunk, error: null).DefaultTimeout());
  107. Assert.Equal("The incoming data chunk cannot be empty.", ex.Message);
  108. // Act & Assert 2
  109. using var mem = new MemoryStream();
  110. ex = await Assert.ThrowsAsync<EndOfStreamException>(async () => await remoteJSDataStream.CopyToAsync(mem).DefaultTimeout());
  111. Assert.Equal("The incoming data chunk cannot be empty.", ex.Message);
  112. }
  113. [Fact]
  114. public async Task ReceiveData_WithLargerChunksThanPermitted()
  115. {
  116. // Arrange
  117. var jsRuntime = new TestRemoteJSRuntime(Options.Create(new CircuitOptions()), Options.Create(new HubOptions<ComponentHub>()), Mock.Of<ILogger<RemoteJSRuntime>>());
  118. var remoteJSDataStream = await CreateRemoteJSDataStreamAsync(jsRuntime);
  119. var streamId = GetStreamId(remoteJSDataStream, jsRuntime);
  120. var chunk = new byte[50_000]; // more than the 32k maximum chunk size
  121. // Act & Assert 1
  122. var ex = await Assert.ThrowsAsync<EndOfStreamException>(async () => await RemoteJSDataStream.ReceiveData(jsRuntime, streamId, chunkId: 0, chunk, error: null).DefaultTimeout());
  123. Assert.Equal("The incoming data chunk exceeded the permitted length.", ex.Message);
  124. // Act & Assert 2
  125. using var mem = new MemoryStream();
  126. ex = await Assert.ThrowsAsync<EndOfStreamException>(async () => await remoteJSDataStream.CopyToAsync(mem).DefaultTimeout());
  127. Assert.Equal("The incoming data chunk exceeded the permitted length.", ex.Message);
  128. }
  129. [Fact]
  130. public async Task ReceiveData_ProvidedWithMoreBytesThanRemaining()
  131. {
  132. // Arrange
  133. var jsRuntime = new TestRemoteJSRuntime(Options.Create(new CircuitOptions()), Options.Create(new HubOptions<ComponentHub>()), Mock.Of<ILogger<RemoteJSRuntime>>());
  134. var jsStreamReference = Mock.Of<IJSStreamReference>();
  135. var remoteJSDataStream = await RemoteJSDataStream.CreateRemoteJSDataStreamAsync(jsRuntime, jsStreamReference, totalLength: 100, signalRMaximumIncomingBytes: 10_000, jsInteropDefaultCallTimeout: TimeSpan.FromMinutes(1), cancellationToken: CancellationToken.None);
  136. var streamId = GetStreamId(remoteJSDataStream, jsRuntime);
  137. var chunk = new byte[110]; // 100 byte totalLength for stream
  138. // Act & Assert 1
  139. var ex = await Assert.ThrowsAsync<EndOfStreamException>(async () => await RemoteJSDataStream.ReceiveData(jsRuntime, streamId, chunkId: 0, chunk, error: null).DefaultTimeout());
  140. Assert.Equal("The incoming data stream declared a length 100, but 110 bytes were sent.", ex.Message);
  141. // Act & Assert 2
  142. using var mem = new MemoryStream();
  143. ex = await Assert.ThrowsAsync<EndOfStreamException>(async () => await remoteJSDataStream.CopyToAsync(mem).DefaultTimeout());
  144. Assert.Equal("The incoming data stream declared a length 100, but 110 bytes were sent.", ex.Message);
  145. }
  146. [Fact]
  147. public async Task ReceiveData_ProvidedWithOutOfOrderChunk_SimulatesSignalRDisconnect()
  148. {
  149. // Arrange
  150. var jsRuntime = new TestRemoteJSRuntime(Options.Create(new CircuitOptions()), Options.Create(new HubOptions<ComponentHub>()), Mock.Of<ILogger<RemoteJSRuntime>>());
  151. var jsStreamReference = Mock.Of<IJSStreamReference>();
  152. var remoteJSDataStream = await RemoteJSDataStream.CreateRemoteJSDataStreamAsync(jsRuntime, jsStreamReference, totalLength: 100, signalRMaximumIncomingBytes: 10_000, jsInteropDefaultCallTimeout: TimeSpan.FromMinutes(1), cancellationToken: CancellationToken.None);
  153. var streamId = GetStreamId(remoteJSDataStream, jsRuntime);
  154. var chunk = new byte[5];
  155. // Act & Assert 1
  156. for (var i = 0; i < 5; i++)
  157. {
  158. await RemoteJSDataStream.ReceiveData(jsRuntime, streamId, chunkId: i, chunk, error: null);
  159. }
  160. var ex = await Assert.ThrowsAsync<EndOfStreamException>(async () => await RemoteJSDataStream.ReceiveData(jsRuntime, streamId, chunkId: 7, chunk, error: null).DefaultTimeout());
  161. Assert.Equal("Out of sequence chunk received, expected 5, but received 7.", ex.Message);
  162. // Act & Assert 2
  163. using var mem = new MemoryStream();
  164. ex = await Assert.ThrowsAsync<EndOfStreamException>(async () => await remoteJSDataStream.CopyToAsync(mem).DefaultTimeout());
  165. Assert.Equal("Out of sequence chunk received, expected 5, but received 7.", ex.Message);
  166. }
  167. [Fact]
  168. public async Task ReceiveData_NoDataProvidedBeforeTimeout_StreamDisposed()
  169. {
  170. // Arrange
  171. var unhandledExceptionRaisedTask = new TaskCompletionSource<bool>();
  172. var jsRuntime = new TestRemoteJSRuntime(Options.Create(new CircuitOptions()), Options.Create(new HubOptions<ComponentHub>()), Mock.Of<ILogger<RemoteJSRuntime>>());
  173. jsRuntime.UnhandledException += (_, ex) =>
  174. {
  175. Assert.Equal("Did not receive any data in the allotted time.", ex.Message);
  176. unhandledExceptionRaisedTask.SetResult(ex is TimeoutException);
  177. };
  178. var jsStreamReference = Mock.Of<IJSStreamReference>();
  179. var remoteJSDataStream = await RemoteJSDataStream.CreateRemoteJSDataStreamAsync(
  180. jsRuntime,
  181. jsStreamReference,
  182. totalLength: 15,
  183. signalRMaximumIncomingBytes: 10_000,
  184. jsInteropDefaultCallTimeout: TimeSpan.FromSeconds(2),
  185. cancellationToken: CancellationToken.None);
  186. var streamId = GetStreamId(remoteJSDataStream, jsRuntime);
  187. var chunk = new byte[] { 3, 5, 7 };
  188. // Act & Assert 1
  189. // Trigger timeout and ensure unhandled exception raised to crush circuit
  190. remoteJSDataStream.InvalidateLastDataReceivedTimeForTimeout();
  191. var unhandledExceptionResult = await unhandledExceptionRaisedTask.Task.DefaultTimeout();
  192. Assert.True(unhandledExceptionResult);
  193. // Act & Assert 2
  194. // Confirm exception also raised on pipe reader
  195. using var mem = new MemoryStream();
  196. var ex = await Assert.ThrowsAsync<TimeoutException>(async () => await remoteJSDataStream.CopyToAsync(mem).DefaultTimeout());
  197. Assert.Equal("Did not receive any data in the allotted time.", ex.Message);
  198. // Act & Assert 3
  199. // Ensures stream is disposed after the timeout and any additional chunks aren't accepted
  200. var success = await RemoteJSDataStream.ReceiveData(jsRuntime, streamId, chunkId: 0, chunk, error: null).DefaultTimeout();
  201. Assert.False(success);
  202. }
  203. [Fact]
  204. public async Task ReceiveData_ReceivesDataThenTimesout_StreamDisposed()
  205. {
  206. // Arrange
  207. var unhandledExceptionRaisedTask = new TaskCompletionSource<bool>();
  208. var jsRuntime = new TestRemoteJSRuntime(Options.Create(new CircuitOptions()), Options.Create(new HubOptions<ComponentHub>()), Mock.Of<ILogger<RemoteJSRuntime>>());
  209. jsRuntime.UnhandledException += (_, ex) =>
  210. {
  211. Assert.Equal("Did not receive any data in the allotted time.", ex.Message);
  212. unhandledExceptionRaisedTask.SetResult(ex is TimeoutException);
  213. };
  214. var jsStreamReference = Mock.Of<IJSStreamReference>();
  215. var remoteJSDataStream = await RemoteJSDataStream.CreateRemoteJSDataStreamAsync(
  216. jsRuntime,
  217. jsStreamReference,
  218. totalLength: 15,
  219. signalRMaximumIncomingBytes: 10_000,
  220. jsInteropDefaultCallTimeout: TimeSpan.FromSeconds(3),
  221. cancellationToken: CancellationToken.None);
  222. var streamId = GetStreamId(remoteJSDataStream, jsRuntime);
  223. var chunk = new byte[] { 3, 5, 7 };
  224. // Act & Assert 1
  225. var success = await RemoteJSDataStream.ReceiveData(jsRuntime, streamId, chunkId: 0, chunk, error: null).DefaultTimeout();
  226. Assert.True(success);
  227. // Act & Assert 2
  228. success = await RemoteJSDataStream.ReceiveData(jsRuntime, streamId, chunkId: 1, chunk, error: null).DefaultTimeout();
  229. Assert.True(success);
  230. // Act & Assert 3
  231. // Trigger timeout and ensure unhandled exception raised to crush circuit
  232. remoteJSDataStream.InvalidateLastDataReceivedTimeForTimeout();
  233. var unhandledExceptionResult = await unhandledExceptionRaisedTask.Task.DefaultTimeout();
  234. Assert.True(unhandledExceptionResult);
  235. // Act & Assert 4
  236. // Confirm exception also raised on pipe reader
  237. using var mem = new MemoryStream();
  238. var ex = await Assert.ThrowsAsync<TimeoutException>(async () => await remoteJSDataStream.CopyToAsync(mem).DefaultTimeout());
  239. Assert.Equal("Did not receive any data in the allotted time.", ex.Message);
  240. // Act & Assert 5
  241. // Ensures stream is disposed after the timeout and any additional chunks aren't accepted
  242. success = await RemoteJSDataStream.ReceiveData(jsRuntime, streamId, chunkId: 2, chunk, error: null).DefaultTimeout();
  243. Assert.False(success);
  244. }
  245. private static async Task<RemoteJSDataStream> CreateRemoteJSDataStreamAsync(TestRemoteJSRuntime jsRuntime = null)
  246. {
  247. var jsStreamReference = Mock.Of<IJSStreamReference>();
  248. var remoteJSDataStream = await RemoteJSDataStream.CreateRemoteJSDataStreamAsync(jsRuntime ?? _jsRuntime, jsStreamReference, totalLength: 100, signalRMaximumIncomingBytes: 10_000, jsInteropDefaultCallTimeout: TimeSpan.FromMinutes(1), cancellationToken: CancellationToken.None);
  249. return remoteJSDataStream;
  250. }
  251. private static long GetStreamId(RemoteJSDataStream stream, RemoteJSRuntime runtime) =>
  252. runtime.RemoteJSDataStreamInstances.FirstOrDefault(kvp => kvp.Value == stream).Key;
  253. class TestRemoteJSRuntime : RemoteJSRuntime, IJSRuntime
  254. {
  255. public TestRemoteJSRuntime(IOptions<CircuitOptions> circuitOptions, IOptions<HubOptions<ComponentHub>> hubOptions, ILogger<RemoteJSRuntime> logger) : base(circuitOptions, hubOptions, logger)
  256. {
  257. }
  258. public new ValueTask<TValue> InvokeAsync<TValue>(string identifier, object[] args)
  259. {
  260. Assert.Equal("Blazor._internal.sendJSDataStream", identifier);
  261. return ValueTask.FromResult<TValue>(default);
  262. }
  263. }
  264. }