"""Tests for configuration loading and merging.""" import copy from pathlib import Path import pytest from derp.config import ( DEFAULTS, _merge, _server_name, build_server_configs, load, resolve_config, ) class TestMerge: """Test deep merge logic.""" def test_flat_override(self): base = {"a": 1, "b": 2} result = _merge(base, {"b": 99}) assert result == {"a": 1, "b": 99} def test_nested_merge(self): base = {"server": {"host": "a", "port": 1}} result = _merge(base, {"server": {"port": 2}}) assert result == {"server": {"host": "a", "port": 2}} def test_list_replacement(self): base = {"channels": ["#old"]} result = _merge(base, {"channels": ["#new"]}) assert result == {"channels": ["#new"]} def test_empty_override(self): base = {"a": 1, "b": 2} result = _merge(base, {}) assert result == {"a": 1, "b": 2} def test_new_keys(self): base = {"a": 1} result = _merge(base, {"b": 2}) assert result == {"a": 1, "b": 2} def test_base_not_mutated(self): base = {"server": {"host": "original"}} original = copy.deepcopy(base) _merge(base, {"server": {"host": "changed"}}) assert base == original class TestLoad: """Test TOML file loading.""" def test_minimal_toml(self, tmp_path: Path): config_file = tmp_path / "test.toml" config_file.write_text('[server]\nnick = "testbot"\n') result = load(config_file) # Overridden value assert result["server"]["nick"] == "testbot" # Default preserved assert result["server"]["host"] == "irc.libera.chat" assert result["server"]["port"] == 6697 def test_full_override(self, tmp_path: Path): config_file = tmp_path / "test.toml" config_file.write_text( '[server]\nhost = "custom.irc"\nport = 6667\n' '[bot]\nprefix = "."\n' ) result = load(config_file) assert result["server"]["host"] == "custom.irc" assert result["server"]["port"] == 6667 assert result["bot"]["prefix"] == "." # Unset defaults still present assert result["bot"]["channels"] == ["#test"] class TestChannelConfig: """Test channel-level configuration merging.""" def test_channel_config_loaded(self, tmp_path: Path): config_file = tmp_path / "test.toml" config_file.write_text( '[channels."#ops"]\nplugins = ["core", "dns"]\n' ) result = load(config_file) assert "#ops" in result["channels"] assert result["channels"]["#ops"]["plugins"] == ["core", "dns"] def test_default_channels_empty(self): assert DEFAULTS["channels"] == {} def test_default_logging_format(self): assert DEFAULTS["logging"]["format"] == "text" class TestResolveConfig: """Test config path resolution and fallback.""" def test_explicit_path(self, tmp_path: Path): config_file = tmp_path / "explicit.toml" config_file.write_text('[server]\nnick = "explicit"\n') result = resolve_config(str(config_file)) assert result["server"]["nick"] == "explicit" def test_no_file_fallback(self, tmp_path: Path, monkeypatch: "pytest.MonkeyPatch"): # Run from tmp_path so config/derp.toml isn't found monkeypatch.chdir(tmp_path) result = resolve_config(str(tmp_path / "nonexistent.toml")) # Should fall back to DEFAULTS assert result["server"]["host"] == DEFAULTS["server"]["host"] assert result["bot"]["prefix"] == DEFAULTS["bot"]["prefix"] def test_defaults_not_mutated(self): original = copy.deepcopy(DEFAULTS) resolve_config(None) assert DEFAULTS == original # --------------------------------------------------------------------------- # _server_name # --------------------------------------------------------------------------- class TestServerName: """Test hostname-to-short-name derivation.""" def test_libera(self): assert _server_name("irc.libera.chat") == "libera" def test_oftc(self): assert _server_name("irc.oftc.net") == "oftc" def test_freenode(self): assert _server_name("chat.freenode.net") == "freenode" def test_plain_hostname(self): assert _server_name("myserver") == "myserver" def test_empty_fallback(self): assert _server_name("") == "" def test_only_common_parts(self): assert _server_name("irc.chat.irc") == "irc.chat.irc" # --------------------------------------------------------------------------- # build_server_configs # --------------------------------------------------------------------------- class TestBuildServerConfigs: """Test multi-server config builder.""" def test_legacy_single_server(self): """Legacy [server] config returns a single-entry dict.""" raw = _merge(DEFAULTS, { "server": {"host": "irc.libera.chat", "nick": "testbot"}, }) result = build_server_configs(raw) assert list(result.keys()) == ["libera"] assert result["libera"]["server"]["nick"] == "testbot" def test_legacy_preserves_full_config(self): """Legacy mode passes through the entire config dict.""" raw = _merge(DEFAULTS, { "server": {"host": "irc.oftc.net"}, "bot": {"prefix": "."}, }) result = build_server_configs(raw) cfg = result["oftc"] assert cfg["bot"]["prefix"] == "." assert cfg["server"]["host"] == "irc.oftc.net" def test_multi_server_creates_entries(self): """Multiple [servers.*] blocks produce multiple entries.""" raw = { "bot": {"prefix": "!", "channels": [], "plugins_dir": "plugins"}, "servers": { "libera": {"host": "irc.libera.chat", "port": 6697, "nick": "derp", "channels": ["#test"]}, "oftc": {"host": "irc.oftc.net", "port": 6697, "nick": "derpbot", "channels": ["#derp"]}, }, } result = build_server_configs(raw) assert set(result.keys()) == {"libera", "oftc"} def test_multi_server_key_separation(self): """Server keys and bot keys are separated correctly.""" raw = { "servers": { "test": { "host": "irc.test.net", "port": 6667, "tls": False, "nick": "bot", "prefix": ".", "channels": ["#a"], "admins": ["*!*@admin"], }, }, } result = build_server_configs(raw) cfg = result["test"] # Server keys assert cfg["server"]["host"] == "irc.test.net" assert cfg["server"]["port"] == 6667 assert cfg["server"]["nick"] == "bot" # Bot keys (overrides) assert cfg["bot"]["prefix"] == "." assert cfg["bot"]["channels"] == ["#a"] assert cfg["bot"]["admins"] == ["*!*@admin"] def test_multi_server_inherits_shared_bot(self): """Per-server configs inherit shared [bot] defaults.""" raw = { "bot": {"prefix": ".", "admins": ["*!*@global"]}, "servers": { "s1": {"host": "irc.s1.net", "nick": "bot1"}, }, } result = build_server_configs(raw) cfg = result["s1"] assert cfg["bot"]["prefix"] == "." assert cfg["bot"]["admins"] == ["*!*@global"] def test_multi_server_overrides_shared_bot(self): """Per-server bot keys override shared [bot] values.""" raw = { "bot": {"prefix": "!", "admins": ["*!*@global"]}, "servers": { "s1": {"host": "irc.s1.net", "nick": "bot1", "prefix": ".", "admins": ["*!*@local"]}, }, } result = build_server_configs(raw) cfg = result["s1"] assert cfg["bot"]["prefix"] == "." assert cfg["bot"]["admins"] == ["*!*@local"] def test_multi_server_defaults_applied(self): """Missing keys fall back to DEFAULTS.""" raw = { "servers": { "minimal": {"host": "irc.min.net", "nick": "m"}, }, } result = build_server_configs(raw) cfg = result["minimal"] assert cfg["server"]["tls"] is True # from DEFAULTS assert cfg["bot"]["prefix"] == "!" # from DEFAULTS assert cfg["bot"]["rate_limit"] == 2.0 def test_multi_server_shared_sections(self): """Shared webhook/logging sections propagate to all servers.""" raw = { "webhook": {"enabled": True, "port": 9090}, "logging": {"format": "json"}, "servers": { "a": {"host": "irc.a.net", "nick": "a"}, "b": {"host": "irc.b.net", "nick": "b"}, }, } result = build_server_configs(raw) for name in ("a", "b"): assert result[name]["webhook"]["enabled"] is True assert result[name]["webhook"]["port"] == 9090 assert result[name]["logging"]["format"] == "json" def test_empty_servers_section_falls_back(self): """Empty [servers] section treated as legacy single-server.""" raw = _merge(DEFAULTS, {"servers": {}}) result = build_server_configs(raw) assert len(result) == 1 def test_no_servers_key_is_legacy(self): """Config without [servers] is legacy single-server mode.""" raw = copy.deepcopy(DEFAULTS) result = build_server_configs(raw) assert len(result) == 1 name = list(result.keys())[0] assert result[name] is raw class TestProxyDefaults: """Verify proxy defaults in each adapter section.""" def test_server_proxy_default_false(self): assert DEFAULTS["server"]["proxy"] is False def test_teams_proxy_default_true(self): assert DEFAULTS["teams"]["proxy"] is True def test_telegram_proxy_default_true(self): assert DEFAULTS["telegram"]["proxy"] is True def test_mumble_proxy_default_true(self): assert DEFAULTS["mumble"]["proxy"] is True