PayrollManagementSystem/backend/tests/README.md
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

99 lines
9.2 KiB
Markdown
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.

# 后端测试
> AI摘要:本项目后端的测试用例清单与运行记录。按「基础设施层随实现即时测、业务模块整模块完成时统一测」分层组织。当前 184 个用例全绿(单元 155 / 集成 29)。
## 一、测试约定
### 编写时机(分层对待)
- `core/` 等**基础设施**随实现即时补测 —— 它们是被业务复用多次的地基,越早锁定越省事
- **业务模块**(系统管理、基础数据管理、业务数据管理、绩效工资核算、报表统计)在**整个模块功能完成时**统一编写用例并执行,不在模块做到一半时零散补测
### 目录二分
| 目录 | 依赖 | 用途 |
| --- | --- | --- |
| `tests/unit/` | 无(不碰 Docker、不连库) | 纯函数与元数据验证,秒级跑完,开发中可高频执行 |
| `tests/integration/` | 完整应用实例或 MySQL/Redis 容器 | 接口级、跨产物一致性验证 |
单元测试里的 Redis 依赖通过 `fake_redis` 夹具(fakeredis)替换,因此验证码 GETDEL、限流计数、黑名单 TTL 这些逻辑无需起容器即可覆盖。
### 注释要求
- 测试类用中文 docstring 说明「这组用例守的是什么」——即**如果不测会出什么问题**
- 用例名描述场景与预期(`test_中文按字节而非字符计算边界`)
- 关键断言补注释说明为何该断言重要,尤其是防回归的用例
### 运行方式
```bash
cd backend
uv run pytest tests/unit -q # 纯单元,无需容器,秒级
uv run pytest -q # 全部(集成部分需容器在跑)
uv run pytest tests/integration -q # 仅集成
```
## 二、用例清单
### 基础设施层(core)
| 测试文件 | 守什么 | 用例数 | 关键覆盖点 |
| --- | --- | :---: | --- |
| `unit/test_models.py` | 建表规范红线 | 49 | 表名小写蛇形复数;每列中文注释**且确实输出到 DDL**;`uk_`/`idx_`/`fk_` 命名前缀;ENGINE/CHARSET 显式指定;`ON UPDATE CURRENT_TIMESTAMP` 存在(`server_onupdate` 不会生成该子句,是 SQLAlchemy 的行为陷阱);日志表反向断言不含该子句 |
| `unit/test_logging.py` | 日志脱敏与格式化 | 16 | 密码/令牌/Bearer/手机号/身份证五类脱敏;**脱敏必须后置于 %-格式化**(早期实现在 filter 阶段改写 `record.msg`,抹掉 `%s` 占位符导致 logging 抛 TypeError、整条日志丢失);setup_logging 幂等 |
| `unit/test_config.py` | 配置加载与安全边界 | 14 | 密钥强度 fail-fast;JWT ≤120 分钟、验证码 ≤300 秒的硬约束;密码含 `@ : / #` 时 URL 构造不破坏;渲染 URL 默认隐藏密码 |
| `unit/test_security.py` | 密码哈希 | 11 | BCrypt 格式与定长 60;**72 字节边界**(24 个汉字正好、25 个超限);超长密码校验时返回 False 而非抛异常(防侧信道);哈希串损坏不炸 |
| `unit/test_response.py` | 统一响应契约 | 11 | 字段固定为 `code/message/data`;分页序列化为规范要求的驼峰 `list/total/pageNum/pageSize` |
| `unit/test_exception_handlers.py` | 异常转统一信封 | 5 | 校验错误 422→400 并压成单行中文;HTTP 状态码与 body 的 `code` 必须一致 |
### 系统管理 · 登录鉴权
| 测试文件 | 守什么 | 用例数 | 关键覆盖点 |
| --- | --- | :---: | --- |
| `unit/test_jwt.py` | 令牌有效期语义与版本机制 | 12 | `exp` 基于 **UTC** 而非本地时间(混算会产生 8 小时偏差,表现为「刚登录就过期」或「过期了还能用」);过期/篡改签名/**alg=none 攻击**/畸形字符串一律收敛为 401 而非 500;jti 唯一 |
| `unit/test_captcha.py` | 验证码**一次性语义** | 12 | GETDEL 后二次使用必然失败(防重放);**校验失败也消费**(否则可用一个验证码反复试密码);字符集剔除 `0O1Il`;每次渲染含随机干扰不可比对;TTL 300 秒 |
| `unit/test_login_limiter.py` | 阈值与**滑动窗口** | 9 | 达阈值即锁;每次失败刷新 TTL(否则「4 次失败 + 等 14 分钟 + 1 次失败」可绕过锁定);按用户名隔离,一个账号被攻击不牵连他人;登录成功清除计数 |
| `unit/test_token_blacklist.py` | 黑名单 **TTL 语义** | 7 | TTL 等于令牌剩余有效期(写死固定值会导致「登出 N 分钟后又活了」,不设过期则 Redis 无限增长);已过期令牌不写入;jti 互不串号 |
| `unit/test_auth_schemas.py` | 密码**按字节**校验 | 9 | 24 个汉字 = 72 字节正好、25 个被拒 —— 按字符数校验(如 `max_length=20`)根本拦不住,会让登录接口 500 |
| `integration/test_auth_flow.py` | 整条链路**串联正确性** | 16 | 验证码→登录→持令牌访问→登出失效;验证码重放被拒;失败原因如实落库且**日志表无任何密码字段**;账号不存在与密码错误文案完全相同(防枚举);禁用账号与 `token_version` 提升均使旧令牌立即失效 |
### 应用骨架与交付产物
| 测试文件 | 守什么 | 用例数 | 关键覆盖点 |
| --- | --- | :---: | --- |
| `integration/test_health.py` | 统一信封与全局异常 | 7 | `/api/health` 始终 200 且依赖状态如实上报;404/405 返回统一信封而非框架默认 `detail` 数组;`/api/docs` 可访问 |
| `integration/test_schema_sync.py` | DDL 不漂移 | 6 | `db/migration/V1__*.sql` 与 `db/init/init-dev.sql` 的**表集合与列集合逐表比对**——两份 DDL 手工同步迟早漂移,且漂移后果隐蔽(开发库结构与生产不一致,上线才发现) |
## 三、运行记录
| 日期 | 范围 | 用例数 | 结果 | 备注 |
| --- | --- | :---: | --- | --- |
| 2026-10-09 | 全部 | 120 | ✅ 通过(4.99s) | 阶段 1 骨架 + 阶段 2 数据库完成后首次全量运行 |
| 2026-10-09 | 全部 | 119 | ✅ 通过(5.01s) | ORM 由异步改回同步后回归;净减 1 例(`sync_database_url` 独立配置已合并) |
| 2026-10-09 | 全部 | 184 | ✅ 通过(25.46s) | **登录鉴权模块完成**后统一编写并执行该模块测试套件(新增 65 例)。集成用例需容器在跑 |
## 四、测试发现并修复的缺陷
| 日期 | 缺陷 | 影响 | 处理 |
| --- | --- | --- | --- |
| 2026-10-09 | 日志脱敏在 `logging.Filter` 阶段改写 `record.msg` | 抹掉 `%s` 占位符 → logging 内部 `msg % args` 抛 TypeError → **整条日志丢失**(比不脱敏更危险) | 改为 `SensitiveFormatter` 在格式化**之后**脱敏;补回归用例 |
| 2026-10-09 | `PasswordHash.recommended()` 在 pwdlib 0.3.x 推荐 Argon2 而非 BCrypt | 密码算法与规范不符,且随库版本变化——同一批密码可能混用两种算法 | 改为显式 `PasswordHash((BcryptHasher(),))` 钉死算法 |
| 2026-10-09 | 模型未设置表级 `comment`,`update_time` 用 `server_onupdate` | 违反规范红线;`server_onupdate` 只是 ORM 标记,**不生成 `ON UPDATE CURRENT_TIMESTAMP` 子句**,更新时间永不刷新 | 补表注释;改用 `text("CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP")` |
| 2026-10-09 | `pymysql` 未带 `[rsa]` extra,`cryptography` 缺失 | MySQL 8.0 默认 `caching_sha2_password`,容器重启后首次连接走全量认证时抛 `RuntimeError`,且报错在建连阶段易被误读为「密码错误」 | 依赖改为 `pymysql[rsa]` |
| 2026-10-09 | `setex` 在 redis-py 8 已弃用 | 规范明确**禁用弃用 API**;弃用接口在未来大版本会被移除 | 验证码与黑名单改用 `set(key, value, ex=…)` |
| 2026-10-09 | 测试中 TTL 断言写死等值 | 属**测试自身缺陷**:Redis 的 TTL 以毫秒精度存绝对过期时刻、返回时向下取整,刚写入就查询必然可能少 1 秒——会变成偶发失败的闪烁测试 | 断言改为容忍 1 秒偏差,并注明原因 |
| 2026-10-09 | 验证码沿用 `captcha` 库默认渲染 | 随机彩色字符 + 重噪点,与界面视觉体系冲突;该库最后更新于 2025-03,内部还引用了 Pillow 的旧别名 | 改为自绘(`core/captcha_image.py`):配色与界面统一,难度由逐字旋转、抖动、干扰曲线与噪点保证;仅沿用其内置字体文件 |
| 2026-10-09 | 噪点用 `ellipse` 绘制 | 3×3 包围盒画出的椭圆恰好填充中心与四个边中点,形状就是「+」,形成规则的十字纹理——既显眼又容易被整体滤波抹掉,起不到干扰作用 | 改用单像素点与 2×2 方块 |
| 2026-10-09 | `tests/unit/test_jwt.py` 两处赋值了未使用的 `settings`(ruff F841) | 变量未被引用,属无效代码 | 删除赋值;该文件的 `get_settings` 导入仍被其他用例使用 |
## 五、待补用例
| 模块 | 状态 | 说明 |
| --- | --- | --- |
| 系统管理 · 用户与角色管理 | 待编写 | 需求 2.5.1 的用户增删改查、密码重置、启用/禁用、角色分配 — 该模块功能尚未实现 |
| 系统管理 · 操作日志 | 待编写 | 除登录日志外的操作审计(当前仅登录日志落库) |
| 基础数据管理 | 未开始 | 依赖 T+ 对接方式确认 |
| 业务数据管理 | 未开始 | 依赖需求澄清(回款天数口径等 10 项) |
| 绩效工资核算 | 未开始 | 依赖需求澄清;金额精度需专项用例(`Fraction` 与 1/90 日斜率) |
| 报表统计 | 未开始 | 依赖报表口径确认 |