feat(5.20): US-004/005/006/009 ingestie+API+worker+import pe mediu RAR

US-004: rezolva_rar_env (cerere>default cont>ancora globala) + MediuIndisponibil
+ cod RAR_MEDIU_INDISPONIBIL.
US-005: camp rar_env pe POST /v1/prezentari + /valideaza (Literal), echo in
SubmissionResult/ValidareResult/GET, build_key + INSERT env-aware.
US-006: AccountSessions re-cheiat (account_id, rar_env); RarClient base_url per
env; creds din slotul env; purge + recover_orphans scoped pe env (E1/1a, 1b/E6);
claim_one propaga rar_env (1c/E8); keepalive pe ancora globala (M2).
US-009: selector mediu la import (>=2 medii), eticheta la 1, banner la 0; commit
seteaza rar_env pe submissions.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Claude Agent
2026-06-29 20:30:11 +00:00
parent d5ce0e2e2b
commit 19d8aaa7aa
19 changed files with 1451 additions and 130 deletions

View File

@@ -0,0 +1,359 @@
"""Teste US-009 (PRD 5.20) — Import web: selector mediu RAR conditionat de disponibilitate.
Verifica:
- La 0 medii: banner avertisment non-blocant (upload functioneaza, commit foloseste ancora globala).
- La 1 mediu: eticheta statica, fara selector; submissions primesc acel mediu.
- La 2 medii: selector vizibil pre-bifat pe default-ul contului.
- La commit: toate submission-urile lotului primesc rar_env ales (sau fallback ancora globala).
"""
from __future__ import annotations
import csv
import io
import os
import re
import tempfile
import pytest
from fastapi.testclient import TestClient
# --------------------------------------------------------------------------- #
# Fixture client cu DB izolat #
# --------------------------------------------------------------------------- #
@pytest.fixture()
def client(monkeypatch):
tmp = tempfile.mkdtemp()
monkeypatch.setenv("AUTOPASS_DB_PATH", os.path.join(tmp, "rar_env_test.db"))
monkeypatch.setenv("AUTOPASS_WEB_AUTH_REQUIRED", "false")
monkeypatch.setenv("AUTOPASS_RAR_ENV", "test") # ancora globala = test
from app.config import get_settings
get_settings.cache_clear()
from app.crypto import reset_cache
reset_cache()
from app.main import app
with TestClient(app) as c:
yield c
get_settings.cache_clear()
reset_cache()
# --------------------------------------------------------------------------- #
# Utilitare #
# --------------------------------------------------------------------------- #
def _csv_bytes(rows: list[dict], sep: str = ";") -> bytes:
buf = io.StringIO()
writer = csv.DictWriter(buf, fieldnames=list(rows[0].keys()), delimiter=sep)
writer.writeheader()
writer.writerows(rows)
return buf.getvalue().encode("utf-8")
def _seed_nomenclator_si_mapare(client: TestClient) -> None:
"""Semeaza nomenclatorul si o mapare pentru randuri ok."""
from app.db import get_connection
conn = get_connection()
try:
conn.execute(
"INSERT OR REPLACE INTO nomenclator_rar (cod_prestatie, nume_prestatie) VALUES (?,?)",
("R-FRANE", "Reparatie frane"),
)
conn.execute(
"INSERT OR IGNORE INTO operations_mapping "
"(account_id, cod_op_service, cod_prestatie, auto_send) VALUES (1,?,?,1)",
("OP-FRANE", "R-FRANE"),
)
conn.commit()
finally:
conn.close()
def _configureaza_un_mediu(client: TestClient, env: str = "test") -> None:
"""Activeaza un singur mediu RAR pe contul 1 (simulate creds disponibile)."""
from app.db import get_connection
from app.crypto import encrypt_creds
conn = get_connection()
try:
fake_creds = encrypt_creds({"email": "test@rar.ro", "password": "pass"})
if env == "test":
conn.execute(
"UPDATE accounts SET rar_test_enabled=1, rar_creds_test_enc=?, "
"rar_prod_enabled=0, rar_creds_prod_enc=NULL, rar_env_default='test' WHERE id=1",
(fake_creds,),
)
else:
conn.execute(
"UPDATE accounts SET rar_prod_enabled=1, rar_creds_prod_enc=?, "
"rar_test_enabled=0, rar_creds_test_enc=NULL, rar_env_default='prod' WHERE id=1",
(fake_creds,),
)
conn.commit()
finally:
conn.close()
def _configureaza_doua_medii(client: TestClient, default_env: str = "test") -> None:
"""Activeaza ambele medii RAR pe contul 1."""
from app.db import get_connection
from app.crypto import encrypt_creds
conn = get_connection()
try:
fake_test = encrypt_creds({"email": "test@rar.ro", "password": "pass_test"})
fake_prod = encrypt_creds({"email": "prod@rar.ro", "password": "pass_prod"})
conn.execute(
"UPDATE accounts SET "
"rar_test_enabled=1, rar_creds_test_enc=?, "
"rar_prod_enabled=1, rar_creds_prod_enc=?, "
"rar_env_default=? WHERE id=1",
(fake_test, fake_prod, default_env),
)
conn.commit()
finally:
conn.close()
_ROWS_OK = [
{
"VIN": "WVWZZZ1KZAW009001",
"Nr": "B009TST",
"Data": "2026-06-15",
"KM": "77000",
"Operatie": "OP-FRANE",
},
]
def _upload_si_mapare(client: TestClient, rows: list[dict]) -> int:
"""Upload CSV si seteaza mapare coloane. Intoarce import_id."""
data = _csv_bytes(rows)
r = client.post(
"/_import/upload",
files={"file": ("test.csv", io.BytesIO(data), "text/csv")},
)
assert r.status_code == 200, r.text
m = re.search(r"/_import/(\d+)/", r.text)
assert m, f"import_id negasit in raspunsul de upload: {r.text[:400]}"
iid = int(m.group(1))
# Seteaza maparea daca nu e deja
if f"/_import/{iid}/mapare-coloane" in r.text or "mapare-coloane" in r.text.lower():
r2 = client.post(
f"/_import/{iid}/mapare-coloane",
data={
"colname": ["VIN", "Nr", "Data", "KM", "Operatie"],
"canon": ["vin", "nr_inmatriculare", "data_prestatie", "odometru_final", "operatie"],
"format_data": "YYYY-MM-DD",
},
)
assert r2.status_code == 200, r2.text
return iid
def _get_preview(client: TestClient, iid: int) -> str:
rp = client.get(f"/_import/{iid}/preview")
assert rp.status_code == 200, rp.text
return rp.text
def _commit(client: TestClient, iid: int, n_ok: int, rar_env: str | None = None) -> object:
data = {
"csrf_token": "",
"n_confirmat": str(n_ok),
"confirmed_by": "test@us009.ro",
}
if rar_env:
data["rar_env"] = rar_env
return client.post(f"/_import/{iid}/confirma", data=data)
# --------------------------------------------------------------------------- #
# Tests #
# --------------------------------------------------------------------------- #
def test_selector_ascuns_la_un_mediu(client):
"""La 1 mediu disponibil: nu apare selector; apare eticheta statica cu mediul."""
_seed_nomenclator_si_mapare(client)
_configureaza_un_mediu(client, env="test")
# GET fragment/import: verifica ca nu exista selector si apare eticheta
r = client.get("/_fragments/import")
assert r.status_code == 200, r.text
html = r.text
# Eticheta statica "Testare" trebuie sa fie prezenta
assert "Testare" in html, "Eticheta mediu 'Testare' lipseste la 1 mediu disponibil"
# Selectorul nu trebuie sa apara (input cu name=rar_env hidden, dar fara <select>)
assert "<select" not in html or 'name="rar_env"' not in html or "rar-env-select" not in html, (
"Selector mediu RAR nu trebuie sa apara la 1 mediu disponibil"
)
def test_selector_prezent_si_prebifat_la_doua(client):
"""La 2 medii disponibile: selectorul apare si e pre-bifat pe default-ul contului."""
_seed_nomenclator_si_mapare(client)
_configureaza_doua_medii(client, default_env="test")
r = client.get("/_fragments/import")
assert r.status_code == 200, r.text
html = r.text
# Selectorul trebuie sa apara
assert "rar-env-select" in html, "Selectorul mediu RAR lipseste la 2 medii disponibile"
assert 'name="rar_env"' in html, 'Atribut name="rar_env" lipsa din selector'
# Default pre-selectat = "test" (default contului)
# Optiunea Testare trebuie sa fie selectata
assert 'value="test"' in html and "selected" in html, (
"Optiunea Testare nu e pre-selectata (default cont = test)"
)
def test_banner_avertisment_la_zero_medii(client):
"""La 0 medii configurate: apare un banner de avertisment (non-blocant)."""
# Contul 1 implicit nu are medii configurate
r = client.get("/_fragments/import")
assert r.status_code == 200, r.text
html = r.text
# Banner avertisment sau link catre configurare credentiale
assert "mediu" in html.lower() or "configureaza" in html.lower() or "credentiale" in html.lower(), (
"Bannerul de avertisment pentru 0 medii lipseste din pagina de upload"
)
# Upload-ul NU e blocat: formularul de upload trebuie sa fie prezent
assert "upload-form" in html, (
"Formularul de upload lipseste — la 0 medii upload-ul nu trebuie blocat"
)
def test_commit_seteaza_env_pe_submissions(client):
"""La commit: submissions primesc rar_env ales (fallback la ancora globala pt 0 medii)."""
_seed_nomenclator_si_mapare(client)
# Contul 1 fara medii configurate -> ancora globala = "test"
iid = _upload_si_mapare(client, _ROWS_OK)
preview_html = _get_preview(client, iid)
m_ok = re.search(r'id="n-confirmat"[^>]*?value="(\d+)"', preview_html)
n_ok = int(m_ok.group(1)) if m_ok else 1
r = _commit(client, iid, n_ok)
assert r.status_code == 200, r.text
assert any(kw in r.text.lower() for kw in ("coada", "prezenta", "trimiter")), (
"Mesajul de succes lipseste din raspunsul de commit"
)
# Verifica ca submission-ul are rar_env setat (fallback "test" via ancora globala)
from app.db import get_connection
conn = get_connection()
try:
sub = conn.execute(
"SELECT rar_env FROM submissions WHERE account_id=1 ORDER BY id DESC LIMIT 1"
).fetchone()
finally:
conn.close()
assert sub is not None, "Niciun submission gasit dupa commit"
assert sub["rar_env"] in ("test", "prod"), f"rar_env invalid: {sub['rar_env']!r}"
# Cu AUTOPASS_RAR_ENV=test si 0 medii configurate, expect "test"
assert sub["rar_env"] == "test", (
f"Expected rar_env='test' (ancora globala) dar primit {sub['rar_env']!r}"
)
def test_commit_cu_un_mediu_seteaza_acel_mediu(client):
"""La commit cu 1 mediu configurat: submission primeste mediul respectiv."""
_seed_nomenclator_si_mapare(client)
_configureaza_un_mediu(client, env="test")
iid = _upload_si_mapare(client, _ROWS_OK)
preview_html = _get_preview(client, iid)
m_ok = re.search(r'id="n-confirmat"[^>]*?value="(\d+)"', preview_html)
n_ok = int(m_ok.group(1)) if m_ok else 1
r = _commit(client, iid, n_ok)
assert r.status_code == 200, r.text
from app.db import get_connection
conn = get_connection()
try:
sub = conn.execute(
"SELECT rar_env FROM submissions WHERE account_id=1 ORDER BY id DESC LIMIT 1"
).fetchone()
finally:
conn.close()
assert sub is not None
assert sub["rar_env"] == "test", (
f"Expected rar_env='test' (singurul mediu disponibil) dar primit {sub['rar_env']!r}"
)
def test_commit_cu_doua_medii_respecta_alegerea(client):
"""La 2 medii: commit cu rar_env explicit seteaza mediul ales pe submissions."""
_seed_nomenclator_si_mapare(client)
_configureaza_doua_medii(client, default_env="test")
iid = _upload_si_mapare(client, _ROWS_OK)
preview_html = _get_preview(client, iid)
m_ok = re.search(r'id="n-confirmat"[^>]*?value="(\d+)"', preview_html)
n_ok = int(m_ok.group(1)) if m_ok else 1
# Commit explicit pe "prod"
r = _commit(client, iid, n_ok, rar_env="prod")
assert r.status_code == 200, r.text
from app.db import get_connection
conn = get_connection()
try:
sub = conn.execute(
"SELECT rar_env FROM submissions WHERE account_id=1 ORDER BY id DESC LIMIT 1"
).fetchone()
finally:
conn.close()
assert sub is not None
assert sub["rar_env"] == "prod", (
f"Expected rar_env='prod' (ales explicit) dar primit {sub['rar_env']!r}"
)
def test_badge_mediu_in_preview(client):
"""Preview-ul afiseaza badge-ul cu mediul tinta (US-009, F9/F10)."""
_seed_nomenclator_si_mapare(client)
_configureaza_un_mediu(client, env="test")
iid = _upload_si_mapare(client, _ROWS_OK)
r = client.get(f"/_import/{iid}/preview")
assert r.status_code == 200, r.text
html = r.text
# Badge cu mediul trebuie sa fie prezent in HTML
assert "Testare" in html or "PRODUCTIE" in html or "rar_env" in html, (
"Badge-ul de mediu RAR lipseste din preview"
)
def test_rar_env_in_confirm_form(client):
"""Preview-ul contine un field hidden rar_env in formularul de confirmare."""
_seed_nomenclator_si_mapare(client)
_configureaza_un_mediu(client, env="test")
iid = _upload_si_mapare(client, _ROWS_OK)
r = client.get(f"/_import/{iid}/preview")
assert r.status_code == 200, r.text
html = r.text
# Formularul de confirmare trebuie sa contina rar_env ca hidden field
assert 'name="rar_env"' in html, (
"Campul hidden 'rar_env' lipseste din formularul de confirmare preview"
)