# ============================================================ # round-46 方向 LL · 矩阵 73 现场对接准备:MES 适配器对接就绪检查 # 覆盖:未配置 fail-closed 提示 / 配置后字段齐(connectivity=unknown)/ # 探测 ok(连通正常)/ 探测 failed 记录 lastError(连接拒绝、HTTP 503)/ # 网关 GET+P probe 接线 200(未配置 configured=false 不 500) # 本地 stub:FastAPI + uvicorn 随机端口(不占 8003/5173) # ============================================================ from __future__ import annotations import contextlib import threading import time import pytest import uvicorn from fastapi import FastAPI, HTTPException from fastapi.testclient import TestClient from server.integrations.mes_http import HttpMesClient, reset_http_mes_client from tests.auth_provider import install_test_auth from tests.golden.test_mes_http import _free_port, mes_http_stub # noqa: F401 fixture 复用 STABLE_KEYS = { "configured", "baseUrl", "tokenPresent", "timeoutSec", "maxRetries", "connectivity", "lastError", "message", } def _client(**kwargs): return HttpMesClient(timeout_sec=2.0, max_retries=1, **kwargs) @contextlib.contextmanager def _run_stub(app: FastAPI, port: int): server = uvicorn.Server(uvicorn.Config( app, host="127.0.0.1", port=port, log_level="warning")) thread = threading.Thread(target=server.run, daemon=True) thread.start() try: deadline = 100 while not server.started and deadline > 0: time.sleep(0.05) deadline -= 1 yield f"http://127.0.0.1:{port}" finally: server.should_exit = True thread.join(timeout=5) # ---------------- 未配置:fail-closed 明确提示 ---------------- def test_readiness_unconfigured_fail_closed_hint(): client = HttpMesClient(base_url="", token="") r = client.readiness() assert set(r) == STABLE_KEYS assert r["configured"] is False assert r["baseUrl"] is None assert r["tokenPresent"] is False assert r["connectivity"] is None assert r["lastError"] is None assert "未配置 MES_HTTP_BASE_URL" in r["message"] # probe=True 且未配置:同样不抛、不联网,保持 fail-closed 语义 r2 = client.readiness(probe=True) assert r2["configured"] is False assert r2["connectivity"] is None # ---------------- 配置后:字段齐,未探测 connectivity=unknown ---------------- def test_readiness_configured_fields_before_probe(mes_http_stub): client = _client(base_url=mes_http_stub["base_url"], token="") r = client.readiness() assert set(r) == STABLE_KEYS assert r["configured"] is True assert r["baseUrl"] == mes_http_stub["base_url"] assert r["tokenPresent"] is False assert r["timeoutSec"] == 2.0 assert r["maxRetries"] == 1 assert r["connectivity"] == "unknown" assert r["lastError"] is None assert "已配置" in r["message"] def test_readiness_token_present_flag(mes_http_stub): client = _client(base_url=mes_http_stub["base_url"], token="secret-token") assert client.readiness()["tokenPresent"] is True # ---------------- 探测 ok ---------------- def test_readiness_probe_ok(mes_http_stub): client = _client(base_url=mes_http_stub["base_url"], token="") r = client.readiness(probe=True) assert set(r) == STABLE_KEYS assert r["configured"] is True assert r["connectivity"] == "ok" assert r["lastError"] is None assert "连通正常" in r["message"] # ---------------- 探测 failed:连接拒绝记录 lastError ---------------- def test_readiness_probe_connection_refused_records_last_error(): client = _client(base_url=f"http://127.0.0.1:{_free_port()}") r = client.readiness(probe=True) assert r["configured"] is True assert r["connectivity"] == "failed" err = r["lastError"] assert err is not None assert err["code"] == "MES_HTTP_CONNECT_FAILED" assert err["statusCode"] == 502 assert err["message"] assert "连通失败" in r["message"] # ---------------- 探测 failed:HTTP 5xx 记录 lastError ---------------- def test_readiness_probe_http_error_records_last_error(): app = FastAPI() @app.get("/health") async def health() -> dict: raise HTTPException(status_code=503, detail="upstream maintenance") with _run_stub(app, _free_port()) as base_url: client = _client(base_url=base_url) r = client.readiness(probe=True) assert r["connectivity"] == "failed" err = r["lastError"] assert err["code"] == "MES_HTTP_UPSTREAM_ERROR" assert err["statusCode"] == 503 # ---------------- 网关接线:GET 只读 + POST probe ---------------- def test_gateway_readiness_and_probe_endpoints(mes_http_stub, monkeypatch): import server.gateway.app as gw install_test_auth(monkeypatch, "tenant-readiness-0000000000000000001") monkeypatch.delenv("MES_HTTP_BASE_URL", raising=False) client = TestClient(gw.create_app()) login = client.post("/api/auth/login", json={"username": "planner", "password": "test"}) assert login.status_code == 200, login.text # 未配置:GET 200 + configured=false 明确提示(不 500) reset_http_mes_client(base_url="", token="") resp = client.get("/api/integrations/mes/readiness") assert resp.status_code == 200, resp.text body = resp.json() assert set(body) == STABLE_KEYS assert body["configured"] is False assert "未配置" in body["message"] # 配置 stub:GET unknown,POST probe ok reset_http_mes_client(base_url=mes_http_stub["base_url"], token="") resp = client.get("/api/integrations/mes/readiness") assert resp.status_code == 200, resp.text assert resp.json()["configured"] is True assert resp.json()["connectivity"] == "unknown" resp = client.post("/api/integrations/mes/readiness/probe") assert resp.status_code == 200, resp.text body = resp.json() assert body["configured"] is True assert body["connectivity"] == "ok" assert body["lastError"] is None assert "连通正常" in body["message"] # 配置指向死端口:probe failed 记录 lastError(仍 200) reset_http_mes_client(base_url=f"http://127.0.0.1:{_free_port()}", token="") resp = client.post("/api/integrations/mes/readiness/probe") assert resp.status_code == 200, resp.text body = resp.json() assert body["connectivity"] == "failed" assert body["lastError"]["code"] == "MES_HTTP_CONNECT_FAILED" assert "连通失败" in body["message"]