feat(5.20): US-007 validare login RAR pe env-ul setului de credentiale

Login de validare loveste base_url_pentru_env(env) (NU ancora globala); endpoint
POST /cont/test-rar-creds + card in _integrare.html; mesaj distinct TESTARE vs
PRODUCTIE la 401 incrucisat (confirmat live).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Claude Agent
2026-06-29 20:46:50 +00:00
parent 19d8aaa7aa
commit 3579a15363
3 changed files with 333 additions and 1 deletions

View File

@@ -90,6 +90,7 @@ from ..mapping import (
)
from ..shared_store import record_human_validation
from ..rar_env import MediuIndisponibil, medii_disponibile_cont, rar_env_efectiv_cont, rezolva_rar_env
from ..rar_client import RarAuthError, RarClient, RarError, base_url_pentru_env
# Campuri canonice cu eticheta umana pentru dropdown mapare coloane
_CANONICAL_FIELDS = [(k, v[0]) for k, v in _CANONICAL_SYNONYMS.items()]
@@ -4034,6 +4035,66 @@ async def web_confirma_import(
# care cere cheie API; sesiunea web e suficienta ca identitate). #
# =========================================================================== #
# ---------------------------------------------------------------------------
# US-007 (PRD 5.20): Validare credentiale RAR env-aware
#
# Premisa confirmata live (2026-06-29): creds de productie NU se valideaza pe RAR
# test si invers (401 incrucisat). Deci login-ul de proba TREBUIE sa loveasca
# endpoint-ul mediului caruia ii apartin credentialele.
#
# Puncte de validare existente:
# - /cont/test-rar-creds (testeaza integrarea RAR, fara efecte secundare)
# Puncte non-aplicabile (nu colecteaza/valideaza creds RAR):
# - signup (/signup): nu colecteaza credentiale RAR — creare cont platforma, nu RAR
# - preview import: nu valideaza credentiale RAR
# Puncte viitoare (US-008):
# - /cont/rar-creds la salvare creds per-mediu (va apela _valideaza_login_rar)
# ---------------------------------------------------------------------------
def _eticheta_mediu_rar(env: str) -> str:
"""Eticheta umana a mediului RAR pentru mesaje de eroare/succes.
'test' -> 'TESTARE', 'prod' -> 'PRODUCTIE'.
"""
return "PRODUCTIE" if env == "prod" else "TESTARE"
def _valideaza_login_rar(
settings,
email: str,
password: str,
env: str,
) -> tuple[bool, str | None]:
"""Valideaza credentialele RAR prin login pe mediul specificat (US-007, PRD 5.20).
Creeaza un RarClient cu base_url-ul mediului `env` (NU base_url-ul global),
deoarece RAR test si RAR prod sunt sisteme separate cu credentiale separate.
Parametri
---------
settings: configuratia aplicatiei (pentru base_url_test/prod si timeout)
email: email-ul contului RAR
password: parola contului RAR
env: mediul tinta: 'test' sau 'prod'
Returneaza
----------
(True, None) la succes (login reusit)
(False, mesaj) la esec; `mesaj` include eticheta mediului ('TESTARE'/'PRODUCTIE'),
ex. 'Credentiale RAR invalide pe TESTARE.'
"""
env_label = _eticheta_mediu_rar(env)
try:
with RarClient(settings, base_url=base_url_pentru_env(settings, env)) as rar:
rar.login(email, password)
return True, None
except RarAuthError:
return False, f"Credentiale RAR invalide pe {env_label}."
except RarError as exc:
return False, f"Eroare la conectare RAR ({env_label}): {exc}"
def _render_cont(
request: Request,
*,
@@ -4365,3 +4426,61 @@ def cont_rar_creds(
)
finally:
conn.close()
@router.post("/cont/test-rar-creds", response_class=HTMLResponse)
def cont_test_rar_creds(
request: Request,
rar_email: str = Form(""),
rar_parola: str = Form(""),
rar_env: str = Form(default=""),
csrf_token: str | None = Form(None),
) -> HTMLResponse:
"""Testeaza credentialele RAR prin login real pe mediul specificat (US-007, PRD 5.20).
Fara efecte secundare: nu salveaza nimic, nu creeaza submission. Pur validare.
Camp parola NICIODATA re-pus in raspuns.
Decizie env (documentata US-007):
- param `rar_env` explicit ('test'/'prod') -> folosit direct
- altfel -> rar_env_efectiv_cont (default-ul contului) sau ancora globala settings.rar_env
- signup nu colecteaza creds RAR, deci nu apeleaza aceasta functie
"""
account_id = require_login(request)
verify_csrf(request, csrf_token)
email = rar_email.strip()
parola = rar_parola.strip()
if not email or not parola:
return templates.TemplateResponse(
"_integrare_test_rezultat.html",
{"request": request, "succes": False,
"mesaj": "Email si parola sunt obligatorii."},
)
# Determina env-ul de validare
settings = get_settings()
env_cerut = (rar_env or "").strip().lower()
if env_cerut in ("test", "prod"):
env = env_cerut
else:
# Fallback: env-ul efectiv al contului (default) sau ancora globala
conn = get_connection()
try:
env = rar_env_efectiv_cont(conn, account_id) or settings.rar_env or "test"
finally:
conn.close()
ok, mesaj_eroare = _valideaza_login_rar(settings, email, parola, env)
if ok:
env_label = _eticheta_mediu_rar(env)
return templates.TemplateResponse(
"_integrare_test_rezultat.html",
{"request": request, "succes": True,
"mesaj": f"Credentiale RAR valide pe {env_label}."},
)
return templates.TemplateResponse(
"_integrare_test_rezultat.html",
{"request": request, "succes": False, "mesaj": mesaj_eroare},
)

View File

@@ -226,7 +226,7 @@
</div>
</div>
{# Formular test conexiune #}
{# Formular test conexiune cheie API #}
<div class="card" style="margin-bottom:16px;">
<h3 style="margin:0 0 12px; font-size:15px;">Testeaza conexiunea</h3>
<form id="form-test-cheie"
@@ -246,6 +246,42 @@
<div id="integrare-test-rezultat" style="margin-top:8px;"></div>
</div>
{# Formular test credentiale RAR (US-007, PRD 5.20) #}
{# Login de proba pe mediul ales — fara efecte secundare, nu salveaza nimic. #}
{# Banner-ul de rezultat include eticheta mediului ("pe TESTARE" / "pe PRODUCTIE"). #}
<div class="card" style="margin-bottom:16px;">
<h3 style="margin:0 0 6px; font-size:15px;">Testeaza credentiale RAR</h3>
<p class="muted" style="font-size:12px; margin:0 0 10px;">
Verifica daca credentialele RAR sunt corecte pe mediul ales. Nu se salveaza nimic.
</p>
<form id="form-test-rar-creds"
hx-post="/cont/test-rar-creds"
hx-target="#rar-test-rezultat"
hx-swap="innerHTML"
style="display:flex; gap:8px; flex-wrap:wrap; align-items:flex-end;">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<div>
<label for="test-rar-email" style="display:block; font-size:13px; color:var(--muted); margin-bottom:4px;">Email RAR</label>
<input type="email" id="test-rar-email" name="rar_email" placeholder="email@service.ro"
style="width:220px;" autocomplete="off">
</div>
<div>
<label for="test-rar-parola" style="display:block; font-size:13px; color:var(--muted); margin-bottom:4px;">Parola RAR</label>
<input type="password" id="test-rar-parola" name="rar_parola"
style="width:160px;" autocomplete="new-password">
</div>
<div>
<label for="test-rar-env" style="display:block; font-size:13px; color:var(--muted); margin-bottom:4px;">Mediu</label>
<select id="test-rar-env" name="rar_env" style="height:36px; padding:0 8px;">
<option value="prod">Productie</option>
<option value="test">Testare</option>
</select>
</div>
<button type="submit">Testeaza RAR</button>
</form>
<div id="rar-test-rezultat" style="margin-top:8px;"></div>
</div>
</div>
<script>

177
tests/test_validare_env.py Normal file
View File

@@ -0,0 +1,177 @@
"""Teste US-007 (PRD 5.20): validare credentiale RAR pe env-ul setului de creds.
Premisa confirmata live (2026-06-29): creds prod NU se valideaza pe RAR test si
invers (401 incrucisat). Deci login-ul de proba TREBUIE sa loveasca endpoint-ul
mediului caruia ii apartin credentialele, nu URL-ul global AUTOPASS_RAR_ENV.
Functie testata:
routes._valideaza_login_rar(settings, email, password, env)
Teste:
test_valideaza_pe_env_creds -- login pe env='prod' foloseste base_url prod (nu test)
test_mesaj_distinge_env -- esec pe test vs prod produce mesaje diferite
"""
from __future__ import annotations
import os
import tempfile
import pytest
from app.rar_client import RarAuthError
# ---------------------------------------------------------------------------
# Fixture izolat
# ---------------------------------------------------------------------------
@pytest.fixture()
def env_db(monkeypatch):
"""DB temporara + settings curate. Numele 'env_db' evita coliziunea cu parametrul
'env' folosit in testele de mai jos ca string ('test'/'prod')."""
tmp = tempfile.mkdtemp()
monkeypatch.setenv("AUTOPASS_DB_PATH", os.path.join(tmp, "t.db"))
monkeypatch.setenv("AUTOPASS_SEED_OPERATII_ENABLED", "false")
from app.config import get_settings
get_settings.cache_clear()
from app.db import init_db
init_db()
yield
get_settings.cache_clear()
# ---------------------------------------------------------------------------
# Stub-uri RarClient
# ---------------------------------------------------------------------------
class _RarClientSpy:
"""Inregistreaza base_url-ul cu care a fost construit si simuleaza login reusit."""
captured: dict = {}
def __init__(self, settings=None, *, base_url=None):
_RarClientSpy.captured["base_url"] = base_url
def login(self, email, password):
return "TOKEN-SPY"
def __enter__(self):
return self
def __exit__(self, *args):
pass
def close(self):
pass
class _RarClientFail:
"""Simuleaza login esuat (RarAuthError 401) indiferent de env."""
def __init__(self, settings=None, *, base_url=None):
pass
def login(self, email, password):
raise RarAuthError("Credentiale RAR invalide", status_code=401)
def __enter__(self):
return self
def __exit__(self, *args):
pass
def close(self):
pass
# ---------------------------------------------------------------------------
# Teste
# ---------------------------------------------------------------------------
def test_valideaza_pe_env_creds(env_db, monkeypatch):
"""Cand validezi creds pentru env='prod', clientul de login e creat cu base_url-ul prod.
US-007 AC: 'validarea foloseste env-ul setului de creds verificat'.
Premisa: creds prod nu se valideaza pe RAR test (401 incrucisat), deci
clientul TREBUIE sa foloseasca base_url-ul prod, nu cel de test.
"""
import app.web.routes as routes_mod
from app.config import get_settings
_RarClientSpy.captured = {}
monkeypatch.setattr(routes_mod, "RarClient", _RarClientSpy)
settings = get_settings()
ok, mesaj = routes_mod._valideaza_login_rar(settings, "a@b.ro", "parola", "prod")
assert ok is True, f"Login simulat trebuia sa reuseasca: mesaj={mesaj!r}"
assert mesaj is None
base_url_folosit = _RarClientSpy.captured.get("base_url")
assert base_url_folosit == settings.rar_base_url_prod, (
f"Clientul trebuia construit cu rar_base_url_prod={settings.rar_base_url_prod!r},"
f" dar a primit base_url={base_url_folosit!r}"
)
assert base_url_folosit != settings.rar_base_url_test, (
"Clientul nu trebuia sa foloseasca base_url-ul de TEST la validarea creds PROD"
)
def test_valideaza_pe_env_creds_test(env_db, monkeypatch):
"""Cand validezi creds pentru env='test', clientul de login e creat cu base_url-ul test."""
import app.web.routes as routes_mod
from app.config import get_settings
_RarClientSpy.captured = {}
monkeypatch.setattr(routes_mod, "RarClient", _RarClientSpy)
settings = get_settings()
ok, mesaj = routes_mod._valideaza_login_rar(settings, "a@b.ro", "parola", "test")
assert ok is True
base_url_folosit = _RarClientSpy.captured.get("base_url")
assert base_url_folosit == settings.rar_base_url_test, (
f"Clientul trebuia construit cu rar_base_url_test={settings.rar_base_url_test!r},"
f" dar a primit base_url={base_url_folosit!r}"
)
assert base_url_folosit != settings.rar_base_url_prod
def test_mesaj_distinge_env(env_db, monkeypatch):
"""La esec de login pe test vs prod, mesajul difera ('TESTARE' vs 'PRODUCTIE').
US-007 AC: 'mesaj distinct creds invalide pe TESTARE vs pe PRODUCTIE'.
Design F6/F7: banner-ul de eroare indica pe ce mediu a esuat login-ul.
"""
import app.web.routes as routes_mod
from app.config import get_settings
monkeypatch.setattr(routes_mod, "RarClient", _RarClientFail)
settings = get_settings()
ok_test, msg_test = routes_mod._valideaza_login_rar(settings, "a@b.ro", "parola", "test")
ok_prod, msg_prod = routes_mod._valideaza_login_rar(settings, "a@b.ro", "parola", "prod")
assert ok_test is False, "Esecul la test trebuia sa returneze ok=False"
assert ok_prod is False, "Esecul la prod trebuia sa returneze ok=False"
assert msg_test is not None and "TESTARE" in msg_test, (
f"Mesajul la esec pe test trebuia sa contina 'TESTARE': {msg_test!r}"
)
assert msg_prod is not None and "PRODUCTIE" in msg_prod, (
f"Mesajul la esec pe prod trebuia sa contina 'PRODUCTIE': {msg_prod!r}"
)
# Cross-check: etichetele nu se amesteca
assert "PRODUCTIE" not in (msg_test or ""), (
f"Mesajul esec test nu trebuia sa mentioneze PRODUCTIE: {msg_test!r}"
)
assert "TESTARE" not in (msg_prod or ""), (
f"Mesajul esec prod nu trebuia sa mentioneze TESTARE: {msg_prod!r}"
)