351 lines
13 KiB
Python
351 lines
13 KiB
Python
"""Unit tests for ConfigService."""
|
|
|
|
import json
|
|
import tempfile
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from src.server.models.config import (
|
|
AppConfig,
|
|
BackupConfig,
|
|
ConfigUpdate,
|
|
LoggingConfig,
|
|
SchedulerConfig,
|
|
)
|
|
from src.server.services.config_service import (
|
|
ConfigBackupError,
|
|
ConfigService,
|
|
ConfigServiceError,
|
|
ConfigValidationError,
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def temp_dir():
|
|
"""Create temporary directory for test config files."""
|
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
yield Path(tmpdir)
|
|
|
|
|
|
@pytest.fixture
|
|
def config_service(temp_dir):
|
|
"""Create ConfigService instance with temporary paths."""
|
|
config_path = temp_dir / "config.json"
|
|
backup_dir = temp_dir / "backups"
|
|
return ConfigService(
|
|
config_path=config_path, backup_dir=backup_dir, max_backups=3
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def sample_config():
|
|
"""Create sample configuration."""
|
|
return AppConfig(
|
|
name="TestApp",
|
|
data_dir="test_data",
|
|
scheduler=SchedulerConfig(enabled=True, interval_minutes=30),
|
|
logging=LoggingConfig(level="DEBUG", file="test.log"),
|
|
backup=BackupConfig(enabled=False),
|
|
other={"custom_key": "custom_value"},
|
|
)
|
|
|
|
|
|
class TestConfigServiceInitialization:
|
|
"""Test ConfigService initialization and directory creation."""
|
|
|
|
def test_initialization_creates_directories(self, temp_dir):
|
|
"""Test that initialization creates necessary directories."""
|
|
config_path = temp_dir / "subdir" / "config.json"
|
|
backup_dir = temp_dir / "subdir" / "backups"
|
|
|
|
service = ConfigService(config_path=config_path, backup_dir=backup_dir)
|
|
|
|
assert config_path.parent.exists()
|
|
assert backup_dir.exists()
|
|
assert service.config_path == config_path
|
|
assert service.backup_dir == backup_dir
|
|
|
|
def test_initialization_with_existing_directories(self, config_service):
|
|
"""Test initialization with existing directories works."""
|
|
assert config_service.config_path.parent.exists()
|
|
assert config_service.backup_dir.exists()
|
|
|
|
|
|
class TestConfigServiceLoadSave:
|
|
"""Test configuration loading and saving."""
|
|
|
|
def test_load_creates_default_config_if_not_exists(self, config_service):
|
|
"""Test that load creates default config if file doesn't exist."""
|
|
config = config_service.load_config()
|
|
|
|
assert isinstance(config, AppConfig)
|
|
assert config.name == "Aniworld"
|
|
assert config_service.config_path.exists()
|
|
|
|
def test_save_and_load_config(self, config_service, sample_config):
|
|
"""Test saving and loading configuration."""
|
|
config_service.save_config(sample_config, create_backup=False)
|
|
|
|
loaded_config = config_service.load_config()
|
|
|
|
assert loaded_config.name == sample_config.name
|
|
assert loaded_config.data_dir == sample_config.data_dir
|
|
assert loaded_config.scheduler.enabled == sample_config.scheduler.enabled
|
|
assert loaded_config.logging.level == sample_config.logging.level
|
|
assert loaded_config.other == sample_config.other
|
|
|
|
def test_save_includes_version(self, config_service, sample_config):
|
|
"""Test that saved config includes version field."""
|
|
config_service.save_config(sample_config, create_backup=False)
|
|
|
|
with open(config_service.config_path, "r", encoding="utf-8") as f:
|
|
data = json.load(f)
|
|
|
|
assert "version" in data
|
|
assert data["version"] == ConfigService.CONFIG_VERSION
|
|
|
|
def test_save_creates_backup_by_default(self, config_service, sample_config):
|
|
"""Test that save creates backup by default if file exists."""
|
|
# Save initial config
|
|
config_service.save_config(sample_config, create_backup=False)
|
|
|
|
# Modify and save again (should create backup)
|
|
sample_config.name = "Modified"
|
|
config_service.save_config(sample_config, create_backup=True)
|
|
|
|
backups = list(config_service.backup_dir.glob("*.json"))
|
|
assert len(backups) == 1
|
|
|
|
def test_save_atomic_operation(self, config_service, sample_config):
|
|
"""Test that save is atomic (uses temp file)."""
|
|
# Mock exception during JSON dump by using invalid data
|
|
# This should not corrupt existing config
|
|
config_service.save_config(sample_config, create_backup=False)
|
|
|
|
# Verify temp file is cleaned up after successful save
|
|
temp_files = list(config_service.config_path.parent.glob("*.tmp"))
|
|
assert len(temp_files) == 0
|
|
|
|
def test_load_invalid_json_raises_error(self, config_service):
|
|
"""Test that loading invalid JSON raises ConfigValidationError."""
|
|
# Write invalid JSON
|
|
config_service.config_path.write_text("invalid json {")
|
|
|
|
with pytest.raises(ConfigValidationError, match="Invalid JSON"):
|
|
config_service.load_config()
|
|
|
|
|
|
class TestConfigServiceValidation:
|
|
"""Test configuration validation."""
|
|
|
|
def test_validate_valid_config(self, config_service, sample_config):
|
|
"""Test validation of valid configuration."""
|
|
result = config_service.validate_config(sample_config)
|
|
|
|
assert result.valid is True
|
|
assert result.errors == []
|
|
|
|
def test_validate_invalid_config(self, config_service):
|
|
"""Test validation of invalid configuration."""
|
|
# Create config with backups enabled but no path
|
|
invalid_config = AppConfig(
|
|
backup=BackupConfig(enabled=True, path=None)
|
|
)
|
|
|
|
result = config_service.validate_config(invalid_config)
|
|
|
|
assert result.valid is False
|
|
assert len(result.errors or []) > 0
|
|
|
|
def test_save_invalid_config_raises_error(self, config_service):
|
|
"""Test that saving invalid config raises error."""
|
|
invalid_config = AppConfig(
|
|
backup=BackupConfig(enabled=True, path=None)
|
|
)
|
|
|
|
with pytest.raises(ConfigValidationError, match="Cannot save invalid"):
|
|
config_service.save_config(invalid_config)
|
|
|
|
|
|
class TestConfigServiceUpdate:
|
|
"""Test configuration updates."""
|
|
|
|
def test_update_config(self, config_service, sample_config):
|
|
"""Test updating configuration."""
|
|
config_service.save_config(sample_config, create_backup=False)
|
|
|
|
update = ConfigUpdate(
|
|
scheduler=SchedulerConfig(enabled=False, interval_minutes=60),
|
|
logging=LoggingConfig(level="INFO"),
|
|
)
|
|
|
|
updated_config = config_service.update_config(update)
|
|
|
|
assert updated_config.scheduler.enabled is False
|
|
assert updated_config.scheduler.interval_minutes == 60
|
|
assert updated_config.logging.level == "INFO"
|
|
# Other fields should remain unchanged
|
|
assert updated_config.name == sample_config.name
|
|
assert updated_config.data_dir == sample_config.data_dir
|
|
|
|
def test_update_persists_changes(self, config_service, sample_config):
|
|
"""Test that updates are persisted to disk."""
|
|
config_service.save_config(sample_config, create_backup=False)
|
|
|
|
update = ConfigUpdate(logging=LoggingConfig(level="ERROR"))
|
|
config_service.update_config(update)
|
|
|
|
# Load fresh config from disk
|
|
loaded = config_service.load_config()
|
|
assert loaded.logging.level == "ERROR"
|
|
|
|
|
|
class TestConfigServiceBackups:
|
|
"""Test configuration backup functionality."""
|
|
|
|
def test_create_backup(self, config_service, sample_config):
|
|
"""Test creating configuration backup."""
|
|
config_service.save_config(sample_config, create_backup=False)
|
|
|
|
backup_path = config_service.create_backup()
|
|
|
|
assert backup_path.exists()
|
|
assert backup_path.suffix == ".json"
|
|
assert "config_backup_" in backup_path.name
|
|
|
|
def test_create_backup_with_custom_name(
|
|
self, config_service, sample_config
|
|
):
|
|
"""Test creating backup with custom name."""
|
|
config_service.save_config(sample_config, create_backup=False)
|
|
|
|
backup_path = config_service.create_backup(name="my_backup")
|
|
|
|
assert backup_path.name == "my_backup.json"
|
|
|
|
def test_create_backup_without_config_raises_error(self, config_service):
|
|
"""Test that creating backup without config file raises error."""
|
|
with pytest.raises(ConfigBackupError, match="Cannot backup non-existent"):
|
|
config_service.create_backup()
|
|
|
|
def test_list_backups(self, config_service, sample_config):
|
|
"""Test listing configuration backups."""
|
|
config_service.save_config(sample_config, create_backup=False)
|
|
|
|
# Create multiple backups
|
|
config_service.create_backup(name="backup1")
|
|
config_service.create_backup(name="backup2")
|
|
config_service.create_backup(name="backup3")
|
|
|
|
backups = config_service.list_backups()
|
|
|
|
assert len(backups) == 3
|
|
assert all("name" in b for b in backups)
|
|
assert all("size_bytes" in b for b in backups)
|
|
assert all("created_at" in b for b in backups)
|
|
|
|
# Should be sorted by creation time (newest first)
|
|
backup_names = [b["name"] for b in backups]
|
|
assert "backup3.json" in backup_names
|
|
|
|
def test_list_backups_empty(self, config_service):
|
|
"""Test listing backups when none exist."""
|
|
backups = config_service.list_backups()
|
|
assert backups == []
|
|
|
|
def test_restore_backup(self, config_service, sample_config):
|
|
"""Test restoring configuration from backup."""
|
|
# Save initial config and create backup
|
|
config_service.save_config(sample_config, create_backup=False)
|
|
config_service.create_backup(name="original")
|
|
|
|
# Modify and save config
|
|
sample_config.name = "Modified"
|
|
config_service.save_config(sample_config, create_backup=False)
|
|
|
|
# Restore from backup
|
|
restored = config_service.restore_backup("original.json")
|
|
|
|
assert restored.name == "TestApp" # Original name
|
|
|
|
def test_restore_backup_creates_pre_restore_backup(
|
|
self, config_service, sample_config
|
|
):
|
|
"""Test that restore creates pre-restore backup."""
|
|
config_service.save_config(sample_config, create_backup=False)
|
|
config_service.create_backup(name="backup1")
|
|
|
|
sample_config.name = "Modified"
|
|
config_service.save_config(sample_config, create_backup=False)
|
|
|
|
config_service.restore_backup("backup1.json")
|
|
|
|
backups = config_service.list_backups()
|
|
backup_names = [b["name"] for b in backups]
|
|
|
|
assert any("pre_restore" in name for name in backup_names)
|
|
|
|
def test_restore_nonexistent_backup_raises_error(self, config_service):
|
|
"""Test that restoring non-existent backup raises error."""
|
|
with pytest.raises(ConfigBackupError, match="Backup not found"):
|
|
config_service.restore_backup("nonexistent.json")
|
|
|
|
def test_delete_backup(self, config_service, sample_config):
|
|
"""Test deleting configuration backup."""
|
|
config_service.save_config(sample_config, create_backup=False)
|
|
config_service.create_backup(name="to_delete")
|
|
|
|
config_service.delete_backup("to_delete.json")
|
|
|
|
backups = config_service.list_backups()
|
|
assert len(backups) == 0
|
|
|
|
def test_delete_nonexistent_backup_raises_error(self, config_service):
|
|
"""Test that deleting non-existent backup raises error."""
|
|
with pytest.raises(ConfigBackupError, match="Backup not found"):
|
|
config_service.delete_backup("nonexistent.json")
|
|
|
|
def test_cleanup_old_backups(self, config_service, sample_config):
|
|
"""Test that old backups are cleaned up when limit exceeded."""
|
|
config_service.save_config(sample_config, create_backup=False)
|
|
|
|
# Create more backups than max_backups (3)
|
|
for i in range(5):
|
|
config_service.create_backup(name=f"backup{i}")
|
|
|
|
backups = config_service.list_backups()
|
|
assert len(backups) == 3 # Should only keep max_backups
|
|
|
|
|
|
class TestConfigServiceSingleton:
|
|
"""Test singleton instance management."""
|
|
|
|
def test_get_config_service_returns_singleton(self):
|
|
"""Test that get_config_service returns same instance."""
|
|
from src.server.services.config_service import get_config_service
|
|
|
|
service1 = get_config_service()
|
|
service2 = get_config_service()
|
|
|
|
assert service1 is service2
|
|
|
|
|
|
class TestConfigServiceErrorHandling:
|
|
"""Test error handling in ConfigService."""
|
|
|
|
def test_save_config_creates_temp_file(
|
|
self, config_service, sample_config
|
|
):
|
|
"""Test that save operation uses temporary file."""
|
|
# Save config and verify temp file is cleaned up
|
|
config_service.save_config(sample_config, create_backup=False)
|
|
|
|
# Verify no temp files remain
|
|
temp_files = list(config_service.config_path.parent.glob("*.tmp"))
|
|
assert len(temp_files) == 0
|
|
|
|
# Verify config was saved successfully
|
|
loaded = config_service.load_config()
|
|
assert loaded.name == sample_config.name
|