Files
rar-autopass/tests/test_web_integrare.py
Claude Agent f0786051f5 feat(web): hub integrare /integrare — exemple cod + retetar VFP + ping + export (PRD 5.1)
Pagina /integrare (tab autentificat, scoped pe cont): exemple cod multi-limbaj
(curl/Python/PHP/C#/Node) + retetar Visual FoxPro (MSXML2 + WinHttp) pe ambele
canale (prezentari JSON + import fisier), export Postman/OpenAPI/Swagger si buton
"Testeaza conexiunea".

- US-001: GET /v1/ping (readiness: account_id/mediu/autentificat_cu_cheie/
  are_creds_rar/ts) + GET /v1/integrare/postman.json (v2.1.0, allowlist 3 rute)
- US-002: app/web/integrare_examples.py pur (7 limbaje x 2 canale, drift-test
  is_required(), JSON compact pentru C#/VFP)
- US-003: tab "Integrare" IA pe 2 niveluri (limbaj->canal, VFP cu dialecte),
  copy din <pre><code>, empty-state CTA, export .cardlink, script scoped
- US-004: POST /integrare/test-cheie (account_for_key direct, scoped sesiune,
  no-echo cheie)

Backend trimitere (worker/masina stari/idempotenta/mapping) si schema neatinse.
568 teste pass. VERIFY context curat + E2E browser (Playwright) + code-review high.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 12:16:41 +00:00

439 lines
18 KiB
Python

"""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 <pre><code> (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 <pre><code> (nu text ascuns in data-*)
assert "<pre>" in html and "<code>" 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 <style>
style_attrs = _re.findall(r'style="([^"]*)"', html)
style_tags = _re.findall(r'<style[^>]*>(.*?)</style>', 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 <a cu href postman.json care contine si atribut download
postman_links = _re.findall(r'<a[^>]*postman\.json[^>]*>', html)
assert postman_links, "Tag-ul <a> 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'<a[^>]*cardlink[^>]*>', html)
assert cardlink_anchors, "Nu exista niciun <a class=\"cardlink\"> 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'<script[^>]*>(.*?)</script>', 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'<script[^>]*>(.*?)</script>', 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')"
)