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.
119 lines
4.5 KiB
Python
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})
|