按 2026-11-02 需求补齐计划交付后端能力: - 角色授权:权限目录化,接口统一准入校验,未登记默认拒绝; 管理员判断改为按数据库角色重新判定,防止权限码持有者绕过 - 系统监控:单进程采集资源、请求与异常指标,分钟级聚合, 阈值配置化;敏感配置增加 repr 保护,Redis 密码 URL 转义 - 试用期计提:按回款日判定并写入快照,年度结清与调整单联动; 人员模型补试用期、离职日期与工资版本历史字段 - 计提模板:公式骨架受控,页面维护名称与参数绑定 - T+ 采集:自动化配置表与进度查询 内置角色禁停用保护对齐模块注释;核算规则版本提为常量并 补充批量核算失败堆栈日志。 影响范围:后端全部模块、数据库迁移 V22-V28 验证:uv run ruff check 通过;922 个单元测试全部通过; 集成测试因本机 MySQL 数据卷被其他容器锁定暂未运行
79 lines
4.3 KiB
Python
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
|
|
|