feat(5.8): reguli mapare pe text (substring/cont) + UX tabel trimiteri (detaliu inline, fara scroll, cod RAR)
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>
This commit is contained in:
251
tests/test_text_rule_telemetry.py
Normal file
251
tests/test_text_rule_telemetry.py
Normal file
@@ -0,0 +1,251 @@
|
||||
"""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"
|
||||
Reference in New Issue
Block a user