7a0da7932b
Backwards Compatibility / Verify Encryption Constants (push) Waiting to run
Backwards Compatibility / PyPI Version Compatibility (push) Waiting to run
Backwards Compatibility / Database Migration Tests (push) Waiting to run
CodeQL Advanced / Analyze (javascript-typescript) (push) Waiting to run
CodeQL Advanced / Analyze (python) (push) Waiting to run
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-form] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-metrics] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-workflow] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-core] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-pages] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [history-news] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [library] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [link-analytics] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [mobile] (push) Blocked by required conditions
Docker Tests (Consolidated) / detect-changes (push) Waiting to run
Docker Tests (Consolidated) / Build Test Image (push) Waiting to run
Docker Tests (Consolidated) / All Pytest Tests + Coverage (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [accessibility] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [api-crud] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-login] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-pages] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-register] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-core] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-lifecycle] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [error-benchmark] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) (push) Blocked by required conditions
Docker Tests (Consolidated) / Accessibility Tests (push) Blocked by required conditions
Docker Tests (Consolidated) / LLM Unit Tests (push) Blocked by required conditions
Docker Tests (Consolidated) / LLM Example Tests (push) Blocked by required conditions
Docker Tests (Consolidated) / Production Image Smoke Test (push) Blocked by required conditions
Docker Tests (Consolidated) / Infrastructure Tests (push) Blocked by required conditions
OSSF Scorecard / OSSF Security Scorecard Analysis (push) Waiting to run
OSV-Scanner (Scheduled) / scan-scheduled (push) Failing after 0s
Create Release / test-gate (push) Has been cancelled
Create Release / release-gate (push) Has been cancelled
Create Release / ci-gate (push) Has been cancelled
Create Release / version-check (push) Has been cancelled
Create Release / e2e-test-gate (push) Has been cancelled
Create Release / responsive-test-gate (push) Has been cancelled
Create Release / compat-test-gate (push) Has been cancelled
Create Release / compose-integration-gate (push) Has been cancelled
Create Release / vulture-gate (push) Has been cancelled
Create Release / build (push) Has been cancelled
Create Release / provenance (push) Has been cancelled
Create Release / prerelease-docker (push) Has been cancelled
Create Release / publish-docker (push) Has been cancelled
Create Release / create-release (push) Has been cancelled
Create Release / cleanup-changelog (push) Has been cancelled
Create Release / trigger-pypi (push) Has been cancelled
Create Release / monitor-pypi (push) Has been cancelled
Create Release / Clean up orphan prerelease tags and signatures (push) Has been cancelled
423 lines
13 KiB
Python
423 lines
13 KiB
Python
"""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
|