banyaro/tests/test_partner_profile.py
rene ce8aa2b699 Feature: Partner-Profile Backend + Pro-Zugang für Partner
Die Partner-Showcase-Seite (#partner) und der Profil-Editor (#partner-profil)
existierten seit v1102 nur als Frontend — /api/partners/public und
/api/partner/my-profile gab es nie (vermutlich Worktree-Merge-Verlust).

Backend neu:
- partner_profiles-Tabelle (user_id PK, ON DELETE CASCADE → DSGVO-Delete greift)
- GET/PUT /partner/my-profile (Texte, Website-Normalisierung, @-Instagram)
- Logo-Upload (≤5 MB → WebP 512px, altes Logo wird geräumt)
- Foto/Video-Upload (max 6, 200-MB-Budget, HEIC→JPEG, MOV→MP4 via ffmpeg,
  Bilder→WebP 1600px) + Lösch-Endpoint
- Submit-Workflow (approved 0/1/-1) + Admin-Mail (best effort)
- GET /partners/public (nur freigegebene, JOIN users für Name/Avatar)
- Admin: GET /admin/partner/profiles + POST .../review

Pro für Partner: has_pro_access() + App._hasPro() prüfen jetzt is_partner —
Multiplikatoren bekommen Pro gratis (mehrere Hunde, KI-Trainer etc.).

UI: Admin-Partner-Tab mit Freigabe-Sektion (offen-Badge, ✓/✗),
Settings zeigt Partnern eine Karte mit Link zum Profil-Editor.

Tests: tests/test_partner_profile.py — 5 Smoke-Tests (403, Voll-Flow
inkl. Freigabe/Ablehnung, Pflicht-Anzeigename, Logo+Foto-Upload, Pro-Zugang).
Suite: 44 passed.
2026-06-07 17:20:20 +02:00

119 lines
4.5 KiB
Python

"""Smoke-Tests fuer Partner-Profile (Editor + Freigabe-Workflow + oeffentlicher Showcase)."""
import io
def _make_partner(user_email: str):
"""Setzt is_partner=1 direkt in der Test-DB."""
from database import db
with db() as conn:
conn.execute("UPDATE users SET is_partner=1 WHERE email=?", (user_email,))
def test_my_profile_requires_partner(client, user):
"""GET /api/partner/my-profile -> 403 fuer normale User."""
r = client.get("/api/partner/my-profile", headers=user["headers"])
assert r.status_code == 403
def test_partner_profile_full_flow(client, user, admin):
"""Texte speichern -> einreichen -> Admin gibt frei -> oeffentlich sichtbar."""
_make_partner(user["email"])
# Leeres Profil mit Storage-Infos
r = client.get("/api/partner/my-profile", headers=user["headers"])
assert r.status_code == 200, r.text
assert r.json()["storage_limit_mb"] == 200
# Texte speichern (Website ohne Schema wird normalisiert)
r = client.put("/api/partner/my-profile", headers=user["headers"], json={
"display_name": "Hundeblog Test",
"tagline": "Testkanal",
"bio": "Wir testen Ban Yaro.",
"website": "hundeblog-test.de",
"instagram": "@hundeblogtest",
})
assert r.status_code == 200, r.text
p = r.json()["profile"]
assert p["display_name"] == "Hundeblog Test"
assert p["website"] == "https://hundeblog-test.de"
# Vor Freigabe nicht oeffentlich
r = client.get("/api/partners/public")
assert all(x.get("display_name") != "Hundeblog Test" for x in r.json()["partners"])
# Einreichen
r = client.post("/api/partner/my-profile/submit", headers=user["headers"], json={})
assert r.status_code == 200, r.text
assert r.json()["profile"]["submitted_at"]
# Admin sieht das Profil und gibt frei
r = client.get("/api/admin/partner/profiles", headers=admin["headers"])
assert r.status_code == 200
mine = [x for x in r.json() if x.get("display_name") == "Hundeblog Test"]
assert mine, "Profil fehlt in der Admin-Liste"
uid = mine[0]["user_id"]
r = client.post(f"/api/admin/partner/profiles/{uid}/review",
headers=admin["headers"], json={"approved": 1})
assert r.status_code == 200
# Jetzt oeffentlich (ohne Login)
r = client.get("/api/partners/public")
names = [x["display_name"] for x in r.json()["partners"]]
assert "Hundeblog Test" in names
# Ablehnen entfernt es wieder von der oeffentlichen Seite
r = client.post(f"/api/admin/partner/profiles/{uid}/review",
headers=admin["headers"], json={"approved": -1})
assert r.status_code == 200
r = client.get("/api/partners/public")
assert "Hundeblog Test" not in [x["display_name"] for x in r.json()["partners"]]
def test_submit_requires_display_name(client, user):
"""Einreichen ohne Anzeigename -> 400."""
_make_partner(user["email"])
r = client.post("/api/partner/my-profile/submit", headers=user["headers"], json={})
assert r.status_code == 400
def test_logo_and_photo_upload(client, user):
"""Logo + Foto hochladen, Foto wieder loeschen."""
from PIL import Image
_make_partner(user["email"])
def _png(size=(64, 64), color="red"):
buf = io.BytesIO()
Image.new("RGB", size, color).save(buf, format="PNG")
buf.seek(0)
return buf
# Logo
r = client.post("/api/partner/my-profile/logo", headers=user["headers"],
files={"file": ("logo.png", _png(), "image/png")})
assert r.status_code == 200, r.text
assert r.json()["logo_url"].startswith("/media/partner/")
# Foto
r = client.post("/api/partner/my-profile/photos", headers=user["headers"],
files={"file": ("foto.png", _png(color="blue"), "image/png")})
assert r.status_code == 200, r.text
photos = r.json()["photos"]
assert len(photos) == 1 and photos[0].endswith(".webp")
# Speicher belegt
r = client.get("/api/partner/my-profile", headers=user["headers"])
assert r.json()["storage_mb"] > 0
# Foto loeschen
r = client.post("/api/partner/my-profile/photos/0/delete", headers=user["headers"], json={})
assert r.status_code == 200
assert r.json()["photos"] == []
def test_partner_has_pro_access(client, user):
"""is_partner=1 -> has_pro_access True (Pro gratis fuer Partner)."""
from auth import has_pro_access
assert has_pro_access({"rolle": "user", "subscription_tier": "standard", "is_partner": 1})
assert not has_pro_access({"rolle": "user", "subscription_tier": "standard", "is_partner": 0})