This commit is contained in:
Team3
2026-07-24 22:39:23 +02:00
commit 92889711a0
27 changed files with 2640 additions and 0 deletions

27
backend/tests/conftest.py Normal file
View File

@@ -0,0 +1,27 @@
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
import httpx
import pytest
import config
@pytest.fixture
def datenordner(tmp_path, monkeypatch):
"""Leitet DATEN_DIR auf einen frischen Ordner um."""
ordner = tmp_path / "daten"
ordner.mkdir()
monkeypatch.setattr(config, "DATEN_DIR", ordner)
return ordner
@pytest.fixture
async def client(datenordner):
from server import app
transport = httpx.ASGITransport(app=app)
async with httpx.AsyncClient(transport=transport, base_url="http://test") as c:
yield c

View File

@@ -0,0 +1,80 @@
import pytest
import features
import projekte
@pytest.fixture
def projekt(datenordner):
projekte.anlegen("Test")
return "Test"
def test_anlegen_erzeugt_slug(projekt, datenordner):
f = features.anlegen(projekt, "<!-- titel -->\nÜber Ätzend!\n")
assert f["id"] == "ueber-aetzend"
assert (datenordner / "Test" / "ueber-aetzend.md").is_file()
def test_slug_kollision(projekt):
a = features.anlegen(projekt, "<!-- titel -->\nGleich\n")
b = features.anlegen(projekt, "<!-- titel -->\nGleich\n")
assert a["id"] == "gleich"
assert b["id"] == "gleich-2"
def test_titel_pflicht(projekt):
with pytest.raises(ValueError):
features.anlegen(projekt, "nur Text ohne Titel-Marker")
def test_titelaenderung_benennt_datei_um(projekt, datenordner):
f = features.anlegen(projekt, "<!-- titel -->\nAlt\n")
g = features.speichern(projekt, f["id"], "<!-- titel -->\nNeu\n")
assert g["id"] == "neu"
assert not (datenordner / "Test" / "alt.md").exists()
assert (datenordner / "Test" / "neu.md").is_file()
def test_umbenennen_mit_kollision(projekt):
features.anlegen(projekt, "<!-- titel -->\nZiel\n")
f = features.anlegen(projekt, "<!-- titel -->\nAnders\n")
g = features.speichern(projekt, f["id"], "<!-- titel -->\nZiel\n")
assert g["id"] == "ziel-2"
def test_gleicher_titel_behaelt_kollisions_suffix(projekt):
features.anlegen(projekt, "<!-- titel -->\nGleich\n")
f = features.anlegen(projekt, "<!-- titel -->\nGleich\n")
g = features.speichern(projekt, f["id"], "<!-- titel -->\nGleich\n<!-- ziel -->\nx\n")
assert g["id"] == "gleich-2"
def test_speichern_verbatim(projekt, datenordner):
f = features.anlegen(projekt, "<!-- titel -->\nX\n")
roh = "Vorspann\n<!-- titel -->\nX\n<!-- fremd -->\n unangetastet \n"
features.speichern(projekt, f["id"], roh)
assert (datenordner / "Test" / "x.md").read_text() == roh
def test_liste_sortiert_nach_titel(projekt):
features.anlegen(projekt, "<!-- titel -->\nZebra\n")
features.anlegen(projekt, "<!-- titel -->\nanton\n")
titel = [f["sektionen"]["titel"] for f in features.liste(projekt)]
assert titel == ["anton", "Zebra"]
def test_loeschen(projekt):
f = features.anlegen(projekt, "<!-- titel -->\nWeg\n")
features.loeschen(projekt, f["id"])
assert features.liste(projekt) == []
def test_unbekannte_id(projekt):
with pytest.raises(FileNotFoundError):
features.speichern(projekt, "gibts-nicht", "x")
def test_unbekanntes_projekt(datenordner):
with pytest.raises(FileNotFoundError):
features.liste("Fremd")

View File

