refactor: form-core wiring, JWT sessions, slowapi, packaging, frontend rewrite, test declutter

This commit is contained in:
Jonas Miederer 2026-09-28 18:03:07 +02:00
parent 06ab25fb47
commit 03af41e837
74 changed files with 6089 additions and 5183 deletions

View file

@ -19,6 +19,8 @@ os.environ["DATABASE_URL"] = f"sqlite:///{TEST_AREA / 'test.db'}"
os.environ["FLOWS_DIR"] = str(TEST_AREA / "flows")
# Flow callbacks (POST /api/approvals) authenticate with this token.
os.environ["CALLBACK_TOKEN"] = "test-callback-token"
# Stateless session JWTs are signed with this (HS256).
os.environ["JWT_SECRET"] = "test-jwt-secret-32-chars-minimum!!"
# The embedded watcher + startup sync stay off in tests; sync is exercised
# directly (sync_from_directory) with per-test tmp dirs instead.
os.environ["FLOWS_WATCH_ENABLED"] = "false"
@ -93,13 +95,11 @@ def login(client, session):
role: str = "admin",
tenant_id: str = "default",
):
from datetime import UTC, datetime, timedelta
from uuid import uuid4
from sqlmodel import select
from app.auth import service
from app.auth.deps import SESSION_COOKIE
from app.models import User, UserSession
from app.models import User
# Idempotent: re-login updates the existing row (tests switch
# identity mid-test by calling login() again).
@ -111,15 +111,9 @@ def login(client, session):
user.role = role
user.tenant_id = tenant_id
session.add(user)
session.flush()
user_session = UserSession(
id=f"test-session-{uuid4().hex}",
user_id=user.id,
expires_at=datetime.now(UTC) + timedelta(days=1),
)
session.add(user_session)
session.commit()
client.cookies.set(SESSION_COOKIE, user_session.id)
session.refresh(user)
client.cookies.set(SESSION_COOKIE, service.mint_session_token(user))
return user
return _login
@ -132,76 +126,33 @@ def lfx_server(tmp_path_factory):
Serves an (initially seeded) flow dir; tests upload fixture flows through
the same deploy path production uses.
"""
import shutil
import socket
import subprocess
import sys
import time
import urllib.request
from pathlib import Path
from app.settings import settings
bindir = Path(sys.executable).parent
lfx_bin = shutil.which("lfx", path=str(bindir)) or "lfx"
sock = socket.socket()
sock.bind(("127.0.0.1", 0))
port = sock.getsockname()[1]
sock.close()
from ._harness import TEST_LFX_KEY, free_port, spawn_lfx, wait_healthy
port = free_port()
flow_dir = tmp_path_factory.mktemp("lfx-flows")
seed = (Path(__file__).parent / "fixtures" / "flows" / "simple_rule.json").read_text()
(flow_dir / "seed.json").write_text(seed)
env = {
**os.environ,
# lfx serve refuses to start without a serve key; requests need it too.
# get_api_key() prefers LANGFLOW_API_KEY over LFX_API_KEY, and the repo
# .env may put a real Langflow key in the environment — clear it so the
# test key below is the one the client sends.
"LANGFLOW_API_KEY": "",
"LFX_API_KEY": "test-lfx-key",
}
proc = subprocess.Popen(
[
lfx_bin,
"serve",
"--host",
"127.0.0.1",
"--port",
str(port),
"--flow-dir",
str(flow_dir),
str(flow_dir),
],
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True,
env=env,
)
proc = spawn_lfx(flow_dir, port)
url = f"http://127.0.0.1:{port}"
deadline = time.time() + 240
healthy = False
while time.time() < deadline:
try:
wait_healthy(url)
except Exception:
if proc.poll() is not None:
out = proc.stdout.read() if proc.stdout else ""
raise RuntimeError(f"lfx serve exited early:\n{out[-4000:]}")
try:
with urllib.request.urlopen(url + "/health", timeout=3) as response:
if response.status == 200:
healthy = True
break
except OSError:
time.sleep(1)
if not healthy:
proc.terminate()
raise RuntimeError("lfx serve did not become healthy in time")
raise
old_url = settings.lfx_url
old_key = settings.lfx_api_key
settings.lfx_url = url
settings.lfx_api_key = "test-lfx-key"
settings.lfx_api_key = TEST_LFX_KEY
yield url
settings.lfx_url = old_url
settings.lfx_api_key = old_key
@ -259,21 +210,11 @@ def override_langflow():
def live_server():
"""Real uvicorn server (ephemeral port) so LFX flows can call back
into our API over HTTP, exactly like production."""
import threading
import time
import uvicorn
from app.main import app
config = uvicorn.Config(app, host="127.0.0.1", port=0, log_level="warning")
server = uvicorn.Server(config)
thread = threading.Thread(target=server.run, daemon=True)
thread.start()
deadline = time.time() + 30
while not server.started and time.time() < deadline:
time.sleep(0.05)
assert server.started, "uvicorn test server did not start"
from ._harness import spawn_uvicorn
server, thread = spawn_uvicorn(app)
port = server.servers[0].sockets[0].getsockname()[1]
yield f"http://127.0.0.1:{port}"
server.should_exit = True
@ -293,12 +234,20 @@ def load_flow_fixture(name: str) -> dict:
def langflow_export_doc(name: str = "fake", flow_id: str = "fake-1") -> dict:
"""Minimal Langflow export envelope (merges _export_doc/_langflow_doc)."""
"""Full Langflow export envelope (merges _export_doc/_langflow_doc).
Carries real fixture flow data so import/deploy paths exercise LFX.
"""
fixture = load_flow_fixture("simple_rule.json")
return {
"id": flow_id,
"name": name,
"description": "",
"data": {"nodes": [], "edges": []},
"description": "aus Langflow",
"endpoint_name": None,
"data": fixture["data"],
"tags": [],
"webhook": False,
"is_component": False,
}