fix: adapt autotest to updated service APIs

This commit is contained in:
Repinoid
2026-10-25 16:30:25 +03:00
parent 8d956c8063
commit 4203e5674c
13 changed files with 462 additions and 292 deletions
+23 -9
View File
@@ -1,4 +1,6 @@
from pathlib import Path
import os
import socket
import subprocess
import sys
import time
@@ -14,17 +16,21 @@ if str(SITE_DIR) not in sys.path:
# Путь к 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."""
def app_client(polygon_server):
"""Flask test client, направленный в локальный Polygon TEST."""
from app import app
old_polygon_endpoint = app.config.get("POLYGON_ENDPOINT", "")
app.config["POLYGON_ENDPOINT"] = f"{polygon_server}/api/v1/svc"
app.config.update(TESTING=True)
with app.test_client() as client:
yield client
try:
with app.test_client() as client:
yield client
finally:
app.config["POLYGON_ENDPOINT"] = old_polygon_endpoint
@pytest.fixture(scope="session")
@@ -37,18 +43,26 @@ def polygon_server():
if not POLYGON_DIR.is_dir():
pytest.skip("polygon repo not found")
with socket.socket() as port_socket:
port_socket.bind(("127.0.0.1", 0))
port = port_socket.getsockname()[1]
polygon_url = f"http://127.0.0.1:{port}"
env = os.environ.copy()
env["POLYGON_STANDS"] = "test:services/test"
proc = subprocess.Popen(
["python3", "app.py"],
["python3", "-c", f"import app; app.app.run(host='127.0.0.1', port={port}, debug=False)"],
cwd=str(POLYGON_DIR),
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
env=env,
)
# Ждём готовности (poll /health, timeout 10s)
deadline = time.time() + 10
while time.time() < deadline:
try:
r = requests.get(f"{POLYGON_URL}/health", timeout=2)
r = requests.get(f"{polygon_url}/health", timeout=2)
if r.status_code == 200:
break
except requests.ConnectionError:
@@ -58,7 +72,7 @@ def polygon_server():
proc.wait()
pytest.fail("polygon did not start within 10s")
yield POLYGON_URL
yield polygon_url
proc.terminate()
proc.wait()
@@ -67,5 +81,5 @@ def polygon_server():
@pytest.fixture(autouse=True)
def reset_polygon(polygon_server):
"""Перед каждым интеграционным тестом — сброс состояния polygon."""
requests.post(f"{polygon_server}/api/v1/svc/_mock/reset", timeout=5)
requests.post(f"{polygon_server}/test/api/v1/svc/_mock/reset", timeout=5)
+41
View File
@@ -5,6 +5,7 @@
"""
import json
import time
import pytest
# ── Хелперы ──
@@ -48,6 +49,46 @@ class TestScenarioLifecycle:
data = resp.get_json()
assert isinstance(data, list)
def test_manual_create_delete_uses_local_polygon(self, app_client, monkeypatch):
"""Ручной API приложения проводит create и delete через локальный Polygon."""
from api.http_client import HttpClient
from routes import api_test as api_test_routes
monkeypatch.setattr(api_test_routes, "_finish_op", lambda *args, **kwargs: None)
monkeypatch.setattr(
HttpClient,
"raw_delete",
lambda *args, **kwargs: pytest.fail("Polygon delete must not call external CMDB"),
)
operations_response = app_client.get("/api/operations/1")
assert operations_response.status_code == 200
operations = operations_response.get_json()["operations"]
create_id = next(op["svcOperationId"] for op in operations if op["operation"] == "create")
delete_id = next(op["svcOperationId"] for op in operations if op["operation"] == "delete")
created = app_client.post("/api/test", json={
"serviceId": 1,
"operation": "create",
"svcOperationId": create_id,
"params": {},
"displayName": "autotest-local-integration",
})
assert created.status_code == 200, created.get_json()
create_result = app_client.get(f"/api/test/status/{created.get_json()['opUid']}").get_json()
assert create_result["status"] == "OK"
deleted = app_client.post("/api/test", json={
"serviceId": 1,
"operation": "delete",
"svcOperationId": delete_id,
"instanceUid": created.get_json()["instanceUid"],
"params": {},
})
assert deleted.status_code == 200, deleted.get_json()
delete_result = app_client.get(f"/api/test/status/{deleted.get_json()['opUid']}").get_json()
assert delete_result["status"] == "OK"
def test_create_and_run_simple_scenario(self, app_client):
"""Сценарий create -> delete. Проверяем что оба шага OK."""
steps = [
+9 -2
View File
@@ -4,7 +4,9 @@
Бьёт в РАЗВЁРНУТЫЙ автотест (atest.pythonk8s.dev.nubes.ru), эмуляция TEST.
Операции идут через app-autotest -> executor -> save_run -> история в БД.
"""
import time, requests
import time, requests, pytest
pytestmark = pytest.mark.skip(reason="live test against deployed atest service; run this file directly")
BASE = "https://atest.pythonk8s.dev.nubes.ru"
COOKIES = {"mode": "polygon", "polygon_stand": "test"}
@@ -19,8 +21,13 @@ def test_services():
def test_create(svc_id):
operations_response = requests.get(f"{BASE}/api/operations/{svc_id}", cookies=COOKIES, timeout=10)
assert operations_response.status_code == 200
operations = operations_response.json().get("operations", [])
create_op = next((op for op in operations if op.get("operation") == "create"), None)
assert create_op is not None, f"No create operation for service {svc_id}"
r = requests.post(f"{BASE}/api/test",
json={"serviceId": svc_id, "operation": "create", "svcOperationId": 18,
json={"serviceId": svc_id, "operation": "create", "svcOperationId": create_op["svcOperationId"],
"params": {}, "displayName": f"autotest-smoke-{svc_id}"},
cookies=COOKIES, timeout=30)
assert r.status_code == 200, f"Create failed: {r.text[:200]}"
+41
View File
@@ -21,3 +21,44 @@ def test_scenario_name_escape_chain_includes_amp_backslash_quote_doublequote():
".replace(/\"/g,'&quot;');"
)
assert expected in content
def test_scenario_editor_loads_operations_from_selected_service():
js_path = Path(__file__).resolve().parents[1] / "site" / "static" / "js" / "scenario-form.js"
content = js_path.read_text(encoding="utf-8")
assert "async function loadServiceOperations(svcId)" in content
assert "fetch('/api/operations/' + svcId)" in content
assert "serviceOps.filter(item => item.operation !== 'reconcile')" in content
assert "const allOps = ['create', 'delete', 'modify', 'suspend', 'resume', 'redeploy']" not in content
def test_manual_create_uses_current_service_operation_id():
js_path = Path(__file__).resolve().parents[1] / "site" / "static" / "js" / "operations.js"
content = js_path.read_text(encoding="utf-8")
assert "find(o => o.operation === 'create')" in content
assert "showParams(createOp.svcOperationId, 'create')" in content
assert "showParams(18, 'create')" not in content
def test_scenario_editor_preserves_legacy_and_cloud_instance_references():
js_path = Path(__file__).resolve().parents[1] / "site" / "static" / "js" / "scenario-form.js"
content = js_path.read_text(encoding="utf-8")
assert "_legacyInstanceFallback: s.operation !== 'create'" in content
assert "_showCloud: Boolean(s.instance_uid)" in content
assert "ref === step.instance_uid" in content
assert "s.instance_uid = ref" in content
assert "&& !scenarioEditorState.steps[i]._legacyInstanceFallback" in content
def test_live_scripts_are_not_collected_as_network_tests():
tests_dir = Path(__file__).resolve().parent
user_simulation = (tests_dir / "test_user_simulation.py").read_text(encoding="utf-8")
smoke = (tests_dir / "test_smoke_all_services.py").read_text(encoding="utf-8")
assert "pytest.skip(" in user_simulation
assert "pytestmark = pytest.mark.skip" in smoke
assert "operation_id(sid, \"create\"" in user_simulation
assert "create_op[\"svcOperationId\"]" in smoke
+17 -6
View File
@@ -4,7 +4,10 @@
ручные операции + сценарии + переключения + краевые случаи.
Запуск: python3 tests/test_user_simulation.py
"""
import time, requests, json, sys
import time, requests, json, sys, pytest
if __name__ != "__main__":
pytest.skip("live test against deployed atest service; run this file directly", allow_module_level=True)
BASE = "https://atest.pythonk8s.dev.nubes.ru"
PASS = FAIL = 0
@@ -31,6 +34,14 @@ def post(path, data, cookies=None, code=200):
assert r.status_code == code, f"POST {path}: {r.status_code} {r.text[:100]}"
return r.json() if r.text.strip() else {}
def operation_id(service_id, operation, cookies=None):
operations = get(f"/api/operations/{service_id}", cookies)
match = next((item for item in operations.get("operations", [])
if item.get("operation") == operation), None)
if not match:
raise RuntimeError(f"Operation {operation!r} not found for service {service_id}")
return match["svcOperationId"]
def poll(op_uid, cookies=None, timeout=15):
deadline = time.time() + timeout
while time.time() < deadline:
@@ -42,7 +53,7 @@ def poll(op_uid, cookies=None, timeout=15):
def create_instance(sid, name=None, cookies=None):
r = post("/api/test",
{"serviceId": sid, "operation": "create", "svcOperationId": 18,
{"serviceId": sid, "operation": "create", "svcOperationId": operation_id(sid, "create", cookies),
"params": {}, "displayName": name or f"autotest-user-{sid}"},
cookies)
return r["opUid"], r.get("instanceUid")
@@ -249,7 +260,7 @@ try:
# 9.1 Создать PostgreSQL
pg_name = f"autotest-pg-{int(time.time())}"
r = post("/api/test",
{"serviceId": 90, "operation": "create", "svcOperationId": 19,
{"serviceId": 90, "operation": "create", "svcOperationId": operation_id(90, "create"),
"params": {}, "displayName": pg_name})
pg_op = r["opUid"]
pg_uid = r.get("instanceUid")
@@ -260,7 +271,7 @@ try:
# 9.2 Создать пользователя
user_name = f"testuser_{int(time.time()) % 10000}"
r = post("/api/test",
{"serviceId": 90, "operation": "create_user", "svcOperationId": 241,
{"serviceId": 90, "operation": "create_user", "svcOperationId": operation_id(90, "create_user"),
"instanceUid": pg_uid,
"params": {"733": user_name, "740": "app_user", "1096": "false"}})
user_op = r["opUid"]
@@ -270,7 +281,7 @@ try:
# 9.3 Создать базу данных
db_name = f"testdb_{int(time.time()) % 10000}"
r = post("/api/test",
{"serviceId": 90, "operation": "create_database", "svcOperationId": 245,
{"serviceId": 90, "operation": "create_database", "svcOperationId": operation_id(90, "create_database"),
"instanceUid": pg_uid,
"params": {"741": db_name, "742": user_name}})
db_op = r["opUid"]
@@ -279,7 +290,7 @@ try:
# 9.4 Удалить PostgreSQL
r = post("/api/test",
{"serviceId": 90, "operation": "delete", "svcOperationId": 20,
{"serviceId": 90, "operation": "delete", "svcOperationId": operation_id(90, "delete"),
"instanceUid": pg_uid, "params": {}})
del_op = r["opUid"]
result = poll(del_op)