Schutz gegen kursierende Partner-Codes (Rene: 'Bonus-Codes kursieren gerne das Internet')
1. QR-URL verrät den Code nicht mehr: /q/{token} → /?qr=TOKEN (vorher stand
der tippbare Code in der Adresszeile jedes Scanners). Registrierung löst
den Code server-seitig aus dem Token auf (auch ohne ref_code).
2. Notbremse: partner_codes.active — Admin kann Codes pausieren (Einlösung
gesperrt, Info-Endpoint 404, Historie/QR-Kontingente bleiben) und
reaktivieren. UI: ⏸/▶-Toggle + pausiert-Badge in der Codes-Tabelle.
3. max_uses im Anlege-Formular standardmäßig 50 statt unbegrenzt.
Tests: QR-only-Registrierung, Pause→keine Einlösung→Reaktivierung,
Redirect ohne Klartext-Code. Suite: 54 passed.
This commit is contained in:
parent
21bcc6b962
commit
2927ae2672
11 changed files with 136 additions and 39 deletions
|
|
@ -50,7 +50,9 @@ def test_scan_redirects_and_counts(client, admin):
|
|||
|
||||
r = client.get(f"/q/{token}", follow_redirects=False)
|
||||
assert r.status_code == 302
|
||||
assert r.headers["location"] == f"/?ref={code['code']}&qr={token}"
|
||||
# Bewusst KEIN Klartext-Code in der URL — sonst liest jeder Scanner den Code ab
|
||||
assert r.headers["location"] == f"/?qr={token}"
|
||||
assert code["code"] not in r.headers["location"]
|
||||
client.get(f"/q/{token}", follow_redirects=False)
|
||||
|
||||
r = client.get("/api/admin/partner/qr-batches", headers=admin["headers"])
|
||||
|
|
@ -104,6 +106,52 @@ def test_registration_attributed_to_qr(client, admin):
|
|||
assert regs[0]["created_at"]
|
||||
|
||||
|
||||
def test_registration_with_qr_only(client, admin):
|
||||
"""Registrierung NUR mit qr_token (ohne ref_code) -> Code wird server-seitig aufgeloest."""
|
||||
code = _create_code(client, admin)
|
||||
batch = _create_batch(client, admin, code["id"], quantity=1)
|
||||
token = _batch_tokens(batch["id"])[0]
|
||||
|
||||
email = f"qro-{secrets.token_hex(4)}@example.com"
|
||||
r = client.post("/api/auth/register", json={
|
||||
"email": email, "password": "QrTest1234!", "name": f"qro{secrets.token_hex(3)}",
|
||||
"qr_token": token,
|
||||
})
|
||||
assert r.status_code == 200, r.text
|
||||
from database import db
|
||||
with db() as conn:
|
||||
row = conn.execute("SELECT referred_by, referred_qr FROM users WHERE email=?", (email,)).fetchone()
|
||||
assert row["referred_by"] == -code["id"]
|
||||
assert row["referred_qr"] == token
|
||||
|
||||
|
||||
def test_paused_code_not_redeemable(client, admin):
|
||||
"""Pausierter Code (Notbremse) -> keine Einloesung, Info-Endpoint 404; reaktivierbar."""
|
||||
code = _create_code(client, admin)
|
||||
r = client.post(f"/api/admin/partner/codes/{code['id']}/toggle", headers=admin["headers"])
|
||||
assert r.status_code == 200 and r.json()["active"] == 0
|
||||
|
||||
# Info-Endpoint: wie nicht existent
|
||||
assert client.get(f"/api/partner/codes/{code['code']}/info").status_code == 404
|
||||
|
||||
# Registrierung mit pausiertem Code -> keine Zuordnung
|
||||
email = f"qrp-{secrets.token_hex(4)}@example.com"
|
||||
r = client.post("/api/auth/register", json={
|
||||
"email": email, "password": "QrTest1234!", "name": f"qrp{secrets.token_hex(3)}",
|
||||
"ref_code": code["code"],
|
||||
})
|
||||
assert r.status_code == 200, r.text
|
||||
from database import db
|
||||
with db() as conn:
|
||||
row = conn.execute("SELECT referred_by FROM users WHERE email=?", (email,)).fetchone()
|
||||
assert row["referred_by"] is None
|
||||
|
||||
# Reaktivieren funktioniert
|
||||
r = client.post(f"/api/admin/partner/codes/{code['id']}/toggle", headers=admin["headers"])
|
||||
assert r.json()["active"] == 1
|
||||
assert client.get(f"/api/partner/codes/{code['code']}/info").status_code == 200
|
||||
|
||||
|
||||
def test_qr_token_must_match_code(client, admin):
|
||||
"""QR-Token eines FREMDEN Codes wird nicht zugeordnet (Manipulationsschutz)."""
|
||||
code_a = _create_code(client, admin)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue