Comutator accounts.auto_send_enabled per cont: Auto OFF (default) tine randurile
la ingestie (submissions.held=1), worker-ul (claim_one AND held=0) le sare pana la
eliberare umana (per rand/bulk/auto-release OFF->ON). Snapshot held prin chokepoint
unic held_for_account pe toate caile de ingestie (API, import, reresolve, reactivare).
- schema/migrare: coloana held + index partial idx_submissions_held; auto_send_enabled
- API: echo onest held+motiv (US-010), ruta /prezentari/{id}/trimite-acum
- web: toggle header, modal confirmare tipata, buton Trimite per rand + Trimite toate,
banner coada tinuta imbatranita (L.142), contor "In asteptare (manual)"
- worker: expire_held (US-008, inchide gaura retentie PII), metrics held gauges
- ops: tools/carantina_held + runbook rollback (R4)
Nota review (/code-review high): re-snapshot held lipseste pe caile repune/corectie
(requeue_submission, post_corectie, bulk-fix) — de aliniat separat cu create_prezentari.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
147 lines
4.7 KiB
Python
147 lines
4.7 KiB
Python
"""Teste PRD 5.19 US-010 — onestitate + observabilitate `held` pe canalul API.
|
|
|
|
- `held` in raspunsul enqueue (SubmissionResult) + motiv non-null.
|
|
- `held` in proiectiile GET /v1/prezentari si /v1/prezentari/{id}.
|
|
- endpoint de eliberare API POST /v1/prezentari/{id}/trimite-acum (paritate cu /repune).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
import tempfile
|
|
|
|
import pytest
|
|
from fastapi.testclient import TestClient
|
|
|
|
|
|
@pytest.fixture()
|
|
def client(monkeypatch):
|
|
tmp = tempfile.mkdtemp()
|
|
monkeypatch.setenv("AUTOPASS_DB_PATH", os.path.join(tmp, "echo.db"))
|
|
monkeypatch.setenv("AUTOPASS_LOG_DIR", os.path.join(tmp, "logs"))
|
|
monkeypatch.setenv("AUTOPASS_REQUIRE_API_KEY", "false")
|
|
from app.config import get_settings
|
|
get_settings.cache_clear()
|
|
from app.main import app
|
|
with TestClient(app) as c:
|
|
yield c
|
|
get_settings.cache_clear()
|
|
|
|
|
|
def _set_auto_send(enabled: bool, account_id: int = 1) -> None:
|
|
from app.db import get_connection
|
|
conn = get_connection()
|
|
try:
|
|
conn.execute(
|
|
"UPDATE accounts SET auto_send_enabled=? WHERE id=?",
|
|
(1 if enabled else 0, account_id),
|
|
)
|
|
conn.commit()
|
|
finally:
|
|
conn.close()
|
|
|
|
|
|
def _held(sid: int) -> int:
|
|
from app.db import get_connection
|
|
conn = get_connection()
|
|
try:
|
|
return conn.execute("SELECT held FROM submissions WHERE id=?", (sid,)).fetchone()["held"]
|
|
finally:
|
|
conn.close()
|
|
|
|
|
|
def _body(**over) -> dict:
|
|
prez = {
|
|
"vin": "WVWZZZ1KZAW000123",
|
|
"nr_inmatriculare": "B999TST",
|
|
"data_prestatie": "2026-06-15",
|
|
"odometru_final": "123456",
|
|
"prestatii": [{"cod_prestatie": "OE-1"}],
|
|
}
|
|
prez.update(over)
|
|
return {"prezentari": [prez]}
|
|
|
|
|
|
def _enqueue_held(client) -> int:
|
|
_set_auto_send(False)
|
|
r = client.post("/v1/prezentari", json=_body())
|
|
return r.json()["results"][0]["submission_id"]
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
# GET proiectii #
|
|
# --------------------------------------------------------------------------- #
|
|
|
|
def test_get_prezentare_expune_held(client):
|
|
sid = _enqueue_held(client)
|
|
r = client.get(f"/v1/prezentari/{sid}")
|
|
assert r.status_code == 200, r.text
|
|
body = r.json()
|
|
assert "held" in body
|
|
assert body["held"] in (1, True)
|
|
|
|
|
|
def test_list_prezentari_expune_held(client):
|
|
sid = _enqueue_held(client)
|
|
r = client.get("/v1/prezentari")
|
|
assert r.status_code == 200, r.text
|
|
rows = {row["id"]: row for row in r.json()["submissions"]}
|
|
assert sid in rows
|
|
assert "held" in rows[sid]
|
|
assert rows[sid]["held"] in (1, True)
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
# POST /v1/prezentari/{id}/trimite-acum #
|
|
# --------------------------------------------------------------------------- #
|
|
|
|
def test_trimite_acum_elibereaza(client):
|
|
"""Rand tinut -> trimite-acum -> held=0."""
|
|
sid = _enqueue_held(client)
|
|
assert _held(sid) == 1
|
|
r = client.post(f"/v1/prezentari/{sid}/trimite-acum")
|
|
assert r.status_code == 200, r.text
|
|
assert r.json()["ok"] is True
|
|
assert _held(sid) == 0
|
|
|
|
|
|
def test_trimite_acum_id_inexistent_404(client):
|
|
r = client.post("/v1/prezentari/999999/trimite-acum")
|
|
assert r.status_code == 404
|
|
|
|
|
|
def test_trimite_acum_rand_nequeued_noop(client):
|
|
"""Rand non-queued (sent) -> no-op sigur, ramane neschimbat, raspuns 200."""
|
|
_set_auto_send(True)
|
|
sid = client.post("/v1/prezentari", json=_body()).json()["results"][0]["submission_id"]
|
|
from app.db import get_connection
|
|
conn = get_connection()
|
|
try:
|
|
conn.execute("UPDATE submissions SET status='sent', held=1 WHERE id=?", (sid,))
|
|
conn.commit()
|
|
finally:
|
|
conn.close()
|
|
r = client.post(f"/v1/prezentari/{sid}/trimite-acum")
|
|
assert r.status_code == 200, r.text
|
|
# held nemodificat (nu era queued)
|
|
assert _held(sid) == 1
|
|
|
|
|
|
def test_trimite_acum_cont_strain_404(client):
|
|
"""Un rand al altui cont -> 404-before-leak (nu elibereaza cross-account)."""
|
|
sid = _enqueue_held(client)
|
|
from app.accounts import create_account
|
|
from app.db import get_connection
|
|
conn = get_connection()
|
|
try:
|
|
# Cont real strain (id>=2) + muta randul pe el (FK valid).
|
|
other = create_account(conn, "Alt cont")
|
|
conn.execute("UPDATE submissions SET account_id=? WHERE id=?", (other, sid))
|
|
conn.commit()
|
|
finally:
|
|
conn.close()
|
|
# Clientul (dev, fara cheie) e pe contul id=1 -> randul altui cont e 404-before-leak.
|
|
r = client.post(f"/v1/prezentari/{sid}/trimite-acum")
|
|
assert r.status_code == 404
|
|
assert _held(sid) == 1
|