210 lines
7.3 KiB
C#
210 lines
7.3 KiB
C#
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<WorkerFrameProtocolException>(
|
|
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<WorkerFrameProtocolException>(
|
|
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<WorkerFrameProtocolException>(
|
|
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<WorkerFrameProtocolException>(
|
|
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<WorkerFrameProtocolException>(
|
|
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<WorkerFrameProtocolException>(
|
|
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<WorkerFrameProtocolException>(
|
|
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<int> ReadAsync(
|
|
Memory<byte> buffer,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
ReadCallCount++;
|
|
int requestedCount = Math.Min(buffer.Length, _chunkSize);
|
|
|
|
return base.ReadAsync(buffer[..requestedCount], cancellationToken);
|
|
}
|
|
}
|
|
}
|