Reguli text per cont (operation_text_rules), resolve_prestatii cu param aditiv text_rules + precedenta stricta, threadat pe toate cele 6 callsite-uri + valid_codes + seam classify_prezentare. UI Mapari: sectiune reguli + preview pre-salvare + overlap + telemetrie text_rule_hit. UX tabel: cod_rar sub operatie, pill eticheta scurta, fara scroll orizontal (scopat .tabel-trimiteri + carduri <768px), detaliu inline expandabil (a11y + pauza poll). code-review: reparat regula auto_send=0 care trimitea automat la RAR in loc sa tina randul pentru review. 814 passed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
252 lines
8.9 KiB
Python
252 lines
8.9 KiB
Python
"""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"
|