Files
netbox-plugin-dns-bridge/netbox_dns_bridge/tests/test_notify_handler.py
T
sven 4ad110ca53 new file: conftest.py
new file:   netbox_dns_bridge/tests/__init__.py
new file:   netbox_dns_bridge/tests/_helpers.py
new file:   netbox_dns_bridge/tests/conftest.py
new file:   netbox_dns_bridge/tests/test_bootstrap.py
new file:   netbox_dns_bridge/tests/test_catalog_zone_manager.py
new file:   netbox_dns_bridge/tests/test_dns_server.py
new file:   netbox_dns_bridge/tests/test_management_commands.py
new file:   netbox_dns_bridge/tests/test_models.py
new file:   netbox_dns_bridge/tests/test_notify_handler.py
new file:   netbox_dns_bridge/tests/test_request_handler_integration.py
new file:   netbox_dns_bridge/tests/test_request_handler_unit.py
new file:   netbox_dns_bridge/tests/test_signals.py
new file:   netbox_dns_bridge/tests/test_smoke.py
new file:   netbox_dns_bridge/tests/test_utils.py
new file:   netbox_dns_bridge/tests/test_views.py
modified:   pyproject.toml
Add some tests. #AI generated
2026-08-12 12:25:46 +02:00

322 lines
12 KiB
Python

