Spaces:
Running
Running
| # backend/tests/conftest.py | |
| # Shared fixtures for all PersonaBot backend tests. | |
| # Sets all required env vars so Settings() never fails with a missing-field error. | |
| # Tests run against no real external services — every dependency is mocked. | |
| import os | |
| import time | |
| import pytest | |
| from unittest.mock import AsyncMock, MagicMock, patch | |
| from fastapi.testclient import TestClient | |
| from jose import jwt | |
| # Set env before any app import so pydantic-settings picks them up. | |
| os.environ.setdefault("ENVIRONMENT", "test") | |
| os.environ.setdefault("LLM_PROVIDER", "groq") | |
| os.environ.setdefault("GROQ_API_KEY", "gsk_test_key_not_real") | |
| os.environ.setdefault("QDRANT_URL", "http://localhost:6333") | |
| os.environ.setdefault("JWT_SECRET", "test-secret-32-chars-long-0000000") | |
| os.environ.setdefault("ALLOWED_ORIGIN", "http://localhost:3000") | |
| os.environ.setdefault("EMBEDDER_URL", "http://localhost:7860") | |
| os.environ.setdefault("RERANKER_URL", "http://localhost:7861") | |
| os.environ.setdefault("DB_PATH", "/tmp/personabot_test.db") | |
| TEST_JWT_SECRET = os.environ["JWT_SECRET"] | |
| TEST_ALGORITHM = "HS256" | |
| TEST_SESSION_ID = "a1b2c3d4-e5f6-4789-8abc-def012345678" | |
| def make_jwt(secret: str = TEST_JWT_SECRET, exp_offset: int = 3600, **extra) -> str: | |
| """Create a signed JWT for use in test requests.""" | |
| payload = {"sub": "test-user", "exp": int(time.time()) + exp_offset, **extra} | |
| return jwt.encode(payload, secret, algorithm=TEST_ALGORITHM) | |
| def valid_token() -> str: | |
| return make_jwt() | |
| def expired_token() -> str: | |
| return make_jwt(exp_offset=-1) # already expired | |
| def wrong_secret_token() -> str: | |
| return make_jwt(secret="completely-different-secret-0000") | |
| def app_client(): | |
| """TestClient with a mocked app.state so no real services are started.""" | |
| # Clear the lru_cache so test env vars are used | |
| from app.core.config import get_settings | |
| get_settings.cache_clear() | |
| mock_pipeline = MagicMock() | |
| async def fake_astream(state, stream_mode=None): | |
| # Support the new stream_mode=["custom", "updates"] tuple format used by chat.py. | |
| if isinstance(stream_mode, list): | |
| yield ("custom", {"type": "status", "label": "Checking your question"}) | |
| yield ("updates", {"guard": {"guard_passed": True}}) | |
| # Fix 1: enumerate_query node runs after guard on every request. | |
| # Non-enumeration queries set is_enumeration_query=False and pass through. | |
| yield ("updates", {"enumerate_query": {"is_enumeration_query": False}}) | |
| yield ("updates", {"cache": {"cached": False}}) | |
| yield ("custom", {"type": "status", "label": "Thinking about your question directly..."}) | |
| yield ("custom", {"type": "token", "text": "I built TextOps."}) | |
| yield ("updates", {"generate": {"answer": "I built TextOps.", "sources": []}}) | |
| else: | |
| # Fallback for any code that still calls astream without stream_mode. | |
| yield {"guard": {"guard_passed": True}} | |
| yield {"enumerate_query": {"is_enumeration_query": False}} | |
| yield {"cache": {"cached": False}} | |
| yield {"generate": {"answer": "I built TextOps.", "sources": []}} | |
| mock_pipeline.astream = fake_astream | |
| # Patch the lifespan so TestClient doesn't try to connect to Qdrant or HF Spaces | |
| with patch("app.main.build_pipeline", return_value=mock_pipeline), \ | |
| patch("app.main.QdrantClient"), \ | |
| patch("app.services.embedder.Embedder"), \ | |
| patch("app.services.reranker.Reranker"): | |
| from app.main import create_app | |
| app = create_app() | |
| app.state.pipeline = mock_pipeline | |
| with TestClient(app, raise_server_exceptions=True) as client: | |
| yield client | |