PayrollManagementSystem/backend/tests/unit/test_monitoring.py
liuwenxin 1461a8d113 feat(后端): 添加授权体系、系统监控与试用期计提
按 2026-11-02 需求补齐计划交付后端能力:
- 角色授权:权限目录化,接口统一准入校验,未登记默认拒绝;
  管理员判断改为按数据库角色重新判定,防止权限码持有者绕过
- 系统监控:单进程采集资源、请求与异常指标,分钟级聚合,
  阈值配置化;敏感配置增加 repr 保护,Redis 密码 URL 转义
- 试用期计提:按回款日判定并写入快照,年度结清与调整单联动;
  人员模型补试用期、离职日期与工资版本历史字段
- 计提模板:公式骨架受控,页面维护名称与参数绑定
- T+ 采集:自动化配置表与进度查询

内置角色禁停用保护对齐模块注释;核算规则版本提为常量并
补充批量核算失败堆栈日志。

影响范围:后端全部模块、数据库迁移 V22-V28
验证:uv run ruff check 通过;922 个单元测试全部通过;
集成测试因本机 MySQL 数据卷被其他容器锁定暂未运行
2026-11-03 11:20:55 +08:00

72 lines
2.6 KiB
Python

"""验证遥测口径、有界内存和请求关联,防止监控干扰业务及泄漏原始地址。"""
from datetime import datetime, timedelta
from fastapi import FastAPI
from fastapi.testclient import TestClient
from app.core.monitoring import REQUEST_ID, MonitorMiddleware, Recorder, merge_metrics
def test_合并直方图计算P95且四百不计系统错误():
recorder = Recorder()
at = datetime(2026, 11, 2, 10)
for status, elapsed in [(200, 10), (400, 100), (403, 200), (500, 750)]:
recorder.record("GET", "/items/{id}", status, elapsed, "r", at=at)
result = merge_metrics(recorder.metrics()[at].values())
assert (result["count"], result["errors_4xx"], result["errors_5xx"]) == (4, 2, 1)
assert result["error_rate"] == 25
assert result["p95_ms"] == 750
assert merge_metrics([])["p95_ms"] is None
def test_中间件生成编号使用模板排除自身且不读取正文():
app, recorder = FastAPI(), Recorder()
app.add_middleware(MonitorMiddleware, recorder=recorder)
@app.get("/items/{id}")
def item(id: str):
return {"request_id": REQUEST_ID.get()}
@app.get("/api/v1/system/monitor/overview")
def monitor():
return {}
client = TestClient(app)
response = client.get("/items/private-id?token=never-record")
assert response.headers["x-request-id"] == response.json()["request_id"]
client.get("/api/v1/system/monitor/overview")
client.get("/private-route")
values = next(iter(recorder.metrics().values()))
assert set(values) == {("GET", "/items/{id}"), ("GET", "/__unmatched__")}
assert "private" not in str(recorder.metrics())
assert REQUEST_ID.get() == "-"
def test_内存分钟路由和事件队列均有界():
recorder = Recorder()
at = datetime(2026, 11, 2, 10)
for i in range(2100):
recorder.record("GET", f"/route/{i}", 500, 1, str(i), at=at)
assert len(recorder.events) == 2000 and recorder.dropped_events == 100
assert len(recorder.metrics()[at]) == 201
for i in range(11):
recorder.record("GET", "/ok", 200, 1, "r", at=at + timedelta(minutes=i))
assert len(recorder.metrics()) == 10
def test_遥测内部失败不改变业务响应(monkeypatch):
app, recorder = FastAPI(), Recorder()
app.add_middleware(MonitorMiddleware, recorder=recorder)
@app.get("/ok")
def ok():
return {"ok": True}
def broken(*args, **kwargs):
raise RuntimeError("synthetic-recorder-failure")
monkeypatch.setattr(recorder, "record", broken)
response = TestClient(app).get("/ok")
assert response.status_code == 200 and response.json() == {"ok": True}