using System.Net; using System.Threading.RateLimiting; using Microsoft.AspNetCore.Http; using ZB.MOM.WW.MxGateway.Server.Configuration; using ZB.MOM.WW.MxGateway.Server.Dashboard; namespace ZB.MOM.WW.MxGateway.Tests.Gateway.Dashboard; /// /// Tests the POST /auth/login fixed-window per-IP rate limiter. Exercised through the same /// partition factory the production policy registers, so the test pins the real permit limit and /// per-IP partitioning without standing up an HTTP server. /// public sealed class DashboardLoginRateLimitTests { /// A burst beyond the permit limit from one IP has its excess attempts rejected (HTTP 429 in the pipeline). [Fact] public void LoginRateLimiter_BurstBeyondPermitLimit_RejectsExcessFromSameIp() { SecurityOptions security = new() { LoginRateLimitPermitLimit = 3, LoginRateLimitWindowSeconds = 60, }; using PartitionedRateLimiter limiter = DashboardEndpointRouteBuilderExtensions.CreateLoginRateLimiter(security); HttpContext context = ContextWithIp("10.0.0.7"); for (int i = 0; i < 3; i++) { using RateLimitLease lease = limiter.AttemptAcquire(context); Assert.True(lease.IsAcquired); } using RateLimitLease rejected = limiter.AttemptAcquire(context); Assert.False(rejected.IsAcquired); } /// Distinct client IPs are partitioned independently — one IP's burst does not starve another. [Fact] public void LoginRateLimiter_DistinctIps_PartitionedIndependently() { SecurityOptions security = new() { LoginRateLimitPermitLimit = 1, LoginRateLimitWindowSeconds = 60, }; using PartitionedRateLimiter limiter = DashboardEndpointRouteBuilderExtensions.CreateLoginRateLimiter(security); using RateLimitLease first = limiter.AttemptAcquire(ContextWithIp("10.0.0.1")); using RateLimitLease exhaustedForFirst = limiter.AttemptAcquire(ContextWithIp("10.0.0.1")); using RateLimitLease second = limiter.AttemptAcquire(ContextWithIp("10.0.0.2")); Assert.True(first.IsAcquired); Assert.False(exhaustedForFirst.IsAcquired); Assert.True(second.IsAcquired); } private static HttpContext ContextWithIp(string ip) { DefaultHttpContext context = new(); context.Connection.RemoteIpAddress = IPAddress.Parse(ip); return context; } }