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:
@@ -238,7 +238,8 @@ def delete_account(conn: sqlite3.Connection, account_id: int) -> None:
|
|||||||
NU acest tombstone — de aceea purjam PII aici, la momentul stergerii."""
|
NU acest tombstone — de aceea purjam PII aici, la momentul stergerii."""
|
||||||
set_status(conn, account_id, "deleted") # valideaza existenta + protejeaza id=1; seteaza status+active=0
|
set_status(conn, account_id, "deleted") # valideaza existenta + protejeaza id=1; seteaza status+active=0
|
||||||
conn.execute(
|
conn.execute(
|
||||||
"UPDATE accounts SET rar_creds_enc=NULL, cui=NULL WHERE id=?", (account_id,)
|
"UPDATE accounts SET rar_creds_test_enc=NULL, rar_creds_prod_enc=NULL, cui=NULL WHERE id=?",
|
||||||
|
(account_id,),
|
||||||
)
|
)
|
||||||
conn.execute(
|
conn.execute(
|
||||||
"UPDATE api_keys SET active=0, revoked_at=datetime('now') WHERE account_id=? AND active=1",
|
"UPDATE api_keys SET active=0, revoked_at=datetime('now') WHERE account_id=? AND active=1",
|
||||||
@@ -247,7 +248,7 @@ def delete_account(conn: sqlite3.Connection, account_id: int) -> None:
|
|||||||
|
|
||||||
|
|
||||||
def list_accounts(conn: sqlite3.Connection) -> list[dict]:
|
def list_accounts(conn: sqlite3.Connection) -> list[dict]:
|
||||||
"""Metadate conturi (FARA `rar_creds_enc`), ordonate dupa id. Exclude conturile 'deleted'
|
"""Metadate conturi (FARA creds RAR criptate), ordonate dupa id. Exclude conturile 'deleted'
|
||||||
(stergere soft -> invizibile in panou)."""
|
(stergere soft -> invizibile in panou)."""
|
||||||
rows = conn.execute(
|
rows = conn.execute(
|
||||||
"SELECT id, name, cui, email, active, status, tier, trial_until, "
|
"SELECT id, name, cui, email, active, status, tier, trial_until, "
|
||||||
|
|||||||
@@ -37,7 +37,9 @@ def ping(
|
|||||||
account_id — contul rezolvat din cheie (sau 1 in dev fara cheie)
|
account_id — contul rezolvat din cheie (sau 1 in dev fara cheie)
|
||||||
mediu — "test" / "prod" (settings.rar_env)
|
mediu — "test" / "prod" (settings.rar_env)
|
||||||
autentificat_cu_cheie — True daca cererea a venit cu o cheie API reala valida
|
autentificat_cu_cheie — True daca cererea a venit cu o cheie API reala valida
|
||||||
are_creds_rar — True daca contul are rar_creds_enc stocat
|
are_creds_rar — True daca contul are creds RAR stocate pe cel putin un mediu (test sau prod)
|
||||||
|
are_creds_test — True daca contul are creds RAR pentru mediul Testare
|
||||||
|
are_creds_prod — True daca contul are creds RAR pentru mediul Productie
|
||||||
ts — timestamp ISO UTC al cererii
|
ts — timestamp ISO UTC al cererii
|
||||||
"""
|
"""
|
||||||
settings = get_settings()
|
settings = get_settings()
|
||||||
@@ -55,23 +57,27 @@ def ping(
|
|||||||
conn.close()
|
conn.close()
|
||||||
autentificat_cu_cheie = acct is not None
|
autentificat_cu_cheie = acct is not None
|
||||||
|
|
||||||
# Verificam daca contul are creds RAR stocate.
|
# Verificam daca contul are creds RAR stocate (per-env, US-013).
|
||||||
aid = account_or_default(account_id)
|
aid = account_or_default(account_id)
|
||||||
conn = get_connection()
|
conn = get_connection()
|
||||||
try:
|
try:
|
||||||
row = conn.execute(
|
row = conn.execute(
|
||||||
"SELECT rar_creds_enc FROM accounts WHERE id=?", (aid,)
|
"SELECT rar_creds_test_enc, rar_creds_prod_enc FROM accounts WHERE id=?", (aid,)
|
||||||
).fetchone()
|
).fetchone()
|
||||||
finally:
|
finally:
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
are_creds_rar = bool(row and row["rar_creds_enc"])
|
are_creds_test = bool(row and row["rar_creds_test_enc"])
|
||||||
|
are_creds_prod = bool(row and row["rar_creds_prod_enc"])
|
||||||
|
are_creds_rar = are_creds_test or are_creds_prod
|
||||||
|
|
||||||
return JSONResponse({
|
return JSONResponse({
|
||||||
"account_id": aid,
|
"account_id": aid,
|
||||||
"mediu": settings.rar_env,
|
"mediu": settings.rar_env,
|
||||||
"autentificat_cu_cheie": autentificat_cu_cheie,
|
"autentificat_cu_cheie": autentificat_cu_cheie,
|
||||||
"are_creds_rar": are_creds_rar,
|
"are_creds_rar": are_creds_rar,
|
||||||
|
"are_creds_test": are_creds_test,
|
||||||
|
"are_creds_prod": are_creds_prod,
|
||||||
"ts": datetime.now(timezone.utc).isoformat(),
|
"ts": datetime.now(timezone.utc).isoformat(),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -149,7 +149,7 @@ def create_prezentari(
|
|||||||
account_id vine din cheia API (resolve_account_id): cont real cu cheie,
|
account_id vine din cheia API (resolve_account_id): cont real cu cheie,
|
||||||
implicit id=1 in dev fara cheie, 401 fara cheie valida in prod.
|
implicit id=1 in dev fara cheie, 401 fara cheie valida in prod.
|
||||||
Cand rar_credentials lipseste, submission-ul intra fara creds efemere: worker-ul
|
Cand rar_credentials lipseste, submission-ul intra fara creds efemere: worker-ul
|
||||||
cade pe creds-urile durabile ale contului (`accounts.rar_creds_enc`).
|
cade pe creds-urile durabile ale contului (per-env: `accounts.rar_creds_{env}_enc`).
|
||||||
"""
|
"""
|
||||||
acct = account_or_default(account_id)
|
acct = account_or_default(account_id)
|
||||||
# Creds RAR efemere: criptate si lipite de fiecare submission nou pana la
|
# Creds RAR efemere: criptate si lipite de fiecare submission nou pana la
|
||||||
@@ -276,12 +276,11 @@ def create_prezentari(
|
|||||||
cl["rar_error"], creds_enc, env, existing["id"]),
|
cl["rar_error"], creds_enc, env, existing["id"]),
|
||||||
)
|
)
|
||||||
if cur.rowcount == 1:
|
if cur.rowcount == 1:
|
||||||
# Creds noi se propaga si in canalul durabil (accounts.rar_creds_enc)
|
# Creds noi se propaga si in slotul durabil per-env al contului
|
||||||
# — ambele canale converg pe parola corectata.
|
# — ambele canale converg pe parola corectata (US-013, env-aware).
|
||||||
# US-013: muta pe slot env dupa login (write-back conservator).
|
|
||||||
if req.rar_credentials is not None:
|
if req.rar_credentials is not None:
|
||||||
conn.execute(
|
conn.execute(
|
||||||
"UPDATE accounts SET rar_creds_enc=? WHERE id=?",
|
f"UPDATE accounts SET rar_creds_{env}_enc=?, rar_{env}_enabled=1 WHERE id=?",
|
||||||
(encrypt_creds(req.rar_credentials.model_dump()), acct),
|
(encrypt_creds(req.rar_credentials.model_dump()), acct),
|
||||||
)
|
)
|
||||||
_emite_text_rule_hits(conn, acct, existing["id"], cl["resolved"])
|
_emite_text_rule_hits(conn, acct, existing["id"], cl["resolved"])
|
||||||
@@ -742,10 +741,16 @@ def create_mapare(
|
|||||||
|
|
||||||
|
|
||||||
class RarCredsIn(BaseModel):
|
class RarCredsIn(BaseModel):
|
||||||
"""Creds RAR durabile per-cont. Stocate criptate (Fernet) in accounts.rar_creds_enc."""
|
"""Creds RAR durabile per-cont, stocate criptat (Fernet) in slotul per-mediu.
|
||||||
|
|
||||||
|
`rar_target` selecteaza mediul: 'test' | 'prod'. Absent -> mediul ancorei globale
|
||||||
|
(AUTOPASS_RAR_ENV), implicit 'test'. Schimbare aditiva — clientii vechi care nu trimit
|
||||||
|
`rar_target` continua sa functioneze (comportament consistent cu ancora globala).
|
||||||
|
"""
|
||||||
|
|
||||||
email: str = Field(..., min_length=1)
|
email: str = Field(..., min_length=1)
|
||||||
password: str = Field(..., min_length=1, repr=False)
|
password: str = Field(..., min_length=1, repr=False)
|
||||||
|
rar_target: str | None = None # 'test' | 'prod' | None -> ancora globala
|
||||||
|
|
||||||
|
|
||||||
@router.post("/conturi/rar-creds")
|
@router.post("/conturi/rar-creds")
|
||||||
@@ -753,21 +758,26 @@ def set_rar_creds(
|
|||||||
req: RarCredsIn,
|
req: RarCredsIn,
|
||||||
account_id: int = Depends(resolve_account_id),
|
account_id: int = Depends(resolve_account_id),
|
||||||
) -> dict:
|
) -> dict:
|
||||||
"""Seteaza creds RAR durabile per-cont.
|
"""Seteaza creds RAR durabile per-cont, in slotul per-mediu (US-013, env-aware).
|
||||||
|
|
||||||
Criptate Fernet in accounts.rar_creds_enc. Worker-ul le foloseste ca fallback
|
Slotul tinta: `req.rar_target` ('test'/'prod') sau ancora globala (AUTOPASS_RAR_ENV).
|
||||||
cand submission-ul nu mai are creds (canal web fara re-pusher, restart worker).
|
Activeaza mediul selectat (`rar_{env}_enabled=1`). Worker-ul le foloseste ca
|
||||||
Contul vine din cheia API.
|
fallback cand submission-ul nu mai are creds efemere. Contul vine din cheia API.
|
||||||
"""
|
"""
|
||||||
|
from ...config import get_settings as _gs
|
||||||
|
_s = _gs()
|
||||||
|
env = req.rar_target if req.rar_target in ("test", "prod") else (
|
||||||
|
_s.rar_env if _s.rar_env in ("test", "prod") else "test"
|
||||||
|
)
|
||||||
acct = account_or_default(account_id)
|
acct = account_or_default(account_id)
|
||||||
enc = encrypt_creds({"email": req.email, "password": req.password})
|
enc = encrypt_creds({"email": req.email, "password": req.password})
|
||||||
conn = get_connection()
|
conn = get_connection()
|
||||||
try:
|
try:
|
||||||
conn.execute(
|
conn.execute(
|
||||||
"UPDATE accounts SET rar_creds_enc=? WHERE id=?",
|
f"UPDATE accounts SET rar_creds_{env}_enc=?, rar_{env}_enabled=1 WHERE id=?",
|
||||||
(enc, acct),
|
(enc, acct),
|
||||||
)
|
)
|
||||||
return {"ok": True, "account_id": acct}
|
return {"ok": True, "account_id": acct, "rar_env": env}
|
||||||
finally:
|
finally:
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
@@ -775,12 +785,27 @@ def set_rar_creds(
|
|||||||
@router.delete("/conturi/rar-creds")
|
@router.delete("/conturi/rar-creds")
|
||||||
def delete_rar_creds(
|
def delete_rar_creds(
|
||||||
account_id: int = Depends(resolve_account_id),
|
account_id: int = Depends(resolve_account_id),
|
||||||
|
env: str | None = None,
|
||||||
) -> dict:
|
) -> dict:
|
||||||
"""Sterge creds RAR durabile per-cont (revenire la modelul efemer Treapta 1)."""
|
"""Sterge creds RAR durabile per-cont (revenire la modelul efemer Treapta 1).
|
||||||
|
|
||||||
|
`env` (query param): 'test' | 'prod' -> sterge DOAR slotul acelui mediu + dezactiveaza-l.
|
||||||
|
Absent -> sterge AMBELE sloturi (revenire completa). Schimbare aditiva (back-compat).
|
||||||
|
"""
|
||||||
acct = account_or_default(account_id)
|
acct = account_or_default(account_id)
|
||||||
conn = get_connection()
|
conn = get_connection()
|
||||||
try:
|
try:
|
||||||
conn.execute("UPDATE accounts SET rar_creds_enc=NULL WHERE id=?", (acct,))
|
if env in ("test", "prod"):
|
||||||
|
conn.execute(
|
||||||
|
f"UPDATE accounts SET rar_creds_{env}_enc=NULL, rar_{env}_enabled=0 WHERE id=?",
|
||||||
|
(acct,),
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
conn.execute(
|
||||||
|
"UPDATE accounts SET rar_creds_test_enc=NULL, rar_test_enabled=0, "
|
||||||
|
"rar_creds_prod_enc=NULL, rar_prod_enabled=0 WHERE id=?",
|
||||||
|
(acct,),
|
||||||
|
)
|
||||||
return {"ok": True, "account_id": acct}
|
return {"ok": True, "account_id": acct}
|
||||||
finally:
|
finally:
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|||||||
99
app/db.py
99
app/db.py
@@ -86,11 +86,12 @@ def _migrate(conn: sqlite3.Connection) -> None:
|
|||||||
|
|
||||||
# Coloane accounts
|
# Coloane accounts
|
||||||
acc_cols = {r["name"] for r in conn.execute("PRAGMA table_info(accounts)").fetchall()}
|
acc_cols = {r["name"] for r in conn.execute("PRAGMA table_info(accounts)").fetchall()}
|
||||||
if "rar_creds_enc" not in acc_cols:
|
# AUTO-FIX 6a ELIMINAT (US-013): NU mai adaugam accounts.rar_creds_enc — coloana e dropata.
|
||||||
conn.execute("ALTER TABLE accounts ADD COLUMN rar_creds_enc TEXT")
|
# _migrate_accounts_medii gestioneaza absenta coloana (guard la ~219: if "rar_creds_enc" not in acc_cols: return).
|
||||||
acc_cols.add("rar_creds_enc")
|
|
||||||
# Medii RAR per cont (PRD 5.20 US-001): activare + slot creds + default, per mediu.
|
# Medii RAR per cont (PRD 5.20 US-001): activare + slot creds + default, per mediu.
|
||||||
_migrate_accounts_medii(conn, acc_cols)
|
_migrate_accounts_medii(conn, acc_cols)
|
||||||
|
# US-013: DROP coloana legacy accounts.rar_creds_enc dupa backfill complet.
|
||||||
|
_drop_legacy_accounts_rar_creds(conn, acc_cols)
|
||||||
if "active" not in acc_cols:
|
if "active" not in acc_cols:
|
||||||
# Conturi existente raman active (default 1).
|
# Conturi existente raman active (default 1).
|
||||||
conn.execute("ALTER TABLE accounts ADD COLUMN active INTEGER NOT NULL DEFAULT 1")
|
conn.execute("ALTER TABLE accounts ADD COLUMN active INTEGER NOT NULL DEFAULT 1")
|
||||||
@@ -229,6 +230,98 @@ def _migrate_accounts_medii(conn: sqlite3.Connection, acc_cols: set[str]) -> Non
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _drop_legacy_accounts_rar_creds(conn: sqlite3.Connection, acc_cols: set[str]) -> None:
|
||||||
|
"""PRD 5.20 US-013: DROP coloana legacy `accounts.rar_creds_enc` dupa backfill complet.
|
||||||
|
|
||||||
|
Idempotent si sigur la fiecare boot (garda one-way: coloana absenta = nimic de facut).
|
||||||
|
La eroare LOGHEAZA si lasa coloana pe loc (fail-safe — nu crapa boot-ul ambelor procese).
|
||||||
|
Structura separata pentru testabilitate: `_garda_si_drop(conn)` expune pasul de
|
||||||
|
assert+backup+DROP izolat (fara re-backfill 6c), apelabil direct din teste.
|
||||||
|
"""
|
||||||
|
if "rar_creds_enc" not in acc_cols:
|
||||||
|
return # garda one-way: coloana deja dropata sau DB fresh
|
||||||
|
try:
|
||||||
|
_drop_legacy_rar_creds_impl(conn)
|
||||||
|
except Exception as exc:
|
||||||
|
print(
|
||||||
|
f"[db] AVERTISMENT: DROP coloana legacy accounts.rar_creds_enc esuat: {exc}. "
|
||||||
|
"Coloana ramane pe loc (fail-safe).",
|
||||||
|
flush=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _drop_legacy_rar_creds_impl(conn: sqlite3.Connection) -> None:
|
||||||
|
"""Re-backfill (AUTO-FIX 6c) + delegate la _garda_si_drop.
|
||||||
|
|
||||||
|
Re-backfill-ul 6c acopera creds setate via POST /v1/conturi/rar-creds intre US-001
|
||||||
|
si US-013 (pot fi DOAR in coloana veche). Ancora globala: AUTOPASS_RAR_ENV.
|
||||||
|
"""
|
||||||
|
if sqlite3.sqlite_version_info < (3, 35, 0):
|
||||||
|
print(
|
||||||
|
f"[db] SQLite {sqlite3.sqlite_version} < 3.35.0 — DROP COLUMN nesuportat; "
|
||||||
|
"coloana legacy accounts.rar_creds_enc ramane.",
|
||||||
|
flush=True,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
# AUTO-FIX 6c: re-backfill creds din coloana veche in slotul per-env (ancora globala).
|
||||||
|
# Independent de _migrate_accounts_medii (care sare pe DB deja migrat cu guard newly_added).
|
||||||
|
env = get_settings().rar_env if get_settings().rar_env in ("test", "prod") else "test"
|
||||||
|
slot = f"rar_creds_{env}_enc"
|
||||||
|
conn.execute(
|
||||||
|
f"UPDATE accounts SET {slot}=rar_creds_enc, rar_{env}_enabled=1 "
|
||||||
|
f"WHERE rar_creds_enc IS NOT NULL AND TRIM(rar_creds_enc)<>'' "
|
||||||
|
f"AND ({slot} IS NULL OR TRIM({slot})='')"
|
||||||
|
)
|
||||||
|
|
||||||
|
_garda_si_drop(conn)
|
||||||
|
|
||||||
|
|
||||||
|
def _garda_si_drop(conn: sqlite3.Connection) -> None:
|
||||||
|
"""Garda de siguranta + backup + DROP accounts.rar_creds_enc. Testabila izolat.
|
||||||
|
|
||||||
|
Verifica ca niciun cont nu are creds DOAR in coloana veche (ambele sloturi per-env goale).
|
||||||
|
Daca exista orfane -> NU dropa (fail-safe: fara pierdere de date).
|
||||||
|
Altfel: backup criptat, DROP, verificare PRAGMA (AUTO-FIX 6d).
|
||||||
|
"""
|
||||||
|
# Garda: orfane = cont cu creds in coloana veche DAR ambele sloturi per-env goale.
|
||||||
|
orphan_count = conn.execute(
|
||||||
|
"SELECT COUNT(*) FROM accounts "
|
||||||
|
"WHERE rar_creds_enc IS NOT NULL AND TRIM(rar_creds_enc)<>'' "
|
||||||
|
"AND (rar_creds_test_enc IS NULL OR TRIM(rar_creds_test_enc)='') "
|
||||||
|
"AND (rar_creds_prod_enc IS NULL OR TRIM(rar_creds_prod_enc)='')"
|
||||||
|
).fetchone()[0]
|
||||||
|
|
||||||
|
if orphan_count > 0:
|
||||||
|
print(
|
||||||
|
f"[db] AVERTISMENT: {orphan_count} cont(uri) cu rar_creds_enc ne-copiat in niciun slot "
|
||||||
|
"per-env. DROP anulat (fail-safe: fara pierdere de date).",
|
||||||
|
flush=True,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Backup criptat inainte de DROP (blob-urile sunt deja criptate Fernet).
|
||||||
|
conn.execute(
|
||||||
|
"CREATE TABLE IF NOT EXISTS accounts_rar_creds_enc_backup "
|
||||||
|
"(account_id INTEGER, rar_creds_enc TEXT, backed_up_at TEXT)"
|
||||||
|
)
|
||||||
|
conn.execute(
|
||||||
|
"INSERT INTO accounts_rar_creds_enc_backup "
|
||||||
|
"SELECT id, rar_creds_enc, datetime('now') FROM accounts "
|
||||||
|
"WHERE rar_creds_enc IS NOT NULL"
|
||||||
|
)
|
||||||
|
|
||||||
|
# DROP coloana legacy.
|
||||||
|
conn.execute("ALTER TABLE accounts DROP COLUMN rar_creds_enc")
|
||||||
|
|
||||||
|
# AUTO-FIX 6d: verifica prin PRAGMA (pe tabela accounts, NU grep — submissions are aceeasi coloana).
|
||||||
|
cols_after = {r["name"] for r in conn.execute("PRAGMA table_info(accounts)").fetchall()}
|
||||||
|
if "rar_creds_enc" in cols_after:
|
||||||
|
raise RuntimeError("DROP COLUMN rar_creds_enc esuat: coloana inca prezenta dupa ALTER TABLE")
|
||||||
|
|
||||||
|
print("[db] DROP coloana legacy accounts.rar_creds_enc: OK", flush=True)
|
||||||
|
|
||||||
|
|
||||||
def _backfill_submissions_rar_env(conn: sqlite3.Connection) -> None:
|
def _backfill_submissions_rar_env(conn: sqlite3.Connection) -> None:
|
||||||
"""PRD 5.20 US-001 (AUTO-FIX G + E4/3): backfill rar_env + recompute idempotency_key.
|
"""PRD 5.20 US-001 (AUTO-FIX G + E4/3): backfill rar_env + recompute idempotency_key.
|
||||||
|
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ class PrezentareRequest(BaseModel):
|
|||||||
"""Body pentru POST /v1/prezentari — una sau mai multe prezentari + creds RAR.
|
"""Body pentru POST /v1/prezentari — una sau mai multe prezentari + creds RAR.
|
||||||
|
|
||||||
`rar_credentials` e OPTIONAL: daca lipseste, worker-ul foloseste creds-urile RAR
|
`rar_credentials` e OPTIONAL: daca lipseste, worker-ul foloseste creds-urile RAR
|
||||||
durabile salvate pe cont (`accounts.rar_creds_enc`, via POST /v1/conturi/rar-creds).
|
durabile salvate pe cont (per-env: `accounts.rar_creds_{env}_enc`, via POST /v1/conturi/rar-creds).
|
||||||
Trimite-le explicit doar cand vrei sa suprascrii creds-urile contului pe acea cerere.
|
Trimite-le explicit doar cand vrei sa suprascrii creds-urile contului pe acea cerere.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|||||||
@@ -19,8 +19,8 @@ CREATE TABLE IF NOT EXISTS accounts (
|
|||||||
-- vezi accounts.delete_account — randul ramane doar pentru audit).
|
-- vezi accounts.delete_account — randul ramane doar pentru audit).
|
||||||
status TEXT NOT NULL DEFAULT 'active'
|
status TEXT NOT NULL DEFAULT 'active'
|
||||||
CHECK (status IN ('pending','active','blocked','archived','deleted')),
|
CHECK (status IN ('pending','active','blocked','archived','deleted')),
|
||||||
rar_creds_enc TEXT, -- LEGACY (PRD 5.20 US-013 dropeaza coloana): creds RAR durabile env-less
|
|
||||||
-- Medii RAR per cont (PRD 5.20 US-001). Fiecare mediu = bifa de activare + slot creds.
|
-- Medii RAR per cont (PRD 5.20 US-001). Fiecare mediu = bifa de activare + slot creds.
|
||||||
|
-- accounts.rar_creds_enc (legacy env-less) a fost dropata in US-013 (SQLite DROP COLUMN).
|
||||||
-- medii_disponibile = enabled AND creds prezente (app/rar_env.py). Cont client nou =
|
-- medii_disponibile = enabled AND creds prezente (app/rar_env.py). Cont client nou =
|
||||||
-- Productie on / Testare off (clientii declara real); contul operator se pune manual pe Testare.
|
-- Productie on / Testare off (clientii declara real); contul operator se pune manual pe Testare.
|
||||||
rar_test_enabled INTEGER NOT NULL DEFAULT 0 CHECK (rar_test_enabled IN (0, 1)),
|
rar_test_enabled INTEGER NOT NULL DEFAULT 0 CHECK (rar_test_enabled IN (0, 1)),
|
||||||
|
|||||||
@@ -294,13 +294,13 @@ def _get_acasa_context(request: Request, conn, account_id: int) -> dict:
|
|||||||
acct = account_or_default(account_id)
|
acct = account_or_default(account_id)
|
||||||
|
|
||||||
# Pas 1: are credentiale RAR configurate? + metadate cont (pentru banner incomplet)
|
# Pas 1: are credentiale RAR configurate? + metadate cont (pentru banner incomplet)
|
||||||
# Verifica atat coloana legacy rar_creds_enc cat si sloturile per-env (US-008, PRD 5.20).
|
# US-013: citim exclusiv sloturile per-env (legacy accounts.rar_creds_enc a fost dropat).
|
||||||
row = conn.execute(
|
row = conn.execute(
|
||||||
"SELECT id, name, cui, email, rar_creds_enc, rar_creds_test_enc, rar_creds_prod_enc "
|
"SELECT id, name, cui, email, rar_creds_test_enc, rar_creds_prod_enc "
|
||||||
"FROM accounts WHERE id=?", (acct,)
|
"FROM accounts WHERE id=?", (acct,)
|
||||||
).fetchone()
|
).fetchone()
|
||||||
are_creds = bool(row and (
|
are_creds = bool(row and (
|
||||||
row["rar_creds_enc"] or row["rar_creds_test_enc"] or row["rar_creds_prod_enc"]
|
row["rar_creds_test_enc"] or row["rar_creds_prod_enc"]
|
||||||
))
|
))
|
||||||
# Banner cont incomplet (US-002): contul nu are companie + email + CUI complete
|
# Banner cont incomplet (US-002): contul nu are companie + email + CUI complete
|
||||||
cont_incomplet = not _acct_is_complete(row) if row else False
|
cont_incomplet = not _acct_is_complete(row) if row else False
|
||||||
@@ -438,9 +438,9 @@ def _render_integrare(request: Request, conn, account_id: int) -> str:
|
|||||||
|
|
||||||
acct = account_or_default(account_id)
|
acct = account_or_default(account_id)
|
||||||
row_creds = conn.execute(
|
row_creds = conn.execute(
|
||||||
"SELECT rar_creds_enc FROM accounts WHERE id=?", (acct,)
|
"SELECT rar_creds_test_enc, rar_creds_prod_enc FROM accounts WHERE id=?", (acct,)
|
||||||
).fetchone()
|
).fetchone()
|
||||||
are_creds = bool(row_creds and row_creds["rar_creds_enc"])
|
are_creds = bool(row_creds and (row_creds["rar_creds_test_enc"] or row_creds["rar_creds_prod_enc"]))
|
||||||
|
|
||||||
row_key = conn.execute(
|
row_key = conn.execute(
|
||||||
"SELECT 1 FROM api_keys WHERE account_id=? AND active=1 LIMIT 1", (acct,)
|
"SELECT 1 FROM api_keys WHERE account_id=? AND active=1 LIMIT 1", (acct,)
|
||||||
@@ -4241,7 +4241,7 @@ def _fetch_cont_env_state(conn, acct: int) -> dict:
|
|||||||
"""
|
"""
|
||||||
row = conn.execute(
|
row = conn.execute(
|
||||||
"SELECT rar_test_enabled, rar_prod_enabled, "
|
"SELECT rar_test_enabled, rar_prod_enabled, "
|
||||||
"rar_creds_test_enc, rar_creds_prod_enc, rar_env_default, rar_creds_enc "
|
"rar_creds_test_enc, rar_creds_prod_enc, rar_env_default "
|
||||||
"FROM accounts WHERE id=?", (acct,)
|
"FROM accounts WHERE id=?", (acct,)
|
||||||
).fetchone()
|
).fetchone()
|
||||||
if not row:
|
if not row:
|
||||||
@@ -4263,8 +4263,9 @@ def _fetch_cont_env_state(conn, acct: int) -> dict:
|
|||||||
medii.append("test")
|
medii.append("test")
|
||||||
if prod_disponibil:
|
if prod_disponibil:
|
||||||
medii.append("prod")
|
medii.append("prod")
|
||||||
|
# US-013: are_creds bazat EXCLUSIV pe sloturile per-env (legacy rar_creds_enc dropat).
|
||||||
are_creds = bool(
|
are_creds = bool(
|
||||||
row["rar_creds_enc"] or row["rar_creds_test_enc"] or row["rar_creds_prod_enc"]
|
row["rar_creds_test_enc"] or row["rar_creds_prod_enc"]
|
||||||
)
|
)
|
||||||
return {
|
return {
|
||||||
"are_creds": are_creds,
|
"are_creds": are_creds,
|
||||||
@@ -4529,10 +4530,12 @@ def cont_rar_creds(
|
|||||||
)
|
)
|
||||||
|
|
||||||
enc = encrypt_creds({"email": email, "password": parola})
|
enc = encrypt_creds({"email": email, "password": parola})
|
||||||
|
# US-013: scrie in slotul per-env al ancorei globale (nu mai exista coloana legacy).
|
||||||
|
_env_w = get_settings().rar_env if get_settings().rar_env in ("test", "prod") else "test"
|
||||||
conn = get_connection()
|
conn = get_connection()
|
||||||
try:
|
try:
|
||||||
conn.execute(
|
conn.execute(
|
||||||
"UPDATE accounts SET rar_creds_enc=? WHERE id=?",
|
f"UPDATE accounts SET rar_creds_{_env_w}_enc=?, rar_{_env_w}_enabled=1 WHERE id=?",
|
||||||
(enc, acct),
|
(enc, acct),
|
||||||
)
|
)
|
||||||
account_meta = _fetch_account_meta(conn, acct)
|
account_meta = _fetch_account_meta(conn, acct)
|
||||||
|
|||||||
@@ -11,11 +11,12 @@ Ruleaza ca proces separat sub `restart: always` (docker compose).
|
|||||||
- lease/timeout pe randuri 'sending' orfane.
|
- lease/timeout pe randuri 'sending' orfane.
|
||||||
- re-login la token expirat (401 mid-sesiune) — JWT 30h, retry NU plafonat la 30h.
|
- re-login la token expirat (401 mid-sesiune) — JWT 30h, retry NU plafonat la 30h.
|
||||||
|
|
||||||
Creds per-cerere: fiecare submission poarta creds RAR CRIPTATE (rar_creds_enc).
|
Creds per-cerere: fiecare submission poarta creds RAR CRIPTATE (submissions.rar_creds_enc).
|
||||||
Worker-ul face login per CONT cu acele creds, cache-uieste JWT (30h) in memorie si
|
Worker-ul face login per CONT cu acele creds, cache-uieste JWT (30h) in memorie si
|
||||||
STERGE creds-urile contului dupa primul login reusit. Token-ul in memorie acopera
|
STERGE creds-urile efemere dupa primul login reusit. Token-ul in memorie acopera
|
||||||
restul trimiterilor; la restart token-ul se pierde si contul re-logheaza la urmatorul
|
restul trimiterilor; la restart token-ul se pierde si contul re-logheaza la urmatorul
|
||||||
submission care aduce creds proaspete (degradare acceptata).
|
submission care aduce creds proaspete (degradare acceptata). Fallback durabil: slotul
|
||||||
|
per-env al contului (accounts.rar_creds_{env}_enc, US-013; coloana legacy dropata).
|
||||||
Dev: `worker_use_test_creds` foloseste creds <test> cand submission-ul nu are enc.
|
Dev: `worker_use_test_creds` foloseste creds <test> cand submission-ul nu are enc.
|
||||||
|
|
||||||
Pornire: python -m app.worker
|
Pornire: python -m app.worker
|
||||||
@@ -161,8 +162,8 @@ def requeue_with_backoff(conn, settings: Settings, submission_id: int, *, reason
|
|||||||
def claim_one(conn) -> dict | None:
|
def claim_one(conn) -> dict | None:
|
||||||
"""Claim atomic 'queued' -> 'sending', respectand next_attempt_at. Intoarce randul sau None.
|
"""Claim atomic 'queued' -> 'sending', respectand next_attempt_at. Intoarce randul sau None.
|
||||||
|
|
||||||
Randul include `account_id` si `rar_creds_enc` (creds RAR criptate) pentru
|
Randul include `account_id` si `submissions.rar_creds_enc` (creds RAR criptate efemere)
|
||||||
login-ul per-cont din `run`.
|
pentru login-ul per-cont din `run`.
|
||||||
"""
|
"""
|
||||||
conn.execute("BEGIN IMMEDIATE")
|
conn.execute("BEGIN IMMEDIATE")
|
||||||
try:
|
try:
|
||||||
@@ -431,18 +432,17 @@ def _creds_for(claimed: dict, settings: Settings) -> dict | None:
|
|||||||
|
|
||||||
|
|
||||||
def _creds_from_account(conn, account_id: int, rar_env: str = "test") -> dict | None:
|
def _creds_from_account(conn, account_id: int, rar_env: str = "test") -> dict | None:
|
||||||
"""Creds RAR durabile per-cont din slotul per-env, cu fallback la coloana legacy.
|
"""Creds RAR durabile per-cont din slotul per-env (US-013 — coloana legacy dropata).
|
||||||
|
|
||||||
Canal web: creds in accounts.rar_creds_{rar_env}_enc (per-env). Fallback la
|
Canal web: creds in accounts.rar_creds_{rar_env}_enc (per-env, singurul slot valid).
|
||||||
accounts.rar_creds_enc (legacy, back-compat inainte de US-013 care dropa coloana veche).
|
|
||||||
"""
|
"""
|
||||||
env_slot = f"rar_creds_{rar_env}_enc"
|
env_slot = f"rar_creds_{rar_env}_enc"
|
||||||
row = conn.execute(
|
row = conn.execute(
|
||||||
f"SELECT {env_slot}, rar_creds_enc FROM accounts WHERE id=?", (account_id,)
|
f"SELECT {env_slot} FROM accounts WHERE id=?", (account_id,)
|
||||||
).fetchone()
|
).fetchone()
|
||||||
if not row:
|
if not row:
|
||||||
return None
|
return None
|
||||||
enc = row[env_slot] or row["rar_creds_enc"] # per-env intai, legacy fallback
|
enc = row[env_slot]
|
||||||
return decrypt_creds(enc) if enc else None
|
return decrypt_creds(enc) if enc else None
|
||||||
|
|
||||||
|
|
||||||
@@ -450,16 +450,16 @@ def _keepalive_target(conn, settings: Settings) -> tuple[int | None, dict | None
|
|||||||
"""Un cont cu creds durabile pentru login-ul de proba (sau creds <test> in dev).
|
"""Un cont cu creds durabile pentru login-ul de proba (sau creds <test> in dev).
|
||||||
|
|
||||||
Ancora M2: cauta in slotul per-env al mediului `settings.rar_env` (ancora globala).
|
Ancora M2: cauta in slotul per-env al mediului `settings.rar_env` (ancora globala).
|
||||||
Fallback la coloana legacy `rar_creds_enc` (back-compat inainte de US-013).
|
US-013: coloana legacy accounts.rar_creds_enc a fost dropata — se foloseste EXCLUSIV
|
||||||
Sare conturile ale caror creds NU se decripteaza sub cheia curenta — in dev
|
slotul per-env. Sare conturile ale caror creds NU se decripteaza sub cheia curenta
|
||||||
`start.sh both` genereaza o cheie efemera noua la fiecare pornire.
|
(in dev `start.sh both` genereaza o cheie efemera noua la fiecare pornire).
|
||||||
"""
|
"""
|
||||||
env_slot = f"rar_creds_{settings.rar_env}_enc"
|
env_slot = f"rar_creds_{settings.rar_env}_enc"
|
||||||
rows = conn.execute(
|
rows = conn.execute(
|
||||||
f"SELECT id, {env_slot}, rar_creds_enc FROM accounts ORDER BY id"
|
f"SELECT id, {env_slot} FROM accounts ORDER BY id"
|
||||||
).fetchall()
|
).fetchall()
|
||||||
for row in rows:
|
for row in rows:
|
||||||
enc = row[env_slot] or row["rar_creds_enc"] # per-env intai, legacy fallback
|
enc = row[env_slot]
|
||||||
if not enc:
|
if not enc:
|
||||||
continue
|
continue
|
||||||
creds = decrypt_creds(enc)
|
creds = decrypt_creds(enc)
|
||||||
|
|||||||
@@ -507,5 +507,48 @@ răspuns de validare (câmpuri, cod_prestatie, rezolvare operație) ca la trimit
|
|||||||
|
|
||||||
Planul Gratuit are limită de **60 prezentări/lună** (indiferent de canal). La depășire: `422 PLAN_LIMITA_LUNARA`.
|
Planul Gratuit are limită de **60 prezentări/lună** (indiferent de canal). La depășire: `422 PLAN_LIMITA_LUNARA`.
|
||||||
Planul Pro nu are limită de volum. `GET /v1/nomenclator` rămâne public pe orice plan (exploatare pre-upgrade).
|
Planul Pro nu are limită de volum. `GET /v1/nomenclator` rămâne public pe orice plan (exploatare pre-upgrade).
|
||||||
|
|
||||||
|
## Creds RAR durabile per-mediu (PRD 5.20 US-013)
|
||||||
|
|
||||||
|
`POST /v1/conturi/rar-creds` — setează credențialele RAR durabile per-cont, cu scriere în slotul per-mediu.
|
||||||
|
|
||||||
|
**Body JSON:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"email": "user@firma.ro",
|
||||||
|
"password": "parola",
|
||||||
|
"rar_target": "test"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`rar_target`: `"test"` | `"prod"` | absent. Absent → ancora globală (`AUTOPASS_RAR_ENV`), implicit `"test"`.
|
||||||
|
Schimbare **aditivă** (back-compat): clienții care nu trimit `rar_target` continuă să funcționeze.
|
||||||
|
|
||||||
|
Efecte: scrie în `accounts.rar_creds_{env}_enc` + activează `rar_{env}_enabled=1`.
|
||||||
|
|
||||||
|
**Răspuns:** `{"ok": true, "account_id": N, "rar_env": "test"}`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
`DELETE /v1/conturi/rar-creds` — șterge credențialele RAR durabile per-cont.
|
||||||
|
|
||||||
|
**Query param** `env`: `test` | `prod` | absent.
|
||||||
|
- `?env=test` → NULL doar slotul Testare + `rar_test_enabled=0`.
|
||||||
|
- `?env=prod` → NULL doar slotul Producție + `rar_prod_enabled=0`.
|
||||||
|
- Absent → NULL ambele sloturi (revenire completă la modelul efemer Treapta 1).
|
||||||
|
|
||||||
|
Schimbare **aditivă** (back-compat).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
`GET /v1/ping` — răspuns include acum și câmpurile per-mediu:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"are_creds_rar": true,
|
||||||
|
"are_creds_test": true,
|
||||||
|
"are_creds_prod": false
|
||||||
|
}
|
||||||
|
```
|
||||||
|
`are_creds_rar` rămâne (back-compat) = OR logic al celor două.
|
||||||
</content>
|
</content>
|
||||||
</invoke>
|
</invoke>
|
||||||
|
|||||||
@@ -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)."""
|
"""Stergerea soft purjeaza creds RAR + revoca cheile API + elibereaza CUI (re-inregistrabil)."""
|
||||||
from app.accounts import create_account, delete_account, list_accounts
|
from app.accounts import create_account, delete_account, list_accounts
|
||||||
acct_id = create_account(conn, "Service GDPR", cui="RO12345", active=True)
|
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.execute("INSERT INTO api_keys (account_id, key_hash, active) VALUES (?, 'h', 1)", (acct_id,))
|
||||||
conn.commit()
|
conn.commit()
|
||||||
|
|
||||||
delete_account(conn, acct_id)
|
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()
|
(acct_id,)).fetchone()
|
||||||
assert row["status"] == "deleted"
|
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"
|
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()
|
key_active = conn.execute("SELECT active FROM api_keys WHERE account_id=?", (acct_id,)).fetchone()
|
||||||
assert key_active["active"] == 0, "cheile API trebuie revocate"
|
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
|
from app.db import _migrate
|
||||||
|
|
||||||
# Reconstruim accounts fara `status` (rebuild de tabela — singura cale in SQLite vechi).
|
# 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(
|
conn.executescript(
|
||||||
"""
|
"""
|
||||||
PRAGMA foreign_keys=OFF;
|
PRAGMA foreign_keys=OFF;
|
||||||
CREATE TABLE accounts_legacy (
|
CREATE TABLE accounts_legacy (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, cui TEXT,
|
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)
|
INSERT INTO accounts_legacy (id, name, cui, active, created_at)
|
||||||
SELECT id, name, cui, active, rar_creds_enc, created_at FROM accounts;
|
SELECT id, name, cui, active, created_at FROM accounts;
|
||||||
DROP TABLE accounts;
|
DROP TABLE accounts;
|
||||||
ALTER TABLE accounts_legacy RENAME TO accounts;
|
ALTER TABLE accounts_legacy RENAME TO accounts;
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -82,16 +82,16 @@ def test_resubmit_actualizeaza_creds_pe_reactivare(client):
|
|||||||
enc_dupa = _creds_enc(sid)
|
enc_dupa = _creds_enc(sid)
|
||||||
assert enc_dupa is not None
|
assert enc_dupa is not None
|
||||||
assert enc_dupa != enc_initial, "creds-urile trebuie actualizate la reactivare"
|
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.db import get_connection
|
||||||
from app.crypto import decrypt_creds
|
from app.crypto import decrypt_creds
|
||||||
conn = get_connection()
|
conn = get_connection()
|
||||||
try:
|
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:
|
finally:
|
||||||
conn.close()
|
conn.close()
|
||||||
assert row["rar_creds_enc"] is not None
|
assert row["rar_creds_test_enc"] is not None
|
||||||
assert decrypt_creds(row["rar_creds_enc"])["password"] == "parolaNoua"
|
assert decrypt_creds(row["rar_creds_test_enc"])["password"] == "parolaNoua"
|
||||||
|
|
||||||
|
|
||||||
def test_resubmit_peste_sent_ramane_deduped(client):
|
def test_resubmit_peste_sent_ramane_deduped(client):
|
||||||
|
|||||||
@@ -34,9 +34,14 @@ def _tables(conn) -> set[str]:
|
|||||||
|
|
||||||
# --- Coloane noi ---
|
# --- Coloane noi ---
|
||||||
|
|
||||||
def test_accounts_rar_creds_enc(db_conn):
|
def test_accounts_rar_creds_enc_dropata(db_conn):
|
||||||
cols = _table_cols(db_conn, "accounts")
|
"""US-013: accounts.rar_creds_enc a fost dropata; submissions.rar_creds_enc ramane."""
|
||||||
assert "rar_creds_enc" in cols
|
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):
|
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 "batch_id" in sub_cols
|
||||||
assert "row_index" in sub_cols
|
assert "row_index" in sub_cols
|
||||||
assert "purge_after" 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()
|
conn.close()
|
||||||
get_settings.cache_clear()
|
get_settings.cache_clear()
|
||||||
|
|
||||||
|
|||||||
@@ -5,9 +5,9 @@ Acopera (plan T15, sect.12):
|
|||||||
-> worker process_one (MockRar) -> submission FINALIZATA cu id_prezentare.
|
-> worker process_one (MockRar) -> submission FINALIZATA cu id_prezentare.
|
||||||
- Scenariul 2: re-upload acelasi continut -> preview marcheaza already_sent
|
- Scenariul 2: re-upload acelasi continut -> preview marcheaza already_sent
|
||||||
(NU al doilea FINALIZATA dupa commit).
|
(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
|
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.
|
- Masina de stari (sect. 6): tranzitii queued->sending->sent/requeued/error.
|
||||||
- Failure registry (sect. 8): 400/403/503+reconciliere.
|
- Failure registry (sect. 8): 400/403/503+reconciliere.
|
||||||
- T16: purge_expired + purge_after setat la commit.
|
- 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).
|
"""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
|
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'.
|
Verificam ca ambele submission-uri ajung la status='sent'.
|
||||||
"""
|
"""
|
||||||
import app.worker.__main__ as w
|
import app.worker.__main__ as w
|
||||||
@@ -486,11 +486,15 @@ class TestE2EMixedQueue:
|
|||||||
assert r_api.status_code == 200, r_api.text
|
assert r_api.status_code == 200, r_api.text
|
||||||
sub_api_id = r_api.json()["results"][0]["submission_id"]
|
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()
|
conn = get_connection()
|
||||||
try:
|
try:
|
||||||
web_creds_enc = encrypt_creds({"email": "web@test.ro", "password": "pass_web"})
|
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()
|
conn.commit()
|
||||||
|
|
||||||
# Verifica precondita: sub_web (import) NU are creds pe submission
|
# Verifica precondita: sub_web (import) NU are creds pe submission
|
||||||
@@ -518,7 +522,7 @@ class TestE2EMixedQueue:
|
|||||||
try:
|
try:
|
||||||
processed = 0
|
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
|
for _ in range(10): # limita de siguranta
|
||||||
claimed = w.claim_one(conn)
|
claimed = w.claim_one(conn)
|
||||||
if claimed is None:
|
if claimed is None:
|
||||||
@@ -526,9 +530,10 @@ class TestE2EMixedQueue:
|
|||||||
|
|
||||||
sid = claimed["id"]
|
sid = claimed["id"]
|
||||||
account_id = claimed["account_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)
|
# 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)
|
creds = w._creds_for(claimed, settings) or w._creds_from_account(conn, account_id, rar_env=rar_env)
|
||||||
assert creds is not None, \
|
assert creds is not None, \
|
||||||
f"Creds None pentru submission {sid} — fallback durabil trebuie sa existe"
|
f"Creds None pentru submission {sid} — fallback durabil trebuie sa existe"
|
||||||
|
|
||||||
@@ -561,9 +566,9 @@ class TestE2EMixedQueue:
|
|||||||
assert mock_rar.post_calls == 2
|
assert mock_rar.post_calls == 2
|
||||||
|
|
||||||
def test_purjare_creds_efemere_nu_sterge_durabile(self, env, monkeypatch):
|
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
|
import app.worker.__main__ as w
|
||||||
from app.crypto import encrypt_creds
|
from app.crypto import encrypt_creds
|
||||||
@@ -585,11 +590,14 @@ class TestE2EMixedQueue:
|
|||||||
assert r_api.status_code == 200
|
assert r_api.status_code == 200
|
||||||
sub_id = r_api.json()["results"][0]["submission_id"]
|
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()
|
conn = get_connection()
|
||||||
try:
|
try:
|
||||||
web_creds_enc = encrypt_creds({"email": "web@test.ro", "password": "pass_web"})
|
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()
|
conn.commit()
|
||||||
finally:
|
finally:
|
||||||
conn.close()
|
conn.close()
|
||||||
@@ -602,8 +610,9 @@ class TestE2EMixedQueue:
|
|||||||
try:
|
try:
|
||||||
claimed = w.claim_one(conn)
|
claimed = w.claim_one(conn)
|
||||||
assert claimed is not None
|
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)
|
sessions.get_token(conn, claimed["account_id"], creds)
|
||||||
|
|
||||||
# Dupa login: submissions.rar_creds_enc sterse (creds efemere purjate)
|
# Dupa login: submissions.rar_creds_enc sterse (creds efemere purjate)
|
||||||
@@ -613,12 +622,12 @@ class TestE2EMixedQueue:
|
|||||||
assert row["rar_creds_enc"] is None, \
|
assert row["rar_creds_enc"] is None, \
|
||||||
"submissions.rar_creds_enc trebuie sterse dupa login (efemere)"
|
"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(
|
acc_row = conn.execute(
|
||||||
"SELECT rar_creds_enc FROM accounts WHERE id=1"
|
"SELECT rar_creds_test_enc FROM accounts WHERE id=1"
|
||||||
).fetchone()
|
).fetchone()
|
||||||
assert acc_row["rar_creds_enc"] is not None, \
|
assert acc_row["rar_creds_test_enc"] is not None, \
|
||||||
"accounts.rar_creds_enc trebuie sa RAMANA intact dupa purjarea creds efemere"
|
"accounts.rar_creds_test_enc trebuie sa RAMANA intact dupa purjarea creds efemere"
|
||||||
finally:
|
finally:
|
||||||
conn.close()
|
conn.close()
|
||||||
sessions.close_all()
|
sessions.close_all()
|
||||||
|
|||||||
@@ -61,13 +61,16 @@ def _creeaza_cheie(monkeypatch) -> str:
|
|||||||
|
|
||||||
|
|
||||||
def _seteaza_rar_creds(monkeypatch=None) -> None:
|
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.db import get_connection
|
||||||
from app.crypto import encrypt_creds
|
from app.crypto import encrypt_creds
|
||||||
conn = get_connection()
|
conn = get_connection()
|
||||||
try:
|
try:
|
||||||
enc = encrypt_creds({"email": "test@rar.ro", "password": "secret"})
|
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()
|
conn.commit()
|
||||||
finally:
|
finally:
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|||||||
282
tests/test_retragere_creds_enc.py
Normal file
282
tests/test_retragere_creds_enc.py
Normal 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"
|
||||||
|
)
|
||||||
@@ -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:
|
Verify:
|
||||||
(a) Serie web, worker restart (sesiune goala), token expirat -> re-login din accounts -> trimite.
|
(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
|
(b) Coada MIXTA API(efemer)+web(durabil): dupa login web, submission-urile API tot se trimit
|
||||||
(purjarea nu le-a rupt prematur).
|
(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
|
from __future__ import annotations
|
||||||
@@ -81,20 +83,23 @@ def test_creds_from_account_fallback(env, monkeypatch):
|
|||||||
conn = get_connection()
|
conn = get_connection()
|
||||||
try:
|
try:
|
||||||
enc = encrypt_creds({"email": "web@test.ro", "password": "webpass"})
|
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)
|
# Submission web fara creds_enc (ex: dupa ce s-au purjat)
|
||||||
_insert(conn, account_id=1, creds_enc=None)
|
_insert(conn, account_id=1, creds_enc=None)
|
||||||
|
|
||||||
# _creds_from_account trebuie sa returneze creds
|
# _creds_from_account trebuie sa returneze creds din slotul per-env test
|
||||||
creds = w._creds_from_account(conn, 1)
|
creds = w._creds_from_account(conn, 1, rar_env="test")
|
||||||
assert creds == {"email": "web@test.ro", "password": "webpass"}
|
assert creds == {"email": "web@test.ro", "password": "webpass"}
|
||||||
finally:
|
finally:
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
def test_creds_from_account_no_creds(env):
|
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
|
import app.worker.__main__ as w
|
||||||
from app.db import get_connection
|
from app.db import get_connection
|
||||||
|
|
||||||
@@ -117,7 +122,10 @@ def test_worker_relogin_dupa_restart(env, monkeypatch):
|
|||||||
conn = get_connection()
|
conn = get_connection()
|
||||||
try:
|
try:
|
||||||
enc = encrypt_creds({"email": "web@test.ro", "password": "webpass"})
|
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)
|
# Submission web fara creds (creds deja purjate de primul login)
|
||||||
_insert(conn, account_id=1, creds_enc=None)
|
_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())
|
sessions = w.AccountSessions(w.get_settings())
|
||||||
assert sessions.get_token(conn, 1, None) is None # fara creds directe
|
assert sessions.get_token(conn, 1, None) is None # fara creds directe
|
||||||
|
|
||||||
# Creds din account -> login posibil
|
# Creds din account (per-env) -> login posibil
|
||||||
creds = w._creds_from_account(conn, 1)
|
creds = w._creds_from_account(conn, 1, rar_env="test")
|
||||||
assert creds is not None
|
assert creds is not None
|
||||||
token = sessions.get_token(conn, 1, creds)
|
token = sessions.get_token(conn, 1, creds)
|
||||||
assert token == "TOK-web@test.ro"
|
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.
|
"""(b) Coada mixta: dupa login web, submission-urile API (efemere) tot se trimit.
|
||||||
|
|
||||||
Scenariul:
|
Scenariul:
|
||||||
1. S1 = submission API cu creds efemere in submission.rar_creds_enc
|
1. S1 = submission API cu creds efemere in submissions.rar_creds_enc
|
||||||
2. S2 = submission WEB fara creds (foloseste accounts.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 -> OK (worker are token)
|
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)
|
4. S2 tot se poate procesa (creds din accounts per-env, US-013)
|
||||||
"""
|
"""
|
||||||
import app.worker.__main__ as w
|
import app.worker.__main__ as w
|
||||||
from app.crypto import encrypt_creds
|
from app.crypto import encrypt_creds
|
||||||
@@ -154,11 +162,13 @@ def test_coada_mixta_api_web(env, monkeypatch):
|
|||||||
|
|
||||||
conn = get_connection()
|
conn = get_connection()
|
||||||
try:
|
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"})
|
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"})
|
enc_api = encrypt_creds({"email": "api@test.ro", "password": "apipass"})
|
||||||
s1 = _insert(conn, account_id=1, creds_enc=enc_api, key_suffix="api1")
|
s1 = _insert(conn, account_id=1, creds_enc=enc_api, key_suffix="api1")
|
||||||
# S2: canal web fara creds in submission
|
# 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())
|
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())
|
creds_s1 = w._creds_for({"creds_enc": enc_api}, w.get_settings())
|
||||||
assert creds_s1 is not None
|
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()
|
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"
|
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
|
# 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)
|
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"}, \
|
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
|
# accounts.rar_creds_test_enc NU a fost atins de purjarea submissions
|
||||||
row_acc = conn.execute("SELECT rar_creds_enc FROM accounts WHERE id=1").fetchone()
|
row_acc = conn.execute("SELECT rar_creds_test_enc FROM accounts WHERE id=1").fetchone()
|
||||||
assert row_acc["rar_creds_enc"] is not None, \
|
assert row_acc["rar_creds_test_enc"] is not None, \
|
||||||
"accounts.rar_creds_enc trebuie sa ramana dupa purjare submissions"
|
"accounts.rar_creds_test_enc trebuie sa ramana dupa purjare submissions"
|
||||||
finally:
|
finally:
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
@@ -199,29 +209,37 @@ def client(env):
|
|||||||
|
|
||||||
|
|
||||||
def test_endpoint_set_rar_creds(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.crypto import decrypt_creds
|
||||||
from app.db import get_connection
|
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"})
|
r = client.post("/v1/conturi/rar-creds", json={"email": "u@test.ro", "password": "pass123"})
|
||||||
assert r.status_code == 200
|
assert r.status_code == 200
|
||||||
assert r.json()["ok"] is True
|
assert r.json()["ok"] is True
|
||||||
|
# Raspunsul include rar_env folosit
|
||||||
|
assert "rar_env" in r.json()
|
||||||
|
|
||||||
conn = get_connection()
|
conn = get_connection()
|
||||||
try:
|
try:
|
||||||
row = conn.execute("SELECT rar_creds_enc FROM accounts WHERE id=1").fetchone()
|
# Cel putin unul din sloturi trebuie sa fie populat
|
||||||
assert row["rar_creds_enc"] is not None
|
row = conn.execute(
|
||||||
creds = decrypt_creds(row["rar_creds_enc"])
|
"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"}
|
assert creds == {"email": "u@test.ro", "password": "pass123"}
|
||||||
finally:
|
finally:
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
def test_endpoint_delete_rar_creds(client, env):
|
def test_endpoint_delete_rar_creds(client, env):
|
||||||
"""DELETE /v1/conturi/rar-creds sterge creds durabile."""
|
"""DELETE /v1/conturi/rar-creds sterge creds durabile (ambele sloturi per-env, US-013)."""
|
||||||
# Mai intai seteaza
|
# Mai intai seteaza pe test si prod
|
||||||
client.post("/v1/conturi/rar-creds", json={"email": "u@test.ro", "password": "pass123"})
|
client.post("/v1/conturi/rar-creds", json={"email": "u@test.ro", "password": "pass123", "rar_target": "test"})
|
||||||
# Sterge
|
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")
|
r = client.delete("/v1/conturi/rar-creds")
|
||||||
assert r.status_code == 200
|
assert r.status_code == 200
|
||||||
assert r.json()["ok"] is True
|
assert r.json()["ok"] is True
|
||||||
@@ -229,14 +247,17 @@ def test_endpoint_delete_rar_creds(client, env):
|
|||||||
from app.db import get_connection
|
from app.db import get_connection
|
||||||
conn = get_connection()
|
conn = get_connection()
|
||||||
try:
|
try:
|
||||||
row = conn.execute("SELECT rar_creds_enc FROM accounts WHERE id=1").fetchone()
|
row = conn.execute(
|
||||||
assert row["rar_creds_enc"] is None
|
"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:
|
finally:
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
def test_gate_purjare_nu_sterge_accounts(env, monkeypatch):
|
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
|
import app.worker.__main__ as w
|
||||||
from app.crypto import encrypt_creds
|
from app.crypto import encrypt_creds
|
||||||
from app.db import get_connection
|
from app.db import get_connection
|
||||||
@@ -246,15 +267,18 @@ def test_gate_purjare_nu_sterge_accounts(env, monkeypatch):
|
|||||||
conn = get_connection()
|
conn = get_connection()
|
||||||
try:
|
try:
|
||||||
enc = encrypt_creds({"email": "u@test.ro", "password": "p"})
|
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)
|
_insert(conn, account_id=1, creds_enc=enc)
|
||||||
|
|
||||||
sessions = w.AccountSessions(w.get_settings())
|
sessions = w.AccountSessions(w.get_settings())
|
||||||
sessions.get_token(conn, 1, {"email": "u@test.ro", "password": "p"})
|
sessions.get_token(conn, 1, {"email": "u@test.ro", "password": "p"})
|
||||||
|
|
||||||
# accounts.rar_creds_enc trebuie sa fie intact
|
# accounts.rar_creds_test_enc NU trebuie atins de purjarea submissions
|
||||||
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()
|
||||||
assert row["rar_creds_enc"] is not None, \
|
assert row["rar_creds_test_enc"] is not None, \
|
||||||
"gate purjare: accounts.rar_creds_enc trebuie sa ramana intact"
|
"gate purjare: accounts.rar_creds_test_enc trebuie sa ramana intact"
|
||||||
finally:
|
finally:
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|||||||
@@ -131,7 +131,7 @@ def test_roteste_cheie_afisata_o_data(client):
|
|||||||
# ============================================================
|
# ============================================================
|
||||||
|
|
||||||
def test_set_creds_rar_din_sesiune(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")
|
acct_id, user_id, _ = _create_account_user("creds@test.com")
|
||||||
_login(client, "creds@test.com", "parolasecreta10")
|
_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(), \
|
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]}"
|
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.db import get_connection
|
||||||
from app.crypto import decrypt_creds
|
from app.crypto import decrypt_creds
|
||||||
conn = get_connection()
|
conn = get_connection()
|
||||||
try:
|
try:
|
||||||
row = conn.execute(
|
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()
|
).fetchone()
|
||||||
assert row is not None
|
assert row is not None
|
||||||
assert row["rar_creds_enc"] is not None, "rar_creds_enc trebuia setat"
|
enc = row["rar_creds_test_enc"] or row["rar_creds_prod_enc"]
|
||||||
creds = decrypt_creds(row["rar_creds_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 is not None
|
||||||
assert creds.get("email") == "user@rar.ro"
|
assert creds.get("email") == "user@rar.ro"
|
||||||
assert creds.get("password") == "parolaRAR123"
|
assert creds.get("password") == "parolaRAR123"
|
||||||
@@ -170,7 +171,7 @@ def test_set_creds_rar_din_sesiune(client):
|
|||||||
# ============================================================
|
# ============================================================
|
||||||
|
|
||||||
def test_creds_alt_cont_neafectat(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_a, user_a, _ = _create_account_user("userA@test.com")
|
||||||
acct_b, user_b, _ = _create_account_user("userB@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
|
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
|
from app.db import get_connection
|
||||||
conn = get_connection()
|
conn = get_connection()
|
||||||
try:
|
try:
|
||||||
row_a = conn.execute("SELECT rar_creds_enc FROM accounts WHERE id=?", (acct_a,)).fetchone()
|
row_a = conn.execute(
|
||||||
row_b = conn.execute("SELECT rar_creds_enc FROM accounts WHERE id=?", (acct_b,)).fetchone()
|
"SELECT rar_creds_test_enc, rar_creds_prod_enc FROM accounts WHERE id=?", (acct_a,)
|
||||||
assert row_a["rar_creds_enc"] is not None, "Contul A trebuia sa aiba creds"
|
).fetchone()
|
||||||
assert row_b["rar_creds_enc"] is None, "Contul B nu trebuia atins"
|
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:
|
finally:
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ def _login(client, email: str, password: str = "parolasecreta10") -> None:
|
|||||||
|
|
||||||
|
|
||||||
def _set_rar_creds(acct_id: int) -> 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.db import get_connection
|
||||||
from app.crypto import encrypt_creds
|
from app.crypto import encrypt_creds
|
||||||
|
|
||||||
@@ -66,9 +66,10 @@ def _set_rar_creds(acct_id: int) -> None:
|
|||||||
try:
|
try:
|
||||||
enc = encrypt_creds({"email": "test@rar.ro", "password": "parola_rar"})
|
enc = encrypt_creds({"email": "test@rar.ro", "password": "parola_rar"})
|
||||||
conn.execute(
|
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),
|
(enc, acct_id),
|
||||||
)
|
)
|
||||||
|
conn.commit()
|
||||||
finally:
|
finally:
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
|
|||||||
@@ -67,11 +67,18 @@ def _set_last_login(conn, *, ago_s: float | None):
|
|||||||
|
|
||||||
|
|
||||||
def _account_cu_creds(conn) -> int:
|
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.accounts import create_account
|
||||||
from app.crypto import encrypt_creds
|
from app.crypto import encrypt_creds
|
||||||
acct = create_account(conn, "Service Cu Creds", email="svc@example.com")
|
acct = create_account(conn, "Service Cu Creds", email="svc@example.com")
|
||||||
enc = encrypt_creds({"email": "svc@example.com", "password": "secret"})
|
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()
|
conn.commit()
|
||||||
return acct
|
return acct
|
||||||
|
|
||||||
@@ -155,11 +162,14 @@ def test_target_sare_creds_nedecriptabile(env):
|
|||||||
settings.worker_use_test_creds = False
|
settings.worker_use_test_creds = False
|
||||||
# Cont cu creds GUNOI (nedecriptabile sub cheia curenta), id mai mic.
|
# Cont cu creds GUNOI (nedecriptabile sub cheia curenta), id mai mic.
|
||||||
bad = create_account(conn, "Cont Cheie Veche", email="old@example.com")
|
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.
|
# Cont cu creds valide, id mai mare.
|
||||||
good = create_account(conn, "Cont Valid", email="good@example.com")
|
good = create_account(conn, "Cont Valid", email="good@example.com")
|
||||||
enc = encrypt_creds({"email": "good@example.com", "password": "pw"})
|
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()
|
conn.commit()
|
||||||
|
|
||||||
acct_id, creds = _keepalive_target(conn, settings)
|
acct_id, creds = _keepalive_target(conn, settings)
|
||||||
|
|||||||
Reference in New Issue
Block a user