import pytest
import importlib
import yaml
from app.config import EmbeddingProvider, TTSProvider, STTProvider, Settings
@pytest.fixture
def tmp_config_file(tmp_path):
"""
Creates a temporary config.yaml file and returns its path.
Adds STT provider settings for testing.
"""
config_content = {
"application": {
"project_name": "Test Project from YAML",
"log_level": "WARNING"
},
"llm_providers": {"deepseek_model_name": "deepseek-from-yaml"},
"embedding_provider": {
"provider": "mock",
"model_name": "embedding-model-from-yaml",
"api_key": "embedding-api-from-yaml"
},
"database": {
"mode": "sqlite",
"local_path": "custom_folder/test_ai_hub.db",
"url": "postgresql://user:pass@host/dbname"
},
"tts_provider": {
"provider": "google_gemini",
"voice_name": "Laomedeia",
"model_name": "tts-model-from-yaml",
"api_key": "tts-api-from-yaml"
},
# NEW: Add STT Provider settings to the YAML fixture
"stt_provider": {
"provider": "openai",
"model_name": "stt-model-from-yaml",
"api_key": "stt-api-from-yaml"
}
}
config_path = tmp_path / "test_config.yaml"
with open(config_path, 'w') as f:
yaml.dump(config_content, f)
return str(config_path)
@pytest.fixture
def clear_all_env(monkeypatch):
"""
A fixture to clear all relevant environment variables for test isolation.
Added new STT environment variables.
"""
monkeypatch.delenv("CONFIG_PATH", raising=False)
monkeypatch.delenv("DEEPSEEK_API_KEY", raising=False)
monkeypatch.delenv("GEMINI_API_KEY", raising=False)
monkeypatch.delenv("OPENAI_API_KEY", raising=False) # NEW
monkeypatch.delenv("EMBEDDING_API_KEY", raising=False)
monkeypatch.delenv("TTS_PROVIDER", raising=False)
monkeypatch.delenv("TTS_VOICE_NAME", raising=False)
monkeypatch.delenv("TTS_MODEL_NAME", raising=False)
monkeypatch.delenv("TTS_API_KEY", raising=False)
monkeypatch.delenv("STT_PROVIDER", raising=False) # NEW
monkeypatch.delenv("STT_MODEL_NAME", raising=False) # NEW
monkeypatch.delenv("STT_API_KEY", raising=False) # NEW
monkeypatch.delenv("DB_MODE", raising=False)
monkeypatch.delenv("LOCAL_DB_PATH", raising=False)
monkeypatch.delenv("DATABASE_URL", raising=False)
@pytest.fixture
def mock_all_keys(monkeypatch):
"""
A fixture to set all mock API keys.
"""
monkeypatch.setenv("DEEPSEEK_API_KEY", "mock_deepseek_key")
monkeypatch.setenv("GEMINI_API_KEY", "mock_gemini_key")
monkeypatch.setenv("OPENAI_API_KEY", "mock_openai_key") # NEW
monkeypatch.setenv("EMBEDDING_API_KEY", "mock_embedding_key")
monkeypatch.setenv("TTS_API_KEY", "mock_tts_key")
monkeypatch.setenv("STT_API_KEY", "mock_stt_key") # NEW
def test_sqlite_db_url_from_yaml(monkeypatch, tmp_config_file, clear_all_env):
"""Tests DATABASE_URL is constructed correctly from YAML sqlite local_path."""
monkeypatch.setenv("CONFIG_PATH", tmp_config_file)
monkeypatch.setenv("DEEPSEEK_API_KEY", "mock_key")
monkeypatch.setenv("GEMINI_API_KEY", "mock_key")
settings = Settings()
expected_path = "sqlite:///./custom_folder/test_ai_hub.db"
assert settings.DB_MODE == "sqlite"
assert settings.DATABASE_URL == expected_path
def test_sqlite_db_url_from_env_local_path(monkeypatch, tmp_path, clear_all_env):
"""Tests that LOCAL_DB_PATH env var overrides YAML for sqlite DATABASE_URL."""
monkeypatch.setenv("DB_MODE", "sqlite")
monkeypatch.setenv("LOCAL_DB_PATH", "env_folder/env_ai_hub.db")
monkeypatch.setenv("DEEPSEEK_API_KEY", "mock_key")
monkeypatch.setenv("GEMINI_API_KEY", "mock_key")
settings = Settings()
expected_path = "sqlite:///./env_folder/env_ai_hub.db"
assert settings.DB_MODE == "sqlite"
assert settings.DATABASE_URL == expected_path
def test_external_db_url_used_when_not_sqlite(monkeypatch, tmp_config_file, clear_all_env):
"""Tests DATABASE_URL uses external URL when DB_MODE is not sqlite."""
monkeypatch.setenv("CONFIG_PATH", tmp_config_file)
monkeypatch.setenv("DB_MODE", "postgresql")
monkeypatch.setenv("DATABASE_URL", "postgresql://env_user:env_pass@env_host/env_db")
monkeypatch.setenv("DEEPSEEK_API_KEY", "mock_key")
monkeypatch.setenv("GEMINI_API_KEY", "mock_key")
settings = Settings()
assert settings.DB_MODE == "postgresql"
assert settings.DATABASE_URL == "postgresql://env_user:env_pass@env_host/env_db"
def test_external_db_url_from_yaml_when_not_sqlite(monkeypatch, tmp_path, clear_all_env):
"""Tests DATABASE_URL uses YAML url when DB_MODE != sqlite and no env DATABASE_URL."""
config_content = {
"database": {
"mode": "postgresql",
"url": "postgresql://yaml_user:yaml_pass@yaml_host/yaml_db",
"local_path": "ignored_path_for_postgresql.db"
},
"llm_providers": {"deepseek_model_name": "deepseek-from-yaml"}
}
config_path = tmp_path / "test_config_pg.yaml"
with open(config_path, 'w') as f:
yaml.dump(config_content, f)
monkeypatch.setenv("CONFIG_PATH", str(config_path))
monkeypatch.setenv("DEEPSEEK_API_KEY", "mock_key")
monkeypatch.setenv("GEMINI_API_KEY", "mock_key")
settings = Settings()
assert settings.DB_MODE == "postgresql"
assert settings.DATABASE_URL == "postgresql://yaml_user:yaml_pass@yaml_host/yaml_db"
def test_sqlite_db_url_defaults(monkeypatch, clear_all_env):
"""Tests DATABASE_URL defaults to sqlite path if no env or YAML."""
monkeypatch.setenv("DB_MODE", "sqlite")
monkeypatch.setenv("DEEPSEEK_API_KEY", "mock_key")
monkeypatch.setenv("GEMINI_API_KEY", "mock_key")
settings = Settings()
assert settings.DB_MODE == "sqlite"
assert settings.DATABASE_URL == "sqlite:///./data/ai_hub.db"
# --- Tests for TTS Configuration ---
def test_tts_settings_from_yaml(monkeypatch, tmp_config_file, clear_all_env):
"""Tests that TTS settings are loaded correctly from a YAML file."""
monkeypatch.setenv("CONFIG_PATH", tmp_config_file)
monkeypatch.setenv("DEEPSEEK_API_KEY", "mock_key")
monkeypatch.setenv("GEMINI_API_KEY", "mock_key")
settings = Settings()
assert settings.TTS_PROVIDER == TTSProvider.GOOGLE_GEMINI
assert settings.TTS_VOICE_NAME == "Laomedeia"
assert settings.TTS_MODEL_NAME == "tts-model-from-yaml"
assert settings.TTS_API_KEY == "tts-api-from-yaml"
def test_tts_settings_from_env(monkeypatch, tmp_config_file, clear_all_env):
"""Tests that TTS environment variables override the YAML file."""
monkeypatch.setenv("CONFIG_PATH", tmp_config_file)
monkeypatch.setenv("TTS_PROVIDER", "google_gemini")
monkeypatch.setenv("TTS_VOICE_NAME", "Zephyr")
monkeypatch.setenv("TTS_MODEL_NAME", "env-tts-model")
monkeypatch.setenv("TTS_API_KEY", "env_tts_key")
monkeypatch.setenv("DEEPSEEK_API_KEY", "mock_key")
monkeypatch.setenv("GEMINI_API_KEY", "mock_key")
settings = Settings()
assert settings.TTS_PROVIDER == TTSProvider.GOOGLE_GEMINI
assert settings.TTS_VOICE_NAME == "Zephyr"
assert settings.TTS_MODEL_NAME == "env-tts-model"
assert settings.TTS_API_KEY == "env_tts_key"
def test_tts_settings_defaults(monkeypatch, clear_all_env):
"""Tests that TTS settings fall back to Pydantic defaults."""
monkeypatch.setenv("DEEPSEEK_API_KEY", "mock_key")
# Setting GEMINI_API_KEY to test its fallback behavior
monkeypatch.setenv("GEMINI_API_KEY", "fallback_gemini_key")
settings = Settings()
assert settings.TTS_PROVIDER == TTSProvider.GOOGLE_GEMINI
assert settings.TTS_VOICE_NAME == "Kore"
assert settings.TTS_MODEL_NAME == "gemini-2.5-flash-preview-tts"
assert settings.TTS_API_KEY == "fallback_gemini_key"
# --- NEW Tests for STT Configuration ---
def test_stt_settings_from_yaml(monkeypatch, tmp_config_file, clear_all_env):
"""Tests that STT settings are loaded correctly from a YAML file."""
monkeypatch.setenv("CONFIG_PATH", tmp_config_file)
monkeypatch.setenv("DEEPSEEK_API_KEY", "mock_key")
monkeypatch.setenv("GEMINI_API_KEY", "mock_gemini_key")
settings = Settings()
assert settings.STT_PROVIDER == STTProvider.OPENAI
assert settings.STT_MODEL_NAME == "stt-model-from-yaml"
assert settings.STT_API_KEY == "stt-api-from-yaml"
def test_stt_settings_from_env_openai(monkeypatch, tmp_config_file, clear_all_env):
"""Tests that STT environment variables override the YAML file for OpenAI provider."""
monkeypatch.setenv("CONFIG_PATH", tmp_config_file)
monkeypatch.setenv("STT_PROVIDER", "openai")
monkeypatch.setenv("STT_MODEL_NAME", "env-stt-model")
monkeypatch.setenv("STT_API_KEY", "env_stt_key")
monkeypatch.setenv("DEEPSEEK_API_KEY", "mock_key")
monkeypatch.setenv("GEMINI_API_KEY", "mock_gemini_key")
settings = Settings()
assert settings.STT_PROVIDER == STTProvider.OPENAI
assert settings.STT_MODEL_NAME == "env-stt-model"
assert settings.STT_API_KEY == "env_stt_key"
def test_stt_api_key_fallback_to_openai_api_key(monkeypatch, clear_all_env):
"""Tests that STT_API_KEY falls back to OPENAI_API_KEY if the provider is OpenAI and no STT_API_KEY is set."""
monkeypatch.setenv("STT_PROVIDER", "openai")
monkeypatch.setenv("OPENAI_API_KEY", "fallback_openai_key")
monkeypatch.setenv("DEEPSEEK_API_KEY", "mock_key")
settings = Settings()
assert settings.STT_PROVIDER == STTProvider.OPENAI
assert settings.STT_API_KEY == "fallback_openai_key"
def test_stt_api_key_fallback_to_gemini_api_key(monkeypatch, clear_all_env):
"""Tests that STT_API_KEY falls back to GEMINI_API_KEY if the provider is not OpenAI and no STT_API_KEY is set."""
monkeypatch.setenv("STT_PROVIDER", "google_gemini")
monkeypatch.setenv("GEMINI_API_KEY", "fallback_gemini_key")
monkeypatch.setenv("DEEPSEEK_API_KEY", "mock_key")
settings = Settings()
assert settings.STT_PROVIDER == STTProvider.GOOGLE_GEMINI
assert settings.STT_API_KEY == "fallback_gemini_key"