File: NativeFramingTests.cs
Web Access
Project: src\tests\Aspire.Hosting.Native.Core.Tests\Aspire.Hosting.Native.Core.Tests.csproj (Aspire.Hosting.Native.Core.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.Text;
using Aspire.Hosting.Native.Core.Tests.TestServices;
using Aspire.Hosting.Native.Rpc;
using Aspire.Hosting.Native.Server;
using Microsoft.Extensions.Logging.Abstractions;
 
namespace Aspire.Hosting.Native.Core.Tests;
 
public class NativeFramingTests
{
    [Theory]
    [InlineData(1)]
    [InlineData(2)]
    [InlineData(3)]
    [InlineData(7)]
    [InlineData(16)]
    public async Task HeaderAndBodyFragmentationPreservesTheNextCoalescedFrame(int maximumReadBytes)
    {
        using var stream = new FragmentedReadStream(
            "Content-Length: 4\r\nContent-Type: application/json\r\n\r\n\"\u00e9\"Content-Length: 2\r\n\r\n{}"u8.ToArray(),
            maximumReadBytes);
        var framing = new NativeRpcFraming(stream);
        Assert.Equal("\"\u00e9\"", Encoding.UTF8.GetString((await framing.ReadAsync(TestContext.Current.CancellationToken))!));
        Assert.Equal("{}", Encoding.UTF8.GetString((await framing.ReadAsync(TestContext.Current.CancellationToken))!));
        Assert.Null(await framing.ReadAsync(TestContext.Current.CancellationToken));
    }
 
    [Theory]
    [InlineData(false)]
    [InlineData(true)]
    public async Task ExactHeaderLimitIsAcceptedButAnExtraByteIsRejected(bool overflow)
    {
        const string prefix = "Content-Type: ";
        const string suffix = "\r\nContent-Length: 2\r\n\r\n";
        var header = prefix + new string('x', NativeRpcFrameProtocol.MaximumHeaderBytes - prefix.Length - suffix.Length + (overflow ? 1 : 0)) + suffix;
        using var stream = new FragmentedReadStream(Encoding.ASCII.GetBytes(header + "{}"), 7);
        var framing = new NativeRpcFraming(stream);
        if (overflow)
        {
            await Assert.ThrowsAsync<InvalidDataException>(() => framing.ReadAsync(TestContext.Current.CancellationToken));
        }
        else
        {
            Assert.Equal("{}", Encoding.UTF8.GetString((await framing.ReadAsync(TestContext.Current.CancellationToken))!));
        }
    }
 
    [Fact]
    public async Task NonAsciiHeaderValuesRemainInvalid()
    {
        using var stream = new FragmentedReadStream("Content-Type: \u00e9\r\nContent-Length: 2\r\n\r\n{}"u8.ToArray(), 1);
        await Assert.ThrowsAsync<InvalidDataException>(() => new NativeRpcFraming(stream).ReadAsync(TestContext.Current.CancellationToken));
    }
 
    [Fact]
    public async Task PeerKeepsBorrowedEnvelopeAliveUntilAsyncDispatchCompletes()
    {
        using var stream = new MemoryStream();
        await NativeRpcFraming.WriteAsync(stream,
            """{"jsonrpc":"2.0","id":1,"method":"pending","params":{"value":"retained"}}"""u8.ToArray(), CancellationToken.None);
        await NativeRpcFraming.WriteAsync(stream,
            """{"jsonrpc":"2.0","id":2,"method":"next"}"""u8.ToArray(), CancellationToken.None);
        stream.Position = 0;
        using var peer = new NativeRpcPeer(stream, new NativeServerOptions(), NullLogger.Instance);
        var resume = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
        var dispatched = 0;
        var run = peer.RunAsync(async envelope =>
        {
            Interlocked.Increment(ref dispatched);
            if (envelope.GetProperty("method").GetString() == "pending")
            {
                await resume.Task.WaitAsync(TestContext.Current.CancellationToken);
                Assert.Equal("retained", envelope.GetProperty("params").GetProperty("value").GetString());
                Assert.Equal(1, envelope.GetProperty("id").GetInt32());
            }
            return null;
        }, TestContext.Current.CancellationToken);
        try
        {
            Assert.Equal(2, dispatched);
            Assert.True(peer.Closed.IsCancellationRequested);
            Assert.False(run.IsCompleted);
        }
        finally
        {
            resume.TrySetResult();
            await run.WaitAsync(TestContext.Current.CancellationToken);
        }
    }
 
    [Theory]
    [InlineData("content-length:\t2 \r\nContent-Type: application/vscode-jsonrpc; charset=utf-8\r\n\r\n{}")]
    [InlineData("Content-Type: application/json\r\nContent-Length: 2\r\n\r\n{}")]
    public async Task ReusedHeaderBufferPreservesCaseWhitespaceAndOptionalContentType(string frame)
    {
        using var stream = new MemoryStream(Encoding.ASCII.GetBytes(frame + "Content-Length: 4\r\n\r\nnull"));
        var reader = new NativeRpcFraming(stream);
        Assert.Equal("{}", Encoding.UTF8.GetString((await reader.ReadAsync(CancellationToken.None))!));
        Assert.Equal("null", Encoding.UTF8.GetString((await reader.ReadAsync(CancellationToken.None))!));
        Assert.Null(await reader.ReadAsync(CancellationToken.None));
    }
 
    [Fact]
    public async Task CoalescedFramesPreserveUtf8ByteLengthsAndTheNextMessage()
    {
        using var stream = new MemoryStream();
        await NativeRpcFraming.WriteAsync(stream, Encoding.UTF8.GetBytes("{\"message\":\"caf\u00e9\"}"), CancellationToken.None);
        await NativeRpcFraming.WriteAsync(stream, "{}"u8.ToArray(), CancellationToken.None);
        stream.Position = 0;
        var reader = new NativeRpcFraming(stream);
        Assert.Equal("{\"message\":\"caf\u00e9\"}", Encoding.UTF8.GetString((await reader.ReadAsync(CancellationToken.None))!));
        Assert.Equal("{}", Encoding.UTF8.GetString((await reader.ReadAsync(CancellationToken.None))!));
        Assert.Null(await reader.ReadAsync(CancellationToken.None));
    }
 
    [Theory]
    [InlineData("Content-Length: 262145\r\n\r\n")]
    [InlineData("Content-Length: 1\r\nContent-Length: 1\r\n\r\n")]
    [InlineData("Content-Length: -1\r\n\r\n")]
    [InlineData("Content-Length: invalid\r\n\r\n")]
    [InlineData("Content-Type: application/json\r\n\r\n")]
    [InlineData("Content-Length: 0\nContent-Type: application/json\r\n\r\n")]
    [InlineData("Content-Length: 0\r\nUnknown: value\r\n\r\n")]
    [InlineData("Content-Length: 0\r\ncontent-length: 0\r\n\r\n")]
    public async Task InvalidLengthsAreRejectedBeforeReadingThePayload(string header)
    {
        using var stream = new MemoryStream(Encoding.ASCII.GetBytes(header));
        var reader = new NativeRpcFraming(stream);
        await Assert.ThrowsAsync<InvalidDataException>(() => reader.ReadAsync(CancellationToken.None));
    }
 
    [Theory]
    [InlineData("Content-Length: 10\r\n")]
    [InlineData("Content-Length: 10\r\n\r\n{}")]
    public async Task TruncatedFramesAreNotTreatedAsCompletedRequests(string frame)
    {
        using var stream = new MemoryStream(Encoding.ASCII.GetBytes(frame));
        var reader = new NativeRpcFraming(stream);
        await Assert.ThrowsAsync<EndOfStreamException>(() => reader.ReadAsync(CancellationToken.None));
    }
}