Files
library-desk/tests/test_required_user.py
T
jpmschweitzerandClaude Fable 5 84e9185371 feat!: require explicit user on every tenant-data endpoint
Remove the implicit jpmschweitzer default tenant (DEFAULT_USER) from
src/core/multi_tenancy.py and every endpoint and request model that
inherited it (~40 endpoints across /query, /wiki, /vector, /graph,
/ingest, /volatile, /documents, /stats, /rag).

- Add validate_required_user() + RequiredUser pydantic type in
  multi_tenancy and a shared require_user FastAPI dependency
  (RequiredUserQuery) that rejects missing, empty, and whitespace-only
  users with 422, following the /maintenance/* pattern.
- Wiki page create / smart-create / dossier request models now require
  user (no fallback in wiki_service).
- /maintenance/cleanup/test-data derives the tenant from the page path
  instead of using the production tenant collection.
- Wiki.js change listener skips changes when no tenant user can be
  derived from the notification email instead of defaulting to the
  production tenant.
- Consolidation service internal helpers no longer default to the
  production tenant.
- Tool catalog marks user as required with honest descriptions.
- OpenAPI descriptions updated honestly; CHANGELOG notes that callers
  (tatlock, Scheduler ingest tasks) must now send explicit user.
- Offline tests: 422 coverage for query/body endpoints, required-user
  validator tests; updated legacy tests that assumed a default tenant.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 11:06:11 +02:00

118 lines
4.3 KiB
Python

"""
Offline unit tests: every tenant-data endpoint must REQUIRE an explicit user.
A request without a user (query param or body field) must be rejected with
422 before any service is touched. Empty/whitespace users are also rejected.
No external services are contacted: validation failures short-circuit the
request before the endpoint body executes.
"""
import pytest
from fastapi.testclient import TestClient
from src.main import app
from src.core.dependencies import verify_api_key
@pytest.fixture(scope="module")
def client():
"""TestClient with API-key auth stubbed out (no lifespan startup)."""
app.dependency_overrides[verify_api_key] = lambda: "test-key"
try:
# No context manager: startup/lifespan events are NOT triggered,
# so no connections to external services are attempted.
yield TestClient(app)
finally:
app.dependency_overrides.pop(verify_api_key, None)
QUERY_PARAM_ENDPOINTS = [
("GET", "/stats"),
("POST", "/query/semantic?query=test"),
("POST", "/query/graph?query=MATCH%20(n)%20RETURN%20n"),
("GET", "/wiki/pages"),
("GET", "/wiki/pages/1"),
("PUT", "/wiki/pages/1"),
("DELETE", "/wiki/pages/1"),
("GET", "/wiki/search?q=test"),
("GET", "/wiki/dossiers"),
("POST", "/vector/update-from-page/1"),
("DELETE", "/vector/pages/1"),
("GET", "/graph/nodes"),
("POST", "/graph/update-from-page/1"),
("POST", "/graph/generate-entity-pages"),
("POST", "/ingest/all"),
("GET", "/volatile/stats"),
("GET", "/volatile/search?q=test"),
("POST", "/volatile/store?namespace=weather&key=test"),
("GET", "/volatile/weather/rotterdam"),
("DELETE", "/volatile/weather/rotterdam"),
("POST", "/documents/webhook-simple?doc_url=http://x/documents/1/"),
]
@pytest.mark.unit
class TestUserQueryParamRequired:
"""Endpoints with a user query parameter must 422 without it."""
@pytest.mark.parametrize("method,path", QUERY_PARAM_ENDPOINTS)
def test_missing_user_is_422(self, client, method, path):
response = client.request(method, path, json={})
assert response.status_code == 422, (
f"{method} {path} returned {response.status_code}, expected 422"
)
@pytest.mark.parametrize("blank", ["", " ", "%20%20"])
def test_blank_user_is_422(self, client, blank):
response = client.get(f"/wiki/pages?user={blank}")
assert response.status_code == 422
def test_hybrid_query_missing_user_is_422(self, client):
response = client.post("/query/hybrid", json={"query": "test"})
assert response.status_code == 422
def test_hybrid_query_whitespace_user_is_422(self, client):
response = client.post("/query/hybrid?user=%20", json={"query": "test"})
assert response.status_code == 422
@pytest.mark.unit
class TestUserBodyFieldRequired:
"""Request models with a user field must reject missing/blank values."""
def test_ingest_page_missing_user_is_422(self, client):
response = client.post("/ingest/page", json={"page_id": 1})
assert response.status_code == 422
def test_ingest_page_blank_user_is_422(self, client):
response = client.post("/ingest/page", json={"page_id": 1, "user": " "})
assert response.status_code == 422
def test_ingest_batch_missing_user_is_422(self, client):
response = client.post("/ingest/batch", json={"page_ids": [1]})
assert response.status_code == 422
def test_wiki_create_page_missing_user_is_422(self, client):
response = client.post(
"/wiki/pages",
json={"title": "T", "path": "/t", "content": "c"},
)
assert response.status_code == 422
def test_wiki_smart_create_missing_user_is_422(self, client):
response = client.post("/wiki/pages/smart-create", json={"topic": "T"})
assert response.status_code == 422
def test_vector_search_missing_user_is_422(self, client):
response = client.post("/vector/search", json={"query": "test"})
assert response.status_code == 422
def test_graph_query_missing_user_is_422(self, client):
response = client.post("/graph/query", json={"query": "MATCH (n) RETURN n"})
assert response.status_code == 422
def test_rag_search_missing_user_is_422(self, client):
response = client.post("/rag/search", json={"query": "test"})
assert response.status_code == 422