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;
}
}