// Go reference: server/mqtt.go — mqttMaxAckPending, flow control logic. using NATS.Server.Mqtt; using Shouldly; namespace NATS.Server.Mqtt.Tests.Mqtt; public sealed class MqttFlowControllerTests { // 1. TryAcquire succeeds when under limit [Fact] public async Task TryAcquire_succeeds_when_under_limit() { using var fc = new MqttFlowController(defaultMaxAckPending: 1024); var result = await fc.TryAcquireAsync("sub-1"); result.ShouldBeTrue(); } // 2. TryAcquire fails when at limit [Fact] public async Task TryAcquire_fails_when_at_limit() { using var fc = new MqttFlowController(defaultMaxAckPending: 1); var first = await fc.TryAcquireAsync("sub-1"); var second = await fc.TryAcquireAsync("sub-1"); first.ShouldBeTrue(); second.ShouldBeFalse(); } // 3. Release allows next acquire [Fact] public async Task Release_allows_next_acquire() { using var fc = new MqttFlowController(defaultMaxAckPending: 1); var first = await fc.TryAcquireAsync("sub-1"); first.ShouldBeTrue(); // At limit — second should fail var atLimit = await fc.TryAcquireAsync("sub-1"); atLimit.ShouldBeFalse(); fc.Release("sub-1"); // After release a slot is available again var afterRelease = await fc.TryAcquireAsync("sub-1"); afterRelease.ShouldBeTrue(); } // 4. GetPendingCount tracks pending [Fact] public async Task GetPendingCount_tracks_pending() { using var fc = new MqttFlowController(defaultMaxAckPending: 10); await fc.AcquireAsync("sub-1"); await fc.AcquireAsync("sub-1"); await fc.AcquireAsync("sub-1"); fc.GetPendingCount("sub-1").ShouldBe(3); } // 5. GetPendingCount decrements on release [Fact] public async Task GetPendingCount_decrements_on_release() { using var fc = new MqttFlowController(defaultMaxAckPending: 10); await fc.AcquireAsync("sub-1"); await fc.AcquireAsync("sub-1"); await fc.AcquireAsync("sub-1"); fc.Release("sub-1"); fc.GetPendingCount("sub-1").ShouldBe(2); } // 6. GetPendingCount returns zero for unknown subscription [Fact] public void GetPendingCount_zero_for_unknown() { using var fc = new MqttFlowController(); fc.GetPendingCount("does-not-exist").ShouldBe(0); } // 7. RemoveSubscription cleans up [Fact] public async Task RemoveSubscription_cleans_up() { using var fc = new MqttFlowController(defaultMaxAckPending: 10); await fc.AcquireAsync("sub-1"); fc.SubscriptionCount.ShouldBe(1); fc.RemoveSubscription("sub-1"); fc.SubscriptionCount.ShouldBe(0); } // 8. SubscriptionCount tracks independent subscriptions [Fact] public async Task SubscriptionCount_tracks_subscriptions() { using var fc = new MqttFlowController(defaultMaxAckPending: 10); await fc.AcquireAsync("sub-a"); await fc.AcquireAsync("sub-b"); await fc.AcquireAsync("sub-c"); fc.SubscriptionCount.ShouldBe(3); } // 9. DefaultMaxAckPending can be updated via UpdateLimit [Fact] public void DefaultMaxAckPending_can_be_updated() { using var fc = new MqttFlowController(defaultMaxAckPending: 1024); fc.DefaultMaxAckPending.ShouldBe(1024); fc.UpdateLimit(512); fc.DefaultMaxAckPending.ShouldBe(512); } // 10. Dispose cleans up all subscriptions [Fact] public async Task Dispose_cleans_up_all() { var fc = new MqttFlowController(defaultMaxAckPending: 10); await fc.AcquireAsync("sub-x"); await fc.AcquireAsync("sub-y"); await fc.AcquireAsync("sub-z"); fc.SubscriptionCount.ShouldBe(3); fc.Dispose(); fc.SubscriptionCount.ShouldBe(0); } }