"""Datenschutz: keine Metadaten in Bildern, private Listen bleiben privat, keine fremden Bilder, keine offene API-Doku.""" import io from fastapi.testclient import TestClient from PIL import Image, PngImagePlugin from app import db from app.config import DATA_DIR from app.main import app GPS = {1: "N", 2: (52.0, 31.0, 12.0), 3: "E", 4: (13.0, 24.0, 36.0)} def jpeg_mit_gps(width=60, height=30, orientation=None) -> bytes: exif = Image.Exif() exif[0x8825] = GPS exif[0x010F] = "Testkamera" if orientation: exif[0x0112] = orientation buf = io.BytesIO() Image.new("RGB", (width, height), "red").save(buf, "JPEG", exif=exif.tobytes()) return buf.getvalue() def upload(client, data: bytes) -> str: res = client.post("/api/uploads", content=data) assert res.status_code == 201, res.text return res.json()["file"] # ---------- Bilder ---------- def test_upload_ohne_gps_und_kamera(login): anna, ben = login("anna"), login("ben") name = upload(anna, jpeg_mit_gps()) img = Image.open(io.BytesIO(ben.get(f"/uploads/{name}").content)) assert not img.getexif() assert img.size == (60, 30) def test_hochkant_foto_bleibt_hochkant(login): # Orientierung 6 = 90° gedreht: ohne Anwenden läge das Foto nach dem Entfernen der EXIF-Daten quer name = upload(login("anna"), jpeg_mit_gps(60, 30, orientation=6)) assert Image.open(DATA_DIR / "uploads" / name).size == (30, 60) def test_png_ohne_textangaben(login): info = PngImagePlugin.PngInfo() info.add_text("Comment", "Aufgenommen in der Musterstraße 1") buf = io.BytesIO() Image.new("RGB", (10, 10)).save(buf, "PNG", pnginfo=info) name = upload(login("anna"), buf.getvalue()) assert not Image.open(DATA_DIR / "uploads" / name).text def test_kaputtes_bild_abgelehnt(login): res = login("anna").post("/api/uploads", content=b"\xff\xd8\xff" + b"kein bild" * 10) assert res.status_code == 400 # ---------- Private Listen ---------- def private_liste(anna): lid = anna.post("/api/haushalt/listen", json={"name": "Privat", "everyone": False}).json()["created"] anna.post("/api/haushalt/items", json={"list": "einkauf", "name": "Schwangerschaftstest", "listId": lid}) return lid def test_privates_produkt_nicht_in_fremden_vorschlaegen(login): anna, ben = login("anna"), login("ben") lid = private_liste(anna) assert "Schwangerschaftstest" in anna.get("/api/haushalt").json()["products"] assert "Schwangerschaftstest" not in ben.get("/api/haushalt").json()["products"] # Nach Freigabe sieht Ben es anna.patch(f"/api/haushalt/listen/{lid}", json={"name": "Privat", "everyone": False, "members": [ben.user["id"]]}) assert "Schwangerschaftstest" in ben.get("/api/haushalt").json()["products"] def test_rezept_zuordnung_verraet_nichts(login): anna, ben = login("anna"), login("ben") lid = anna.post("/api/haushalt/listen", json={"name": "Privat", "everyone": False}).json()["created"] anna.post("/api/haushalt/items", json={"list": "einkauf", "name": "Kondome", "listId": lid}) # Ähnliche Schreibweise würde sonst Annas privaten Eintrag als "bekanntes Produkt" zuordnen ing = ben.post("/api/haushalt/rezept", json={"text": "2 Kondom"}).json()["ingredients"][0] assert ing["known"] is False and ing["product"] != "Kondome" ing = anna.post("/api/haushalt/rezept", json={"text": "2 Kondom"}).json()["ingredients"][0] assert ing["known"] is True and ing["product"] == "Kondome" def test_vorschlag_loeschen_nur_in_eigenen_listen(login): anna, ben = login("anna"), login("ben") home = anna.get("/api/haushalt").json()["lists"][0]["id"] private_liste(anna) anna.post("/api/haushalt/items", json={"list": "einkauf", "name": "Schwangerschaftstest", "listId": home}) # Ben löscht den Vorschlag: weg aus "Zuhause", in Annas privater Liste bleibt er assert ben.delete("/api/haushalt/produkte", params={"name": "Schwangerschaftstest"}).status_code == 200 assert "Schwangerschaftstest" not in ben.get("/api/haushalt").json()["products"] assert "Schwangerschaftstest" in anna.get("/api/haushalt").json()["products"] # Ein Vorschlag, den Ben nicht sieht, lässt sich auch nicht löschen assert ben.delete("/api/haushalt/produkte", params={"name": "Schwangerschaftstest"}).status_code == 404 # ---------- Artikel, API-Doku ---------- def test_artikel_nur_eigene_bilder(login): anna = login("anna") name = upload(anna, jpeg_mit_gps()) html = f'

x

ok' content = anna.post("/api/projects", json={"title": "T", "content": html}).json()["content"] assert "tracker.example" not in content assert f"/uploads/{name}" in content def test_api_doku_nicht_oeffentlich(): client = TestClient(app) assert client.get("/api/openapi.json").status_code == 404 assert client.get("/api/docs").status_code == 404 # ---------- Umstellung bestehender Daten (Schritt 2) ---------- def test_umstellung_bestehender_daten(conn, login): anna = login("anna") home = anna.get("/api/haushalt").json()["lists"][0]["id"] with conn: # Stand vor Schritt 2: Katalog ohne Listen-Zuordnung, Artikel mit fremdem Bild, Foto mit GPS conn.execute("INSERT INTO shopping_lists (id, name, everyone, position, created_at, created_by) VALUES (99, 'P', 0, 2, 'x', ?)", (anna.user["id"],)) for key, name in (("geheim", "Geheim"), ("alt", "Alt")): conn.execute("INSERT INTO products (key, name, uses, last_used) VALUES (?, ?, 3, 'x')", (key, name)) conn.execute("INSERT INTO household_items (key, name, list, list_id, quantity, unit, updated_at) VALUES ('geheim', 'Geheim', 'einkauf', 99, 1, 'stk', 'x')") conn.execute("DELETE FROM list_products") conn.execute("""INSERT INTO projects (id, title, status, content, created_at, updated_at) VALUES ('a1', 'T', 'aktiv', '

x

', 'x', 'x')""") conn.execute("PRAGMA user_version = 1") foto = DATA_DIR / "uploads" / ("ab" * 12 + ".jpg") foto.write_bytes(jpeg_mit_gps()) db.init() rows = {tuple(r) for r in conn.execute("SELECT list_id, key FROM list_products")} assert rows == {(99, "geheim"), (home, "alt")} assert "tracker" not in conn.execute("SELECT content FROM projects WHERE id = 'a1'").fetchone()[0] assert not Image.open(foto).getexif() def test_problembilder_verhindern_start_nicht(conn): """Schreibgeschütztes Foto mit GPS und kaputte Datei: Umstellung läuft trotzdem durch.""" uploads = DATA_DIR / "uploads" gesperrt = uploads / ("cd" * 12 + ".jpg") gesperrt.write_bytes(jpeg_mit_gps()) gesperrt.chmod(0o444) (uploads / ("ef" * 12 + ".png")).write_bytes(b"\x89PNG kaputt") try: with conn: conn.execute("PRAGMA user_version = 1") db.init() assert conn.execute("PRAGMA user_version").fetchone()[0] == 2 finally: gesperrt.chmod(0o644)