Seitenleiste: Eintraege ausblendbar (Backend)

Zwei Ebenen in einer Tabelle, nach dem Vorbild von DashboardLayout:
user_id NULL ist die Vorgabe des Administrators, eine Zeile mit user_id
die zusaetzliche Auswahl eines Kontos. Beide Listen gelten zusammen - wer
etwas versteckt, versteckt es zusaetzlich; einblenden, was der
Administrator ausgeblendet hat, kann niemand. Das erspart einen dritten
Zustand je Eintrag.

Als Schluessel dient der Routenpfad; der ist ohnehin eindeutig, eine
zweite Benennung waere nur eine weitere Stelle zum Auseinanderlaufen.

Drei Routen: lesen (Vorgabe, eigene Auswahl und die fertige Vereinigung
in einem Zug), eigene Auswahl setzen, Vorgabe setzen (nur Admin).

Die Eindeutigkeit laeuft ueber einen Index auf coalesce(user_id, -1).
Ein schlichtes UNIQUE(user_id) haette zwei Vorgabezeilen zugelassen, weil
NULL in SQL nie gleich NULL ist - das war im Entwurf noch falsch.

Versteckt wird nur der Eintrag, nicht die Seite: Lesezeichen,
Dashboard-Karten und Lagerort-QR sollen weiter funktionieren.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-10-28 20:27:14 +01:00
co-authored by Claude Opus 5
parent 8f5d679d9d
commit be267e70a5
5 changed files with 305 additions and 0 deletions
+137
View File
@@ -0,0 +1,137 @@
"""Seitenleiste ausblenden: Vorgabe des Admins, eigene Auswahl, Vereinigung."""
import json
import pytest
from fastapi import HTTPException
from sqlalchemy.exc import IntegrityError
from app.models import NavigationSichtbarkeit, Role, User
from app.routers.navigation import (
eigene_auswahl_setzen,
navigation_lesen,
vorgabe_setzen,
)
from app.schemas import NavigationSetzen
@pytest.fixture()
def admin(db):
person = User(username="admin", password_hash="x", role=Role.admin)
db.add(person)
db.commit()
db.refresh(person)
return person
@pytest.fixture()
def mitbewohner(db):
person = User(username="mitbewohner", password_hash="x", role=Role.user)
db.add(person)
db.commit()
db.refresh(person)
return person
def _setze_vorgabe(db, admin, werte):
return vorgabe_setzen(NavigationSetzen(versteckt=werte), db=db, user=admin)
def _setze_eigen(db, user, werte):
return eigene_auswahl_setzen(NavigationSetzen(versteckt=werte), db=db, user=user)
# ---- Vorgabe ----
def test_ohne_eintrag_ist_nichts_versteckt(db, admin):
out = navigation_lesen(db=db, user=admin)
assert out.vorgabe == [] and out.eigen == [] and out.versteckt == []
def test_vorgabe_gilt_fuer_alle(db, admin, mitbewohner):
_setze_vorgabe(db, admin, ["/rezepte"])
assert navigation_lesen(db=db, user=mitbewohner).versteckt == ["/rezepte"]
assert navigation_lesen(db=db, user=admin).versteckt == ["/rezepte"]
def test_normaler_nutzer_darf_die_vorgabe_nicht_setzen(db, mitbewohner):
"""require_admin greift – geprueft ueber die Rolle, wie in deps.py."""
from app.deps import require_admin
with pytest.raises(HTTPException) as fehler:
require_admin(mitbewohner)
assert fehler.value.status_code == 403
# ---- Eigene Auswahl ----
def test_eigene_auswahl_betrifft_nur_einen(db, admin, mitbewohner):
_setze_eigen(db, mitbewohner, ["/history"])
assert navigation_lesen(db=db, user=mitbewohner).versteckt == ["/history"]
assert navigation_lesen(db=db, user=admin).versteckt == []
def test_eigene_auswahl_kommt_zur_vorgabe_dazu(db, admin, mitbewohner):
_setze_vorgabe(db, admin, ["/rezepte"])
_setze_eigen(db, mitbewohner, ["/history"])
out = navigation_lesen(db=db, user=mitbewohner)
assert out.vorgabe == ["/rezepte"]
assert out.eigen == ["/history"]
assert out.versteckt == ["/history", "/rezepte"]
def test_nutzer_kann_die_vorgabe_nicht_aufheben(db, admin, mitbewohner):
"""Die eigene Auswahl addiert nur – sie nimmt nichts weg."""
_setze_vorgabe(db, admin, ["/shopping"])
_setze_eigen(db, mitbewohner, [])
assert navigation_lesen(db=db, user=mitbewohner).versteckt == ["/shopping"]
# ---- Speicherung ----
def test_zweimal_setzen_legt_keine_zweite_zeile_an(db, mitbewohner):
_setze_eigen(db, mitbewohner, ["/history"])
_setze_eigen(db, mitbewohner, ["/verliehen"])
zeilen = db.query(NavigationSichtbarkeit).all()
assert len(zeilen) == 1
assert json.loads(zeilen[0].versteckt) == ["/verliehen"]
def test_nur_eine_vorgabezeile(db, admin):
"""Der Teilindex muss eine zweite Vorgabe verhindern."""
_setze_vorgabe(db, admin, ["/rezepte"])
db.add(NavigationSichtbarkeit(user_id=None, versteckt='["/shopping"]'))
with pytest.raises(IntegrityError):
db.commit()
db.rollback()
def test_nur_eine_zeile_je_konto(db, mitbewohner):
_setze_eigen(db, mitbewohner, ["/history"])
db.add(NavigationSichtbarkeit(user_id=mitbewohner.id, versteckt="[]"))
with pytest.raises(IntegrityError):
db.commit()
db.rollback()
def test_doppelte_und_leere_eintraege_fallen_weg(db, mitbewohner):
out = _setze_eigen(db, mitbewohner, ["/history", "/history", "", " ", "/verliehen"])
assert out.eigen == ["/history", "/verliehen"]
def test_kaputter_inhalt_sperrt_niemanden_aus(db, mitbewohner):
"""Lieber die volle Leiste als eine Fehlermeldung."""
db.add(NavigationSichtbarkeit(user_id=mitbewohner.id, versteckt="kein JSON"))
db.commit()
assert navigation_lesen(db=db, user=mitbewohner).versteckt == []
def test_unbekannter_schluessel_wird_mitgefuehrt_aber_stoert_nicht(db, mitbewohner):
"""Ein umbenannter Eintrag darf die Leiste nicht zerreissen – der Server
kennt die Pfade gar nicht, die Oberflaeche ignoriert Unbekanntes."""
out = _setze_eigen(db, mitbewohner, ["/gibtsnichtmehr"])
assert out.versteckt == ["/gibtsnichtmehr"]