PayrollManagementSystem/backend/tests/unit/test_logging.py
liuwenxin 273ebf4657 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>
2026-10-09 18:46:36 +08:00

112 lines
4.0 KiB
Python
Raw Permalink 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.

"""日志脱敏单元测试。
规范第 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