@@ -0,0 +1,92 @@
from parser import zerlege
VOLL = """<!-- titel -->
Login
----------------------------------------
<!-- ziel -->
Nutzer können sich anmelden.
----------------------------------------
<!-- funktion -->
Anmeldung mit E-Mail
<!-- regel -->
Passwort nie loggen
<!-- regel -->
Sperre nach 5 Fehlversuchen
<!-- funktion -->
Abmeldung
----------------------------------------
<!-- sonstiges -->
Offen: 2FA.
"""
def test_alle_sektionen():
s = zerlege(VOLL)
assert s["titel"] == "Login"
assert s["ziel"] == "Nutzer können sich anmelden."
assert s["funktionen"] == [
{"text": "Anmeldung mit E-Mail", "regeln": ["Passwort nie loggen", "Sperre nach 5 Fehlversuchen"]},
{"text": "Abmeldung", "regeln": []},
]
assert s["sonstiges"] == "Offen: 2FA."
def test_funktionen_roh_rekonstruiert_marker():
s = zerlege(VOLL)
assert s["funktionen_roh"] == (
"<!-- funktion -->\nAnmeldung mit E-Mail\n"
"<!-- regel -->\nPasswort nie loggen\n"
"<!-- regel -->\nSperre nach 5 Fehlversuchen\n"
"<!-- funktion -->\nAbmeldung"
)
def test_trenner_zeilen_werden_ignoriert():
s = zerlege("<!-- ziel -->\nText\n-----\n")
assert s["ziel"] == "Text"
def test_praeambel_landet_in_sonstiges():
s = zerlege("Vorspann ohne Marker\n<!-- titel -->\nX\n")
assert s["titel"] == "X"
assert s["sonstiges"] == "Vorspann ohne Marker"
def test_unbekannter_marker_landet_in_sonstiges():
s = zerlege("<!-- titel -->\nX\n<!-- notizen -->\nGeheim\n")
assert s["sonstiges"] == "Geheim"
def test_regel_ohne_funktion_landet_in_sonstiges():
s = zerlege("<!-- regel -->\nverwaist\n")
assert s["funktionen"] == []
assert s["sonstiges"] == "verwaist"
def test_doppelter_marker_wird_konkateniert():
s = zerlege("<!-- ziel -->\na\n<!-- titel -->\nX\n<!-- ziel -->\nb\n")
assert s["ziel"] == "a\n\nb"
def test_leerer_text():
s = zerlege("")
assert s == {"titel": "", "art": "kern", "ziel": "", "funktionen": [], "funktionen_roh": "", "sonstiges": ""}
def test_art_optional():
assert zerlege("<!-- art -->\nOptional\n")["art"] == "optional"
def test_art_default_und_unbekannter_wert():
assert zerlege("<!-- titel -->\nX\n")["art"] == "kern"
assert zerlege("<!-- art -->\nquatsch\n")["art"] == "kern"
def test_marker_mit_leerraum():
s = zerlege("<!-- titel -->\nX\n")
assert s["titel"] == "X"
def test_titel_erste_nicht_leere_zeile():
s = zerlege("<!-- titel -->\n\nErste\nZweite\n")
assert s["titel"] == "Erste"

View File

@@ -0,0 +1,59 @@
ROH = "<!-- titel -->\nLogin\n<!-- funktion -->\nAnmeldung\n<!-- regel -->\nPasswort nie loggen\n"
async def test_health(client):
res = await client.get("/api/health")
assert res.status_code == 200
assert res.json() == {"ok": True}
async def test_projekt_crud(client):
assert (await client.get("/api/projekte")).json() == []
res = await client.post("/api/projekte", json={"name": "Alpha"})
assert res.status_code == 200
# Duplikat → 409, ungültig → 400
assert (await client.post("/api/projekte", json={"name": "Alpha"})).status_code == 409
assert (await client.post("/api/projekte", json={"name": ""})).status_code == 400
assert (await client.post("/api/projekte", json={"name": "../boese"})).status_code == 400
res = await client.put("/api/projekte/Alpha", json={"name": "Beta"})
assert res.status_code == 200
assert res.json() == {"name": "Beta"}
assert (await client.put("/api/projekte/Alpha", json={"name": "X"})).status_code == 404
assert [p["name"] for p in (await client.get("/api/projekte")).json()] == ["Beta"]
assert (await client.delete("/api/projekte/Beta")).status_code == 200
assert (await client.delete("/api/projekte/Beta")).status_code == 404
async def test_feature_crud(client):
await client.post("/api/projekte", json={"name": "Alpha"})
res = await client.post("/api/projekte/Alpha/features", json={"roh": ROH})
assert res.status_code == 200
f = res.json()
assert f["id"] == "login"
assert f["roh"] == ROH
assert f["sektionen"]["titel"] == "Login"
# Titel fehlt → 400
res = await client.post("/api/projekte/Alpha/features", json={"roh": "kein Titel"})
assert res.status_code == 400
res = await client.put("/api/projekte/Alpha/features/login", json={"roh": "mehr\n" + ROH})
assert res.status_code == 200
assert res.json()["sektionen"]["sonstiges"] == "mehr"
liste = (await client.get("/api/projekte/Alpha/features")).json()
assert len(liste) == 1
assert (await client.delete("/api/projekte/Alpha/features/login")).status_code == 200
assert (await client.delete("/api/projekte/Alpha/features/login")).status_code == 404
async def test_unbekanntes_projekt(client):
assert (await client.get("/api/projekte/Fremd/features")).status_code == 404
assert (await client.post("/api/projekte/Fremd/features", json={"roh": ROH})).status_code == 404