using System.IO; using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Shared; using ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Shared.Contracts; namespace ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Shared.Tests; [Trait("Category", "Unit")] public sealed class FramingTests { [Fact] public async Task FrameWriter_round_trips_single_frame_through_FrameReader() { var buffer = new MemoryStream(); using (var writer = new FrameWriter(buffer, leaveOpen: true)) { await writer.WriteAsync(FocasMessageKind.Hello, new Hello { PeerName = "proxy", SharedSecret = "s3cr3t" }, TestContext.Current.CancellationToken); } buffer.Position = 0; using var reader = new FrameReader(buffer, leaveOpen: true); var frame = await reader.ReadFrameAsync(TestContext.Current.CancellationToken); frame.ShouldNotBeNull(); frame!.Value.Kind.ShouldBe(FocasMessageKind.Hello); var hello = FrameReader.Deserialize(frame.Value.Body); hello.PeerName.ShouldBe("proxy"); hello.SharedSecret.ShouldBe("s3cr3t"); } [Fact] public async Task FrameReader_returns_null_on_clean_EOF_at_frame_boundary() { using var empty = new MemoryStream(); using var reader = new FrameReader(empty, leaveOpen: true); var frame = await reader.ReadFrameAsync(TestContext.Current.CancellationToken); frame.ShouldBeNull(); } [Fact] public async Task FrameReader_throws_on_oversized_length_prefix() { var hostile = new byte[] { 0x7F, 0xFF, 0xFF, 0xFF, 0x01 }; // length > 16 MiB using var stream = new MemoryStream(hostile); using var reader = new FrameReader(stream, leaveOpen: true); await Should.ThrowAsync(async () => await reader.ReadFrameAsync(TestContext.Current.CancellationToken)); } [Fact] public async Task FrameReader_throws_on_mid_frame_eof() { var buffer = new MemoryStream(); using (var writer = new FrameWriter(buffer, leaveOpen: true)) { await writer.WriteAsync(FocasMessageKind.Hello, new Hello { PeerName = "x" }, TestContext.Current.CancellationToken); } // Truncate so body is incomplete. var truncated = buffer.ToArray()[..(buffer.ToArray().Length - 2)]; using var partial = new MemoryStream(truncated); using var reader = new FrameReader(partial, leaveOpen: true); await Should.ThrowAsync(async () => await reader.ReadFrameAsync(TestContext.Current.CancellationToken)); } [Fact] public async Task FrameWriter_serializes_concurrent_writes() { var buffer = new MemoryStream(); using var writer = new FrameWriter(buffer, leaveOpen: true); var tasks = Enumerable.Range(0, 20).Select(i => writer.WriteAsync( FocasMessageKind.Heartbeat, new Heartbeat { MonotonicTicks = i }, TestContext.Current.CancellationToken)).ToArray(); await Task.WhenAll(tasks); buffer.Position = 0; using var reader = new FrameReader(buffer, leaveOpen: true); var seen = new List(); while (await reader.ReadFrameAsync(TestContext.Current.CancellationToken) is { } frame) { frame.Kind.ShouldBe(FocasMessageKind.Heartbeat); seen.Add(FrameReader.Deserialize(frame.Body).MonotonicTicks); } seen.Count.ShouldBe(20); seen.OrderBy(x => x).ShouldBe(Enumerable.Range(0, 20).Select(x => (long)x)); } [Fact] public void MessageKind_values_are_stable() { // Guardrail — if someone reorders/renumbers, the wire format breaks for deployed peers. ((byte)FocasMessageKind.Hello).ShouldBe((byte)0x01); ((byte)FocasMessageKind.Heartbeat).ShouldBe((byte)0x03); ((byte)FocasMessageKind.OpenSessionRequest).ShouldBe((byte)0x10); ((byte)FocasMessageKind.ReadRequest).ShouldBe((byte)0x30); ((byte)FocasMessageKind.WriteRequest).ShouldBe((byte)0x32); ((byte)FocasMessageKind.PmcBitWriteRequest).ShouldBe((byte)0x34); ((byte)FocasMessageKind.SubscribeRequest).ShouldBe((byte)0x40); ((byte)FocasMessageKind.OnDataChangeNotification).ShouldBe((byte)0x43); ((byte)FocasMessageKind.ProbeRequest).ShouldBe((byte)0x70); ((byte)FocasMessageKind.ErrorResponse).ShouldBe((byte)0xFE); } }