"""Teste US-003 — tab "Integrare" (hub documentatie + exemple cod). TDD: testele se scriu INAINTE de implementare; la inceput pica (RED), dupa implementare trec (GREEN). Rute testate: - GET / -> tab-bar contine tab "Integrare" - GET /?tab=integrare -> panou randat server-side - GET /_fragments/integrare -> fragment HTMX cu require_login """ from __future__ import annotations import os import re import tempfile import pytest from starlette.testclient import TestClient # =========================================================================== # # Helpers comune # # =========================================================================== # def _create_account_user(email: str, password: str = "parolasecreta10"): """Creeaza cont + user. Intoarce (acct_id, user_id).""" 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 Integrare", 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 = "parolasecreta10") -> None: """Autentificare HTTP; seteaza cookie de sesiune pe client.""" resp = client.get("/login") m = re.search(r'name="csrf_token"\s+value="([^"]+)"', resp.text) or \ 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 _add_creds(acct_id: int) -> None: """Adauga credentiale RAR criptate pe cont (pentru test are_creds=True).""" from app.db import get_connection from app.crypto import encrypt_creds conn = get_connection() try: creds_enc = encrypt_creds({"email": "test@rar.ro", "password": "secret"}) conn.execute( "UPDATE accounts SET rar_creds_enc=? WHERE id=?", (creds_enc, acct_id), ) conn.commit() finally: conn.close() def _add_api_key(acct_id: int) -> str: """Adauga o cheie API activa pe cont. Intoarce cheia bruta.""" from app.db import get_connection from app.auth import create_api_key conn = get_connection() try: key = create_api_key(conn, acct_id) return key finally: conn.close() @pytest.fixture() def client(monkeypatch): """Client cu BD izolata si autentificare web activata.""" tmp = tempfile.mkdtemp() monkeypatch.setenv("AUTOPASS_DB_PATH", os.path.join(tmp, "integrare_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() # =========================================================================== # # test_tab_integrare_in_nav # # =========================================================================== # def test_tab_integrare_in_nav(client): """Tab-bar-ul principal contine link-ul catre tab-ul Integrare.""" _create_account_user("nav_integrare@test.com") _login(client, "nav_integrare@test.com") resp = client.get("/") assert resp.status_code == 200 html = resp.text # Trebuie sa existe un link sau buton cu ?tab=integrare assert "tab=integrare" in html, "tab-ul 'integrare' lipseste din tab-bar" # Textul "Integrare" vizibil assert "Integrare" in html # =========================================================================== # # test_deeplink_tab_integrare_randeaza_panou_server_side # # =========================================================================== # def test_deeplink_tab_integrare_randeaza_panou_server_side(client): """GET /?tab=integrare randeaza panoul server-side (nu redirectioneaza).""" _create_account_user("deeplink_integrare@test.com") _login(client, "deeplink_integrare@test.com") resp = client.get("/?tab=integrare") assert resp.status_code == 200 html = resp.text # Panoul trebuie sa contina continut specific integrare # (nu sa cada pe Acasa din cauza unui tab invalid) assert "integrare" in html.lower() # tab-ul activ trebuie sa fie "integrare" assert 'aria-selected="true"' in html or "tab-activ" in html # =========================================================================== # # test_fragment_integrare_necesita_login # # =========================================================================== # def test_fragment_integrare_necesita_login(client): """GET /_fragments/integrare fara sesiune redirectioneaza la /login (require_login).""" # Fara login -> trebuie 303 catre /login resp = client.get("/_fragments/integrare") assert resp.status_code == 303 assert "/login" in resp.headers.get("location", "") # =========================================================================== # # test_pagina_contine_account_id_si_endpoint_real # # =========================================================================== # def test_pagina_contine_account_id_si_endpoint_real(client): """Panoul Integrare afiseaza account_id-ul contului si URL-ul real al endpoint-ului.""" acct_id, _ = _create_account_user("endpoint_real@test.com") _login(client, "endpoint_real@test.com") resp = client.get("/_fragments/integrare") assert resp.status_code == 200 html = resp.text # account_id trebuie sa apara in pagina assert str(acct_id) in html # endpoint-ul /v1/prezentari trebuie sa apara assert "/v1/prezentari" in html # =========================================================================== # # test_pagina_are_tab_limbaje_si_vfp_cu_dialecte # # =========================================================================== # def test_pagina_are_tab_limbaje_si_vfp_cu_dialecte(client): """Panoul Integrare are tab-uri pentru limbaje si VFP are sub-tab-uri (dialecte).""" _create_account_user("limbaje@test.com") _login(client, "limbaje@test.com") resp = client.get("/_fragments/integrare") assert resp.status_code == 200 html = resp.text # Limbajele principale trebuie sa fie prezente for limbaj in ("curl", "Python", "PHP"): assert limbaj in html, f"Limbajul '{limbaj}' lipseste din panoul Integrare" # VFP (Visual FoxPro) trebuie sa fie prezent assert "VFP" in html or "FoxPro" in html or "Visual Fox" in html # Dialectele VFP assert "MSXML2" in html or "MSXML" in html assert "WinHttp" in html # =========================================================================== # # test_canal_secundar_prezentari_si_import # # =========================================================================== # def test_canal_secundar_prezentari_si_import(client): """Fiecare limbaj are sectiuni pentru canalele Prezentari JSON si Import fisier.""" _create_account_user("canal_sec@test.com") _login(client, "canal_sec@test.com") resp = client.get("/_fragments/integrare") assert resp.status_code == 200 html = resp.text # Ambele canale trebuie sa fie vizibile in pagina assert "prezentari" in html.lower() or "/v1/prezentari" in html assert "import" in html.lower() or "/v1/import" in html # =========================================================================== # # test_export_card_openapi_postman_swagger # # =========================================================================== # def test_export_card_openapi_postman_swagger(client): """Panoul Integrare are card cu linkuri: /docs, /openapi.json, postman.json.""" _create_account_user("export_card@test.com") _login(client, "export_card@test.com") resp = client.get("/_fragments/integrare") assert resp.status_code == 200 html = resp.text # Linkuri de referinta assert "/docs" in html assert "/openapi.json" in html assert "postman" in html.lower() # =========================================================================== # # test_buton_copiaza_citeste_din_pre_code # # =========================================================================== # def test_buton_copiaza_citeste_din_pre_code(client): """Snippet-urile au buton 'Copiaza' care citeste din
 (nu din data-*)."""
    _create_account_user("copiaza@test.com")
    _login(client, "copiaza@test.com")

    resp = client.get("/_fragments/integrare")
    assert resp.status_code == 200
    html = resp.text
    # Trebuie sa existe un buton de copiere
    assert "Copiaza" in html or "copiaza" in html.lower()
    # Snippet-urile trebuie sa fie in 
 (nu text ascuns in data-*)
    assert "
" in html and "" in html
    # Butonul NU trebuie sa aiba data-cod sau data-snippet cu continutul
    # (copiaza din DOM, nu din attribut)
    assert 'data-cod="' not in html
    assert 'data-snippet="' not in html


# =========================================================================== #
# test_empty_state_cta_cont_cand_fara_cheie_sau_creds                          #
# =========================================================================== #

def test_empty_state_cta_cont_cand_fara_cheie_sau_creds(client):
    """Fara cheie API sau credentiale RAR, panoul afiseaza CTA catre tab Cont."""
    # Cont nou fara cheie si fara credentiale
    _create_account_user("empty_state@test.com")
    _login(client, "empty_state@test.com")

    resp = client.get("/_fragments/integrare")
    assert resp.status_code == 200
    html = resp.text
    # Trebuie sa existe un mesaj de empty-state cu link catre tab cont
    assert "/?tab=cont" in html or "tab=cont" in html
    # Mesajul trebuie sa atraga atentia ca lipsesc ceva
    # (cheie sau credentiale)
    lower = html.lower()
    assert "cheie" in lower or "credentiale" in lower or "cont" in lower


# =========================================================================== #
# test_fara_culori_hardcodate_doar_tokens                                      #
# =========================================================================== #

def test_fara_culori_hardcodate_doar_tokens(client):
    """Panoul Integrare nu contine culori hex hardcodate (#RRGGBB) — doar var(--...) tokens."""
    _create_account_user("tokens_css@test.com")
    _login(client, "tokens_css@test.com")

    resp = client.get("/_fragments/integrare")
    assert resp.status_code == 200
    html = resp.text
    # Cauta culori hex in stil inline (style="...#...") sau in tag style
    # Pattern: # urmat de 3 sau 6 hex digits, in context CSS
    import re as _re
    # Cautam doar in atributele style="" inline si in taguri ', html, _re.DOTALL)
    all_css = " ".join(style_attrs) + " ".join(style_tags)
    # Culori hex in CSS: #rgb sau #rrggbb (precedate de spatiu, :, sau ;)
    hex_colors = _re.findall(r'(?<=[: ])#[0-9a-fA-F]{3,6}\b', all_css)
    assert not hex_colors, (
        f"Culori hex hardcodate gasite in _integrare.html: {hex_colors}. "
        "Foloseste var(--...) tokens CSS."
    )


# =========================================================================== #
# test_export_postman_are_atribut_download  [FIX-2]                            #
# =========================================================================== #

def test_export_postman_are_atribut_download(client):
    """Linkul Postman (.json) contine atributul download (PRD US-003)."""
    _create_account_user("postman_download@test.com")
    _login(client, "postman_download@test.com")

    resp = client.get("/_fragments/integrare")
    assert resp.status_code == 200
    html = resp.text
    # Trebuie sa existe linkul postman.json cu atribut download
    assert "postman.json" in html, "Linkul postman.json lipseste din pagina"
    import re as _re
    # Cautam ]*postman\.json[^>]*>', html)
    assert postman_links, "Tag-ul  cu postman.json nu a fost gasit"
    assert any("download" in lnk for lnk in postman_links), (
        f"Linkul Postman nu are atribut 'download'. Tag gasit: {postman_links}"
    )


# =========================================================================== #
# test_export_card_foloseste_cardlink  [FIX-1]                                 #
# =========================================================================== #

def test_export_card_foloseste_cardlink(client):
    """Cardul Export & referinta foloseste componenta .cardlink (PRD US-003)."""
    _create_account_user("export_cardlink@test.com")
    _login(client, "export_cardlink@test.com")

    resp = client.get("/_fragments/integrare")
    assert resp.status_code == 200
    html = resp.text
    # Cardul export trebuie sa contina clasa cardlink
    assert "cardlink" in html, (
        "Clasa 'cardlink' lipseste din panoul Integrare. "
        "Cardul Export & referinta trebuie sa foloseasca componenta .cardlink."
    )
    # Linkurile de export trebuie sa foloseasca clasa cardlink
    import re as _re
    cardlink_anchors = _re.findall(r']*cardlink[^>]*>', html)
    assert cardlink_anchors, "Nu exista niciun  in pagina"


# =========================================================================== #
# test_microcopy_anticonfuzie_la_test_cheie  [FIX-3]                           #
# =========================================================================== #

def test_microcopy_anticonfuzie_la_test_cheie(client):
    """Formularul 'Testeaza conexiunea' contine microcopy anti-confuzie (PRD US-004)."""
    _create_account_user("microcopy@test.com")
    _login(client, "microcopy@test.com")

    resp = client.get("/_fragments/integrare")
    assert resp.status_code == 200
    html = resp.text
    # Textul specific din PRD US-004
    assert "Nu o salvam" in html, (
        "Microcopy anti-confuzie lipseste din formularul 'Testeaza conexiunea'. "
        "Trebuie sa contina: 'Nu o salvam si nu o memoram'."
    )


# =========================================================================== #
# test_buton_copiaza_schimba_label_in_copiat  [FIX-4]                          #
# =========================================================================== #

def test_buton_copiaza_schimba_label_in_copiat(client):
    """Scriptul JS schimba label-ul butonului in 'Copiat' la copiere (PRD US-003)."""
    _create_account_user("copiat_label@test.com")
    _login(client, "copiat_label@test.com")

    resp = client.get("/_fragments/integrare")
    assert resp.status_code == 200
    html = resp.text
    # Scriptul trebuie sa contina logica de schimbare a textContent pe buton
    assert "textContent" in html, (
        "Scriptul JS nu contine 'textContent' — schimbarea label-ului butonului lipseste."
    )
    assert "Copiat" in html, (
        "Textul 'Copiat' lipseste din script — butonul nu isi schimba label-ul."
    )
    import re as _re
    script_blocks = _re.findall(r']*>(.*?)', html, _re.DOTALL)
    script_text = " ".join(script_blocks)
    # Verificam ca butonul (btn) isi schimba textContent (nu doar feedback div)
    assert "btn.textContent" in script_text, (
        "Scriptul JS nu contine 'btn.textContent' — label-ul butonului nu se schimba."
    )
    # Verificam valoarea setata pe buton
    assert "'Copiat'" in script_text or '"Copiat"' in script_text, (
        "Scriptul JS nu seteaza valoarea 'Copiat' pe buton."
    )
    # Verificam revenirea la 'Copiaza' dupa setTimeout
    assert "setTimeout" in script_text, (
        "Scriptul JS nu contine setTimeout — revenirea la 'Copiaza' dupa 2s lipseste."
    )
    assert "'Copiaza'" in script_text or '"Copiaza"' in script_text, (
        "Scriptul JS nu seteaza revenirea la 'Copiaza' dupa timeout."
    )


# =========================================================================== #
# test_script_integrare_scoped_pe_container  [FIX-3]                           #
# =========================================================================== #

def test_script_integrare_scoped_pe_container(client):
    """Scriptul JS din _integrare.html este scoped pe #integrare-section.

    Un querySelectorAll global ar ataca si tablist-ul principal din dashboard.html,
    acumuland handlere si provocand dubla-legare pe fiecare swap HTMX.
    Verificam ca scriptul porneste de la getElementById('integrare-section')
    si NU face document.querySelectorAll('[role="tablist"]') global.
    """
    _create_account_user("scoped_script@test.com")
    _login(client, "scoped_script@test.com")

    resp = client.get("/_fragments/integrare")
    assert resp.status_code == 200
    html = resp.text

    import re as _re
    script_blocks = _re.findall(r']*>(.*?)', html, _re.DOTALL)
    script_text = " ".join(script_blocks)

    # Trebuie sa existe getElementById('integrare-section') ca root
    assert "getElementById('integrare-section')" in script_text or \
           'getElementById("integrare-section")' in script_text, (
        "Scriptul JS nu contine getElementById('integrare-section') — "
        "scoping-ul pe container lipseste"
    )

    # NU trebuie sa existe document.querySelectorAll cu '[role="tablist"]' global
    # (adica fara a folosi root-ul)
    has_global_tablist = bool(
        _re.search(r'document\.querySelectorAll\([\'"][^"\']*\[role=["\']tablist["\'][^"\']*[\'"]\)', script_text)
    )
    assert not has_global_tablist, (
        "Scriptul JS contine document.querySelectorAll('[role=\"tablist\"]') global — "
        "trebuie sa foloseasca root.querySelectorAll dupa getElementById('integrare-section')"
    )