using System.Buffers.Binary; using Google.Protobuf; using MxGateway.Contracts; using MxGateway.Contracts.Proto; using MxGateway.Server.Workers; namespace MxGateway.Tests.Gateway.Workers; public sealed class WorkerFrameProtocolTests { private const string SessionId = "session-1"; [Fact] public async Task WriteAndReadAsync_WithValidEnvelope_RoundTripsFrame() { WorkerFrameProtocolOptions options = new(SessionId); await using MemoryStream stream = new(); WorkerEnvelope original = CreateEnvelope(); WorkerFrameWriter writer = new(stream, options); await writer.WriteAsync(original); stream.Position = 0; WorkerFrameReader reader = new(stream, options); WorkerEnvelope parsed = await reader.ReadAsync(); Assert.Equal(original, parsed); } [Fact] public async Task ReadAsync_WithPartialReads_ReassemblesFrame() { WorkerFrameProtocolOptions options = new(SessionId); WorkerEnvelope original = CreateEnvelope(); byte[] frame = CreateFrame(original); await using ChunkedReadStream stream = new(frame, chunkSize: 2); WorkerFrameReader reader = new(stream, options); WorkerEnvelope parsed = await reader.ReadAsync(); Assert.Equal(original, parsed); Assert.True(stream.ReadCallCount > 2); } [Fact] public async Task ReadAsync_WithZeroLengthFrame_ThrowsMalformedLength() { WorkerFrameProtocolOptions options = new(SessionId); await using MemoryStream stream = new(new byte[sizeof(uint)]); WorkerFrameReader reader = new(stream, options); WorkerFrameProtocolException exception = await Assert.ThrowsAsync( async () => await reader.ReadAsync()); Assert.Equal(WorkerFrameProtocolErrorCode.MalformedLength, exception.ErrorCode); } [Fact] public async Task ReadAsync_WithOversizedLength_ThrowsBeforePayloadAllocation() { WorkerFrameProtocolOptions options = new(SessionId, GatewayContractInfo.WorkerProtocolVersion, maxMessageBytes: 16); byte[] lengthPrefix = new byte[sizeof(uint)]; BinaryPrimitives.WriteUInt32LittleEndian(lengthPrefix, 17); await using MemoryStream stream = new(lengthPrefix); WorkerFrameReader reader = new(stream, options); WorkerFrameProtocolException exception = await Assert.ThrowsAsync( async () => await reader.ReadAsync()); Assert.Equal(WorkerFrameProtocolErrorCode.MessageTooLarge, exception.ErrorCode); } [Fact] public async Task ReadAsync_WithWrongProtocolVersion_ThrowsProtocolVersionMismatch() { WorkerFrameProtocolOptions options = new(SessionId); WorkerEnvelope envelope = CreateEnvelope(); envelope.ProtocolVersion++; await using MemoryStream stream = new(CreateFrame(envelope)); WorkerFrameReader reader = new(stream, options); WorkerFrameProtocolException exception = await Assert.ThrowsAsync( async () => await reader.ReadAsync()); Assert.Equal(WorkerFrameProtocolErrorCode.ProtocolVersionMismatch, exception.ErrorCode); } [Fact] public async Task ReadAsync_WithWrongSessionId_ThrowsSessionMismatch() { WorkerFrameProtocolOptions options = new(SessionId); WorkerEnvelope envelope = CreateEnvelope(); envelope.SessionId = "different-session"; await using MemoryStream stream = new(CreateFrame(envelope)); WorkerFrameReader reader = new(stream, options); WorkerFrameProtocolException exception = await Assert.ThrowsAsync( async () => await reader.ReadAsync()); Assert.Equal(WorkerFrameProtocolErrorCode.SessionMismatch, exception.ErrorCode); } [Fact] public async Task ReadAsync_WithMalformedPayload_ThrowsInvalidEnvelope() { WorkerFrameProtocolOptions options = new(SessionId); byte[] frame = CreateFrame([0x80]); await using MemoryStream stream = new(frame); WorkerFrameReader reader = new(stream, options); WorkerFrameProtocolException exception = await Assert.ThrowsAsync( async () => await reader.ReadAsync()); Assert.Equal(WorkerFrameProtocolErrorCode.InvalidEnvelope, exception.ErrorCode); } [Fact] public async Task ReadAsync_WithMissingEnvelopeBody_ThrowsInvalidEnvelope() { WorkerFrameProtocolOptions options = new(SessionId); WorkerEnvelope envelope = CreateEnvelope(); envelope.ClearBody(); await using MemoryStream stream = new(CreateFrame(envelope)); WorkerFrameReader reader = new(stream, options); WorkerFrameProtocolException exception = await Assert.ThrowsAsync( async () => await reader.ReadAsync()); Assert.Equal(WorkerFrameProtocolErrorCode.InvalidEnvelope, exception.ErrorCode); } [Fact] public async Task WriteAsync_WithOversizedEnvelope_ThrowsMessageTooLarge() { WorkerFrameProtocolOptions options = new(SessionId, GatewayContractInfo.WorkerProtocolVersion, maxMessageBytes: 8); await using MemoryStream stream = new(); WorkerFrameWriter writer = new(stream, options); WorkerFrameProtocolException exception = await Assert.ThrowsAsync( async () => await writer.WriteAsync(CreateEnvelope())); Assert.Equal(WorkerFrameProtocolErrorCode.MessageTooLarge, exception.ErrorCode); Assert.Equal(0, stream.Length); } private static WorkerEnvelope CreateEnvelope() { return new WorkerEnvelope { ProtocolVersion = GatewayContractInfo.WorkerProtocolVersion, SessionId = SessionId, Sequence = 1, CorrelationId = "correlation-1", WorkerHello = new WorkerHello { ProtocolVersion = GatewayContractInfo.WorkerProtocolVersion, Nonce = "nonce", WorkerProcessId = 1234, WorkerVersion = "test-worker", }, }; } private static byte[] CreateFrame(IMessage message) { return CreateFrame(message.ToByteArray()); } private static byte[] CreateFrame(byte[] payload) { byte[] frame = new byte[sizeof(uint) + payload.Length]; BinaryPrimitives.WriteUInt32LittleEndian(frame.AsSpan(0, sizeof(uint)), (uint)payload.Length); payload.CopyTo(frame.AsSpan(sizeof(uint))); return frame; } private sealed class ChunkedReadStream : MemoryStream { private readonly int _chunkSize; public ChunkedReadStream( byte[] buffer, int chunkSize) : base(buffer) { _chunkSize = chunkSize; } public int ReadCallCount { get; private set; } public override ValueTask ReadAsync( Memory buffer, CancellationToken cancellationToken = default) { ReadCallCount++; int requestedCount = Math.Min(buffer.Length, _chunkSize); return base.ReadAsync(buffer[..requestedCount], cancellationToken); } } }