PRD 5.16 — propagare design finalizata (system font stack, fara IBM Plex self-hostat): - US-001/002/008: tokeni --font-ui/--font-mono (system stack) + scala --fs-*; zero @font-face si zero /static/fonts/; landing aliniat la acelasi stack - US-003: RAR online = dot compact in antet + meniu burger; banda rosie DOAR pe blocat (invariant zero-silent-failures pastrat) - US-010: antet "ROMFAST AUTOPASS" + nume service + /login brandeit 2 coloane + badge plan; meniu burger cu separatoare; gate strict pe is_authenticated - US-011: selector tema pill icon+eticheta (reuse THEMES) - US-004/005/006/007: bug-fix editor prestatii (picker cod+denumire, add_extra in mod operatii, cod ales se salveaza fara "+", Renunta inchide via closest) - US-012/013: landing Autentificare->/login; wizard import colapsat + 4 pasi pe tokeni - fix VERIFY E2E: contoare duplicate pe 390px (inline display:flex batea @media) -> CSS + test-lock PRD 5.17 — tipuri de cont + trial Pro 30z + enforcement DUR: - US-001/002/008: accounts.tier + trial_until (migrare aditiva defensiva); app/plans.py sursa unica (PLANS, FREE_MONTHLY_LIMIT=60, effective_tier(now injectabil), monthly_usage, CONSUMED_STATUSES); create_account trial Pro 30z; CLI set-tier (protejat id=1, audit) - US-003/004/005: enforce volum 60/luna INAINTE de build_key pe ambele canale (PLAN_LIMITA_LUNARA, 3 niveluri + log_event); gate API Pro+ (PLAN_FARA_API 403 actionabil); valideaza/nomenclator raman permise; downgrade lazy; flag AUTOPASS_ENFORCE_PLANS (kill-switch) - US-006: badge plan antet + linie burger + consum N/60 + warn>=80% + 6 stari + copy RO pluralizat + banner one-time trial->Gratuit + pagina Cont Regresie: 1380 passed, 0 failed, 1 deselected (live). E2E browser pe 390/1280 confirmat. Backend trimitere (worker/masina stari/idempotenta/contract RAR) NEATINS. Lucrul 5.18 (corpus kNN) ramane separat, necomis. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
360 lines
13 KiB
Python
360 lines
13 KiB
Python
"""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
|