Files
trendyol-analiz/backend/tests/test_auth_routing.py
furkanyigit34 eba9709962 fix(auth): CiroMarket kategori 401 kalıcı düzeltme — router-based auth
Ne yaptık:
- PUBLIC_PATHS whitelist tamamen kaldırıldı
- protected_router = APIRouter(dependencies=[Depends(verify_api_key)]) eklendi
- 33 korunan route @protected_router.*'a taşındı
- 6 public route (/health, /, category-tree×4) @app.*'de kaldı
- verify_api_key sadeleşti: request.url.path artık kontrol edilmiyor
- test_auth_routing.py: regresyon guard — PUBLIC_PATHS geri gelirse CI fail eder

Neden yaptık:
- PUBLIC_PATHS her deploy/merge'de kayboluyordu → tekrarlayan 401
- Yapısal çözüm: ayrım kod mimarisinde, whitelist config'de değil
2026-07-22 00:10:22 +03:00

142 lines
5.8 KiB
Python

"""
Auth routing regression tests — Priority: P0 (Critical)
Ensures public routes never accidentally require auth, and protected routes
always enforce auth. Written after repeated incidents where the PUBLIC_PATHS
whitelist was lost during deploys, causing /api/category-tree to return 401
and breaking the CiroMarket category dropdown.
If these tests fail → do NOT deploy.
"""
import os
import inspect
import pathlib
import pytest
from fastapi import status
from fastapi.testclient import TestClient
# ─── Fixtures ───────────────────────────────────────────────────────────────
@pytest.fixture(scope="module")
def client_no_key():
"""TestClient with no API key header."""
os.environ.setdefault("API_KEY", "test-secret-key")
from main import app
with TestClient(app, raise_server_exceptions=False) as c:
yield c
@pytest.fixture(scope="module")
def client_with_key():
"""TestClient with a valid API key header."""
key = os.environ.setdefault("API_KEY", "test-secret-key")
from main import app
with TestClient(app, raise_server_exceptions=False) as c:
c.headers.update({"X-API-Key": key})
yield c
# ─── Public routes must never require auth ───────────────────────────────────
class TestPublicRoutesRequireNoAuth:
"""
These routes MUST return 2xx without any API key.
If any return 401, the CiroMarket frontend category dropdown breaks.
"""
def test_health_is_public(self, client_no_key):
response = client_no_key.get("/health")
assert response.status_code == status.HTTP_200_OK, (
"/health returned 401 — Docker will mark container as unhealthy"
)
def test_root_is_public(self, client_no_key):
response = client_no_key.get("/")
assert response.status_code == status.HTTP_200_OK
def test_category_tree_root_is_public(self, client_no_key):
response = client_no_key.get("/api/category-tree")
assert response.status_code != status.HTTP_401_UNAUTHORIZED, (
"/api/category-tree returned 401 — category dropdown will be stuck loading"
)
def test_category_tree_roots_is_public(self, client_no_key):
response = client_no_key.get("/api/category-tree/roots")
assert response.status_code != status.HTTP_401_UNAUTHORIZED
def test_category_tree_children_is_public(self, client_no_key):
response = client_no_key.get("/api/category-tree/1/children")
assert response.status_code != status.HTTP_401_UNAUTHORIZED
def test_category_tree_search_is_public(self, client_no_key):
response = client_no_key.get("/api/category-tree/search?q=test")
assert response.status_code != status.HTTP_401_UNAUTHORIZED
# ─── Protected routes must enforce auth ─────────────────────────────────────
class TestProtectedRoutesRequireAuth:
"""Protected routes MUST return 401 without an API key."""
def test_categories_requires_auth(self, client_no_key):
assert client_no_key.get("/categories").status_code == status.HTTP_401_UNAUTHORIZED
def test_categories_main_requires_auth(self, client_no_key):
assert client_no_key.get("/categories/main").status_code == status.HTTP_401_UNAUTHORIZED
def test_snapshots_requires_auth(self, client_no_key):
assert client_no_key.get("/snapshots").status_code == status.HTTP_401_UNAUTHORIZED
def test_reports_requires_auth(self, client_no_key):
assert client_no_key.get("/api/reports").status_code == status.HTTP_401_UNAUTHORIZED
def test_queue_active_requires_auth(self, client_no_key):
assert client_no_key.get("/api/queue/active").status_code == status.HTTP_401_UNAUTHORIZED
def test_product_lookup_requires_auth(self, client_no_key):
assert client_no_key.get(
"/api/product/lookup?url=https://trendyol.com/p/x-p-123456"
).status_code == status.HTTP_401_UNAUTHORIZED
def test_report_detail_requires_auth(self, client_no_key):
assert client_no_key.get("/api/reports/1").status_code == status.HTTP_401_UNAUTHORIZED
def test_sales_analytics_requires_auth(self, client_no_key):
assert client_no_key.get(
"/api/reports/1/sales-analytics"
).status_code == status.HTTP_401_UNAUTHORIZED
# ─── Structural guard: whitelist pattern must never come back ────────────────
class TestAuthStructure:
"""
Guards against regression to the whitelist (PUBLIC_PATHS) pattern.
If these fail, someone re-introduced the fragile approach that caused
the repeated CiroMarket breakage.
"""
def test_no_public_paths_whitelist_in_source(self):
source = pathlib.Path(__file__).parent.parent / "main.py"
text = source.read_text()
assert "PUBLIC_PATHS" not in text, (
"PUBLIC_PATHS whitelist was re-introduced in main.py. "
"Use APIRouter-based auth instead."
)
assert "PUBLIC_PATH_PREFIXES" not in text, (
"PUBLIC_PATH_PREFIXES whitelist was re-introduced in main.py. "
"Use APIRouter-based auth instead."
)
def test_verify_api_key_has_no_path_inspection(self):
"""verify_api_key must not inspect request.url.path."""
from main import verify_api_key
src = inspect.getsource(verify_api_key)
assert "url.path" not in src, (
"verify_api_key is inspecting request.url.path — whitelist regression."
)
assert "startswith" not in src, (
"verify_api_key contains startswith() — path-prefix whitelist regression."
)