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,226 @@
"""Teste US-005 — camp rar_env pe POST /v1/prezentari si /valideaza.
Acopera: default din cont, tinta explicita, respingere tinta indisponibila,
echo GET, valoare invalida (422 Pydantic), echo dry-run valideaza.
"""
from __future__ import annotations
import os
import tempfile
import pytest
from fastapi.testclient import TestClient
@pytest.fixture()
def env(monkeypatch):
"""DB temporara izolata per test + settings reincarcate."""
tmp = tempfile.mkdtemp()
monkeypatch.setenv("AUTOPASS_DB_PATH", os.path.join(tmp, "t.db"))
from app.config import get_settings
get_settings.cache_clear()
yield monkeypatch
get_settings.cache_clear()
def _client():
from app.main import app
return TestClient(app)
def _body(rar_env=None, **over):
prez = {
"vin": "WVWZZZ1KZAW000123",
"nr_inmatriculare": "B999TST",
"data_prestatie": "2026-06-15",
"odometru_final": "123456",
"prestatii": [{"cod_prestatie": "OE-1"}],
}
prez.update(over)
body = {"rar_credentials": {"email": "x@y.ro", "password": "s"}, "prezentari": [prez]}
if rar_env is not None:
body["rar_env"] = rar_env
return body
def _setup_prod_only(conn):
"""Configureaza contul 1 ca prod-only (rar_prod_enabled=1, creds prod, default prod)."""
from app.crypto import encrypt_creds
enc = encrypt_creds({"email": "prod@rar.ro", "password": "paraprod"})
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",
(enc,),
)
conn.commit()
def _setup_dual_env(conn):
"""Configureaza contul 1 cu ambele medii disponibile, default test."""
from app.crypto import encrypt_creds
enc_test = encrypt_creds({"email": "test@rar.ro", "password": "paratest"})
enc_prod = encrypt_creds({"email": "prod@rar.ro", "password": "paraprod"})
conn.execute(
"UPDATE accounts SET rar_prod_enabled=1, rar_creds_prod_enc=?, "
"rar_test_enabled=1, rar_creds_test_enc=?, rar_env_default='test' WHERE id=1",
(enc_prod, enc_test),
)
conn.commit()
# --------------------------------------------------------------------------- #
# test_default_din_cont_cand_lipseste #
# --------------------------------------------------------------------------- #
def test_default_din_cont_cand_lipseste(env):
"""Cont prod-only, POST fara rar_env -> submission rar_env='prod'."""
with _client() as c:
from app.db import get_connection
conn = get_connection()
try:
_setup_prod_only(conn)
finally:
conn.close()
r = c.post("/v1/prezentari", json=_body())
assert r.status_code == 200, r.text
res = r.json()["results"][0]
assert res["status"] == "queued"
assert res["rar_env"] == "prod"
# --------------------------------------------------------------------------- #
# test_target_explicit #
# --------------------------------------------------------------------------- #
def test_target_explicit(env):
"""Cont cu ambele medii, POST cu rar_env='test' -> submission rar_env='test'."""
with _client() as c:
from app.db import get_connection
conn = get_connection()
try:
_setup_dual_env(conn)
finally:
conn.close()
r = c.post("/v1/prezentari", json=_body(rar_env="test"))
assert r.status_code == 200, r.text
res = r.json()["results"][0]
assert res["status"] == "queued"
assert res["rar_env"] == "test"
# Aceeasi prezentare cu rar_env='prod' -> cheie diferita (env-aware) -> alt submission
r2 = c.post("/v1/prezentari", json=_body(rar_env="prod"))
assert r2.status_code == 200, r2.text
res2 = r2.json()["results"][0]
assert res2["rar_env"] == "prod"
# Nu e dedup (env diferit -> cheie diferita)
assert res2["submission_id"] != res["submission_id"]
# --------------------------------------------------------------------------- #
# test_target_indisponibil_respins #
# --------------------------------------------------------------------------- #
def test_target_indisponibil_respins(env):
"""Cont prod-only, POST cu rar_env='test' -> 422 RAR_MEDIU_INDISPONIBIL, fara enqueue."""
with _client() as c:
from app.db import get_connection
conn = get_connection()
try:
_setup_prod_only(conn)
finally:
conn.close()
r = c.post("/v1/prezentari", json=_body(rar_env="test"))
assert r.status_code == 422, r.text
detail = r.json()["detail"]
assert detail["cod"] == "RAR_MEDIU_INDISPONIBIL"
# Cauza contine mediul cerut si lista disponibilelor
assert "test" in detail["cauza"]
assert "prod" in detail["cauza"]
# Verifica ca nu s-a facut enqueue
lista = c.get("/v1/prezentari").json()["submissions"]
assert lista == []
# --------------------------------------------------------------------------- #
# test_get_ecou_rar_env #
# --------------------------------------------------------------------------- #
def test_get_ecou_rar_env(env):
"""Dupa enqueue, GET /v1/prezentari/{id} si lista contin rar_env."""
with _client() as c:
from app.db import get_connection
conn = get_connection()
try:
_setup_prod_only(conn)
finally:
conn.close()
# Enqueue pe prod (default contului prod-only)
r = c.post("/v1/prezentari", json=_body())
assert r.status_code == 200, r.text
sub_id = r.json()["results"][0]["submission_id"]
assert sub_id is not None
# GET detaliu
r_det = c.get(f"/v1/prezentari/{sub_id}")
assert r_det.status_code == 200, r_det.text
assert r_det.json()["rar_env"] == "prod"
# GET lista
r_lst = c.get("/v1/prezentari")
assert r_lst.status_code == 200, r_lst.text
sub_in_lista = next(s for s in r_lst.json()["submissions"] if s["id"] == sub_id)
assert sub_in_lista["rar_env"] == "prod"
# --------------------------------------------------------------------------- #
# test_valoare_invalida_422 #
# --------------------------------------------------------------------------- #
def test_valoare_invalida_422(env):
"""POST cu rar_env='staging' -> 422 din Pydantic Literal, fara echo de input."""
with _client() as c:
body = _body(rar_env="staging")
r = c.post("/v1/prezentari", json=body)
assert r.status_code == 422, r.text
# Handler-ul global sterge 'input'/'ctx' — valoarea invalida nu se ecou-ieste.
assert "staging" not in r.text
# Fara enqueue
lista = c.get("/v1/prezentari").json()["submissions"]
assert lista == []
# --------------------------------------------------------------------------- #
# test_valideaza_ecou_rar_env #
# --------------------------------------------------------------------------- #
def test_valideaza_ecou_rar_env(env):
"""POST /valideaza (dry-run) ecou-ieste rar_env rezolvat in ValidareResult."""
with _client() as c:
from app.db import get_connection
conn = get_connection()
try:
_setup_prod_only(conn)
finally:
conn.close()
# Dry-run fara rar_env -> default cont = prod
r = c.post("/v1/prezentari/valideaza", json=_body())
assert r.status_code == 200, r.text
res = r.json()["results"][0]
assert res["rar_env"] == "prod"
# Dry-run cu rar_env='prod' explicit
r2 = c.post("/v1/prezentari/valideaza", json=_body(rar_env="prod"))
assert r2.status_code == 200, r2.text
assert r2.json()["results"][0]["rar_env"] == "prod"
# Dry-run tinta indisponibila -> 422, fara echo sensibil
r3 = c.post("/v1/prezentari/valideaza", json=_body(rar_env="test"))
assert r3.status_code == 422, r3.text
assert r3.json()["detail"]["cod"] == "RAR_MEDIU_INDISPONIBIL"