"""Tests for notify_handler: _load_tsig_key, _get_soa_serial, _resolve_zone_nameserver_ips,
schedule_client_notify, schedule_ns_notify, and job classes."""
from unittest.mock import patch, MagicMock
import dns.name
import dns.rdatatype
import dns.tsig
import pytest
import netbox_dns_bridge.signals.notify_handler as nh
class TestLoadTsigKey:
def _patch_settings(self, monkeypatch, tsig_keys):
monkeypatch.setattr(nh, "SETTINGS", {"tsig_keys": tsig_keys})
def test_valid_config(self, monkeypatch):
self._patch_settings(monkeypatch, {
"myview": {
"keyname": "testkey.",
"secret": "YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWE=",
"algorithm": "hmac-sha256",
}
})
key = nh._load_tsig_key("myview")
assert isinstance(key, dns.tsig.Key)
assert key.name == dns.name.from_text("testkey.", dns.name.root).canonicalize()
def test_missing_view_raises(self, monkeypatch):
self._patch_settings(monkeypatch, {})
with pytest.raises(RuntimeError, match="No TSIG key configured"):
nh._load_tsig_key("nope")
def test_missing_algorithm_raises(self, monkeypatch):
self._patch_settings(monkeypatch, {
"v": {"keyname": "k.", "secret": "s"}
})
with pytest.raises(RuntimeError, match="Missing 'algorithm'"):
nh._load_tsig_key("v")
def test_missing_keyname_raises(self, monkeypatch):
self._patch_settings(monkeypatch, {
"v": {"secret": "s", "algorithm": "hmac-sha256"}
})
with pytest.raises(RuntimeError, match="Incomplete TSIG key"):
nh._load_tsig_key("v")
def test_missing_secret_raises(self, monkeypatch):
self._patch_settings(monkeypatch, {
"v": {"keyname": "k.", "algorithm": "hmac-sha256"}
})
with pytest.raises(RuntimeError, match="Incomplete TSIG key"):
nh._load_tsig_key("v")
def test_relative_key_name_gets_root(self, monkeypatch):
self._patch_settings(monkeypatch, {
"v": {
"keyname": "relkey",
"secret": "YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWE=",
"algorithm": "hmac-sha256",
}
})
key = nh._load_tsig_key("v")
assert key.name.is_absolute()
@pytest.mark.django_db
class TestGetSoaSerial:
def test_valid_soa(self, make_zone):
zone = make_zone(name="soa-valid.test", soa_serial=12345)
result = nh._get_soa_serial(zone)
assert result == 12345
def test_no_soa_record_returns_none(self, make_zone):
from netbox_dns.models import Record
zone = make_zone(name="soa-none.test")
Record.objects.filter(zone=zone, type="SOA").delete()
result = nh._get_soa_serial(zone)
assert result is None
def test_inactive_soa_returns_none(self, make_zone):
from netbox_dns.models import Record
from netbox_dns.choices import RecordStatusChoices
zone = make_zone(name="soa-inactive.test")
Record.objects.filter(zone=zone, type="SOA").update(
status=RecordStatusChoices.STATUS_INACTIVE
)
result = nh._get_soa_serial(zone)
assert result is None
def test_malformed_serial_returns_none(self, make_zone):
from netbox_dns.models import Record
zone = make_zone(name="soa-bad.test")
Record.objects.filter(zone=zone, type="SOA").update(
value="ns1.test. hostmaster.test. NOTANUMBER 300 100 1209600 3600"
)
result = nh._get_soa_serial(zone)
assert result is None
@pytest.mark.django_db
class TestResolveZoneNameserverIps:
def test_resolves_a_records(self, make_zone, make_record, make_nameserver):
zone = make_zone(name="nsresolv.test")
ns = make_nameserver("ns1.nsresolv.test")
zone.nameservers.add(ns)
make_record(zone, name="ns1", type_="A", value="192.0.2.1")
ips = nh._resolve_zone_nameserver_ips(zone)
assert "192.0.2.1" in ips
def test_resolves_aaaa_records(self, make_zone, make_record, make_nameserver):
zone = make_zone(name="nsresolv6.test")
ns = make_nameserver("ns1.nsresolv6.test")
zone.nameservers.add(ns)
make_record(zone, name="ns1", type_="AAAA", value="2001:db8::1")
ips = nh._resolve_zone_nameserver_ips(zone)
assert "2001:db8::1" in ips
def test_deduplicates_ips(self, make_zone, make_record, make_nameserver):
zone = make_zone(name="nsdedup.test")
ns = make_nameserver("ns1.nsdedup.test")
zone.nameservers.add(ns)
make_record(zone, name="ns1", type_="A", value="192.0.2.1")
make_record(zone, name="ns1", type_="A", value="192.0.2.2")
ips = nh._resolve_zone_nameserver_ips(zone)
assert "192.0.2.1" in ips
assert "192.0.2.2" in ips
assert len(ips) == 2
def test_no_nameservers_returns_empty(self, make_zone):
zone = make_zone(name="nsnone.test")
ips = nh._resolve_zone_nameserver_ips(zone)
assert ips == []
def test_multiple_nameservers(self, make_zone, make_record, make_nameserver):
zone = make_zone(name="nsmulti.test")
ns1 = make_nameserver("ns1.nsmulti.test")
ns2 = make_nameserver("ns2.nsmulti.test")
zone.nameservers.add(ns1, ns2)
make_record(zone, name="ns1", type_="A", value="192.0.2.1")
make_record(zone, name="ns2", type_="A", value="192.0.2.2")
ips = nh._resolve_zone_nameserver_ips(zone)
assert set(ips) == {"192.0.2.1", "192.0.2.2"}
def test_trailing_dot_resilience(self, make_zone, make_record, make_nameserver):
zone = make_zone(name="nstrail.test")
ns = make_nameserver("ns1.nstrail.test")
zone.nameservers.add(ns)
make_record(zone, name="ns1", type_="A", value="192.0.2.5")
ips = nh._resolve_zone_nameserver_ips(zone)
assert "192.0.2.5" in ips
@pytest.mark.django_db
class TestScheduleClientNotify:
def test_enqueues_job_when_soa_exists(self, make_zone):
zone = make_zone(name="sched-client.test", soa_serial=77)
with patch.object(nh.SendClientDNSNotify, "enqueue") as mock_enq:
nh.schedule_client_notify(zone)
mock_enq.assert_called_once_with(
zone_name="sched-client.test",
view_name=zone.view.name,
soa_serial=77,
)
def test_does_not_enqueue_when_no_soa(self, make_zone):
from netbox_dns.models import Record
zone = make_zone(name="sched-client-nosoa.test")
Record.objects.filter(zone=zone, type="SOA").delete()
with patch.object(nh.SendClientDNSNotify, "enqueue") as mock_enq:
nh.schedule_client_notify(zone)
mock_enq.assert_not_called()
@pytest.mark.django_db
class TestScheduleNsNotify:
def test_enqueues_job_when_soa_exists(self, make_zone):
zone = make_zone(name="sched-ns.test", soa_serial=88)
with patch.object(nh.SendNSNotify, "enqueue") as mock_enq:
nh.schedule_ns_notify(zone)
mock_enq.assert_called_once_with(
zone_id=zone.pk,
zone_name="sched-ns.test",
view_name=zone.view.name,
soa_serial=88,
)
def test_does_not_enqueue_when_no_soa(self, make_zone):
from netbox_dns.models import Record
zone = make_zone(name="sched-ns-nosoa.test")
Record.objects.filter(zone=zone, type="SOA").delete()
with patch.object(nh.SendNSNotify, "enqueue") as mock_enq:
nh.schedule_ns_notify(zone)
mock_enq.assert_not_called()
@pytest.mark.django_db
class TestSendClientDNSNotifyRun:
def test_sends_notify_to_seen_clients(self, make_zone, make_view, monkeypatch):
import netbox_dns_bridge.signals.notify_handler as nh
from netbox_dns_bridge.models import SeenTransferClients, IntegerKeyValueSetting
from django.utils import timezone
view = make_view(name="notify-run-view")
IntegerKeyValueSetting.objects.update_or_create(
key="catalog-zone-soa-serial", defaults={"value": 42}
)
SeenTransferClients.objects.create(
source_ip="192.0.2.99", view=view, last_seen=timezone.now()
)
monkeypatch.setattr(nh, "SETTINGS", {
"notify_clients": True,
"tsig_keys": {
"notify-run-view": {
"keyname": "nk.",
"secret": "YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWE=",
"algorithm": "hmac-sha256",
}
},
"notify_over_tcp": False,
"notify_client_port": 53,
"notify_client_alive_threshold_hours": 24,
})
job = MagicMock()
job.run.__wrapped__ = nh.SendClientDNSNotify.run
with patch("dns.query.udp") as mock_udp:
nh.SendClientDNSNotify.run(job,
zone_name="z.test", soa_serial=10, view_name="notify-run-view")
assert mock_udp.call_count >= 3 # zone + catz + view.catz
def test_handles_missing_serial_setting(self, monkeypatch, make_view):
# When the IntegerKeyValueSetting for catz serial doesn't exist,
# the job should log an error and not send any notifies
view = make_view(name="missing-serial-view")
monkeypatch.setattr(nh, "SETTINGS", {
"notify_clients": True,
"tsig_keys": {
"missing-serial-view": {
"keyname": "nk.",
"secret": "YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWE=",
"algorithm": "hmac-sha256",
}
},
"notify_over_tcp": False,
"notify_client_port": 53,
"notify_client_alive_threshold_hours": 24,
})
job = MagicMock()
with patch("dns.query.udp") as mock_udp:
nh.SendClientDNSNotify.run(job,
zone_name="z.test", soa_serial=1, view_name="missing-serial-view")
mock_udp.assert_not_called()
@pytest.mark.django_db
class TestSendNSNotifyRun:
def test_sends_notify_to_nameserver_ips(self, make_zone, make_record, make_nameserver, monkeypatch):
import netbox_dns_bridge.signals.notify_handler as nh
zone = make_zone(name="ns-notify-run.test", soa_serial=55)
ns = make_nameserver("ns1.ns-notify-run.test")
zone.nameservers.add(ns)
make_record(zone, name="ns1", type_="A", value="192.0.2.200")
monkeypatch.setattr(nh, "SETTINGS", {
"tsig_keys": {
zone.view.name: {
"keyname": "nk.",
"secret": "YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWE=",
"algorithm": "hmac-sha256",
}
},
"notify_over_tcp": False,
"notify_ns_port": 53,
})
job = MagicMock()
with patch("dns.query.udp") as mock_udp:
nh.SendNSNotify.run(job,
zone_id=zone.pk, zone_name="ns-notify-run.test",
view_name=zone.view.name, soa_serial=55)
mock_udp.assert_called_once()
def test_skips_when_zone_deleted(self, monkeypatch):
job = MagicMock()
with patch("dns.query.udp") as mock_udp:
nh.SendNSNotify.run(job,
zone_id=999999, zone_name="gone.test",
view_name="v", soa_serial=1)
mock_udp.assert_not_called()
def test_skips_when_no_nameservers(self, make_zone, monkeypatch):
zone = make_zone(name="ns-notify-empty.test")
monkeypatch.setattr(nh, "SETTINGS", {
"tsig_keys": {
zone.view.name: {
"keyname": "nk.",
"secret": "YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWE=",
"algorithm": "hmac-sha256",
}
},
"notify_over_tcp": False,
"notify_ns_port": 53,
})
job = MagicMock()
with patch("dns.query.udp") as mock_udp:
nh.SendNSNotify.run(job,
zone_id=zone.pk, zone_name="ns-notify-empty.test",
view_name=zone.view.name, soa_serial=1)
mock_udp.assert_not_called()