mirror of
https://github.com/nethunterzist/trendyol-analiz
synced 2026-08-30 05:48:00 +00:00
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
This commit is contained in:
@@ -63,6 +63,28 @@ def client(test_db):
|
||||
app.dependency_overrides.clear()
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def authed_client(test_db):
|
||||
"""Test client with valid API key — use for protected routes."""
|
||||
test_key = "test-api-key-for-pytest"
|
||||
os.environ["API_KEY"] = test_key
|
||||
|
||||
def override_get_db():
|
||||
try:
|
||||
yield test_db
|
||||
finally:
|
||||
pass
|
||||
|
||||
app.dependency_overrides[get_db] = override_get_db
|
||||
|
||||
with TestClient(app) as test_client:
|
||||
test_client.headers.update({"X-API-Key": test_key})
|
||||
yield test_client
|
||||
|
||||
app.dependency_overrides.clear()
|
||||
os.environ.pop("API_KEY", None)
|
||||
|
||||
|
||||
# Sample test data fixtures
|
||||
@pytest.fixture
|
||||
def sample_categories():
|
||||
|
||||
@@ -10,20 +10,20 @@ class TestCategoryEndpoints:
|
||||
"""Test category CRUD endpoints"""
|
||||
|
||||
def test_root_endpoint(self, client):
|
||||
"""Test root endpoint returns welcome message"""
|
||||
"""Test root endpoint returns welcome message (public route — no auth needed)"""
|
||||
response = client.get("/")
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
data = response.json()
|
||||
assert "message" in data
|
||||
assert "version" in data
|
||||
|
||||
def test_get_categories_empty(self, client):
|
||||
def test_get_categories_empty(self, authed_client):
|
||||
"""Test getting categories when database is empty"""
|
||||
response = client.get("/categories")
|
||||
response = authed_client.get("/categories")
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert response.json() == []
|
||||
|
||||
def test_create_category_success(self, client):
|
||||
def test_create_category_success(self, authed_client):
|
||||
"""Test creating a new category successfully"""
|
||||
category_data = {
|
||||
"name": "Elektronik",
|
||||
@@ -32,7 +32,7 @@ class TestCategoryEndpoints:
|
||||
"trendyol_url": "https://www.trendyol.com/elektronik"
|
||||
}
|
||||
|
||||
response = client.post("/categories", json=category_data)
|
||||
response = authed_client.post("/categories", json=category_data)
|
||||
assert response.status_code == status.HTTP_201_CREATED
|
||||
|
||||
data = response.json()
|
||||
@@ -43,97 +43,86 @@ class TestCategoryEndpoints:
|
||||
assert "created_at" in data
|
||||
assert data["is_active"] is True
|
||||
|
||||
def test_create_category_minimal_data(self, client):
|
||||
def test_create_category_minimal_data(self, authed_client):
|
||||
"""Test creating category with minimal required data"""
|
||||
category_data = {
|
||||
"name": "Test Category",
|
||||
"parent_id": None
|
||||
}
|
||||
|
||||
response = client.post("/categories", json=category_data)
|
||||
response = authed_client.post("/categories", json=category_data)
|
||||
assert response.status_code == status.HTTP_201_CREATED
|
||||
|
||||
data = response.json()
|
||||
assert data["name"] == "Test Category"
|
||||
assert data["parent_id"] is None
|
||||
|
||||
def test_get_category_by_id_success(self, client):
|
||||
def test_get_category_by_id_success(self, authed_client):
|
||||
"""Test getting single category by ID"""
|
||||
# Create category
|
||||
category_data = {"name": "Test Category", "parent_id": None}
|
||||
create_response = client.post("/categories", json=category_data)
|
||||
create_response = authed_client.post("/categories", json=category_data)
|
||||
category_id = create_response.json()["id"]
|
||||
|
||||
# Get category
|
||||
response = client.get(f"/categories/{category_id}")
|
||||
response = authed_client.get(f"/categories/{category_id}")
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert response.json()["name"] == "Test Category"
|
||||
|
||||
def test_get_category_not_found(self, client):
|
||||
def test_get_category_not_found(self, authed_client):
|
||||
"""Test getting non-existent category returns 404"""
|
||||
response = client.get("/categories/99999")
|
||||
response = authed_client.get("/categories/99999")
|
||||
assert response.status_code == status.HTTP_404_NOT_FOUND
|
||||
|
||||
def test_update_category_success(self, client):
|
||||
def test_update_category_success(self, authed_client):
|
||||
"""Test updating category name"""
|
||||
# Create category
|
||||
category_data = {"name": "Old Name", "parent_id": None}
|
||||
create_response = client.post("/categories", json=category_data)
|
||||
create_response = authed_client.post("/categories", json=category_data)
|
||||
category_id = create_response.json()["id"]
|
||||
|
||||
# Update category
|
||||
update_data = {"name": "New Name"}
|
||||
response = client.put(f"/categories/{category_id}", json=update_data)
|
||||
response = authed_client.put(f"/categories/{category_id}", json=update_data)
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert response.json()["name"] == "New Name"
|
||||
|
||||
# Verify update persisted
|
||||
get_response = client.get(f"/categories/{category_id}")
|
||||
get_response = authed_client.get(f"/categories/{category_id}")
|
||||
assert get_response.json()["name"] == "New Name"
|
||||
|
||||
def test_update_category_not_found(self, client):
|
||||
def test_update_category_not_found(self, authed_client):
|
||||
"""Test updating non-existent category returns 404"""
|
||||
update_data = {"name": "New Name"}
|
||||
response = client.put("/categories/99999", json=update_data)
|
||||
response = authed_client.put("/categories/99999", json=update_data)
|
||||
assert response.status_code == status.HTTP_404_NOT_FOUND
|
||||
|
||||
def test_delete_category_success(self, client):
|
||||
def test_delete_category_success(self, authed_client):
|
||||
"""Test deleting category"""
|
||||
# Create category
|
||||
category_data = {"name": "To Delete", "parent_id": None}
|
||||
create_response = client.post("/categories", json=category_data)
|
||||
create_response = authed_client.post("/categories", json=category_data)
|
||||
category_id = create_response.json()["id"]
|
||||
|
||||
# Delete category
|
||||
response = client.delete(f"/categories/{category_id}")
|
||||
response = authed_client.delete(f"/categories/{category_id}")
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
|
||||
# Verify deletion
|
||||
get_response = client.get(f"/categories/{category_id}")
|
||||
get_response = authed_client.get(f"/categories/{category_id}")
|
||||
assert get_response.status_code == status.HTTP_404_NOT_FOUND
|
||||
|
||||
def test_delete_category_not_found(self, client):
|
||||
def test_delete_category_not_found(self, authed_client):
|
||||
"""Test deleting non-existent category returns 404"""
|
||||
response = client.delete("/categories/99999")
|
||||
response = authed_client.delete("/categories/99999")
|
||||
assert response.status_code == status.HTTP_404_NOT_FOUND
|
||||
|
||||
def test_get_main_categories(self, client):
|
||||
def test_get_main_categories(self, authed_client):
|
||||
"""Test getting only main categories (parent_id = null)"""
|
||||
# Create main categories
|
||||
client.post("/categories", json={"name": "Elektronik", "parent_id": None})
|
||||
client.post("/categories", json={"name": "Moda", "parent_id": None})
|
||||
authed_client.post("/categories", json={"name": "Elektronik", "parent_id": None})
|
||||
authed_client.post("/categories", json={"name": "Moda", "parent_id": None})
|
||||
|
||||
# Create parent and subcategory
|
||||
parent_response = client.post("/categories", json={"name": "Parent", "parent_id": None})
|
||||
parent_response = authed_client.post("/categories", json={"name": "Parent", "parent_id": None})
|
||||
parent_id = parent_response.json()["id"]
|
||||
client.post("/categories", json={"name": "Subcategory", "parent_id": parent_id})
|
||||
authed_client.post("/categories", json={"name": "Subcategory", "parent_id": parent_id})
|
||||
|
||||
# Get main categories
|
||||
response = client.get("/categories/main")
|
||||
response = authed_client.get("/categories/main")
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
|
||||
data = response.json()
|
||||
assert len(data) == 3 # Only main categories
|
||||
assert len(data) == 3
|
||||
assert all(cat["parent_id"] is None for cat in data)
|
||||
|
||||
names = [cat["name"] for cat in data]
|
||||
@@ -142,19 +131,16 @@ class TestCategoryEndpoints:
|
||||
assert "Parent" in names
|
||||
assert "Subcategory" not in names
|
||||
|
||||
def test_get_category_children(self, client):
|
||||
def test_get_category_children(self, authed_client):
|
||||
"""Test getting category children"""
|
||||
# Create parent category
|
||||
parent_response = client.post("/categories", json={"name": "Elektronik", "parent_id": None})
|
||||
parent_response = authed_client.post("/categories", json={"name": "Elektronik", "parent_id": None})
|
||||
parent_id = parent_response.json()["id"]
|
||||
|
||||
# Create children
|
||||
client.post("/categories", json={"name": "Telefon", "parent_id": parent_id})
|
||||
client.post("/categories", json={"name": "Laptop", "parent_id": parent_id})
|
||||
client.post("/categories", json={"name": "Tablet", "parent_id": parent_id})
|
||||
authed_client.post("/categories", json={"name": "Telefon", "parent_id": parent_id})
|
||||
authed_client.post("/categories", json={"name": "Laptop", "parent_id": parent_id})
|
||||
authed_client.post("/categories", json={"name": "Tablet", "parent_id": parent_id})
|
||||
|
||||
# Get children
|
||||
response = client.get(f"/categories/{parent_id}/children")
|
||||
response = authed_client.get(f"/categories/{parent_id}/children")
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
|
||||
data = response.json()
|
||||
@@ -166,140 +152,114 @@ class TestCategoryEndpoints:
|
||||
assert "Laptop" in names
|
||||
assert "Tablet" in names
|
||||
|
||||
def test_get_category_children_no_children(self, client):
|
||||
def test_get_category_children_no_children(self, authed_client):
|
||||
"""Test getting children for category with no children"""
|
||||
# Create category without children
|
||||
category_response = client.post("/categories", json={"name": "Elektronik", "parent_id": None})
|
||||
category_response = authed_client.post("/categories", json={"name": "Elektronik", "parent_id": None})
|
||||
category_id = category_response.json()["id"]
|
||||
|
||||
# Get children (should be empty)
|
||||
response = client.get(f"/categories/{category_id}/children")
|
||||
response = authed_client.get(f"/categories/{category_id}/children")
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert response.json() == []
|
||||
|
||||
def test_pagination_skip_limit(self, client):
|
||||
def test_pagination_skip_limit(self, authed_client):
|
||||
"""Test category pagination with skip and limit"""
|
||||
# Create 10 categories
|
||||
for i in range(10):
|
||||
client.post("/categories", json={"name": f"Category {i}", "parent_id": None})
|
||||
authed_client.post("/categories", json={"name": f"Category {i}", "parent_id": None})
|
||||
|
||||
# Test first page
|
||||
response = client.get("/categories?skip=0&limit=5")
|
||||
response = authed_client.get("/categories?skip=0&limit=5")
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert len(response.json()) == 5
|
||||
|
||||
# Test second page
|
||||
response = client.get("/categories?skip=5&limit=5")
|
||||
response = authed_client.get("/categories?skip=5&limit=5")
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert len(response.json()) == 5
|
||||
|
||||
# Test beyond available data
|
||||
response = client.get("/categories?skip=10&limit=5")
|
||||
response = authed_client.get("/categories?skip=10&limit=5")
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert len(response.json()) == 0
|
||||
|
||||
def test_pagination_default_limit(self, client):
|
||||
def test_pagination_default_limit(self, authed_client):
|
||||
"""Test default pagination limit is 200"""
|
||||
# Create 250 categories
|
||||
for i in range(250):
|
||||
client.post("/categories", json={"name": f"Category {i}", "parent_id": None})
|
||||
authed_client.post("/categories", json={"name": f"Category {i}", "parent_id": None})
|
||||
|
||||
# Get without limit (should default to 200)
|
||||
response = client.get("/categories")
|
||||
response = authed_client.get("/categories")
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert len(response.json()) == 200
|
||||
|
||||
def test_cors_headers_present(self, client):
|
||||
def test_cors_headers_present(self, authed_client):
|
||||
"""Test that CORS headers are present in response"""
|
||||
response = client.get("/categories")
|
||||
|
||||
# Check for CORS headers (lowercase keys in response.headers)
|
||||
response = authed_client.get("/categories")
|
||||
assert "access-control-allow-origin" in response.headers
|
||||
|
||||
def test_hierarchical_category_structure(self, client):
|
||||
def test_hierarchical_category_structure(self, authed_client):
|
||||
"""Test creating multi-level category hierarchy"""
|
||||
# Level 1
|
||||
level1_response = client.post("/categories", json={"name": "Elektronik", "parent_id": None})
|
||||
level1_response = authed_client.post("/categories", json={"name": "Elektronik", "parent_id": None})
|
||||
level1_id = level1_response.json()["id"]
|
||||
|
||||
# Level 2
|
||||
level2_response = client.post("/categories", json={"name": "Telefon", "parent_id": level1_id})
|
||||
level2_response = authed_client.post("/categories", json={"name": "Telefon", "parent_id": level1_id})
|
||||
level2_id = level2_response.json()["id"]
|
||||
|
||||
# Level 3
|
||||
level3_response = client.post("/categories", json={"name": "iPhone", "parent_id": level2_id})
|
||||
level3_response = authed_client.post("/categories", json={"name": "iPhone", "parent_id": level2_id})
|
||||
level3_id = level3_response.json()["id"]
|
||||
|
||||
# Verify level 3 category
|
||||
response = client.get(f"/categories/{level3_id}")
|
||||
response = authed_client.get(f"/categories/{level3_id}")
|
||||
data = response.json()
|
||||
assert data["name"] == "iPhone"
|
||||
assert data["parent_id"] == level2_id
|
||||
|
||||
# Verify level 2 has children
|
||||
response = client.get(f"/categories/{level2_id}/children")
|
||||
response = authed_client.get(f"/categories/{level2_id}/children")
|
||||
assert len(response.json()) == 1
|
||||
assert response.json()[0]["name"] == "iPhone"
|
||||
|
||||
def test_get_category_products_not_implemented(self, client):
|
||||
def test_get_category_products_not_implemented(self, authed_client):
|
||||
"""Test get category products endpoint (if implemented)"""
|
||||
# Create category
|
||||
category_response = client.post("/categories", json={"name": "Elektronik", "parent_id": None})
|
||||
category_response = authed_client.post("/categories", json={"name": "Elektronik", "parent_id": None})
|
||||
category_id = category_response.json()["id"]
|
||||
|
||||
# Try to get products (endpoint exists but may not have data)
|
||||
response = client.get(f"/categories/{category_id}/products")
|
||||
|
||||
# Check if endpoint exists (should return 200 or 404)
|
||||
response = authed_client.get(f"/categories/{category_id}/products")
|
||||
assert response.status_code in [status.HTTP_200_OK, status.HTTP_404_NOT_FOUND]
|
||||
|
||||
def test_create_category_with_special_characters(self, client):
|
||||
def test_create_category_with_special_characters(self, authed_client):
|
||||
"""Test creating category with Turkish characters"""
|
||||
category_data = {
|
||||
"name": "Giyim & Aksesuar - Çanta/Çorap",
|
||||
"parent_id": None
|
||||
}
|
||||
|
||||
response = client.post("/categories", json=category_data)
|
||||
response = authed_client.post("/categories", json=category_data)
|
||||
assert response.status_code == status.HTTP_201_CREATED
|
||||
|
||||
data = response.json()
|
||||
assert data["name"] == "Giyim & Aksesuar - Çanta/Çorap"
|
||||
|
||||
def test_create_category_duplicate_name_allowed(self, client):
|
||||
def test_create_category_duplicate_name_allowed(self, authed_client):
|
||||
"""Test that duplicate category names are allowed (no unique constraint)"""
|
||||
category_data = {"name": "Duplicate Name", "parent_id": None}
|
||||
|
||||
# Create first category
|
||||
response1 = client.post("/categories", json=category_data)
|
||||
response1 = authed_client.post("/categories", json=category_data)
|
||||
assert response1.status_code == status.HTTP_201_CREATED
|
||||
|
||||
# Create second category with same name
|
||||
response2 = client.post("/categories", json=category_data)
|
||||
response2 = authed_client.post("/categories", json=category_data)
|
||||
assert response2.status_code == status.HTTP_201_CREATED
|
||||
|
||||
# Verify both exist with different IDs
|
||||
id1 = response1.json()["id"]
|
||||
id2 = response2.json()["id"]
|
||||
assert id1 != id2
|
||||
|
||||
def test_update_category_parent_id(self, client):
|
||||
def test_update_category_parent_id(self, authed_client):
|
||||
"""Test updating category parent_id (moving in hierarchy)"""
|
||||
# Create categories
|
||||
parent1_response = client.post("/categories", json={"name": "Parent 1", "parent_id": None})
|
||||
parent1_response = authed_client.post("/categories", json={"name": "Parent 1", "parent_id": None})
|
||||
parent1_id = parent1_response.json()["id"]
|
||||
|
||||
parent2_response = client.post("/categories", json={"name": "Parent 2", "parent_id": None})
|
||||
parent2_response = authed_client.post("/categories", json={"name": "Parent 2", "parent_id": None})
|
||||
parent2_id = parent2_response.json()["id"]
|
||||
|
||||
child_response = client.post("/categories", json={"name": "Child", "parent_id": parent1_id})
|
||||
child_response = authed_client.post("/categories", json={"name": "Child", "parent_id": parent1_id})
|
||||
child_id = child_response.json()["id"]
|
||||
|
||||
# Move child from parent1 to parent2
|
||||
update_data = {"parent_id": parent2_id}
|
||||
response = client.put(f"/categories/{child_id}", json=update_data)
|
||||
response = authed_client.put(f"/categories/{child_id}", json=update_data)
|
||||
|
||||
# This may or may not be supported by the API - test documents behavior
|
||||
# If supported, child should now be under parent2
|
||||
if response.status_code == status.HTTP_200_OK:
|
||||
assert response.json()["parent_id"] == parent2_id
|
||||
|
||||
141
backend/tests/test_auth_routing.py
Normal file
141
backend/tests/test_auth_routing.py
Normal file
@@ -0,0 +1,141 @@
|
||||
"""
|
||||
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."
|
||||
)
|
||||
Reference in New Issue
Block a user