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