personabot-api / tests /conftest.py
GitHub Actions
Deploy 27439fc
0da0699
# 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)
@pytest.fixture
def valid_token() -> str:
return make_jwt()
@pytest.fixture
def expired_token() -> str:
return make_jwt(exp_offset=-1) # already expired
@pytest.fixture
def wrong_secret_token() -> str:
return make_jwt(secret="completely-different-secret-0000")
@pytest.fixture
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