"""Mealie-Anbindung: Anmelden, Dauertoken holen, und das Passwort vergessen. Ohne Netz: ``httpx.MockTransport`` liefert die Antworten, die eine echte Mealie-Instanz geben würde. """ import json import httpx import pytest from fastapi import HTTPException from app.models import IntegrationSecret, Role, Setting, User from app.routers.mealie import ( mealie_available, mealie_konfigurieren, mealie_status, mealie_token_setzen, mealie_trennen, ) from app.routers.settings import list_settings from app.schemas import MealieConfigIn, MealieTokenIn from app.services import mealie @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(autouse=True) def sauber(): """Caches und Transport zwischen den Tests zuruecksetzen.""" mealie.cache_leeren() yield mealie.setze_transport(None) mealie.cache_leeren() def _mealie_server(*, passwort_ok=True, token_ok=True, erreichbar=True): """Eine Mealie-Instanz nachstellen. Merkt sich, was sie gesehen hat.""" gesehen: list[httpx.Request] = [] def handler(request: httpx.Request) -> httpx.Response: gesehen.append(request) if not erreichbar: raise httpx.ConnectError("keine Verbindung", request=request) pfad = request.url.path if pfad == "/api/auth/token": if not passwort_ok: return httpx.Response(401, json={"detail": "Unauthorized"}) return httpx.Response(200, json={ "access_token": "kurzlebig", "token_type": "bearer", "expires_in": 3600, }) if pfad == "/api/users/api-tokens": if not token_ok: return httpx.Response(403, json={"detail": "nope"}) return httpx.Response(201, json={"token": "dauertoken-xyz", "name": "Vorrania"}) if pfad == "/api/app/about": return httpx.Response(200, json={"version": "2.4.1"}) if pfad == "/api/recipes": return httpx.Response(200, json={"items": [], "total": 0, "page": 1}) if pfad == "/api/households/shopping/lists": return httpx.Response(200, json={"items": []}) return httpx.Response(404, json={"detail": "Not found"}) mealie.setze_transport(httpx.MockTransport(handler)) return gesehen # -------------------------------------------------------------------------- # Einrichten # -------------------------------------------------------------------------- def test_einrichten_holt_ein_dauertoken(db, admin): _mealie_server() status = mealie_konfigurieren( MealieConfigIn(base_url="https://mealie.example.com", username="guido", password="geheim"), db=db, _=admin, ) assert status.configured is True assert status.token_set is True assert status.base_url == "https://mealie.example.com" assert status.reachable is True assert status.version == "2.4.1" assert db.get(IntegrationSecret, "mealie").secret == "dauertoken-xyz" def test_passwort_wird_nirgends_gespeichert(db, admin): """Der Kern der Zusage: Das Passwort sieht nur die Anmeldung.""" _mealie_server() mealie_konfigurieren( MealieConfigIn(base_url="https://mealie.example.com", username="guido", password="streng-geheim"), db=db, _=admin, ) alle_werte = [s.value for s in db.query(Setting).all()] alle_werte += [s.secret for s in db.query(IntegrationSecret).all()] assert all("streng-geheim" not in wert for wert in alle_werte) def test_passwort_steht_nicht_im_fehlertext(db, admin): _mealie_server(passwort_ok=False) with pytest.raises(HTTPException) as fehler: mealie_konfigurieren( MealieConfigIn(base_url="https://mealie.example.com", username="guido", password="streng-geheim"), db=db, _=admin, ) assert fehler.value.status_code == 400 assert "streng-geheim" not in str(fehler.value.detail) def test_adresse_wird_ohne_schraegstrich_abgelegt(db, admin): _mealie_server() mealie_konfigurieren( MealieConfigIn(base_url="https://mealie.example.com/", username="g", password="p"), db=db, _=admin, ) assert db.get(Setting, "mealie_base_url").value == "https://mealie.example.com" def test_falsches_passwort_wird_als_solches_gemeldet(db, admin): _mealie_server(passwort_ok=False) with pytest.raises(HTTPException) as fehler: mealie_konfigurieren( MealieConfigIn(base_url="https://m.example.com", username="g", password="p"), db=db, _=admin, ) assert fehler.value.status_code == 400 assert "Passwort" in str(fehler.value.detail) assert db.get(IntegrationSecret, "mealie") is None def test_server_weg_ist_kein_passwortfehler(db, admin): """Unterscheidung, die der Nutzer braucht: 502 statt 400.""" _mealie_server(erreichbar=False) with pytest.raises(HTTPException) as fehler: mealie_konfigurieren( MealieConfigIn(base_url="https://m.example.com", username="g", password="p"), db=db, _=admin, ) assert fehler.value.status_code == 502 def test_kein_dauertoken_wird_gemeldet(db, admin): _mealie_server(token_ok=False) with pytest.raises(HTTPException) as fehler: mealie_konfigurieren( MealieConfigIn(base_url="https://m.example.com", username="g", password="p"), db=db, _=admin, ) assert fehler.value.status_code == 502 assert db.get(IntegrationSecret, "mealie") is None def test_eigenes_token_einsetzen(db, admin): _mealie_server() status = mealie_token_setzen( MealieTokenIn(base_url="https://m.example.com", token="selbst-erzeugt"), db=db, _=admin, ) assert status.configured is True assert db.get(IntegrationSecret, "mealie").secret == "selbst-erzeugt" # -------------------------------------------------------------------------- # Auskunft – und was sie NICHT verrät # -------------------------------------------------------------------------- def test_status_gibt_das_token_nie_zurueck(db, admin): _mealie_server() mealie_konfigurieren( MealieConfigIn(base_url="https://m.example.com", username="g", password="p"), db=db, _=admin, ) status = mealie_status(db=db, _=admin) assert status.token_set is True assert "dauertoken-xyz" not in json.dumps(status.model_dump(mode="json")) def test_einstellungen_enthalten_das_token_nicht(db, admin): """GET /settings liest jeder angemeldete Nutzer – deshalb eigene Tabelle.""" _mealie_server() mealie_konfigurieren( MealieConfigIn(base_url="https://m.example.com", username="g", password="p"), db=db, _=admin, ) eintraege = list_settings(db=db, _=admin) assert all("dauertoken-xyz" not in e.value for e in eintraege) # Die Adresse darf dort stehen, sie ist kein Geheimnis. assert any(e.key == "mealie_base_url" for e in eintraege) def test_ohne_einrichtung_ist_nichts_verfuegbar(db, admin): assert mealie_available(db=db, _=admin).available is False status = mealie_status(db=db, _=admin) assert status.configured is False assert status.reachable is None # gar nicht erst nachgefragt def test_nach_einrichtung_ist_es_verfuegbar(db, admin): _mealie_server() mealie_konfigurieren( MealieConfigIn(base_url="https://m.example.com", username="g", password="p"), db=db, _=admin, ) assert mealie_available(db=db, _=admin).available is True def test_trennen_entfernt_adresse_und_token(db, admin): _mealie_server() mealie_konfigurieren( MealieConfigIn(base_url="https://m.example.com", username="g", password="p"), db=db, _=admin, ) mealie_trennen(db=db, _=admin) assert db.get(IntegrationSecret, "mealie") is None assert db.get(Setting, "mealie_base_url") is None assert mealie_available(db=db, _=admin).available is False def test_abgelaufenes_token_meldet_sich_deutlich(db, admin): def handler(request: httpx.Request) -> httpx.Response: return httpx.Response(401, json={"detail": "expired"}) db.add(Setting(key="mealie_base_url", value="https://m.example.com")) db.add(IntegrationSecret(service="mealie", secret="alt")) db.commit() mealie.setze_transport(httpx.MockTransport(handler)) status = mealie_status(pruefen=True, db=db, _=admin) assert status.reachable is False assert "Token" in (status.error or "") # -------------------------------------------------------------------------- # Fassungsunterschied 1.x / 2.x # -------------------------------------------------------------------------- def test_erkennt_die_haushalts_pfade(db, admin): _mealie_server() mealie_konfigurieren( MealieConfigIn(base_url="https://m.example.com", username="g", password="p"), db=db, _=admin, ) assert mealie.pfadvariante(db) == "households" def test_faellt_auf_die_gruppen_pfade_zurueck(db, admin): """Mealie 1.x kennt /api/households/ nicht.""" def handler(request: httpx.Request) -> httpx.Response: if request.url.path.startswith("/api/households/"): return httpx.Response(404, json={"detail": "Not Found"}) return httpx.Response(200, json={"items": []}) db.add(Setting(key="mealie_base_url", value="https://alt.example.com")) db.add(IntegrationSecret(service="mealie", secret="t")) db.commit() mealie.setze_transport(httpx.MockTransport(handler)) assert mealie.pfadvariante(db) == "groups" def test_pfadvariante_wird_gepuffert(db, admin): gesehen = _mealie_server() db.add(Setting(key="mealie_base_url", value="https://m.example.com")) db.add(IntegrationSecret(service="mealie", secret="t")) db.commit() mealie.pfadvariante(db) anzahl = len(gesehen) mealie.pfadvariante(db) assert len(gesehen) == anzahl # keine zweite Anfrage