""" Integration test for API base path support. Tests that the API works correctly when deployed with a base path (e.g., /hindsight) for reverse proxy deployments. """ import os import pytest import pytest_asyncio import httpx from hindsight_api.api import create_app from hindsight_api.config import clear_config_cache @pytest_asyncio.fixture async def api_client_with_base_path(memory): """Create an async test client for the FastAPI app with a base path.""" # Set base path in environment base_path = "/hindsight" os.environ["HINDSIGHT_API_BASE_PATH"] = base_path # Clear config cache to force reload with new base_path clear_config_cache() # Memory is already initialized by the conftest fixture (with migrations) app = create_app(memory, initialize_memory=False) # Use base_url with base path transport = httpx.ASGITransport(app=app) async with httpx.AsyncClient( transport=transport, base_url=f"http://test{base_path}" ) as client: yield client # Cleanup: unset base path os.environ.pop("HINDSIGHT_API_BASE_PATH", None) clear_config_cache() @pytest_asyncio.fixture async def api_client_without_base_path(memory): """Create an async test client for the FastAPI app without a base path (root).""" # Ensure no base path is set os.environ.pop("HINDSIGHT_API_BASE_PATH", None) clear_config_cache() app = create_app(memory, initialize_memory=False) transport = httpx.ASGITransport(app=app) async with httpx.AsyncClient(transport=transport, base_url="http://test") as client: yield client @pytest.mark.asyncio async def test_base_path_health_endpoint(api_client_with_base_path): """Test that health endpoint works with base path.""" # With base path set to /hindsight, health should be at /hindsight/health # But since our client base_url is already http://test/hindsight, we request /health response = await api_client_with_base_path.get("/health") assert response.status_code == 200 data = response.json() assert "status" in data assert data["status"] in ["ok", "healthy"] # Accept both formats @pytest.mark.asyncio async def test_base_path_banks_endpoint(api_client_with_base_path): """Test that banks endpoint works with base path.""" response = await api_client_with_base_path.get("/v1/default/banks") assert response.status_code == 200 data = response.json() assert "banks" in data @pytest.mark.asyncio async def test_base_path_openapi_schema(api_client_with_base_path): """Test that OpenAPI schema includes correct base path in servers.""" response = await api_client_with_base_path.get("/openapi.json") assert response.status_code == 200 openapi_schema = response.json() # Check that servers array includes base path assert "servers" in openapi_schema servers = openapi_schema["servers"] assert len(servers) > 0 # FastAPI should set server URL to the root_path assert servers[0]["url"] == "/hindsight" @pytest.mark.asyncio async def test_base_path_docs_redirect(api_client_with_base_path): """Test that /docs redirects correctly with base path.""" # FastAPI docs endpoint should work response = await api_client_with_base_path.get("/docs", follow_redirects=False) # Should either return 200 (direct) or 307 (redirect to trailing slash) assert response.status_code in [200, 307] @pytest.mark.asyncio async def test_base_path_metrics(api_client_with_base_path): """Test that metrics endpoint works with base path.""" response = await api_client_with_base_path.get("/metrics") assert response.status_code == 200 # Metrics should be in Prometheus format assert "# HELP" in response.text or "# TYPE" in response.text @pytest.mark.asyncio async def test_base_path_full_workflow(api_client_with_base_path): """ Test a full retain/recall workflow with base path. This ensures that all memory operations work correctly when the API is deployed with a base path. """ bank_id = "test_base_path_bank" # 1. Create/get bank response = await api_client_with_base_path.get(f"/v1/default/banks/{bank_id}/profile") assert response.status_code == 200 # 2. Store a memory response = await api_client_with_base_path.post( f"/v1/default/banks/{bank_id}/memories", json={ "items": [ { "content": "The API supports base path deployment for reverse proxy use cases.", "context": "testing base path feature" } ] } ) assert response.status_code == 200 result = response.json() assert result["success"] is True # 3. Recall the memory response = await api_client_with_base_path.post( f"/v1/default/banks/{bank_id}/memories/recall", json={ "query": "base path support" } ) assert response.status_code == 200 recall_result = response.json() # API returns "results" not "memories" assert "results" in recall_result assert len(recall_result["results"]) > 0 @pytest.mark.asyncio async def test_without_base_path_still_works(api_client_without_base_path): """ Regression test: ensure default behavior (no base path) still works. This test verifies that when HINDSIGHT_API_BASE_PATH is not set, the API works at the root path as before. """ # Health check at root response = await api_client_without_base_path.get("/health") assert response.status_code == 200 # Banks endpoint at root response = await api_client_without_base_path.get("/v1/default/banks") assert response.status_code == 200 # OpenAPI schema should have empty or "/" server path response = await api_client_without_base_path.get("/openapi.json") assert response.status_code == 200 openapi_schema = response.json() servers = openapi_schema.get("servers", []) if servers: # Server URL should be empty string (root) or "/" assert servers[0]["url"] in ["", "/"] @pytest.mark.skip(reason="MCP endpoint routing with base path needs investigation") @pytest.mark.asyncio async def test_base_path_mcp_endpoint(api_client_with_base_path): """Test that MCP endpoint is accessible with base path.""" bank_id = "test_mcp_bank" # MCP endpoint should be mounted at /mcp/{bank_id}/ # The MCP server uses a different protocol, so just check the root exists response = await api_client_with_base_path.get(f"/mcp/{bank_id}/") # MCP may return various status codes, but should not be 404 (not found) # Accept 405 (method not allowed), 400 (bad request), etc. assert response.status_code != 404, "MCP endpoint should exist"