Files
Deal/archive/leadradar-legacy/backend/devtests/e2e_test.py
T
Rustam Khalimov 9e07568ddd Инициализировать репозиторий «Дейл»
Первый коммит: модульный монолит ядра (.NET 10) и gRPC-сервисы
ai/ml/telegram, фронтенд Vue 3/Vite/Tailwind, документация (ТЗ,
инструкция пользователя, техдокументация, код-стайл), бэклог,
скрипты развёртывания и архив прототипа LeadRadar.
2026-09-11 02:50:17 +03:00

176 lines
8.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Временный 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 "<div id=\"app\">" in page.text)
print("E2E OK" if ok else "E2E FAILED")
finally:
server.terminate()
try:
server.wait(timeout=10)
except Exception:
server.kill()