PayrollManagementSystem/backend/tests/unit/test_config.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

84 lines
3.2 KiB
Python
Raw 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.

"""配置模块单元测试。
用 `_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"