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)