"""Tests for settings and API key database models.""" from datetime import datetime, timezone import pytest from sqlalchemy import create_engine from sqlalchemy.exc import IntegrityError from sqlalchemy.orm import sessionmaker from local_deep_research.database.models import ( APIKey, Base, Setting, SettingType, UserSettings, ) class TestSettingsModels: """Test suite for settings-related models.""" @pytest.fixture def engine(self): """Create an in-memory SQLite database for testing.""" engine = create_engine("sqlite:///:memory:") Base.metadata.create_all(engine) yield engine engine.dispose() @pytest.fixture def session(self, engine): """Create a database session for testing.""" Session = sessionmaker(bind=engine) session = Session() yield session session.close() def test_setting_creation(self, session): """Test creating various types of settings.""" # App setting app_setting = Setting( key="app.theme", value={"mode": "dark", "accent": "blue"}, type=SettingType.APP, name="Theme", category="appearance", description="Application theme settings", ui_element="select", options=["light", "dark", "auto"], visible=True, editable=True, ) # LLM setting llm_setting = Setting( key="llm.temperature", value=0.7, type=SettingType.LLM, name="Temperature", category="model", description="LLM response temperature", ui_element="slider", min_value=0.0, max_value=2.0, step=0.1, visible=True, editable=True, ) # Search setting search_setting = Setting( key="search.max_results", value=20, type=SettingType.SEARCH, name="Max Results", category="search", description="Maximum search results per query", ui_element="number", min_value=1, max_value=100, visible=True, editable=True, ) # Report setting report_setting = Setting( key="report.format", value="markdown", type=SettingType.REPORT, name="Report Format", category="output", description="Default report format", ui_element="select", options=["markdown", "html", "pdf"], visible=True, editable=True, ) session.add_all( [app_setting, llm_setting, search_setting, report_setting] ) session.commit() # Verify settings all_settings = session.query(Setting).all() assert len(all_settings) == 4 llm = session.query(Setting).filter_by(key="llm.temperature").first() assert llm.value == 0.7 assert llm.type == SettingType.LLM assert llm.min_value == 0.0 assert llm.max_value == 2.0 def test_unique_key_constraint(self, session): """Test that setting keys must be unique.""" setting1 = Setting( key="api.timeout", value=30, type=SettingType.APP, name="API Timeout", category="api", ) setting2 = Setting( key="api.timeout", # Duplicate key value=60, type=SettingType.APP, name="API Timeout", category="api", ) session.add(setting1) session.commit() session.add(setting2) with pytest.raises(IntegrityError): session.commit() def test_secret_settings(self, session): """Test handling of secret settings (though Setting model doesn't have is_secret).""" # Using APIKey for secrets instead api_key = APIKey( provider="openai", key="sk-abc123...", # Would be encrypted by SQLCipher description="OpenAI API key for GPT models", is_active=True, ) session.add(api_key) session.commit() # Verify API key saved = session.query(APIKey).filter_by(provider="openai").first() assert saved is not None assert saved.is_active is True assert saved.key == "sk-abc123..." # In real usage, the key would be encrypted by SQLCipher def test_system_settings(self, session): """Test system-wide settings that shouldn't be user-editable.""" system_setting = Setting( key="system.version", value="1.0.0", type=SettingType.APP, name="System Version", category="system", description="Current system version", visible=True, editable=False, # System setting - not editable ) session.add(system_setting) session.commit() saved = session.query(Setting).filter_by(key="system.version").first() assert saved.editable is False assert saved.visible is True def test_user_settings(self, session): """Test UserSettings for user-specific preferences.""" # User preferences preferences = [ UserSettings( key="preferred_model", value={"provider": "openai", "model": "gpt-4"}, category="preferences", description="User's preferred LLM model", ), UserSettings( key="search_history_enabled", value=True, category="privacy", description="Whether to save search history", ), UserSettings( key="ui_language", value="en", category="localization", description="User interface language", ), ] session.add_all(preferences) session.commit() # Query by category privacy_settings = ( session.query(UserSettings).filter_by(category="privacy").all() ) assert len(privacy_settings) == 1 assert privacy_settings[0].value is True def test_api_key_management(self, session): """Test APIKey model for secure API key storage.""" # Add multiple API keys keys = [ APIKey( provider="openai", key="sk-openai...", description="OpenAI GPT models", is_active=True, ), APIKey( provider="anthropic", key="sk-ant...", description="Claude models", is_active=True, ), APIKey( provider="google", key="AIza...", description="Google search API", is_active=False, # Disabled ), ] session.add_all(keys) session.commit() # Query active keys active_keys = session.query(APIKey).filter_by(is_active=True).all() assert len(active_keys) == 2 # Test unique provider constraint duplicate = APIKey( provider="openai", # Already exists key="sk-different...", description="Duplicate provider", ) session.add(duplicate) with pytest.raises(IntegrityError): session.commit() def test_setting_categories(self, session): """Test organizing settings by category.""" categories = ["appearance", "performance", "security", "advanced"] for i, cat in enumerate(categories): for j in range(3): setting = Setting( key=f"{cat}.setting_{j}", value=f"value_{j}", type=SettingType.APP, name=f"{cat.title()} Setting {j}", category=cat, visible=True if i < 3 else False, # Hide advanced ) session.add(setting) session.commit() # Query by category appearance = ( session.query(Setting).filter_by(category="appearance").all() ) assert len(appearance) == 3 # Query visible settings visible = session.query(Setting).filter_by(visible=True).count() assert visible == 9 # 3 categories * 3 settings each def test_setting_value_types(self, session): """Test different value types stored in JSON columns.""" settings = [ UserSettings(key="string_val", value="text value"), UserSettings(key="int_val", value=42), UserSettings(key="float_val", value=3.14), UserSettings(key="bool_val", value=True), UserSettings(key="list_val", value=[1, 2, 3]), UserSettings(key="dict_val", value={"a": 1, "b": 2}), UserSettings(key="null_val", value=None), ] session.add_all(settings) session.commit() # Verify different types assert ( session.query(UserSettings) .filter_by(key="string_val") .first() .value == "text value" ) assert ( session.query(UserSettings).filter_by(key="int_val").first().value == 42 ) assert ( session.query(UserSettings).filter_by(key="float_val").first().value == 3.14 ) assert ( session.query(UserSettings).filter_by(key="bool_val").first().value is True ) assert session.query(UserSettings).filter_by( key="list_val" ).first().value == [1, 2, 3] assert session.query(UserSettings).filter_by( key="dict_val" ).first().value == {"a": 1, "b": 2} assert ( session.query(UserSettings).filter_by(key="null_val").first().value is None ) def test_api_key_rotation(self, session): """Test API key rotation and usage tracking.""" # Create initial key api_key = APIKey( provider="openai", key="sk-old...", description="OpenAI API key", is_active=True, usage_count=100, ) session.add(api_key) session.commit() # Simulate key usage api_key.usage_count += 1 api_key.last_used = datetime.now(timezone.utc) session.commit() # Rotate key (deactivate old, add new) api_key.is_active = False new_key = APIKey( provider="openai_new", # Different provider name to avoid constraint key="sk-new...", description="OpenAI API key (rotated)", is_active=True, ) session.add(new_key) session.commit() # Verify rotation old_key = session.query(APIKey).filter_by(provider="openai").first() assert old_key.is_active is False assert old_key.usage_count == 101 active_key = ( session.query(APIKey) .filter_by(is_active=True, provider="openai_new") .first() ) assert active_key.key == "sk-new..." def test_user_settings_defaults(self, session): """Test default values for user settings.""" # Get a non-existent setting setting = ( session.query(UserSettings) .filter_by(key="non_existent_setting") .first() ) assert setting is None # Create setting with defaults default_setting = UserSettings( key="new_feature_enabled", value=False, # Default to disabled category="features", description="New experimental feature", ) session.add(default_setting) session.commit() saved = ( session.query(UserSettings) .filter_by(key="new_feature_enabled") .first() ) assert saved.value is False assert saved.created_at is not None assert saved.updated_at is not None def test_setting_update_tracking(self, session): """Test that updated_at is properly tracked.""" setting = Setting( key="test.update", value="initial", type=SettingType.APP, name="Test Update", category="test", ) session.add(setting) session.commit() # Update the setting setting.value = "updated" session.commit() # Note: onupdate might not trigger in SQLite without proper configuration # In production with PostgreSQL, this would work automatically