"""API-Routen: Lesen + Scan-Start über httpx/ASGITransport (gleicher Event-Loop wie create_task und der DB-Write-Lock — fastapi.TestClient wäre die Loop-Falle).""" import asyncio import httpx import pytest import config import scan_dienst @pytest.fixture async def client(gescanntes_repo, monkeypatch, tmp_path): wurzel, git = gescanntes_repo datei = tmp_path / "projekte.txt" datei.write_text(f"{wurzel}\n{tmp_path}/tot\n") monkeypatch.setattr(config, "PROJEKTE_DATEI", datei) scan_dienst._laeufe.clear() from server import app transport = httpx.ASGITransport(app=app) async with httpx.AsyncClient(transport=transport, base_url="http://test") as c: yield c, wurzel, git async def test_projektliste(client): c, wurzel, _ = client r = await c.get("/api/projekte") assert r.status_code == 200 eintraege = {p["name"]: p for p in r.json()} assert eintraege["mini"]["hat_planer"] is True assert eintraege["mini"]["drift"] == [] assert eintraege["mini"]["scan_laeuft"] is False assert "fehler" in eintraege["tot"] async def test_projekt_detail(client): c, _, _ = client r = await c.get("/api/projekte/mini") daten = r.json() assert daten["kern"]["titel"] == "mini" assert daten["bereiche"] and daten["bereiche"][0]["features"] assert "rechner.py::addiere" in daten["einheiten"] assert daten["doku_ordnung"] assert (await c.get("/api/projekte/gibtsnicht")).status_code == 404 async def test_einheit_mit_code(client): c, _, _ = client r = await c.get("/api/projekte/mini/einheit", params={"id": "rechner.py::addiere"}) e = r.json() assert "return a + b" in e["code"] assert e["veraltet"] is False assert (await c.get("/api/projekte/mini/einheit", params={"id": "x.py::y"})).status_code == 404 async def test_drift_nach_commit(client): c, wurzel, git = client (wurzel / "rechner.py").write_text((wurzel / "rechner.py").read_text() + "\nX9 = 9\n") git("add", "-A") git("commit", "-m", "drift") r = await c.get("/api/projekte") mini = next(p for p in r.json() if p["name"] == "mini") assert mini["drift"] == ["rechner.py"] e = (await c.get("/api/projekte/mini/einheit", params={"id": "rechner.py::addiere"})).json() assert e["veraltet"] is True async def test_scan_starten_und_pollen(client): c, _, _ = client r = await c.post("/api/projekte/mini/scan", json={"art": "rescan"}) assert r.json()["status"] == "gestartet" doppel = await c.post("/api/projekte/mini/scan", json={"art": "rescan"}) assert doppel.json()["status"] in ("laeuft", "gestartet") # idempotent, nie Doppellauf for _ in range(100): s = (await c.get("/api/projekte/mini/scan-status")).json() if not s["laeuft"]: break await asyncio.sleep(0.1) assert s["ergebnis"]["status"] in ("ok", "unveraendert") async def test_scan_409_bei_dirty_tree(client): c, wurzel, _ = client (wurzel / "schmutz.py").write_text("X = 1\n") r = await c.post("/api/projekte/mini/scan", json={"art": "scan"}) assert r.status_code == 409 assert "uncommittete" in r.json()["detail"] async def test_fs_browse(client, tmp_path): c, _, _ = client (tmp_path / "unter" / ".planer").mkdir(parents=True) (tmp_path / ".versteckt").mkdir() r = await c.get("/api/fs", params={"pfad": str(tmp_path)}) daten = r.json() assert daten["pfad"] == str(tmp_path.resolve()) assert daten["eltern"] == str(tmp_path.resolve().parent) ordner = {o["name"]: o for o in daten["ordner"]} assert ordner["unter"]["hat_planer"] is True assert ".versteckt" not in ordner assert (await c.get("/api/fs", params={"pfad": str(tmp_path / "fehlt")})).status_code == 404 async def test_projekt_hinzufuegen_und_entfernen(client, tmp_path): c, _, _ = client neu = tmp_path / "neues-repo" (neu / ".planer").mkdir(parents=True) assert (await c.post("/api/projekte", json={"pfad": str(neu)})).status_code == 200 namen = [p["name"] for p in (await c.get("/api/projekte")).json()] assert "neues-repo" in namen and "mini" in namen assert (await c.post("/api/projekte", json={"pfad": str(neu)})).status_code == 409 assert (await c.delete("/api/projekte/neues-repo")).status_code == 200 namen = [p["name"] for p in (await c.get("/api/projekte")).json()] assert "neues-repo" not in namen and "mini" in namen assert (neu / ".planer").is_dir() # Artefakte bleiben erhalten assert (await c.delete("/api/projekte/neues-repo")).status_code == 404