test: полная симуляция пользователя — 38 проверок, 8 сценариев
ТЕСТ 1: загрузка страницы, сервисы, инстансы ТЕСТ 2: create 9 сервисов (Болванка, PostgreSQL, Redis, ...) ТЕСТ 3: переключение DEV/TEST/PROD ТЕСТ 4: Облако/Эмуляция туда-обратно ТЕСТ 5: CRUD сценария + запуск + повтор + seed ТЕСТ 6: история + проверка дубликатов ТЕСТ 7: логи ТЕСТ 8: валидация входных данных
This commit is contained in:
@@ -0,0 +1,247 @@
|
||||
"""
|
||||
Полное моделирование поведения пользователя автотеста.
|
||||
Бьёт в atest.pythonk8s.dev.nubes.ru, эмуляция + облако, все стенды,
|
||||
ручные операции + сценарии + переключения + краевые случаи.
|
||||
Запуск: python3 tests/test_user_simulation.py
|
||||
"""
|
||||
import time, requests, json, sys
|
||||
|
||||
BASE = "https://atest.pythonk8s.dev.nubes.ru"
|
||||
PASS = FAIL = 0
|
||||
|
||||
def cookie(mode="polygon", stand="test"):
|
||||
return {"mode": mode, "polygon_stand": stand}
|
||||
|
||||
def log(ok, msg):
|
||||
global PASS, FAIL
|
||||
mark = "OK" if ok else "FAIL"
|
||||
if ok: PASS += 1
|
||||
else: FAIL += 1
|
||||
print(f" [{mark}] {msg}")
|
||||
if not ok:
|
||||
sys.stdout.flush()
|
||||
|
||||
def get(path, cookies=None, code=200):
|
||||
r = requests.get(f"{BASE}{path}", cookies=cookies or cookie(), timeout=15)
|
||||
assert r.status_code == code, f"GET {path}: {r.status_code}"
|
||||
return r.json() if r.text.strip() else {}
|
||||
|
||||
def post(path, data, cookies=None, code=200):
|
||||
r = requests.post(f"{BASE}{path}", json=data, cookies=cookies or cookie(), timeout=15)
|
||||
assert r.status_code == code, f"POST {path}: {r.status_code} {r.text[:100]}"
|
||||
return r.json() if r.text.strip() else {}
|
||||
|
||||
def poll(op_uid, cookies=None, timeout=15):
|
||||
deadline = time.time() + timeout
|
||||
while time.time() < deadline:
|
||||
d = get(f"/api/test/status/{op_uid}", cookies)
|
||||
if d.get("done") or d.get("status") in ("OK", "FAIL"):
|
||||
return d
|
||||
time.sleep(0.5)
|
||||
return {"status": "TIMEOUT"}
|
||||
|
||||
def create_instance(sid, name=None, cookies=None):
|
||||
r = post("/api/test",
|
||||
{"serviceId": sid, "operation": "create", "svcOperationId": 18,
|
||||
"params": {}, "displayName": name or f"autotest-user-{sid}"},
|
||||
cookies)
|
||||
return r["opUid"], r.get("instanceUid")
|
||||
|
||||
def run_scenario(def_id, cookies=None):
|
||||
r = post("/api/scenario/run", {"definition_id": def_id}, cookies, code=202)
|
||||
return r["run_id"]
|
||||
|
||||
def poll_scenario(run_id, cookies=None, timeout=20):
|
||||
deadline = time.time() + timeout
|
||||
while time.time() < deadline:
|
||||
d = get(f"/api/scenario/run/{run_id}", cookies)
|
||||
if d.get("status") != "RUNNING":
|
||||
return d
|
||||
time.sleep(1)
|
||||
return {"status": "TIMEOUT"}
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════
|
||||
print("=" * 60)
|
||||
print("ТЕСТ 1: Загрузка страницы, сервисы, эмуляция")
|
||||
print("=" * 60)
|
||||
|
||||
# 1.1 Главная страница рендерится
|
||||
r = requests.get(BASE, cookies=cookie(), timeout=10)
|
||||
log(r.status_code == 200, "GET / -> 200")
|
||||
log("Эмуляция" in r.text, "страница содержит 'Эмуляция'")
|
||||
log("v1.2" in r.text, "версия присутствует")
|
||||
|
||||
# 1.2 Список сервисов
|
||||
svc = get("/api/services")
|
||||
log(len(svc) > 30, f"сервисов: {len(svc)}")
|
||||
log(any(s["svcId"] == 1 for s in svc), "Болванка есть")
|
||||
|
||||
# 1.3 Инстансы
|
||||
inst = get("/api/instances/list")
|
||||
log(isinstance(inst, list), f"инстансов: {len(inst)}")
|
||||
|
||||
|
||||
print(f"\n{'='*60}")
|
||||
print("ТЕСТ 2: Ручные операции (create + modify + delete)")
|
||||
print("=" * 60)
|
||||
|
||||
for sid in [1, 90, 91, 92, 93, 28, 21, 25, 99]:
|
||||
sname = next((s["svc"] for s in svc if s["svcId"] == sid), "?")
|
||||
try:
|
||||
op_uid, iuid = create_instance(sid, f"autotest-user-{sid}-{int(time.time())}")
|
||||
result = poll(op_uid)
|
||||
log(result.get("status") == "OK", f"create {sid} {sname} -> {iuid[:8] if iuid else '?'}")
|
||||
except Exception as e:
|
||||
log(False, f"create {sid} {sname} -> {str(e)[:60]}")
|
||||
|
||||
|
||||
print(f"\n{'='*60}")
|
||||
print("ТЕСТ 3: Переключение стендов в эмуляции")
|
||||
print("=" * 60)
|
||||
|
||||
for stand in ["dev", "test", "prod"]:
|
||||
svc_s = get("/api/services", cookie("polygon", stand))
|
||||
cnt = len(svc_s)
|
||||
log(cnt > 0, f"стенд {stand}: {cnt} сервисов")
|
||||
|
||||
|
||||
print(f"\n{'='*60}")
|
||||
print("ТЕСТ 4: Переключение Облако / Эмуляция")
|
||||
print("=" * 60)
|
||||
|
||||
# 4.1 Переключиться в облако
|
||||
r = requests.post(BASE, data={"action": "set_mode", "mode": "cloud", "polygon_stand": "test"},
|
||||
cookies=cookie(), allow_redirects=False, timeout=10)
|
||||
log(r.status_code in (302, 200), "POST set_mode cloud -> редирект")
|
||||
|
||||
# 4.2 В облаке без токена — ошибка
|
||||
c = cookie("cloud", "test")
|
||||
r = requests.get(BASE, cookies=c, timeout=10)
|
||||
log("Токен невалиден" in r.text or "Token" in r.text or r.status_code in (200, 302),
|
||||
"облако без токена: ожидаемая ошибка")
|
||||
|
||||
# 4.3 Обратно в эмуляцию
|
||||
r = requests.post(BASE, data={"action": "set_mode", "mode": "polygon", "polygon_stand": "test"},
|
||||
cookies=c, allow_redirects=False, timeout=10)
|
||||
log(r.status_code in (302, 200), "POST set_mode polygon -> редирект")
|
||||
|
||||
# 4.4 После возврата — сервисы снова есть
|
||||
svc2 = get("/api/services", cookie())
|
||||
log(len(svc2) > 30, f"после возврата: {len(svc2)} сервисов")
|
||||
|
||||
|
||||
print(f"\n{'='*60}")
|
||||
print("ТЕСТ 5: Сценарии — CRUD + запуск")
|
||||
print("=" * 60)
|
||||
|
||||
# 5.1 Список сценариев
|
||||
scenarios = get("/api/scenarios")
|
||||
log(len(scenarios) > 0, f"сценариев: {len(scenarios)}")
|
||||
|
||||
# 5.2 Создать сценарий
|
||||
name = f"user-test-{int(time.time())}"
|
||||
steps = [
|
||||
{"service_id": 1, "operation": "create", "params": {}, "output": "u1"},
|
||||
{"service_id": 1, "operation": "modify", "instance_ref": "u1", "params": {}},
|
||||
{"service_id": 1, "operation": "delete", "instance_ref": "u1", "params": {}},
|
||||
]
|
||||
r = requests.post(f"{BASE}/api/scenario/definitions",
|
||||
json={"name": name, "steps": steps},
|
||||
cookies=cookie(), timeout=10)
|
||||
sc_def = r.json()
|
||||
log("id" in sc_def, f"создан сценарий {name} (id={sc_def.get('id')})")
|
||||
|
||||
if "id" in sc_def:
|
||||
# 5.3 Запустить
|
||||
run_id = run_scenario(sc_def["id"])
|
||||
log(run_id is not None, f"запущен run_id={run_id}")
|
||||
|
||||
# 5.4 Поллинг
|
||||
result = poll_scenario(run_id)
|
||||
log(result.get("status") == "OK", f"сценарий: {result.get('status')} (шаг {result.get('current_step')}/{result.get('total_steps')})")
|
||||
|
||||
# 5.5 Запустить ещё раз (после завершения должно работать)
|
||||
run_id2 = run_scenario(sc_def["id"])
|
||||
result2 = poll_scenario(run_id2)
|
||||
log(result2.get("status") == "OK", f"повторный запуск: {result2.get('status')}")
|
||||
|
||||
# 5.6 Удалить
|
||||
r = requests.delete(f"{BASE}/api/scenario/definitions/{sc_def['id']}", cookies=cookie(), timeout=10)
|
||||
log(r.status_code == 200, f"удалён сценарий {sc_def['id']}")
|
||||
|
||||
# 5.7 Seed-сценарий запустить
|
||||
seed = next((s for s in scenarios if s.get("is_seed")), None)
|
||||
if seed:
|
||||
run_id = run_scenario(seed["id"])
|
||||
result = poll_scenario(run_id, timeout=20)
|
||||
log(result.get("status") == "OK", f"seed-сценарий '{seed['name']}': {result.get('status')}")
|
||||
else:
|
||||
# Ищем dummy_test среди обычных
|
||||
for s in scenarios:
|
||||
if "dummy" in s.get("name", "").lower():
|
||||
run_id = run_scenario(s["id"])
|
||||
result = poll_scenario(run_id, timeout=20)
|
||||
log(result.get("status") == "OK", f"сценарий '{s['name']}': {result.get('status')}")
|
||||
break
|
||||
else:
|
||||
log(False, "нет сценария для запуска")
|
||||
|
||||
|
||||
print(f"\n{'='*60}")
|
||||
print("ТЕСТ 6: История")
|
||||
print("=" * 60)
|
||||
|
||||
rows = get("/api/history")
|
||||
log(len(rows) > 0, f"записей в истории: {len(rows)}")
|
||||
log(any(r["status"] == "OK" for r in rows), "есть OK-записи")
|
||||
|
||||
# Проверка что нет дубликатов RUNNING/OK (фикс v1.2.39)
|
||||
op_uids = [r["op_uid"] for r in rows if r.get("op_uid")]
|
||||
dup_count = len(op_uids) - len(set(op_uids))
|
||||
log(dup_count == 0, f"дубликатов op_uid: {dup_count} (0 = хорошо)")
|
||||
|
||||
|
||||
print(f"\n{'='*60}")
|
||||
print("ТЕСТ 7: Логи")
|
||||
print("=" * 60)
|
||||
|
||||
logs = get("/api/log")
|
||||
log(isinstance(logs, list), f"строк лога: {len(logs)}")
|
||||
log(any("api_test" in l or "save_run" in l or "OK" in l for l in logs),
|
||||
"логи содержат операции")
|
||||
|
||||
|
||||
print(f"\n{'='*60}")
|
||||
print("ТЕСТ 8: Валидация входных данных")
|
||||
print("=" * 60)
|
||||
|
||||
# 8.1 Невалидный serviceId
|
||||
r = requests.post(f"{BASE}/api/test",
|
||||
json={"serviceId": "abc", "operation": "create", "svcOperationId": 18, "params": {}},
|
||||
cookies=cookie(), timeout=10)
|
||||
log(r.status_code == 400, f"serviceId='abc' -> 400")
|
||||
|
||||
# 8.2 Пустой operation
|
||||
r = requests.post(f"{BASE}/api/test",
|
||||
json={"serviceId": 1, "operation": "", "svcOperationId": 18, "params": {}},
|
||||
cookies=cookie(), timeout=10)
|
||||
log(r.status_code == 400, f"operation='' -> 400")
|
||||
|
||||
# 8.3 Невалидный instanceUid
|
||||
r = requests.post(f"{BASE}/api/test",
|
||||
json={"serviceId": 1, "operation": "delete", "svcOperationId": 71,
|
||||
"instanceUid": "not-a-uuid", "params": {}},
|
||||
cookies=cookie(), timeout=10)
|
||||
log(r.status_code == 400, f"instanceUid='not-a-uuid' -> 400")
|
||||
|
||||
# 8.4 Невалидный определение сценария
|
||||
r = requests.post(f"{BASE}/api/scenario/run",
|
||||
json={"definition_id": 99999}, cookies=cookie(), timeout=10)
|
||||
log(r.status_code == 404, f"run definition_id=99999 -> 404")
|
||||
|
||||
|
||||
print(f"\n{'='*60}")
|
||||
print(f"ИТОГО: PASS={PASS} FAIL={FAIL}")
|
||||
print(f"{'='*60}")
|
||||
sys.exit(0 if FAIL == 0 else 1)
|
||||
Reference in New Issue
Block a user