Files
planer/backend/tests/test_routes.py
2026-07-23 11:54:31 +02:00

119 lines
4.5 KiB
Python

"""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