From 11f98738d0dfd3c7f88f0f10f3bdb11e1a1af701 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CNaeel=E2=80=9D?= Date: Sat, 1 Aug 2026 19:56:20 +0400 Subject: [PATCH] =?UTF-8?q?tests:=20test=5Fapi.py=20=E2=80=94=2035=20smoke?= =?UTF-8?q?+edge=20=D1=82=D0=B5=D1=81=D1=82=D0=BE=D0=B2=20(health,=20servi?= =?UTF-8?q?ces,=20instances,=20operations,=20fail-next,=20auth,=20mock)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/test_api.py | 358 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 358 insertions(+) create mode 100644 tests/test_api.py diff --git a/tests/test_api.py b/tests/test_api.py new file mode 100644 index 0000000..2ae6020 --- /dev/null +++ b/tests/test_api.py @@ -0,0 +1,358 @@ +#!/usr/bin/env python3 +""" +test_api.py — smoke + edge-case тесты Polygon API. + +Запуск: python3 tests/test_api.py +Требует: pip install requests +""" + +import os +import sys +import json + +try: + import requests +except ImportError: + print("❌ Нужен requests: pip install requests") + sys.exit(1) + +BASE = os.getenv("POLYGON_ENDPOINT", "https://polygon.pythonk8s.dev.nubes.ru") +AUTH = os.getenv("MOCK_AUTH_TOKEN", "test-token-123") +H = {"Content-Type": "application/json"} +MH = {**H, "X-Mock-Auth": AUTH} + +passed = 0 +failed = 0 +errors = [] + + +def t(name, fn): + global passed, failed + try: + fn() + passed += 1 + print(f" ✅ {name}") + except AssertionError as e: + failed += 1 + msg = f" ❌ {name}: {e}" + print(msg) + errors.append(msg) + except Exception as e: + failed += 1 + msg = f" 💥 {name}: {type(e).__name__}: {e}" + print(msg) + errors.append(msg) + + +def st(r, expected, note=""): + extra = f" ({note})" if note else "" + assert r.status_code == expected, f"expected {expected}, got {r.status_code}: {r.text[:200]}{extra}" + + +def js(r): + try: + return r.json() + except Exception: + raise AssertionError(f"not JSON: {r.text[:200]}") + + +# ═══════════════════════════════════════ +# Setup +# ═══════════════════════════════════════ +print("=" * 60) +print(f"Polygon API Tests — {BASE}") +print("=" * 60) + +r = requests.post(f"{BASE}/api/v1/svc/_mock/reset", headers=MH) +st(r, 200) +print() + +# Состояние между тестами +svc_list = {} +inst_uid = None +op_uid = None +svc_op_id = None +fail_inst_uid = None +fail_op_uid = None + + +# ═══════════════════════════════════════ +# 1. HEALTH +# ═══════════════════════════════════════ +print("── 1. Health ──") + +def test_health(): + st(requests.get(f"{BASE}/health"), 200) +t("GET /health → 200 OK", test_health) + + +# ═══════════════════════════════════════ +# 2. SERVICES +# ═══════════════════════════════════════ +print("\n── 2. Services ──") + +def test_svc_list(): + global svc_list + d = js(requests.get(f"{BASE}/api/v1/svc/services")) + svc_list = d + assert "results" in d, "no results key" + assert len(d["results"]) >= 37, f"only {len(d['results'])} services" +t("GET /services → 200, >=37 сервисов", test_svc_list) + +def test_svc_fields(): + for s in svc_list["results"]: + for k in ("svcId", "svc", "svcShort"): + assert k in s, f"missing {k} in {s}" +t("GET /services → каждый имеет svcId, svc, svcShort", test_svc_fields) + +def test_svc_detail(): + d = js(requests.get(f"{BASE}/api/v1/svc/services/1")) + assert "svc" in d + assert "operations" in d["svc"] + assert len(d["svc"]["operations"]) > 0 +t("GET /services/1 → 200 + operations", test_svc_detail) + +def test_svc_404(): + st(requests.get(f"{BASE}/api/v1/svc/services/99999"), 404) +t("GET /services/99999 → 404", test_svc_404) + + +# ═══════════════════════════════════════ +# 3. INSTANCES +# ═══════════════════════════════════════ +print("\n── 3. Instances ──") + +def test_inst_empty(): + d = js(requests.get(f"{BASE}/api/v1/svc/instances")) + assert d["total"] == 0 +t("GET /instances (после reset) → total=0", test_inst_empty) + +def test_inst_no_service_id(): + st(requests.post(f"{BASE}/api/v1/svc/instances", json={}, headers=H), 400) +t("POST /instances (без serviceId) → 400", test_inst_no_service_id) + +def test_inst_bad_service(): + st(requests.post(f"{BASE}/api/v1/svc/instances", json={"serviceId": 99999}, headers=H), 404) +t("POST /instances (serviceId=99999) → 404", test_inst_bad_service) + +def test_inst_create(): + global inst_uid + r = requests.post(f"{BASE}/api/v1/svc/instances", + json={"serviceId": 1, "displayName": "test-bolvanka"}, headers=H) + st(r, 201) + assert "Location" in r.headers + d = js(r) + assert "instanceUid" in d + inst_uid = d["instanceUid"] +t("POST /instances → 201 + Location + instanceUid", test_inst_create) + +def test_inst_list_one(): + d = js(requests.get(f"{BASE}/api/v1/svc/instances")) + assert d["total"] == 1 +t("GET /instances → total=1", test_inst_list_one) + +def test_inst_pagesize_clamp(): + d = js(requests.get(f"{BASE}/api/v1/svc/instances?pageSize=500")) + assert d["pageSize"] == 200 +t("GET /instances?pageSize=500 → pageSize=200 (clamped)", test_inst_pagesize_clamp) + +def test_inst_negative_page(): + d = js(requests.get(f"{BASE}/api/v1/svc/instances?page=-1")) + assert d["page"] >= 1 +t("GET /instances?page=-1 → не падает", test_inst_negative_page) + +def test_inst_detail(): + d = js(requests.get(f"{BASE}/api/v1/svc/instances/{inst_uid}")) + inst = d["instance"] + assert inst["status"] == "creating" + assert inst["displayName"] == "test-bolvanka" +t("GET /instances/{uid} → status=creating, имя верное", test_inst_detail) + +def test_inst_fields(): + d = js(requests.get(f"{BASE}/api/v1/svc/instances/{inst_uid}?fields=dtCreate,status")) + assert "instance" in d +t("GET /instances/{uid}?fields=... → 200", test_inst_fields) + +def test_inst_404(): + st(requests.get(f"{BASE}/api/v1/svc/instances/00000000-0000-0000-0000-000000000000"), 404) +t("GET /instances/nonexistent → 404", test_inst_404) + + +# ═══════════════════════════════════════ +# 4. OPERATIONS +# ═══════════════════════════════════════ +print("\n── 4. Operations ──") + +def test_op_default(): + global svc_op_id + d = js(requests.get(f"{BASE}/api/v1/svc/instanceOperations/default/18")) + assert "svcOperation" in d + assert "cfsParams" in d["svcOperation"] + assert len(d["svcOperation"]["cfsParams"]) > 0 + svc_op_id = 18 +t("GET /instanceOperations/default/18 → 200 + cfsParams", test_op_default) + +def test_op_create(): + global op_uid + r = requests.post(f"{BASE}/api/v1/svc/instanceOperations", + json={"instanceUid": inst_uid, "operation": "create", + "svcOperationId": svc_op_id}, headers=H) + st(r, 201) + assert "Location" in r.headers + d = js(r) + assert "instanceOperationUid" in d + op_uid = d["instanceOperationUid"] +t("POST /instanceOperations → 201 + Location", test_op_create) + +def test_op_null_fields(): + d = js(requests.get(f"{BASE}/api/v1/svc/instanceOperations/{op_uid}")) + op = d["instanceOperation"] + assert op["dtStart"] is None, f"dtStart={op['dtStart']}" + assert op["dtFinish"] is None, f"dtFinish={op['dtFinish']}" + assert op["isSuccessful"] is None, f"isSuccessful={op['isSuccessful']}" +t("GET /op/{uid} → dtStart=null, dtFinish=null, isSuccessful=null", test_op_null_fields) + +def test_op_set_param(): + st(requests.post(f"{BASE}/api/v1/svc/instanceOperationCfsParams", + json={"instanceOperationUid": op_uid, + "svcOperationCfsParamId": 242, + "paramValue": "test-realm"}, headers=H), 200) +t("POST /instanceOperationCfsParams → 200", test_op_set_param) + +def test_op_validate(): + st(requests.get(f"{BASE}/api/v1/svc/instanceOperations/{op_uid}/validate-cfs"), 200) +t("GET /validate-cfs → 200", test_op_validate) + +def test_op_run(): + d = js(requests.post(f"{BASE}/api/v1/svc/instanceOperations/{op_uid}/run", headers=H)) + assert d["ok"] is True +t("POST /run → ok=true", test_op_run) + +def test_op_double_run(): + st(requests.post(f"{BASE}/api/v1/svc/instanceOperations/{op_uid}/run", headers=H), 409) +t("POST /run (повторно) → 409", test_op_double_run) + +def test_op_after_run(): + d = js(requests.get(f"{BASE}/api/v1/svc/instanceOperations/{op_uid}")) + op = d["instanceOperation"] + assert op["dtFinish"] is not None + assert op["isSuccessful"] is True +t("GET /op/{uid} (после run) → dtFinish!=null, isSuccessful=true", test_op_after_run) + + +# ═══════════════════════════════════════ +# 5. FAIL-NEXT +# ═══════════════════════════════════════ +print("\n── 5. Fail-next ──") + +def test_fail_set(): + d = js(requests.post(f"{BASE}/api/v1/svc/_mock/fail-next", headers=MH)) + assert d["fail_next"] is True +t("POST /_mock/fail-next → 200, fail_next=true", test_fail_set) + +def test_fail_inst(): + global fail_inst_uid + r = requests.post(f"{BASE}/api/v1/svc/instances", + json={"serviceId": 1, "displayName": "fail-test"}, headers=H) + st(r, 201) + fail_inst_uid = js(r)["instanceUid"] +t("POST /instances (fail-next) → 201", test_fail_inst) + +def test_fail_op(): + global fail_op_uid + r = requests.post(f"{BASE}/api/v1/svc/instanceOperations", + json={"instanceUid": fail_inst_uid, "operation": "create", + "svcOperationId": 18}, headers=H) + st(r, 201) + fail_op_uid = js(r)["instanceOperationUid"] +t("POST /instanceOperations (fail-next) → 201", test_fail_op) + +def test_fail_run(): + d = js(requests.post(f"{BASE}/api/v1/svc/instanceOperations/{fail_op_uid}/run", headers=H)) + assert d["ok"] is False + assert "error" in d + assert "mock failure" in d["error"] +t("POST /run (fail-next) → ok=false, error='mock failure'", test_fail_run) + + +# ═══════════════════════════════════════ +# 6. AUTH +# ═══════════════════════════════════════ +print("\n── 6. Auth (_mock/*) ──") + +# Детектируем, включена ли auth на сервере +_auth_enabled = requests.post(f"{BASE}/api/v1/svc/_mock/reset", headers=H).status_code == 403 + +if _auth_enabled: + def test_auth_no_token(): + st(requests.post(f"{BASE}/api/v1/svc/_mock/reset", headers=H), 403) + t("POST /_mock/reset (без токена) → 403", test_auth_no_token) + + def test_auth_bad_token(): + st(requests.post(f"{BASE}/api/v1/svc/_mock/reset", + headers={**H, "X-Mock-Auth": "wrong-token"}), 403) + t("POST /_mock/reset (неверный токен) → 403", test_auth_bad_token) +else: + print(" ⚠️ Auth отключена (MOCK_AUTH_TOKEN не задан) — тесты 403 пропущены") + +def test_auth_good_token(): + st(requests.post(f"{BASE}/api/v1/svc/_mock/reset", headers=MH), 200) +t("POST /_mock/reset (верный токен) → 200", test_auth_good_token) + +def test_no_auth_services(): + st(requests.get(f"{BASE}/api/v1/svc/services"), 200) +t("GET /services (без токена) → 200", test_no_auth_services) + +def test_no_auth_create(): + r = requests.post(f"{BASE}/api/v1/svc/instances", + json={"serviceId": 1, "displayName": "no-auth"}, headers=H) + st(r, 201) +t("POST /instances (без токена) → 201", test_no_auth_create) + + +# ═══════════════════════════════════════ +# 7. MOCK STATE +# ═══════════════════════════════════════ +print("\n── 7. Mock (state/services/delay) ──") + +def test_state(): + d = js(requests.get(f"{BASE}/api/v1/svc/_mock/state", headers=MH)) + assert "instances" in d + assert "operations" in d +t("GET /_mock/state → instances + operations", test_state) + +def test_mock_services(): + d = js(requests.get(f"{BASE}/api/v1/svc/_mock/services", headers=MH)) + assert d["count"] >= 37 +t("GET /_mock/services → count >= 37", test_mock_services) + +def test_delay_ok(): + d = js(requests.post(f"{BASE}/api/v1/svc/_mock/delay/0.1", headers=MH)) + assert d["delay"] == 0.1 +t("POST /_mock/delay/0.1 → delay=0.1", test_delay_ok) + +def test_delay_too_high(): + st(requests.post(f"{BASE}/api/v1/svc/_mock/delay/100", headers=MH), 400) +t("POST /_mock/delay/100 → 400", test_delay_too_high) + +def test_delay_negative(): + st(requests.post(f"{BASE}/api/v1/svc/_mock/delay/-1", headers=MH), 400) +t("POST /_mock/delay/-1 → 400", test_delay_negative) + + +# ═══════════════════════════════════════ +# ИТОГ +# ═══════════════════════════════════════ +print() +print("=" * 60) +total = passed + failed +print(f" PASS: {passed}/{total}") +if failed: + print(f" FAIL: {failed}/{total}") + for e in errors: + print(f" {e}") +else: + print(" 🎉 Все тесты пройдены!") +print("=" * 60) + +sys.exit(0 if failed == 0 else 1)