File: PullFromJSDataStreamTest.cs
Web Access
Project: src\src\Components\WebAssembly\WebAssembly\test\Microsoft.AspNetCore.Components.WebAssembly.Tests.csproj (Microsoft.AspNetCore.Components.WebAssembly.Tests)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Diagnostics.CodeAnalysis;
using Microsoft.AspNetCore.InternalTesting;
using Microsoft.JSInterop;
using Moq;
 
namespace Microsoft.AspNetCore.Components;
 
public class PullFromJSDataStreamTest
{
    private static readonly TestJSRuntime _jsRuntime = new();
    private static readonly byte[] Data = new byte[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 };
 
    [Fact]
    public void CreateJSDataStream_CreatesStream()
    {
        // Arrange
        var jsStreamReference = Mock.Of<IJSStreamReference>();
 
        // Act
        var pullFromJSDataStream = PullFromJSDataStream.CreateJSDataStream(_jsRuntime, jsStreamReference, totalLength: 100, cancellationToken: CancellationToken.None);
 
        // Assert
        Assert.NotNull(pullFromJSDataStream);
    }
 
    [Fact]
    public async Task ReceiveData_SuccessReadsBackStream_UsingByteArrayBuffer()
    {
        // Arrange
        var expectedChunks = new byte[][]
        {
                new byte[] { 1, 2, 3 },
                new byte[] { 4, 5, 6 },
                new byte[] { 7, 8, 9 },
        };
        var pullFromJSDataStream = CreateJSDataStream(Data);
 
        // Act & Assert
        for (byte i = 0; i < 3; i++)
        {
            var buffer = new byte[3];
            Assert.Equal(3, await pullFromJSDataStream.ReadAsync(buffer));
            Assert.Equal(expectedChunks[i], buffer);
        }
        Assert.Equal(pullFromJSDataStream.Position, Data.Length);
    }
 
    [Fact]
    public async Task ReceiveData_SuccessReadsBackStream_UsingMemoryBuffer()
    {
        // Arrange
        var pullFromJSDataStream = CreateJSDataStream(Data);
 
        // Act & Assert
        using var mem = new MemoryStream();
        await pullFromJSDataStream.CopyToAsync(mem).DefaultTimeout();
        Assert.Equal(Data, mem.ToArray());
    }
 
    [Fact]
    public async Task ReceiveData_JSProvidesInsufficientData_Throws()
    {
        // Arrange
        var insufficientDataJSRuntime = new TestJSRuntime_ProvidesInsufficientData(Data);
        var pullFromJSDataStream = CreateJSDataStream(Data, insufficientDataJSRuntime);
 
        // Act & Assert
        using var mem = new MemoryStream();
        var ex = await Assert.ThrowsAsync<EndOfStreamException>(async () => await pullFromJSDataStream.CopyToAsync(mem).DefaultTimeout());
        Assert.Equal("Failed to read the requested number of bytes from the stream.", ex.Message);
    }
 
    [Fact]
    public async Task ReceiveData_JSProvidesExcessData_Throws()
    {
        // Arrange
        var data = new byte[50000];
        var excessDataJSRuntime = new TestJSRuntime_ProvidesExcessData(data);
        var pullFromJSDataStream = CreateJSDataStream(Data, excessDataJSRuntime);
 
        // Act & Assert
        using var mem = new MemoryStream();
        var ex = await Assert.ThrowsAsync<EndOfStreamException>(async () => await pullFromJSDataStream.CopyToAsync(mem).DefaultTimeout());
        Assert.Equal("Failed to read the requested number of bytes from the stream.", ex.Message);
    }
 
    [Fact]
    public async Task ReceiveData_JSProvidesExcessData_Throws2()
    {
        // Arrange
        var data = new byte[50000];
        var excessDataJSRuntime = new TestJSRuntime_ProvidesExcessData(data);
        var pullFromJSDataStream = CreateJSDataStream(Data, excessDataJSRuntime);
 
        // Act & Assert
        using var mem = new MemoryStream();
        var ex = await Assert.ThrowsAsync<EndOfStreamException>(async () => await pullFromJSDataStream.CopyToAsync(mem).DefaultTimeout());
        Assert.Equal("Failed to read the requested number of bytes from the stream.", ex.Message);
    }
 
    private static PullFromJSDataStream CreateJSDataStream(byte[] data, IJSRuntime runtime = null)
    {
        runtime ??= new TestJSRuntime(data);
        var jsStreamReference = Mock.Of<IJSStreamReference>();
        var pullFromJSDataStream = PullFromJSDataStream.CreateJSDataStream(runtime, jsStreamReference, totalLength: data.Length, cancellationToken: CancellationToken.None);
        return pullFromJSDataStream;
    }
 
    class TestJSRuntime : IJSRuntime
    {
        protected readonly byte[] _data;
 
        public TestJSRuntime(byte[] data = default)
        {
            _data = data;
        }
 
        public virtual ValueTask<TValue> InvokeAsync<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.PublicFields | DynamicallyAccessedMemberTypes.PublicProperties)] TValue>(string identifier, CancellationToken cancellationToken, object[] args)
        {
            Assert.Equal("Blazor._internal.getJSDataStreamChunk", identifier);
            if (typeof(TValue) != typeof(byte[]))
            {
                throw new ArgumentException($"Unexpected call to InvokeAsync, expected byte[] got {typeof(TValue)}");
            }
            var offset = (long)args[1];
            var bytesToRead = (int)args[2];
            return ValueTask.FromResult((TValue)(object)_data.Skip((int)offset).Take(bytesToRead).ToArray());
        }
 
        public async ValueTask<TValue> InvokeAsync<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.PublicFields | DynamicallyAccessedMemberTypes.PublicProperties)] TValue>(string identifier, object[] args)
        {
            return await InvokeAsync<TValue>(identifier, CancellationToken.None, args);
        }
    }
 
    class TestJSRuntime_ProvidesInsufficientData : TestJSRuntime
    {
        public TestJSRuntime_ProvidesInsufficientData(byte[] data) : base(data)
        {
        }
 
        public override ValueTask<TValue> InvokeAsync<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.PublicFields | DynamicallyAccessedMemberTypes.PublicProperties)] TValue>(string identifier, CancellationToken cancellationToken, object[] args)
        {
            var offset = (long)args[1];
            var bytesToRead = (int)args[2];
            return ValueTask.FromResult((TValue)(object)_data.Skip((int)offset).Take(bytesToRead - 1).ToArray());
        }
    }
 
    class TestJSRuntime_ProvidesExcessData : TestJSRuntime
    {
        public TestJSRuntime_ProvidesExcessData(byte[] data) : base(data)
        {
        }
 
        public override ValueTask<TValue> InvokeAsync<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.PublicFields | DynamicallyAccessedMemberTypes.PublicProperties)] TValue>(string identifier, CancellationToken cancellationToken, object[] args)
        {
            var offset = (long)args[1];
            var bytesToRead = (int)args[2];
            return ValueTask.FromResult((TValue)(object)_data.Skip((int)offset).Take(bytesToRead + 1).ToArray());
        }
    }
}