feat(api): лимиты частоты запросов (AGENT.md 8.6)
- httpx.RateLimiter: token bucket в памяти с уборкой неактивных ключей и подменяемым источником времени; - login/register — 5 запросов в минуту на IP, весь API — 120 в минуту на пользователя (по хэшу токена сессии) или на IP для анонимных запросов; - превышение отдаёт 429 с Retry-After и retry_after_ms в конверте ошибки; - тесты: наполнение и пополнение ведра, независимость ключей, 429 на ручке входа.
This commit is contained in:
@@ -629,3 +629,22 @@ func hasAction[T any](entries []T, action string) bool {
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func TestAuthRateLimitReturns429(t *testing.T) {
|
||||
srv, _ := newTestServer(t)
|
||||
var last *httptest.ResponseRecorder
|
||||
// Лимит входа — 5 запросов в минуту на IP (AGENT.md 8.6).
|
||||
for range 6 {
|
||||
last = doJSON(t, srv, http.MethodPost, "/api/v1/auth/login",
|
||||
`{"email":"nobody@example.com","password":"wrong-password"}`)
|
||||
}
|
||||
if last.Code != http.StatusTooManyRequests {
|
||||
t.Fatalf("sixth login attempt = %d, want 429", last.Code)
|
||||
}
|
||||
if code := errorCodeOf(t, last); code != "rate_limited" {
|
||||
t.Fatalf("error code = %q, want rate_limited", code)
|
||||
}
|
||||
if last.Header().Get("Retry-After") == "" {
|
||||
t.Error("Retry-After header is missing")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user