init
This commit is contained in:
27
backend/tests/conftest.py
Normal file
27
backend/tests/conftest.py
Normal 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
|
||||
80
backend/tests/test_features.py
Normal file
80
backend/tests/test_features.py
Normal 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")
|
||||
92
backend/tests/test_parser.py
Normal file
92
backend/tests/test_parser.py
Normal 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"
|
||||
59
backend/tests/test_routes.py
Normal file
59
backend/tests/test_routes.py
Normal 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
|
||||
Reference in New Issue
Block a user