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.
This commit is contained in:
parent
178aef7fb0
commit
ce8aa2b699
11 changed files with 557 additions and 19 deletions
119
tests/test_partner_profile.py
Normal file
119
tests/test_partner_profile.py
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
"""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})
|
||||
Loading…
Add table
Add a link
Reference in a new issue