131 lines
4.5 KiB
Python
131 lines
4.5 KiB
Python
import json
|
|
import logging
|
|
|
|
from fastapi.testclient import TestClient
|
|
|
|
from ragcore.infrastructure.health import ReadinessChecker
|
|
from ragcore.main import app, create_app
|
|
from ragcore.platform.logging import JsonRedactingFormatter
|
|
|
|
|
|
def test_liveness() -> None:
|
|
with TestClient(app) as client:
|
|
response = client.get("/health/live")
|
|
assert response.status_code == 200
|
|
assert response.json() == {"status": "ok", "service": "ragcore"}
|
|
assert response.headers["x-request-id"]
|
|
|
|
|
|
def test_request_id_is_preserved_when_safe() -> None:
|
|
with TestClient(app) as client:
|
|
response = client.get("/health/live", headers={"X-Request-ID": "client-request-123"})
|
|
|
|
assert response.headers["x-request-id"] == "client-request-123"
|
|
|
|
|
|
def test_readiness_returns_dependency_status() -> None:
|
|
async def dependency_ok() -> None:
|
|
return None
|
|
|
|
test_app = create_app(
|
|
readiness_checker=ReadinessChecker(
|
|
{"postgres": dependency_ok, "qdrant": dependency_ok, "storage": dependency_ok}
|
|
)
|
|
)
|
|
with TestClient(test_app) as client:
|
|
response = client.get("/health/ready")
|
|
|
|
assert response.status_code == 200
|
|
assert response.json()["status"] == "ok"
|
|
assert response.headers["x-request-id"]
|
|
|
|
|
|
def test_readiness_openapi_declares_problem_json() -> None:
|
|
async def dependency_ok() -> None:
|
|
return None
|
|
|
|
test_app = create_app(
|
|
readiness_checker=ReadinessChecker(
|
|
{"postgres": dependency_ok, "qdrant": dependency_ok, "storage": dependency_ok}
|
|
)
|
|
)
|
|
response = test_app.openapi()["paths"]["/health/ready"]["get"]["responses"]["503"]
|
|
|
|
assert set(response["content"]) == {"application/problem+json"}
|
|
|
|
def test_exception_problem_contains_correlation_without_internal_detail() -> None:
|
|
test_app = create_app()
|
|
|
|
@test_app.get("/test-error")
|
|
async def test_error() -> None:
|
|
raise RuntimeError("database password should not be returned")
|
|
|
|
with TestClient(test_app, raise_server_exceptions=False) as client:
|
|
response = client.get("/test-error", headers={"X-Request-ID": "error-request-456"})
|
|
|
|
assert response.status_code == 500
|
|
assert response.headers["x-request-id"] == "error-request-456"
|
|
assert response.headers["content-type"].startswith("application/problem+json")
|
|
assert response.json()["trace_id"] == "error-request-456"
|
|
assert "database password" not in response.text
|
|
|
|
|
|
def test_json_formatter_redacts_configured_secrets() -> None:
|
|
secret = "configured-secret-value"
|
|
formatter = JsonRedactingFormatter([secret])
|
|
record = logging.LogRecord(
|
|
"test",
|
|
logging.INFO,
|
|
__file__,
|
|
1,
|
|
"token=%s",
|
|
(secret,),
|
|
None,
|
|
)
|
|
record.api_token = secret
|
|
|
|
rendered = formatter.format(record)
|
|
payload = json.loads(rendered)
|
|
|
|
assert secret not in rendered
|
|
assert payload["message"] == "token=[REDACTED]"
|
|
assert payload["api_token"] == "[REDACTED]"
|
|
|
|
def test_rate_limiting_is_really_wired_into_the_live_app() -> None:
|
|
"""No OIDC configured, so ``/auth/login`` 503s every time -- but the
|
|
``RateLimitMiddleware`` still runs ahead of the route (it's real ASGI
|
|
middleware, not a route dependency) and still turns the 11th call within
|
|
the default ``auth_login`` budget (10/60s) into a 429."""
|
|
|
|
test_app = create_app()
|
|
with TestClient(test_app) as client:
|
|
for _ in range(10):
|
|
response = client.get("/auth/login", follow_redirects=False)
|
|
assert response.status_code == 503
|
|
|
|
limited = client.get("/auth/login", follow_redirects=False)
|
|
|
|
assert limited.status_code == 429
|
|
assert limited.json()["code"] == "RATE_LIMITED"
|
|
|
|
|
|
def test_json_formatter_redacts_sensitive_structured_fields() -> None:
|
|
formatter = JsonRedactingFormatter()
|
|
record = logging.LogRecord("test", logging.INFO, __file__, 1, "audit", (), None)
|
|
record.audit_data = {
|
|
"authorization": "Bearer credential",
|
|
"query": "private query text",
|
|
"prompt": "private prompt text",
|
|
"document_content": "private document",
|
|
"safe_operation": "search",
|
|
}
|
|
|
|
payload = json.loads(formatter.format(record))
|
|
|
|
assert payload["audit_data"]["authorization"] == "[REDACTED]"
|
|
assert payload["audit_data"]["query"] == "[REDACTED]"
|
|
assert payload["audit_data"]["prompt"] == "[REDACTED]"
|
|
assert payload["audit_data"]["document_content"] == "[REDACTED]"
|
|
assert payload["audit_data"]["safe_operation"] == "search"
|
|
assert "private query text" not in formatter.format(record)
|