feat(setari): meniu grupat + pagina Setari cu sub-taburi + schimbare parola
Meniu hamburger reorganizat pe grupuri cu etichete (LUCRU / CONT) cu iconite SVG discrete; "Cont" redenumit "Setari" (URL ?tab=cont pastrat, zero rute moarte). Pagina Setari restructurata in sub-taburi interne: Date firma / Credentiale RAR / Cheie API / Cont (plan + schimbare parola), cu indicator de stare per tab. Sub-tabul activ e pastrat server-side (activ_subtab) dupa fiecare swap HTMX; comutarea intre taburi e client-side, delegata pe document (supravietuieste swap-urilor). Credentiale RAR: mediile Testare/Productie ca doua carduri paralele. Schimbare parola de acces din sub-tabul Cont (functie noua, lipsea): - users.change_password (scrypt): verifica parola actuala constant-time, valideaza noua (10..128, diferita), rescrie hash+salt nou; - ruta POST /cont/schimba-parola, scoped pe user_id din sesiune, CSRF enforce, parola niciodata re-pusa in raspuns, jurnal 'parola_schimbata'. Referinte copy "Meniu cont > Cont" -> "Setari". Teste: schimbare parola (succes / parola actuala gresita / confirmare nepotrivita / lungime) + izolare rate-limit login in fixture-ul clientului. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,4 @@
|
||||
"""Teste US-007 (PRD 3.3b): sectiunea 'Contul meu' — rotire cheie API + creds RAR din UI.
|
||||
|
||||
TDD: testele se scriu INAINTE de implementare; la inceput pica (RED),
|
||||
dupa implementare trec (GREEN).
|
||||
"""Sectiunea 'Contul meu' — rotire cheie API + creds RAR din UI.
|
||||
|
||||
Rute testate:
|
||||
- GET /_fragments/cont -> card "Contul meu"
|
||||
@@ -25,6 +22,10 @@ def client(monkeypatch):
|
||||
monkeypatch.setenv("AUTOPASS_DB_PATH", os.path.join(tmp, "t.db"))
|
||||
from app.config import get_settings
|
||||
get_settings.cache_clear()
|
||||
# Izolare rate-limit login intre teste (limiter global per-IP pe viata procesului):
|
||||
# fara reset, seria de _login din fisier depaseste login_rate_max si da 429 (fals negativ).
|
||||
from app.web import ratelimit
|
||||
ratelimit._hits.clear()
|
||||
from app.main import app
|
||||
with TestClient(app, follow_redirects=False) as c:
|
||||
yield c
|
||||
@@ -162,7 +163,7 @@ def test_fragment_cont_nelogat_redirect(monkeypatch):
|
||||
|
||||
|
||||
# ============================================================
|
||||
# US-002: sectiunea 'Date firma' + banner cont incomplet
|
||||
# Sectiunea 'Date firma' + banner cont incomplet
|
||||
# ============================================================
|
||||
|
||||
def _create_complete_account(
|
||||
@@ -282,3 +283,117 @@ def test_banner_cont_incomplet_pe_legacy(client):
|
||||
# Banner trebuie sa apara cand contul e incomplet
|
||||
assert "completeaza" in text or "date firm" in text or "incomplet" in text, \
|
||||
f"Banner 'Completeaza datele firmei' lipsa pe Acasa: {resp.text[:2000]}"
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Sub-tab Cont: schimbare parola de acces (PRD 5.21)
|
||||
# ============================================================
|
||||
|
||||
def _get_csrf_from_cont(client) -> str:
|
||||
"""CSRF token din panoul Setari (tab=cont)."""
|
||||
resp = client.get("/?tab=cont")
|
||||
assert resp.status_code == 200
|
||||
m = re.search(r'name="csrf_token"\s+value="([^"]+)"', resp.text)
|
||||
if not m:
|
||||
m = re.search(r'value="([^"]+)"\s+name="csrf_token"', resp.text)
|
||||
assert m, f"csrf_token negasit in panoul Setari: {resp.text[:500]}"
|
||||
return m.group(1)
|
||||
|
||||
|
||||
def test_setari_subtaburi_prezente(client):
|
||||
"""Panoul Setari expune cele 4 sub-taburi + forma de schimbare parola."""
|
||||
_create_account_user("subtab@test.com")
|
||||
_login(client, "subtab@test.com", "parolasecreta10")
|
||||
|
||||
resp = client.get("/?tab=cont")
|
||||
assert resp.status_code == 200
|
||||
html = resp.text
|
||||
for eticheta in ("Date firma", "Credentiale RAR", "Cheie API", "Parola de acces"):
|
||||
assert eticheta in html, f"Lipseste sub-tabul/zona '{eticheta}': {html[:800]}"
|
||||
assert 'hx-post="/cont/schimba-parola"' in html, "Lipseste forma de schimbare parola"
|
||||
|
||||
|
||||
def test_schimba_parola_succes(client):
|
||||
"""Schimbare parola cu parola actuala corecta -> succes + login cu parola noua."""
|
||||
_create_account_user("pwok@test.com", "parolaveche10")
|
||||
_login(client, "pwok@test.com", "parolaveche10")
|
||||
csrf = _get_csrf_from_cont(client)
|
||||
|
||||
resp = client.post("/cont/schimba-parola", data={
|
||||
"csrf_token": csrf,
|
||||
"parola_actuala": "parolaveche10",
|
||||
"parola_noua": "parolanoua123",
|
||||
"parola_confirma": "parolanoua123",
|
||||
})
|
||||
assert resp.status_code == 200, f"POST schimba-parola a returnat {resp.status_code}"
|
||||
assert "schimbat" in resp.text.lower(), f"Lipseste confirmarea: {resp.text[:600]}"
|
||||
|
||||
# Parola noua verifica in DB
|
||||
from app.db import get_connection
|
||||
from app.users import verify_password
|
||||
conn = get_connection()
|
||||
try:
|
||||
assert verify_password(conn, "pwok@test.com", "parolanoua123") is not None, \
|
||||
"Parola noua nu autentifica"
|
||||
assert verify_password(conn, "pwok@test.com", "parolaveche10") is None, \
|
||||
"Parola veche inca autentifica dupa schimbare"
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
def test_schimba_parola_actuala_gresita(client):
|
||||
"""Parola actuala gresita -> eroare, parola neschimbata."""
|
||||
_create_account_user("pwbad@test.com", "parolaveche10")
|
||||
_login(client, "pwbad@test.com", "parolaveche10")
|
||||
csrf = _get_csrf_from_cont(client)
|
||||
|
||||
resp = client.post("/cont/schimba-parola", data={
|
||||
"csrf_token": csrf,
|
||||
"parola_actuala": "gresita000",
|
||||
"parola_noua": "parolanoua123",
|
||||
"parola_confirma": "parolanoua123",
|
||||
})
|
||||
assert resp.status_code == 200
|
||||
assert "incorect" in resp.text.lower(), f"Lipseste eroarea de parola gresita: {resp.text[:600]}"
|
||||
|
||||
from app.db import get_connection
|
||||
from app.users import verify_password
|
||||
conn = get_connection()
|
||||
try:
|
||||
assert verify_password(conn, "pwbad@test.com", "parolaveche10") is not None, \
|
||||
"Parola veche trebuia sa ramana valida dupa esec"
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
def test_schimba_parola_confirmare_nepotrivita(client):
|
||||
"""Confirmarea nu se potriveste -> eroare, fara scriere."""
|
||||
_create_account_user("pwmis@test.com", "parolaveche10")
|
||||
_login(client, "pwmis@test.com", "parolaveche10")
|
||||
csrf = _get_csrf_from_cont(client)
|
||||
|
||||
resp = client.post("/cont/schimba-parola", data={
|
||||
"csrf_token": csrf,
|
||||
"parola_actuala": "parolaveche10",
|
||||
"parola_noua": "parolanoua123",
|
||||
"parola_confirma": "altceva99999",
|
||||
})
|
||||
assert resp.status_code == 200
|
||||
assert "confirmarea" in resp.text.lower() or "potriv" in resp.text.lower(), \
|
||||
f"Lipseste eroarea de confirmare: {resp.text[:600]}"
|
||||
|
||||
|
||||
def test_schimba_parola_prea_scurta(client):
|
||||
"""Parola noua sub 10 caractere -> respinsa cu mesaj."""
|
||||
_create_account_user("pwshort@test.com", "parolaveche10")
|
||||
_login(client, "pwshort@test.com", "parolaveche10")
|
||||
csrf = _get_csrf_from_cont(client)
|
||||
|
||||
resp = client.post("/cont/schimba-parola", data={
|
||||
"csrf_token": csrf,
|
||||
"parola_actuala": "parolaveche10",
|
||||
"parola_noua": "scurt",
|
||||
"parola_confirma": "scurt",
|
||||
})
|
||||
assert resp.status_code == 200
|
||||
assert "scurt" in resp.text.lower(), f"Lipseste eroarea de lungime: {resp.text[:600]}"
|
||||
|
||||
Reference in New Issue
Block a user