feat(5.20): US-013 retragere accounts.rar_creds_enc -> per-env + DROP cu garda

Toate citirile pe coloana legacy accounts.rar_creds_enc mutate pe sloturile
per-env (rar_creds_test_enc/rar_creds_prod_enc): worker fallback+keepalive,
are_creds (web) si are_creds_rar (integrare, +are_creds_test/_prod), write-back
API la reactivare, purjare la stergere cont, _get_acasa_context/_fetch_cont_env_state.

Contract API (aditiv): POST /v1/conturi/rar-creds primeste rar_target optional
(test/prod), scrie in slotul corect + activeaza mediul; DELETE primeste ?env
(sterge un slot sau ambele). Documentat in docs/api-rar-contract.md.

DROP cu garda in db.py (schema.sql fara coloana pe DB fresh):
- 6a: eliminat ADD COLUMN rar_creds_enc (fara ping-pong re-ADD dupa DROP)
- 6b: try/except fail-safe (nu crapa boot-ul) + garda sqlite_version >= 3.35
- 6c: re-backfill old->new imediat inainte de assert (ancora globala)
- garda orfane: DROP anulat daca vreun creds legacy nu a aterizat in slot per-env
- backup criptat accounts_rar_creds_enc_backup inainte de DROP
- 6d: verificare prin PRAGMA table_info (NU grep — submissions are aceeasi coloana)
Garda one-way, idempotenta la boot repetat (verificat). submissions.rar_creds_enc
ramane neatinsa.

tests/test_retragere_creds_enc.py: niciun read pe coloana veche, conturi rar-creds
env-aware, are_creds per-env, DROP blocat de garda la lipsa copiere. 9 teste
existente actualizate pe sloturi per-env.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Claude Agent
2026-07-02 21:03:08 +00:00
parent 3d3eb71a1e
commit b1d825e66b
19 changed files with 657 additions and 138 deletions

View File

