test: 15 интеграционных тестов polygon + bump v1.2.25
This commit is contained in:
+1
-1
@@ -32,7 +32,7 @@ from routes.api_scenario_defs import bp_defs as api_scenario_defs_bp
|
|||||||
|
|
||||||
# Версия — показывается в топбаре UI. Меняется при КАЖДОМ изменении кода.
|
# Версия — показывается в топбаре UI. Меняется при КАЖДОМ изменении кода.
|
||||||
# Нужна для фильтрации истории (пользователь видит только записи своей версии).
|
# Нужна для фильтрации истории (пользователь видит только записи своей версии).
|
||||||
VERSION = "1.2.24"
|
VERSION = "1.2.25"
|
||||||
|
|
||||||
# Flask-приложение с Jinja2-шаблонами из папки templates/
|
# Flask-приложение с Jinja2-шаблонами из папки templates/
|
||||||
app = Flask(__name__, template_folder="templates", static_folder="static")
|
app = Flask(__name__, template_folder="templates", static_folder="static")
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
|
import time
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
import requests
|
||||||
|
|
||||||
|
|
||||||
# Make `site/` importable as top-level modules: app, routes, db, operations, ...
|
# 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:
|
if str(SITE_DIR) not in sys.path:
|
||||||
sys.path.insert(0, str(SITE_DIR))
|
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
|
@pytest.fixture
|
||||||
def app_client():
|
def app_client():
|
||||||
|
"""Flask test client для app-autotest."""
|
||||||
from app import app
|
from app import app
|
||||||
|
|
||||||
app.config.update(TESTING=True)
|
app.config.update(TESTING=True)
|
||||||
with app.test_client() as client:
|
with app.test_client() as client:
|
||||||
yield 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