"""Teste US-001/US-002 (PRD 5.17): app/plans.py — definitia planurilor + helperi tier/consum.""" from __future__ import annotations import os import tempfile from datetime import datetime, timedelta, timezone import pytest @pytest.fixture() def conn(monkeypatch): tmp = tempfile.mkdtemp() monkeypatch.setenv("AUTOPASS_DB_PATH", os.path.join(tmp, "test_plans.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() # --------------------------------------------------------------------------- # PLANS — sursa de adevar # --------------------------------------------------------------------------- def test_plan_definitii_free(): from app.plans import PLANS, FREE_MONTHLY_LIMIT p = PLANS["free"] assert p["monthly_limit"] == FREE_MONTHLY_LIMIT assert p["monthly_limit"] == 60 assert p["api_access"] is False assert p["label"] == "Gratuit" def test_plan_definitii_standard(): from app.plans import PLANS p = PLANS["standard"] assert p["monthly_limit"] is None assert p["api_access"] is False assert "label" in p def test_plan_definitii_pro(): from app.plans import PLANS p = PLANS["pro"] assert p["monthly_limit"] is None assert p["api_access"] is True assert "label" in p def test_plan_definitii_premium(): from app.plans import PLANS p = PLANS["premium"] assert p["monthly_limit"] is None assert p["api_access"] is True assert "label" in p def test_toate_tierurile_prezente(): from app.plans import PLANS assert set(PLANS.keys()) == {"free", "standard", "pro", "premium"} def test_consumed_statuses_exportata(): from app.plans import CONSUMED_STATUSES assert "queued" in CONSUMED_STATUSES assert "sending" in CONSUMED_STATUSES assert "sent" in CONSUMED_STATUSES # statusuri blocate nu se numara assert "error" not in CONSUMED_STATUSES assert "needs_mapping" not in CONSUMED_STATUSES assert "needs_data" not in CONSUMED_STATUSES def test_free_monthly_limit_constanta(): """FREE_MONTHLY_LIMIT e o singura constanta (DRY), referita din PLANS.""" from app.plans import FREE_MONTHLY_LIMIT, PLANS assert isinstance(FREE_MONTHLY_LIMIT, int) assert FREE_MONTHLY_LIMIT == 60 # PLANS["free"]["monthly_limit"] refera aceeasi valoare (nu hardcodat separat) assert PLANS["free"]["monthly_limit"] == FREE_MONTHLY_LIMIT # --------------------------------------------------------------------------- # effective_tier # --------------------------------------------------------------------------- def _now_utc(): return datetime.now(timezone.utc) def test_effective_tier_trial_activ_returneaza_pro(): from app.plans import effective_tier now = _now_utc() trial_until = (now + timedelta(days=15)).strftime("%Y-%m-%d %H:%M:%S") account = {"tier": "free", "trial_until": trial_until} assert effective_tier(account, now) == "pro" def test_effective_tier_trial_expirat_returneaza_tier_baza(): from app.plans import effective_tier now = _now_utc() trial_until = (now - timedelta(days=1)).strftime("%Y-%m-%d %H:%M:%S") account = {"tier": "free", "trial_until": trial_until} assert effective_tier(account, now) == "free" def test_effective_tier_fara_trial_returneaza_tier(): from app.plans import effective_tier now = _now_utc() account = {"tier": "standard", "trial_until": None} assert effective_tier(account, now) == "standard" def test_effective_tier_plan_platit_nu_downgradat_de_trial_expirat(): """Un cont pro setat de admin NU e downgradat de expirarea trial-ului.""" from app.plans import effective_tier now = _now_utc() # tier=pro, trial_until in trecut: downgrade nu se produce (pro > free) trial_until = (now - timedelta(days=5)).strftime("%Y-%m-%d %H:%M:%S") account = {"tier": "pro", "trial_until": trial_until} # tier de baza e pro, deci effective = pro (nu se coboara la free) assert effective_tier(account, now) == "pro" def test_effective_tier_trial_malformat_fallback_defensiv(): from app.plans import effective_tier now = _now_utc() account = {"tier": "free", "trial_until": "nu-e-o-data-valida"} # malformat -> fallback la tier de baza, fara exceptie assert effective_tier(account, now) == "free" def test_effective_tier_trial_null_fallback(): from app.plans import effective_tier now = _now_utc() account = {"tier": "free", "trial_until": None} assert effective_tier(account, now) == "free" def test_effective_tier_injectat_determinist(): """now injectabil: putem simula orice moment — teste deterministe fara datetime.now().""" from app.plans import effective_tier # trial_until fix trial_until = "2026-07-10 12:00:00" account = {"tier": "free", "trial_until": trial_until} # inainte de expirare now_before = datetime(2026, 7, 5, 12, 0, 0, tzinfo=timezone.utc) assert effective_tier(account, now_before) == "pro" # dupa expirare now_after = datetime(2026, 7, 15, 12, 0, 0, tzinfo=timezone.utc) assert effective_tier(account, now_after) == "free" def test_effective_tier_premium_cu_trial_pro(): """premium are api_access=True oricum; trial_until viitor nu strica.""" from app.plans import effective_tier now = _now_utc() trial_until = (now + timedelta(days=10)).strftime("%Y-%m-%d %H:%M:%S") account = {"tier": "premium", "trial_until": trial_until} # trial activ -> 'pro', dar premium e oricum superior (nu ne intereseaza downgrade) # functia intoarce 'pro' cand trial e activ; consumatorul vede pro (care are api_access) assert effective_tier(account, now) == "pro" # --------------------------------------------------------------------------- # monthly_usage # --------------------------------------------------------------------------- def _uid(): """Cheie idempotenta unica per apel (pentru INSERT in teste).""" import binascii return binascii.hexlify(os.urandom(8)).decode() def _insert_submission(conn, account_id, status, created_at_str): """Insereaza o submisie de test cu timestamp explicit.""" conn.execute( "INSERT INTO submissions (idempotency_key, account_id, status, payload_json, created_at) " "VALUES (?, ?, ?, '{}', ?)", (_uid(), account_id, status, created_at_str), ) def test_consum_lunar_numara_consumed_statuses(conn): from app.plans import monthly_usage from app.accounts import create_account now = _now_utc() now_str = now.strftime("%Y-%m-%d %H:%M:%S") account_id = create_account(conn, "Test Consum", cui="RO1001") # 3 statusuri consumate _insert_submission(conn, account_id, "queued", now_str) _insert_submission(conn, account_id, "sending", now_str) _insert_submission(conn, account_id, "sent", now_str) assert monthly_usage(conn, account_id, now) == 3 def test_consum_lunar_exclude_statusuri_blocate(conn): from app.plans import monthly_usage from app.accounts import create_account now = _now_utc() now_str = now.strftime("%Y-%m-%d %H:%M:%S") account_id = create_account(conn, "Test Blocat", cui="RO1002") # statusuri care NU se numara for status in ("error", "needs_mapping", "needs_data"): _insert_submission(conn, account_id, status, now_str) assert monthly_usage(conn, account_id, now) == 0 def test_consum_lunar_scoped_pe_cont(conn): from app.plans import monthly_usage from app.accounts import create_account now = _now_utc() now_str = now.strftime("%Y-%m-%d %H:%M:%S") acct_a = create_account(conn, "Cont A", cui="RO1003") acct_b = create_account(conn, "Cont B", cui="RO1004") _insert_submission(conn, acct_a, "sent", now_str) _insert_submission(conn, acct_a, "sent", now_str) _insert_submission(conn, acct_b, "sent", now_str) assert monthly_usage(conn, acct_a, now) == 2 assert monthly_usage(conn, acct_b, now) == 1 def test_consum_lunar_luna_trecuta_nu_se_numara(conn): from app.plans import monthly_usage from app.accounts import create_account now = _now_utc() account_id = create_account(conn, "Test Luna Trecuta", cui="RO1005") # Calculam o data din luna trecuta (prima zi a lunii curente - 1 zi) first_of_month = now.replace(day=1, hour=0, minute=0, second=0, microsecond=0) last_of_prev_month = first_of_month - timedelta(days=1) prev_str = last_of_prev_month.strftime("%Y-%m-%d %H:%M:%S") _insert_submission(conn, account_id, "sent", prev_str) # luna curenta: 0 assert monthly_usage(conn, account_id, now) == 0 def test_consum_lunar_granita_luna_noua(conn): """Submisii la granita intre luni sunt bucketate corect (timp local RO = UTC in container).""" from app.plans import monthly_usage from app.accounts import create_account now = _now_utc() now_str = now.strftime("%Y-%m-%d %H:%M:%S") account_id = create_account(conn, "Test Granita", cui="RO1006") # Prima secunda a lunii curente (calculata consistent cu 'localtime' = UTC in container) first_of_month = now.replace(day=1, hour=0, minute=0, second=1, microsecond=0) first_str = first_of_month.strftime("%Y-%m-%d %H:%M:%S") # Ultima secunda a lunii trecute last_of_prev_month = first_of_month - timedelta(seconds=2) prev_str = last_of_prev_month.strftime("%Y-%m-%d %H:%M:%S") _insert_submission(conn, account_id, "sent", first_str) # luna curenta _insert_submission(conn, account_id, "sent", prev_str) # luna trecuta _insert_submission(conn, account_id, "sent", now_str) # luna curenta assert monthly_usage(conn, account_id, now) == 2 def test_consum_lunar_zero_pe_cont_gol(conn): from app.plans import monthly_usage from app.accounts import create_account now = _now_utc() account_id = create_account(conn, "Cont Gol", cui="RO1007") assert monthly_usage(conn, account_id, now) == 0 def test_consum_lunar_nu_numara_cross_account(conn): """Verificare scoping: contul default (id=1) nu influenteaza alt cont.""" from app.plans import monthly_usage from app.accounts import create_account now = _now_utc() now_str = now.strftime("%Y-%m-%d %H:%M:%S") account_id = create_account(conn, "Cont Izolat", cui="RO1008") # Inseram pentru contul default (id=1) _insert_submission(conn, 1, "sent", now_str) _insert_submission(conn, 1, "sent", now_str) # Contul nou nu trebuie sa numere al celor de pe id=1 assert monthly_usage(conn, account_id, now) == 0 assert monthly_usage(conn, 1, now) == 2 # --------------------------------------------------------------------------- # PRD 5.17 enforcement — logica de limita + kill-switch config # --------------------------------------------------------------------------- def test_volume_la_limita_exacta(conn): """La exact FREE_MONTHLY_LIMIT submissions, usage == limita (nu inca depasit). Enforcer-ul verifica usage + nr_cerut > limit, deci la usage=60, nr_cerut=1 -> 61 > 60 -> respins; dar usage=60 in sine (inainte de cerere) e valid. """ from app.plans import monthly_usage, FREE_MONTHLY_LIMIT from app.accounts import create_account now = _now_utc() now_str = now.strftime("%Y-%m-%d %H:%M:%S") account_id = create_account(conn, "Test La Limita", cui="RO2001") for _ in range(FREE_MONTHLY_LIMIT): _insert_submission(conn, account_id, "queued", now_str) conn.commit() usage = monthly_usage(conn, account_id, now) assert usage == FREE_MONTHLY_LIMIT, ( f"La limita exacta: asteptat {FREE_MONTHLY_LIMIT}, primit {usage}" ) # Simulam logica enforcer: 1 cerere noua depaseste limita assert usage + 1 > FREE_MONTHLY_LIMIT, "O cerere noua trebuia sa depaseasca limita" # La 0 cereri noi: nu depaseste assert usage + 0 <= FREE_MONTHLY_LIMIT, "La 0 cereri noi, limita nu e depasita" def test_enforce_plans_config_default_true(monkeypatch): """AUTOPASS_ENFORCE_PLANS implicit True — enforcement activ de la deploy. Decizie user (autoplan 2026-06-28): nu exista conturi legacy, produs in TESTE, enforcement DUR activ implicit. Kill-switch oprit explicit cand e necesar. """ from app.config import Settings # Creem Settings fresh (fara env var setata) -> default True monkeypatch.delenv("AUTOPASS_ENFORCE_PLANS", raising=False) s = Settings() assert s.enforce_plans is True, ( "AUTOPASS_ENFORCE_PLANS trebuia sa fie True implicit (enforcement activ din start)" ) def test_enforce_plans_kill_switch_false(monkeypatch): """AUTOPASS_ENFORCE_PLANS=false dezactiveaza enforcement.""" from app.config import Settings monkeypatch.setenv("AUTOPASS_ENFORCE_PLANS", "false") s = Settings() assert s.enforce_plans is False