"""Teste US-001 (PRD 3.3): tabela users + helper-e parole scrypt in app/users.py.""" from __future__ import annotations import os import sqlite3 import tempfile import pytest @pytest.fixture() def conn(monkeypatch): tmp = tempfile.mkdtemp() monkeypatch.setenv("AUTOPASS_DB_PATH", os.path.join(tmp, "test_users.db")) from app.config import get_settings get_settings.cache_clear() from app.db import get_connection, init_db init_db() c = get_connection() yield c c.close() get_settings.cache_clear() @pytest.fixture() def account_id(conn): """Cont de test (nu default id=1).""" from app.accounts import create_account return create_account(conn, "Service Test") def test_create_user_hash_nu_e_plaintext(conn, account_id): """password_hash din DB nu contine parola in clar si nu e egal cu ea.""" from app.users import create_user parola = "parola_sigura_123" user_id = create_user(conn, account_id, "test@exemplu.ro", parola) assert isinstance(user_id, int) row = conn.execute( "SELECT password_hash, salt FROM users WHERE id=?", (user_id,) ).fetchone() assert row is not None assert row["password_hash"] != parola assert parola not in row["password_hash"] assert row["salt"] != parola def test_verify_parola_corecta_si_gresita(conn, account_id): """verify_password intoarce account_id la parola corecta, None la cea gresita.""" from app.users import create_user, verify_password create_user(conn, account_id, "user@exemplu.ro", "parola_corecta_99") result_ok = verify_password(conn, "user@exemplu.ro", "parola_corecta_99") assert result_ok == account_id result_gresit = verify_password(conn, "user@exemplu.ro", "parola_gresita_00") assert result_gresit is None result_inexistent = verify_password(conn, "inexistent@exemplu.ro", "parola_corecta_99") assert result_inexistent is None def test_email_unic_global(conn, account_id): """Al doilea create_user cu acelasi email (diferit doar in case) ridica ValueError.""" from app.users import create_user create_user(conn, account_id, "Unic@exemplu.ro", "parola_unica_001") with pytest.raises(ValueError, match="email deja folosit"): create_user(conn, account_id, "unic@exemplu.ro", "alta_parola_002") def test_get_user_by_email(conn, account_id): """get_user_by_email intoarce metadate fara password_hash si salt.""" from app.users import create_user, get_user_by_email create_user(conn, account_id, "meta@exemplu.ro", "parola_meta_xyz") user = get_user_by_email(conn, "meta@exemplu.ro") assert user is not None assert user["email"].lower() == "meta@exemplu.ro" assert user["account_id"] == account_id assert "id" in user assert "is_admin" in user assert "email_verified" in user assert "created_at" in user assert "password_hash" not in user assert "salt" not in user assert get_user_by_email(conn, "inexistent@exemplu.ro") is None def test_parola_scurta_si_lunga_eroare(conn, account_id): """Parola < 10 caractere sau > 128 ridica ValueError (C9 anti-DoS).""" from app.users import create_user with pytest.raises(ValueError): create_user(conn, account_id, "scurta@ex.ro", "scurt") with pytest.raises(ValueError): create_user(conn, account_id, "lunga@ex.ro", "x" * 129) # exact 10 caractere — trebuie sa mearga uid = create_user(conn, account_id, "exact10@ex.ro", "a" * 10) assert uid > 0 # exact 128 caractere — trebuie sa mearga uid2 = create_user(conn, account_id, "exact128@ex.ro", "b" * 128) assert uid2 > 0 def test_verify_honoreaza_scrypt_params(conn, account_id, monkeypatch): """verify_password foloseste parametrii din DB (scrypt_params), nu constantele globale. Simuleaza migrare cost: hash creat cu n=4 (vechi), constanta _N ridicata la 2**15 (nou). verify_password trebuie sa returneze account_id folosind n=4 din DB, nu _N global. """ import hashlib import secrets as _secrets import app.users as users_mod email = "legacy@test.com" password = "parolasecreta" # Hash cu parametri "vechi" (n=4, rapid pentru teste) n_old, r_old, p_old = 4, 8, 1 salt = _secrets.token_bytes(16) pw_hash = hashlib.scrypt( password.encode("utf-8"), salt=salt, n=n_old, r=r_old, p=p_old, maxmem=64 * 1024 * 1024, dklen=32, ) conn.execute( "INSERT INTO users (account_id, email, password_hash, salt, scrypt_params) " "VALUES (?, ?, ?, ?, ?)", (account_id, email, pw_hash.hex(), salt.hex(), "n4_r8_p1"), ) # Simuleaza cresterea costului: _N e acum mai mare monkeypatch.setattr(users_mod, "_N", 2**15) # verify_password trebuie sa onoreze n=4 din DB, nu sa foloseasca _N=2**15 result = users_mod.verify_password(conn, email, password) assert result == account_id, "verify_password trebuia sa onoreze scrypt_params din DB" assert users_mod.verify_password(conn, email, "gresita123456") is None def test_verify_params_corupti_return_none(conn, account_id): """scrypt_params corupt/necunoscut -> verify returneaza None (no crash).""" import hashlib import secrets as _secrets email = "corupt@test.com" password = "parolasecreta" salt = _secrets.token_bytes(16) pw_hash = hashlib.scrypt(password.encode(), salt=salt, n=4, r=8, p=1, maxmem=64 * 1024 * 1024, dklen=32) conn.execute( "INSERT INTO users (account_id, email, password_hash, salt, scrypt_params) " "VALUES (?, ?, ?, ?, ?)", (account_id, email, pw_hash.hex(), salt.hex(), "FORMAT_NECUNOSCUT"), ) from app.users import verify_password result = verify_password(conn, email, password) assert result is None, "Eticheta corupta trebuia sa returneze None, nu crash" def test_init_db_pe_db_fara_users_creeaza_tabela(monkeypatch, tmp_path): """init_db pe o DB existenta fara tabela users o creeaza fara eroare (migrare idempotenta).""" db_path = tmp_path / "veche.db" monkeypatch.setenv("AUTOPASS_DB_PATH", str(db_path)) from app.config import get_settings get_settings.cache_clear() # Creeaza DB fara tabela users (simuleaza DB veche) import sqlite3 as _sq conn_raw = _sq.connect(str(db_path)) conn_raw.execute("PRAGMA journal_mode = WAL") conn_raw.execute("PRAGMA foreign_keys = ON") conn_raw.execute( "CREATE TABLE IF NOT EXISTS accounts " "(id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, " "cui TEXT, active INTEGER NOT NULL DEFAULT 1, " "rar_creds_enc TEXT, created_at TEXT NOT NULL DEFAULT (datetime('now')))" ) conn_raw.execute("INSERT OR IGNORE INTO accounts (id, name) VALUES (1, 'default')") conn_raw.commit() conn_raw.close() # init_db trebuie sa creeze tabela users fara eroare from app.db import init_db init_db() from app.db import get_connection c = get_connection() tables = {r[0] for r in c.execute( "SELECT name FROM sqlite_master WHERE type='table'" ).fetchall()} c.close() assert "users" in tables get_settings.cache_clear()