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>
439 lines
18 KiB
Python
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')"
|
|
)
|