View File

@@ -157,7 +157,7 @@ class FakeRarClient:
made: list = []
def __init__(self, settings=None, login_exc=None):
def __init__(self, settings=None, *, base_url=None, login_exc=None):
self.closed = False
self.login_calls = 0
self._login_exc = login_exc
@@ -234,7 +234,7 @@ def test_get_token_bad_creds_raises(env, monkeypatch):
import app.worker.__main__ as w
from app.db import get_connection
def _factory(settings=None):
def _factory(settings=None, **kwargs):
return FakeRarClient(settings, login_exc=RarAuthError("Credentiale RAR invalide", status_code=401))
monkeypatch.setattr(w, "RarClient", _factory)

View File

@@ -511,7 +511,7 @@ class TestE2EMixedQueue:
# 4. Worker cu MockRar injectat prin AccountSessions (simulam bucla worker)
mock_rar = MockRar(id_prezentare=66001, login_token="tok-mock")
monkeypatch.setattr(w, "RarClient", lambda settings=None: mock_rar)
monkeypatch.setattr(w, "RarClient", lambda settings=None, **kw: mock_rar)
sessions = w.AccountSessions(settings)
conn = get_connection()
@@ -595,7 +595,7 @@ class TestE2EMixedQueue:
conn.close()
mock_rar = MockRar(id_prezentare=77777)
monkeypatch.setattr(w, "RarClient", lambda settings=None: mock_rar)
monkeypatch.setattr(w, "RarClient", lambda settings=None, **kw: mock_rar)
sessions = w.AccountSessions(settings)
conn = get_connection()

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"
)

