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>
145 lines
5.0 KiB
Python
145 lines
5.0 KiB
Python
"""Tests for client certificate management."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from bouncer.cert import (
|
|
cert_path,
|
|
delete_cert,
|
|
fingerprint,
|
|
generate_cert,
|
|
has_cert,
|
|
list_certs,
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def data_dir(tmp_path: Path) -> Path:
|
|
"""Provide a temporary data directory."""
|
|
return tmp_path
|
|
|
|
|
|
class TestCertPath:
|
|
def test_standard_path(self, data_dir: Path) -> None:
|
|
p = cert_path(data_dir, "libera", "fabesune")
|
|
assert p == data_dir / "certs" / "libera" / "fabesune.pem"
|
|
|
|
|
|
class TestGenerateCert:
|
|
def test_creates_pem_file(self, data_dir: Path) -> None:
|
|
pem = generate_cert(data_dir, "libera", "testnick")
|
|
assert pem.is_file()
|
|
assert pem == cert_path(data_dir, "libera", "testnick")
|
|
|
|
def test_pem_contains_cert_and_key(self, data_dir: Path) -> None:
|
|
pem = generate_cert(data_dir, "libera", "testnick")
|
|
content = pem.read_text()
|
|
assert "BEGIN CERTIFICATE" in content
|
|
assert "BEGIN PRIVATE KEY" in content
|
|
|
|
def test_file_permissions(self, data_dir: Path) -> None:
|
|
pem = generate_cert(data_dir, "libera", "testnick")
|
|
mode = pem.stat().st_mode & 0o777
|
|
assert mode == 0o600
|
|
|
|
def test_overwrites_existing(self, data_dir: Path) -> None:
|
|
pem1 = generate_cert(data_dir, "libera", "testnick")
|
|
fp1 = fingerprint(pem1)
|
|
pem2 = generate_cert(data_dir, "libera", "testnick")
|
|
fp2 = fingerprint(pem2)
|
|
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:
|
|
pem = generate_cert(data_dir, "libera", "testnick")
|
|
fp = fingerprint(pem)
|
|
parts = fp.split(":")
|
|
assert len(parts) == 32 # SHA-256 = 32 bytes
|
|
for part in parts:
|
|
assert len(part) == 2
|
|
int(part, 16) # Must be valid hex
|
|
|
|
def test_uppercase_hex(self, data_dir: Path) -> None:
|
|
pem = generate_cert(data_dir, "libera", "testnick")
|
|
fp = fingerprint(pem)
|
|
assert fp == fp.upper()
|
|
|
|
def test_deterministic_for_same_cert(self, data_dir: Path) -> None:
|
|
pem = generate_cert(data_dir, "libera", "testnick")
|
|
assert fingerprint(pem) == fingerprint(pem)
|
|
|
|
|
|
class TestHasCert:
|
|
def test_exists(self, data_dir: Path) -> None:
|
|
generate_cert(data_dir, "libera", "testnick")
|
|
assert has_cert(data_dir, "libera", "testnick") is True
|
|
|
|
def test_not_exists(self, data_dir: Path) -> None:
|
|
assert has_cert(data_dir, "libera", "testnick") is False
|
|
|
|
|
|
class TestDeleteCert:
|
|
def test_delete_existing(self, data_dir: Path) -> None:
|
|
generate_cert(data_dir, "libera", "testnick")
|
|
assert delete_cert(data_dir, "libera", "testnick") is True
|
|
assert has_cert(data_dir, "libera", "testnick") is False
|
|
|
|
def test_delete_nonexistent(self, data_dir: Path) -> None:
|
|
assert delete_cert(data_dir, "libera", "testnick") is False
|
|
|
|
def test_cleans_empty_dir(self, data_dir: Path) -> None:
|
|
generate_cert(data_dir, "libera", "testnick")
|
|
delete_cert(data_dir, "libera", "testnick")
|
|
assert not (data_dir / "certs" / "libera").exists()
|
|
|
|
|
|
class TestListCerts:
|
|
def test_empty(self, data_dir: Path) -> None:
|
|
assert list_certs(data_dir) == []
|
|
|
|
def test_list_all(self, data_dir: Path) -> None:
|
|
generate_cert(data_dir, "libera", "nick1")
|
|
generate_cert(data_dir, "oftc", "nick2")
|
|
certs = list_certs(data_dir)
|
|
assert len(certs) == 2
|
|
networks = {c[0] for c in certs}
|
|
assert networks == {"libera", "oftc"}
|
|
|
|
def test_list_by_network(self, data_dir: Path) -> None:
|
|
generate_cert(data_dir, "libera", "nick1")
|
|
generate_cert(data_dir, "oftc", "nick2")
|
|
certs = list_certs(data_dir, network="libera")
|
|
assert len(certs) == 1
|
|
assert certs[0][0] == "libera"
|
|
assert certs[0][1] == "nick1"
|
|
|
|
def test_list_multiple_per_network(self, data_dir: Path) -> None:
|
|
generate_cert(data_dir, "libera", "nick1")
|
|
generate_cert(data_dir, "libera", "nick2")
|
|
certs = list_certs(data_dir, network="libera")
|
|
assert len(certs) == 2
|
|
nicks = {c[1] for c in certs}
|
|
assert nicks == {"nick1", "nick2"}
|
|
|
|
def test_fingerprints_present(self, data_dir: Path) -> None:
|
|
generate_cert(data_dir, "libera", "testnick")
|
|
certs = list_certs(data_dir)
|
|
assert len(certs) == 1
|
|
_, _, fp = certs[0]
|
|
assert ":" in fp
|
|
assert len(fp.split(":")) == 32
|