Решение по TD-STYLE-ANALYZERS: LF — инструменты проекта (Python/Node) пишут LF, CRLF-.sh не работают на Linux CI (sh scripts/ci.sh), большинство файлов уже были LF. Добавлен .gitattributes (* text=auto eol=lf, бинарные исключения), .editorconfig переведён на lf, 1029 файлов конвертированы, git add --renormalize. Из индекса убраны закравшиеся archive/**/__pycache__/*.pyc.
176 lines
8.1 KiB
Python
176 lines
8.1 KiB
Python
"""Временный 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()
|