Files
suitelinkclient/tests/SuiteLink.Client.Tests/SuiteLinkClientWriteTests.cs
2026-03-16 16:46:32 -04:00

127 lines
4.1 KiB
C#

using SuiteLink.Client.Protocol;
using SuiteLink.Client.Transport;
namespace SuiteLink.Client.Tests;
public sealed class SuiteLinkClientWriteTests
{
[Fact]
public async Task WriteAsync_SendsPokeFrame_ForSubscribedTag()
{
var transport = new FakeTransport();
transport.EnqueueReceive(BuildHandshakeAckFrame());
transport.EnqueueReceive(BuildAdviseAckFrame(1));
var client = new SuiteLinkClient(transport);
await client.ConnectAsync(CreateOptions());
_ = await client.SubscribeAsync("Pump001.Run", _ => { });
await client.WriteAsync("Pump001.Run", SuiteLinkValue.FromBoolean(true));
Assert.Equal(4, transport.SentBuffers.Count);
var pokeFrame = SuiteLinkFrameReader.ParseFrame(transport.SentBuffers[3]);
Assert.Equal(SuiteLinkWriteCodec.PokeMessageType, pokeFrame.MessageType);
Assert.Equal(1u, SuiteLinkEncoding.ReadUInt32LittleEndian(pokeFrame.Payload.Span[..4]));
Assert.Equal((byte)SuiteLinkWireValueType.Binary, pokeFrame.Payload.Span[4]);
Assert.Equal((byte)1, pokeFrame.Payload.Span[5]);
}
[Fact]
public async Task WriteAsync_UnknownTag_ThrowsInvalidOperationException()
{
var transport = new FakeTransport();
transport.EnqueueReceive(BuildHandshakeAckFrame());
var client = new SuiteLinkClient(transport);
await client.ConnectAsync(CreateOptions());
var ex = await Assert.ThrowsAsync<InvalidOperationException>(
() => client.WriteAsync("Pump001.Unknown", SuiteLinkValue.FromInt32(42)));
Assert.Contains("not subscribed", ex.Message, StringComparison.OrdinalIgnoreCase);
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 static byte[] BuildHandshakeAckFrame()
{
return [0x06, 0x00, 0x01, 0x00, 0xA1, 0xB2, 0xC3, 0xA5];
}
private static byte[] BuildAdviseAckFrame(uint tagId)
{
Span<byte> payload = stackalloc byte[5];
SuiteLinkEncoding.WriteUInt32LittleEndian(payload[..4], tagId);
payload[4] = 0x00;
return SuiteLinkFrameWriter.WriteFrame(SuiteLinkSubscriptionCodec.AdviseAckMessageType, payload);
}
private sealed class FakeTransport : ISuiteLinkTransport
{
private readonly Queue<byte[]> _receiveChunks = [];
private readonly object _syncRoot = new();
public bool IsConnected { get; private set; }
public List<byte[]> SentBuffers { get; } = [];
public void EnqueueReceive(byte[] bytes)
{
lock (_syncRoot)
{
_receiveChunks.Enqueue(bytes);
}
}
public ValueTask ConnectAsync(string host, int port, CancellationToken cancellationToken = default)
{
IsConnected = true;
return ValueTask.CompletedTask;
}
public ValueTask SendAsync(ReadOnlyMemory<byte> buffer, CancellationToken cancellationToken = default)
{
lock (_syncRoot)
{
SentBuffers.Add(buffer.ToArray());
}
return ValueTask.CompletedTask;
}
public ValueTask<int> ReceiveAsync(Memory<byte> buffer, CancellationToken cancellationToken = default)
{
lock (_syncRoot)
{
if (_receiveChunks.Count == 0)
{
return new ValueTask<int>(0);
}
var next = _receiveChunks.Dequeue();
next.CopyTo(buffer);
return new ValueTask<int>(next.Length);
}
}
public ValueTask DisposeAsync()
{
IsConnected = false;
return ValueTask.CompletedTask;
}
}
}