View File

@@ -0,0 +1,96 @@
"""Teste US-004 (PRD 5.20): rezolvare mediu tinta la ingestie + respingere tinte indisponibile.
Fixtura `conn` urmareste acelasi pattern ca tests/test_accounts.py:
monkeypatch AUTOPASS_DB_PATH pe tempdir, cache_clear, init_db, get_connection.
"""
from __future__ import annotations
import os
import tempfile
import pytest
@pytest.fixture()
def conn(monkeypatch):
tmp = tempfile.mkdtemp()
monkeypatch.setenv("AUTOPASS_DB_PATH", os.path.join(tmp, "test_rar_env_resolve.db"))
from app.config import get_settings
get_settings.cache_clear()
from app.db import get_connection, init_db
init_db()
c = get_connection()
yield c
c.close()
get_settings.cache_clear()
def _seteaza_cont_ambele(conn) -> None:
"""Configureaza contul id=1 cu ambele medii disponibile, default = prod."""
conn.execute(
"""UPDATE accounts
SET rar_test_enabled=1, rar_creds_test_enc='T',
rar_prod_enabled=1, rar_creds_prod_enc='P',
rar_env_default='prod'
WHERE id=1"""
)
conn.commit()
def _seteaza_cont_doar_prod(conn) -> None:
"""Configureaza contul id=1 cu doar prod disponibil, test off."""
conn.execute(
"""UPDATE accounts
SET rar_test_enabled=0, rar_creds_test_enc=NULL,
rar_prod_enabled=1, rar_creds_prod_enc='P',
rar_env_default='prod'
WHERE id=1"""
)
conn.commit()
def test_cerere_castiga(conn):
"""Cererea explicita bate default-ul contului (prod)."""
_seteaza_cont_ambele(conn)
from app.rar_env import rezolva_rar_env
rezultat = rezolva_rar_env(conn, 1, "test")
assert rezultat == "test"
def test_fallback_default_cont(conn):
"""Fara cerere explicita -> default-ul contului (prod)."""
_seteaza_cont_ambele(conn)
from app.rar_env import rezolva_rar_env
rezultat = rezolva_rar_env(conn, 1, None)
assert rezultat == "prod"
def test_tinta_indisponibila_respinsa(conn):
"""Cerere pentru 'test' pe un cont doar-prod -> MediuIndisponibil cu .disponibile=['prod']."""
_seteaza_cont_doar_prod(conn)
from app.rar_env import MediuIndisponibil, rezolva_rar_env
with pytest.raises(MediuIndisponibil) as exc_info:
rezolva_rar_env(conn, 1, "test")
err = exc_info.value
assert err.env == "test"
assert err.disponibile == ["prod"]
def test_valoare_invalida(conn):
"""Cerere cu valoare in afara VALID_ENVS -> ValueError, fara fallback silentios."""
from app.rar_env import rezolva_rar_env
with pytest.raises(ValueError, match="mediu invalid"):
rezolva_rar_env(conn, 1, "staging")
def test_zero_medii_cade_pe_ancora(conn, monkeypatch):
"""Cont fara niciun mediu disponibil -> ancora globala AUTOPASS_RAR_ENV."""
# id=1 din fresh DB: rar_prod_enabled=1 dar rar_creds_prod_enc=NULL -> 0 disponibile
# (valoarea implicita a schemei: prod enabled fara creds -> nedisponibil)
monkeypatch.setenv("AUTOPASS_RAR_ENV", "test")
from app.config import get_settings
get_settings.cache_clear()
from app.rar_env import rezolva_rar_env
rezultat = rezolva_rar_env(conn, 1, None)
assert rezultat == "test"

