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
This commit is contained in:
1 parent
e8762bf807
commit
4ad110ca53
17 files changed
+2680
No files matched your search
@@ -0,0 +1,321 @@
|
||||
"""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()
|
||||
Reference in new issue
Block a user