Inlocuieste cele 3 controale de asociere cod RAR cu un singur rand de chips + un singur select care adauga instant la change. Stare = un hidden chips_state JSON versionat; post_form_chips redus la 2 actiuni (add/remove). Optgroup Sugestii (fuzzy/k-NN) + optiune "Nu se declara la RAR" in select, cu tinta implicita evidentiata si placeholder care o numeste. exclus persistat prin payload_json (treapta noua de precedenta in resolve_prestatii, round-trip complet). Siguranta: itemii exclusi sunt scosi din payload la momentul trimiterii (worker split_prestatii_excluse inainte de build_rar_payload + filtru defensiv), ca sa nu ajunga NICIODATA la RAR ca codPrestatie:null. Suita: 1680 passed, 1 skipped. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
746 lines
28 KiB
Python
746 lines
28 KiB
Python
"""US-001: stare chips extinsa (JSON versionat, D4) + actiuni add/remove unificate.
|
|
|
|
Contract nou /form-chips:
|
|
- Stare curenta: UN SINGUR hidden `chips_state` = JSON {"v":1,"items":[{"cod","op","den","exclus"}]}.
|
|
- chips_action=add + chips_pick (codul ales) + chips_target_index optional:
|
|
cu tinta -> leaga codul de operatia de la acel index; fara tinta -> chip liber.
|
|
chips_pick=__NEDECLARAT__ + tinta valida -> item exclus=1, fara cod_prestatie.
|
|
- chips_action=remove + chips_remove_index: chip legat de operatie -> revine warning
|
|
(cod_prestatie="", exclus=0); chip liber -> iese din lista.
|
|
- Actiunile vechi (add_flat/add_extra/remove_flat/chips_add_cod_{i}/chips_add_cod_flat
|
|
ca actiuni) sunt ELIMINATE.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import os
|
|
import re
|
|
import tempfile
|
|
|
|
import pytest
|
|
from starlette.testclient import TestClient
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
# Fixtures #
|
|
# --------------------------------------------------------------------------- #
|
|
|
|
@pytest.fixture()
|
|
def client(monkeypatch):
|
|
tmp = tempfile.mkdtemp()
|
|
monkeypatch.setenv("AUTOPASS_DB_PATH", os.path.join(tmp, "chips.db"))
|
|
monkeypatch.setenv("AUTOPASS_WEB_AUTH_REQUIRED", "true")
|
|
from app.config import get_settings
|
|
get_settings.cache_clear()
|
|
from app.web import ratelimit
|
|
ratelimit._hits.clear()
|
|
from app.main import app
|
|
with TestClient(app, follow_redirects=False) as c:
|
|
yield c
|
|
ratelimit._hits.clear()
|
|
get_settings.cache_clear()
|
|
|
|
|
|
def _create_account_user(email: str, password: str = "parolasecreta10"):
|
|
from app.accounts import create_account
|
|
from app.users import create_user
|
|
from app.db import get_connection
|
|
conn = get_connection()
|
|
try:
|
|
acct_id = create_account(conn, "Service", active=True)
|
|
create_user(conn, acct_id, email, password)
|
|
return acct_id
|
|
finally:
|
|
conn.close()
|
|
|
|
|
|
def _login(client, email: str, password: str = "parolasecreta10") -> None:
|
|
resp = client.get("/login")
|
|
m = re.search(r'name="csrf_token"\s+value="([^"]+)"', resp.text) or \
|
|
re.search(r'value="([^"]+)"\s+name="csrf_token"', resp.text)
|
|
assert m, "csrf_token nu gasit in login"
|
|
resp = client.post("/login", data={"email": email, "parola": password, "csrf_token": m.group(1)})
|
|
assert resp.status_code == 303
|
|
|
|
|
|
def _csrf(client) -> str:
|
|
resp = client.get("/?tab=coada")
|
|
m = re.search(r'name="csrf_token"\s+value="([^"]+)"', resp.text)
|
|
assert m, "csrf_token nu gasit in dashboard"
|
|
return m.group(1)
|
|
|
|
|
|
def _seed_cod(cod: str, denumire: str = "Prestatie test") -> None:
|
|
from app.db import get_connection
|
|
conn = get_connection()
|
|
try:
|
|
conn.execute(
|
|
"INSERT OR REPLACE INTO nomenclator_rar (cod_prestatie, nume_prestatie) VALUES (?, ?)",
|
|
(cod, denumire),
|
|
)
|
|
conn.commit()
|
|
finally:
|
|
conn.close()
|
|
|
|
|
|
def _state(items: list[dict]) -> str:
|
|
"""Construieste JSON-ul hidden chips_state din itemi scurti {cod,op,den,exclus}."""
|
|
return json.dumps({"v": 1, "items": items}, ensure_ascii=False)
|
|
|
|
|
|
def _login_seed(client, email: str) -> None:
|
|
_create_account_user(email)
|
|
_login(client, email)
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
# add cu tinta -> leaga codul de operatia respectiva #
|
|
# --------------------------------------------------------------------------- #
|
|
|
|
def test_add_cu_tinta_leaga_de_operatie(client):
|
|
_login_seed(client, "add.tinta@test.com")
|
|
_seed_cod("OE-1", "Schimb ulei")
|
|
csrf = _csrf(client)
|
|
|
|
state = _state([
|
|
{"cod": "", "op": "Op-A", "den": "Schimb ulei motor", "exclus": 0},
|
|
])
|
|
resp = client.post("/form-chips", data={
|
|
"csrf_token": csrf,
|
|
"chips_state": state,
|
|
"chips_action": "add",
|
|
"chips_target_index": "0",
|
|
"chips_pick": "OE-1",
|
|
})
|
|
assert resp.status_code == 200, resp.text[:400]
|
|
html = resp.text
|
|
assert "OE-1" in html
|
|
|
|
m = re.search(r'name="chips_state"\s+value=\'([^\']*)\'', html)
|
|
assert m, f"hidden chips_state lipsa din raspuns: {html[:400]}"
|
|
payload = json.loads(m.group(1).replace(""", '"'))
|
|
assert payload["v"] == 1
|
|
items = payload["items"]
|
|
assert len(items) == 1
|
|
assert items[0]["cod"] == "OE-1"
|
|
assert items[0]["op"] == "Op-A"
|
|
assert items[0]["exclus"] == 0
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
# add fara tinta -> chip liber #
|
|
# --------------------------------------------------------------------------- #
|
|
|
|
def test_add_fara_tinta_adauga_cod_liber(client):
|
|
_login_seed(client, "add.liber@test.com")
|
|
_seed_cod("FRN1", "Sistem de franare")
|
|
csrf = _csrf(client)
|
|
|
|
resp = client.post("/form-chips", data={
|
|
"csrf_token": csrf,
|
|
"chips_state": _state([]),
|
|
"chips_action": "add",
|
|
"chips_pick": "FRN1",
|
|
})
|
|
assert resp.status_code == 200, resp.text[:400]
|
|
m = re.search(r'name="chips_state"\s+value=\'([^\']*)\'', resp.text)
|
|
assert m
|
|
payload = json.loads(m.group(1).replace(""", '"'))
|
|
items = payload["items"]
|
|
assert len(items) == 1
|
|
assert items[0]["cod"] == "FRN1"
|
|
assert items[0]["op"] == ""
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
# add __NEDECLARAT__ cu tinta -> marcheaza exclus #
|
|
# --------------------------------------------------------------------------- #
|
|
|
|
def test_add_nedeclarat_marcheaza_exclus(client):
|
|
_login_seed(client, "add.ned@test.com")
|
|
csrf = _csrf(client)
|
|
|
|
state = _state([
|
|
{"cod": "", "op": "OP-ITP", "den": "Inspectie tehnica", "exclus": 0},
|
|
])
|
|
resp = client.post("/form-chips", data={
|
|
"csrf_token": csrf,
|
|
"chips_state": state,
|
|
"chips_action": "add",
|
|
"chips_target_index": "0",
|
|
"chips_pick": "__NEDECLARAT__",
|
|
})
|
|
assert resp.status_code == 200, resp.text[:400]
|
|
m = re.search(r'name="chips_state"\s+value=\'([^\']*)\'', resp.text)
|
|
assert m
|
|
payload = json.loads(m.group(1).replace(""", '"'))
|
|
items = payload["items"]
|
|
assert len(items) == 1
|
|
assert items[0]["exclus"] == 1
|
|
assert items[0]["cod"] == "", f"item exclus nu trebuie sa aiba cod_prestatie: {items[0]}"
|
|
assert items[0]["op"] == "OP-ITP"
|
|
assert "nu se va declara" in resp.text.lower()
|
|
|
|
|
|
def test_add_nedeclarat_fara_tinta_da_eroare(client):
|
|
"""Sentinelul __NEDECLARAT__ fara tinta valida -> chips_error explicit, fara modificare."""
|
|
_login_seed(client, "add.ned.notinta@test.com")
|
|
csrf = _csrf(client)
|
|
|
|
resp = client.post("/form-chips", data={
|
|
"csrf_token": csrf,
|
|
"chips_state": _state([]),
|
|
"chips_action": "add",
|
|
"chips_pick": "__NEDECLARAT__",
|
|
# fara chips_target_index
|
|
})
|
|
assert resp.status_code == 200, resp.text[:400]
|
|
assert "chips-extra-error" in resp.text, (
|
|
f"Eroare vizibila lipsa pentru NEDECLARAT fara tinta: {resp.text[:400]}"
|
|
)
|
|
m = re.search(r'name="chips_state"\s+value=\'([^\']*)\'', resp.text)
|
|
assert m
|
|
payload = json.loads(m.group(1).replace(""", '"'))
|
|
assert payload["items"] == [], "NEDECLARAT fara tinta nu trebuie sa modifice starea"
|
|
|
|
|
|
def test_add_nedeclarat_tinta_deja_mapata_da_eroare(client):
|
|
"""Tinta trebuie sa fie o operatie NEMAPATA — index spre operatie deja rezolvata -> eroare."""
|
|
_login_seed(client, "add.ned.mapata@test.com")
|
|
_seed_cod("OE-1", "Schimb ulei")
|
|
csrf = _csrf(client)
|
|
|
|
state = _state([
|
|
{"cod": "OE-1", "op": "Op-A", "den": "Schimb ulei", "exclus": 0},
|
|
])
|
|
resp = client.post("/form-chips", data={
|
|
"csrf_token": csrf,
|
|
"chips_state": state,
|
|
"chips_action": "add",
|
|
"chips_target_index": "0",
|
|
"chips_pick": "__NEDECLARAT__",
|
|
})
|
|
assert resp.status_code == 200
|
|
assert "chips-extra-error" in resp.text
|
|
m = re.search(r'name="chips_state"\s+value=\'([^\']*)\'', resp.text)
|
|
payload = json.loads(m.group(1).replace(""", '"'))
|
|
assert payload["items"][0]["cod"] == "OE-1", "Tinta invalida nu trebuie sa strice starea existenta"
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
# remove -> readuce operatia nemapata (warning) #
|
|
# --------------------------------------------------------------------------- #
|
|
|
|
def test_remove_cod_readuce_operatia_nemapata(client):
|
|
_login_seed(client, "remove.readuce@test.com")
|
|
csrf = _csrf(client)
|
|
|
|
state = _state([
|
|
{"cod": "OE-1", "op": "Op-A", "den": "Schimb ulei", "exclus": 0},
|
|
])
|
|
resp = client.post("/form-chips", data={
|
|
"csrf_token": csrf,
|
|
"chips_state": state,
|
|
"chips_action": "remove",
|
|
"chips_remove_index": "0",
|
|
})
|
|
assert resp.status_code == 200, resp.text[:400]
|
|
m = re.search(r'name="chips_state"\s+value=\'([^\']*)\'', resp.text)
|
|
assert m
|
|
payload = json.loads(m.group(1).replace(""", '"'))
|
|
items = payload["items"]
|
|
assert len(items) == 1, "Operatia trebuie sa RAMANA (doar codul dispare)"
|
|
assert items[0]["cod"] == ""
|
|
assert items[0]["op"] == "Op-A"
|
|
assert items[0]["exclus"] == 0
|
|
|
|
|
|
def test_remove_pe_nedeclarat_readuce_warning(client):
|
|
"""x pe un chip Nedeclarat readuce operatia la warning (nu se sterge operatia)."""
|
|
_login_seed(client, "remove.nedeclarat@test.com")
|
|
csrf = _csrf(client)
|
|
|
|
state = _state([
|
|
{"cod": "", "op": "OP-ITP", "den": "Inspectie", "exclus": 1},
|
|
])
|
|
resp = client.post("/form-chips", data={
|
|
"csrf_token": csrf,
|
|
"chips_state": state,
|
|
"chips_action": "remove",
|
|
"chips_remove_index": "0",
|
|
})
|
|
assert resp.status_code == 200
|
|
m = re.search(r'name="chips_state"\s+value=\'([^\']*)\'', resp.text)
|
|
payload = json.loads(m.group(1).replace(""", '"'))
|
|
items = payload["items"]
|
|
assert len(items) == 1
|
|
assert items[0]["exclus"] == 0
|
|
assert items[0]["cod"] == ""
|
|
assert items[0]["op"] == "OP-ITP"
|
|
|
|
|
|
def test_remove_cod_liber_il_sterge_complet(client):
|
|
"""Chip liber (fara operatie sursa) e eliminat integral din lista la remove."""
|
|
_login_seed(client, "remove.liber@test.com")
|
|
csrf = _csrf(client)
|
|
|
|
state = _state([
|
|
{"cod": "FRN1", "op": "", "den": "", "exclus": 0},
|
|
])
|
|
resp = client.post("/form-chips", data={
|
|
"csrf_token": csrf,
|
|
"chips_state": state,
|
|
"chips_action": "remove",
|
|
"chips_remove_index": "0",
|
|
})
|
|
assert resp.status_code == 200
|
|
m = re.search(r'name="chips_state"\s+value=\'([^\']*)\'', resp.text)
|
|
payload = json.loads(m.group(1).replace(""", '"'))
|
|
assert payload["items"] == []
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
# dedup (operatie, cod) #
|
|
# --------------------------------------------------------------------------- #
|
|
|
|
def test_dedup_operatie_cod(client):
|
|
"""Acelasi cod liber nu se adauga de doua ori (pereche op='',cod deja existenta)."""
|
|
_login_seed(client, "dedup.op.cod@test.com")
|
|
_seed_cod("OE-1", "Schimb ulei")
|
|
csrf = _csrf(client)
|
|
|
|
state = _state([
|
|
{"cod": "OE-1", "op": "", "den": "", "exclus": 0},
|
|
])
|
|
resp = client.post("/form-chips", data={
|
|
"csrf_token": csrf,
|
|
"chips_state": state,
|
|
"chips_action": "add",
|
|
"chips_pick": "OE-1",
|
|
})
|
|
assert resp.status_code == 200
|
|
assert "chips-extra-error" in resp.text or "deja in lista" in resp.text
|
|
m = re.search(r'name="chips_state"\s+value=\'([^\']*)\'', resp.text)
|
|
payload = json.loads(m.group(1).replace(""", '"'))
|
|
assert len(payload["items"]) == 1, f"Dedup a esuat, itemi: {payload['items']}"
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
# actiuni vechi eliminate #
|
|
# --------------------------------------------------------------------------- #
|
|
|
|
@pytest.mark.parametrize("actiune", ["add_flat", "add_extra", "remove_flat"])
|
|
def test_actiuni_vechi_eliminate_sunt_no_op(client, actiune):
|
|
"""Actiunile eliminate nu mai au ramura dedicata — cad prin fara sa schimbe starea."""
|
|
_login_seed(client, f"vechi.{actiune}@test.com")
|
|
_seed_cod("OE-1", "Schimb ulei")
|
|
csrf = _csrf(client)
|
|
|
|
state = _state([{"cod": "OE-1", "op": "Op-A", "den": "Schimb ulei", "exclus": 0}])
|
|
resp = client.post("/form-chips", data={
|
|
"csrf_token": csrf,
|
|
"chips_state": state,
|
|
"chips_action": actiune,
|
|
"chips_add_cod_flat": "FRN1",
|
|
"chips_remove_cod": "OE-1",
|
|
})
|
|
assert resp.status_code == 200
|
|
m = re.search(r'name="chips_state"\s+value=\'([^\']*)\'', resp.text)
|
|
payload = json.loads(m.group(1).replace(""", '"'))
|
|
assert payload["items"] == [{"cod": "OE-1", "op": "Op-A", "den": "Schimb ulei", "exclus": 0}]
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
# chips_state prezent mereu, inclusiv gol #
|
|
# --------------------------------------------------------------------------- #
|
|
|
|
def test_chips_state_gol_ramane_prezent_in_raspuns(client):
|
|
_login_seed(client, "state.gol@test.com")
|
|
csrf = _csrf(client)
|
|
resp = client.post("/form-chips", data={
|
|
"csrf_token": csrf,
|
|
"chips_state": _state([]),
|
|
"chips_action": "",
|
|
})
|
|
assert resp.status_code == 200
|
|
m = re.search(r'name="chips_state"\s+value=\'([^\']*)\'', resp.text)
|
|
assert m, "hidden chips_state trebuie sa fie mereu prezent, chiar cu lista goala"
|
|
payload = json.loads(m.group(1).replace(""", '"'))
|
|
assert payload == {"v": 1, "items": []}
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
# JSON invalid -> eroare vizibila, nu stare goala silentioasa #
|
|
# --------------------------------------------------------------------------- #
|
|
|
|
def test_chips_state_json_invalid_da_eroare_vizibila(client):
|
|
_login_seed(client, "state.corupt@test.com")
|
|
csrf = _csrf(client)
|
|
resp = client.post("/form-chips", data={
|
|
"csrf_token": csrf,
|
|
"chips_state": "{not valid json",
|
|
"chips_action": "",
|
|
})
|
|
assert resp.status_code == 200
|
|
assert "chips-extra-error" in resp.text or "invalid" in resp.text.lower(), (
|
|
f"JSON corupt trebuie sa dea eroare vizibila: {resp.text[:400]}"
|
|
)
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
# compat tranzitorie: chips_state lipsa -> fallback pe listele vechi #
|
|
# --------------------------------------------------------------------------- #
|
|
|
|
def test_compat_fallback_pe_liste_vechi_cand_chips_state_lipseste(client):
|
|
_login_seed(client, "compat.fallback@test.com")
|
|
_seed_cod("OE-1", "Schimb ulei")
|
|
csrf = _csrf(client)
|
|
|
|
resp = client.post("/form-chips", data={
|
|
"csrf_token": csrf,
|
|
# FARA chips_state -> fallback pe listele vechi
|
|
"cod_prestatie": [""],
|
|
"chip_op_service": ["Op-A"],
|
|
"chip_denumire": ["Schimb ulei motor"],
|
|
"chips_action": "add",
|
|
"chips_target_index": "0",
|
|
"chips_pick": "OE-1",
|
|
})
|
|
assert resp.status_code == 200, resp.text[:400]
|
|
m = re.search(r'name="chips_state"\s+value=\'([^\']*)\'', resp.text)
|
|
assert m
|
|
payload = json.loads(m.group(1).replace(""", '"'))
|
|
assert payload["items"][0]["cod"] == "OE-1"
|
|
assert payload["items"][0]["exclus"] == 0
|
|
|
|
|
|
# =========================================================================== #
|
|
# US-002: template unificat -- un container .chips, un select, fara op-row #
|
|
# =========================================================================== #
|
|
|
|
|
|
def _post_chips(client, csrf, items, **extra):
|
|
data = {
|
|
"csrf_token": csrf,
|
|
"chips_state": _state(items),
|
|
"chips_action": "",
|
|
}
|
|
data.update(extra)
|
|
return client.post("/form-chips", data=data)
|
|
|
|
|
|
def test_render_un_singur_container_chips(client):
|
|
"""UN singur container `.chips` -- indiferent de mixul de tipuri de chip."""
|
|
_login_seed(client, "un.container@test.com")
|
|
_seed_cod("OE-1", "Schimb ulei")
|
|
csrf = _csrf(client)
|
|
|
|
items = [
|
|
{"cod": "OE-1", "op": "Op-A", "den": "Schimb ulei", "exclus": 0},
|
|
{"cod": "", "op": "Op-B", "den": "Vulcanizare", "exclus": 0},
|
|
{"cod": "FRN1", "op": "", "den": "", "exclus": 0},
|
|
{"cod": "", "op": "Op-C", "den": "ITP anual", "exclus": 1},
|
|
]
|
|
_seed_cod("FRN1", "Sistem de franare")
|
|
resp = _post_chips(client, csrf, items)
|
|
assert resp.status_code == 200, resp.text[:400]
|
|
html = resp.text
|
|
|
|
assert html.count('class="chips"') == 1, (
|
|
f"Trebuie sa existe UN SINGUR container .chips, gasit {html.count('class=\"chips\"')}: {html[:800]}"
|
|
)
|
|
|
|
|
|
def test_render_fara_op_row_si_fara_butoane_adauga(client):
|
|
"""Fara .op-row, fara butoane Adauga/+, fara actiunile vechi in markup."""
|
|
_login_seed(client, "fara.oprow@test.com")
|
|
_seed_cod("OE-1", "Schimb ulei")
|
|
csrf = _csrf(client)
|
|
|
|
items = [
|
|
{"cod": "OE-1", "op": "Op-A", "den": "Schimb ulei", "exclus": 0},
|
|
{"cod": "", "op": "Op-B", "den": "Vulcanizare", "exclus": 0},
|
|
]
|
|
resp = _post_chips(client, csrf, items)
|
|
assert resp.status_code == 200, resp.text[:400]
|
|
html = resp.text
|
|
|
|
assert "op-row" not in html
|
|
assert "Adauga" not in html
|
|
assert "add_flat" not in html
|
|
assert "chips_add_cod_" not in html
|
|
|
|
|
|
def test_chip_warning_pentru_operatie_nemapata(client):
|
|
"""Operatie nemapata -> chip warning `warn OP-X . fara cod` cu title complet."""
|
|
_login_seed(client, "chip.warning@test.com")
|
|
csrf = _csrf(client)
|
|
|
|
items = [
|
|
{"cod": "", "op": "OP-ITP", "den": "Inspectie tehnica periodica", "exclus": 0},
|
|
]
|
|
resp = _post_chips(client, csrf, items)
|
|
assert resp.status_code == 200, resp.text[:400]
|
|
html = resp.text
|
|
|
|
assert "OP-ITP" in html
|
|
assert "fara cod" in html
|
|
assert 'title="OP-ITP' in html
|
|
|
|
|
|
def test_select_are_hx_disabled_elt_this(client):
|
|
"""Selectul unic are hx-disabled-elt='this' EXPLICIT (capcana 5.22 -- nu mostenit de la form)."""
|
|
_login_seed(client, "select.disabled@test.com")
|
|
_seed_cod("OE-1", "Schimb ulei")
|
|
csrf = _csrf(client)
|
|
|
|
resp = _post_chips(client, csrf, [])
|
|
assert resp.status_code == 200, resp.text[:400]
|
|
html = resp.text
|
|
|
|
m = re.search(r'<select[^>]*name="chips_pick"[^>]*>', html)
|
|
assert m, f"selectul chips_pick lipseste: {html[:600]}"
|
|
select_tag = m.group(0)
|
|
assert 'id="chips-picker"' in select_tag
|
|
assert 'hx-trigger="change"' in select_tag
|
|
assert 'hx-post="/form-chips"' in select_tag
|
|
assert 'hx-target="#chips-section"' in select_tag
|
|
assert 'hx-swap="outerHTML"' in select_tag
|
|
assert 'hx-include="closest form"' in select_tag
|
|
assert 'hx-disabled-elt="this"' in select_tag, (
|
|
f"hx-disabled-elt='this' lipseste de pe select (capcana 5.22): {select_tag}"
|
|
)
|
|
|
|
|
|
def test_mod_plat_fara_operatii_select_doar_coduri_libere(client):
|
|
"""Mod plat (zero operatii din import): doar chips libere + select placeholder generic."""
|
|
_login_seed(client, "mod.plat@test.com")
|
|
_seed_cod("OE-1", "Schimb ulei")
|
|
csrf = _csrf(client)
|
|
|
|
items = [
|
|
{"cod": "OE-1", "op": "", "den": "", "exclus": 0},
|
|
]
|
|
resp = _post_chips(client, csrf, items)
|
|
assert resp.status_code == 200, resp.text[:400]
|
|
html = resp.text
|
|
|
|
assert "⚠" not in html, "Mod plat nu trebuie sa aiba niciun chip warning"
|
|
m = re.search(r'<select[^>]*name="chips_pick"[^>]*>\s*<option value="">([^<]*)</option>', html)
|
|
assert m, f"select cu optiune placeholder lipseste: {html[:600]}"
|
|
assert "adauga cod RAR" in m.group(1)
|
|
|
|
|
|
def test_has_r_odo_pastrat_dupa_actiuni(client):
|
|
"""has_r_odo / data-has-r-odo + chip-warn pe R-ODO/I-ODO raman neschimbate."""
|
|
_login_seed(client, "r.odo@test.com")
|
|
_seed_cod("R-ODO", "Odometru initial")
|
|
csrf = _csrf(client)
|
|
|
|
items = [
|
|
{"cod": "R-ODO", "op": "", "den": "", "exclus": 0},
|
|
]
|
|
resp = _post_chips(client, csrf, items)
|
|
assert resp.status_code == 200, resp.text[:400]
|
|
html = resp.text
|
|
|
|
assert 'data-has-r-odo="true"' in html
|
|
assert "chip-warn" in html
|
|
assert "R-ODO" in html
|
|
|
|
|
|
def test_chip_nedeclarat_are_x_functional(client):
|
|
"""Item exclus=1 -> chip Nedeclarat (line-through) cu buton de sters functional (remove -> warning)."""
|
|
_login_seed(client, "nedeclarat.x@test.com")
|
|
csrf = _csrf(client)
|
|
|
|
items = [
|
|
{"cod": "", "op": "OP-ITP", "den": "Inspectie", "exclus": 1},
|
|
]
|
|
resp = _post_chips(client, csrf, items)
|
|
assert resp.status_code == 200, resp.text[:400]
|
|
html = resp.text
|
|
|
|
assert "Nedeclarat" in html
|
|
assert 'hx-vals=\'{"chips_action":"remove","chips_remove_index":0}\'' in html
|
|
|
|
|
|
def test_chips_submission_id_ecouat(client):
|
|
"""chips_submission_id trimis in form e ecouat ca hidden in raspunsul /form-chips."""
|
|
_login_seed(client, "sub.id.ecou@test.com")
|
|
csrf = _csrf(client)
|
|
|
|
resp = _post_chips(client, csrf, [], chips_submission_id="4242")
|
|
assert resp.status_code == 200, resp.text[:400]
|
|
assert re.search(r'name="chips_submission_id"\s+value="4242"', resp.text), (
|
|
f"chips_submission_id nu a fost ecouat: {resp.text[:600]}"
|
|
)
|
|
|
|
|
|
# =========================================================================== #
|
|
# US-003: sugestii fuzzy/k-NN + optiunea "Nu se declara la RAR" in select #
|
|
# =========================================================================== #
|
|
|
|
|
|
def _extrage_select(html: str) -> str:
|
|
"""Extrage markup-ul complet al <select name="chips_pick">...</select>."""
|
|
m = re.search(r'<select[^>]*name="chips_pick".*?</select>', html, re.DOTALL)
|
|
assert m, f"select chips_pick lipseste din raspuns: {html[:600]}"
|
|
return m.group(0)
|
|
|
|
|
|
def test_select_are_optgroup_sugestii_pt_tinta(client):
|
|
"""Cu tinta activa (operatie nemapata): optgroup 'Sugestii' + __NEDECLARAT__ + nomenclator."""
|
|
_login_seed(client, "sugestii.tinta@test.com")
|
|
_seed_cod("OE-1", "Schimb ulei motor")
|
|
_seed_cod("FRN1", "Sistem de franare")
|
|
csrf = _csrf(client)
|
|
|
|
items = [
|
|
{"cod": "", "op": "Op-A", "den": "Schimb ulei motor", "exclus": 0},
|
|
]
|
|
resp = _post_chips(client, csrf, items)
|
|
assert resp.status_code == 200, resp.text[:400]
|
|
select_html = _extrage_select(resp.text)
|
|
|
|
assert '<optgroup label="Sugestii">' in select_html, (
|
|
f"optgroup Sugestii lipseste cu tinta activa: {select_html[:600]}"
|
|
)
|
|
assert "OE-1" in select_html.split('<optgroup label="Sugestii">')[1].split("</optgroup>")[0], (
|
|
"sugestia fuzzy pentru 'Schimb ulei motor' trebuie sa contina OE-1"
|
|
)
|
|
assert "__NEDECLARAT__" in select_html
|
|
assert "Nu se declara la RAR" in select_html
|
|
# Nomenclatorul complet ramane dupa Sugestii/NEDECLARAT.
|
|
assert "FRN1" in select_html
|
|
|
|
|
|
def test_select_are_nedeclarat_doar_cu_tinta(client):
|
|
"""Optiunea __NEDECLARAT__ apare DOAR cand exista o tinta (operatie nemapata) activa."""
|
|
_login_seed(client, "nedeclarat.doar.tinta@test.com")
|
|
_seed_cod("OE-1", "Schimb ulei")
|
|
csrf = _csrf(client)
|
|
|
|
# Cu tinta (operatie nemapata) -> NEDECLARAT prezent.
|
|
resp_cu_tinta = _post_chips(client, csrf, [
|
|
{"cod": "", "op": "Op-A", "den": "Schimb ulei", "exclus": 0},
|
|
])
|
|
assert "__NEDECLARAT__" in _extrage_select(resp_cu_tinta.text)
|
|
|
|
# Fara tinta (doar cod liber, nicio operatie nemapata) -> NEDECLARAT absent.
|
|
resp_fara_tinta = _post_chips(client, csrf, [
|
|
{"cod": "OE-1", "op": "", "den": "", "exclus": 0},
|
|
])
|
|
assert "__NEDECLARAT__" not in _extrage_select(resp_fara_tinta.text)
|
|
|
|
|
|
def test_select_fara_tinta_fara_sugestii(client):
|
|
"""Fara tinta: selectul listeaza DOAR nomenclatorul (fara optgroup Sugestii, fara NEDECLARAT)."""
|
|
_login_seed(client, "fara.tinta.fara.sugestii@test.com")
|
|
_seed_cod("OE-1", "Schimb ulei motor")
|
|
csrf = _csrf(client)
|
|
|
|
items = [
|
|
{"cod": "OE-1", "op": "", "den": "", "exclus": 0},
|
|
]
|
|
resp = _post_chips(client, csrf, items)
|
|
assert resp.status_code == 200, resp.text[:400]
|
|
select_html = _extrage_select(resp.text)
|
|
|
|
assert "<optgroup" not in select_html
|
|
assert "__NEDECLARAT__" not in select_html
|
|
assert "OE-1" in select_html
|
|
|
|
|
|
def test_sugestii_indisponibile_degradare_fara_optgroup(client, monkeypatch):
|
|
"""Motor de sugestii care arunca exceptie -> select DOAR cu nomenclatorul (fara optgroup),
|
|
fara eroare 500 si fara blocare (degradare gratioasa, GAP inchis din PRD 5.23)."""
|
|
_login_seed(client, "sugestii.indisponibile@test.com")
|
|
_seed_cod("OE-1", "Schimb ulei motor")
|
|
csrf = _csrf(client)
|
|
|
|
from app.web import routes as _routes
|
|
|
|
def _boom(*args, **kwargs):
|
|
raise RuntimeError("motor de sugestii indisponibil (simulat)")
|
|
|
|
monkeypatch.setattr(_routes, "suggest_codes", _boom)
|
|
monkeypatch.setattr(_routes, "enrich_suggestions", _boom)
|
|
|
|
items = [
|
|
{"cod": "", "op": "Op-A", "den": "Schimb ulei motor", "exclus": 0},
|
|
]
|
|
resp = _post_chips(client, csrf, items)
|
|
assert resp.status_code == 200, resp.text[:400]
|
|
select_html = _extrage_select(resp.text)
|
|
|
|
assert "<optgroup" not in select_html, (
|
|
f"optgroup nu trebuie sa apara cand motorul de sugestii arunca exceptie: {select_html[:600]}"
|
|
)
|
|
assert "OE-1" in select_html, "nomenclatorul complet trebuie sa ramana disponibil"
|
|
|
|
|
|
# =========================================================================== #
|
|
# FIX: tinta implicita + sugestii la RANDAREA INITIALA a modalului (fara #
|
|
# niciun round-trip /form-chips) — invariant PRD 5.23 §3.1. #
|
|
# =========================================================================== #
|
|
|
|
def _insert_submission(acct: int, *, status: str, payload: dict) -> int:
|
|
from app.db import get_connection
|
|
conn = get_connection()
|
|
try:
|
|
cur = conn.execute(
|
|
"INSERT INTO submissions (idempotency_key, account_id, status, payload_json) "
|
|
"VALUES (?, ?, ?, ?)",
|
|
(f"k-{os.urandom(6).hex()}", acct, status, json.dumps(payload)),
|
|
)
|
|
conn.commit()
|
|
return int(cur.lastrowid)
|
|
finally:
|
|
conn.close()
|
|
|
|
|
|
def test_randare_initiala_are_tinta_si_optgroup(client):
|
|
"""Deschiderea INITIALA a modalului de detaliu (needs_mapping, operatie nemapata),
|
|
FARA niciun /form-chips, trebuie sa aiba deja is-target pe chip-ul warning,
|
|
placeholder-ul D2 "Cod pentru OP-X — alege" si optgroup-ul Sugestii + NEDECLARAT."""
|
|
acct = _login_seed_ret(client, "randare.initiala@test.com")
|
|
_seed_cod("OE-1", "Schimb ulei motor")
|
|
_seed_cod("FRN1", "Sistem de franare")
|
|
|
|
op = "Schimb ulei motor"
|
|
sid = _insert_submission(acct, status="needs_mapping", payload={
|
|
"vin": "WVWZZZ1JZXW0SC001",
|
|
"nr_inmatriculare": "B100AAA",
|
|
"data_prestatie": "2026-06-10",
|
|
"odometru_final": "50000",
|
|
"prestatii": [{"cod_op_service": op, "denumire": op}],
|
|
})
|
|
|
|
resp = client.get(f"/_fragments/trimitere/{sid}")
|
|
assert resp.status_code == 200, resp.text[:400]
|
|
html = resp.text
|
|
|
|
assert "is-target" in html, (
|
|
f"chip-ul warning al primei operatii nemapate trebuie sa fie tinta la deschiderea initiala: {html[:2000]}"
|
|
)
|
|
assert f"Cod pentru {op} — alege" in html, (
|
|
f"placeholder-ul D2 trebuie sa numeasca operatia tinta la deschiderea initiala: {html[:2000]}"
|
|
)
|
|
select_html = _extrage_select(html)
|
|
assert '<optgroup label="Sugestii">' in select_html, (
|
|
f"optgroup Sugestii trebuie sa apara la deschiderea initiala (fara /form-chips): {select_html[:600]}"
|
|
)
|
|
assert "OE-1" in select_html.split('<optgroup label="Sugestii">')[1].split("</optgroup>")[0], (
|
|
"sugestia fuzzy pentru 'Schimb ulei motor' trebuie sa contina OE-1 chiar la randarea initiala"
|
|
)
|
|
assert "__NEDECLARAT__" in select_html
|
|
|
|
|
|
def _login_seed_ret(client, email: str) -> int:
|
|
"""Ca `_login_seed`, dar intoarce account_id (necesar pentru inserarea directa a submission-ului)."""
|
|
acct = _create_account_user(email)
|
|
_login(client, email)
|
|
return acct
|