PayrollManagementSystem/backend/tests/integration/test_tplus_sync_console.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

79 lines
4.3 KiB
Python

"""同步控制台的真实鉴权、配置保存、审计与进度契约。仅隔离库执行。"""
from pathlib import Path
from uuid import uuid4
import pytest
from sqlalchemy import select
from app.core.db import get_session_factory
from app.models import OperationLog, TPlusSyncBatch
from app.services.tplus import task_service
pytestmark = pytest.mark.integration
BASE = "/api/v1/system/tplus"
def test_新增配置表迁移可往返且不修改同步批次():
from alembic import command
from alembic.config import Config
from sqlalchemy import inspect, text
from app.core.db import get_engine
engine = get_engine()
assert engine.url.database.startswith("payroll_test_")
with engine.connect() as connection:
before = connection.exec_driver_sql('SELECT COUNT(*) FROM tplus_sync_batches').scalar_one()
config = Config(str(Path(__file__).resolve().parents[2] / "alembic.ini"))
command.downgrade(config, "26e7f8a9b0c1")
assert not inspect(engine).has_table("tplus_automation_configs")
command.upgrade(config, "head")
assert inspect(engine).has_table("tplus_automation_configs")
with engine.connect() as connection:
assert connection.execute(text('SELECT version_num FROM alembic_version')).scalar_one() == '28a9b0c1d2e3'
assert connection.exec_driver_sql('SELECT COUNT(*) FROM tplus_sync_batches').scalar_one() == before
def test_开关保存后刷新生效且拒绝过期版本(client, admin, normal, tplus_tasks, monkeypatch):
settings = task_service.get_settings().model_copy(update={
"t_plus_database": "unit_auto_" + uuid4().hex[:8], "t_plus_automation_enabled": True})
monkeypatch.setattr(task_service, "get_settings", lambda: settings)
initial = client.get(BASE + "/overview", headers=admin["headers"]).json()["data"]["automation"]
assert initial["enabled"] and initial["editable"] and initial["version"] == 0
payload = {"enabled": False, "expected_version": 0}
assert client.put(BASE + "/automation", json=payload).status_code == 401
assert client.put(BASE + "/automation", json=payload, headers=normal["headers"]).status_code == 403
saved = client.put(BASE + "/automation", json=payload, headers=admin["headers"])
assert saved.status_code == 200 and not saved.json()["data"]["enabled"]
assert saved.json()["data"]["version"] == 1
refreshed = client.get(BASE + "/overview", headers=admin["headers"]).json()["data"]["automation"]
assert refreshed["enabled"] is False and refreshed["configuration_origin"] == "SAVED"
assert client.put(BASE + "/automation", json={"enabled": True, "expected_version": 0}, headers=admin["headers"]).status_code == 409
assert client.put(BASE + "/automation", json={"enabled": "false", "expected_version": 1}, headers=admin["headers"]).status_code == 400
with get_session_factory()() as session:
assert session.scalar(select(OperationLog.id).where(
OperationLog.user_id == admin["id"], OperationLog.action == "修改自动采集",
OperationLog.success == 1,
).limit(1))
def test_列表详情概况的进度一致且不透传原始报告(client, admin, tplus_tasks):
number = client.post(BASE + "/batches", json=tplus_tasks(), headers=admin["headers"]).json()["data"]["batch_no"]
with get_session_factory()() as session:
batch = session.scalar(select(TPlusSyncBatch).where(TPlusSyncBatch.batch_no == number))
batch.status, batch.source_record_count, batch.skipped_record_count = "RUNNING", 50, 50
batch.audit_result_json = {"password": "private-string-not-exposed", "progress": {
"phase": "READING", "total_records": 200, "active_entity": "personnel",
"entities": {"personnel": {"total": 200, "processed": 50, "status": "READING"}},
}}
session.commit()
for path in (f"/batches/{number}", "/batches", "/overview"):
response = client.get(BASE + path, headers=admin["headers"])
assert response.status_code == 200
assert "private-string-not-exposed" not in response.text
data = response.json()["data"]
batch = data["current_batch"] if path == "/overview" else next(b for b in data["list"] if b["batch_no"] == number) if path == "/batches" else data
assert batch["progress"]["percentage"] == 25
assert batch["progress"]["processed_records"] == 50