diff --git a/tests/test_user_simulation.py b/tests/test_user_simulation.py new file mode 100644 index 0000000..6dfb75c --- /dev/null +++ b/tests/test_user_simulation.py @@ -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)