Toate citirile pe coloana legacy accounts.rar_creds_enc mutate pe sloturile per-env (rar_creds_test_enc/rar_creds_prod_enc): worker fallback+keepalive, are_creds (web) si are_creds_rar (integrare, +are_creds_test/_prod), write-back API la reactivare, purjare la stergere cont, _get_acasa_context/_fetch_cont_env_state. Contract API (aditiv): POST /v1/conturi/rar-creds primeste rar_target optional (test/prod), scrie in slotul corect + activeaza mediul; DELETE primeste ?env (sterge un slot sau ambele). Documentat in docs/api-rar-contract.md. DROP cu garda in db.py (schema.sql fara coloana pe DB fresh): - 6a: eliminat ADD COLUMN rar_creds_enc (fara ping-pong re-ADD dupa DROP) - 6b: try/except fail-safe (nu crapa boot-ul) + garda sqlite_version >= 3.35 - 6c: re-backfill old->new imediat inainte de assert (ancora globala) - garda orfane: DROP anulat daca vreun creds legacy nu a aterizat in slot per-env - backup criptat accounts_rar_creds_enc_backup inainte de DROP - 6d: verificare prin PRAGMA table_info (NU grep — submissions are aceeasi coloana) Garda one-way, idempotenta la boot repetat (verificat). submissions.rar_creds_enc ramane neatinsa. tests/test_retragere_creds_enc.py: niciun read pe coloana veche, conturi rar-creds env-aware, are_creds per-env, DROP blocat de garda la lipsa copiere. 9 teste existente actualizate pe sloturi per-env. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
219 lines
8.1 KiB
Python
219 lines
8.1 KiB
Python
"""Teste US-001: endpoint-uri de integrare (GET /v1/ping + export Postman).
|
|
|
|
TDD — toate testele RED inainte de implementare.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import os
|
|
import tempfile
|
|
|
|
import pytest
|
|
from fastapi.testclient import TestClient
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
# Fixture client izolat #
|
|
# --------------------------------------------------------------------------- #
|
|
|
|
@pytest.fixture()
|
|
def client(monkeypatch):
|
|
"""Client FastAPI cu DB temporara izolata, require_api_key=False (dev)."""
|
|
tmp = tempfile.mkdtemp()
|
|
monkeypatch.setenv("AUTOPASS_DB_PATH", os.path.join(tmp, "t.db"))
|
|
monkeypatch.setenv("AUTOPASS_REQUIRE_API_KEY", "false")
|
|
monkeypatch.setenv("AUTOPASS_RAR_ENV", "test")
|
|
from app.config import get_settings
|
|
get_settings.cache_clear()
|
|
from app.main import app
|
|
with TestClient(app) as c:
|
|
yield c
|
|
get_settings.cache_clear()
|
|
|
|
|
|
@pytest.fixture()
|
|
def client_prod(monkeypatch):
|
|
"""Client cu require_api_key=True (mod prod)."""
|
|
tmp = tempfile.mkdtemp()
|
|
monkeypatch.setenv("AUTOPASS_DB_PATH", os.path.join(tmp, "t.db"))
|
|
monkeypatch.setenv("AUTOPASS_REQUIRE_API_KEY", "true")
|
|
monkeypatch.setenv("AUTOPASS_RAR_ENV", "prod")
|
|
from app.config import get_settings
|
|
get_settings.cache_clear()
|
|
from app.main import app
|
|
with TestClient(app) as c:
|
|
yield c
|
|
get_settings.cache_clear()
|
|
|
|
|
|
def _creeaza_cheie(monkeypatch) -> str:
|
|
"""Seed cont id=1 + creeaza cheie API; intoarce cheia in clar."""
|
|
from app.db import get_connection
|
|
from app.auth import create_api_key
|
|
conn = get_connection()
|
|
try:
|
|
cheie = create_api_key(conn, 1)
|
|
conn.commit()
|
|
finally:
|
|
conn.close()
|
|
return cheie
|
|
|
|
|
|
def _seteaza_rar_creds(monkeypatch=None) -> None:
|
|
"""Seteaza creds RAR pe contul id=1 (slotul per-env, US-013)."""
|
|
from app.db import get_connection
|
|
from app.crypto import encrypt_creds
|
|
conn = get_connection()
|
|
try:
|
|
enc = encrypt_creds({"email": "test@rar.ro", "password": "secret"})
|
|
# US-013: scrie in slotul per-env (test = ancora globala in teste)
|
|
conn.execute(
|
|
"UPDATE accounts SET rar_creds_test_enc=?, rar_test_enabled=1 WHERE id=1", (enc,)
|
|
)
|
|
conn.commit()
|
|
finally:
|
|
conn.close()
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
# Teste ping #
|
|
# --------------------------------------------------------------------------- #
|
|
|
|
def test_ping_cu_cheie_valida_200(client, monkeypatch):
|
|
"""GET /v1/ping cu X-API-Key valida -> 200 cu campurile cerute."""
|
|
cheie = _creeaza_cheie(monkeypatch)
|
|
r = client.get("/v1/ping", headers={"X-API-Key": cheie})
|
|
assert r.status_code == 200, r.text
|
|
body = r.json()
|
|
assert "account_id" in body
|
|
assert "mediu" in body
|
|
assert "autentificat_cu_cheie" in body
|
|
assert "are_creds_rar" in body
|
|
assert "ts" in body
|
|
assert body["autentificat_cu_cheie"] is True
|
|
assert body["mediu"] == "test"
|
|
|
|
|
|
def test_ping_cu_bearer_valid_200(client, monkeypatch):
|
|
"""GET /v1/ping cu Authorization: Bearer valida -> 200, autentificat_cu_cheie=True."""
|
|
cheie = _creeaza_cheie(monkeypatch)
|
|
r = client.get("/v1/ping", headers={"Authorization": f"Bearer {cheie}"})
|
|
assert r.status_code == 200, r.text
|
|
body = r.json()
|
|
assert body["autentificat_cu_cheie"] is True
|
|
assert body["account_id"] == 1
|
|
|
|
|
|
def test_ping_fara_cheie_dev_cont_implicit(client, monkeypatch):
|
|
"""Fara cheie, require_api_key=False -> cont 1, autentificat_cu_cheie=False."""
|
|
r = client.get("/v1/ping")
|
|
assert r.status_code == 200, r.text
|
|
body = r.json()
|
|
assert body["account_id"] == 1
|
|
assert body["autentificat_cu_cheie"] is False
|
|
|
|
|
|
def test_ping_x_api_key_gol_in_dev_cont_implicit(client, monkeypatch):
|
|
"""X-API-Key cu doar spatii = lipsa cheie -> cont 1, autentificat_cu_cheie=False in dev."""
|
|
r = client.get("/v1/ping", headers={"X-API-Key": " "})
|
|
assert r.status_code == 200, r.text
|
|
body = r.json()
|
|
assert body["account_id"] == 1
|
|
assert body["autentificat_cu_cheie"] is False
|
|
|
|
|
|
def test_ping_are_creds_rar_reflecta_contul(client, monkeypatch):
|
|
"""Cont fara creds -> are_creds_rar=False; dupa setare -> True."""
|
|
# Fara creds
|
|
r1 = client.get("/v1/ping")
|
|
assert r1.status_code == 200
|
|
assert r1.json()["are_creds_rar"] is False
|
|
|
|
# Seteaza creds pe cont 1
|
|
_seteaza_rar_creds()
|
|
|
|
r2 = client.get("/v1/ping")
|
|
assert r2.status_code == 200
|
|
assert r2.json()["are_creds_rar"] is True
|
|
|
|
|
|
def test_ping_cheie_invalida_401(client, monkeypatch):
|
|
"""Cheie invalida -> 401, indiferent de require_api_key."""
|
|
r = client.get("/v1/ping", headers={"X-API-Key": "rfak_cheie_falsa_xxxxxxxx"})
|
|
assert r.status_code == 401
|
|
|
|
|
|
def test_ping_prod_fara_cheie_401(client_prod, monkeypatch):
|
|
"""require_api_key=True, fara cheie -> 401."""
|
|
r = client_prod.get("/v1/ping")
|
|
assert r.status_code == 401
|
|
|
|
|
|
def test_ruta_ping_inregistrata_o_singura_data(client, monkeypatch):
|
|
"""Ruta /v1/ping trebuie sa apara exact o data in app.routes."""
|
|
from app.main import app
|
|
rute_ping = [r for r in app.routes if hasattr(r, "path") and r.path == "/v1/ping"]
|
|
assert len(rute_ping) == 1, f"Asteptat 1 ruta /v1/ping, gasit: {len(rute_ping)}"
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
# Teste export Postman #
|
|
# --------------------------------------------------------------------------- #
|
|
|
|
def test_postman_export_json_valid(client, monkeypatch):
|
|
"""GET /v1/integrare/postman.json -> 200, Content-Type JSON, structura Postman v2.1.0."""
|
|
r = client.get("/v1/integrare/postman.json")
|
|
assert r.status_code == 200, r.text
|
|
assert "application/json" in r.headers.get("content-type", "")
|
|
body = r.json()
|
|
assert "info" in body
|
|
assert "v2.1.0" in body["info"].get("schema", ""), f"Schema gresita: {body['info'].get('schema')}"
|
|
|
|
|
|
def test_postman_contine_exact_trei_requesturi(client, monkeypatch):
|
|
"""Colectia Postman trebuie sa contina exact 3 requesturi cu headers si url corecte."""
|
|
r = client.get("/v1/integrare/postman.json")
|
|
assert r.status_code == 200
|
|
body = r.json()
|
|
items = body.get("item", [])
|
|
assert len(items) == 3, f"Asteptat 3 requesturi, gasit: {len(items)}"
|
|
|
|
for item in items:
|
|
req = item.get("request", {})
|
|
# Fiecare trebuie sa aiba header X-API-Key cu valoarea {{api_key}}
|
|
headers = req.get("header", [])
|
|
cheie_header = [h for h in headers if h.get("key") == "X-API-Key"]
|
|
assert len(cheie_header) == 1, f"Header X-API-Key lipsa in request '{item.get('name')}'"
|
|
assert cheie_header[0].get("value") == "{{api_key}}", \
|
|
f"Valoare header gresita: {cheie_header[0].get('value')}"
|
|
|
|
# URL-ul trebuie sa contina {{base_url}}
|
|
url = req.get("url", {})
|
|
url_raw = url.get("raw", "") if isinstance(url, dict) else str(url)
|
|
assert "{{base_url}}" in url_raw, \
|
|
f"{{{{base_url}}}} lipsa in url pentru '{item.get('name')}': {url_raw}"
|
|
|
|
|
|
def test_postman_nu_deriva_din_app_routes(client, monkeypatch):
|
|
"""Colectia Postman contine EXACT cele 3 rute allowlist, nu mai mult."""
|
|
r = client.get("/v1/integrare/postman.json")
|
|
assert r.status_code == 200
|
|
body = r.json()
|
|
items = body.get("item", [])
|
|
|
|
# Extrage URL-urile / path-urile din colectie
|
|
cai_expuse = set()
|
|
for item in items:
|
|
req = item.get("request", {})
|
|
url = req.get("url", {})
|
|
url_raw = url.get("raw", "") if isinstance(url, dict) else str(url)
|
|
# Normalizeaza: scoate {{base_url}} si parametrii query
|
|
cale = url_raw.replace("{{base_url}}", "").split("?")[0]
|
|
cai_expuse.add(cale)
|
|
|
|
# Allowlist: exact acestea 3
|
|
allowlist = {"/v1/prezentari", "/v1/import", "/v1/ping"}
|
|
assert cai_expuse == allowlist, \
|
|
f"Colectia expune cai neasteptate: {cai_expuse - allowlist} sau lipsesc: {allowlist - cai_expuse}"
|