import os
from pathlib import Path
from typing import Generator
import pytest
import json
from skillhub.config import (
Settings,
PlatformConfig,
CacheConfig,
SecurityConfig,
DiscoveryConfig,
get_config,
save_config,
set_config_value,
CONFIG_FILE_NAME,
)
@pytest.fixture
def temp_config_dir() -> Generator[Path, None, None]:
"""Create temporary config directory."""
import tempfile
import shutil
directory = Path(tempfile.mkdtemp())
yield directory
shutil.rmtree(directory, ignore_errors=True)
class TestPlatformConfig:
"""Tests for PlatformConfig."""
def test_platform_config_defaults(self):
"""Test platform config default values."""
config = PlatformConfig(api_url="https://api.example.com")
assert config.api_url == "https://api.example.com"
assert config.auth_type == "pat"
assert config.rate_limit == 5000
assert config.timeout == 30
def test_platform_config_custom_values(self):
"""Test platform config with custom values."""
config = PlatformConfig(
api_url="https://custom.api.com",
auth_type="oauth",
rate_limit=1000,
timeout=60,
)
assert config.auth_type == "oauth"
assert config.rate_limit == 1000
def test_platform_config_model_dump(self):
"""Test converting platform config to dict."""
config = PlatformConfig(api_url="https://api.test.com")
data = config.model_dump()
assert data["api_url"] == "https://api.test.com"
class TestCacheConfig:
"""Tests for CacheConfig."""
def test_cache_config_defaults(self):
"""Test cache config default values."""
config = CacheConfig()
assert config.enabled is True
assert config.ttl_metadata == 3600
assert config.ttl_search == 1800
assert config.ttl_releases == 7200
assert config.max_size_mb == 1024
def test_cache_config_disabled(self):
"""Test cache config when disabled."""
config = CacheConfig(enabled=False)
assert config.enabled is False
def test_cache_config_custom_ttl(self):
"""Test cache config with custom TTL."""
config = CacheConfig(
ttl_metadata=7200,
ttl_search=3600,
)
assert config.ttl_metadata == 7200
assert config.ttl_search == 3600
class TestSecurityConfig:
"""Tests for SecurityConfig."""
def test_security_config_defaults(self):
"""Test security config default values."""
config = SecurityConfig()
assert config.allow_unsigned is False
assert config.sandbox_installs is True
assert config.strict_permissions is True
assert config.trusted_authors == []
assert config.trusted_sources == []
def test_security_config_allow_unsigned(self):
"""Test security config allowing unsigned."""
config = SecurityConfig(allow_unsigned=True)
assert config.allow_unsigned is True
def test_security_config_trusted_sources(self):
"""Test security config with trusted sources."""
config = SecurityConfig(
trusted_sources=["github.com", "gitee.com"],
)
assert "github.com" in config.trusted_sources
def test_security_config_trusted_authors(self):
"""Test security config with trusted authors."""
config = SecurityConfig(
trusted_authors=["trusted-author"],
)
assert "trusted-author" in config.trusted_authors
class TestDiscoveryConfig:
"""Tests for DiscoveryConfig."""
def test_discovery_config_defaults(self):
"""Test discovery config default values."""
config = DiscoveryConfig()
assert config.default_sources == []
assert config.search_timeout == 30
assert config.max_results == 100
def test_discovery_config_custom_values(self):
"""Test discovery config with custom values."""
config = DiscoveryConfig(
default_sources=["github", "gitee"],
search_timeout=60,
max_results=50,
)
assert len(config.default_sources) == 2
assert config.search_timeout == 60
class TestSettings:
"""Tests for Settings."""
def test_settings_default_paths(self, mock_settings: Settings):
"""Test settings with default paths from fixture."""
assert mock_settings.config_dir is not None
assert mock_settings.cache_dir is not None
assert mock_settings.data_dir is not None
assert mock_settings.skills_dir is not None
def test_settings_cache_config(self, mock_settings: Settings):
"""Test settings contains cache config."""
assert mock_settings.cache.enabled is True
assert isinstance(mock_settings.cache, CacheConfig)
def test_settings_security_config(self, mock_settings: Settings):
"""Test settings contains security config."""
assert isinstance(mock_settings.security, SecurityConfig)
assert mock_settings.security.sandbox_installs is True
def test_settings_discovery_config(self, mock_settings: Settings):
"""Test settings contains discovery config."""
assert isinstance(mock_settings.discovery, DiscoveryConfig)
assert mock_settings.discovery.max_results == 100
def test_settings_platform_configs(self, mock_settings: Settings):
"""Test settings contains platform configs."""
assert mock_settings.github.api_url == "https://api.github.com"
assert mock_settings.gitee.api_url == "https://gitee.com/api/v5"
assert mock_settings.gitcode.api_url == "https://api.gitcode.com/api/v5"
def test_settings_log_level(self, mock_settings: Settings):
"""Test settings log level."""
assert mock_settings.log_level == "INFO"
def test_settings_log_file_optional(self, mock_settings: Settings):
"""Test settings log file is optional."""
assert mock_settings.log_file is None
def test_settings_model_dump(self, mock_settings: Settings):
"""Test converting settings to dict."""
data = mock_settings.model_dump()
assert "cache" in data
assert "security" in data
def test_settings_dirs_created(self, temp_config_dir: Path):
"""Test that directories are created."""
cache_dir = temp_config_dir / "cache"
data_dir = temp_config_dir / "data"
skills_dir = temp_config_dir / "skills"
settings = Settings(
config_dir=temp_config_dir,
cache_dir=cache_dir,
data_dir=data_dir,
skills_dir=skills_dir,
)
assert settings.config_dir.exists()
assert settings.cache_dir.exists()
assert settings.data_dir.exists()
assert settings.skills_dir.exists()
def test_settings_env_prefix(self):
"""Test that settings uses SKILLHUB_ env prefix."""
original = os.environ.get("SKILLHUB_LOG_LEVEL")
try:
os.environ["SKILLHUB_LOG_LEVEL"] = "DEBUG"
settings = Settings()
assert settings.log_level == "DEBUG"
finally:
if original:
os.environ["SKILLHUB_LOG_LEVEL"] = original
else:
os.environ.pop("SKILLHUB_LOG_LEVEL", None)
class TestGetConfig:
"""Tests for get_config function."""
def test_get_config_default(self):
"""Test getting default config."""
config = get_config()
assert isinstance(config, Settings)
def test_get_config_with_path(self, temp_config_dir: Path):
"""Test getting config with custom path."""
env_file = temp_config_dir / ".env"
env_file.write_text("SKILLHUB_LOG_LEVEL=WARNING\n")
config = get_config(config_path=str(env_file))
assert config.log_level == "WARNING"
def test_get_config_returns_settings(self):
"""Test that get_config returns Settings instance."""
config = get_config()
assert isinstance(config, Settings)
def test_get_config_with_invalid_json(self, temp_config_dir: Path):
"""Test get_config with invalid JSON file uses defaults."""
config_file = temp_config_dir / CONFIG_FILE_NAME
config_file.write_text("{ invalid json }")
original_dir = os.environ.get("SKILLHUB_CONFIG_DIR")
os.environ["SKILLHUB_CONFIG_DIR"] = str(temp_config_dir)
try:
config = get_config()
assert isinstance(config, Settings)
assert config.log_level == "INFO"
finally:
if original_dir:
os.environ["SKILLHUB_CONFIG_DIR"] = original_dir
else:
os.environ.pop("SKILLHUB_CONFIG_DIR", None)
class TestSaveConfig:
"""Tests for save_config function."""
def test_save_config_creates_file(self, temp_config_dir: Path):
"""Test save_config creates config file."""
settings = Settings(config_dir=temp_config_dir)
save_config(settings)
config_file = temp_config_dir / CONFIG_FILE_NAME
assert config_file.exists()
with open(config_file, "r", encoding="utf-8") as f:
data = json.load(f)
assert data["log_level"] == "INFO"
def test_save_config_preserves_values(self, temp_config_dir: Path):
"""Test save_config preserves all settings."""
settings = Settings(
config_dir=temp_config_dir,
log_level="DEBUG",
cache=CacheConfig(ttl_metadata=3600, enabled=False),
)
save_config(settings)
config_file = temp_config_dir / CONFIG_FILE_NAME
with open(config_file, "r", encoding="utf-8") as f:
data = json.load(f)
assert data["log_level"] == "DEBUG"
assert data["cache"]["ttl_metadata"] == 3600
assert data["cache"]["enabled"] is False
class TestSetConfigValue:
"""Tests for set_config_value function."""
def test_set_config_value_string(self, temp_config_dir: Path):
"""Test setting string config value."""
settings = Settings(config_dir=temp_config_dir)
save_config(settings)
updated = set_config_value("log_level", "WARNING")
assert updated.log_level == "WARNING"
def test_set_config_value_int(self, temp_config_dir: Path):
"""Test setting integer config value."""
settings = Settings(config_dir=temp_config_dir)
save_config(settings)
updated = set_config_value("cache.ttl_metadata", "7200")
assert updated.cache.ttl_metadata == 7200
def test_set_config_value_bool_true(self, temp_config_dir: Path):
"""Test setting boolean config value to true."""
settings = Settings(config_dir=temp_config_dir, cache=CacheConfig(enabled=False))
save_config(settings)
updated = set_config_value("cache.enabled", "true")
assert updated.cache.enabled is True
def test_set_config_value_bool_false(self, temp_config_dir: Path):
"""Test setting boolean config value to false."""
settings = Settings(config_dir=temp_config_dir, cache=CacheConfig(enabled=True))
save_config(settings)
updated = set_config_value("cache.enabled", "false")
assert updated.cache.enabled is False
def test_set_config_value_bool_on(self, temp_config_dir: Path):
"""Test setting boolean with 'on' value."""
settings = Settings(config_dir=temp_config_dir, cache=CacheConfig(enabled=False))
save_config(settings)
updated = set_config_value("cache.enabled", "on")
assert updated.cache.enabled is True
def test_set_config_value_invalid_key(self, temp_config_dir: Path):
"""Test set_config_value raises on invalid key."""
settings = Settings(config_dir=temp_config_dir)
save_config(settings)
with pytest.raises(ValueError, match="Configuration key not found"):
set_config_value("invalid_key", "value")
def test_set_config_value_invalid_nested_key(self, temp_config_dir: Path):
"""Test set_config_value raises on invalid nested key."""
settings = Settings(config_dir=temp_config_dir)
save_config(settings)
with pytest.raises(ValueError, match="Configuration key not found"):
set_config_value("cache.invalid_key", "value")
def test_set_config_value_invalid_type(self, temp_config_dir: Path):
"""Test set_config_value raises on invalid type."""
settings = Settings(config_dir=temp_config_dir)
save_config(settings)
with pytest.raises(ValueError, match="Invalid value type"):
set_config_value("cache.ttl_metadata", "not_an_int")
def test_set_config_value_path(self, temp_config_dir: Path):
"""Test setting Path config value."""
settings = Settings(config_dir=temp_config_dir)
save_config(settings)
new_dir = temp_config_dir / "new_config"
new_dir.mkdir(exist_ok=True)
new_path = str(new_dir)
updated = set_config_value("config_dir", new_path)
assert updated.config_dir == Path(new_path)
def test_set_config_value_list_json(self, temp_config_dir: Path):
"""Test setting list config value with JSON array."""
settings = Settings(config_dir=temp_config_dir)
save_config(settings)
updated = set_config_value("security.trusted_authors", '["author1", "author2"]')
assert updated.security.trusted_authors == ["author1", "author2"]
def test_set_config_value_list_single(self, temp_config_dir: Path):
"""Test setting list config value with single value."""
settings = Settings(config_dir=temp_config_dir)
save_config(settings)
updated = set_config_value("security.trusted_sources", "source1")
assert updated.security.trusted_sources == ["source1"]