diff --git a/app/web/routes.py b/app/web/routes.py index a11295f..55fff6a 100644 --- a/app/web/routes.py +++ b/app/web/routes.py @@ -292,10 +292,14 @@ def _get_acasa_context(request: Request, conn, account_id: int) -> dict: acct = account_or_default(account_id) # Pas 1: are credentiale RAR configurate? + metadate cont (pentru banner incomplet) + # Verifica atat coloana legacy rar_creds_enc cat si sloturile per-env (US-008, PRD 5.20). row = conn.execute( - "SELECT id, name, cui, email, rar_creds_enc FROM accounts WHERE id=?", (acct,) + "SELECT id, name, cui, email, rar_creds_enc, rar_creds_test_enc, rar_creds_prod_enc " + "FROM accounts WHERE id=?", (acct,) ).fetchone() - are_creds = bool(row and row["rar_creds_enc"]) + are_creds = bool(row and ( + row["rar_creds_enc"] or row["rar_creds_test_enc"] or row["rar_creds_prod_enc"] + )) # Banner cont incomplet (US-002): contul nu are companie + email + CUI complete cont_incomplet = not _acct_is_complete(row) if row else False @@ -385,22 +389,25 @@ def _render_panel_cont(request: Request, conn, account_id: int) -> str: """Randeaza panoul Cont ca string HTML.""" from ..mapping import account_or_default acct = account_or_default(account_id) - row = conn.execute( - "SELECT id, name, cui, email, rar_creds_enc FROM accounts WHERE id=?", (acct,) - ).fetchone() - are_creds = bool(row and row["rar_creds_enc"]) account_meta = _fetch_account_meta(conn, acct) + env_ctx = _fetch_cont_env_state(conn, acct) cont_ctx = { "request": request, "csrf_token": get_csrf_token(request), "api_key": None, - "are_creds": are_creds, "creds_mesaj": None, "creds_eroare": None, "rot_eroare": None, "account_meta": account_meta, "date_firma_mesaj": None, "date_firma_eroare": None, + "creds_test_mesaj": None, + "creds_test_eroare": None, + "creds_prod_mesaj": None, + "creds_prod_eroare": None, + "creds_default_eroare": None, + "creds_default_mesaj": None, + **env_ctx, } # US-006 (5.17): context plan pentru sectiunea Plan din _cont.html. cont_ctx.update(_plan_ctx(conn, account_id)) @@ -4106,6 +4113,19 @@ def _render_cont( account_meta: dict | None = None, date_firma_mesaj: str | None = None, date_firma_eroare: str | None = None, + # Per-env (US-008, PRD 5.20): starea mediilor RAR Testare + Productie. + test_enabled: bool = False, + prod_enabled: bool = True, + test_disponibil: bool = False, + prod_disponibil: bool = False, + rar_env_default: str = "prod", + medii_disponibile: list | None = None, + creds_test_mesaj: str | None = None, + creds_test_eroare: str | None = None, + creds_prod_mesaj: str | None = None, + creds_prod_eroare: str | None = None, + creds_default_eroare: str | None = None, + creds_default_mesaj: str | None = None, ) -> HTMLResponse: """Randeaza cardul 'Contul meu'. Parola niciodata in value=.""" return templates.TemplateResponse( @@ -4120,6 +4140,18 @@ def _render_cont( account_meta=account_meta or {}, date_firma_mesaj=date_firma_mesaj, date_firma_eroare=date_firma_eroare, + test_enabled=test_enabled, + prod_enabled=prod_enabled, + test_disponibil=test_disponibil, + prod_disponibil=prod_disponibil, + rar_env_default=rar_env_default, + medii_disponibile=medii_disponibile or [], + creds_test_mesaj=creds_test_mesaj, + creds_test_eroare=creds_test_eroare, + creds_prod_mesaj=creds_prod_mesaj, + creds_prod_eroare=creds_prod_eroare, + creds_default_eroare=creds_default_eroare, + creds_default_mesaj=creds_default_mesaj, ), ) @@ -4139,6 +4171,56 @@ def _fetch_account_meta(conn, acct: int) -> dict: } +def _fetch_cont_env_state(conn, acct: int) -> dict: + """Starea mediilor RAR per env pentru sectiunea 'Credentiale RAR' din _cont.html (US-008). + + Returneaza un dict compatibil cu parametrii per-env ai _render_cont: + are_creds -- True daca ORICE credentiale RAR sunt configurate (legacy SAU per-env) + test_enabled -- bifa activare Testare + prod_enabled -- bifa activare Productie + test_disponibil -- Testare activata SI cu creds (poate trimite) + prod_disponibil -- Productie activata SI cu creds (poate trimite) + rar_env_default -- mediul implicit al contului + medii_disponibile -- lista mediilor disponibile (subset din ['test','prod']) + """ + row = conn.execute( + "SELECT rar_test_enabled, rar_prod_enabled, " + "rar_creds_test_enc, rar_creds_prod_enc, rar_env_default, rar_creds_enc " + "FROM accounts WHERE id=?", (acct,) + ).fetchone() + if not row: + return { + "are_creds": False, + "test_enabled": False, + "prod_enabled": True, + "test_disponibil": False, + "prod_disponibil": False, + "rar_env_default": "prod", + "medii_disponibile": [], + } + test_enabled = bool(row["rar_test_enabled"]) + prod_enabled = bool(row["rar_prod_enabled"]) + test_disponibil = test_enabled and bool(row["rar_creds_test_enc"]) + prod_disponibil = prod_enabled and bool(row["rar_creds_prod_enc"]) + medii: list[str] = [] + if test_disponibil: + medii.append("test") + if prod_disponibil: + medii.append("prod") + are_creds = bool( + row["rar_creds_enc"] or row["rar_creds_test_enc"] or row["rar_creds_prod_enc"] + ) + return { + "are_creds": are_creds, + "test_enabled": test_enabled, + "prod_enabled": prod_enabled, + "test_disponibil": test_disponibil, + "prod_disponibil": prod_disponibil, + "rar_env_default": row["rar_env_default"] or "prod", + "medii_disponibile": medii, + } + + @router.get("/_fragments/cont", response_class=HTMLResponse) def fragment_cont(request: Request) -> HTMLResponse: """Fragment HTMX card 'Contul meu': stare cheie + creds RAR + date firma.""" @@ -4146,12 +4228,9 @@ def fragment_cont(request: Request) -> HTMLResponse: acct = account_or_default(account_id) conn = get_connection() try: - row = conn.execute( - "SELECT id, name, cui, email, rar_creds_enc FROM accounts WHERE id=?", (acct,) - ).fetchone() - are_creds = bool(row and row["rar_creds_enc"]) account_meta = _fetch_account_meta(conn, acct) - return _render_cont(request, are_creds=are_creds, account_meta=account_meta) + env_ctx = _fetch_cont_env_state(conn, acct) + return _render_cont(request, account_meta=account_meta, **env_ctx) finally: conn.close() @@ -4168,12 +4247,9 @@ def cont_roteste_cheie( conn = get_connection() try: new_key = rotate_api_key(conn, acct) - row = conn.execute( - "SELECT rar_creds_enc FROM accounts WHERE id=?", (acct,) - ).fetchone() - are_creds = bool(row and row["rar_creds_enc"]) account_meta = _fetch_account_meta(conn, acct) - return _render_cont(request, api_key=new_key, are_creds=are_creds, account_meta=account_meta) + env_ctx = _fetch_cont_env_state(conn, acct) + return _render_cont(request, api_key=new_key, account_meta=account_meta, **env_ctx) finally: conn.close() @@ -4202,15 +4278,14 @@ async def cont_date_firma(request: Request) -> HTMLResponse: conn = get_connection() try: account_meta = _fetch_account_meta(conn, acct) - row_cr = conn.execute("SELECT rar_creds_enc FROM accounts WHERE id=?", (acct,)).fetchone() - are_creds = bool(row_cr and row_cr["rar_creds_enc"]) + env_ctx = _fetch_cont_env_state(conn, acct) finally: conn.close() return _render_cont( request, - are_creds=are_creds, account_meta=account_meta, date_firma_eroare="Compania (numele firmei) este obligatorie.", + **env_ctx, ) # Normalizare si validare email @@ -4219,31 +4294,27 @@ async def cont_date_firma(request: Request) -> HTMLResponse: except ValueError as exc: conn = get_connection() try: - account_meta = _fetch_account_meta(conn, acct) - row_cr = conn.execute("SELECT rar_creds_enc FROM accounts WHERE id=?", (acct,)).fetchone() - are_creds = bool(row_cr and row_cr["rar_creds_enc"]) + env_ctx = _fetch_cont_env_state(conn, acct) finally: conn.close() return _render_cont( request, - are_creds=are_creds, account_meta={"name": companie_raw, "email": email_raw, "cui": cui_raw}, date_firma_eroare=f"Email invalid: {exc}", + **env_ctx, ) if not email_norm: conn = get_connection() try: - account_meta = _fetch_account_meta(conn, acct) - row_cr = conn.execute("SELECT rar_creds_enc FROM accounts WHERE id=?", (acct,)).fetchone() - are_creds = bool(row_cr and row_cr["rar_creds_enc"]) + env_ctx = _fetch_cont_env_state(conn, acct) finally: conn.close() return _render_cont( request, - are_creds=are_creds, account_meta={"name": companie_raw, "email": email_raw, "cui": cui_raw}, date_firma_eroare="Email-ul de contact este obligatoriu.", + **env_ctx, ) # Normalizare si validare CUI @@ -4252,31 +4323,27 @@ async def cont_date_firma(request: Request) -> HTMLResponse: except ValueError as exc: conn = get_connection() try: - account_meta = _fetch_account_meta(conn, acct) - row_cr = conn.execute("SELECT rar_creds_enc FROM accounts WHERE id=?", (acct,)).fetchone() - are_creds = bool(row_cr and row_cr["rar_creds_enc"]) + env_ctx = _fetch_cont_env_state(conn, acct) finally: conn.close() return _render_cont( request, - are_creds=are_creds, account_meta={"name": companie_raw, "email": email_norm, "cui": cui_raw}, date_firma_eroare=f"CUI invalid: {exc}", + **env_ctx, ) if not cui_norm: conn = get_connection() try: - account_meta = _fetch_account_meta(conn, acct) - row_cr = conn.execute("SELECT rar_creds_enc FROM accounts WHERE id=?", (acct,)).fetchone() - are_creds = bool(row_cr and row_cr["rar_creds_enc"]) + env_ctx = _fetch_cont_env_state(conn, acct) finally: conn.close() return _render_cont( request, - are_creds=are_creds, account_meta={"name": companie_raw, "email": email_norm, "cui": cui_raw}, date_firma_eroare="CUI-ul firmei este obligatoriu.", + **env_ctx, ) # Actualizare in DB @@ -4294,26 +4361,24 @@ async def cont_date_firma(request: Request) -> HTMLResponse: ).fetchone() owner = existing["id"] if existing else "?" account_meta = _fetch_account_meta(conn, acct) - row_cr = conn.execute("SELECT rar_creds_enc FROM accounts WHERE id=?", (acct,)).fetchone() - are_creds = bool(row_cr and row_cr["rar_creds_enc"]) + env_ctx = _fetch_cont_env_state(conn, acct) return _render_cont( request, - are_creds=are_creds, account_meta={"name": companie_raw, "email": email_norm, "cui": cui_norm}, date_firma_eroare=( f"CUI-ul {cui_norm} este deja folosit de alt cont (id={owner}). " "Foloseste un CUI diferit sau contacteaza administratorul." ), + **env_ctx, ) account_meta = _fetch_account_meta(conn, acct) - row_cr = conn.execute("SELECT rar_creds_enc FROM accounts WHERE id=?", (acct,)).fetchone() - are_creds = bool(row_cr and row_cr["rar_creds_enc"]) + env_ctx = _fetch_cont_env_state(conn, acct) return _render_cont( request, - are_creds=are_creds, account_meta=account_meta, date_firma_mesaj="Datele firmei au fost salvate.", + **env_ctx, ) finally: conn.close() @@ -4396,18 +4461,15 @@ def cont_rar_creds( if not email or not parola: conn = get_connection() try: - row = conn.execute( - "SELECT rar_creds_enc FROM accounts WHERE id=?", (acct,) - ).fetchone() - are_creds = bool(row and row["rar_creds_enc"]) account_meta = _fetch_account_meta(conn, acct) + env_ctx = _fetch_cont_env_state(conn, acct) finally: conn.close() return _render_cont( request, - are_creds=are_creds, creds_eroare="Email si parola sunt obligatorii.", account_meta=account_meta, + **env_ctx, ) enc = encrypt_creds({"email": email, "password": parola}) @@ -4418,11 +4480,12 @@ def cont_rar_creds( (enc, acct), ) account_meta = _fetch_account_meta(conn, acct) + env_ctx = _fetch_cont_env_state(conn, acct) return _render_cont( request, - are_creds=True, creds_mesaj="Credentialele RAR au fost salvate cu succes.", account_meta=account_meta, + **env_ctx, ) finally: conn.close() @@ -4484,3 +4547,155 @@ def cont_test_rar_creds( "_integrare_test_rezultat.html", {"request": request, "succes": False, "mesaj": mesaj_eroare}, ) + + +# =========================================================================== # +# US-008 (PRD 5.20): Configurare medii RAR per cont — Testare + Productie. # +# Ruta noua /cont/rar-medii: gestioneaza bifa activare, credentiale si # +# mediul implicit separat pentru fiecare din cele doua medii RAR. # +# =========================================================================== # + +@router.post("/cont/rar-medii", response_class=HTMLResponse) +async def cont_rar_medii(request: Request) -> HTMLResponse: + """Salveaza configuratia mediilor RAR per cont (US-008, PRD 5.20). + + Doua sectiuni independente (Testare / Productie): fiecare cu bifa de activare + si campuri email/parola. La salvare, pentru fiecare mediu activat cu creds noi: + - valideaza prin login pe acel env (US-007) — RAR test si prod sunt sisteme separate; + - OK -> cripteaza cu Fernet si scrie in rar_creds_{env}_enc + enabled=1; + - esec login -> eroare per-env, mediul NU devine disponibil (creds nesalvate). + + Activarea Productie pentru prima oara necesita checkbox-ul de confirmare + (constientizare L.142 — trimiterile sunt declaratii oficiale, finale si fara anulare). + + Mediul implicit (rar_env_default) poate fi setat DOAR pe un mediu disponibil + (validat server-side post-update; altfel eroare, valoarea veche ramane). + + Parolele NICIODATA reflectate inapoi in pagina (camp gol cu placeholder). + """ + account_id = require_login(request) + form = await request.form() + verify_csrf(request, str(form.get("csrf_token") or "")) + acct = account_or_default(account_id) + + test_enabled_form = form.get("test_enabled") == "1" + prod_enabled_form = form.get("prod_enabled") == "1" + prod_confirmare = form.get("prod_confirmare") == "1" + test_email = str(form.get("test_email") or "").strip() + test_parola = str(form.get("test_parola") or "").strip() + prod_email = str(form.get("prod_email") or "").strip() + prod_parola = str(form.get("prod_parola") or "").strip() + rar_env_default_form = str(form.get("rar_env_default") or "").strip() + + settings = get_settings() + creds_test_eroare: str | None = None + creds_test_mesaj: str | None = None + creds_prod_eroare: str | None = None + creds_prod_mesaj: str | None = None + creds_default_eroare: str | None = None + creds_default_mesaj: str | None = None + + conn = get_connection() + try: + # Starea curenta din DB (inainte de update) — necesara pt logica de confirmare prod. + row_before = conn.execute( + "SELECT rar_prod_enabled FROM accounts WHERE id=?", (acct,) + ).fetchone() + was_prod_enabled = bool(row_before["rar_prod_enabled"]) if row_before else False + + # Confirmare obligatorie la PRIMA activare Productie (constientizare L.142). + # Nu se cere daca Productie era deja activata (confirmare unica per-activare). + if prod_enabled_form and not was_prod_enabled and not prod_confirmare: + creds_prod_eroare = ( + "Bifati confirmarea de mai jos pentru a activa mediul Productie: " + "\"Inteleg ca trimiterile pe Productie sunt declaratii reale (L.142)\"." + ) + account_meta = _fetch_account_meta(conn, acct) + env_ctx = _fetch_cont_env_state(conn, acct) + return _render_cont( + request, + account_meta=account_meta, + creds_prod_eroare=creds_prod_eroare, + **env_ctx, + ) + + # --- Procesare Testare --- + if test_enabled_form: + if test_email and test_parola: + # Ambele campuri completate -> valideaza prin login pe RAR Testare. + ok, mesaj = _valideaza_login_rar(settings, test_email, test_parola, "test") + if ok: + enc = encrypt_creds({"email": test_email, "password": test_parola}) + conn.execute( + "UPDATE accounts SET rar_test_enabled=1, rar_creds_test_enc=? WHERE id=?", + (enc, acct), + ) + creds_test_mesaj = "Credentiale Testare salvate si validate." + else: + # Login esuat: nu schimbam creds sau enabled; eroare per-env. + creds_test_eroare = mesaj + elif test_email or test_parola: + # Doar unul din campuri completat -> eroare (nu pot fi partial). + creds_test_eroare = "Email si parola Testare trebuie completate impreuna." + else: + # Activat fara creds noi -> marcheaza enabled (creds existente, daca sunt, raman). + conn.execute("UPDATE accounts SET rar_test_enabled=1 WHERE id=?", (acct,)) + else: + # Dezactivat -> disabled=0; creds raman pentru posibila re-activare ulterioara. + conn.execute("UPDATE accounts SET rar_test_enabled=0 WHERE id=?", (acct,)) + + # --- Procesare Productie --- + if prod_enabled_form: + if prod_email and prod_parola: + ok, mesaj = _valideaza_login_rar(settings, prod_email, prod_parola, "prod") + if ok: + enc = encrypt_creds({"email": prod_email, "password": prod_parola}) + conn.execute( + "UPDATE accounts SET rar_prod_enabled=1, rar_creds_prod_enc=? WHERE id=?", + (enc, acct), + ) + creds_prod_mesaj = "Credentiale Productie salvate si validate." + else: + creds_prod_eroare = mesaj + elif prod_email or prod_parola: + creds_prod_eroare = "Email si parola Productie trebuie completate impreuna." + else: + conn.execute("UPDATE accounts SET rar_prod_enabled=1 WHERE id=?", (acct,)) + else: + conn.execute("UPDATE accounts SET rar_prod_enabled=0 WHERE id=?", (acct,)) + + # --- Mediu implicit (validat post-update contra mediilor disponibile) --- + if rar_env_default_form and rar_env_default_form in ("test", "prod"): + from ..rar_env import medii_disponibile as _medii_disponibile_fn + row_after = conn.execute( + "SELECT rar_test_enabled, rar_prod_enabled, " + "rar_creds_test_enc, rar_creds_prod_enc " + "FROM accounts WHERE id=?", (acct,) + ).fetchone() + medii_post = _medii_disponibile_fn(row_after) + if rar_env_default_form in medii_post: + conn.execute( + "UPDATE accounts SET rar_env_default=? WHERE id=?", + (rar_env_default_form, acct), + ) + creds_default_mesaj = "Mediu implicit actualizat." + else: + creds_default_eroare = ( + "Mediul ales nu e disponibil — activeaza-l si adauga credentiale valide intai." + ) + + account_meta = _fetch_account_meta(conn, acct) + env_ctx = _fetch_cont_env_state(conn, acct) + return _render_cont( + request, + account_meta=account_meta, + creds_test_mesaj=creds_test_mesaj, + creds_test_eroare=creds_test_eroare, + creds_prod_mesaj=creds_prod_mesaj, + creds_prod_eroare=creds_prod_eroare, + creds_default_eroare=creds_default_eroare, + creds_default_mesaj=creds_default_mesaj, + **env_ctx, + ) + finally: + conn.close() diff --git a/app/web/templates/_cont.html b/app/web/templates/_cont.html index 0623940..8b24ec3 100644 --- a/app/web/templates/_cont.html +++ b/app/web/templates/_cont.html @@ -114,36 +114,106 @@
-

