Files
suitelinkclient/tests/SuiteLink.Client.Tests/Transport/SuiteLinkTcpTransportTests.cs
2026-03-17 11:04:19 -04:00

212 lines
7.0 KiB
C#

using SuiteLink.Client.Transport;
using System.Net;
using System.Net.Sockets;
namespace SuiteLink.Client.Tests.Transport;
public sealed class SuiteLinkTcpTransportTests
{
[Fact]
public async Task SendAsync_WithInjectedStream_WritesBytes()
{
using var stream = new MemoryStream();
await using var transport = new SuiteLinkTcpTransport(stream);
byte[] payload = [0x01, 0x02, 0x03];
await transport.SendAsync(payload, CancellationToken.None);
Assert.Equal(payload, stream.ToArray());
}
[Fact]
public async Task ReceiveAsync_WithInjectedStream_ReadsBytes()
{
using var stream = new MemoryStream([0x10, 0x20, 0x30]);
await using var transport = new SuiteLinkTcpTransport(stream);
byte[] buffer = new byte[2];
var bytesRead = await transport.ReceiveAsync(buffer, CancellationToken.None);
Assert.Equal(2, bytesRead);
Assert.Equal(0x10, buffer[0]);
Assert.Equal(0x20, buffer[1]);
}
[Fact]
public async Task SendAsync_WithoutConnection_ThrowsInvalidOperationException()
{
await using var transport = new SuiteLinkTcpTransport();
var ex = await Assert.ThrowsAsync<InvalidOperationException>(
() => transport.SendAsync(new byte[] { 0xAA }, CancellationToken.None).AsTask());
Assert.Contains("connected", ex.Message, StringComparison.OrdinalIgnoreCase);
}
[Fact]
public async Task ReceiveAsync_WithPartialReadStream_ReturnsPartialReadsAndEof()
{
using var stream = new PartialReadStream([0x10, 0x20, 0x30], maxChunkSize: 1);
await using var transport = new SuiteLinkTcpTransport(stream);
byte[] buffer = new byte[3];
var read1 = await transport.ReceiveAsync(buffer, CancellationToken.None);
var read2 = await transport.ReceiveAsync(buffer, CancellationToken.None);
var read3 = await transport.ReceiveAsync(buffer, CancellationToken.None);
var read4 = await transport.ReceiveAsync(buffer, CancellationToken.None);
Assert.Equal(1, read1);
Assert.Equal(1, read2);
Assert.Equal(1, read3);
Assert.Equal(0, read4);
}
[Fact]
public async Task DisposeAsync_AfterDisposal_SendAndReceiveThrowObjectDisposedException()
{
using var stream = new MemoryStream([0x01, 0x02, 0x03]);
var transport = new SuiteLinkTcpTransport(stream);
await transport.DisposeAsync();
await Assert.ThrowsAsync<ObjectDisposedException>(
() => transport.SendAsync(new byte[] { 0xAA }, CancellationToken.None).AsTask());
await Assert.ThrowsAsync<ObjectDisposedException>(
() => transport.ReceiveAsync(new byte[1], CancellationToken.None).AsTask());
}
[Fact]
public async Task DisposeAsync_LeaveOpenTrue_DoesNotDisposeInjectedStream()
{
var stream = new TrackingStream();
await using (var transport = new SuiteLinkTcpTransport(stream, leaveOpen: true))
{
await transport.DisposeAsync();
}
Assert.False(stream.WasDisposed);
}
[Fact]
public async Task ConnectAsync_ConcurrentCalls_CreateSingleConnection()
{
using var listener = new TcpListener(IPAddress.Loopback, 0);
listener.Start();
var endpoint = (IPEndPoint)listener.LocalEndpoint;
await using var transport = new SuiteLinkTcpTransport();
Task[] connectTasks =
[
transport.ConnectAsync(endpoint.Address.ToString(), endpoint.Port).AsTask(),
transport.ConnectAsync(endpoint.Address.ToString(), endpoint.Port).AsTask(),
transport.ConnectAsync(endpoint.Address.ToString(), endpoint.Port).AsTask(),
transport.ConnectAsync(endpoint.Address.ToString(), endpoint.Port).AsTask()
];
await Task.WhenAll(connectTasks);
using var accepted1 = await listener.AcceptTcpClientAsync();
using var secondAcceptCts = new CancellationTokenSource(TimeSpan.FromMilliseconds(300));
await Assert.ThrowsAnyAsync<OperationCanceledException>(
async () => await listener.AcceptTcpClientAsync(secondAcceptCts.Token));
}
[Fact]
public async Task ResetConnectionAsync_AfterConnect_AllowsReconnect()
{
using var listener = new TcpListener(IPAddress.Loopback, 0);
listener.Start();
var endpoint = (IPEndPoint)listener.LocalEndpoint;
await using var transport = new SuiteLinkTcpTransport();
await transport.ConnectAsync(endpoint.Address.ToString(), endpoint.Port);
using var accepted1 = await listener.AcceptTcpClientAsync();
await transport.ResetConnectionAsync();
Assert.False(transport.IsConnected);
await transport.ConnectAsync(endpoint.Address.ToString(), endpoint.Port);
using var accepted2 = await listener.AcceptTcpClientAsync();
Assert.True(transport.IsConnected);
}
[Fact]
public async Task ResetConnectionAsync_LeaveOpenTrue_DoesNotDisposeInjectedStream()
{
var stream = new TrackingStream();
await using var transport = new SuiteLinkTcpTransport(stream, leaveOpen: true);
await transport.ResetConnectionAsync();
Assert.False(stream.WasDisposed);
}
private sealed class PartialReadStream : Stream
{
private readonly MemoryStream _inner;
private readonly int _maxChunkSize;
public PartialReadStream(byte[] bytes, int maxChunkSize)
{
_inner = new MemoryStream(bytes);
_maxChunkSize = maxChunkSize;
}
public override bool CanRead => _inner.CanRead;
public override bool CanSeek => false;
public override bool CanWrite => false;
public override long Length => _inner.Length;
public override long Position
{
get => _inner.Position;
set => throw new NotSupportedException();
}
public override int Read(byte[] buffer, int offset, int count)
{
return _inner.Read(buffer, offset, Math.Min(count, _maxChunkSize));
}
public override ValueTask<int> ReadAsync(Memory<byte> buffer, CancellationToken cancellationToken = default)
{
return _inner.ReadAsync(buffer[..Math.Min(buffer.Length, _maxChunkSize)], cancellationToken);
}
public override void Flush()
{
throw new NotSupportedException();
}
public override long Seek(long offset, SeekOrigin origin)
{
throw new NotSupportedException();
}
public override void SetLength(long value)
{
throw new NotSupportedException();
}
public override void Write(byte[] buffer, int offset, int count)
{
throw new NotSupportedException();
}
}
private sealed class TrackingStream : MemoryStream
{
public bool WasDisposed { get; private set; }
protected override void Dispose(bool disposing)
{
WasDisposed = true;
base.Dispose(disposing);
}
}
}