节点在线、应用在线、配置在线使用令牌查询
大石头 authored at 2021-12-16 19:49:30
4.04 KiB
Stardust
using System;
using System.Net;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Stardust.Server;
using Xunit;

namespace ServerTest.Services;

/// <summary>限流中间件单元测试(安全扫描 V-8)。使用独立来源IP避免共享计数串扰</summary>
public class RateLimitMiddlewareTests
{
    private static Int32 _seed = 0;

    private static String NewIP()
    {
        _seed++;
        return $"10.{(Int32)(_seed / 250)}.{_seed % 250}.{new Random().Next(2, 254)}";
    }

    private static (RateLimitMiddleware mw, DefaultHttpContext ctx) Build(String paths, Int32 times, String path, String ip)
    {
        RequestDelegate next = _ => Task.CompletedTask;
        var ctx = new DefaultHttpContext();
        ctx.Connection.RemoteIpAddress = IPAddress.Parse(ip);
        ctx.Request.Path = path;
        return (new RateLimitMiddleware(next, paths, times), ctx);
    }

    [Fact(DisplayName = "同一IP同一路径超过阈值被429拒绝")]
    public async Task OverThreshold_Rejected()
    {
        var ip = NewIP();
        var (mw, ctx) = Build("/App/Login,/Node/Login", 3, "/App/Login", ip);

        for (var i = 0; i < 3; i++)
        {
            await mw.Invoke(ctx);
            Assert.Equal(200, ctx.Response.StatusCode);
        }

        await mw.Invoke(ctx);
        Assert.Equal(429, ctx.Response.StatusCode);
    }

    [Fact(DisplayName = "非限流路径不受限制")]
    public async Task UnlistedPath_Allowed()
    {
        var ip = NewIP();
        var (mw, ctx) = Build("/App/Login,/Node/Login", 3, "/api", ip);

        for (var i = 0; i < 10; i++)
        {
            await mw.Invoke(ctx);
            Assert.Equal(200, ctx.Response.StatusCode);
        }
    }

    [Fact(DisplayName = "不同来源IP独立计数")]
    public async Task DifferentIP_Independent()
    {
        // 第一个IP已触发429,第二个IP仍应放行
        var ip1 = NewIP();
        var (mw1, ctx1) = Build("/App/Login", 2, "/App/Login", ip1);
        for (var i = 0; i < 3; i++) await mw1.Invoke(ctx1);
        Assert.Equal(429, ctx1.Response.StatusCode);

        var ip2 = NewIP();
        var (mw2, ctx2) = Build("/App/Login", 2, "/App/Login", ip2);
        // 第二个IP打满自身阈值(2次)仍应全部放行,证明未继承第一个IP的计数
        for (var i = 0; i < 2; i++)
        {
            await mw2.Invoke(ctx2);
            Assert.Equal(200, ctx2.Response.StatusCode);
        }
    }

    [Fact(DisplayName = "路径前缀匹配不区分大小写")]
    public async Task Path_CaseInsensitive()
    {
        var ip = NewIP();
        var (mw, ctx) = Build("/App/Login", 2, "/app/login", ip);

        await mw.Invoke(ctx);
        Assert.Equal(200, ctx.Response.StatusCode);
        await mw.Invoke(ctx);
        Assert.Equal(200, ctx.Response.StatusCode);
        await mw.Invoke(ctx);
        Assert.Equal(429, ctx.Response.StatusCode);
    }

    [Fact(DisplayName = "优先取X-Forwarded-For真实客户端IP")]
    public async Task XForwardedFor_Used()
    {
        // 反代场景:直连地址相同,但 XFF 不同 → 独立计数
        var mwPaths = "/App/Login";

        var ctx1 = new DefaultHttpContext();
        ctx1.Connection.RemoteIpAddress = IPAddress.Parse("192.168.1.1");
        ctx1.Request.Headers["X-Forwarded-For"] = "8.8.8.8";
        ctx1.Request.Path = "/App/Login";
        var mw1 = new RateLimitMiddleware(_ => Task.CompletedTask, mwPaths, 2);

        var ctx2 = new DefaultHttpContext();
        ctx2.Connection.RemoteIpAddress = IPAddress.Parse("192.168.1.1");
        ctx2.Request.Headers["X-Forwarded-For"] = "8.8.4.4";
        ctx2.Request.Path = "/App/Login";
        var mw2 = new RateLimitMiddleware(_ => Task.CompletedTask, mwPaths, 2);

        for (var i = 0; i < 3; i++) await mw1.Invoke(ctx1);
        Assert.Equal(429, ctx1.Response.StatusCode);

        // 不同XFF的真实客户端IP独立计数,打满自身阈值(2次)仍放行
        for (var i = 0; i < 2; i++)
        {
            await mw2.Invoke(ctx2);
            Assert.Equal(200, ctx2.Response.StatusCode);
        }
    }
}