feat: make all operational constants configurable via bouncer.toml

Replace hardcoded values across network, captcha, email, and cert
modules with BouncerConfig fields. All values have safe defaults
and are overridable in the [bouncer] section of the config file.

Configurable: probation_seconds, backoff_steps, nick_timeout,
rejoin_delay, http_timeout, captcha_poll_interval/timeout,
email_poll_interval/max_polls/request_timeout, cert_validity_days.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
user
2026-02-21 16:33:08 +01:00
parent ed576b002d
commit d13d090e8e
14 changed files with 506 additions and 97 deletions

37
tests/test_captcha.py Normal file
View File

@@ -0,0 +1,37 @@
"""Tests for bouncer.captcha module."""
from bouncer.captcha import _extract_sitekey
class TestExtractSitekey:
"""Test hCaptcha sitekey extraction from HTML."""
def test_extracts_sitekey_from_div(self) -> None:
html = '<div class="h-captcha" data-sitekey="a1b2c3d4-e5f6-7890-abcd-ef1234567890"></div>'
assert _extract_sitekey(html) == "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
def test_extracts_sitekey_single_quotes(self) -> None:
html = "<div class='h-captcha' data-sitekey='10000000-ffff-ffff-ffff-000000000001'></div>"
assert _extract_sitekey(html) == "10000000-ffff-ffff-ffff-000000000001"
def test_returns_none_no_sitekey(self) -> None:
html = "<div>No captcha here</div>"
assert _extract_sitekey(html) is None
def test_returns_none_empty_html(self) -> None:
assert _extract_sitekey("") is None
def test_extracts_from_full_page(self) -> None:
html = """<!DOCTYPE html>
<html>
<head><title>Verify</title></head>
<body>
<form action="" method="POST">
<input type="hidden" name="token" value="abc123">
<div class="h-captcha" data-sitekey="abcdef01-2345-6789-abcd-ef0123456789"
data-callback="on_success"></div>
<input type="submit" value="Verify">
</form>
</body>
</html>"""
assert _extract_sitekey(html) == "abcdef01-2345-6789-abcd-ef0123456789"

View File

@@ -53,6 +53,15 @@ class TestGenerateCert:
assert pem1 == pem2
assert fp1 != fp2 # New cert = new fingerprint
def test_custom_validity_days(self, data_dir: Path) -> None:
import datetime
from cryptography import x509 as x509_mod
pem = generate_cert(data_dir, "libera", "testnick", validity_days=365)
cert_data = pem.read_bytes()
cert_obj = x509_mod.load_pem_x509_certificate(cert_data)
delta = cert_obj.not_valid_after_utc - cert_obj.not_valid_before_utc
assert 364 <= delta.days <= 366
class TestFingerprint:
def test_format(self, data_dir: Path) -> None:

View File

@@ -51,6 +51,7 @@ def _make_router(*networks: MagicMock) -> MagicMock:
router.add_network = AsyncMock()
router.remove_network = AsyncMock(return_value=True)
router.config = MagicMock()
router.config.bouncer.cert_validity_days = 3650
return router
@@ -143,7 +144,7 @@ class TestInfo:
host="user/fabesune", channels={"#test", "#dev"})
router = _make_router(net)
router.backlog.list_nickserv_creds.return_value = [
("libera", "fabesune", "test@mail.tm", "user/fabesune", 1700000000.0, "verified"),
("libera", "fabesune", "test@mail.tm", "user/fabesune", 1700000000.0, "verified", ""),
]
client = _make_client()
lines = await commands.dispatch("INFO libera", router, client)
@@ -218,14 +219,15 @@ class TestCreds:
net = _make_network("libera", State.READY)
router = _make_router(net)
router.backlog.list_nickserv_creds.return_value = [
("libera", "fabesune", "test@mail.tm", "user/fabesune", 1700000000.0, "verified"),
("libera", "oldnick", "old@mail.tm", "old/host", 1699000000.0, "pending"),
("libera", "fabesune", "test@mail.tm", "user/fabesune", 1700000000.0, "verified", ""),
("libera", "oldnick", "old@mail.tm", "old/host", 1699000000.0, "pending", "https://example.com/verify/abc"),
]
client = _make_client()
lines = await commands.dispatch("CREDS libera", router, client)
assert lines[0] == "[CREDS]"
assert any("+" in line and "fabesune" in line and "verified" in line for line in lines)
assert any("~" in line and "oldnick" in line and "pending" in line for line in lines)
assert any("verify: https://example.com/verify/abc" in line for line in lines)
@pytest.mark.asyncio
async def test_creds_unknown_network(self) -> None:
@@ -758,8 +760,8 @@ class TestDropCreds:
net = _make_network("libera", State.READY)
router = _make_router(net)
router.backlog.list_nickserv_creds.return_value = [
("libera", "nick1", "a@b.c", "", 0.0, "verified"),
("libera", "nick2", "d@e.f", "", 0.0, "pending"),
("libera", "nick1", "a@b.c", "", 0.0, "verified", ""),
("libera", "nick2", "d@e.f", "", 0.0, "pending", ""),
]
client = _make_client()
lines = await commands.dispatch("DROPCREDS libera", router, client)

View File

@@ -123,3 +123,58 @@ tls = true
"""
cfg = load(_write_config(config))
assert cfg.networks["test"].port == 6697
def test_operational_defaults(self):
"""Ensure all operational values have sane defaults."""
cfg = load(_write_config(MINIMAL_CONFIG))
b = cfg.bouncer
assert b.probation_seconds == 45
assert b.backoff_steps == [5, 10, 30, 60, 120, 300]
assert b.nick_timeout == 10
assert b.rejoin_delay == 3
assert b.http_timeout == 15
assert b.captcha_api_key == ""
assert b.captcha_poll_interval == 3
assert b.captcha_poll_timeout == 120
assert b.email_poll_interval == 15
assert b.email_max_polls == 30
assert b.email_request_timeout == 20
assert b.cert_validity_days == 3650
def test_operational_overrides(self):
"""Configurable operational values are parsed from TOML."""
config = """\
[bouncer]
password = "x"
probation_seconds = 60
backoff_steps = [10, 30, 90]
nick_timeout = 20
rejoin_delay = 5
http_timeout = 30
captcha_api_key = "test-key"
captcha_poll_interval = 5
captcha_poll_timeout = 60
email_poll_interval = 10
email_max_polls = 20
email_request_timeout = 25
cert_validity_days = 365
[proxy]
[networks.test]
host = "irc.example.com"
"""
cfg = load(_write_config(config))
b = cfg.bouncer
assert b.probation_seconds == 60
assert b.backoff_steps == [10, 30, 90]
assert b.nick_timeout == 20
assert b.rejoin_delay == 5
assert b.http_timeout == 30
assert b.captcha_api_key == "test-key"
assert b.captcha_poll_interval == 5
assert b.captcha_poll_timeout == 60
assert b.email_poll_interval == 10
assert b.email_max_polls == 20
assert b.email_request_timeout == 25
assert b.cert_validity_days == 365