View File

@@ -39,7 +39,7 @@ def env(monkeypatch):
class FakeRar:
"""Stub RarClient pentru teste."""
def __init__(self, settings=None):
def __init__(self, settings=None, *, base_url=None):
self.login_calls = 0
self.closed = False

View File

@@ -42,10 +42,10 @@ class _FakeSessions:
self.invalidated: list[int] = []
self.tokens: list[int] = []
def invalidate(self, account_id: int) -> None:
def invalidate(self, account_id: int, rar_env=None) -> None:
self.invalidated.append(account_id)
def get_token(self, conn, account_id: int, creds) -> str | None:
def get_token(self, conn, account_id: int, creds, rar_env="test") -> str | None:
self.tokens.append(account_id)
if self._fail:
raise RuntimeError("RAR jos")

View File

@@ -63,7 +63,7 @@ class FakeRar:
def test_login_reusit_logat(env, monkeypatch):
conn, settings = env
import app.worker.__main__ as w
monkeypatch.setattr(w, "RarClient", lambda s: FakeRar())
monkeypatch.setattr(w, "RarClient", lambda s, **kw: FakeRar())
sessions = w.AccountSessions(settings)
tok = sessions.get_token(conn, 2, {"email": "a@b.ro", "password": "secretaXY"})
assert tok == "JWT-TEST"
@@ -80,7 +80,7 @@ def test_login_reusit_logat(env, monkeypatch):
def test_login_401_logat_fara_parola(env, monkeypatch):
conn, settings = env
import app.worker.__main__ as w
monkeypatch.setattr(w, "RarClient", lambda s: FakeRar(login_exc=RarAuthError("401", status_code=401)))
monkeypatch.setattr(w, "RarClient", lambda s, **kw: FakeRar(login_exc=RarAuthError("401", status_code=401)))
sessions = w.AccountSessions(settings)
with pytest.raises(RarAuthError):
sessions.get_token(conn, 3, {"email": "a@b.ro", "password": "parolaGRESITA"})

View File

