"""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): """Worker viu + RAR login recent -> glifa verde ✓ + text 'declaratiile curg normal'.""" _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 # Glifa accesibila ✓ (nu doar culoare) assert "✓" in html, f"Lipseste glifa ✓ cand totul e ok. HTML: {html[:600]}" # US-003 D6: strip unificat (nu bife individuale worker/RAR) assert "curg normal" in html.lower(), ( f"Textul 'curg normal' din strip sanatate lipseste. 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]}" # 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 "✗" 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-003: fragment status contine exact 3 carduri .contor-card (In coada / Trimise / 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 >= 3, ( f"Trebuie minim 3 elemente contor-card in fragment, gasit: {count}. HTML: {html[:800]}" ) # Etichete asteptate assert "In coada" in html, "Eticheta 'In coada' lipseste din contoare." assert "Trimise" in html, "Eticheta 'Trimise' 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 _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() # Ieri la 22:00 UTC = azi 00:00 (iarna) / 01:00 (vara) Romania -> 'azi' in ambele. today_utc = datetime.now(timezone.utc).date() yesterday_utc = today_utc - timedelta(days=1) boundary_updated_at = f"{yesterday_utc} 22:00:00" 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()