mirror of
https://github.com/nethunterzist/trendyol-analiz
synced 2026-08-29 21:38:00 +00:00
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
142 lines
5.8 KiB
Python
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."
|
|
)
|