212 lines
7.0 KiB
C#
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);
|
|
}
|
|
}
|
|
}
|