diff --git a/app/web/routes.py b/app/web/routes.py index 55fff6a..6a6223b 100644 --- a/app/web/routes.py +++ b/app/web/routes.py @@ -916,6 +916,10 @@ def _build_status_ctx(request: Request, conn, account_id: int, *, oob: bool = Fa else: sanatate_text = "Declaratiile curg normal" + # US-011 (5.20): mediul RAR activ per cont pentru indicatorul din statusbar. + medii_disp = medii_disponibile_cont(conn, account_id) + env_default = rar_env_efectiv_cont(conn, account_id) or "prod" + status_ctx = { "request": request, "worker_lbl": worker_lbl, @@ -937,6 +941,10 @@ def _build_status_ctx(request: Request, conn, account_id: int, *, oob: bool = Fa "tab_activ": tab_activ, "mapari_badge": counts.get("needs_mapping", 0), "oob": oob, + # US-011: indicator mediu RAR + toggle conditionat + "env_default": env_default, + "medii_disponibile": medii_disp, + "csrf_token": get_csrf_token(request), } # US-006 (5.17): context plan pentru linia de consum/trial in _status.html. status_ctx.update(_plan_ctx(conn, account_id)) @@ -4699,3 +4707,38 @@ async def cont_rar_medii(request: Request) -> HTMLResponse: ) finally: conn.close() + + +@router.post("/_fragments/status/toggle-env", response_class=HTMLResponse) +async def fragment_status_toggle_env(request: Request) -> HTMLResponse: + """Comuta rar_env_default intre mediile disponibile ale contului (US-011, PRD 5.20). + + Valideaza ca noul mediu e in lista mediilor disponibile inainte de UPDATE. + Intoarce statusbar-ul actualizat (acelasi format ca /_fragments/status). + Ignorat silentios daca contul are un singur mediu sau zero medii disponibile. + """ + account_id = require_login(request) + form = await request.form() + verify_csrf(request, str(form.get("csrf_token") or "")) + acct = account_or_default(account_id) + + conn = get_connection() + try: + medii = medii_disponibile_cont(conn, account_id) + if len(medii) >= 2: + env_curent = rar_env_efectiv_cont(conn, account_id) or medii[0] + # Alterneaza la urmatorul mediu din lista disponibile (ciclic) + idx_curent = medii.index(env_curent) if env_curent in medii else 0 + env_nou = medii[(idx_curent + 1) % len(medii)] + # Dubla validare: env_nou trebuie sa fie in medii disponibile + if env_nou in medii: + conn.execute( + "UPDATE accounts SET rar_env_default=? WHERE id=?", + (env_nou, acct), + ) + conn.commit() + + ctx = _build_status_ctx(request, conn, account_id, tab_activ="acasa") + return templates.TemplateResponse("_status.html", ctx) + finally: + conn.close() diff --git a/app/web/templates/_status.html b/app/web/templates/_status.html index ba464a3..b77256f 100644 --- a/app/web/templates/_status.html +++ b/app/web/templates/_status.html @@ -123,6 +123,43 @@ + {# === US-011 (5.20): Indicator mediu RAR activ per cont. + La 0 medii: nu afisa (cont fara configuratie RAR). + La 1 mediu: eticheta statica (fara toggle). + La 2 medii: buton de comutare HTMX (toggle conditionat). + #} + {% set _medii = medii_disponibile | default([]) %} + {% set _env = env_default | default('prod') %} + {% set _env_label = "Testare" if _env == "test" else "Productie" %} + {% if _medii | length >= 1 %} +
+ + Mediu RAR: {{ _env_label }} + + {% if _medii | length >= 2 %} + {# Toggle: apare DOAR cand sunt cel putin 2 medii disponibile #} +
+ + +
+ {% endif %} +
+ {% endif %} + {# === Navigatie rapida: Trimiteri + Mapari cu badge needs_mapping === Pastrata exact ca inainte (US-005): tab_activ determina marcajul activ. #} diff --git a/app/web/templates/base.html b/app/web/templates/base.html index 897d6ce..17f2b7e 100644 --- a/app/web/templates/base.html +++ b/app/web/templates/base.html @@ -834,7 +834,7 @@ Titlul linkeaza la / (Trimiteri) ca si logo-ul. #}
-

ROA AUTOPASS{{ rar_env }}{% if is_authenticated|default(false) and tier_label|default('') %}{{ tier_label }}{% endif %}

+

ROA AUTOPASS{% if not is_authenticated|default(false) %}{{ rar_env }}{% endif %}{% if is_authenticated|default(false) and tier_label|default('') %}{{ tier_label }}{% endif %}

{% if is_authenticated|default(false) and account_name|default('') %}
Service auto: {{ account_name }}
diff --git a/tests/test_statusbar_env.py b/tests/test_statusbar_env.py new file mode 100644 index 0000000..a619e03 --- /dev/null +++ b/tests/test_statusbar_env.py @@ -0,0 +1,232 @@ +"""Teste US-011 (PRD 5.20): indicator mediu RAR in statusbar + toggle conditionat. + +Rute testate: + GET /_fragments/status -- indicator mediu (eticheta statica sau toggle) + POST /_fragments/status/toggle-env -- comuta rar_env_default intre mediile disponibile + +Teste: + test_afiseaza_env_default -- un mediu disponibil -> eticheta statica, fara toggle + test_toggle_doar_la_doua_medii -- doua medii -> toggle prezent; un mediu -> toggle absent + test_toggle_schimba_default -- POST toggle -> rar_env_default schimbat in DB, 200 +""" + +from __future__ import annotations + +import os +import re +import tempfile + +import pytest +from cryptography.fernet import Fernet + + +# --------------------------------------------------------------------------- +# 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_statusbar.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 __import__("starlette.testclient", fromlist=["TestClient"]).TestClient( + app, follow_redirects=False + ) as c: + yield c + + get_settings.cache_clear() + crypto.reset_cache() + + +# --------------------------------------------------------------------------- +# Helpere +# --------------------------------------------------------------------------- + + +def _create_account_user( + name: str = "Service Env SRL", + email: str = "env@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 _seteaza_mediu_disponibil(acct_id: int, env: str) -> None: + """Scrie direct in DB: activeaza mediul `env` cu creds criptate mock. + + Alternativa rapida la POST /cont/rar-medii cu monkeypatch (nu necesita HTTP). + Reutilizeaza cheia Fernet curenta (setata in fixture). + """ + from app.db import get_connection + from app.crypto import encrypt_creds + + enc = encrypt_creds({"email": f"rar_{env}@firma.ro", "password": f"parola_{env}"}) + conn = get_connection() + try: + if env == "test": + conn.execute( + "UPDATE accounts SET rar_test_enabled=1, rar_creds_test_enc=? WHERE id=?", + (enc, acct_id), + ) + elif env == "prod": + conn.execute( + "UPDATE accounts SET rar_prod_enabled=1, rar_creds_prod_enc=? WHERE id=?", + (enc, acct_id), + ) + conn.commit() + finally: + conn.close() + + +def _get_csrf_din_status(client) -> str: + """Obtine CSRF token din fragmentul /_fragments/status.""" + resp = client.get("/_fragments/status") + assert resp.status_code == 200, f"/_fragments/status 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 statusbar: {resp.text[:600]}" + return m.group(1) + + +# --------------------------------------------------------------------------- +# Teste +# --------------------------------------------------------------------------- + + +def test_afiseaza_env_default(client): + """Cont cu UN singur mediu disponibil (prod) -> statusbar contine eticheta 'Productie', + fara niciun control de toggle/comutare. + """ + acct_id, _ = _create_account_user("Firma Env1", "env1@test.com") + _seteaza_mediu_disponibil(acct_id, "prod") + _login(client, "env1@test.com", "parolasecreta10") + + resp = client.get("/_fragments/status") + assert resp.status_code == 200, f"Status HTTP neasteptat: {resp.status_code}" + html = resp.text + + # Eticheta mediului trebuie prezenta + assert "Productie" in html, f"Eticheta 'Productie' absenta in statusbar: {html[:600]}" + + # Nu trebuie sa existe un control de comutare (toggle) la un singur mediu + assert "toggle-env" not in html, \ + "Control toggle gresit prezent la un singur mediu disponibil" + + +def test_toggle_doar_la_doua_medii(client): + """Cont cu DOUA medii disponibile -> statusbar contine controlul de toggle. + Cont cu UN singur mediu -> NU contine toggle. + """ + # Cont cu doua medii + acct2_id, _ = _create_account_user("Firma Env2", "env2@test.com") + _seteaza_mediu_disponibil(acct2_id, "test") + _seteaza_mediu_disponibil(acct2_id, "prod") + _login(client, "env2@test.com", "parolasecreta10") + + resp2 = client.get("/_fragments/status") + assert resp2.status_code == 200 + html2 = resp2.text + assert "toggle-env" in html2, \ + f"Control toggle absent cand sunt doua medii disponibile: {html2[:600]}" + + # Cont cu un singur mediu (test) + acct1_id, _ = _create_account_user("Firma Env1B", "env1b@test.com") + _seteaza_mediu_disponibil(acct1_id, "test") + # Relogin pe alt cont (simulam logout/login rapid prin schimbare sesiune) + _login(client, "env1b@test.com", "parolasecreta10") + + resp1 = client.get("/_fragments/status") + assert resp1.status_code == 200 + html1 = resp1.text + assert "toggle-env" not in html1, \ + f"Control toggle gresit prezent la un singur mediu: {html1[:600]}" + assert "Testare" in html1, \ + f"Eticheta 'Testare' absenta cand mediul unic e 'test': {html1[:600]}" + + +def test_toggle_schimba_default(client): + """POST la /_fragments/status/toggle-env comuta rar_env_default in DB + si intoarce statusbar-ul actualizat (200). + """ + acct_id, _ = _create_account_user("Firma Toggle", "toggle@test.com") + _seteaza_mediu_disponibil(acct_id, "test") + _seteaza_mediu_disponibil(acct_id, "prod") + _login(client, "toggle@test.com", "parolasecreta10") + + # Verifica valoarea initiala in DB (default=prod) + from app.db import get_connection + + conn = get_connection() + try: + row_inainte = conn.execute( + "SELECT rar_env_default FROM accounts WHERE id=?", (acct_id,) + ).fetchone() + finally: + conn.close() + + env_inainte = row_inainte["rar_env_default"] or "prod" + + # Obtine CSRF si face toggle + csrf = _get_csrf_din_status(client) + resp = client.post("/_fragments/status/toggle-env", data={"csrf_token": csrf}) + assert resp.status_code == 200, \ + f"Toggle a returnat {resp.status_code}: {resp.text[:300]}" + + # Verifica ca statusbar-ul actualizat e in raspuns + html = resp.text + assert "status-bar" in html, \ + f"Raspunsul toggle nu contine statusbar: {html[:400]}" + + # Verifica schimbarea in DB + conn = get_connection() + try: + row_dupa = conn.execute( + "SELECT rar_env_default FROM accounts WHERE id=?", (acct_id,) + ).fetchone() + finally: + conn.close() + + env_dupa = row_dupa["rar_env_default"] + assert env_dupa != env_inainte, \ + f"rar_env_default NU s-a schimbat: inainte={env_inainte!r}, dupa={env_dupa!r}" + assert env_dupa in ("test", "prod"), \ + f"rar_env_default are valoare invalida: {env_dupa!r}"