2026-11-02 22:20:55 -05:00
|
|
|
"""同步控制台的真实鉴权、配置保存、审计与进度契约。仅隔离库执行。"""
|
|
|
|
|
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"
|
|
|
|
|
|
|
|
|
|
|
2026-11-05 06:32:20 -05:00
|
|
|
def test_不可逆迁移拒绝回退且不修改同步批次():
|
2026-11-02 22:20:55 -05:00
|
|
|
from alembic import command
|
|
|
|
|
from alembic.config import Config
|
2026-11-05 06:32:20 -05:00
|
|
|
from alembic.script import ScriptDirectory
|
2026-11-02 22:20:55 -05:00
|
|
|
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"))
|
2026-11-05 06:32:20 -05:00
|
|
|
with pytest.raises(RuntimeError, match="不可直接回退"):
|
|
|
|
|
command.downgrade(config, "26e7f8a9b0c1")
|
2026-11-02 22:20:55 -05:00
|
|
|
command.upgrade(config, "head")
|
|
|
|
|
assert inspect(engine).has_table("tplus_automation_configs")
|
|
|
|
|
with engine.connect() as connection:
|
2026-11-05 06:32:20 -05:00
|
|
|
assert connection.execute(text('SELECT version_num FROM alembic_version')).scalar_one() == ScriptDirectory.from_config(config).get_current_head()
|
2026-11-02 22:20:55 -05:00
|
|
|
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
|
|
|
|
|
|