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:
37
tests/test_captcha.py
Normal file
37
tests/test_captcha.py
Normal 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"
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user