签入版本

This commit is contained in:
2026-09-12 22:24:53 +08:00
commit 05cb22302c
863 changed files with 118186 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
"""core 层单元测试。"""
+37
View File
@@ -0,0 +1,37 @@
"""
业务异常单元测试
=============================================================
验证 BizError 及子类的 msg/code 约定:
业务层 raise BizError → 全局异常处理器转 R.failed。
"""
from src.core.exceptions import BizError, NotFoundError, PermissionDeniedError
class TestBizError:
def test_default_values(self):
"""默认 msg='操作失败',code=1"""
e = BizError()
assert e.msg == "操作失败"
assert e.code == 1
def test_custom_msg_and_code(self):
e = BizError("自定义错误", code=403)
assert e.msg == "自定义错误"
assert e.code == 403
def test_is_exception(self):
assert issubclass(BizError, Exception)
class TestSubclasses:
def test_not_found(self):
e = NotFoundError()
assert e.msg == "资源不存在"
assert e.code == 1
def test_permission_denied(self):
e = PermissionDeniedError()
assert e.msg == "权限不足"
assert e.code == 403
@@ -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")
+62
View File
@@ -0,0 +1,62 @@
"""
R 统一响应格式单元测试
验证 `R.ok()` / `R.failed()` 返回的 JSON 响应结构符合前后端约定:
{code, data, msg, ok}(code=0 成功,非 0 失败)。
说明:`R.ok()` 内部使用 `jsonify`,须在 Flask 应用上下文内调用
(fixture:flask_app,见 tests/conftest.py)。
"""
from src.core import response as R
class TestOk:
"""成功响应"""
def test_ok_default_format(self, flask_app):
with flask_app.app_context():
resp = R.ok()
body = resp.get_json()
assert body["code"] == 0
assert body["ok"] is True
assert body["data"] is None
assert body["msg"]
def test_ok_with_data(self, flask_app):
with flask_app.app_context():
resp = R.ok(data={"id": 1, "name": "张三"})
body = resp.get_json()
assert body["code"] == 0
assert body["data"] == {"id": 1, "name": "张三"}
def test_ok_with_kwargs(self, flask_app):
with flask_app.app_context():
resp = R.ok(data=[], total=100)
body = resp.get_json()
assert body["code"] == 0
assert body["total"] == 100
class TestFailed:
"""失败响应"""
def test_failed_default_format(self, flask_app):
with flask_app.app_context():
resp = R.failed()
body = resp.get_json()
assert body["code"] != 0
assert body["ok"] is False
assert body["data"] is None
def test_failed_custom_code(self, flask_app):
with flask_app.app_context():
resp = R.failed(msg="未授权访问", code=401)
body = resp.get_json()
assert body["code"] == 401
assert body["msg"] == "未授权访问"