test: 15 интеграционных тестов polygon + bump v1.2.25
This commit is contained in:
@@ -1,7 +1,10 @@
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
|
||||
import pytest
|
||||
import requests
|
||||
|
||||
|
||||
# Make `site/` importable as top-level modules: app, routes, db, operations, ...
|
||||
@@ -9,11 +12,60 @@ SITE_DIR = Path(__file__).resolve().parents[1] / "site"
|
||||
if str(SITE_DIR) not in sys.path:
|
||||
sys.path.insert(0, str(SITE_DIR))
|
||||
|
||||
# Путь к polygon: соседняя репа в корне autotest
|
||||
POLYGON_DIR = Path(__file__).resolve().parents[2] / "polygon" / "site"
|
||||
POLYGON_URL = "http://localhost:5000"
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def app_client():
|
||||
"""Flask test client для app-autotest."""
|
||||
from app import app
|
||||
|
||||
app.config.update(TESTING=True)
|
||||
with app.test_client() as client:
|
||||
yield client
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def polygon_server():
|
||||
"""Запустить polygon как subprocess на весь pytest-session.
|
||||
|
||||
Используется для интеграционных тестов app-autotest ↔ polygon.
|
||||
После всех тестов процесс убивается.
|
||||
"""
|
||||
if not POLYGON_DIR.is_dir():
|
||||
pytest.skip("polygon repo not found")
|
||||
|
||||
proc = subprocess.Popen(
|
||||
["python3", "app.py"],
|
||||
cwd=str(POLYGON_DIR),
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL,
|
||||
)
|
||||
|
||||
# Ждём готовности (poll /health, timeout 10s)
|
||||
deadline = time.time() + 10
|
||||
while time.time() < deadline:
|
||||
try:
|
||||
r = requests.get(f"{POLYGON_URL}/health", timeout=2)
|
||||
if r.status_code == 200:
|
||||
break
|
||||
except requests.ConnectionError:
|
||||
time.sleep(0.5)
|
||||
else:
|
||||
proc.terminate()
|
||||
proc.wait()
|
||||
pytest.fail("polygon did not start within 10s")
|
||||
|
||||
yield POLYGON_URL
|
||||
|
||||
proc.terminate()
|
||||
proc.wait()
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def reset_polygon(polygon_server):
|
||||
"""Перед каждым интеграционным тестом — сброс состояния polygon."""
|
||||
requests.post(f"{polygon_server}/api/v1/svc/_mock/reset", timeout=5)
|
||||
|
||||
|
||||
@@ -0,0 +1,227 @@
|
||||
"""
|
||||
Интеграционные тесты app-autotest ↔ polygon.
|
||||
|
||||
Каждый тест требует запущенного polygon (фикстура polygon_server).
|
||||
Перед каждым тестом состояние polygon сбрасывается (autouse reset_polygon).
|
||||
|
||||
Все запросы — через requests (HTTP), имитируя работу HttpClient из app-autotest.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import requests
|
||||
|
||||
API = "/api/v1/svc"
|
||||
|
||||
|
||||
def _create_instance(url, service_id=1, display_name="test"):
|
||||
"""POST /instances → извлечь instanceUid из Location."""
|
||||
r = requests.post(
|
||||
f"{url}{API}/instances",
|
||||
json={"serviceId": service_id, "displayName": display_name},
|
||||
allow_redirects=False,
|
||||
timeout=10,
|
||||
)
|
||||
assert r.status_code == 201
|
||||
location = r.headers.get("Location", "")
|
||||
uid = location.rsplit("/", 1)[-1]
|
||||
assert len(uid) >= 32 # UUID без дефисов
|
||||
return uid
|
||||
|
||||
|
||||
def _create_operation(url, instance_uid, operation, svc_op_id=None):
|
||||
"""POST /instanceOperations → извлечь opUid из Location."""
|
||||
body = {"instanceUid": instance_uid, "operation": operation}
|
||||
if svc_op_id is not None:
|
||||
body["svcOperationId"] = svc_op_id
|
||||
r = requests.post(
|
||||
f"{url}{API}/instanceOperations",
|
||||
json=body,
|
||||
allow_redirects=False,
|
||||
timeout=10,
|
||||
)
|
||||
assert r.status_code == 201
|
||||
location = r.headers.get("Location", "")
|
||||
uid = location.rsplit("/", 1)[-1]
|
||||
assert len(uid) >= 32
|
||||
return uid
|
||||
|
||||
|
||||
def _run(url, op_uid):
|
||||
"""POST /run → дождаться dtFinish."""
|
||||
r = requests.post(f"{url}{API}/instanceOperations/{op_uid}/run", timeout=10)
|
||||
assert r.status_code == 200
|
||||
# Проверить что операция завершена
|
||||
r2 = requests.get(f"{url}{API}/instanceOperations/{op_uid}", timeout=10)
|
||||
op = r2.json()["instanceOperation"]
|
||||
assert op["dtFinish"] is not None
|
||||
assert op["isSuccessful"] is True
|
||||
|
||||
|
||||
class TestServices:
|
||||
"""Тесты эндпоинтов /services."""
|
||||
|
||||
def test_list_services(self, polygon_server):
|
||||
"""GET /services возвращает все сервисы."""
|
||||
r = requests.get(f"{polygon_server}{API}/services", timeout=10)
|
||||
assert r.status_code == 200
|
||||
data = r.json()
|
||||
assert len(data["results"]) >= 30 # минимум 30 сервисов
|
||||
|
||||
def test_service_detail(self, polygon_server):
|
||||
"""GET /services/1 возвращает операции Болванки."""
|
||||
r = requests.get(f"{polygon_server}{API}/services/1", timeout=10)
|
||||
assert r.status_code == 200
|
||||
ops = r.json()["svc"]["operations"]
|
||||
assert any(op["operation"] == "create" for op in ops)
|
||||
|
||||
def test_service_404(self, polygon_server):
|
||||
"""GET /services/99999 → 404."""
|
||||
r = requests.get(f"{polygon_server}{API}/services/99999", timeout=10)
|
||||
assert r.status_code == 404
|
||||
|
||||
|
||||
class TestInstanceCreate:
|
||||
"""Тесты создания инстанса."""
|
||||
|
||||
def test_create_dummy(self, polygon_server):
|
||||
"""POST /instances → 201 + Location → GET → status=creating."""
|
||||
uid = _create_instance(polygon_server)
|
||||
r = requests.get(f"{polygon_server}{API}/instances/{uid}", timeout=10)
|
||||
assert r.status_code == 200
|
||||
inst = r.json()["instance"]
|
||||
assert inst["status"] == "creating"
|
||||
assert inst["serviceId"] == 1
|
||||
|
||||
def test_create_unknown_service(self, polygon_server):
|
||||
"""POST /instances с несуществующим serviceId → 404."""
|
||||
r = requests.post(
|
||||
f"{polygon_server}{API}/instances",
|
||||
json={"serviceId": 99999, "displayName": "bad"},
|
||||
timeout=10,
|
||||
)
|
||||
assert r.status_code == 404
|
||||
|
||||
|
||||
class TestFullCreateFlow:
|
||||
"""Полный цикл create → run → проверить params."""
|
||||
|
||||
def test_create_and_run(self, polygon_server):
|
||||
"""create → run → статус running + params из шаблона."""
|
||||
uid = _create_instance(polygon_server)
|
||||
op_uid = _create_operation(polygon_server, uid, "create")
|
||||
_run(polygon_server, op_uid)
|
||||
|
||||
r = requests.get(f"{polygon_server}{API}/instances/{uid}", timeout=10)
|
||||
inst = r.json()["instance"]
|
||||
assert inst["status"] == "running"
|
||||
assert "resourceRealm" in inst["state"]["params"]
|
||||
assert inst["state"]["params"]["resourceRealm"] == "dummy"
|
||||
|
||||
def test_create_postgres_state_out(self, polygon_server):
|
||||
"""create PG → state.out содержит users, databases."""
|
||||
uid = _create_instance(polygon_server, service_id=90, display_name="pg")
|
||||
op_uid = _create_operation(polygon_server, uid, "create")
|
||||
_run(polygon_server, op_uid)
|
||||
|
||||
r = requests.get(f"{polygon_server}{API}/instances/{uid}", timeout=10)
|
||||
out = r.json()["instance"]["state"]["out"]
|
||||
assert "users" in out
|
||||
assert "databases" in out
|
||||
|
||||
def test_run_twice_409(self, polygon_server):
|
||||
"""Повторный POST /run → 409."""
|
||||
uid = _create_instance(polygon_server)
|
||||
op_uid = _create_operation(polygon_server, uid, "create")
|
||||
_run(polygon_server, op_uid)
|
||||
|
||||
r = requests.post(
|
||||
f"{polygon_server}{API}/instanceOperations/{op_uid}/run", timeout=10
|
||||
)
|
||||
assert r.status_code == 409
|
||||
|
||||
|
||||
class TestOperations:
|
||||
"""Тесты операций: modify, suspend, resume, delete."""
|
||||
|
||||
def test_modify_merges_params(self, polygon_server):
|
||||
"""modify → новый paramValue в state.params."""
|
||||
uid = _create_instance(polygon_server)
|
||||
_run(polygon_server, _create_operation(polygon_server, uid, "create"))
|
||||
|
||||
# Установить новое значение durationMs (modify-версия: paramId=287, create-версия: 198)
|
||||
op_uid = _create_operation(polygon_server, uid, "modify")
|
||||
requests.post(
|
||||
f"{polygon_server}{API}/instanceOperationCfsParams",
|
||||
json={"instanceOperationUid": op_uid, "svcOperationCfsParamId": 287, "paramValue": "999"},
|
||||
timeout=10,
|
||||
)
|
||||
_run(polygon_server, op_uid)
|
||||
|
||||
r = requests.get(f"{polygon_server}{API}/instances/{uid}", timeout=10)
|
||||
assert r.json()["instance"]["state"]["params"]["durationMs"] == "999"
|
||||
|
||||
def test_suspend_resume(self, polygon_server):
|
||||
"""suspend → status=suspended, resume → status=running."""
|
||||
uid = _create_instance(polygon_server)
|
||||
_run(polygon_server, _create_operation(polygon_server, uid, "create"))
|
||||
|
||||
_run(polygon_server, _create_operation(polygon_server, uid, "suspend"))
|
||||
r = requests.get(f"{polygon_server}{API}/instances/{uid}", timeout=10)
|
||||
assert r.json()["instance"]["status"] == "suspended"
|
||||
|
||||
_run(polygon_server, _create_operation(polygon_server, uid, "resume"))
|
||||
r = requests.get(f"{polygon_server}{API}/instances/{uid}", timeout=10)
|
||||
assert r.json()["instance"]["status"] == "running"
|
||||
|
||||
def test_delete_removes_instance(self, polygon_server):
|
||||
"""delete → инстанс удалён из списка."""
|
||||
uid = _create_instance(polygon_server)
|
||||
_run(polygon_server, _create_operation(polygon_server, uid, "create"))
|
||||
_run(polygon_server, _create_operation(polygon_server, uid, "delete"))
|
||||
|
||||
r = requests.get(f"{polygon_server}{API}/instances", timeout=10)
|
||||
uids = [i["instanceUid"] for i in r.json()["results"]]
|
||||
assert uid not in uids
|
||||
|
||||
|
||||
class TestPagination:
|
||||
"""Тесты пагинации."""
|
||||
|
||||
def test_pagination(self, polygon_server):
|
||||
"""3 инстанса, pageSize=2 → 2 страницы."""
|
||||
for i in range(3):
|
||||
_create_instance(polygon_server, display_name=f"p{i}")
|
||||
|
||||
r1 = requests.get(
|
||||
f"{polygon_server}{API}/instances", params={"pageSize": 2, "page": 1}, timeout=10
|
||||
)
|
||||
assert len(r1.json()["results"]) == 2
|
||||
|
||||
r2 = requests.get(
|
||||
f"{polygon_server}{API}/instances", params={"pageSize": 2, "page": 2}, timeout=10
|
||||
)
|
||||
assert len(r2.json()["results"]) == 1 # последний
|
||||
|
||||
|
||||
class TestMockEndpoints:
|
||||
"""Тесты /_mock/* эндпоинтов."""
|
||||
|
||||
def test_mock_state(self, polygon_server):
|
||||
"""GET /_mock/state — отладочный дамп."""
|
||||
_create_instance(polygon_server)
|
||||
r = requests.get(f"{polygon_server}{API}/_mock/state", timeout=10)
|
||||
assert r.status_code == 200
|
||||
assert len(r.json()["instances"]) == 1
|
||||
|
||||
def test_mock_services(self, polygon_server):
|
||||
"""GET /_mock/services — список конфигов."""
|
||||
r = requests.get(f"{polygon_server}{API}/_mock/services", timeout=10)
|
||||
assert r.status_code == 200
|
||||
assert r.json()["count"] >= 30
|
||||
|
||||
def test_mock_delay(self, polygon_server):
|
||||
"""POST /_mock/delay — изменение задержки."""
|
||||
r = requests.post(f"{polygon_server}{API}/_mock/delay/2", timeout=10)
|
||||
assert r.json()["delay"] == 2.0
|
||||
# Сбросить
|
||||
requests.post(f"{polygon_server}{API}/_mock/delay/0.1", timeout=10)
|
||||
Reference in New Issue
Block a user