Files
rar-autopass/tests/test_web_status.py
Claude Agent 96973f60f3 test: repara flake nocturn miez-noapte RO + 2 teste stale landing/login
test_web_status::test_granita_miez_noapte_local_ro: ancoreaza boundary pe RO-now
(00:30 RO local, mereu ziua UTC precedenta, DST-aware) in loc de today_utc. Bug
vechi: boundary pe today_utc pica in fereastra de dupa miezul noptii RO cand
date('now','localtime') e deja ziua urmatoare. Fereastra de esec de ~3h -> race
sub-secunda la exact miezul noptii.

test_web_responsive::test_login_branded_nu_schelet: loginul a fost simplificat la
o coloana (commit 'simplifica login'); nu mai cerem .login-aside. Guard pastrat pe
.login-shell + titlu ROA AUTOPASS + POST /login + CSRF. Comentariile stale '2 coloane'
din login.html aliniate la realitate (o coloana).

test_web_responsive::test_landing_limita_60: terminologia landing prestații->trimiteri
(rework ff9d0f4); asertie pe '60 de trimiteri' (limita Gratuit), scoase asertiile pe
formularile vechi 'prestații'/'prezentări'.

Suita: 1458 passed, 1 deselected (live), 0 failed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 12:25:01 +00:00

695 lines
29 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Teste US-003 (PRD 5.15): strip sanatate mereu-vizibil + carduri-contor pe dashboard.
D6 (strip sanatate): linie colorata DEASUPRA contoarelor — verde "declaratiile curg" /
rosu "Blocat: worker oprit / RAR inaccesibil", cu glifa accesibila (✓/✗).
D4 (contoare): In coada / Trimise (all-time + luna/azi) / De corectat.
E7 (timezone): azi/luna bucketate in timp local RO (UTC+3), nu UTC.
Actualizat in US-003 (PRD 5.15): bara veche cu bife individuale worker/RAR
inlocuita de strip unificat de sanatate + carduri-contor.
"""
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()
# ---------------------------------------------------------------------------
# Teste existente — actualizate pentru US-003 D6 (strip unificat in loc de bife individuale)
# ---------------------------------------------------------------------------
def test_status_are_bife_verzi_cand_totul_ok(client):
"""US-003 PRD 5.16: 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
# US-003: 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 "&#10007;" 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 "&#10007;" in html, f"Lipseste glifa ✗ cand worker oprit. HTML: {html[:600]}"
# US-003 D6: 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 (US-001 AC)."""
_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."
# ---------------------------------------------------------------------------
# Teste NOI pentru US-003 (RED inainte de implementare)
# ---------------------------------------------------------------------------
def test_strip_sanatate_mereu_vizibil(client):
"""D6: 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):
"""D6: 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 "&#10007;" 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):
"""US-002 PRD 5.16: 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 (US-002 PRD 5.16: Total/Luna/Azi/Coada/Corectat), "
f"gasit: {count}. HTML: {html[:800]}"
)
# Etichete asteptate (US-002 PRD 5.16: 5 carduri separate)
assert "Total" in html, "Eticheta 'Total' lipseste din contoare (US-002 PRD 5.16)."
assert "Luna asta" in html, "Eticheta 'Luna asta' lipseste din contoare (US-002 PRD 5.16)."
assert "Azi" in html, "Eticheta 'Azi' lipseste din contoare (US-002 PRD 5.16)."
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):
"""D4: 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):
"""US-003: 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 (bara de la PRD 3.5)
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):
"""US-003 (PRD 5.16): 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 "&#10007;" 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 "&#10007;" in html_err, "Glifa ✗ lipseste cand BLOCAT (banda trebuie sa fie rosie)"
def test_rar_dot_in_antet_ok(client):
"""US-003 (PRD 5.16): 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, US-003)"
assert "rar-ok" in html, "Clasa rar-ok lipseste — dot verde cand sanatate ok (US-003)"
assert "rar-dot" in html, "Clasa rar-dot lipseste din chip (US-003)"
def test_rar_in_meniu_burger(client):
"""US-003/010 (PRD 5.16): 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 (US-003/010)"
# Prima intrare e starea RAR — prezenta class menu-rar-line
assert "menu-rar-line" in html, "Clasa menu-rar-line lipseste din burger (US-003)"
def test_anuleaza_are_data_modal_close(client):
"""US-007 (PRD 5.16): 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 (US-007)"
def test_modal_close_pe_element_interior(client):
"""US-007 (PRD 5.16): handler-ul modal foloseste .closest('[data-modal-close]') nu
.hasAttribute directe — 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 (US-007) — 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):
"""E7: 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):
"""Bug fix (code-review 5.15): 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()
# ===========================================================================
# US-006 (PRD 5.17) — 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):
"""US-006 + T-6 (5.16): 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):
"""US-006 + T-6 (5.16): 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):
"""US-006: >=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):
"""US-006: 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):
"""US-006: 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)
# T-6 (5.16): 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):
"""US-006 T-DES-1: 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):
"""US-006: 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):
"""US-006: 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):
"""US-006: 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]}"