Aniworld/tests/unit/test_config_service.py
2025-12-10 21:12:34 +01:00

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