- ledger.py:students/keys CRUD + 日限额 check_and_count(跨日重置,注入时钟)+ 流水分页;计费两阶段拆分至 billing.py(BillingMixin,评审聚焦): try_hold 锁内读现状->日上限判定->余额原子防线->holding 流水(request_id 幂等)、 settle 按真实值回补(预扣-实际)差额、void 全额退款(上游失败) - auth.py:issue_key(sk-campus- 前缀,明文只返回一次,库存 sha256+前缀)、 authenticate 校验链(形态/注销/停用/日额/rpm,热路径 LRU TTL30s D-P10)、 RateLimiter(令牌桶 rpm + per-key 并发信号量,D-P9 单进程)、 verify_admin(hmac.compare_digest;未配置仅 loopback) - 测试 +17:签发/错key/注销/停用/403/rpm 429/日额 429/并发槽/admin 策略 + 预扣-结算-回补一致/双向差额/余额不足/日上限/幂等/跨日重置/void/并发10路不超扣 - 全量 339 passed
119 lines
3.8 KiB
Python
119 lines
3.8 KiB
Python
"""代理鉴权/限流测试(T-P1)。"""
|
|
import threading
|
|
import time
|
|
|
|
import pytest
|
|
|
|
from gateway.proxy.auth import (
|
|
KEY_PREFIX,
|
|
RateLimiter,
|
|
authenticate,
|
|
issue_key,
|
|
reset_auth_state,
|
|
verify_admin,
|
|
)
|
|
from gateway.proxy.errors import (
|
|
ProxyAuthError,
|
|
QuotaError,
|
|
SuspendedError,
|
|
)
|
|
from gateway.proxy.ledger import Ledger
|
|
|
|
|
|
@pytest.fixture()
|
|
def env(tmp_path):
|
|
reset_auth_state()
|
|
led = Ledger.init_db(tmp_path / "proxy.sqlite3")
|
|
sid = led.upsert_student("张三", "py24", balance_yuan=10.0, daily_cap_yuan=5.0)
|
|
yield {"ledger": led, "sid": sid}
|
|
reset_auth_state()
|
|
|
|
|
|
def test_issue_and_authenticate_roundtrip(env):
|
|
"""签发 -> 鉴权通过 -> 上下文含学生信息。"""
|
|
led = env["ledger"]
|
|
k = issue_key(led, env["sid"], rpm_cap=10, day_cap_req=200)
|
|
assert k["key"].startswith(KEY_PREFIX)
|
|
ctx = authenticate(f"Bearer {k['key']}", led, _AUTH_LIMITS())
|
|
assert ctx["key_id"] == k["key_id"]
|
|
assert ctx["student_id"] == env["sid"]
|
|
assert ctx["student_status"] == "active"
|
|
|
|
|
|
def _AUTH_LIMITS():
|
|
from gateway.proxy.auth import _AUTH_SINGLETON_LIMITS
|
|
return _AUTH_SINGLETON_LIMITS
|
|
|
|
|
|
def test_auth_bad_key_401(env):
|
|
led = env["ledger"]
|
|
with pytest.raises(ProxyAuthError):
|
|
authenticate("Bearer sk-campus-wrong", led, _AUTH_LIMITS())
|
|
with pytest.raises(ProxyAuthError):
|
|
authenticate("wrong-scheme", led, _AUTH_LIMITS())
|
|
with pytest.raises(ProxyAuthError):
|
|
authenticate("", led, _AUTH_LIMITS())
|
|
|
|
|
|
def test_auth_revoked_401(env):
|
|
"""注销后鉴权 401(热缓存主动失效路径)。"""
|
|
led = env["ledger"]
|
|
k = issue_key(led, env["sid"])
|
|
authenticate(f"Bearer {k['key']}", led, _AUTH_LIMITS()) # 热缓存
|
|
assert led.revoke_key(k["key_id"]) is True
|
|
# 注销后缓存内 ctx 仍会查库 revoked 字段?——当前实现缓存整行:
|
|
# revoke 后 find_key 不再命中;这里强制走一次缓存过期模拟最坏 30s 窗口
|
|
reset_auth_state()
|
|
with pytest.raises(ProxyAuthError):
|
|
authenticate(f"Bearer {k['key']}", led, _AUTH_LIMITS())
|
|
|
|
|
|
def test_auth_suspended_403(env):
|
|
led = env["ledger"]
|
|
k = issue_key(led, env["sid"])
|
|
led.set_status(env["sid"], "suspended")
|
|
reset_auth_state()
|
|
with pytest.raises(SuspendedError):
|
|
authenticate(f"Bearer {k['key']}", led, _AUTH_LIMITS())
|
|
|
|
|
|
def test_rpm_token_bucket_429(env):
|
|
"""rpm 令牌桶:突发超过容量被拒(进程内)。"""
|
|
led = env["ledger"]
|
|
k = issue_key(led, env["sid"], rpm_cap=3, day_cap_req=1000)
|
|
results = [authenticate(f"Bearer {k['key']}", led, _AUTH_LIMITS())
|
|
for _ in range(3)]
|
|
assert len(results) == 3
|
|
with pytest.raises(QuotaError):
|
|
authenticate(f"Bearer {k['key']}", led, _AUTH_LIMITS())
|
|
|
|
|
|
def test_day_req_cap_429(env):
|
|
"""日请求上限(持久计数,跨进程语义):第 cap+1 次 429。"""
|
|
led = env["ledger"]
|
|
k = issue_key(led, env["sid"], rpm_cap=1000, day_cap_req=5)
|
|
for _ in range(5):
|
|
authenticate(f"Bearer {k['key']}", led, _AUTH_LIMITS())
|
|
with pytest.raises(QuotaError):
|
|
authenticate(f"Bearer {k['key']}", led, _AUTH_LIMITS())
|
|
|
|
|
|
def test_rate_limiter_concurrent_slots():
|
|
"""并发槽信号量:acquire/release 配平。"""
|
|
rl = RateLimiter(concurrent_per_key=2)
|
|
assert rl.acquire_slot(1) is True
|
|
assert rl.acquire_slot(1) is True
|
|
assert rl.acquire_slot(1) is False
|
|
rl.release_slot(1)
|
|
assert rl.acquire_slot(1) is True
|
|
rl.release_slot(1)
|
|
rl.release_slot(1)
|
|
|
|
|
|
def test_verify_admin_policy():
|
|
"""配置 admin_key -> compare_digest;未配置 -> 仅 loopback。"""
|
|
assert verify_admin("secret", "secret", "1.2.3.4") is True
|
|
assert verify_admin("wrong", "secret", "127.0.0.1") is False
|
|
assert verify_admin("", "", "127.0.0.1") is True
|
|
assert verify_admin("", "", "8.8.8.8") is False
|