""" 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." )