v0.5.0: 3 стенда — изоляция состояния, WSGI middleware, OpenAPI server selector, POLYGON_STANDS env

- loader.py: STANDS dict, get_services/get_ops_index per stand, POLYGON_STANDS env
- mock_state.py: get_state(stand_id) — ленивая инициализация
- app.py: StandMiddleware — перезапись PATH_INFO до Flask-роутинга
- Все роуты: LocalProxy(state), LocalProxy(SERVICES/OPS_INDEX) через g.stand_id
- openapi.py: _build_servers() — по одному серверу на стенд
- services/stand{1,2,3}/ — 37 YAML на стенд из STANDS/dev и STANDS/test
- tests/test_api.py: +4 multi-stand теста (изоляция, fail-next, OpenAPI servers)
- Обратная совместимость: /api/v1/svc/... → стенд по умолчанию
This commit is contained in:
2026-08-01 20:10:19 +04:00
parent 11f98738d0
commit 58d54f2a95
122 changed files with 25223 additions and 76 deletions
+61
View File
@@ -340,6 +340,67 @@ def test_delay_negative():
t("POST /_mock/delay/-1 → 400", test_delay_negative)
# ═══════════════════════════════════════
# 8. MULTI-STAND (v0.5.0+)
# ═══════════════════════════════════════
print("\n── 8. Multi-Stand (stand1/stand2/stand3) ──")
STANDS = ["stand1", "stand2", "stand3"]
stand_instances = {}
def test_stands_services():
for sid in STANDS:
r = requests.get(f"{BASE}/{sid}/api/v1/svc/services")
st(r, 200, f"{sid} services")
t("GET /{stand}/api/v1/svc/services → 200 для всех стендов", test_stands_services)
def test_stands_isolated():
"""Каждый стенд — свои инстансы."""
for sid in STANDS:
requests.post(f"{BASE}/{sid}/api/v1/svc/_mock/reset", headers=MH)
# Создаём по 1 инстансу на стенд
for i, sid in enumerate(STANDS):
r = requests.post(f"{BASE}/{sid}/api/v1/svc/instances",
json={"serviceId": 1, "displayName": f"inst-on-{sid}"}, headers=H)
st(r, 201)
stand_instances[sid] = js(r)["instanceUid"]
# Проверяем что на каждом стенде ровно 1 инстанс
for sid in STANDS:
d = js(requests.get(f"{BASE}/{sid}/api/v1/svc/instances"))
assert d["total"] == 1, f"{sid} has {d['total']} instances, expected 1"
t("Изоляция: каждый стенд — свои инстансы", test_stands_isolated)
def test_stands_openapi():
r = requests.get(f"{BASE}/api/v1/svc/openapi.json")
spec = js(r)
servers = [s["url"] for s in spec["servers"]]
assert len(servers) == 3, f"expected 3 servers, got {len(servers)}"
for sid in STANDS:
assert any(f"/{sid}" in u for u in servers), f"{sid} not in servers"
t("GET /openapi.json → 3 servers (по одному на стенд)", test_stands_openapi)
def test_stands_fail_next_isolated():
"""fail-next на одном стенде не влияет на другой."""
requests.post(f"{BASE}/stand1/api/v1/svc/_mock/reset", headers=MH)
requests.post(f"{BASE}/stand2/api/v1/svc/_mock/reset", headers=MH)
# fail-next только на stand1
requests.post(f"{BASE}/stand1/api/v1/svc/_mock/fail-next", headers=MH)
# Создаём операции на обоих стендах
for sid, label in [("stand1", "fail"), ("stand2", "ok")]:
r = requests.post(f"{BASE}/{sid}/api/v1/svc/instances",
json={"serviceId": 1, "displayName": label}, headers=H)
inst_uid = js(r)["instanceUid"]
r = requests.post(f"{BASE}/{sid}/api/v1/svc/instanceOperations",
json={"instanceUid": inst_uid, "operation": "create", "svcOperationId": 18}, headers=H)
op_uid = js(r)["instanceOperationUid"]
d = js(requests.post(f"{BASE}/{sid}/api/v1/svc/instanceOperations/{op_uid}/run", headers=H))
if sid == "stand1":
assert d["ok"] is False, f"stand1 should fail, got ok={d['ok']}"
else:
assert d["ok"] is True, f"stand2 should succeed, got ok={d['ok']}"
t("Изоляция fail-next: stand1 падает, stand2 работает", test_stands_fail_next_isolated)
# ═══════════════════════════════════════
# ИТОГ
# ═══════════════════════════════════════