"""运行控制以实际心跳呈现状态,并保护并发变更。""" from datetime import datetime, timedelta from threading import Event from unittest.mock import MagicMock from urllib.error import HTTPError import pytest from sqlalchemy import select from sqlalchemy.exc import SQLAlchemyError from app.core.exceptions import ConflictError, ForbiddenError from app.models import OperationLog, TPlusRuntimeControl, User from app.schemas.tplus import TPlusRuntimeUpdate from app.services.tplus import runtime_control, supervisor, task_service from tests.unit.test_tplus_workbench import work_session as work_session def test_启动请求不冒充运行且冲突受保护(work_session): session = work_session session.add(TPlusRuntimeControl(id=1)) session.commit() actor = task_service.TaskActor(session.get(User, 1)) payload = TPlusRuntimeUpdate(desired_state="RUNNING", expected_version=0) status = runtime_control.save(session, payload, actor) assert status["desired_state"] == "RUNNING" assert status["actual_state"] == "OFFLINE" assert status["version"] == 1 assert session.scalar(select(OperationLog).where(OperationLog.action == "修改读取服务状态")) with pytest.raises(ConflictError): runtime_control.save(session, payload, actor) assert supervisor.may_claim(session) session.rollback() runtime_control.save(session, TPlusRuntimeUpdate(desired_state="STOPPED", expected_version=1), actor) assert not supervisor.may_claim(session) def test_过期心跳显示离线且权限拒绝(work_session, monkeypatch): session = work_session now = datetime(2026, 11, 5, 10) session.add(TPlusRuntimeControl(id=1, desired_state="RUNNING", actual_state="RUNNING", worker_phase="RUNNING", heartbeat_at=now - timedelta(seconds=91))) session.commit() assert runtime_control.overview(session, now=now)["actual_state"] == "OFFLINE" def denied(*_args): raise ForbiddenError() monkeypatch.setattr(runtime_control, "require_permission", denied) with pytest.raises(ForbiddenError): runtime_control.save(session, TPlusRuntimeUpdate(desired_state="STOPPED", expected_version=0), task_service.TaskActor(session.get(User, 1))) assert session.get(TPlusRuntimeControl, 1).desired_state == "RUNNING" def test_健康检查拒绝外部地址(): from app.core.config import Settings settings = Settings(_env_file=None, app_secret_key="x" * 32, t_plus_supervisor_health_url="http://example.com/api/health") assert supervisor.health_error(settings) == "后端健康检查地址必须是本机 HTTP 地址" def test_健康检查处理器拒绝外部重定向(): request = MagicMock() response = MagicMock() assert supervisor._LocalOnlyRedirectHandler().redirect_request( request, response, 302, "Found", {}, "http://example.com/api/health" ) is None def test_健康检查重定向到外部地址时拒绝(monkeypatch): from app.core.config import Settings settings = Settings(_env_file=None, app_secret_key="x" * 32) monkeypatch.setattr( supervisor, "_open_health", lambda *_args, **_kwargs: (_ for _ in ()).throw( HTTPError("http://127.0.0.1/api/health", 302, "Found", {}, None) ), ) assert supervisor.health_error(settings) == "系统健康检查或 T+ 连接配置未就绪" @pytest.mark.parametrize("body", [b'[]', b'null', b'{"data":null}', b'{"data":[]}', b'{"data":"bad"}']) def test_健康检查结构异常时返回未就绪(monkeypatch, body): from app.core.config import Settings settings = Settings(_env_file=None, app_secret_key="x" * 32) response = MagicMock() response.__enter__.return_value.read.return_value = body monkeypatch.setattr(supervisor, "_open_health", lambda *_args, **_kwargs: response) assert supervisor.health_error(settings) == "系统或依赖尚未就绪" @pytest.mark.parametrize("connected", [True, False]) def test_健康检查实际探测来源连接且不泄露错误(monkeypatch, connected): from app.core.config import Settings settings = Settings(_env_file=None, app_secret_key="x" * 32, t_plus_enabled=True, t_plus_database="source", t_plus_user="reader", t_plus_odbc_driver="test-driver") response = MagicMock() response.__enter__.return_value.read.return_value = b'{"data":{"app":true,"db":true,"redis":true}}' monkeypatch.setattr(supervisor, "_open_health", lambda *_args, **_kwargs: response) import pyodbc monkeypatch.setattr(pyodbc, "drivers", lambda: ["test-driver"]) engine = MagicMock() if not connected: engine.connect.side_effect = SQLAlchemyError("secret-connection-string") monkeypatch.setattr(supervisor, "create_engine", lambda *_args, **_kwargs: engine) result = supervisor.health_error(settings) assert result is None if connected else result == "T+ 数据库连接不可用,请检查连接配置" if connected: connection = engine.connect.return_value.__enter__.return_value assert connection.connection.driver_connection.timeout == settings.t_plus_query_timeout connection.exec_driver_sql.assert_called_once_with("SELECT 1") engine.dispose.assert_called_once() def test_来源不可用时托管器不排队或领取任务(monkeypatch): connection = MagicMock() connection.dialect.name = "mysql" connection.execute.return_value.scalar.return_value = 1 factory = MagicMock() factory.kw = {"bind": MagicMock(connect=lambda: connection)} session = factory.return_value.__enter__.return_value session.scalar.return_value.desired_state = "RUNNING" stopped = Event() def unavailable(_settings): stopped.set() return "T+ 数据库连接不可用,请检查连接配置" maintenance, worker = MagicMock(), MagicMock() monkeypatch.setattr(supervisor, "health_error", unavailable) monkeypatch.setattr(supervisor, "tick", maintenance) monkeypatch.setattr(supervisor, "run_once", worker) assert supervisor.run(factory, MagicMock(), stop_event=stopped) == 0 maintenance.assert_not_called() worker.assert_not_called() def test_托管检查异常记录类型但不泄露内容(caplog): connection = MagicMock() connection.dialect.name = "mysql" connection.execute.side_effect = [MagicMock(scalar=lambda: 1), RuntimeError("secret-value"), MagicMock(scalar=lambda: 1)] factory = MagicMock() factory.kw = {"bind": MagicMock(connect=lambda: connection)} assert supervisor.run(factory, MagicMock()) == 1 assert "RuntimeError" in caplog.text assert "secret-value" not in caplog.text @pytest.mark.parametrize("release", [None, 0]) def test_托管锁释放失败时使连接失效(release): connection = MagicMock() connection.dialect.name = "mysql" connection.execute.side_effect = [MagicMock(scalar=lambda: 1), MagicMock(scalar=lambda: release)] factory = MagicMock() factory.kw = {"bind": MagicMock(connect=lambda: connection)} stopped = Event() stopped.set() assert supervisor.run(factory, MagicMock(), stop_event=stopped) == 0 connection.invalidate.assert_called_once() connection.close.assert_called_once() def test_停止状态写入失败仍释放托管锁(): connection = MagicMock() connection.dialect.name = "mysql" connection.execute.side_effect = [MagicMock(scalar=lambda: 1), MagicMock(scalar=lambda: 1)] factory = MagicMock() factory.kw = {"bind": MagicMock(connect=lambda: connection)} factory.return_value.__enter__.side_effect = RuntimeError("数据库暂不可用") stopped = Event() stopped.set() assert supervisor.run(factory, MagicMock(), stop_event=stopped) == 0 assert connection.execute.call_count == 2 connection.invalidate.assert_not_called() connection.close.assert_called_once() def test_释放托管锁抛错时使连接失效(): connection = MagicMock() connection.dialect.name = "mysql" connection.execute.side_effect = [MagicMock(scalar=lambda: 1), RuntimeError("连接异常")] factory = MagicMock() factory.kw = {"bind": MagicMock(connect=lambda: connection)} stopped = Event() stopped.set() assert supervisor.run(factory, MagicMock(), stop_event=stopped) == 0 connection.invalidate.assert_called_once() connection.close.assert_called_once()