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>
299 lines
10 KiB
Python
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
|