Credentiale RAR (portal AUTOPASS)

- - {% if are_creds %} -
Credentiale RAR configurate.
- {% endif %} +

Credentiale RAR (portal AUTOPASS)

{% if creds_mesaj %}
{{ creds_mesaj }}
{% endif %} - {% if creds_eroare %} {% endif %} -
-

-
- -

-

-
- -

- - Parola stocata criptat, niciodata in clar. + + +
+

+ + {% if test_disponibil %} + configurat + {% endif %} +

+

+
+ +

+

+
+ +

+ {% if creds_test_mesaj %} +
{{ creds_test_mesaj }}
+ {% endif %} + {% if creds_test_eroare %} + + {% endif %} +
+ + +
+

+ + {% if prod_disponibil %} + configurat + {% endif %} +

+

+
+ +

+

+
+ +

+

+ +

+ {% if creds_prod_mesaj %} +
{{ creds_prod_mesaj }}
+ {% endif %} + {% if creds_prod_eroare %} + + {% endif %} +
+ + +
+
+ {% if medii_disponibile %} + + {% else %} +

Activeaza si valideaza un mediu intai.

+ {% endif %} + {% if creds_default_mesaj %} +
{{ creds_default_mesaj }}
+ {% endif %} + {% if creds_default_eroare %} + + {% endif %} +
+ + + Parolele stocate criptat, niciodata in clar.
diff --git a/tests/test_cont_medii.py b/tests/test_cont_medii.py new file mode 100644 index 0000000..23a4ace --- /dev/null +++ b/tests/test_cont_medii.py @@ -0,0 +1,313 @@ +"""Teste US-008 (PRD 5.20): configurare medii RAR per cont — Testare + Productie. + +Ruta testata: POST /cont/rar-medii + +Teste: + test_activeaza_si_salveaza_creds_per_env -- creds salvate criptat, mediu marcat disponibil + test_default_doar_dintre_disponibile -- mediu implicit validat contra disponibilelor + test_activare_prod_cere_confirmare -- prima activare prod cere checkbox L.142 + test_creds_criptate_fara_echo -- parola niciodata in clar in DB sau HTML +""" + +from __future__ import annotations + +import os +import re +import tempfile + +import pytest +from cryptography.fernet import Fernet +from starlette.testclient import TestClient + + +# --------------------------------------------------------------------------- +# Fixture +# --------------------------------------------------------------------------- + + +@pytest.fixture() +def client(monkeypatch): + """Client izolat cu DB temporara + cheie Fernet pentru criptare creds.""" + tmp = tempfile.mkdtemp() + monkeypatch.setenv("AUTOPASS_DB_PATH", os.path.join(tmp, "t_medii.db")) + monkeypatch.setenv("AUTOPASS_CREDS_KEY", Fernet.generate_key().decode()) + from app.config import get_settings + from app import crypto + + get_settings.cache_clear() + crypto.reset_cache() + from app.main import app + + with TestClient(app, follow_redirects=False) as c: + yield c + + get_settings.cache_clear() + crypto.reset_cache() + + +# --------------------------------------------------------------------------- +# Helpere +# --------------------------------------------------------------------------- + + +def _create_account_user( + name: str = "Service Test SRL", + email: str = "user@test.com", + password: str = "parolasecreta10", +): + """Creeaza cont + user. Returneaza (acct_id, user_id).""" + from app.accounts import create_account + from app.users import create_user + from app.db import get_connection + + conn = get_connection() + try: + acct_id = create_account(conn, name, active=True) + user_id = create_user(conn, acct_id, email, password) + return acct_id, user_id + finally: + conn.close() + + +def _login(client, email: str, password: str) -> None: + """Face login real prin HTTP si seteaza cookie-ul de sesiune pe client.""" + resp = client.get("/login") + 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, "csrf_token negasit pe /login" + csrf = m.group(1) + + resp = client.post("/login", data={ + "email": email, + "parola": password, + "csrf_token": csrf, + }) + assert resp.status_code == 303, f"Login esuat: {resp.status_code} {resp.text[:200]}" + + +def _get_csrf(client) -> str: + """Obtine CSRF token din fragmentul /_fragments/cont.""" + resp = client.get("/_fragments/cont") + assert resp.status_code == 200, f"/_fragments/cont a returnat {resp.status_code}" + 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 /_fragments/cont: {resp.text[:400]}" + return m.group(1) + + +def _mock_login_ok(monkeypatch) -> None: + """Monkeypatch _valideaza_login_rar sa returneze (True, None) fara RAR live.""" + import app.web.routes as routes_mod + monkeypatch.setattr(routes_mod, "_valideaza_login_rar", lambda *a, **kw: (True, None)) + + +# --------------------------------------------------------------------------- +# Teste +# --------------------------------------------------------------------------- + + +def test_activeaza_si_salveaza_creds_per_env(client, monkeypatch): + """Activez Testare cu creds valide (mock) -> DB: rar_test_enabled=1, rar_creds_test_enc non-null. + medii_disponibile si test_disponibil reflecta starea noua. + """ + _mock_login_ok(monkeypatch) + acct_id, _ = _create_account_user("Firma T1", "t1@test.com") + _login(client, "t1@test.com", "parolasecreta10") + + csrf = _get_csrf(client) + resp = client.post("/cont/rar-medii", data={ + "csrf_token": csrf, + "test_enabled": "1", + "test_email": "rar_test@firma.ro", + "test_parola": "parolaRARtest", + # prod_enabled absent -> rar_prod_enabled setat la 0 + }) + assert resp.status_code == 200 + + from app.db import get_connection + conn = get_connection() + try: + row = conn.execute( + "SELECT rar_test_enabled, rar_creds_test_enc, rar_prod_enabled FROM accounts WHERE id=?", + (acct_id,), + ).fetchone() + finally: + conn.close() + + assert row["rar_test_enabled"] == 1, "rar_test_enabled trebuia setat la 1" + assert row["rar_creds_test_enc"] is not None, "rar_creds_test_enc trebuia salvat" + # Indicator test_disponibil sau mesaj succes in HTML + assert "configurat" in resp.text or "salvate si validate" in resp.text, \ + f"Indicator 'configurat' sau mesaj succes lipsa: {resp.text[:600]}" + + +def test_default_doar_dintre_disponibile(client, monkeypatch): + """Incerc sa setez rar_env_default pe un mediu indisponibil -> valoarea veche ramane + eroare. + Setarea pe mediu disponibil reuseste. + """ + _mock_login_ok(monkeypatch) + acct_id, _ = _create_account_user("Firma T2", "t2@test.com") + _login(client, "t2@test.com", "parolasecreta10") + + # Pasul 1: activeaza Testare cu creds + seteaza default=test (test va fi singurul disponibil) + csrf = _get_csrf(client) + resp1 = client.post("/cont/rar-medii", data={ + "csrf_token": csrf, + "test_enabled": "1", + "test_email": "rar_test@firma.ro", + "test_parola": "parolaRAR123", + "rar_env_default": "test", + # prod_enabled absent -> rar_prod_enabled=0 (prod indisponibil) + }) + assert resp1.status_code == 200 + assert "actualizat" in resp1.text.lower(), \ + f"Mesaj 'actualizat' asteptat pentru setarea default=test: {resp1.text[:500]}" + + from app.db import get_connection + conn = get_connection() + try: + row1 = conn.execute( + "SELECT rar_env_default FROM accounts WHERE id=?", (acct_id,) + ).fetchone() + finally: + conn.close() + assert row1["rar_env_default"] == "test", "rar_env_default trebuia setat la 'test'" + + # Pasul 2: incerc sa setez default=prod (prod indisponibil: enabled=0, fara creds) + csrf = _get_csrf(client) + resp2 = client.post("/cont/rar-medii", data={ + "csrf_token": csrf, + "test_enabled": "1", # re-trimit enabled fara creds noi (creds existente raman) + "rar_env_default": "prod", + }) + assert resp2.status_code == 200 + assert "disponibil" in resp2.text.lower(), \ + f"Eroare 'nu e disponibil' asteptata in raspuns: {resp2.text[:500]}" + + conn = get_connection() + try: + row2 = conn.execute( + "SELECT rar_env_default FROM accounts WHERE id=?", (acct_id,) + ).fetchone() + finally: + conn.close() + assert row2["rar_env_default"] == "test", \ + "rar_env_default NU trebuia schimbat la 'prod' (mediu indisponibil)" + + +def test_activare_prod_cere_confirmare(client, monkeypatch): + """Prima activare Productie (de la dezactivat) fara prod_confirmare -> NU se activeaza. + Cu prod_confirmare=1 -> rar_prod_enabled devine 1. + """ + _mock_login_ok(monkeypatch) + acct_id, _ = _create_account_user("Firma T3", "t3@test.com") + _login(client, "t3@test.com", "parolasecreta10") + + # Pasul 0: dezactiveaza prod (schema default=1, trebuie adus la 0 pt a testa confirmarea) + csrf = _get_csrf(client) + client.post("/cont/rar-medii", data={ + "csrf_token": csrf, + # prod_enabled absent -> rar_prod_enabled setat la 0 + # test_enabled absent -> rar_test_enabled setat la 0 + }) + + from app.db import get_connection + conn = get_connection() + try: + row0 = conn.execute( + "SELECT rar_prod_enabled FROM accounts WHERE id=?", (acct_id,) + ).fetchone() + finally: + conn.close() + assert row0["rar_prod_enabled"] == 0, "Prod trebuia dezactivat in pasul 0" + + # Pasul 1: incerc sa activez prod FARA confirmare -> refuzat + csrf = _get_csrf(client) + resp1 = client.post("/cont/rar-medii", data={ + "csrf_token": csrf, + "prod_enabled": "1", + "prod_email": "rar_prod@firma.ro", + "prod_parola": "parolaRARprod", + # prod_confirmare absent + }) + assert resp1.status_code == 200 + text1 = resp1.text.lower() + assert "confirmare" in text1 or "l.142" in text1 or "inteleg" in text1, \ + f"Mesaj de confirmare asteptat in raspuns: {resp1.text[:600]}" + + conn = get_connection() + try: + row1 = conn.execute( + "SELECT rar_prod_enabled FROM accounts WHERE id=?", (acct_id,) + ).fetchone() + finally: + conn.close() + assert row1["rar_prod_enabled"] == 0, "rar_prod_enabled NU trebuia activat fara confirmare" + + # Pasul 2: activeaza cu confirmare -> reuseste + csrf = _get_csrf(client) + resp2 = client.post("/cont/rar-medii", data={ + "csrf_token": csrf, + "prod_enabled": "1", + "prod_email": "rar_prod@firma.ro", + "prod_parola": "parolaRARprod", + "prod_confirmare": "1", + }) + assert resp2.status_code == 200 + + conn = get_connection() + try: + row2 = conn.execute( + "SELECT rar_prod_enabled FROM accounts WHERE id=?", (acct_id,) + ).fetchone() + finally: + conn.close() + assert row2["rar_prod_enabled"] == 1, "rar_prod_enabled trebuia activat cu confirmare" + + +def test_creds_criptate_fara_echo(client, monkeypatch): + """Dupa salvare, rar_creds_test_enc e criptat (nu parola in clar) si + parola NU apare in HTML-ul raspuns. + """ + _mock_login_ok(monkeypatch) + acct_id, _ = _create_account_user("Firma T4", "t4@test.com") + _login(client, "t4@test.com", "parolasecreta10") + + parola_test = "SECRETPAROLATEST999" + csrf = _get_csrf(client) + resp = client.post("/cont/rar-medii", data={ + "csrf_token": csrf, + "test_enabled": "1", + "test_email": "rar_test@firma.ro", + "test_parola": parola_test, + }) + assert resp.status_code == 200 + + # Parola NU trebuie sa apara in HTML-ul raspuns + assert parola_test not in resp.text, \ + f"Parola apare in raspunsul HTML (echo interzis): {resp.text[:500]}" + + # In DB: rar_creds_test_enc e criptat (nu contine parola in clar) + from app.db import get_connection + from app.crypto import decrypt_creds + + conn = get_connection() + try: + row = conn.execute( + "SELECT rar_creds_test_enc FROM accounts WHERE id=?", (acct_id,) + ).fetchone() + finally: + conn.close() + + enc = row["rar_creds_test_enc"] + assert enc is not None, "rar_creds_test_enc trebuia salvat" + assert parola_test not in enc, "Parola in clar gasita in rar_creds_test_enc (neacceptat)" + + # Decriptarea trebuie sa recupereze parola originala + creds = decrypt_creds(enc) + assert creds is not None, "Decriptarea a returnat None" + assert creds.get("password") == parola_test, \ + f"Parola decriptata nu corespunde: {creds!r}"