Files
rar-autopass/tests/test_retragere_creds_enc.py
Claude Agent b1d825e66b 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>
2026-07-02 21:03:08 +00:00

283 lines
11 KiB
Python

"""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"
)