170 lines
6.4 KiB
Python
170 lines
6.4 KiB
Python
# ============================================================
|
||
# 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"]
|