using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; using StellaOps.Gateway.WebService.Middleware; using StellaOps.Router.Common.Models; using Xunit; namespace StellaOps.Gateway.WebService.Tests; public class PayloadTrackerTests { private readonly PayloadLimits _limits = new() { MaxRequestBytesPerCall = 1024, MaxRequestBytesPerConnection = 4096, MaxAggregateInflightBytes = 8192 }; private PayloadTracker CreateTracker() { return new PayloadTracker( Options.Create(_limits), NullLogger.Instance); } [Fact] public void TryReserve_WithinLimits_ReturnsTrue() { var tracker = CreateTracker(); var result = tracker.TryReserve("conn-1", 500); Assert.True(result); Assert.Equal(500, tracker.CurrentInflightBytes); } [Fact] public void TryReserve_ExceedsAggregateLimits_ReturnsFalse() { var tracker = CreateTracker(); // Reserve from multiple connections to approach aggregate limit (8192) // Each connection can have up to 4096 bytes Assert.True(tracker.TryReserve("conn-1", 4000)); Assert.True(tracker.TryReserve("conn-2", 4000)); // Now at 8000 bytes // Another reservation that exceeds aggregate limit (8000 + 500 > 8192) should fail var result = tracker.TryReserve("conn-3", 500); Assert.False(result); Assert.Equal(8000, tracker.CurrentInflightBytes); } [Fact] public void TryReserve_ExceedsPerConnectionLimit_ReturnsFalse() { var tracker = CreateTracker(); // Reserve up to per-connection limit Assert.True(tracker.TryReserve("conn-1", 4000)); // Next reservation on same connection should fail var result = tracker.TryReserve("conn-1", 500); Assert.False(result); } [Fact] public void TryReserve_DifferentConnections_TrackedSeparately() { var tracker = CreateTracker(); Assert.True(tracker.TryReserve("conn-1", 3000)); Assert.True(tracker.TryReserve("conn-2", 3000)); Assert.Equal(3000, tracker.GetConnectionInflightBytes("conn-1")); Assert.Equal(3000, tracker.GetConnectionInflightBytes("conn-2")); Assert.Equal(6000, tracker.CurrentInflightBytes); } [Fact] public void Release_DecreasesInflightBytes() { var tracker = CreateTracker(); tracker.TryReserve("conn-1", 1000); tracker.Release("conn-1", 500); Assert.Equal(500, tracker.CurrentInflightBytes); Assert.Equal(500, tracker.GetConnectionInflightBytes("conn-1")); } [Fact] public void Release_CannotGoNegative() { var tracker = CreateTracker(); tracker.TryReserve("conn-1", 100); tracker.Release("conn-1", 500); // More than reserved Assert.Equal(0, tracker.GetConnectionInflightBytes("conn-1")); } [Fact] public void IsOverloaded_TrueWhenExceedsLimit() { var tracker = CreateTracker(); // Reservation at limit passes (8192 <= 8192 is false for >, so not overloaded at exactly limit) // But we can't exceed the limit. The IsOverloaded check is for current > limit // So at exactly 8192, IsOverloaded should be false (8192 > 8192 is false) // Reserving 8193 would be rejected. So let's test that at limit, IsOverloaded is false tracker.TryReserve("conn-1", 8192); // At exactly the limit, IsOverloaded is false (8192 > 8192 = false) Assert.False(tracker.IsOverloaded); } [Fact] public void IsOverloaded_FalseWhenWithinLimit() { var tracker = CreateTracker(); tracker.TryReserve("conn-1", 4000); Assert.False(tracker.IsOverloaded); } [Fact] public void GetConnectionInflightBytes_ReturnsZeroForUnknownConnection() { var tracker = CreateTracker(); var result = tracker.GetConnectionInflightBytes("unknown"); Assert.Equal(0, result); } } public class ByteCountingStreamTests { [Fact] public async Task ReadAsync_CountsBytesRead() { var data = new byte[] { 1, 2, 3, 4, 5 }; using var inner = new MemoryStream(data); using var stream = new ByteCountingStream(inner, 100); var buffer = new byte[10]; var read = await stream.ReadAsync(buffer); Assert.Equal(5, read); Assert.Equal(5, stream.BytesRead); } [Fact] public async Task ReadAsync_ThrowsWhenLimitExceeded() { var data = new byte[100]; using var inner = new MemoryStream(data); using var stream = new ByteCountingStream(inner, 50); var buffer = new byte[100]; var ex = await Assert.ThrowsAsync( () => stream.ReadAsync(buffer).AsTask()); Assert.Equal(100, ex.BytesRead); Assert.Equal(50, ex.Limit); } [Fact] public async Task ReadAsync_CallsCallbackOnLimitExceeded() { var data = new byte[100]; using var inner = new MemoryStream(data); var callbackCalled = false; using var stream = new ByteCountingStream(inner, 50, () => callbackCalled = true); var buffer = new byte[100]; await Assert.ThrowsAsync( () => stream.ReadAsync(buffer).AsTask()); Assert.True(callbackCalled); } [Fact] public async Task ReadAsync_AccumulatesAcrossMultipleReads() { var data = new byte[100]; using var inner = new MemoryStream(data); using var stream = new ByteCountingStream(inner, 60); var buffer = new byte[30]; // First read - 30 bytes var read1 = await stream.ReadAsync(buffer); Assert.Equal(30, read1); Assert.Equal(30, stream.BytesRead); // Second read - 30 more bytes var read2 = await stream.ReadAsync(buffer); Assert.Equal(30, read2); Assert.Equal(60, stream.BytesRead); // Third read should exceed limit await Assert.ThrowsAsync( () => stream.ReadAsync(buffer).AsTask()); } [Fact] public void Stream_Properties_AreCorrect() { using var inner = new MemoryStream(); using var stream = new ByteCountingStream(inner, 100); Assert.True(stream.CanRead); Assert.False(stream.CanWrite); Assert.False(stream.CanSeek); } [Fact] public void Write_ThrowsNotSupported() { using var inner = new MemoryStream(); using var stream = new ByteCountingStream(inner, 100); Assert.Throws(() => stream.Write(new byte[10], 0, 10)); } [Fact] public void Seek_ThrowsNotSupported() { using var inner = new MemoryStream(); using var stream = new ByteCountingStream(inner, 100); Assert.Throws(() => stream.Seek(0, SeekOrigin.Begin)); } } public class PayloadLimitExceededExceptionTests { [Fact] public void Constructor_SetsProperties() { var ex = new PayloadLimitExceededException(1000, 500); Assert.Equal(1000, ex.BytesRead); Assert.Equal(500, ex.Limit); Assert.Contains("1000", ex.Message); Assert.Contains("500", ex.Message); } }