@@ -94,16 +94,21 @@ def test_delete_purjeaza_pii_si_elibereaza_cui(conn):
"""Stergerea soft purjeaza creds RAR + revoca cheile API + elibereaza CUI (re-inregistrabil)."""
from app.accounts import create_account, delete_account, list_accounts
acct_id = create_account(conn, "Service GDPR", cui="RO12345", active=True)
conn.execute("UPDATE accounts SET rar_creds_enc='secret_enc' WHERE id=?", (acct_id,))
# US-013: creds in sloturi per-env (rar_creds_enc legacy dropata)
conn.execute(
"UPDATE accounts SET rar_creds_test_enc='secret_enc_test', rar_creds_prod_enc='secret_enc_prod' WHERE id=?",
(acct_id,),
)
conn.execute("INSERT INTO api_keys (account_id, key_hash, active) VALUES (?, 'h', 1)", (acct_id,))
conn.commit()
delete_account(conn, acct_id)
row = conn.execute("SELECT status, rar_creds_enc, cui FROM accounts WHERE id=?",
row = conn.execute("SELECT status, rar_creds_test_enc, rar_creds_prod_enc, cui FROM accounts WHERE id=?",
(acct_id,)).fetchone()
assert row["status"] == "deleted"
assert row["rar_creds_enc"] is None, "creds RAR trebuie purjate la stergere"
assert row["rar_creds_test_enc"] is None, "creds RAR test trebuie purjate la stergere"
assert row["rar_creds_prod_enc"] is None, "creds RAR prod trebuie purjate la stergere"
assert row["cui"] is None, "CUI trebuie eliberat la stergere"
key_active = conn.execute("SELECT active FROM api_keys WHERE account_id=?", (acct_id,)).fetchone()
assert key_active["active"] == 0, "cheile API trebuie revocate"
@@ -133,15 +138,16 @@ def test_migrare_deriva_status_din_active(conn):
from app.db import _migrate
# Reconstruim accounts fara `status` (rebuild de tabela — singura cale in SQLite vechi).
# US-013: rar_creds_enc nu mai exista in accounts (dropata); nu e in legacy DDL.
conn.executescript(
"""
PRAGMA foreign_keys=OFF;
CREATE TABLE accounts_legacy (
id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, cui TEXT,
active INTEGER NOT NULL DEFAULT 1, rar_creds_enc TEXT, created_at TEXT
active INTEGER NOT NULL DEFAULT 1, created_at TEXT
);
INSERT INTO accounts_legacy (id, name, cui, active, rar_creds_enc, created_at)
SELECT id, name, cui, active, rar_creds_enc, created_at FROM accounts;
INSERT INTO accounts_legacy (id, name, cui, active, created_at)
SELECT id, name, cui, active, created_at FROM accounts;
DROP TABLE accounts;
ALTER TABLE accounts_legacy RENAME TO accounts;
"""

View File

@@ -82,16 +82,16 @@ def test_resubmit_actualizeaza_creds_pe_reactivare(client):
enc_dupa = _creds_enc(sid)
assert enc_dupa is not None
assert enc_dupa != enc_initial, "creds-urile trebuie actualizate la reactivare"
# propagat si in accounts.rar_creds_enc (canal durabil, decizie #17)
# US-013: creds propagate in slotul per-env (rar_creds_test_enc, ancora globala=test)
from app.db import get_connection
from app.crypto import decrypt_creds
conn = get_connection()
try:
row = conn.execute("SELECT rar_creds_enc FROM accounts WHERE id=1").fetchone()
row = conn.execute("SELECT rar_creds_test_enc FROM accounts WHERE id=1").fetchone()
finally:
conn.close()
assert row["rar_creds_enc"] is not None
assert decrypt_creds(row["rar_creds_enc"])["password"] == "parolaNoua"
assert row["rar_creds_test_enc"] is not None
assert decrypt_creds(row["rar_creds_test_enc"])["password"] == "parolaNoua"
def test_resubmit_peste_sent_ramane_deduped(client):

View File

@@ -34,9 +34,14 @@ def _tables(conn) -> set[str]:
# --- Coloane noi ---
def test_accounts_rar_creds_enc(db_conn):
cols = _table_cols(db_conn, "accounts")
assert "rar_creds_enc" in cols
def test_accounts_rar_creds_enc_dropata(db_conn):
"""US-013: accounts.rar_creds_enc a fost dropata; submissions.rar_creds_enc ramane."""
acc_cols = _table_cols(db_conn, "accounts")
sub_cols = _table_cols(db_conn, "submissions")
assert "rar_creds_enc" not in acc_cols, \
"accounts.rar_creds_enc trebuie sa fie ABSENTA dupa US-013 DROP"
assert "rar_creds_enc" in sub_cols, \
"submissions.rar_creds_enc trebuie sa RAMANA (creds efemere per-cerere)"
def test_submissions_batch_id(db_conn):
@@ -153,7 +158,8 @@ def test_migrate_on_existing_db(monkeypatch):
assert "batch_id" in sub_cols
assert "row_index" in sub_cols
assert "purge_after" in sub_cols
assert "rar_creds_enc" in acc_cols
# US-013: accounts.rar_creds_enc a fost dropata.
assert "rar_creds_enc" not in acc_cols
conn.close()
get_settings.cache_clear()

View File

@@ -5,9 +5,9 @@ Acopera (plan T15, sect.12):
-> worker process_one (MockRar) -> submission FINALIZATA cu id_prezentare.
- Scenariul 2: re-upload acelasi continut -> preview marcheaza already_sent
(NU al doilea FINALIZATA dupa commit).
- Scenariul 3: coada MIXTA API(creds efemere)+web(creds durabile accounts.rar_creds_enc):
- Scenariul 3: coada MIXTA API(creds efemere)+web(creds durabile per-env, US-013):
dupa login + purjare creds efemere, submission-urile web tot se trimit
prin fallback accounts.rar_creds_enc (T1/Voce#5).
prin fallback accounts.rar_creds_{env}_enc (T1/Voce#5).
- Masina de stari (sect. 6): tranzitii queued->sending->sent/requeued/error.
- Failure registry (sect. 8): 400/403/503+reconciliere.
- T16: purge_expired + purge_after setat la commit.
@@ -454,7 +454,7 @@ class TestE2EMixedQueue:
"""Scenariul 3 (T1/Voce#5): API (creds efemere) + web (canal import, fara creds pe submission).
Dupa purjarea creds efemere ale canalului API (la primul login), submission-urile
import (web) tot se trimit prin fallback accounts.rar_creds_enc.
import (web) tot se trimit prin fallback accounts.rar_creds_{env}_enc (US-013, per-env).
Verificam ca ambele submission-uri ajung la status='sent'.
"""
import app.worker.__main__ as w
@@ -486,11 +486,15 @@ class TestE2EMixedQueue:
assert r_api.status_code == 200, r_api.text
sub_api_id = r_api.json()["results"][0]["submission_id"]
# 3. Seteaza creds durabile in accounts (canal web fallback)
# 3. Seteaza creds durabile in accounts per-env (US-013, canal web fallback)
conn = get_connection()
try:
web_creds_enc = encrypt_creds({"email": "web@test.ro", "password": "pass_web"})
conn.execute("UPDATE accounts SET rar_creds_enc=? WHERE id=1", (web_creds_enc,))
# US-013: scrie in slotul per-env test (rar_env default = 'test')
conn.execute(
"UPDATE accounts SET rar_creds_test_enc=?, rar_test_enabled=1 WHERE id=1",
(web_creds_enc,),
)
conn.commit()
# Verifica precondita: sub_web (import) NU are creds pe submission
@@ -518,7 +522,7 @@ class TestE2EMixedQueue:
try:
processed = 0
# Drena coada simuland bucla worker completa (T1/D4: creds efemere + fallback durabil)
# Drena coada simuland bucla worker completa (T1/D4: creds efemere + fallback per-env)
for _ in range(10): # limita de siguranta
claimed = w.claim_one(conn)
if claimed is None:
@@ -526,9 +530,10 @@ class TestE2EMixedQueue:
sid = claimed["id"]
account_id = claimed["account_id"]
rar_env = claimed.get("rar_env", "test")
# T1/D4: creds din submission (API efemer) OR fallback accounts.rar_creds_enc (web)
creds = w._creds_for(claimed, settings) or w._creds_from_account(conn, account_id)
# T1/D4: creds din submission (API efemer) OR fallback per-env (US-013)
creds = w._creds_for(claimed, settings) or w._creds_from_account(conn, account_id, rar_env=rar_env)
assert creds is not None, \
f"Creds None pentru submission {sid} — fallback durabil trebuie sa existe"
@@ -561,9 +566,9 @@ class TestE2EMixedQueue:
assert mock_rar.post_calls == 2
def test_purjare_creds_efemere_nu_sterge_durabile(self, env, monkeypatch):
"""T1/Gate purjare (OV-5): dupa login, submissions.rar_creds_enc sterse DAR accounts.rar_creds_enc INTACT.
"""T1/Gate purjare (OV-5): dupa login, submissions.rar_creds_enc sterse DAR per-env INTACT.
Worker sterge DOAR submissions.rar_creds_enc (efemere), NU accounts.rar_creds_enc (durabile).
Worker sterge DOAR submissions.rar_creds_enc (efemere), NU accounts.rar_creds_{env}_enc (durabile, US-013).
"""
import app.worker.__main__ as w
from app.crypto import encrypt_creds
@@ -585,11 +590,14 @@ class TestE2EMixedQueue:
assert r_api.status_code == 200
sub_id = r_api.json()["results"][0]["submission_id"]
# Seteaza creds durabile pe cont
# Seteaza creds durabile pe cont (slotul per-env, US-013)
conn = get_connection()
try:
web_creds_enc = encrypt_creds({"email": "web@test.ro", "password": "pass_web"})
conn.execute("UPDATE accounts SET rar_creds_enc=? WHERE id=1", (web_creds_enc,))
conn.execute(
"UPDATE accounts SET rar_creds_test_enc=?, rar_test_enabled=1 WHERE id=1",
(web_creds_enc,),
)
conn.commit()
finally:
conn.close()
@@ -602,8 +610,9 @@ class TestE2EMixedQueue:
try:
claimed = w.claim_one(conn)
assert claimed is not None
rar_env = claimed.get("rar_env", "test")
creds = w._creds_for(claimed, settings) or w._creds_from_account(conn, claimed["account_id"])
creds = w._creds_for(claimed, settings) or w._creds_from_account(conn, claimed["account_id"], rar_env=rar_env)
sessions.get_token(conn, claimed["account_id"], creds)
# Dupa login: submissions.rar_creds_enc sterse (creds efemere purjate)
@@ -613,12 +622,12 @@ class TestE2EMixedQueue:
assert row["rar_creds_enc"] is None, \
"submissions.rar_creds_enc trebuie sterse dupa login (efemere)"
# accounts.rar_creds_enc TREBUIE sa ramana (durabile, nu se sterg)
# accounts.rar_creds_test_enc TREBUIE sa ramana (durabile per-env, nu se sterg)
acc_row = conn.execute(
"SELECT rar_creds_enc FROM accounts WHERE id=1"
"SELECT rar_creds_test_enc FROM accounts WHERE id=1"
).fetchone()
assert acc_row["rar_creds_enc"] is not None, \
"accounts.rar_creds_enc trebuie sa RAMANA intact dupa purjarea creds efemere"
assert acc_row["rar_creds_test_enc"] is not None, \
"accounts.rar_creds_test_enc trebuie sa RAMANA intact dupa purjarea creds efemere"
finally:
conn.close()
sessions.close_all()

View File

@@ -61,13 +61,16 @@ def _creeaza_cheie(monkeypatch) -> str:
def _seteaza_rar_creds(monkeypatch=None) -> None:
"""Seteaza rar_creds_enc pe contul id=1."""
"""Seteaza creds RAR pe contul id=1 (slotul per-env, US-013)."""
from app.db import get_connection
from app.crypto import encrypt_creds
conn = get_connection()
try:
enc = encrypt_creds({"email": "test@rar.ro", "password": "secret"})
conn.execute("UPDATE accounts SET rar_creds_enc=? WHERE id=1", (enc,))
# US-013: scrie in slotul per-env (test = ancora globala in teste)
conn.execute(
"UPDATE accounts SET rar_creds_test_enc=?, rar_test_enabled=1 WHERE id=1", (enc,)
)
conn.commit()
finally:
conn.close()

View File

@@ -0,0 +1,282 @@
"""Teste US-013 (PRD 5.20): retragere coloana legacy accounts.rar_creds_enc.
Teste:
test_niciun_read_pe_coloana_veche -- DB fresh: accounts N-are rar_creds_enc, submissions ARE
test_conturi_rar_creds_env_aware -- POST /v1/conturi/rar-creds scrie in slotul per-env corect
test_are_creds_pe_per_env -- are_creds (web/integrare) reflecta sloturile per-env
test_drop_cu_garda_blocat_daca_lipsa_copiere -- garda refuza DROP cand exista orfane
"""
from __future__ import annotations
import os
import sqlite3
import tempfile
import pytest
from cryptography.fernet import Fernet
from starlette.testclient import TestClient
# ---------------------------------------------------------------------------
# Fixture
# ---------------------------------------------------------------------------
@pytest.fixture()
def client(monkeypatch):
"""Client izolat cu DB temporara + cheie Fernet pentru criptare creds."""
tmp = tempfile.mkdtemp()
monkeypatch.setenv("AUTOPASS_DB_PATH", os.path.join(tmp, "t_retragere.db"))
monkeypatch.setenv("AUTOPASS_CREDS_KEY", Fernet.generate_key().decode())
monkeypatch.setenv("AUTOPASS_RAR_ENV", "test")
monkeypatch.setenv("AUTOPASS_WEB_AUTH_REQUIRED", "false")
from app.config import get_settings
from app import crypto
get_settings.cache_clear()
crypto.reset_cache()
from app.main import app
with TestClient(app, follow_redirects=False) as c:
yield c
get_settings.cache_clear()
crypto.reset_cache()
@pytest.fixture()
def conn_izolat(monkeypatch):
"""Conexiune directa la DB temporara (fara app), pentru teste de migrare in-process."""
tmp = tempfile.mkdtemp()
db_path = os.path.join(tmp, "t_migrare.db")
monkeypatch.setenv("AUTOPASS_DB_PATH", db_path)
monkeypatch.setenv("AUTOPASS_CREDS_KEY", Fernet.generate_key().decode())
monkeypatch.setenv("AUTOPASS_RAR_ENV", "test")
from app.config import get_settings
from app import crypto
get_settings.cache_clear()
crypto.reset_cache()
from app.db import init_db, get_connection
init_db()
conn = get_connection()
conn.row_factory = sqlite3.Row
yield conn
conn.close()
get_settings.cache_clear()
crypto.reset_cache()
# ---------------------------------------------------------------------------
# Helpere
# ---------------------------------------------------------------------------
def _create_account_user(
name: str = "Service Test SRL",
email: str = "user@test.com",
password: str = "parolasecreta10",
):
"""Creeaza cont + user. Returneaza (acct_id, user_id)."""
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, name, active=True)
user_id = create_user(conn, acct_id, email, password)
return acct_id, user_id
finally:
conn.close()
def _login_web(client, email: str, password: str) -> None:
"""Face login prin HTTP si seteaza cookie-ul de sesiune."""
import re
resp = client.get("/login")
assert resp.status_code == 200
m = re.search(r'name="csrf_token"\s+value="([^"]+)"', resp.text)
if not m:
m = re.search(r'value="([^"]+)"\s+name="csrf_token"', resp.text)
assert m, "csrf_token negasit pe /login"
csrf = m.group(1)
resp = client.post("/login", data={"email": email, "parola": password, "csrf_token": csrf})
assert resp.status_code == 303, f"Login esuat: {resp.status_code} {resp.text[:200]}"
# ---------------------------------------------------------------------------
# Teste
# ---------------------------------------------------------------------------
def test_niciun_read_pe_coloana_veche(client):
"""DB fresh dupa init_db NU are accounts.rar_creds_enc; submissions INCA are rar_creds_enc."""
from app.db import get_connection
conn = get_connection()
try:
acc_cols = {r["name"] for r in conn.execute("PRAGMA table_info(accounts)").fetchall()}
sub_cols = {r["name"] for r in conn.execute("PRAGMA table_info(submissions)").fetchall()}
finally:
conn.close()
assert "rar_creds_enc" not in acc_cols, (
"accounts.rar_creds_enc INCA prezenta dupa init_db pe DB fresh — DROP esuat sau schema neactualizata"
)
assert "rar_creds_enc" in sub_cols, (
"submissions.rar_creds_enc TREBUIE sa ramana (creds efemere per-cerere)"
)
def test_conturi_rar_creds_env_aware(client):
"""POST /v1/conturi/rar-creds cu rar_target='test' scrie in rar_creds_test_enc + rar_test_enabled=1.
Fara rar_target -> scrie in slotul ancorei globale (AUTOPASS_RAR_ENV='test').
"""
from app.db import get_connection
acct_id, _ = _create_account_user("Firma ENV1", "env1@test.com")
acct2_id, _ = _create_account_user("Firma ENV2", "env2@test.com")
# Cu rar_target='test' explicit
resp = client.post(
"/v1/conturi/rar-creds",
json={"email": "rar@firma.ro", "password": "parolatest", "rar_target": "test"},
headers={"X-Account-ID": str(acct_id)},
)
# Fara cheie API in dev, cont implicit id=1; dar testam prin conn directa
# POST cu rar_target='test' pe contul 1 (dev, WEB_AUTH_REQUIRED=false)
resp_t = client.post(
"/v1/conturi/rar-creds",
json={"email": "rar_test@firma.ro", "password": "parolatest123", "rar_target": "test"},
)
assert resp_t.status_code == 200, f"set_rar_creds(test) esuat: {resp_t.text}"
body_t = resp_t.json()
assert body_t.get("ok") is True
assert body_t.get("rar_env") == "test", f"rar_env asteptat 'test', primit: {body_t}"
conn = get_connection()
try:
row_t = conn.execute(
"SELECT rar_creds_test_enc, rar_test_enabled FROM accounts WHERE id=1"
).fetchone()
finally:
conn.close()
assert row_t["rar_creds_test_enc"] is not None, "rar_creds_test_enc trebuia scris"
assert row_t["rar_test_enabled"] == 1, "rar_test_enabled trebuia setat la 1"
# Fara rar_target -> ancora globala = 'test' (AUTOPASS_RAR_ENV=test)
resp_n = client.post(
"/v1/conturi/rar-creds",
json={"email": "rar_noenv@firma.ro", "password": "paroladefault"},
)
assert resp_n.status_code == 200, f"set_rar_creds(no env) esuat: {resp_n.text}"
body_n = resp_n.json()
assert body_n.get("rar_env") == "test", f"rar_env implicit asteptat 'test': {body_n}"
# Cu rar_target='prod' -> scrie in rar_creds_prod_enc
resp_p = client.post(
"/v1/conturi/rar-creds",
json={"email": "rar_prod@firma.ro", "password": "parolaprod456", "rar_target": "prod"},
)
assert resp_p.status_code == 200, f"set_rar_creds(prod) esuat: {resp_p.text}"
body_p = resp_p.json()
assert body_p.get("rar_env") == "prod", f"rar_env asteptat 'prod': {body_p}"
conn = get_connection()
try:
row_p = conn.execute(
"SELECT rar_creds_prod_enc, rar_prod_enabled FROM accounts WHERE id=1"
).fetchone()
finally:
conn.close()
assert row_p["rar_creds_prod_enc"] is not None, "rar_creds_prod_enc trebuia scris la rar_target=prod"
assert row_p["rar_prod_enabled"] == 1, "rar_prod_enabled trebuia setat la 1"
def test_are_creds_pe_per_env(client):
"""GET /v1/ping are_creds_rar/are_creds_test/are_creds_prod reflecta sloturile per-env.
Fara creds -> are_creds_rar=False. Cu creds test -> are_creds_test=True, are_creds_rar=True.
"""
# Stare initiala: niciun slot cu creds
resp0 = client.get("/v1/ping")
assert resp0.status_code == 200
body0 = resp0.json()
assert body0.get("are_creds_rar") is False, f"are_creds_rar asteptat False initial: {body0}"
assert body0.get("are_creds_test") is False, f"are_creds_test asteptat False: {body0}"
assert body0.get("are_creds_prod") is False, f"are_creds_prod asteptat False: {body0}"
# Seteaza creds test
client.post(
"/v1/conturi/rar-creds",
json={"email": "rar@test.ro", "password": "parola123", "rar_target": "test"},
)
resp1 = client.get("/v1/ping")
assert resp1.status_code == 200
body1 = resp1.json()
assert body1.get("are_creds_rar") is True, f"are_creds_rar asteptat True dupa set test: {body1}"
assert body1.get("are_creds_test") is True, f"are_creds_test asteptat True: {body1}"
assert body1.get("are_creds_prod") is False, f"are_creds_prod asteptat False: {body1}"
# DELETE ?env=test -> test dispare, prod ramane False
resp_d = client.delete("/v1/conturi/rar-creds?env=test")
assert resp_d.status_code == 200
resp2 = client.get("/v1/ping")
body2 = resp2.json()
assert body2.get("are_creds_rar") is False, f"are_creds_rar asteptat False dupa delete test: {body2}"
assert body2.get("are_creds_test") is False
def test_drop_cu_garda_blocat_daca_lipsa_copiere(conn_izolat):
"""Garda refuza DROP cand exista cont cu rar_creds_enc dar ambele sloturi per-env goale.
Apelam _garda_si_drop direct (fara re-backfill 6c) pe o stare adversariala:
cont cu valoare in coloana legacy, rar_creds_test_enc=NULL, rar_creds_prod_enc=NULL.
Rezultat asteptat: DROP refuzat (coloana inca prezenta pe accounts).
"""
conn = conn_izolat
# Adaugam manual coloana legacy daca nu exista (DB fresh a dropat-o deja prin _drop_legacy)
acc_cols = {r["name"] for r in conn.execute("PRAGMA table_info(accounts)").fetchall()}
if "rar_creds_enc" not in acc_cols:
conn.execute("ALTER TABLE accounts ADD COLUMN rar_creds_enc TEXT")
conn.commit()
# Stare adversariala: insert valoare dummy in coloana legacy, sloturi per-env goale
conn.execute(
"UPDATE accounts SET rar_creds_enc='DUMMY_CRIPTAT', "
"rar_creds_test_enc=NULL, rar_creds_prod_enc=NULL WHERE id=1"
)
conn.commit()
# Verifica ca starea adversariala e corecta inainte de test
row_before = conn.execute(
"SELECT rar_creds_enc, rar_creds_test_enc, rar_creds_prod_enc FROM accounts WHERE id=1"
).fetchone()
assert row_before["rar_creds_enc"] == "DUMMY_CRIPTAT"
assert row_before["rar_creds_test_enc"] is None
assert row_before["rar_creds_prod_enc"] is None
# Apelam _garda_si_drop direct (fara re-backfill 6c)
from app.db import _garda_si_drop
_garda_si_drop(conn)
# Garda trebuia sa refuze DROP-ul: coloana rar_creds_enc inca prezenta pe accounts
acc_cols_after = {r["name"] for r in conn.execute("PRAGMA table_info(accounts)").fetchall()}
assert "rar_creds_enc" in acc_cols_after, (
"Garda a permis DROP cand exista orfane — pierdere de date (BUG CRITIC)"
)
# Valoarea legacy trebuie sa fie inca prezenta (neatinsa)
row_after = conn.execute(
"SELECT rar_creds_enc FROM accounts WHERE id=1"
).fetchone()
assert row_after["rar_creds_enc"] == "DUMMY_CRIPTAT", (
"Valoarea rar_creds_enc disparuta desi DROP a fost refuzat"
)

View File

@@ -1,9 +1,11 @@
"""Teste T1: accounts.rar_creds_enc durabile + worker re-login fallback + gate purjare.
"""Teste T1: creds durabile per-cont (per-env, US-013) + worker re-login fallback + gate purjare.
Verify:
(a) Serie web, worker restart (sesiune goala), token expirat -> re-login din accounts -> trimite.
(b) Coada MIXTA API(efemer)+web(durabil): dupa login web, submission-urile API tot se trimit
(purjarea nu le-a rupt prematur).
US-013: accounts.rar_creds_enc (legacy) a fost dropata; sloturile per-env sunt singura sursa.
"""
from __future__ import annotations
@@ -81,20 +83,23 @@ def test_creds_from_account_fallback(env, monkeypatch):
conn = get_connection()
try:
enc = encrypt_creds({"email": "web@test.ro", "password": "webpass"})
conn.execute("UPDATE accounts SET rar_creds_enc=? WHERE id=1", (enc,))
# US-013: scrie in slotul per-env test (slotul legacy rar_creds_enc dropat)
conn.execute(
"UPDATE accounts SET rar_creds_test_enc=?, rar_test_enabled=1 WHERE id=1", (enc,)
)
# Submission web fara creds_enc (ex: dupa ce s-au purjat)
_insert(conn, account_id=1, creds_enc=None)
# _creds_from_account trebuie sa returneze creds
creds = w._creds_from_account(conn, 1)
# _creds_from_account trebuie sa returneze creds din slotul per-env test
creds = w._creds_from_account(conn, 1, rar_env="test")
assert creds == {"email": "web@test.ro", "password": "webpass"}
finally:
conn.close()
def test_creds_from_account_no_creds(env):
"""Cont fara rar_creds_enc -> None (canal API pur, neatins)."""
"""Cont fara creds durabile (niciun slot per-env) -> None (canal API pur, neatins)."""
import app.worker.__main__ as w
from app.db import get_connection
@@ -117,7 +122,10 @@ def test_worker_relogin_dupa_restart(env, monkeypatch):
conn = get_connection()
try:
enc = encrypt_creds({"email": "web@test.ro", "password": "webpass"})
conn.execute("UPDATE accounts SET rar_creds_enc=? WHERE id=1", (enc,))
# US-013: scrie in slotul per-env test
conn.execute(
"UPDATE accounts SET rar_creds_test_enc=?, rar_test_enabled=1 WHERE id=1", (enc,)
)
# Submission web fara creds (creds deja purjate de primul login)
_insert(conn, account_id=1, creds_enc=None)
@@ -126,8 +134,8 @@ def test_worker_relogin_dupa_restart(env, monkeypatch):
sessions = w.AccountSessions(w.get_settings())
assert sessions.get_token(conn, 1, None) is None # fara creds directe
# Creds din account -> login posibil
creds = w._creds_from_account(conn, 1)
# Creds din account (per-env) -> login posibil
creds = w._creds_from_account(conn, 1, rar_env="test")
assert creds is not None
token = sessions.get_token(conn, 1, creds)
assert token == "TOK-web@test.ro"
@@ -141,10 +149,10 @@ def test_coada_mixta_api_web(env, monkeypatch):
"""(b) Coada mixta: dupa login web, submission-urile API (efemere) tot se trimit.
Scenariul:
1. S1 = submission API cu creds efemere in submission.rar_creds_enc
2. S2 = submission WEB fara creds (foloseste accounts.rar_creds_enc)
3. Login cu creds S1 -> purjare S1.rar_creds_enc -> OK (worker are token)
4. S2 tot se poate procesa (creds din accounts)
1. S1 = submission API cu creds efemere in submissions.rar_creds_enc
2. S2 = submission WEB fara creds (foloseste accounts.rar_creds_test_enc per-env)
3. Login cu creds S1 -> purjare S1.rar_creds_enc (pe submissions) -> OK (worker are token)
4. S2 tot se poate procesa (creds din accounts per-env, US-013)
"""
import app.worker.__main__ as w
from app.crypto import encrypt_creds
@@ -154,11 +162,13 @@ def test_coada_mixta_api_web(env, monkeypatch):
conn = get_connection()
try:
# Creds durabile pentru contul web
# Creds durabile pentru contul web (slotul per-env, US-013)
enc_web = encrypt_creds({"email": "web@test.ro", "password": "webpass"})
conn.execute("UPDATE accounts SET rar_creds_enc=? WHERE id=1", (enc_web,))
conn.execute(
"UPDATE accounts SET rar_creds_test_enc=?, rar_test_enabled=1 WHERE id=1", (enc_web,)
)
# S1: canal API cu creds efemere
# S1: canal API cu creds efemere in submission
enc_api = encrypt_creds({"email": "api@test.ro", "password": "apipass"})
s1 = _insert(conn, account_id=1, creds_enc=enc_api, key_suffix="api1")
# S2: canal web fara creds in submission
@@ -166,24 +176,24 @@ def test_coada_mixta_api_web(env, monkeypatch):
sessions = w.AccountSessions(w.get_settings())
# Procesare S1: login cu creds API -> purjare rar_creds_enc pe TOATE submission-urile contului
# Procesare S1: login cu creds API -> purjare submissions.rar_creds_enc
creds_s1 = w._creds_for({"creds_enc": enc_api}, w.get_settings())
assert creds_s1 is not None
sessions.get_token(conn, 1, creds_s1) # login + purjare
sessions.get_token(conn, 1, creds_s1) # login + purjare submissions.rar_creds_enc
# Verifica purjarea: S1.rar_creds_enc = NULL acum
# Verifica purjarea: S1.rar_creds_enc (submissions) = NULL acum
row_s1 = conn.execute("SELECT rar_creds_enc FROM submissions WHERE id=?", (s1,)).fetchone()
assert row_s1["rar_creds_enc"] is None, "creds efemere trebuie sterse dupa login"
# S2 nu mai are creds in submission (nici nu a avut); fallback la accounts
creds_s2 = w._creds_for({"creds_enc": None}, w.get_settings()) or w._creds_from_account(conn, 1)
# S2 nu mai are creds in submission (nici nu a avut); fallback la accounts per-env
creds_s2 = w._creds_for({"creds_enc": None}, w.get_settings()) or w._creds_from_account(conn, 1, rar_env="test")
assert creds_s2 == {"email": "web@test.ro", "password": "webpass"}, \
"S2 trebuie sa ia creds din accounts.rar_creds_enc"
"S2 trebuie sa ia creds din accounts.rar_creds_test_enc (per-env)"
# accounts.rar_creds_enc NU a fost sters de purjare
row_acc = conn.execute("SELECT rar_creds_enc FROM accounts WHERE id=1").fetchone()
assert row_acc["rar_creds_enc"] is not None, \
"accounts.rar_creds_enc trebuie sa ramana dupa purjare submissions"
# accounts.rar_creds_test_enc NU a fost atins de purjarea submissions
row_acc = conn.execute("SELECT rar_creds_test_enc FROM accounts WHERE id=1").fetchone()
assert row_acc["rar_creds_test_enc"] is not None, \
"accounts.rar_creds_test_enc trebuie sa ramana dupa purjare submissions"
finally:
conn.close()
@@ -199,29 +209,37 @@ def client(env):
def test_endpoint_set_rar_creds(client, env):
"""POST /v1/conturi/rar-creds seteaza creds criptate in accounts."""
"""POST /v1/conturi/rar-creds seteaza creds criptate in slotul per-env (US-013)."""
from app.crypto import decrypt_creds
from app.db import get_connection
# Fara rar_target -> ancora globala (test in fixture, AUTOPASS_RAR_ENV implicit)
r = client.post("/v1/conturi/rar-creds", json={"email": "u@test.ro", "password": "pass123"})
assert r.status_code == 200
assert r.json()["ok"] is True
# Raspunsul include rar_env folosit
assert "rar_env" in r.json()
conn = get_connection()
try:
row = conn.execute("SELECT rar_creds_enc FROM accounts WHERE id=1").fetchone()
assert row["rar_creds_enc"] is not None
creds = decrypt_creds(row["rar_creds_enc"])
# Cel putin unul din sloturi trebuie sa fie populat
row = conn.execute(
"SELECT rar_creds_test_enc, rar_creds_prod_enc FROM accounts WHERE id=1"
).fetchone()
enc = row["rar_creds_test_enc"] or row["rar_creds_prod_enc"]
assert enc is not None, "Cel putin un slot per-env trebuia populat"
creds = decrypt_creds(enc)
assert creds == {"email": "u@test.ro", "password": "pass123"}
finally:
conn.close()
def test_endpoint_delete_rar_creds(client, env):
"""DELETE /v1/conturi/rar-creds sterge creds durabile."""
# Mai intai seteaza
client.post("/v1/conturi/rar-creds", json={"email": "u@test.ro", "password": "pass123"})
# Sterge
"""DELETE /v1/conturi/rar-creds sterge creds durabile (ambele sloturi per-env, US-013)."""
# Mai intai seteaza pe test si prod
client.post("/v1/conturi/rar-creds", json={"email": "u@test.ro", "password": "pass123", "rar_target": "test"})
client.post("/v1/conturi/rar-creds", json={"email": "u@prod.ro", "password": "pass456", "rar_target": "prod"})
# Sterge fara env -> ambele sloturi NULL
r = client.delete("/v1/conturi/rar-creds")
assert r.status_code == 200
assert r.json()["ok"] is True
@@ -229,14 +247,17 @@ def test_endpoint_delete_rar_creds(client, env):
from app.db import get_connection
conn = get_connection()
try:
row = conn.execute("SELECT rar_creds_enc FROM accounts WHERE id=1").fetchone()
assert row["rar_creds_enc"] is None
row = conn.execute(
"SELECT rar_creds_test_enc, rar_creds_prod_enc FROM accounts WHERE id=1"
).fetchone()
assert row["rar_creds_test_enc"] is None, "rar_creds_test_enc trebuia sters"
assert row["rar_creds_prod_enc"] is None, "rar_creds_prod_enc trebuia sters"
finally:
conn.close()
def test_gate_purjare_nu_sterge_accounts(env, monkeypatch):
"""Gate purjare T1: stergerea submissions.rar_creds_enc NU atinge accounts.rar_creds_enc."""
"""Gate purjare T1: stergerea submissions.rar_creds_enc NU atinge accounts per-env (US-013)."""
import app.worker.__main__ as w
from app.crypto import encrypt_creds
from app.db import get_connection
@@ -246,15 +267,18 @@ def test_gate_purjare_nu_sterge_accounts(env, monkeypatch):
conn = get_connection()
try:
enc = encrypt_creds({"email": "u@test.ro", "password": "p"})
conn.execute("UPDATE accounts SET rar_creds_enc=? WHERE id=1", (enc,))
# US-013: scrie in slotul per-env test
conn.execute(
"UPDATE accounts SET rar_creds_test_enc=?, rar_test_enabled=1 WHERE id=1", (enc,)
)
_insert(conn, account_id=1, creds_enc=enc)
sessions = w.AccountSessions(w.get_settings())
sessions.get_token(conn, 1, {"email": "u@test.ro", "password": "p"})
# accounts.rar_creds_enc trebuie sa fie intact
row = conn.execute("SELECT rar_creds_enc FROM accounts WHERE id=1").fetchone()
assert row["rar_creds_enc"] is not None, \
"gate purjare: accounts.rar_creds_enc trebuie sa ramana intact"
# accounts.rar_creds_test_enc NU trebuie atins de purjarea submissions
row = conn.execute("SELECT rar_creds_test_enc FROM accounts WHERE id=1").fetchone()
assert row["rar_creds_test_enc"] is not None, \
"gate purjare: accounts.rar_creds_test_enc trebuie sa ramana intact"
finally:
conn.close()

View File

@@ -131,7 +131,7 @@ def test_roteste_cheie_afisata_o_data(client):
# ============================================================
def test_set_creds_rar_din_sesiune(client):
"""User logat seteaza creds RAR: accounts.rar_creds_enc != NULL, decriptabil."""
"""User logat seteaza creds RAR: slotul per-env (US-013) != NULL, decriptabil."""
acct_id, user_id, _ = _create_account_user("creds@test.com")
_login(client, "creds@test.com", "parolasecreta10")
@@ -147,17 +147,18 @@ def test_set_creds_rar_din_sesiune(client):
assert "succes" in resp.text.lower() or "salvat" in resp.text.lower() or "configurat" in resp.text.lower(), \
f"Mesaj de succes lipsa: {resp.text[:500]}"
# Verifica in DB: rar_creds_enc setat si decriptabil
# Verifica in DB: slotul per-env setat si decriptabil (US-013: rar_creds_enc legacy dropata)
from app.db import get_connection
from app.crypto import decrypt_creds
conn = get_connection()
try:
row = conn.execute(
"SELECT rar_creds_enc FROM accounts WHERE id=?", (acct_id,)
"SELECT rar_creds_test_enc, rar_creds_prod_enc FROM accounts WHERE id=?", (acct_id,)
).fetchone()
assert row is not None
assert row["rar_creds_enc"] is not None, "rar_creds_enc trebuia setat"
creds = decrypt_creds(row["rar_creds_enc"])
enc = row["rar_creds_test_enc"] or row["rar_creds_prod_enc"]
assert enc is not None, "Cel putin un slot per-env trebuia setat (US-013)"
creds = decrypt_creds(enc)
assert creds is not None
assert creds.get("email") == "user@rar.ro"
assert creds.get("password") == "parolaRAR123"
@@ -170,7 +171,7 @@ def test_set_creds_rar_din_sesiune(client):
# ============================================================
def test_creds_alt_cont_neafectat(client):
"""User A seteaza creds -> contul B ramane cu rar_creds_enc NULL."""
"""User A seteaza creds -> contul B ramane fara creds (sloturi per-env NULL, US-013)."""
acct_a, user_a, _ = _create_account_user("userA@test.com")
acct_b, user_b, _ = _create_account_user("userB@test.com")
@@ -184,14 +185,20 @@ def test_creds_alt_cont_neafectat(client):
})
assert resp.status_code == 200
# Verifica: contul A are creds, contul B ramane NULL
# Verifica: contul A are creds in slotul per-env, contul B ramane NULL (US-013)
from app.db import get_connection
conn = get_connection()
try:
row_a = conn.execute("SELECT rar_creds_enc FROM accounts WHERE id=?", (acct_a,)).fetchone()
row_b = conn.execute("SELECT rar_creds_enc FROM accounts WHERE id=?", (acct_b,)).fetchone()
assert row_a["rar_creds_enc"] is not None, "Contul A trebuia sa aiba creds"
assert row_b["rar_creds_enc"] is None, "Contul B nu trebuia atins"
row_a = conn.execute(
"SELECT rar_creds_test_enc, rar_creds_prod_enc FROM accounts WHERE id=?", (acct_a,)
).fetchone()
row_b = conn.execute(
"SELECT rar_creds_test_enc, rar_creds_prod_enc FROM accounts WHERE id=?", (acct_b,)
).fetchone()
enc_a = row_a["rar_creds_test_enc"] or row_a["rar_creds_prod_enc"]
enc_b = row_b["rar_creds_test_enc"] or row_b["rar_creds_prod_enc"]
assert enc_a is not None, "Contul A trebuia sa aiba creds in slotul per-env"
assert enc_b is None, "Contul B nu trebuia atins"
finally:
conn.close()

View File

@@ -58,7 +58,7 @@ def _login(client, email: str, password: str = "parolasecreta10") -> None:
def _set_rar_creds(acct_id: int) -> None:
"""Seteaza rar_creds_enc pe cont (simuleaza configurarea credentialelor RAR)."""
"""Seteaza creds RAR pe cont in slotul per-env test (US-013 — legacy rar_creds_enc dropata)."""
from app.db import get_connection
from app.crypto import encrypt_creds
@@ -66,9 +66,10 @@ def _set_rar_creds(acct_id: int) -> None:
try:
enc = encrypt_creds({"email": "test@rar.ro", "password": "parola_rar"})
conn.execute(
"UPDATE accounts SET rar_creds_enc=? WHERE id=?",
"UPDATE accounts SET rar_creds_test_enc=?, rar_test_enabled=1 WHERE id=?",
(enc, acct_id),
)
conn.commit()
finally:
conn.close()

View File

@@ -67,11 +67,18 @@ def _set_last_login(conn, *, ago_s: float | None):
def _account_cu_creds(conn) -> int:
"""Creeaza cont cu creds in slotul per-env (US-013 — legacy rar_creds_enc dropata)."""
from app.accounts import create_account
from app.crypto import encrypt_creds
acct = create_account(conn, "Service Cu Creds", email="svc@example.com")
enc = encrypt_creds({"email": "svc@example.com", "password": "secret"})
conn.execute("UPDATE accounts SET rar_creds_enc=? WHERE id=?", (enc, acct))
# US-013: scrie in slotul per-env; rar_env din fixture = valoarea default (test sau prod).
# Folosim rar_creds_test_enc si rar_creds_prod_enc (ambele) pentru robustete.
conn.execute(
"UPDATE accounts SET rar_creds_test_enc=?, rar_test_enabled=1, "
"rar_creds_prod_enc=?, rar_prod_enabled=1 WHERE id=?",
(enc, enc, acct),
)
conn.commit()
return acct
@@ -155,11 +162,14 @@ def test_target_sare_creds_nedecriptabile(env):
settings.worker_use_test_creds = False
# Cont cu creds GUNOI (nedecriptabile sub cheia curenta), id mai mic.
bad = create_account(conn, "Cont Cheie Veche", email="old@example.com")
conn.execute("UPDATE accounts SET rar_creds_enc=? WHERE id=?", ("gAAAAA-token-invalid", bad))
# US-013: scrie in slotul per-env (rar_env = settings.rar_env, implicit in fixture)
bad_slot = f"rar_creds_{settings.rar_env}_enc"
conn.execute(f"UPDATE accounts SET {bad_slot}='gAAAAA-token-invalid' WHERE id=?", (bad,))
# Cont cu creds valide, id mai mare.
good = create_account(conn, "Cont Valid", email="good@example.com")
enc = encrypt_creds({"email": "good@example.com", "password": "pw"})
conn.execute("UPDATE accounts SET rar_creds_enc=? WHERE id=?", (enc, good))
good_slot = f"rar_creds_{settings.rar_env}_enc"
conn.execute(f"UPDATE accounts SET {good_slot}=? WHERE id=?", (enc, good))
conn.commit()
acct_id, creds = _keepalive_target(conn, settings)