"""Tests for file-based configuration system.""" import asyncio import tempfile import yaml from pathlib import Path from unittest.mock import patch import pytest from guardden.services.file_config import FileConfigurationManager, ConfigurationError class TestFileConfigurationManager: """Tests for the file-based configuration manager.""" @pytest.fixture def temp_config_dir(self): """Create a temporary configuration directory.""" with tempfile.TemporaryDirectory() as temp_dir: yield temp_dir @pytest.fixture async def config_manager(self, temp_config_dir): """Create a configuration manager with temporary directory.""" manager = FileConfigurationManager(temp_config_dir) await manager.initialize() yield manager await manager.shutdown() def test_directory_creation(self, temp_config_dir): """Test that required directories are created.""" manager = FileConfigurationManager(temp_config_dir) expected_dirs = [ "guilds", "wordlists", "schemas", "templates", "backups" ] for dir_name in expected_dirs: assert (Path(temp_config_dir) / dir_name).exists() @pytest.mark.asyncio async def test_guild_config_creation(self, config_manager, temp_config_dir): """Test creating a new guild configuration.""" guild_id = 123456789 name = "Test Guild" owner_id = 987654321 file_path = await config_manager.create_guild_config(guild_id, name, owner_id) assert file_path.exists() assert file_path.name == f"guild-{guild_id}.yml" # Verify content with open(file_path, 'r') as f: content = yaml.safe_load(f) assert content["guild_id"] == guild_id assert content["name"] == name assert content["owner_id"] == owner_id assert "settings" in content @pytest.mark.asyncio async def test_duplicate_guild_config_creation(self, config_manager): """Test that creating duplicate guild configs raises error.""" guild_id = 123456789 name = "Test Guild" # Create first config await config_manager.create_guild_config(guild_id, name) # Attempt to create duplicate should raise error with pytest.raises(ConfigurationError): await config_manager.create_guild_config(guild_id, name) @pytest.mark.asyncio async def test_guild_config_loading(self, config_manager, temp_config_dir): """Test loading guild configuration from file.""" guild_id = 123456789 # Create config file manually config_data = { "guild_id": guild_id, "name": "Test Guild", "premium": False, "settings": { "general": { "prefix": "!", "locale": "en" }, "ai_moderation": { "enabled": True, "sensitivity": 80, "nsfw_only_filtering": True } } } guild_dir = Path(temp_config_dir) / "guilds" guild_file = guild_dir / f"guild-{guild_id}.yml" with open(guild_file, 'w') as f: yaml.dump(config_data, f) # Load config await config_manager._load_guild_config(guild_file) # Verify loaded config config = config_manager.get_guild_config(guild_id) assert config is not None assert config.guild_id == guild_id assert config.name == "Test Guild" assert config.settings["ai_moderation"]["nsfw_only_filtering"] is True @pytest.mark.asyncio async def test_invalid_config_validation(self, config_manager, temp_config_dir): """Test that invalid configurations are rejected.""" guild_id = 123456789 # Create invalid config (missing required fields) invalid_config = { "name": "Test Guild", # Missing guild_id "settings": {} } guild_dir = Path(temp_config_dir) / "guilds" guild_file = guild_dir / f"guild-{guild_id}.yml" with open(guild_file, 'w') as f: yaml.dump(invalid_config, f) # Should return None for invalid config result = await config_manager._load_guild_config(guild_file) assert result is None def test_configuration_validation(self, config_manager): """Test configuration validation against schema.""" valid_config = { "guild_id": 123456789, "name": "Test Guild", "settings": { "general": {"prefix": "!", "locale": "en"}, "channels": {"log_channel_id": None}, "roles": {"mod_role_ids": []}, "moderation": {"automod_enabled": True}, "automod": {"message_rate_limit": 5}, "ai_moderation": {"enabled": True}, "verification": {"enabled": False} } } # Should return no errors for valid config errors = config_manager.validate_config(valid_config) assert len(errors) == 0 # Invalid config should return errors invalid_config = { "guild_id": "not-a-number", # Should be integer "name": "Test Guild" # Missing required settings } errors = config_manager.validate_config(invalid_config) assert len(errors) > 0 @pytest.mark.asyncio async def test_wordlist_config_loading(self, config_manager, temp_config_dir): """Test loading wordlist configurations.""" wordlist_dir = Path(temp_config_dir) / "wordlists" # Create banned words config banned_words_config = { "global_patterns": [ { "pattern": "badword", "action": "delete", "is_regex": False, "category": "profanity" } ] } with open(wordlist_dir / "banned-words.yml", 'w') as f: yaml.dump(banned_words_config, f) # Create allowlist config allowlist_config = { "global_allowlist": [ { "domain": "discord.com", "reason": "Official Discord domain" } ] } with open(wordlist_dir / "domain-allowlists.yml", 'w') as f: yaml.dump(allowlist_config, f) # Load configs await config_manager._load_wordlist_configs() # Verify loaded configs wordlist_config = config_manager.get_wordlist_config() assert wordlist_config is not None assert "global_patterns" in wordlist_config assert len(wordlist_config["global_patterns"]) == 1 allowlist = config_manager.get_allowlist_config() assert allowlist is not None assert "global_allowlist" in allowlist assert len(allowlist["global_allowlist"]) == 1 @pytest.mark.asyncio async def test_config_backup(self, config_manager, temp_config_dir): """Test configuration backup functionality.""" guild_id = 123456789 # Create a guild config await config_manager.create_guild_config(guild_id, "Test Guild") # Create backup backup_path = await config_manager.backup_config(guild_id) assert backup_path.exists() assert "backup" in backup_path.parent.name assert f"guild-{guild_id}" in backup_path.name # Verify backup content matches original original_config = config_manager.get_guild_config(guild_id) with open(backup_path, 'r') as f: backup_content = yaml.safe_load(f) assert backup_content["guild_id"] == original_config.guild_id assert backup_content["name"] == original_config.name @pytest.mark.asyncio async def test_config_change_callbacks(self, config_manager, temp_config_dir): """Test that configuration change callbacks are triggered.""" callback_called = False callback_guild_id = None def test_callback(guild_id, config): nonlocal callback_called, callback_guild_id callback_called = True callback_guild_id = guild_id # Register callback config_manager.register_change_callback(test_callback) # Create config (should trigger callback) guild_id = 123456789 await config_manager.create_guild_config(guild_id, "Test Guild") # Wait a moment for callback to be called await asyncio.sleep(0.1) assert callback_called assert callback_guild_id == guild_id def test_all_guild_configs_retrieval(self, config_manager, temp_config_dir): """Test retrieving all guild configurations.""" # Initially should be empty all_configs = config_manager.get_all_guild_configs() assert len(all_configs) == 0 @pytest.mark.asyncio async def test_nsfw_only_filtering_in_config(self, config_manager): """Test that NSFW-only filtering setting is properly handled.""" guild_id = 123456789 # Create config with NSFW-only filtering enabled file_path = await config_manager.create_guild_config(guild_id, "NSFW Test Guild") # Load and modify config to enable NSFW-only filtering with open(file_path, 'r') as f: config_data = yaml.safe_load(f) config_data["settings"]["ai_moderation"]["nsfw_only_filtering"] = True with open(file_path, 'w') as f: yaml.dump(config_data, f) # Reload config await config_manager._load_guild_config(file_path) # Verify NSFW-only filtering is enabled config = config_manager.get_guild_config(guild_id) assert config is not None assert config.settings["ai_moderation"]["nsfw_only_filtering"] is True