"""Tests zum Zugriffsschutz. Grundsatz: lesen darf jeder, schreiben nur der Betreiber - mit der einen Ausnahme des Reservierens. """ import io import pytest from PIL import Image from app import security from app.config import einstellungen from tests.conftest import TEST_PASSWORT def bild_bytes() -> bytes: puffer = io.BytesIO() Image.new("RGB", (400, 300), (10, 10, 200)).save(puffer, "JPEG") return puffer.getvalue() # ------------------------------------------------------------- Anmeldung --- def test_anmelden_und_abmelden(gast): assert gast.get("/api/v1/auth/me").json()["angemeldet"] is False antwort = gast.post("/api/v1/auth/login", json={"passwort": TEST_PASSWORT}) assert antwort.status_code == 200 assert gast.get("/api/v1/auth/me").json()["angemeldet"] is True gast.post("/api/v1/auth/logout") assert gast.get("/api/v1/auth/me").json()["angemeldet"] is False def test_falsches_passwort(gast): antwort = gast.post("/api/v1/auth/login", json={"passwort": "daneben"}) assert antwort.status_code == 401 assert gast.get("/api/v1/auth/me").json()["angemeldet"] is False def test_ohne_passwort_ist_der_bereich_gesperrt(gast, monkeypatch, kategorie_id): """Ohne hinterlegtes Passwort bleibt gesperrt statt offen zu stehen.""" monkeypatch.delenv("ADMIN_PASSWORD", raising=False) einstellungen.cache_clear() security.hash_zwischenspeicher_leeren() assert gast.post("/api/v1/auth/login", json={"passwort": "x"}).status_code == 503 antwort = gast.post("/api/v1/items", json={"size": "80", "category_id": kategorie_id}) assert antwort.status_code == 503 # Die Galerie funktioniert trotzdem assert gast.get("/api/v1/items").status_code == 200 # ------------------------------------------------------- Schreibzugriffe --- @pytest.mark.parametrize( "methode,pfad,rumpf", [ ("post", "/api/v1/items", {"json": {"size": "80"}}), ("patch", "/api/v1/items/1", {"json": {"size": "92"}}), ("delete", "/api/v1/items/1", {}), ("post", "/api/v1/items/1/mark-given", {}), ("post", "/api/v1/images/1/primary", {}), ("delete", "/api/v1/images/1", {}), ], ) def test_schreiben_ohne_anmeldung_wird_abgewiesen(gast, methode, pfad, rumpf): antwort = getattr(gast, methode)(pfad, **rumpf) assert antwort.status_code == 401, f"{methode.upper()} {pfad} war offen!" def test_hochladen_ohne_anmeldung_wird_abgewiesen(gast): antwort = gast.post( "/api/v1/items/batch-upload", files={"dateien": ("a.jpg", bild_bytes(), "image/jpeg")}, ) assert antwort.status_code == 401 def test_lesen_ist_offen(gast, client, kategorie_id): item = client.post( "/api/v1/items", json={"size": "80", "category_id": kategorie_id} ).json() client.post("/api/v1/auth/logout") assert gast.get("/api/v1/items").status_code == 200 assert gast.get(f"/api/v1/items/{item['id']}").status_code == 200 assert gast.get("/api/v1/categories").status_code == 200 assert gast.get("/api/v1/sizes").status_code == 200 def test_reservieren_bleibt_ohne_anmeldung_moeglich(gast, client, kategorie_id): """Die bewusste Ausnahme - Nachbarn sollen kein Konto brauchen.""" item = client.post( "/api/v1/items", json={"size": "80", "category_id": kategorie_id} ).json() client.post("/api/v1/auth/logout") antwort = gast.post( f"/api/v1/items/{item['id']}/reserve", json={"reserved_by": "Familie Meier"} ) assert antwort.status_code == 200 def test_betreiber_darf_ohne_token_freigeben(client, kategorie_id): """Damit eine Missbrauchswelle aufgeräumt werden kann.""" item = client.post( "/api/v1/items", json={"size": "80", "category_id": kategorie_id} ).json() client.post(f"/api/v1/items/{item['id']}/reserve", json={"reserved_by": "Wer auch immer"}) antwort = client.post(f"/api/v1/items/{item['id']}/release") assert antwort.status_code == 200 assert antwort.json()["status"] == "available" # ------------------------------------------------------------ Rate-Limit --- def test_reservieren_ist_begrenzt(gast, client, kategorie_id): """Bei offener Galerie die einzige Bremse vor dem Bestand. Geprüft wird der tatsächlich ausgelieferte Standardwert (5/Stunde), nicht ein für den Test heruntergesetzter: sonst sagte der Test nichts darüber aus, was im Betrieb gilt. Ein erster Anlauf mit genau fünf Anfragen gegen ein Limit von fünf konnte gar nichts zeigen. """ assert einstellungen().limit_reservieren == "5/hour" ids = [ client.post("/api/v1/items", json={"size": "80", "category_id": kategorie_id}).json()["id"] for _ in range(8) ] client.post("/api/v1/auth/logout") ergebnisse = [ gast.post(f"/api/v1/items/{i}/reserve", json={"reserved_by": "Skript"}).status_code for i in ids ] assert ergebnisse[:5] == [200] * 5, f"unerwartet früh gebremst: {ergebnisse}" assert ergebnisse[5:] == [429] * 3, f"Rate-Limit griff nicht: {ergebnisse}" def test_anmelden_ist_begrenzt(gast): """Ein Konto, aus dem Internet erreichbar - ohne Bremse liesse sich das Passwort in Ruhe durchprobieren.""" assert einstellungen().limit_anmelden == "10/hour" codes = [ gast.post("/api/v1/auth/login", json={"passwort": "falsch"}).status_code for _ in range(13) ] assert codes[:10] == [401] * 10, f"unerwartet früh gebremst: {codes}" assert 429 in codes[10:], f"Passwort liess sich unbegrenzt durchprobieren: {codes}" # --------------------------------------------------------------- Header --- def test_sicherheits_header(gast): kopf = gast.get("/api/v1/items").headers assert "script-src 'self'" in kopf["content-security-policy"] assert kopf["x-content-type-options"] == "nosniff" assert kopf["x-frame-options"] == "DENY" assert "noindex" in kopf["x-robots-tag"] def test_robots_txt_sperrt_suchmaschinen(gast): antwort = gast.get("/robots.txt") assert antwort.status_code == 200 assert "Disallow: /" in antwort.text