Cand `rar_credentials` lipseste din cerere, submission-ul intra fara creds efemere, iar worker-ul cade pe creds-urile RAR durabile ale contului (accounts.rar_creds_enc). Identificarea contului ramane pe cheia API. Trimiterea explicita a creds-urilor suprascrie creds-urile contului pe acea cerere (back-compat: fluxul vechi ROAAUTO merge identic). - models.py: rar_credentials: RarCredentials | None = None - router.py: cripteaza creds doar daca exista (altfel creds_enc=NULL) - worker NEATINS: avea deja fallback _creds_for(...) or _creds_from_account(...) Pagina /integrare aliniata: exemplele cod (7 limbaje) + export Postman nu mai includ rar_credentials in payload; nota noua explica modelul (creds pe cont, optional in payload). README rescris compact + reflecta optionalitatea. Test nou: enqueue fara creds -> submission fara creds efemere -> fallback pe contul cu creds salvate. Suita: 673 passed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
240 lines
9.7 KiB
Python
240 lines
9.7 KiB
Python
"""Teste pentru app.web.integrare_examples — modul PUR, fara I/O.
|
|
|
|
Ordinea: RED (fara implementare) -> GREEN (dupa implementare).
|
|
"""
|
|
|
|
import json
|
|
|
|
import pytest
|
|
|
|
from app.models import PrezentareIn
|
|
from app.web.integrare_examples import exemple
|
|
|
|
BASE_URL = "https://autopass.example.com"
|
|
ACCOUNT_ID = 7
|
|
|
|
RESULT = exemple(BASE_URL, ACCOUNT_ID)
|
|
|
|
LIMBAJE_OBLIGATORII = ["curl", "python", "php", "csharp", "node", "vfp_msxml", "vfp_winhttp"]
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Structura de baza
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_toate_limbajele_prezente():
|
|
"""Toate limbajele obligatorii trebuie sa fie chei in dictionar."""
|
|
for limbaj in LIMBAJE_OBLIGATORII:
|
|
assert limbaj in RESULT, f"lipseste limbajul: {limbaj}"
|
|
|
|
|
|
def test_ambele_canale_per_limbaj():
|
|
"""Fiecare limbaj are atat 'prezentari' cat si 'import'."""
|
|
for limbaj in LIMBAJE_OBLIGATORII:
|
|
assert "prezentari" in RESULT[limbaj], f"{limbaj} lipseste canalul 'prezentari'"
|
|
assert "import" in RESULT[limbaj], f"{limbaj} lipseste canalul 'import'"
|
|
assert isinstance(RESULT[limbaj]["prezentari"], str), f"{limbaj}.prezentari nu e string"
|
|
assert isinstance(RESULT[limbaj]["import"], str), f"{limbaj}.import nu e string"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# curl — prezentari
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_snippet_curl_prezentari_contine_endpoint_si_header():
|
|
"""Snippetul curl prezentari contine URL-ul corect si header-ul X-API-Key."""
|
|
snippet = RESULT["curl"]["prezentari"]
|
|
assert f"{BASE_URL}/v1/prezentari" in snippet, "lipseste endpoint-ul /v1/prezentari"
|
|
assert "X-API-Key" in snippet, "lipseste header-ul X-API-Key"
|
|
assert "rfak_..." in snippet, "placeholder-ul cheii trebuie sa fie rfak_..."
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# python — import (multipart)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_snippet_python_import_upload_multipart():
|
|
"""Snippetul python import foloseste multipart (files=) pe /v1/import."""
|
|
snippet = RESULT["python"]["import"]
|
|
assert f"{BASE_URL}/v1/import" in snippet, "lipseste endpoint-ul /v1/import"
|
|
assert "files=" in snippet or 'files =' in snippet, "lipseste multipart files="
|
|
assert "rfak_..." in snippet, "placeholder-ul cheii trebuie sa fie rfak_..."
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# VFP — doua dialecte distincte
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_vfp_msxml_si_winhttp_distincte():
|
|
"""vfp_msxml foloseste MSXML2.ServerXMLHTTP.6.0; vfp_winhttp foloseste WinHttp.WinHttpRequest.5.1."""
|
|
msxml_snippet = RESULT["vfp_msxml"]["prezentari"]
|
|
winhttp_snippet = RESULT["vfp_winhttp"]["prezentari"]
|
|
|
|
assert "MSXML2.ServerXMLHTTP.6.0" in msxml_snippet, (
|
|
"vfp_msxml trebuie sa foloseasca MSXML2.ServerXMLHTTP.6.0"
|
|
)
|
|
assert "WinHttp.WinHttpRequest.5.1" in winhttp_snippet, (
|
|
"vfp_winhttp trebuie sa foloseasca WinHttp.WinHttpRequest.5.1"
|
|
)
|
|
# Distincte — nu acelasi obiect COM in ambele
|
|
assert "MSXML2.ServerXMLHTTP.6.0" not in winhttp_snippet, (
|
|
"vfp_winhttp nu trebuie sa contina MSXML2 (dialect gresit)"
|
|
)
|
|
assert "WinHttp.WinHttpRequest.5.1" not in msxml_snippet, (
|
|
"vfp_msxml nu trebuie sa contina WinHttp (dialect gresit)"
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Drift-test schema — campuri obligatorii din model
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_payload_acopera_campurile_obligatorii_din_model():
|
|
"""Snippetul curl prezentari contine toate campurile obligatorii din PrezentareIn.
|
|
|
|
Deriva lista din model_fields pentru a fi rezistenta la schimbari de schema.
|
|
Campurile cu default (odometru_initial, obs, b64_image, sistem_reparat) nu sunt
|
|
obligatorii si nu trebuie sa cauzeze drift fals.
|
|
"""
|
|
obligatorii = {
|
|
camp
|
|
for camp, field in PrezentareIn.model_fields.items()
|
|
if field.is_required()
|
|
}
|
|
# Obligatorii asteptate conform spec: vin, nr_inmatriculare, data_prestatie,
|
|
# odometru_final, prestatii
|
|
snippet = RESULT["curl"]["prezentari"]
|
|
for camp in obligatorii:
|
|
assert camp in snippet, f"camp obligatoriu absent din snippet: {camp}"
|
|
|
|
|
|
def test_payload_nu_include_credentiale_rar():
|
|
"""rar_credentials e OPTIONAL: snippet-ul exemplu trimite doar cheia API + datele
|
|
prezentarii (creds-urile RAR se configureaza pe cont, nu in fiecare cerere)."""
|
|
for limbaj in LIMBAJE_OBLIGATORII:
|
|
snippet = RESULT[limbaj]["prezentari"]
|
|
assert "rar_credentials" not in snippet, (
|
|
f"{limbaj}.prezentari include rar_credentials — nu mai e necesar in payload"
|
|
)
|
|
|
|
|
|
def test_prestatii_in_snippet_are_cod():
|
|
"""Snippetul prezentari contine cod_prestatie sau cod_op_service in payload."""
|
|
for limbaj in LIMBAJE_OBLIGATORII:
|
|
snippet = RESULT[limbaj]["prezentari"]
|
|
are_cod = "cod_prestatie" in snippet or "cod_op_service" in snippet
|
|
assert are_cod, f"{limbaj}.prezentari lipseste cod_prestatie / cod_op_service"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Placeholder cheie
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_placeholder_cheie_nu_e_valoare_reala():
|
|
"""Toate snippet-urile cu autentificare contin literal 'rfak_', nu o cheie reala."""
|
|
for limbaj in LIMBAJE_OBLIGATORII:
|
|
for canal in ["prezentari", "import"]:
|
|
snippet = RESULT[limbaj][canal]
|
|
assert "rfak_" in snippet, (
|
|
f"{limbaj}.{canal} lipseste placeholder-ul rfak_ (cheie API)"
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# FIX-1 — C# si VFP: JSON compact (fara newline in string literal)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_csharp_payload_pe_o_singura_linie():
|
|
"""Snippetul C# prezentari: JSON-ul din var json = "..." e pe o singura linie.
|
|
|
|
Un JSON multi-linie intr-un string literal C# NU compileaza.
|
|
Verificam ca helper-ul compact produce un JSON fara newline
|
|
si ca acel JSON compact apare in snippetul C#.
|
|
"""
|
|
from app.web.integrare_examples import _payload_json_compact, _snippet_csharp_prezentari
|
|
|
|
compact = _payload_json_compact(ACCOUNT_ID)
|
|
assert "\n" not in compact, (
|
|
"_payload_json_compact produce newline — nu e compact"
|
|
)
|
|
|
|
snippet = _snippet_csharp_prezentari(BASE_URL, ACCOUNT_ID)
|
|
# JSON-ul compact (cu ghilimele escape-uite) trebuie sa fie substring in snippet
|
|
escaped = compact.replace('"', '\\"')
|
|
assert escaped in snippet, (
|
|
"JSON-ul compact (escaped) nu apare in snippetul C#; "
|
|
"probabil _snippet_csharp_prezentari inca foloseste _payload_json_str"
|
|
)
|
|
# Verificare directa: linia var json = "..." nu contine newline in interiorul ei
|
|
# In C# linia se termina cu "; (ghilimea de inchidere + punct-virgula)
|
|
for line in snippet.splitlines():
|
|
if 'var json = "' in line:
|
|
stripped = line.rstrip()
|
|
# Linia trebuie sa se inchida pe acelasi rand (cu "; sau ")
|
|
assert stripped.endswith('";') or stripped.endswith('"'), (
|
|
f"Linia 'var json = \"...' nu se termina pe acelasi rand: {line!r}"
|
|
)
|
|
break
|
|
else:
|
|
assert False, "Linia 'var json = \"...' nu a fost gasita in snippetul C#"
|
|
|
|
|
|
def test_vfp_payload_pe_o_singura_linie():
|
|
"""Snippetul VFP prezentari (ambele dialecte): cPayload = "..." e pe o linie.
|
|
|
|
Un string literal VFP multi-linie NU e valid.
|
|
Verificam ambele dialecte: vfp_msxml si vfp_winhttp.
|
|
"""
|
|
from app.web.integrare_examples import _payload_json_compact
|
|
|
|
compact = _payload_json_compact(ACCOUNT_ID)
|
|
# In VFP ghilimele se dubleaza
|
|
compact_vfp = compact.replace('"', '""')
|
|
|
|
for dialect in ("vfp_msxml", "vfp_winhttp"):
|
|
snippet = RESULT[dialect]["prezentari"]
|
|
|
|
# Payload-ul VFP (cu doubling) trebuie sa fie substring in snippet
|
|
assert compact_vfp in snippet, (
|
|
f"{dialect}: JSON-ul compact (cu \"\" doubling) nu apare in snippet; "
|
|
"probabil inca se foloseste _payload_json_str cu indent"
|
|
)
|
|
|
|
# Linia cPayload = "..." nu trebuie sa contina newline in interiorul valorii
|
|
for line in snippet.splitlines():
|
|
if 'cPayload = "' in line:
|
|
assert line.rstrip().endswith('"'), (
|
|
f"{dialect}: linia 'cPayload = \"...' nu se termina pe acelasi rand: {line!r}"
|
|
)
|
|
break
|
|
else:
|
|
assert False, f"{dialect}: linia 'cPayload = \"...' nu a fost gasita in snippet"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# FIX-2 — Node import: FormData/Blob globale, fara import din node:buffer
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_node_import_nu_foloseste_node_buffer():
|
|
"""Snippetul Node import nu importa din 'node:buffer' si foloseste FormData globala.
|
|
|
|
'node:buffer' nu exporta FormData — `new FormData()` ar arunca TypeError.
|
|
In Node 18+ FormData si Blob sunt globale.
|
|
"""
|
|
snippet = RESULT["node"]["import"]
|
|
|
|
assert "node:buffer" not in snippet, (
|
|
"Snippetul Node import contine 'node:buffer' — invalid, FormData nu e acolo"
|
|
)
|
|
assert "new FormData()" in snippet, (
|
|
"Snippetul Node import nu contine 'new FormData()' — FormData trebuie folosita global"
|
|
)
|