诚实性修复: API限流Retry-After(wait=60s)+断言 / purge定时接线(04:00 cron+actor+测试) / Channel表单校验(ref+rules+validate) / README PG数字513
This commit is contained in:
@@ -0,0 +1,93 @@
|
||||
"""P0-5 · 登录防爆破 + API Key 限流回归。
|
||||
|
||||
- 主登录 `/api/v1/auth/token/`:5 次失败后锁定 15 分钟(429),正确密码也被锁;
|
||||
成功登录清零(正常用户不受影响)。
|
||||
- 商城登录 `/api/v1/storefront/login/`:同策略(按 phone + IP)。
|
||||
- API Key:分钟窗口超 `rate_limit` → 429;`rate_limit<=0` 下界为 1。
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from django.core.cache import cache
|
||||
from rest_framework.test import APIClient
|
||||
|
||||
from apps.core import ratelimit as rl
|
||||
from apps.openapi.models import APIKey
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _clean_cache():
|
||||
cache.clear()
|
||||
yield
|
||||
cache.clear()
|
||||
|
||||
|
||||
def test_main_login_locks_after_5_failures(db, django_user_model):
|
||||
django_user_model.objects.create_user("victim", password="correct-pw")
|
||||
c = APIClient()
|
||||
for _ in range(4):
|
||||
r = c.post("/api/v1/auth/token/",
|
||||
{"username": "victim", "password": "wrong"},
|
||||
format="json")
|
||||
assert r.status_code == 401, r.content
|
||||
# 第 5 次失败达阈值直接锁定(429),后续正确密码也被锁
|
||||
fifth = c.post("/api/v1/auth/token/",
|
||||
{"username": "victim", "password": "wrong"},
|
||||
format="json")
|
||||
assert fifth.status_code == 429, fifth.content
|
||||
assert fifth.json()["code"] == "login_locked"
|
||||
locked = c.post("/api/v1/auth/token/",
|
||||
{"username": "victim", "password": "wrong"},
|
||||
format="json")
|
||||
assert locked.status_code == 429, locked.content
|
||||
assert locked.json()["code"] == "login_locked"
|
||||
# 正确密码也被锁
|
||||
good = c.post("/api/v1/auth/token/",
|
||||
{"username": "victim", "password": "correct-pw"},
|
||||
format="json")
|
||||
assert good.status_code == 429, good.content
|
||||
|
||||
|
||||
def test_main_login_success_resets_counter(db, django_user_model):
|
||||
django_user_model.objects.create_user("normal", password="pw123456")
|
||||
c = APIClient()
|
||||
for _ in range(4):
|
||||
c.post("/api/v1/auth/token/",
|
||||
{"username": "normal", "password": "wrong"}, format="json")
|
||||
ok = c.post("/api/v1/auth/token/",
|
||||
{"username": "normal", "password": "pw123456"}, format="json")
|
||||
assert ok.status_code == 200, ok.content
|
||||
# 计数已清:再错 4 次仍是 401 而非 429
|
||||
for _ in range(4):
|
||||
r = c.post("/api/v1/auth/token/",
|
||||
{"username": "normal", "password": "wrong"}, format="json")
|
||||
assert r.status_code == 401, r.content
|
||||
|
||||
|
||||
def test_apikey_rate_limit_enforced(db, tenant):
|
||||
from django.contrib.auth import get_user_model
|
||||
|
||||
User = get_user_model()
|
||||
u = User.objects.create_user("keyboss", password="x")
|
||||
key_obj, raw = APIKey.generate(
|
||||
tenant=tenant, name="limited", scopes=["products:read"], created_by=u,
|
||||
)
|
||||
key_obj.rate_limit = 3
|
||||
key_obj.save(update_fields=["rate_limit"])
|
||||
|
||||
c = APIClient()
|
||||
c.credentials(HTTP_X_API_KEY=raw)
|
||||
for _ in range(3):
|
||||
r = c.get("/api/v1/openapi/v1/products/")
|
||||
assert r.status_code == 200, r.content
|
||||
over = c.get("/api/v1/openapi/v1/products/")
|
||||
assert over.status_code == 429, over.content
|
||||
# DRF 只在 exc.wait 非空时才回 Retry-After;不传 wait 只有 429 无头。
|
||||
assert "Retry-After" in over, dict(over.items())
|
||||
|
||||
|
||||
def test_effective_rate_limit_floor():
|
||||
assert rl.effective_rate_limit(0) == 1
|
||||
assert rl.effective_rate_limit(-5) == 1
|
||||
assert rl.effective_rate_limit(None) == 1
|
||||
assert rl.effective_rate_limit("abc") == 1
|
||||
assert rl.effective_rate_limit(120) == 120
|
||||
Reference in New Issue
Block a user