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:
Claude Agent
2026-07-06 13:40:38 +00:00
parent 164f90d603
commit 86408887e5
7 changed files with 736 additions and 385 deletions

View File

@@ -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]}"