"""Teste strip sanatate mereu-vizibil + carduri-contor pe dashboard. Strip sanatate: linie colorata DEASUPRA contoarelor — verde "declaratiile curg" / rosu "Blocat: worker oprit / RAR inaccesibil", cu glifa accesibila (✓/✗). Contoare: In coada / Trimise (all-time + luna/azi) / De corectat. Timezone: azi/luna bucketate in timp local RO, nu UTC. """ from __future__ import annotations import json import os import re import tempfile from datetime import datetime, timedelta, timezone import pytest from starlette.testclient import TestClient def _create_account_user(email: str, password: str = "parolasecreta10"): 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, "Service Test Bife", active=True) user_id = create_user(conn, acct_id, email, password) return acct_id, user_id finally: conn.close() def _login(client, email: str, password: str) -> None: 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" resp = client.post("/login", data={"email": email, "parola": password, "csrf_token": m.group(1)}) assert resp.status_code == 303, f"Login esuat: {resp.status_code} {resp.text[:200]}" def _set_heartbeat(last_beat: str | None, last_rar_login_ok: str | None) -> None: from app.db import get_connection conn = get_connection() try: conn.execute( "UPDATE worker_heartbeat SET last_beat=?, last_rar_login_ok=? WHERE id=1", (last_beat, last_rar_login_ok), ) conn.commit() finally: conn.close() @pytest.fixture() def client(monkeypatch): tmp = tempfile.mkdtemp() monkeypatch.setenv("AUTOPASS_DB_PATH", os.path.join(tmp, "bife_test.db")) monkeypatch.setenv("AUTOPASS_WEB_AUTH_REQUIRED", "true") from app.config import get_settings get_settings.cache_clear() from app.web import ratelimit ratelimit._hits.clear() from app.main import app with TestClient(app, follow_redirects=False) as c: yield c ratelimit._hits.clear() get_settings.cache_clear() # --------------------------------------------------------------------------- # Strip unificat de sanatate (in loc de bife individuale) # --------------------------------------------------------------------------- def test_status_are_bife_verzi_cand_totul_ok(client): """Worker viu + RAR login recent -> strip-sanatate in DOM dar ASCUNS (hidden). Banda rosie apare DOAR cand BLOCAT. Starea OK e indicata de dot-ul verde din antet (base.html). Elementul id=strip-sanatate ramane in DOM pentru compatibilitate (nu dispare complet). """ _create_account_user("bifeok@test.com") _login(client, "bifeok@test.com", "parolasecreta10") now = datetime.now(timezone.utc).isoformat() _set_heartbeat(last_beat=now, last_rar_login_ok=now) resp = client.get("/_fragments/status") assert resp.status_code == 200 html = resp.text # Elementul strip-sanatate e prezent in DOM dar ascuns cand totul e ok assert 'id="strip-sanatate"' in html, f"id=strip-sanatate lipseste complet din fragment. HTML: {html[:600]}" # Cand OK, banda nu trebuie sa afiseze ✗ (eroare) — ✓ nu mai apare (banda e ascunsa) assert "✗" not in html, \ f"Glifa ✗ (eroare) apare cand starea e ok — banda e gresit afisata. HTML: {html[:600]}" def test_status_are_bife_rosii_cand_worker_oprit(client): """Fara heartbeat -> worker oprit -> glifa rosie ✗ + text explicit 'blocat' / 'nu pleaca'.""" _create_account_user("biferosu@test.com") _login(client, "biferosu@test.com", "parolasecreta10") _set_heartbeat(last_beat=None, last_rar_login_ok=None) resp = client.get("/_fragments/status") assert resp.status_code == 200 html = resp.text assert "✗" in html, f"Lipseste glifa ✗ cand worker oprit. HTML: {html[:600]}" # Mesaj explicit (nu text vag "oprita") assert "blocat" in html.lower(), f"Cuvantul 'blocat' lipseste la worker oprit. HTML: {html[:600]}" assert "nu pleaca" in html.lower(), f"Avertismentul 'nu pleaca' lipseste. HTML: {html[:600]}" def test_status_data_formatata_romaneste(client): """Ultima autentificare RAR apare ca dd.mm.yyyy hh24:mi:ss.""" _create_account_user("bifedata@test.com") _login(client, "bifedata@test.com", "parolasecreta10") now = datetime.now(timezone.utc).isoformat() _set_heartbeat(last_beat=now, last_rar_login_ok="2026-06-18T14:30:22") resp = client.get("/_fragments/status") assert resp.status_code == 200 assert "18.06.2026 14:30:22" in resp.text, ( f"Data nu e formatata romaneste. HTML: {resp.text[:800]}" ) def test_status_fara_fonturi_minuscule(client): """Niciun text din bara nu mai foloseste font-size literal sub 13px.""" _create_account_user("bifefont@test.com") _login(client, "bifefont@test.com", "parolasecreta10") resp = client.get("/_fragments/status") assert resp.status_code == 200 html = resp.text # Culorile prin clase CSS (nu inline font-size); shorthand font:N Xpx nu e acoperit de aceste litere for bad in ("font-size:11px", "font-size:12px", "font-size: 11px", "font-size: 12px"): assert bad not in html, f"Bara de status foloseste {bad} (sub 13px) inline." # --------------------------------------------------------------------------- # Strip de sanatate mereu vizibil # --------------------------------------------------------------------------- def test_strip_sanatate_mereu_vizibil(client): """Strip de sanatate e prezent in fragment, indiferent de starea worker/RAR.""" _create_account_user("stripviz@test.com") _login(client, "stripviz@test.com", "parolasecreta10") # Stare worker viu now = datetime.now(timezone.utc).isoformat() _set_heartbeat(last_beat=now, last_rar_login_ok=now) resp = client.get("/_fragments/status") assert resp.status_code == 200 html = resp.text assert 'id="strip-sanatate"' in html, ( f"Strip sanatate (id='strip-sanatate') lipseste din fragment. HTML: {html[:600]}" ) def test_strip_rosu_worker_oprit(client): """Worker oprit → strip rosu cu glifа ✗ + text 'Blocat: worker oprit — declaratiile NU pleaca'.""" _create_account_user("stroprosu@test.com") _login(client, "stroprosu@test.com", "parolasecreta10") _set_heartbeat(last_beat=None, last_rar_login_ok=None) resp = client.get("/_fragments/status") assert resp.status_code == 200 html = resp.text assert 'id="strip-sanatate"' in html, "Strip sanatate lipseste." assert "✗" in html, "Glifa ✗ lipseste in strip rosu." assert "blocat" in html.lower(), "Cuvantul 'Blocat' trebuie sa apara cand worker e oprit." assert "worker oprit" in html.lower(), "Textul 'worker oprit' trebuie sa fie explicit in strip." assert "nu pleaca" in html.lower(), "Avertismentul 'NU pleaca' trebuie sa fie in strip." def test_trei_contoare_card(client): """Fragment status contine 5 carduri .contor-card separate: Total / Luna asta / Azi / In coada / De corectat.""" _create_account_user("treicont@test.com") _login(client, "treicont@test.com", "parolasecreta10") resp = client.get("/_fragments/status") assert resp.status_code == 200 html = resp.text count = html.count("contor-card") assert count >= 5, ( f"Trebuie minim 5 elemente contor-card (Total/Luna/Azi/Coada/Corectat), " f"gasit: {count}. HTML: {html[:800]}" ) # Etichete asteptate (5 carduri separate) assert "Total" in html, "Eticheta 'Total' lipseste din contoare." assert "Luna asta" in html, "Eticheta 'Luna asta' lipseste din contoare." assert "Azi" in html, "Eticheta 'Azi' lipseste din contoare." assert "In coada" in html, "Eticheta 'In coada' lipseste din contoare." assert "De corectat" in html, "Eticheta 'De corectat' lipseste din contoare." def test_trimise_all_time_luna_azi(client): """Cardul Trimise afiseaza all-time ca cifra principala + sub-linie 'luna N · azi N'.""" acct_id, _ = _create_account_user("trimisetime@test.com") _login(client, "trimisetime@test.com", "parolasecreta10") # Insereaza o trimitere sent cu updated_at = acum from app.db import get_connection conn = get_connection() try: conn.execute( "INSERT INTO submissions (account_id, status, payload_json, idempotency_key, updated_at) " "VALUES (?, 'sent', ?, 'key-luna-azi', datetime('now'))", (acct_id, json.dumps({"vin": "VINTEST00000000001"})), ) conn.commit() finally: conn.close() resp = client.get("/_fragments/status") assert resp.status_code == 200 html = resp.text # Sub-linia trebuie sa contina "luna" si "azi" (format: "luna N · azi N") assert "luna" in html.lower(), ( f"Sub-linia 'luna N · azi N' lipseste din cardul Trimise. HTML: {html[:800]}" ) assert "azi" in html.lower(), ( f"Sub-linia 'luna N · azi N' nu contine 'azi'. HTML: {html[:800]}" ) def test_fara_bara_veche(client): """Contoarele vechi inline ('In asteptare:' / 'Declarate la RAR:') nu mai apar.""" _create_account_user("faraveche@test.com") _login(client, "faraveche@test.com", "parolasecreta10") resp = client.get("/_fragments/status") assert resp.status_code == 200 html = resp.text # Stilul vechi: etichete inline cu colon assert "In asteptare:" not in html, ( f"Contorul vechi 'In asteptare:' inca prezent. HTML: {html[:600]}" ) assert "Declarate la RAR:" not in html, ( f"Contorul vechi 'Declarate la RAR:' inca prezent. HTML: {html[:600]}" ) def test_banda_apare_doar_cand_blocat(client): """Banda rosie completa apare NUMAI cand BLOCAT. Cand totul e ok, strip-sanatate are atributul 'hidden' (ascuns, nu disparut). Cand worker e oprit, strip-sanatate NU are 'hidden' (e vizibil, rosu). """ _create_account_user("bandablocat@test.com") _login(client, "bandablocat@test.com", "parolasecreta10") # Stare OK: strip ascuns now = datetime.now(timezone.utc).isoformat() _set_heartbeat(last_beat=now, last_rar_login_ok=now) resp = client.get("/_fragments/status") assert resp.status_code == 200 html_ok = resp.text # Cand OK, elementul e ascuns assert 'id="strip-sanatate"' in html_ok, "strip-sanatate lipseste din DOM cand totul e ok" assert "✗" not in html_ok, "Glifa eroare apare cand sanatate=ok (banda nu trebuie sa fie rosie)" # Stare BLOCAT: strip vizibil cu glifa ✗ _set_heartbeat(last_beat=None, last_rar_login_ok=None) resp = client.get("/_fragments/status") assert resp.status_code == 200 html_err = resp.text assert 'id="strip-sanatate"' in html_err, "strip-sanatate lipseste din DOM cand blocat" assert "✗" in html_err, "Glifa ✗ lipseste cand BLOCAT (banda trebuie sa fie rosie)" def test_rar_dot_in_antet_ok(client): """Cand logat si sanatate_ok, antetul contine chip-ul RAR cu clasa rar-ok. Starea ok se vede din header (dot verde pulsant), nu din banda de stare (care e ascunsa). """ _create_account_user("rardot@test.com") _login(client, "rardot@test.com", "parolasecreta10") now = datetime.now(timezone.utc).isoformat() _set_heartbeat(last_beat=now, last_rar_login_ok=now) resp = client.get("/", follow_redirects=True) assert resp.status_code == 200 html = resp.text # Chip RAR in antet (nu in banda de stare) assert "rar-chip" in html, "Clasa rar-chip lipseste din HTML (dot RAR in antet)" assert "rar-ok" in html, "Clasa rar-ok lipseste — dot verde cand sanatate ok" assert "rar-dot" in html, "Clasa rar-dot lipseste din chip" def test_rar_in_meniu_burger(client): """Meniul burger contine starea RAR ca prima intrare (RAR online / RAR indisponibil).""" _create_account_user("rarmeniu@test.com") _login(client, "rarmeniu@test.com", "parolasecreta10") now = datetime.now(timezone.utc).isoformat() _set_heartbeat(last_beat=now, last_rar_login_ok=now) resp = client.get("/", follow_redirects=True) assert resp.status_code == 200 html = resp.text # Meniul burger (cont-menu) contine indicatorul RAR assert "cont-menu" in html, "Meniu burger (cont-menu) lipseste din HTML" assert "RAR online" in html or "RAR indisponibil" in html, \ "Starea RAR nu apare in meniu burger" # Prima intrare e starea RAR — prezenta class menu-rar-line assert "menu-rar-line" in html, "Clasa menu-rar-line lipseste din burger" def test_anuleaza_are_data_modal_close(client): """Overlay-ul modal si butonul de inchidere au atributul data-modal-close.""" # Butonul si overlay-ul trebuie sa aiba data-modal-close pentru ca handler-ul cu .closest() sa functioneze # Verificam in baza template-ului base.html (modal e definit acolo, randat pe toate paginile) # Testam pe dashboard dupa login (unde baza e incarcata) _create_account_user("modalclose@test.com") _login(client, "modalclose@test.com", "parolasecreta10") resp = client.get("/", follow_redirects=True) assert resp.status_code == 200 html = resp.text assert "data-modal-close" in html, \ "data-modal-close lipseste din template — modalul nu se poate inchide" def test_modal_close_pe_element_interior(client): """Handler-ul modal foloseste .closest('[data-modal-close]') nu .hasAttribute direct — astfel click pe un element interior al backdrop-ului functioneaza.""" _create_account_user("modalclosest@test.com") _login(client, "modalclosest@test.com", "parolasecreta10") resp = client.get("/", follow_redirects=True) assert resp.status_code == 200 html = resp.text # Verificam ca JS-ul foloseste closest, nu hasAttribute assert "closest('[data-modal-close]')" in html, \ "Handler-ul modal foloseste hasAttribute in loc de closest — click pe copil nu va inchide modalul" def _set_tz_bucuresti(monkeypatch, request): """Forteaza TZ=Europe/Bucharest pentru ca modificatorul SQLite 'localtime' sa rezolve la fusul RO indiferent de TZ-ul runner-ului (CI ruleaza de regula in UTC). Restaureaza tzset-ul la teardown (monkeypatch reface env-ul TZ; tzset reciteste).""" import time monkeypatch.setenv("TZ", "Europe/Bucharest") if hasattr(time, "tzset"): time.tzset() request.addfinalizer(time.tzset) # dupa ce monkeypatch reface TZ -> reciteste def test_granita_miez_noapte_local_ro(monkeypatch, request): """Trimitere cu updated_at = ieri UTC 22:00 = azi Romania (UTC+2/+3) se numara 'azi'. Cu date(updated_at) simplu (UTC) ar aparea pe ziua precedenta — GRESIT. Cu date(updated_at, 'localtime') + TZ=Europe/Bucharest apare pe ziua de azi RO — CORECT (DST-aware: +2h iarna, +3h vara). """ _set_tz_bucuresti(monkeypatch, request) tmp = tempfile.mkdtemp() monkeypatch.setenv("AUTOPASS_DB_PATH", os.path.join(tmp, "granita.db")) from app.config import get_settings get_settings.cache_clear() from app.db import get_connection, init_db from app.accounts import create_account from app.web.routes import _status_counts # Initializeaza schema (init_db o face idempotent) init_db() # Construim un timestamp care, stocat ca UTC, cade pe ziua PRECEDENTA in UTC dar pe # ACEEASI zi RO ca 'now'. 00:30 RO local e mereu ziua UTC precedenta (UTC+2 iarna / # UTC+3 vara -> 22:30 / 21:30 UTC), DST-aware. Ancorat pe RO-now (nu pe data UTC), # deci robust la orice ora de rulare. Bug vechi: boundary ancorat pe today_utc pica # in fereastra de dupa miezul noptii RO, cand date('now','localtime') e deja ziua # urmatoare fata de today_utc -> sent_today=0 fals. from zoneinfo import ZoneInfo now_ro = datetime.now(ZoneInfo("Europe/Bucharest")) ro_devreme = now_ro.replace(hour=0, minute=30, second=0, microsecond=0) boundary_updated_at = ro_devreme.astimezone(timezone.utc).strftime("%Y-%m-%d %H:%M:%S") conn = get_connection() try: acct_id = create_account(conn, "Service Granita", active=True) conn.execute( "INSERT INTO submissions (account_id, status, payload_json, idempotency_key, updated_at) " "VALUES (?, 'sent', ?, 'key-granita-1', ?)", (acct_id, json.dumps({"vin": "VIN00000000000001"}), boundary_updated_at), ) conn.commit() counts = _status_counts(conn, acct_id) # 22:00 UTC -> azi in RO (localtime) => sent_today=1 assert counts["sent_today"] == 1, ( f"E7 FAIL: trimitere la {boundary_updated_at} UTC = azi in Romania " f"trebuie sa fie 'azi', dar sent_today={counts.get('sent_today')}. " "SQL trebuie sa foloseasca date(updated_at, 'localtime') = date('now', 'localtime')." ) assert counts["sent_month"] >= 1, ( f"E7: sent_month trebuie sa fie >= 1, got {counts.get('sent_month')}" ) finally: conn.close() get_settings.cache_clear() def test_iarna_nu_bleed_in_ziua_urmatoare(monkeypatch, request): """Iarna (UTC+2), o trimitere la 21:30 UTC = 23:30 RO AZI NU trebuie sa cada pe ziua de MAINE. Vechiul offset fix '+3 hours' o impingea la 00:30 maine -> sent_today gresit. 'localtime' (DST-aware) o pastreaza corect pe azi. Testul fixeaza o data de iarna explicita (15 ianuarie) ca sa fie determinist indiferent de cand ruleaza. """ _set_tz_bucuresti(monkeypatch, request) tmp = tempfile.mkdtemp() monkeypatch.setenv("AUTOPASS_DB_PATH", os.path.join(tmp, "iarna.db")) from app.config import get_settings get_settings.cache_clear() from app.db import get_connection, init_db from app.accounts import create_account init_db() # Data de iarna fixa: 2026-01-15. 21:30 UTC = 23:30 RO (EET, UTC+2) -> ziua 15, nu 16. conn = get_connection() try: acct_id = create_account(conn, "Service Iarna", active=True) conn.execute( "INSERT INTO submissions (account_id, status, payload_json, idempotency_key, updated_at) " "VALUES (?, 'sent', ?, 'key-iarna-1', ?)", (acct_id, json.dumps({"vin": "VIN00000000000002"}), "2026-01-15 21:30:00"), ) conn.commit() # Verifica direct ce zi RO atribuie SQLite (localtime vs vechiul +3h). row = conn.execute( "SELECT date(updated_at, 'localtime') AS zi_local, " " date(updated_at, '+3 hours') AS zi_plus3 " "FROM submissions WHERE idempotency_key='key-iarna-1'" ).fetchone() assert row["zi_local"] == "2026-01-15", ( f"localtime (EET, UTC+2) trebuie sa pastreze 21:30 UTC pe 15 ian RO, " f"got {row['zi_local']}" ) # Demonstreaza bug-ul vechi: +3h impingea pe 16 ian (ziua gresita iarna). assert row["zi_plus3"] == "2026-01-16", ( f"Confirmare bug vechi: '+3 hours' iarna pune 21:30 UTC pe 16 ian, got {row['zi_plus3']}" ) finally: conn.close() get_settings.cache_clear() # =========================================================================== # Afisaj plan curent: trial / consum / warn / banner # =========================================================================== def _set_trial_until(account_id: int, trial_until_str: str | None) -> None: """Seteaza direct trial_until pentru un cont (helper de test).""" from app.db import get_connection conn = get_connection() try: conn.execute( "UPDATE accounts SET trial_until=? WHERE id=?", (trial_until_str, account_id), ) conn.commit() finally: conn.close() def _insert_submissions_sent(account_id: int, n: int) -> None: """Insereaza N submissions sent in luna curenta (helper de test).""" from app.db import get_connection import json as _json conn = get_connection() try: for i in range(n): conn.execute( "INSERT INTO submissions (account_id, status, payload_json, idempotency_key, created_at) " "VALUES (?, 'sent', ?, ?, datetime('now'))", (account_id, _json.dumps({"vin": f"VIN{i:013d}"}), f"key-plan-{account_id}-{i}"), ) conn.commit() finally: conn.close() def test_afisaj_plan_si_zile_trial(client): """Cont in trial Pro -> linia de plan din meniul burger (pagina completa) arata 'Plan: Pro · trial N zile ramase'. In starea normala (non-warn) plan_linie NU mai e rand in corpul fragmentului status — traieste in badge antet + burger. Contul nou primeste trial_until=now+30z automat la creare. """ acct_id, _ = _create_account_user("trialzile@test.com") _login(client, "trialzile@test.com", "parolasecreta10") # trial_until = now + 18 zile + 12h (buffer pt a evita delta.days=17 din timing test) future = (datetime.now(timezone.utc) + timedelta(days=18, hours=12)).strftime("%Y-%m-%d %H:%M:%S") _set_trial_until(acct_id, future) resp = client.get("/", follow_redirects=True) assert resp.status_code == 200 html = resp.text assert "Plan: Pro" in html, f"Textul 'Plan: Pro' lipseste in trial. HTML: {html[:800]}" assert "trial" in html.lower(), f"Cuvantul 'trial' lipseste in starea de trial. HTML: {html[:800]}" assert "18" in html, f"Numarul de zile (18) nu apare in afisaj. HTML: {html[:800]}" assert "zile" in html, f"Cuvantul 'zile' lipseste (pluralizare). HTML: {html[:800]}" def test_afisaj_consum_lunar(client): """Cont free (fara trial) -> linia de plan din burger (pagina completa) arata 'Gratuit · N/60 luna asta'. Consumul normal nu mai e rand in corp.""" acct_id, _ = _create_account_user("consumlun@test.com") _login(client, "consumlun@test.com", "parolasecreta10") # Dezactiveaza trial-ul (cont free pur) _set_trial_until(acct_id, None) # Insereaza 5 submissions sent luna asta _insert_submissions_sent(acct_id, 5) resp = client.get("/", follow_redirects=True) assert resp.status_code == 200 html = resp.text assert "Gratuit" in html, f"'Gratuit' lipseste din afisajul de consum. HTML: {html[:800]}" assert "5" in html, f"Contorul de consum (5) nu apare. HTML: {html[:800]}" assert "60" in html, f"Limita (60) nu apare in afisajul de consum. HTML: {html[:800]}" assert "luna asta" in html, f"'luna asta' lipseste din afisajul de consum. HTML: {html[:800]}" def test_avertizare_aproape_de_limita(client): """>=80% din 60 -> avertizare cu text 'aproape de limita' + culoare warn.""" acct_id, _ = _create_account_user("aproapelim@test.com") _login(client, "aproapelim@test.com", "parolasecreta10") _set_trial_until(acct_id, None) # 50/60 = 83% -> warn _insert_submissions_sent(acct_id, 50) resp = client.get("/_fragments/status") assert resp.status_code == 200 html = resp.text assert "aproape de limita" in html, ( f"Textul 'aproape de limita' lipseste la 50/60. HTML: {html[:800]}" ) assert "50" in html, f"Contorul 50 nu apare. HTML: {html[:800]}" # Warn = culoare (var(--warn) in inline style) assert "var(--warn)" in html or "plan-warn" in html, ( f"Stilul de warn (var(--warn) sau clasa plan-warn) lipseste la aproape-de-limita. HTML: {html[:800]}" ) def test_limita_atinsa(client): """60/60 -> text 'limita atinsa'.""" acct_id, _ = _create_account_user("limitaatinsa@test.com") _login(client, "limitaatinsa@test.com", "parolasecreta10") _set_trial_until(acct_id, None) _insert_submissions_sent(acct_id, 60) resp = client.get("/_fragments/status") assert resp.status_code == 200 html = resp.text assert "limita atinsa" in html, ( f"Textul 'limita atinsa' lipseste la 60/60. HTML: {html[:800]}" ) def test_copy_pluralizare_zi_zile(client): """Pluralizare RO corecta — 1 zi (nu '1 zile'), 18 zile (nu '18 zi').""" acct_id, _ = _create_account_user("pluralzile@test.com") _login(client, "pluralzile@test.com", "parolasecreta10") # 18 zile: trebuie "18 zile ramase" (buffer 12h pt delta.days determinist) future_18 = (datetime.now(timezone.utc) + timedelta(days=18, hours=12)).strftime("%Y-%m-%d %H:%M:%S") _set_trial_until(acct_id, future_18) # Linia de plan (cu pluralizarea zilelor) traieste in burger pe pagina completa. resp = client.get("/", follow_redirects=True) assert resp.status_code == 200 html = resp.text assert "18 zile" in html, f"'18 zile' lipseste. HTML: {html[:800]}" assert "18 zi " not in html and "18 zi<" not in html, ( f"'18 zi' (plural gresit) apare in loc de '18 zile'. HTML: {html[:800]}" ) # 1 zi: trebuie "1 zi ramasa" (singular); buffer 12h future_1 = (datetime.now(timezone.utc) + timedelta(days=1, hours=12)).strftime("%Y-%m-%d %H:%M:%S") _set_trial_until(acct_id, future_1) resp = client.get("/", follow_redirects=True) assert resp.status_code == 200 html = resp.text assert "1 zi" in html, f"'1 zi' (singular) lipseste la o zi ramasa. HTML: {html[:800]}" assert "1 zile" not in html, ( f"'1 zile' (plural gresit) apare in loc de '1 zi'. HTML: {html[:800]}" ) def test_banner_one_time_trial_expirat(client): """Dupa expirarea trial-ului, banner 'Trial Pro expirat' apare in _status.html.""" acct_id, _ = _create_account_user("trialexp@test.com") _login(client, "trialexp@test.com", "parolasecreta10") # trial_until in trecut -> trial expirat -> banner one-time past = (datetime.now(timezone.utc) - timedelta(days=2)).strftime("%Y-%m-%d %H:%M:%S") _set_trial_until(acct_id, past) resp = client.get("/_fragments/status") assert resp.status_code == 200 html = resp.text assert "Trial Pro expirat" in html, ( f"Banner 'Trial Pro expirat' lipseste dupa expirarea trial-ului. HTML: {html[:800]}" ) assert "Gratuit" in html, ( f"Dupa expirarea trial-ului, planul trebuie sa afiseze 'Gratuit'. HTML: {html[:800]}" ) # Bannerul are buton de dismiss assert "banner-trial-expirat" in html, ( f"Elementul id=banner-trial-expirat lipseste. HTML: {html[:800]}" ) def test_cont_arata_plan(client): """Tab-ul Cont (/tab=cont) afiseaza planul curent si explicatia de upgrade.""" acct_id, _ = _create_account_user("contplan@test.com") _login(client, "contplan@test.com", "parolasecreta10") _set_trial_until(acct_id, None) # free fara trial resp = client.get("/?tab=cont", follow_redirects=True) assert resp.status_code == 200 html = resp.text assert "Plan curent" in html or "sectiune-plan" in html, ( f"Sectiunea 'Plan curent' lipseste din tab-ul Cont. HTML: {html[:1000]}" ) assert "Gratuit" in html, f"'Gratuit' lipseste din planul afisat in Cont. HTML: {html[:1000]}" assert "Standard" in html or "Pro" in html, ( f"Optiunile de upgrade (Standard/Pro) lipsesc din sectiunea Plan. HTML: {html[:1000]}" ) def test_plan_linie_in_burger(client): """Meniul burger contine linia de plan (Plan: Gratuit / Pro · trial N zile).""" acct_id, _ = _create_account_user("burgerplan@test.com") _login(client, "burgerplan@test.com", "parolasecreta10") _set_trial_until(acct_id, None) # free fara trial resp = client.get("/", follow_redirects=True) assert resp.status_code == 200 html = resp.text # Meniul burger trebuie sa contina linia de plan assert "Plan: Gratuit" in html, ( f"'Plan: Gratuit' lipseste din meniu burger. HTML (fragment): {html[html.find('cont-menu'):html.find('cont-menu')+500] if 'cont-menu' in html else html[:500]}" ) def test_trial_pro_arata_zile_in_burger(client): """Cont in trial -> burger arata 'Plan: Pro · trial N zile ramase'.""" acct_id, _ = _create_account_user("burgertrial@test.com") _login(client, "burgertrial@test.com", "parolasecreta10") future = (datetime.now(timezone.utc) + timedelta(days=10)).strftime("%Y-%m-%d %H:%M:%S") _set_trial_until(acct_id, future) resp = client.get("/", follow_redirects=True) assert resp.status_code == 200 html = resp.text assert "Plan: Pro" in html, f"'Plan: Pro' lipseste din burger in trial. HTML: {html[:800]}" assert "trial" in html.lower(), f"'trial' lipseste din linia de plan din burger. HTML: {html[:800]}"