@@ -0,0 +1,326 @@
"""Teste US-006 (PRD 5.20) — sesiuni si trimitere worker per (cont, env).
Verifica:
- AccountSessions re-cheiat pe (account_id, rar_env): doua env ale aceluiasi cont
au sesiuni distincte.
- RarClient creat cu base_url-ul mediului (test -> rar_base_url_test,
prod -> rar_base_url_prod), nu ancora globala.
- Creds extrase din slotul accounts.rar_creds_{env}_enc corect per env.
- Purjarea creds efemere scoped pe (account_id, rar_env): login pe test NU sterge
creds efemere ale submission-urilor PROD ale aceluiasi cont (auto-fix E1/1a).
- recover_orphans per (cont, env): orfanii prod reconciliati contra endpoint prod,
nu contra test (auto-fix 1b/E6).
"""
from __future__ import annotations
import json
import os
import tempfile
import pytest
from cryptography.fernet import Fernet
# ---------------------------------------------------------------------------
# Fixture DB
# ---------------------------------------------------------------------------
@pytest.fixture()
def env(monkeypatch):
tmp = tempfile.mkdtemp()
monkeypatch.setenv("AUTOPASS_DB_PATH", os.path.join(tmp, "t.db"))
monkeypatch.setenv("AUTOPASS_CREDS_KEY", Fernet.generate_key().decode())
monkeypatch.setenv("AUTOPASS_WORKER_USE_TEST_CREDS", "false")
monkeypatch.setenv("AUTOPASS_REQUIRE_API_KEY", "false")
from app.config import get_settings
from app import crypto
get_settings.cache_clear()
crypto.reset_cache()
from app.db import get_connection, init_db
init_db()
conn = get_connection()
settings = get_settings()
yield conn, settings
conn.close()
get_settings.cache_clear()
crypto.reset_cache()
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
_CONTENT = {
"vin": "WVWZZZ1KZAW000123", "nr_inmatriculare": "B999TST",
"data_prestatie": "2026-06-15", "odometru_final": "123456",
"prestatii": [{"cod_prestatie": "OE-1"}], "sistem_reparat": "null",
}
def _insert_sub(conn, account_id=1, rar_env="test", creds_enc=None, status="queued"):
"""Insereaza un submission cu env si creds explicite."""
content = _CONTENT.copy()
cur = conn.execute(
"INSERT INTO submissions "
"(idempotency_key, account_id, status, payload_json, rar_env, rar_creds_enc) "
"VALUES (?, ?, ?, ?, ?, ?)",
(f"k-{os.urandom(4).hex()}", account_id, status, json.dumps(content), rar_env, creds_enc),
)
return int(cur.lastrowid)
def _row(conn, sid):
return conn.execute("SELECT * FROM submissions WHERE id=?", (sid,)).fetchone()
# Captura base_url-urilor clientilor creati de AccountSessions
_created_clients: list = []
class FakeRarClient:
"""RarClient stub care captura base_url-ul pentru assertii."""
def __init__(self, settings=None, *, base_url=None, login_exc=None):
self.base_url = base_url
self._login_exc = login_exc
self.login_calls = 0
self.closed = False
_created_clients.append(self)
def login(self, email, password):
self.login_calls += 1
if self._login_exc:
raise self._login_exc
return f"TOK-{email}-{self.base_url}"
def get_nomenclator(self, token):
return []
def close(self):
self.closed = True
# ---------------------------------------------------------------------------
# test_sesiune_separata_per_env
# ---------------------------------------------------------------------------
def test_sesiune_separata_per_env(env, monkeypatch):
"""Doua submission-uri ale aceluiasi cont, env test + prod -> doua login-uri distincte.
Cheia sesiunii e (account_id, rar_env): sesiunile test si prod sunt independente.
"""
import app.worker.__main__ as w
_created_clients.clear()
monkeypatch.setattr(w, "RarClient", FakeRarClient)
conn, settings = env
# Cont secundar (contul 1 e default din schema)
conn.execute("INSERT INTO accounts (id, name) VALUES (2, 'Cont2')")
conn.commit()
sessions = w.AccountSessions(settings)
creds_test = {"email": "test@example.ro", "password": "ptest"}
creds_prod = {"email": "prod@example.ro", "password": "pprod"}
tok_test = sessions.get_token(conn, 2, creds_test, "test")
tok_prod = sessions.get_token(conn, 2, creds_prod, "prod")
# Doua login-uri distincte
assert len(_created_clients) == 2
assert _created_clients[0].login_calls == 1
assert _created_clients[1].login_calls == 1
# Tokenuri distincte (de la email-uri diferite)
assert tok_test != tok_prod
# Sesiunile active: doua intrari, ambele pt cont 2, env diferite
active = sessions.active()
assert len(active) == 2
envs_active = {env for _, env, _, _ in active}
assert envs_active == {"test", "prod"}
# Al doilea apel cu acelasi (cont, env) -> cache, NU re-login
tok_test2 = sessions.get_token(conn, 2, creds_test, "test")
assert tok_test2 == tok_test
assert len(_created_clients) == 2 # niciun client nou creat
# ---------------------------------------------------------------------------
# test_base_url_dupa_submission
# ---------------------------------------------------------------------------
def test_base_url_dupa_submission(env, monkeypatch):
"""Un submission prod foloseste rar_base_url_prod; un submission test foloseste rar_base_url_test."""
import app.worker.__main__ as w
_created_clients.clear()
monkeypatch.setattr(w, "RarClient", FakeRarClient)
conn, settings = env
sessions = w.AccountSessions(settings)
creds = {"email": "x@example.ro", "password": "pw"}
sessions.get_token(conn, 1, creds, "test")
sessions.get_token(conn, 1, creds, "prod")
urls = {c.base_url for c in _created_clients}
assert settings.rar_base_url_test in urls, f"URL test asteptat in {urls}"
assert settings.rar_base_url_prod in urls, f"URL prod asteptat in {urls}"
# Cele doua URL-uri trebuie sa fie diferite (sisteme RAR separate)
assert settings.rar_base_url_test != settings.rar_base_url_prod
# ---------------------------------------------------------------------------
# test_creds_din_slotul_env
# ---------------------------------------------------------------------------
def test_creds_din_slotul_env(env, monkeypatch):
"""Cand submissions.rar_creds_enc lipseste, worker ia din accounts.rar_creds_{env}_enc.
Prod ia din rar_creds_prod_enc, nu din slotul test (auto-fix 1c/E8 + fallback per-env).
"""
import app.worker.__main__ as w
from app.crypto import encrypt_creds
conn, settings = env
enc_test = encrypt_creds({"email": "test@rar.ro", "password": "ptest"})
enc_prod = encrypt_creds({"email": "prod@rar.ro", "password": "pprod"})
# Salveaza creds in ambele sloturi per-env
conn.execute(
"UPDATE accounts SET rar_creds_test_enc=?, rar_creds_prod_enc=? WHERE id=1",
(enc_test, enc_prod),
)
conn.commit()
# Fara creds efemere pe submission -> fallback la slotul per-env
creds_test = w._creds_from_account(conn, 1, "test")
creds_prod = w._creds_from_account(conn, 1, "prod")
assert creds_test is not None, "slotul test trebuia sa aiba creds"
assert creds_test["email"] == "test@rar.ro"
assert creds_prod is not None, "slotul prod trebuia sa aiba creds"
assert creds_prod["email"] == "prod@rar.ro"
# Crucialmente: prod NU ia creds din slotul test
assert creds_prod["email"] != creds_test["email"]
# ---------------------------------------------------------------------------
# test_purge_creds_doar_pe_env (auto-fix E1/1a)
# ---------------------------------------------------------------------------
def test_purge_creds_doar_pe_env(env, monkeypatch):
"""Dupa login pe env=test, creds efemere ale submission-urilor PROD raman neatinse.
Scopul purjarii: WHERE account_id=? AND rar_env=?. Altfel un login TEST sterge
creds ale submission-urilor PROD -> prod blocat (auto-fix E1/1a).
"""
import app.worker.__main__ as w
from app.crypto import encrypt_creds
_created_clients.clear()
monkeypatch.setattr(w, "RarClient", FakeRarClient)
conn, settings = env
enc = encrypt_creds({"email": "u@rar.ro", "password": "pw"})
# Doua submission-uri ale aceluiasi cont: unul test, unul prod (ambele cu creds efemere)
sid_test = _insert_sub(conn, account_id=1, rar_env="test", creds_enc=enc)
sid_prod = _insert_sub(conn, account_id=1, rar_env="prod", creds_enc=enc)
sessions = w.AccountSessions(settings)
# Login pe env=test
sessions.get_token(conn, 1, {"email": "u@rar.ro", "password": "pw"}, "test")
# Creds efemere ale submission-ului TEST trebuie sterse (purjare normala)
row_test = _row(conn, sid_test)
assert row_test["rar_creds_enc"] is None, "creds test trebuiau sterse dupa login test"
# Creds efemere ale submission-ului PROD trebuie PASTRATE (nu sunt pentru env=test)
row_prod = _row(conn, sid_prod)
assert row_prod["rar_creds_enc"] is not None, \
"creds prod NU trebuiau sterse la login test (auto-fix E1/1a)"
# ---------------------------------------------------------------------------
# test_reconcile_pe_env_corect (auto-fix 1b/E6)
# ---------------------------------------------------------------------------
def test_reconcile_pe_env_corect(env, monkeypatch):
"""Un orfan env=prod e reconciliat contra endpoint PROD, nu contra test.
auto-fix 1b/E6: recover_orphans filtreaza pe rar_env si foloseste clientul/token-ul
env-ului corect. Orfanii prod contra endpoint test -> no-match -> re-POST prod =
DUPLICAT real ireversibil.
"""
import app.worker.__main__ as w
conn, settings = env
# Submission prod orfan (sending de mult timp)
sid_prod = _insert_sub(conn, account_id=1, rar_env="prod", status="sending")
conn.execute(
"UPDATE submissions SET sending_since=datetime('now', '-1 hour') WHERE id=?", (sid_prod,)
)
conn.commit()
# Submission test orfan (de verificat ca NU e atins de recover_orphans(rar_env='prod'))
sid_test = _insert_sub(conn, account_id=1, rar_env="test", status="sending")
conn.execute(
"UPDATE submissions SET sending_since=datetime('now', '-1 hour') WHERE id=?", (sid_test,)
)
conn.commit()
# Clientul prod fake — "gaseste" prezentarea prod la RAR
class FakeProdRar:
def __init__(self):
self.get_finalizate_calls = 0
def get_finalizate(self, token):
self.get_finalizate_calls += 1
return [{"id": 9999, "vin": "WVWZZZ1KZAW000123",
"dataPrestatie": "2026-06-15", "odometruFinal": 123456}]
def post_prezentare(self, token, payload):
return {"id": 9999}
# Clientul test fake — nu gaseste nimic (sistemul test nu are prezentarea)
class FakeTestRar:
def __init__(self):
self.get_finalizate_calls = 0
def get_finalizate(self, token):
self.get_finalizate_calls += 1
return [] # nu e la RAR test
def post_prezentare(self, token, payload):
return {"id": 1111}
rar_prod = FakeProdRar()
rar_test = FakeTestRar()
# Apelam recover_orphans cu clientul PROD si env='prod' -> trebuie sa gaseasca orfanul prod
n = w.recover_orphans(conn, settings, rar_prod, "tok-prod", account_id=1, rar_env="prod")
assert n == 1, f"trebuia sa reconcilieze 1 orfan prod, a gasit {n}"
row_prod = _row(conn, sid_prod)
assert row_prod["status"] == "sent", f"orfanul prod trebuia marcat sent, e {row_prod['status']}"
assert row_prod["id_prezentare"] == 9999
# Submission-ul TEST nu trebuia atins de recover_orphans cu rar_env='prod'
row_test = _row(conn, sid_test)
assert row_test["status"] == "sending", \
f"orfanul test NU trebuia atins de recover cu env=prod, e {row_test['status']}"
# Confirmare ca clientul prod a interogat finalizate (reconciliere pe endpoint corect)
assert rar_prod.get_finalizate_calls == 1
# Clientul test NU trebuia folosit (recover_orphans cu env=prod NU atinge endpoint test)
assert rar_test.get_finalizate_calls == 0