Files
rar-autopass/tests/test_integrare_api.py
Claude Agent 63b6cbc01d fix(securitate): hardening prod — headere, body-cap, non-root, backup
Findings security-review P1/P2 (2026-07-03):
- headere de securitate pe toate raspunsurile (nosniff, X-Frame-Options,
  Referrer-Policy, HSTS doar pe HTTPS) + teste
- body-cap global 10MB ca middleware ASGI pur (413 inainte de parserul
  multipart/JSON; verificarea per-endpoint ramane strat 2)
- imagine Docker non-root (uid 10001), port 8010 aliniat, loguri pe
  volumul /data
- fail-fast la boot cu rar_env=prod fara AUTOPASS_REQUIRE_API_KEY sau
  AUTOPASS_SESSION_SECRET
- compose: env-uri critice obligatorii (:?) ca api/worker sa nu diverga
  tacit; FORWARDED_ALLOW_IPS ca rate-limit-ul sa vada IP-ul real dupa
  Traefik
- signup fara PII in stdout: log_event in loc de print cu email (idem
  notify degradat)
- ratelimit: sterge cheile fara timestamp-uri valide (crestere monotona
  a memoriei pe IP-uri reale)
- backup criptat SQLite (backup online API, gpg AES256) + verificare
  restore + docs/backup.md

Suita completa verde: 1557 passed, 1 skipped (live).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 12:46:49 +00:00

222 lines
8.3 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")
# rar_env=prod declanseaza invarianta de startup (validate_prod_invariants,
# hardening 2026-07-03): cere si session_secret setat, altfel boot-ul refuza.
monkeypatch.setenv("AUTOPASS_SESSION_SECRET", "x" * 32)
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}"