Files
signal-platform/tests/unit/test_sentiment_provider_service.py
T
dennisthiessen 126c3b3c17
Deploy / lint (push) Successful in 5s
Deploy / test (push) Successful in 32s
Deploy / deploy (push) Successful in 22s
Add DeepSeek/xAI/OpenAI-compatible sentiment providers; custom dark dropdown
Providers (admin-switchable, no redeploy):
- DeepSeek and any OpenAI-compatible endpoint (OpenRouter, Together,
  Groq, local Ollama) via a generic Chat Completions adapter + base_url
- xAI Grok with Live Search (search_parameters web+X, citations) —
  grounded tier alongside OpenAI and Gemini
- DeepSeek / generic compatible endpoints are ungrounded (no web
  search); UI shows an amber warning and labels each provider's grounding
- Optional env fallbacks DEEPSEEK_API_KEY / XAI_API_KEY

UI: replace native <select> (unstyleable white popup on Windows) with a
custom dark Dropdown component everywhere — sentiment provider, scanner
filters, market sort, indicators, admin universe, user role.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-13 12:42:04 +02:00

131 lines
5.9 KiB
Python

"""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_with_live_search(self, session: AsyncSession):
await sps.update_sentiment_config(session, provider="xai", api_key="xai-x")
provider = await sps.build_sentiment_provider(session)
assert type(provider).__name__ == "OpenAICompatibleSentimentProvider"
# xAI is wired with Live Search enabled
assert provider._live_search is True
assert provider._extra_body == {"search_parameters": {"mode": "auto", "return_citations": True}}
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"