"""US-010: Telemetrie hit regula text in `app_events`. Cand o operatie nemapata primeste cod RAR dintr-o regula text (substring), apelantii cu `conn` (ingestie API, import, re-rezolvare) emit un eveniment `text_rule_hit` in `app_events` cu {submission_id, account_id, pattern, cod_prestatie}. Maparea exacta (cod_op_service -> cod_prestatie) NU emite acest eveniment. Evenimentul e redactat (fara PII) si scoped pe cont. Folosim `OE-2`, cod valid din seed-ul nomenclatorului. """ from __future__ import annotations import io import json import os import re import tempfile import openpyxl import pytest from fastapi.testclient import TestClient # --------------------------------------------------------------------------- # # Fixtures # # --------------------------------------------------------------------------- # @pytest.fixture() def api_env(monkeypatch): """Client API + get_connection, DB temporara izolata (fara web-auth).""" tmp = tempfile.mkdtemp() monkeypatch.setenv("AUTOPASS_DB_PATH", os.path.join(tmp, "rr.db")) from app.config import get_settings get_settings.cache_clear() from app.db import get_connection from app.main import app with TestClient(app) as c: yield c, get_connection get_settings.cache_clear() # --------------------------------------------------------------------------- # # Helpere # # --------------------------------------------------------------------------- # def _seed_text_rule(get_connection, account_id, pattern, cod, auto_send=True): from app.mapping import save_text_rule conn = get_connection() try: save_text_rule(conn, account_id, pattern, cod, auto_send=auto_send) conn.commit() finally: conn.close() def _seed_mapping(get_connection, account_id, op, cod, auto_send=True): from app.mapping import save_mapping conn = get_connection() try: save_mapping(conn, account_id, op, cod, auto_send=auto_send) conn.commit() finally: conn.close() def _text_rule_hits(get_connection): conn = get_connection() try: rows = conn.execute( "SELECT account_id, context_json FROM app_events WHERE tip='text_rule_hit' ORDER BY id" ).fetchall() return [dict(r) for r in rows] finally: conn.close() _VIN = "WVWZZZ1KZAW000123" def _post_prezentare(client, op, denumire=None): body = { "rar_credentials": {"email": "x@y.ro", "password": "parola-secreta"}, "prezentari": [{ "vin": _VIN, "nr_inmatriculare": "B999TST", "data_prestatie": "2026-06-15", "odometru_final": "123456", "prestatii": [{"cod_op_service": op, "denumire": denumire or op}], }], } return client.post("/v1/prezentari", json=body) # --------------------------------------------------------------------------- # # 1. Hit regula -> eveniment # # --------------------------------------------------------------------------- # def test_hit_regula_emite_app_event(api_env): """O operatie rezolvata prin regula text emite `text_rule_hit` cu pattern + cod.""" client, get_connection = api_env _seed_text_rule(get_connection, 1, "verificare", "OE-2") r = _post_prezentare(client, "Verificare frane") assert r.status_code == 200, r.text res = r.json()["results"][0] assert res["status"] == "queued", res sub_id = res["submission_id"] hits = _text_rule_hits(get_connection) assert len(hits) == 1, hits ctx = json.loads(hits[0]["context_json"]) assert ctx["submission_id"] == sub_id assert ctx["account_id"] == 1 assert ctx["pattern"] == "verificare" assert ctx["cod_prestatie"] == "OE-2" # --------------------------------------------------------------------------- # # 2. Maparea exacta NU emite # # --------------------------------------------------------------------------- # def test_mapare_exacta_nu_emite_text_rule_hit(api_env): """O operatie rezolvata prin mapare exacta (cod_op_service) nu emite text_rule_hit.""" client, get_connection = api_env _seed_mapping(get_connection, 1, "Verificare frane", "OE-2") r = _post_prezentare(client, "Verificare frane") assert r.status_code == 200, r.text res = r.json()["results"][0] assert res["status"] == "queued", res hits = _text_rule_hits(get_connection) assert hits == [], hits # --------------------------------------------------------------------------- # # 3. Eveniment redactat + scoped pe cont # # --------------------------------------------------------------------------- # def test_event_redactat_si_scoped_pe_cont(api_env): """Evenimentul e scoped pe cont (coloana account_id) si nu contine PII (VIN integral).""" client, get_connection = api_env _seed_text_rule(get_connection, 1, "verificare", "OE-2") r = _post_prezentare(client, "Verificare frane") assert r.status_code == 200, r.text hits = _text_rule_hits(get_connection) assert len(hits) == 1, hits assert hits[0]["account_id"] == 1 # Fara PII: VIN-ul integral nu apare in context. assert _VIN not in (hits[0]["context_json"] or "") # --------------------------------------------------------------------------- # # 4. Calea web import commit (routes.py -> web_confirma_import) # # --------------------------------------------------------------------------- # @pytest.fixture() def web_env(monkeypatch): """Client web (mod dev) + get_connection + cont creat; require_login fixat pe cont.""" tmp = tempfile.mkdtemp() monkeypatch.setenv("AUTOPASS_DB_PATH", os.path.join(tmp, "rrw.db")) monkeypatch.setenv("AUTOPASS_WEB_AUTH_REQUIRED", "false") from app.config import get_settings get_settings.cache_clear() from app.web import ratelimit ratelimit._hits.clear() from app.accounts import create_account from app.db import get_connection from app.main import app with TestClient(app, follow_redirects=False) as c: conn = get_connection() acct = create_account(conn, "Cont Web Telemetrie", active=True) conn.commit() conn.close() monkeypatch.setattr("app.web.routes.require_login", lambda r: acct) yield c, get_connection, acct ratelimit._hits.clear() get_settings.cache_clear() def _make_xlsx(rows): wb = openpyxl.Workbook() ws = wb.active if rows: ws.append(list(rows[0].keys())) for r in rows: ws.append(list(r.values())) buf = io.BytesIO() wb.save(buf) return buf.getvalue() def _csrf_from(html): m = re.search(r'name="csrf_token" value="([^"]*)"', html) return m.group(1) if m else "" def test_web_import_commit_emite_text_rule_hit(web_env): """Import web commit pe un rand a carui operatie da match pe o regula text emite `text_rule_hit` in app_events cu submission_id-ul randului creat.""" client, get_connection, acct = web_env _seed_text_rule(get_connection, acct, "verificare", "OE-2") rows = [{ "vin": "WVWZZZ1KZAW001111", "nr_inmatriculare": "B100TST", "data_prestatie": "2026-06-15", "odometru_final": "123456", "operatie": "Verificare frane", }] r_up = client.post( "/_import/upload", files={"file": ("t.xlsx", _make_xlsx(rows), "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")}, ) assert r_up.status_code == 200, r_up.text csrf = _csrf_from(r_up.text) batch_id = int(re.search(r"/_import/(\d+)/mapare-coloane", r_up.text).group(1)) r_map = client.post( f"/_import/{batch_id}/mapare-coloane", data={ "csrf_token": csrf, "colname": ["vin", "nr_inmatriculare", "data_prestatie", "odometru_final", "operatie"], "canon": ["vin", "nr_inmatriculare", "data_prestatie", "odometru_final", "operatie"], }, ) assert r_map.status_code == 200, r_map.text csrf = _csrf_from(r_map.text) or csrf r_commit = client.post(f"/_import/{batch_id}/confirma", data={"n_confirmat": "1", "csrf_token": csrf}) assert r_commit.status_code == 200, r_commit.text conn = get_connection() try: sub = conn.execute( "SELECT id, status FROM submissions WHERE account_id=? AND batch_id=?", (acct, batch_id) ).fetchone() finally: conn.close() assert sub is not None, "submission-ul nu a fost creat" assert sub["status"] == "queued" hits = _text_rule_hits(get_connection) assert len(hits) == 1, hits ctx = json.loads(hits[0]["context_json"]) assert ctx["submission_id"] == sub["id"] assert ctx["account_id"] == acct assert ctx["pattern"] == "verificare" assert ctx["cod_prestatie"] == "OE-2"