210 lines
7.2 KiB
C#
210 lines
7.2 KiB
C#
using SuiteLink.Client.Protocol;
|
|
using SuiteLink.Client.Transport;
|
|
|
|
namespace SuiteLink.Client.Tests;
|
|
|
|
public sealed class SuiteLinkClientConnectionTests
|
|
{
|
|
[Fact]
|
|
public async Task ConnectAsync_SendsHandshakeThenConnect_ButDoesNotReportReadyYet()
|
|
{
|
|
var handshakeAckFrame = new byte[] { 0x06, 0x00, 0x01, 0x00, 0xA1, 0xB2, 0xC3, 0xA5 };
|
|
var transport = new FakeTransport([handshakeAckFrame[..4], handshakeAckFrame[4..]]);
|
|
var client = new SuiteLinkClient(transport);
|
|
var options = CreateOptions();
|
|
|
|
await client.ConnectAsync(options);
|
|
|
|
Assert.False(client.IsConnected);
|
|
Assert.Equal(2, transport.SentBuffers.Count);
|
|
Assert.Equal(
|
|
SuiteLinkHandshakeCodec.EncodeNormalQueryHandshake(
|
|
options.Application,
|
|
options.ClientNode,
|
|
options.UserName),
|
|
transport.SentBuffers[0]);
|
|
Assert.Equal(SuiteLinkConnectCodec.Encode(options), transport.SentBuffers[1]);
|
|
Assert.Equal(1, transport.ConnectCallCount);
|
|
Assert.Equal(options.Host, transport.ConnectedHost);
|
|
Assert.Equal(options.Port, transport.ConnectedPort);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task DisconnectAsync_UsesSingleUseClientSemantics_AndDoesNotDisposeExternalTransport()
|
|
{
|
|
var handshakeAckFrame = new byte[] { 0x06, 0x00, 0x01, 0x00, 0xA1, 0xB2, 0xC3, 0xA5 };
|
|
var transport = new FakeTransport([handshakeAckFrame]);
|
|
var client = new SuiteLinkClient(transport);
|
|
|
|
await client.ConnectAsync(CreateOptions());
|
|
await client.DisconnectAsync();
|
|
|
|
Assert.False(client.IsConnected);
|
|
Assert.Equal(0, transport.DisposeCallCount);
|
|
await Assert.ThrowsAsync<ObjectDisposedException>(() => client.ConnectAsync(CreateOptions()));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task DisposeAsync_WithOwnedTransport_DisposesUnderlyingTransport()
|
|
{
|
|
var handshakeAckFrame = new byte[] { 0x06, 0x00, 0x01, 0x00, 0xA1, 0xB2, 0xC3, 0xA5 };
|
|
var transport = new FakeTransport([handshakeAckFrame]);
|
|
var client = new SuiteLinkClient(transport, ownsTransport: true);
|
|
|
|
await client.ConnectAsync(CreateOptions());
|
|
await client.DisposeAsync();
|
|
|
|
Assert.False(client.IsConnected);
|
|
Assert.Equal(1, transport.DisposeCallCount);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ConnectAsync_MalformedHandshakeAck_ThrowsAndFaultsClient()
|
|
{
|
|
var malformedAck = new byte[] { 0x03, 0x00, 0x02, 0x00, 0xA5 };
|
|
var transport = new FakeTransport([malformedAck]);
|
|
var client = new SuiteLinkClient(transport);
|
|
|
|
await Assert.ThrowsAsync<FormatException>(() => client.ConnectAsync(CreateOptions()));
|
|
|
|
Assert.False(client.IsConnected);
|
|
Assert.Equal(1, transport.ConnectCallCount);
|
|
Assert.Single(transport.SentBuffers); // handshake only
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ConnectAsync_RemoteEofDuringHandshake_ThrowsAndFaultsClient()
|
|
{
|
|
var transport = new FakeTransport(receiveChunks: []);
|
|
var client = new SuiteLinkClient(transport);
|
|
|
|
await Assert.ThrowsAsync<IOException>(() => client.ConnectAsync(CreateOptions()));
|
|
|
|
Assert.False(client.IsConnected);
|
|
Assert.Equal(1, transport.ConnectCallCount);
|
|
Assert.Single(transport.SentBuffers); // handshake only
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ConnectAsync_RepeatedWhilePending_DoesNotSendDuplicateStartupFrames()
|
|
{
|
|
var handshakeAckFrame = new byte[] { 0x06, 0x00, 0x01, 0x00, 0xA1, 0xB2, 0xC3, 0xA5 };
|
|
var transport = new FakeTransport([handshakeAckFrame]);
|
|
var client = new SuiteLinkClient(transport);
|
|
var options = CreateOptions();
|
|
|
|
await client.ConnectAsync(options);
|
|
await client.ConnectAsync(options);
|
|
|
|
Assert.Equal(1, transport.ConnectCallCount);
|
|
Assert.Equal(2, transport.SentBuffers.Count);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ConnectAsync_ConcurrentCalls_AreSerializedAndDoNotDuplicateStartupFrames()
|
|
{
|
|
var handshakeAckFrame = new byte[] { 0x06, 0x00, 0x01, 0x00, 0xA1, 0xB2, 0xC3, 0xA5 };
|
|
var receiveGate = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
|
|
var transport = new FakeTransport([handshakeAckFrame])
|
|
{
|
|
ReceiveGate = receiveGate.Task
|
|
};
|
|
var client = new SuiteLinkClient(transport);
|
|
var options = CreateOptions();
|
|
|
|
var connectTask1 = client.ConnectAsync(options);
|
|
while (transport.SentBuffers.Count == 0)
|
|
{
|
|
await Task.Delay(10);
|
|
}
|
|
|
|
var connectTask2 = client.ConnectAsync(options);
|
|
receiveGate.SetResult();
|
|
|
|
await Task.WhenAll(connectTask1, connectTask2);
|
|
|
|
Assert.Equal(1, transport.ConnectCallCount);
|
|
Assert.Equal(2, transport.SentBuffers.Count);
|
|
}
|
|
|
|
private static SuiteLinkConnectionOptions CreateOptions()
|
|
{
|
|
return new SuiteLinkConnectionOptions(
|
|
host: "127.0.0.1",
|
|
application: "App",
|
|
topic: "Topic",
|
|
clientName: "Client",
|
|
clientNode: "Node",
|
|
userName: "User",
|
|
serverNode: "Server",
|
|
timezone: "UTC",
|
|
port: 5413);
|
|
}
|
|
|
|
private sealed class FakeTransport : ISuiteLinkTransport
|
|
{
|
|
private readonly object _syncRoot = new();
|
|
private readonly Queue<byte[]> _receiveChunks;
|
|
private bool _disposed;
|
|
|
|
public FakeTransport(IEnumerable<byte[]> receiveChunks)
|
|
{
|
|
_receiveChunks = new Queue<byte[]>(receiveChunks);
|
|
}
|
|
|
|
public Task? ReceiveGate { get; init; }
|
|
public string ConnectedHost { get; private set; } = string.Empty;
|
|
public int ConnectedPort { get; private set; }
|
|
public int ConnectCallCount { get; private set; }
|
|
public int DisposeCallCount { get; private set; }
|
|
public bool IsConnected => ConnectCallCount > 0 && !_disposed;
|
|
public List<byte[]> SentBuffers { get; } = [];
|
|
|
|
public ValueTask ConnectAsync(string host, int port, CancellationToken cancellationToken = default)
|
|
{
|
|
ConnectCallCount++;
|
|
ConnectedHost = host;
|
|
ConnectedPort = port;
|
|
return ValueTask.CompletedTask;
|
|
}
|
|
|
|
public ValueTask SendAsync(ReadOnlyMemory<byte> buffer, CancellationToken cancellationToken = default)
|
|
{
|
|
lock (_syncRoot)
|
|
{
|
|
SentBuffers.Add(buffer.ToArray());
|
|
}
|
|
return ValueTask.CompletedTask;
|
|
}
|
|
|
|
public async ValueTask<int> ReceiveAsync(Memory<byte> buffer, CancellationToken cancellationToken = default)
|
|
{
|
|
if (ReceiveGate is not null)
|
|
{
|
|
await ReceiveGate.WaitAsync(cancellationToken).ConfigureAwait(false);
|
|
}
|
|
|
|
byte[]? next;
|
|
lock (_syncRoot)
|
|
{
|
|
if (_receiveChunks.Count == 0)
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
next = _receiveChunks.Dequeue();
|
|
}
|
|
|
|
next.CopyTo(buffer);
|
|
return next.Length;
|
|
}
|
|
|
|
public ValueTask DisposeAsync()
|
|
{
|
|
_disposed = true;
|
|
DisposeCallCount++;
|
|
return ValueTask.CompletedTask;
|
|
}
|
|
}
|
|
}
|