"""Временный e2e-тест по HTTP: реальный uvicorn + фронтовые API-вызовы.""" import os import subprocess import sys import tempfile import time import httpx # devtests/ лежит внутри backend/ — backend нужен как cwd для uvicorn tmp = tempfile.mkdtemp(prefix="leadradar_e2e_") env = dict(os.environ) env["LEADRADAR_DATA"] = tmp env["LEADRADAR_DEMO"] = "1" BACKEND_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) server = subprocess.Popen( [sys.executable, "-m", "uvicorn", "app.main:app", "--port", "8077", "--log-level", "warning"], cwd=BACKEND_DIR, env=env, ) BASE = "http://127.0.0.1:8077" ok = True def check(name, cond, extra=""): global ok if not cond: ok = False print("FAIL:", name, extra) else: print("ok:", name) try: for _ in range(40): try: r = httpx.get(BASE + "/api/health", timeout=1) if r.status_code == 200: break except Exception: time.sleep(0.5) else: raise SystemExit("server did not start") with httpx.Client(base_url=BASE, timeout=20) as c: check("health", c.get("/api/health").status_code == 200) # вход r = c.post("/api/auth/login", json={"login": "admin", "password": "admin"}) check("login", r.status_code == 200) check("cookie set", bool(c.cookies.get("leadradar_session"))) me = c.get("/api/auth/me") check("me", me.status_code == 200 and me.json().get("login") == "admin") # стартовые данные: колонок нет по умолчанию — создаём одну через API boards = c.get("/api/boards").json() check("no default boards", boards == []) bid = c.post("/api/boards", json={"name": "Python"}).json()["id"] check("board created", bool(bid)) settings = c.get("/api/settings").json() check("settings public", settings.get("targetCurrency") == "RUB" and settings.get("aiProvider") == "deepseek") # демо-лид -> поиск lead = c.post("/api/demo/simulate-lead").json() check("demo lead", bool(lead.get("id")), str(lead)[:120]) lid = lead["id"] q = c.get("/api/search", params={"q": "Python"}).json() check("search", isinstance(q.get("leads"), list)) # перенос на доску r = c.post(f"/api/leads/{lid}/move", json={"to": bid}) check("move to board", r.status_code == 200 and r.json().get("col") == bid, r.text) r = c.post(f"/api/leads/{lid}/comments", json={"text": "Комментарий из e2e"}) check("comment", r.status_code == 200 and len(r.json().get("comments", [])) == 1) # демо-старение -> архив r = c.post("/api/demo/age-lead") check("age-lead", r.status_code == 200, r.text) leads = c.get("/api/leads").json()["items"] arch = c.get("/api/leads", params={"col": "archive"}).json()["items"] check("lead archived", any(l["id"] == lid for l in arch), f"leads={len(leads)}") # восстановление r = c.post(f"/api/leads/{lid}/restore") check("restore", r.status_code == 200 and r.json().get("col") in ("inbox", bid), r.text) # проекты: взять в работу card = c.post("/api/projects/take", json={"leadId": lid}).json() check("take to projects", bool(card.get("id")), str(card)[:120]) cid = card["id"] leads_after = c.get("/api/leads").json()["items"] check("lead gone from board", all(l["id"] != lid for l in leads_after)) # стадия + комментарий + ссылка + ТЗ r = c.post(f"/api/projects/{cid}/move", json={"stage": "reply"}) check("stage reply", r.status_code == 200 and r.json().get("stage") == "reply") r = c.post(f"/api/projects/{cid}/comments", json={"text": "Откликнулся"}) check("proj comment", r.status_code == 200) r = c.post(f"/api/projects/{cid}/links", json={"name": "Макет", "url": "figma.com/x"}) check("proj link", r.status_code == 200 and r.json().get("links", [])[0]["url"].startswith("https://")) r = c.patch(f"/api/projects/{cid}", json={"tzText": "ТЗ: интеграция с amoCRM"}) check("proj tz", r.status_code == 200 and r.json().get("tzText") == "ТЗ: интеграция с amoCRM") # файл (локальный fallback без MinIO) + скачивание r = c.post(f"/api/projects/{cid}/files", files=[("files", ("tz.pdf", b"%PDF-1.4 test", "application/pdf"))]) check("file upload", r.status_code == 200 and len(r.json().get("items", [])) == 1, r.text) file_id = r.json()["items"][0]["id"] dl = c.get(f"/api/projects/{cid}/files/{file_id}/download") check("file download", dl.status_code == 200 and dl.content == b"%PDF-1.4 test", dl.text[:80]) r = c.delete(f"/api/projects/{cid}/files/{file_id}") check("file remove", r.status_code == 200) # локальная карточка loc = c.post("/api/projects", json={"title": "", "stack": ["Go"]}).json() check("local card", loc.get("local") is True and loc.get("title") == "") # напоминание (hold) r = c.post(f"/api/projects/{cid}/move", json={"stage": "hold"}) check("stage hold", r.status_code == 200) at = int(time.time() * 1000) + 60000 r = c.post(f"/api/projects/{cid}/reminder", json={"at": at}) check("set reminder", r.status_code == 200 and r.json().get("reminder", {}).get("at") == at, r.text) rem = c.get("/api/projects/reminders").json()["items"] check("active reminders", any(x["id"] == cid for x in rem)) # настройки валюты и пересчёт r = c.patch("/api/settings", json={"targetCurrency": "RUB", "conversionOn": True}) check("settings patch", r.status_code == 200) rates = c.get("/api/rates").json() check("rates have USD", "USD" in rates.get("rates", {})) # mark-col-seen (новый эндпоинт) r = c.post("/api/leads/mark-col-seen", json={"col": "inbox"}) check("mark col seen", r.status_code == 200) # ручная полная очистка «Отклонено» (проектные карточки) r = c.post(f"/api/projects/{cid}/move", json={"stage": "rejected"}) check("stage rejected", r.status_code == 200) r = c.post("/api/projects/clear-rejected") check("clear rejected", r.status_code == 200 and r.json().get("cleared", 0) >= 1, r.text) gone = c.get("/api/projects").json()["items"] check("rejected gone", all(x["id"] != cid for x in gone)) # ручная полная очистка корзины (лиды дашборда) d2 = c.post("/api/demo/simulate-lead").json() c.post(f"/api/leads/{d2['id']}/trash") trash_items = c.get("/api/leads", params={"col": "trash"}).json()["items"] check("trash has lead", any(x["id"] == d2["id"] for x in trash_items)) r = c.post("/api/leads/clear-col", json={"col": "trash"}) check("clear trash", r.status_code == 200 and r.json().get("cleared", 0) >= 1, r.text) trash_items = c.get("/api/leads", params={"col": "trash"}).json()["items"] check("trash empty", len(trash_items) == 0) # архив: тот же эндпоинт (сейчас пуст — просто валидируем) r = c.post("/api/leads/clear-col", json={"col": "archive"}) check("clear archive", r.status_code == 200) # FTS rebuild r = c.post("/api/admin/fts/rebuild") check("fts rebuild", r.status_code == 200 and r.json().get("ready") is True, r.text) # статика фронтенда из dist page = c.get("/") check("spa served", page.status_code == 200 and "