Files
derp/tests/test_config.py
user 9d4cb09069 feat: make SOCKS5 proxy configurable per adapter
Add `proxy` config option to server (IRC), teams, telegram, and mumble
sections. IRC defaults to false (preserving current direct-connect
behavior); all others default to true. The `derp.http` module now
accepts `proxy=True/False` on urlopen, create_connection,
open_connection, and build_opener -- when false, uses stdlib directly.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 21:19:22 +01:00

299 lines
10 KiB
Python

"""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