chore: 初始化项目结构与登录鉴权模块
后端 FastAPI 六层结构(api/services/repositories/models/schemas/core), 统一响应信封、全局异常、日志脱敏;6 张表经 Alembic 迁移建库,SQL 留存 db/migration;登录鉴权含图形验证码(Redis GETDEL 防重放)、JWT、登录限流、 登出黑名单(TTL=令牌剩余有效期)、登录日志。 前端 Vue3 + TS + Element Plus 骨架,含登录页、路由守卫、请求封装与 Vite 代理。 工程配置:uv 锁定 Python 3.12、ruff 检查与格式化、editorconfig、gitattributes。 测试 184 项(单元 155 / 集成 29)全绿,浏览器端到端 8 项通过。 Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
commit
273ebf4657
101 files changed
+8725
No files matched your search
@@ -0,0 +1,111 @@
|
||||
"""日志脱敏单元测试。
|
||||
|
||||
规范第 8 节禁止日志输出密码、手机号、身份证、Token。
|
||||
脱敏是最容易在重构中悄悄失效的一环,故用参数化用例覆盖各条规则。
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
import pytest
|
||||
|
||||
from app.core.logging import SensitiveFormatter, scrub
|
||||
|
||||
|
||||
class TestScrub:
|
||||
"""文本脱敏规则。"""
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("raw", "leaked"),
|
||||
[
|
||||
('{"password": "Admin@123"}', "Admin@123"),
|
||||
("password=Secret123", "Secret123"),
|
||||
("pwd: hunter2xyz", "hunter2xyz"),
|
||||
('{"app_secret_key": "abcdef123456"}', "abcdef123456"),
|
||||
("token=eyJhbGciOiJIUzI1NiJ9", "eyJhbGciOiJIUzI1NiJ9"),
|
||||
],
|
||||
)
|
||||
def test_凭据值被脱敏(self, raw: str, leaked: str) -> None:
|
||||
assert leaked not in scrub(raw)
|
||||
|
||||
def test_Bearer令牌被脱敏(self) -> None:
|
||||
result = scrub("Authorization: Bearer eyJhbGciOi.J9.abc-_123")
|
||||
assert "eyJhbGciOi.J9.abc-_123" not in result
|
||||
assert "Bearer ***" in result
|
||||
|
||||
def test_手机号保留前三后四(self) -> None:
|
||||
assert scrub("联系电话 13812345678") == "联系电话 138****5678"
|
||||
|
||||
def test_身份证保留前六后四(self) -> None:
|
||||
assert scrub("证件号 110101199003071234") == "证件号 110101********1234"
|
||||
|
||||
def test_普通文本不被误伤(self) -> None:
|
||||
text = "用户 admin 登录成功,耗时 120ms"
|
||||
assert scrub(text) == text
|
||||
|
||||
|
||||
def _format(msg: str, args: object = None) -> str:
|
||||
"""用 SensitiveFormatter 渲染一条日志,返回最终文本。"""
|
||||
record = logging.LogRecord(
|
||||
name="t",
|
||||
level=logging.INFO,
|
||||
pathname="",
|
||||
lineno=0,
|
||||
msg=msg,
|
||||
args=args,
|
||||
exc_info=None,
|
||||
)
|
||||
return SensitiveFormatter("%(message)s").format(record)
|
||||
|
||||
|
||||
class TestSensitiveFormatter:
|
||||
"""脱敏必须在 %-格式化之后进行。"""
|
||||
|
||||
def test_脱敏位置参数中的敏感值(self) -> None:
|
||||
# 回归用例:早期实现在 filter 阶段改写 record.msg,把 %s 占位符一并抹掉,
|
||||
# 导致 logging 内部 msg % args 抛 TypeError,整条日志丢失
|
||||
result = _format("登录失败 password=%s", ("Secret123",))
|
||||
assert "Secret123" not in result
|
||||
assert "登录失败" in result
|
||||
|
||||
def test_脱敏字典参数中的敏感值(self) -> None:
|
||||
# LogRecord 要求 args 为元组;字典参数须包成单元素元组,
|
||||
# 这正是 logger.info(msg, {...}) 的内部形态
|
||||
result = _format("token=%(token)s", ({"token": "abc123xyz"},))
|
||||
assert "abc123xyz" not in result
|
||||
|
||||
def test_脱敏消息模板内联的敏感值(self) -> None:
|
||||
"""敏感值直接写在消息模板里(无占位符)时同样要脱敏。"""
|
||||
result = _format('请求体 {"password": "Admin@123"}')
|
||||
assert "Admin@123" not in result
|
||||
|
||||
def test_普通日志不被改动(self) -> None:
|
||||
assert (
|
||||
_format("用户 admin 登录成功,耗时 %d ms", (120,)) == "用户 admin 登录成功,耗时 120 ms"
|
||||
)
|
||||
|
||||
def test_无参数日志正常通过(self) -> None:
|
||||
assert _format("服务已启动") == "服务已启动"
|
||||
|
||||
|
||||
class TestSetupLogging:
|
||||
"""setup_logging 必须挂上脱敏格式化器,否则规则形同虚设。"""
|
||||
|
||||
def test_所有handler均使用脱敏格式化器(self) -> None:
|
||||
import logging as _logging
|
||||
|
||||
from app.core.logging import setup_logging
|
||||
|
||||
setup_logging(level="INFO", log_to_file=False)
|
||||
handlers = _logging.getLogger().handlers
|
||||
assert handlers, "根日志器应至少有一个 handler"
|
||||
assert all(isinstance(h.formatter, SensitiveFormatter) for h in handlers)
|
||||
|
||||
def test_重复调用不会累积handler(self) -> None:
|
||||
import logging as _logging
|
||||
|
||||
from app.core.logging import setup_logging
|
||||
|
||||
setup_logging(log_to_file=False)
|
||||
first = len(_logging.getLogger().handlers)
|
||||
setup_logging(log_to_file=False)
|
||||
assert len(_logging.getLogger().handlers) == first
|
||||
Reference in new issue
Block a user