"""配置模块单元测试。 用 `_env_file=None` 绕过 backend/.env,使断言不受开发者本地配置影响。 """ import pytest from pydantic import ValidationError from app.core.config import Settings VALID_SECRET = "x" * 32 def _settings(**overrides: object) -> Settings: """构造一个不读取 .env 的配置实例。""" params: dict[str, object] = {"_env_file": None, "app_secret_key": VALID_SECRET} params.update(overrides) return Settings(**params) # type: ignore[arg-type] class TestSecretKey: """密钥强度校验。""" def test_缺失密钥时拒绝构造(self) -> None: with pytest.raises(ValidationError): Settings(_env_file=None) # type: ignore[call-arg] @pytest.mark.parametrize("length", [0, 1, 31]) def test_密钥短于32字符时拒绝(self, length: int) -> None: with pytest.raises(ValidationError): _settings(app_secret_key="x" * length) def test_密钥恰好32字符时通过(self) -> None: assert _settings(app_secret_key="x" * 32).app_secret_key == "x" * 32 class TestSecurityBounds: """安全边界:规范要求 JWT ≤120 分钟、验证码 300 秒。""" def test_jwt有效期超过120分钟时拒绝(self) -> None: with pytest.raises(ValidationError): _settings(jwt_expire_minutes=121) def test_jwt有效期为120分钟时通过(self) -> None: assert _settings(jwt_expire_minutes=120).jwt_expire_minutes == 120 def test_验证码有效期超过300秒时拒绝(self) -> None: with pytest.raises(ValidationError): _settings(captcha_expire_seconds=301) def test_验证码有效期为300秒时通过(self) -> None: assert _settings(captcha_expire_seconds=300).captcha_expire_seconds == 300 class TestConnectionUrl: """连接 URL 构造:密码含特殊字符时不可手工拼接。""" def test_数据库URL使用同步驱动(self) -> None: """运行期与迁移共用同步连接串,不存在同步/异步两套配置。""" url = _settings().database_url assert url.drivername == "mysql+pymysql" assert url.database == "payroll" assert url.query.get("charset") == "utf8mb4" def test_密码含特殊字符时被原样保存于URL对象(self) -> None: url = _settings(mysql_password="p@ss:w/ord#1").database_url # URL.create 以结构化方式保存各组件,不做编码 —— 编码发生在驱动建连时, # 这正是手工拼接字符串会出错(如把 : 误认为端口分隔符)而它不会的原因 assert url.password == "p@ss:w/ord#1" assert url.username == "payroll" assert url.port == 3306 def test_渲染URL时默认隐藏密码(self) -> None: """异常或日志中打印 URL 时不得泄露密码。""" url = _settings(mysql_password="p@ss:w/ord#1").database_url assert "p@ss:w/ord#1" not in url.render_as_string() def test_Redis无密码时不拼接认证段(self) -> None: assert _settings(redis_password="").redis_url == "redis://127.0.0.1:6379/0" def test_Redis有密码时拼接认证段(self) -> None: url = _settings(redis_password="secret").redis_url assert url == "redis://:secret@127.0.0.1:6379/0"