"""Unit tests for runtime sentiment provider configuration.""" from __future__ import annotations import pytest from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from app.exceptions import ProviderError, ValidationError from app.models.settings import SystemSetting from app.services import sentiment_provider_service as sps @pytest.fixture async def session() -> AsyncSession: from tests.conftest import _test_session_factory async with _test_session_factory() as session: yield session @pytest.fixture(autouse=True) def _clear_env_keys(monkeypatch): """Default: no env keys, so DB-only behavior is deterministic.""" monkeypatch.setattr(sps.settings, "openai_api_key", "") monkeypatch.setattr(sps.settings, "gemini_api_key", "") class TestGetConfig: async def test_defaults_no_key(self, session: AsyncSession): config = await sps.get_sentiment_config(session) assert config["provider"] == "openai" assert config["model"] == "gpt-4o-mini" assert config["api_key_configured"] is False assert config["api_key_source"] == "none" assert config["web_search"] is True assert set(config["valid_providers"]) == {"openai", "gemini", "deepseek", "xai", "openai_compatible"} async def test_never_returns_raw_key(self, session: AsyncSession): await sps.update_sentiment_config(session, api_key="sk-secret-123") config = await sps.get_sentiment_config(session) # No field should leak the key assert "sk-secret-123" not in str(config) assert config["api_key_configured"] is True assert config["api_key_source"] == "database" async def test_env_fallback_reported(self, session: AsyncSession, monkeypatch): monkeypatch.setattr(sps.settings, "openai_api_key", "sk-from-env") config = await sps.get_sentiment_config(session) assert config["api_key_configured"] is True assert config["api_key_source"] == "environment" class TestUpdateConfig: async def test_update_provider_and_model(self, session: AsyncSession): result = await sps.update_sentiment_config( session, provider="gemini", model="gemini-2.0-flash" ) assert result["provider"] == "gemini" assert result["model"] == "gemini-2.0-flash" async def test_empty_key_does_not_overwrite(self, session: AsyncSession): await sps.update_sentiment_config(session, api_key="sk-original") # Subsequent save without a key must keep the original await sps.update_sentiment_config(session, provider="openai", api_key="") result = await session.execute( select(SystemSetting).where(SystemSetting.key == sps.KEY_API_KEY) ) assert result.scalar_one().value == "sk-original" async def test_rejects_invalid_provider(self, session: AsyncSession): with pytest.raises(ValidationError): await sps.update_sentiment_config(session, provider="anthropic") class TestBuildProvider: async def test_raises_without_key(self, session: AsyncSession): with pytest.raises(ProviderError): await sps.build_sentiment_provider(session) async def test_builds_openai(self, session: AsyncSession): await sps.update_sentiment_config(session, provider="openai", api_key="sk-x") provider = await sps.build_sentiment_provider(session) assert type(provider).__name__ == "OpenAISentimentProvider" async def test_builds_gemini(self, session: AsyncSession): await sps.update_sentiment_config(session, provider="gemini", api_key="g-x") provider = await sps.build_sentiment_provider(session) assert type(provider).__name__ == "GeminiSentimentProvider" async def test_uses_env_key_when_db_empty(self, session: AsyncSession, monkeypatch): monkeypatch.setattr(sps.settings, "openai_api_key", "sk-env") await sps.update_sentiment_config(session, provider="openai") provider = await sps.build_sentiment_provider(session) assert type(provider).__name__ == "OpenAISentimentProvider" async def test_builds_deepseek_with_fixed_base_url(self, session: AsyncSession): await sps.update_sentiment_config(session, provider="deepseek", api_key="ds-x") provider = await sps.build_sentiment_provider(session) assert type(provider).__name__ == "OpenAICompatibleSentimentProvider" config = await sps.get_sentiment_config(session) assert config["base_url"] == "https://api.deepseek.com" assert config["web_search"] is False async def test_builds_xai_via_responses_web_search(self, session: AsyncSession): await sps.update_sentiment_config(session, provider="xai", api_key="xai-x") provider = await sps.build_sentiment_provider(session) # xAI grounds via the Responses API web_search tool assert type(provider).__name__ == "OpenAISentimentProvider" assert provider._tool_type == "web_search" assert provider._source == "xai" config = await sps.get_sentiment_config(session) assert config["base_url"] == "https://api.x.ai/v1" assert config["web_search"] is True async def test_openai_compatible_requires_base_url(self, session: AsyncSession): await sps.update_sentiment_config(session, provider="openai_compatible", api_key="x") with pytest.raises(ProviderError): await sps.build_sentiment_provider(session) async def test_openai_compatible_with_base_url(self, session: AsyncSession): await sps.update_sentiment_config( session, provider="openai_compatible", api_key="x", model="llama-3.1-70b", base_url="https://openrouter.ai/api/v1", ) provider = await sps.build_sentiment_provider(session) assert type(provider).__name__ == "OpenAICompatibleSentimentProvider"