feat(account): CLI lifecycle conturi + accounts.active (PRD 3.1)

Inlocuieste crearea conturilor prin INSERT SQL manual cu un tool admin
dedicat, simetric cu tools/apikey.py. Fundatia Etapei 3 (3.2/3.3).

- app/accounts.py: create_account/set_active/list_accounts (helper pur,
  partajat CLI + viitor flux web 3.3). Normalizeaza CUI (trim+upper),
  prinde IntegrityError -> ValueError cu cauza+fix.
- accounts.active (lifecycle cont) + index unic partial ux_accounts_cui
  (unicitate la nivel de index, fara fereastra de coliziune). Migrare
  idempotenta in _migrate.
- tools/account.py: create (--name/--cui/--inactive/--with-key atomic),
  list [--pending], activate/deactivate --account N. Erori -> exit 2.
- 20 teste noi (12 helper + 8 CLI); suita 299 passed.

active e inert pana la gate-ul worker din 3.3 (documentat).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Claude Agent
2026-06-17 12:38:13 +00:00
parent 6515de415b
commit 1c5b0cbc18
8 changed files with 475 additions and 5 deletions

115
tests/test_accounts.py Normal file
View File

@@ -0,0 +1,115 @@
"""Teste US-001 (PRD 3.1): coloana accounts.active + helper-e cont in app/accounts.py."""
from __future__ import annotations
import os
import tempfile
import pytest
@pytest.fixture()
def conn(monkeypatch):
tmp = tempfile.mkdtemp()
monkeypatch.setenv("AUTOPASS_DB_PATH", os.path.join(tmp, "test_accounts.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()
def test_create_account_returneaza_id(conn):
from app.accounts import create_account
acct_id = create_account(conn, "Service X")
assert isinstance(acct_id, int)
# AUTOINCREMENT peste default id=1 -> primul cont creat are id>=2 (nu atinge default).
assert acct_id >= 2
def test_create_account_activ_implicit(conn):
from app.accounts import create_account
acct_id = create_account(conn, "Service X")
row = conn.execute("SELECT active FROM accounts WHERE id=?", (acct_id,)).fetchone()
assert row["active"] == 1
def test_create_account_inactiv(conn):
from app.accounts import create_account
acct_id = create_account(conn, "Service X", active=False)
row = conn.execute("SELECT active FROM accounts WHERE id=?", (acct_id,)).fetchone()
assert row["active"] == 0
def test_create_account_name_gol_ridica_eroare(conn):
from app.accounts import create_account
with pytest.raises(ValueError):
create_account(conn, " ")
# nu a inserat nimic peste default
n = conn.execute("SELECT COUNT(*) AS n FROM accounts").fetchone()["n"]
assert n == 1
def test_create_account_cui_duplicat_respins(conn):
from app.accounts import create_account
first = create_account(conn, "Service A", cui="RO123")
with pytest.raises(ValueError) as exc:
create_account(conn, "Service B", cui="RO123")
# mesaj cu cauza + fix care numeste contul existent (A4)
msg = str(exc.value)
assert "RO123" in msg
assert str(first) in msg
def test_create_cui_null_multiplu_permis(conn):
from app.accounts import create_account
a = create_account(conn, "Fara CUI 1")
b = create_account(conn, "Fara CUI 2")
assert a != b
def test_create_cui_normalizat(conn):
from app.accounts import create_account
create_account(conn, "Service A", cui=" ro123 ")
# normalizat la RO123 -> duplicat respins indiferent de spatii/caz
with pytest.raises(ValueError):
create_account(conn, "Service B", cui="RO123")
row = conn.execute("SELECT cui FROM accounts WHERE name='Service A'").fetchone()
assert row["cui"] == "RO123"
def test_set_active_comuta(conn):
from app.accounts import create_account, set_active
acct_id = create_account(conn, "Service X")
set_active(conn, acct_id, False)
assert conn.execute("SELECT active FROM accounts WHERE id=?", (acct_id,)).fetchone()["active"] == 0
set_active(conn, acct_id, True)
assert conn.execute("SELECT active FROM accounts WHERE id=?", (acct_id,)).fetchone()["active"] == 1
def test_set_active_idempotent(conn):
from app.accounts import create_account, set_active
acct_id = create_account(conn, "Service X") # deja activ
set_active(conn, acct_id, True) # nu trebuie sa arunce
assert conn.execute("SELECT active FROM accounts WHERE id=?", (acct_id,)).fetchone()["active"] == 1
def test_set_active_inexistent_ridica(conn):
from app.accounts import set_active
with pytest.raises(ValueError):
set_active(conn, 9999, True)
def test_list_accounts_ordonat_fara_creds(conn):
from app.accounts import create_account, list_accounts
create_account(conn, "Service B")
create_account(conn, "Service A")
rows = list_accounts(conn)
ids = [r["id"] for r in rows]
assert ids == sorted(ids)
for r in rows:
assert "rar_creds_enc" not in r
assert set(r.keys()) == {"id", "name", "cui", "active", "created_at"}

124
tests/test_tools_account.py Normal file
View File

@@ -0,0 +1,124 @@
"""Teste US-002 (PRD 3.1): CLI tools/account.py (create/list/activate/deactivate)."""
from __future__ import annotations
import os
import tempfile
import pytest
@pytest.fixture()
def env(monkeypatch):
tmp = tempfile.mkdtemp()
monkeypatch.setenv("AUTOPASS_DB_PATH", os.path.join(tmp, "test_tools_account.db"))
from app.config import get_settings
get_settings.cache_clear()
yield
get_settings.cache_clear()
def _run(argv):
from tools.account import main
return main(argv)
def test_create_afiseaza_id(env, capsys):
rc = _run(["create", "--name", "Service X"])
out = capsys.readouterr().out
assert rc == 0
assert "id=2" in out
assert "activ=da" in out
def test_create_inactive_in_asteptare(env, capsys):
rc = _run(["create", "--name", "Service X", "--inactive"])
out = capsys.readouterr().out
assert rc == 0
assert "activ=nu" in out
def test_create_with_key_emite_cheie(env, capsys):
rc = _run(["create", "--name", "Service X", "--with-key"])
out = capsys.readouterr().out
assert rc == 0
assert "rfak_" in out
# cheia exista in DB pentru contul nou
from app.db import get_connection
conn = get_connection()
try:
n = conn.execute(
"SELECT COUNT(*) AS n FROM api_keys k JOIN accounts a ON a.id=k.account_id "
"WHERE a.name='Service X'"
).fetchone()["n"]
finally:
conn.close()
assert n == 1
def test_create_cui_duplicat_exit_2(env, capsys):
assert _run(["create", "--name", "Service A", "--cui", "RO123"]) == 0
rc = _run(["create", "--name", "Service B", "--cui", "RO123"])
err = capsys.readouterr().err
assert rc == 2
assert "RO123" in err
def test_with_key_atomic_pe_cui_duplicat(env, capsys):
# cont initial care ocupa CUI
assert _run(["create", "--name", "Service A", "--cui", "RO123"]) == 0
capsys.readouterr()
# --with-key pe CUI duplicat: rollback -> niciun cont B, nicio cheie orfana
rc = _run(["create", "--name", "Service B", "--cui", "RO123", "--with-key"])
assert rc == 2
from app.db import get_connection
conn = get_connection()
try:
assert conn.execute("SELECT COUNT(*) AS n FROM accounts WHERE name='Service B'").fetchone()["n"] == 0
# o singura cheie n-a fost emisa (doar contul A nu are cheie)
assert conn.execute("SELECT COUNT(*) AS n FROM api_keys").fetchone()["n"] == 0
finally:
conn.close()
def test_activate_comuta_starea(env, capsys):
_run(["create", "--name", "Service X", "--inactive"])
capsys.readouterr()
assert _run(["deactivate", "--account", "2"]) == 0
assert _run(["activate", "--account", "2"]) == 0
capsys.readouterr()
rc = _run(["list"])
out = capsys.readouterr().out
assert rc == 0
assert "Service X" in out
# contul 2 apare activ
line = [ln for ln in out.splitlines() if "Service X" in ln][0]
assert "da" in line
def test_activate_inexistent_exit_2(env, capsys):
rc = _run(["activate", "--account", "9999"])
err = capsys.readouterr().err
assert rc == 2
assert "inexistent" in err
def test_list_afiseaza_activ(env, capsys):
_run(["create", "--name", "Service X"])
capsys.readouterr()
rc = _run(["list"])
out = capsys.readouterr().out
assert rc == 0
assert "Service X" in out
assert "activ" in out # antet tabel
def test_list_pending_filtreaza(env, capsys):
_run(["create", "--name", "Activ SRL"])
_run(["create", "--name", "Asteptare SRL", "--inactive"])
capsys.readouterr()
rc = _run(["list", "--pending"])
out = capsys.readouterr().out
assert rc == 0
assert "Asteptare SRL" in out
assert "Activ SRL" not in out