using System.Net; using System.Text.Json.Nodes; namespace Monitor.Backend.Tests; /// 登录限流按来源地址计,会影响同一地址上的其他登录,所以用独立的后端实例。 public sealed class LoginRateLimitTests : IClassFixture { private readonly StrictLoginFixture _backend; public LoginRateLimitTests(StrictLoginFixture backend) { _backend = backend; } [Fact] public async Task LoginAttempts_AreRateLimited_WithRetryAfter_AndASuccessfulLoginIsAlsoCounted() { using HttpClient client = _backend.NewClient(); HttpResponseMessage? limited = null; int attempts = 0; for (; attempts < 15 && limited is null; attempts++) { HttpResponseMessage response = await client.PostAsync("/api/admin/auth/login", BackendFixture.Json(new JsonObject { ["username"] = "admin", ["password"] = "wrong" })); if (response.StatusCode == HttpStatusCode.TooManyRequests) { limited = response; } else { Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode); response.Dispose(); } } Assert.NotNull(limited); using (limited) { // 前一次登录(创建管理员令牌)也占了一次配额:第 10 次之内的都是 401 Assert.InRange(attempts, 9, 10); Assert.Equal("too_many_requests", JsonNode.Parse(await limited.Content.ReadAsStringAsync())!["code"]!.GetValue()); Assert.True(limited.Headers.RetryAfter!.Delta!.Value.TotalSeconds is >= 1 and <= 60); } // 被限流的来源连正确的密码也不行:限流发生在校验密码之前,不能被用来猜密码 using HttpResponseMessage correct = await client.PostAsync("/api/admin/auth/login", BackendFixture.Json(new JsonObject { ["username"] = BackendFixture.AdminUser, ["password"] = BackendFixture.AdminPassword })); Assert.Equal(HttpStatusCode.TooManyRequests, correct.StatusCode); } }