Files
DjangoAdmin_FastAPI_AntdVue/tests/unit/middleware/test_rate_limit.py
T
2026-09-12 18:15:20 +08:00

106 lines
4.0 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""middleware/rate_limit.py 请求限流中间件单元测试。"""
from contextlib import asynccontextmanager
import fakeredis.aioredis
from fastapi import FastAPI
from fastapi.testclient import TestClient
import middleware.rate_limit as rate_limit_module
from middleware.rate_limit import rate_limit_middleware
def _make_app():
@asynccontextmanager
async def lifespan(app):
# 在应用运行的事件循环内创建 FakeRedis,避免跨循环绑定问题
app.state.redis = fakeredis.aioredis.FakeRedis(decode_responses=True)
yield
await app.state.redis.aclose()
app = FastAPI(lifespan=lifespan)
@app.get("/")
async def home():
return {"ok": True}
app.middleware("http")(rate_limit_middleware)
return app
def test_rate_limit_disabled_passes(monkeypatch):
monkeypatch.setattr(rate_limit_module, "RATE_LIMIT_ENABLED", False)
with TestClient(_make_app()) as client:
for _ in range(5):
r = client.get("/")
assert r.status_code == 200
assert r.json()["ok"] is True
async def _fake_sliding_window(redis, current_key, prev_key, window_seconds, window_index, now):
"""fakeredis 不支持 Lua eval,用等价实现打桩滑动窗口计数/加权语义。"""
cur = await redis.incr(current_key)
if cur == 1:
await redis.expire(current_key, window_seconds)
prev = int(await redis.get(prev_key) or 0)
elapsed = now - window_index * window_seconds
f = elapsed / window_seconds
f = max(0.0, min(1.0, f))
return prev * (1 - f) + cur
def test_rate_limit_exceeds_returns_business_error(monkeypatch):
monkeypatch.setattr(rate_limit_module, "RATE_LIMIT_ENABLED", True)
monkeypatch.setattr(rate_limit_module, "RATE_LIMIT_LIMIT", 3)
monkeypatch.setattr(rate_limit_module, "RATE_LIMIT_WINDOW_SECONDS", 60)
monkeypatch.setattr(rate_limit_module, "sliding_window_incr", _fake_sliding_window)
with TestClient(_make_app()) as client:
for _ in range(3):
r = client.get("/")
assert r.status_code == 200
assert r.json()["ok"] is True
r = client.get("/")
assert r.status_code == 200
assert r.json()["code"] == 1
assert "过于频繁" in r.json()["msg"]
def test_rate_limit_window_accepts_after_reset(monkeypatch):
monkeypatch.setattr(rate_limit_module, "RATE_LIMIT_ENABLED", True)
monkeypatch.setattr(rate_limit_module, "RATE_LIMIT_LIMIT", 2)
monkeypatch.setattr(rate_limit_module, "RATE_LIMIT_WINDOW_SECONDS", 60)
monkeypatch.setattr(rate_limit_module, "sliding_window_incr", _fake_sliding_window)
with TestClient(_make_app()) as client:
for _ in range(2):
r = client.get("/")
assert r.json()["ok"] is True
r = client.get("/")
assert r.json()["code"] == 1
# 同一窗口内持续限流,不会因后续请求解除
r = client.get("/")
assert r.json()["code"] == 1
def test_rate_limit_sliding_window_prevents_boundary_burst(monkeypatch):
"""窗口边界处请求数按时间加权,不发生固定窗口的 2× 突发。"""
monkeypatch.setattr(rate_limit_module, "RATE_LIMIT_ENABLED", True)
monkeypatch.setattr(rate_limit_module, "RATE_LIMIT_LIMIT", 3)
monkeypatch.setattr(rate_limit_module, "RATE_LIMIT_WINDOW_SECONDS", 100)
monkeypatch.setattr(rate_limit_module, "sliding_window_incr", _fake_sliding_window)
clock = {"t": 50.0}
monkeypatch.setattr(rate_limit_module.time, "time", lambda: clock["t"])
with TestClient(_make_app()) as client:
# 窗口0(t=50):2 个请求,配额充足,放行
for _ in range(2):
assert client.get("/").json()["ok"] is True
# 进入窗口1(t=150,f=0.5):上一窗口 2 个请求加权计入
clock["t"] = 150.0
# effective: 2*0.5+1=2 放行;2*0.5+2=3 放行;2*0.5+3=4 > 3 拒绝
for _ in range(2):
assert client.get("/").json()["ok"] is True
r = client.get("/")
assert r.json()["code"] == 1