签入版本

This commit is contained in:
2026-09-12 18:57:19 +08:00
parent d45b61d808
commit 2857e0f2da
295 changed files with 20713 additions and 13125 deletions
@@ -0,0 +1,106 @@
"""
JWT 滑动会话单元测试
=============================================================
验证:
- create_session:登录后写入 Redis 会话键,TTL = 空闲超时
- touch_session:活跃请求重置会话 TTL(返回 True)
- touch_session:空闲超时后会话键缺失 → 返回 False(需重新登录)
- touch_session:Redis 异常时 fail-closed(返回 False)
- 会话键基于 Token SHA256 指纹,不落完整 Token
测试不依赖真实 Redis,通过 monkeypatch 注入内存假 Redis。
"""
import pytest
from src.core import jwt as jwt_module
from src.core import redis_client as redis_client_module
class FakeRedis:
"""内存版 Redis 桩:仅支持滑动会话用到的 set/expire"""
def __init__(self):
self.store = {}
self.ttls = {}
def set(self, key, value, expire=None):
self.store[key] = value
if expire is not None:
self.ttls[key] = expire
return True
def expire(self, key, seconds):
if key not in self.store:
return False
self.ttls[key] = seconds
return True
class FailingRedis:
"""模拟 Redis 故障:expire 抛异常"""
def set(self, key, value, expire=None):
raise ConnectionError("redis down")
def expire(self, key, seconds):
raise ConnectionError("redis down")
@pytest.fixture
def fake_redis(monkeypatch):
fake = FakeRedis()
monkeypatch.setattr(redis_client_module, "redis", fake)
return fake
def test_session_key_is_fingerprint_based():
"""会话键基于 Token 指纹,不含完整 Token"""
key = jwt_module._session_key("a" * 200)
assert key.startswith("jwt:session:")
assert len(key) > len("jwt:session:") # 含 SHA256 摘要
assert "a" * 200 not in key # 未落完整 Token
def test_create_session_sets_ttl(fake_redis):
token = "some-jwt-token"
assert jwt_module.create_session(token) is True
key = jwt_module._session_key(token)
assert fake_redis.store[key] == "1"
assert fake_redis.ttls[key] == jwt_module.SESSION_IDLE_MINUTES * 60
def test_create_session_custom_idle(fake_redis):
token = "custom-idle-token"
assert jwt_module.create_session(token, idle_minutes=5) is True
key = jwt_module._session_key(token)
assert fake_redis.ttls[key] == 5 * 60
def test_touch_session_resets_ttl(fake_redis):
token = "touch-token"
assert jwt_module.create_session(token, idle_minutes=5) is True
# 活跃请求续期:TTL 重置为完整空闲超时
assert jwt_module.touch_session(token) is True
key = jwt_module._session_key(token)
assert fake_redis.ttls[key] == jwt_module.SESSION_IDLE_MINUTES * 60
def test_touch_session_missing_key_returns_false(fake_redis):
"""空闲超时后会话键已消失 → touch_session 返回 False(需重新登录)"""
assert jwt_module.touch_session("never-logged-in-token") is False
assert fake_redis.store == {}
def test_touch_session_fail_closed_on_redis_error(monkeypatch):
"""Redis 故障时 touch_session 返回 False(fail-closed)"""
monkeypatch.setattr(redis_client_module, "redis", FailingRedis())
assert jwt_module.touch_session("any-token") is False
def test_create_session_fail_closed_on_redis_error(monkeypatch):
"""Redis 故障时 create_session 返回 False"""
monkeypatch.setattr(redis_client_module, "redis", FailingRedis())
assert jwt_module.create_session("any-token") is False
+93
View File
@@ -0,0 +1,93 @@
"""
中间件单元测试
=============================================================
验证:
- 敏感字段递归脱敏(密码/salt/令牌等不会写入操作日志)
- 操作日志标题:@operation_log 声明的元数据被 _build_title 读取;未标注走兜底
"""
from src.middleware import _is_excluded_path, _redact_sensitive, _build_title, operation_log
class TestRedactSensitive:
"""递归脱敏"""
def test_redacts_sensitive_keys(self):
data = {
"username": "admin",
"password": "plaintext123",
"code": "business-code", # 业务字段(role.code 等)不得误打码
"key": "business-key",
"nested": {"salt": "s1", "ok": 1},
"list": [{"token": "jwt"}, "plain"],
}
result = _redact_sensitive(data)
assert result["password"] == "******"
assert result["code"] == "business-code" # 不再误脱敏
assert result["key"] == "business-key"
assert result["nested"]["salt"] == "******"
assert result["nested"]["ok"] == 1
assert result["list"][0]["token"] == "******"
assert result["list"][1] == "plain"
assert result["username"] == "admin"
def test_non_dict_passthrough(self):
assert _redact_sensitive("plain") == "plain"
assert _redact_sensitive(123) == 123
assert _redact_sensitive(None) is None
class TestBuildTitle:
"""@operation_log 元数据解析 + 兜底"""
def test_uses_view_metadata(self, flask_app):
@flask_app.route('/test/add', methods=['POST'])
@operation_log("测试模块", "新增")
def add():
return 'ok'
with flask_app.app_context():
# 裸 Flask app 的 endpoint 即视图函数名
title = _build_title('/test/add', 'add')
assert title == '测试模块-新增'
def test_action_only_uses_module(self, flask_app):
@flask_app.route('/test/list', methods=['GET'])
@operation_log("测试模块")
def lst():
return 'ok'
with flask_app.app_context():
title = _build_title('/test/list', 'lst')
assert title == '测试模块'
def test_unannotated_fallback(self, flask_app):
with flask_app.app_context():
title = _build_title('/api/v1/user/add', 'unannotated.endpoint')
assert title == 'user-add'
class TestExcludedPath:
"""敏感端点尾段匹配:兼容任意版本前缀"""
def test_v1_paths_excluded(self):
assert _is_excluded_path("/api/v1/login")
assert _is_excluded_path("/api/v1/captcha")
assert _is_excluded_path("/api/v1/logout")
assert _is_excluded_path("/api/v1/job/sendSms")
def test_v2_prefix_also_excluded(self):
# 尾段匹配 → 后续 v2/v3 无需追加前缀配置
assert _is_excluded_path("/api/v2/login")
assert _is_excluded_path("/api/v2/job/sendSms")
def test_normal_path_not_excluded(self):
assert not _is_excluded_path("/api/v1/user/add")
assert not _is_excluded_path("/api/v1/job/page")
assert not _is_excluded_path("/api/v1/job/runOnce/1")
def test_root_mounted_paths_excluded(self):
assert _is_excluded_path("/login")
assert _is_excluded_path("/captcha")