diff --git a/CHANGELOG.md b/CHANGELOG.md index 98ae9a3..25321ad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,16 @@ ## Unreleased +- Move `notify_handler` from `signals/` to `jobs/` and rename to `notify`. +- Rename `SendClientDNSNotify` to `SendClientNotify`. It now sends a single NOTIFY + for the zone only; catalog zone entries (`catz`, `{view}.catz`) have been removed. +- Catalog zone NOTIFY is now scheduled by `increment_soa_serial` via + `schedule_catalog_zone_notify`, which enqueues `SendClientNotify` for `catz` and + `{view}.catz` — no separate job class needed. +- Extract a shared `_send_notify` helper used by both `SendClientNotify` and + `SendNSNotify` for message building and the send loop. +- Move all NOTIFY scheduling gates (`notify_clients`, `notify_ns_all_zones`, custom + field) into the scheduling functions. The signal handler calls the scheduling + functions unconditionally; each function decides whether to enqueue based on its + own settings gate. - Replace the global `IntegerKeyValueSetting` model (single `catalog-zone-soa-serial` value shared by all catalog zones) with a new `CatalogZone` model that has a 1:1 foreign key to `netbox_dns.models.View` and its own `soa_serial` field. Each view's diff --git a/netbox_dns_bridge/jobs/__init__.py b/netbox_dns_bridge/jobs/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/netbox_dns_bridge/signals/notify_handler.py b/netbox_dns_bridge/jobs/notify.py similarity index 62% rename from netbox_dns_bridge/signals/notify_handler.py rename to netbox_dns_bridge/jobs/notify.py index 8a1507e..b919244 100644 --- a/netbox_dns_bridge/signals/notify_handler.py +++ b/netbox_dns_bridge/jobs/notify.py @@ -17,6 +17,7 @@ from netbox_dns_bridge.models import SeenTransferClients, CatalogZone LOGGER = get_logger(__name__) SETTINGS = settings.PLUGINS_CONFIG.get("netbox_dns_bridge", {}) + def _load_tsig_key(view_name: str) -> dns.tsig.Key: tsig_keys = SETTINGS.get("tsig_keys", {}) if view_name not in tsig_keys: @@ -41,7 +42,50 @@ def _load_tsig_key(view_name: str) -> dns.tsig.Key: return dns.tsig.Key(name=key_name, secret=secret, algorithm=algorithm) -class SendClientDNSNotify(JobRunner): +def _send_notify( + zone_name, + soa_serial, + targets, + tsig_key, + view_name, + notify_over_tcp, + port, + log_prefix="NOTIFY", +): + """Build a DNS NOTIFY message for *zone_name* and send it to every target IP.""" + fqdn = dns.name.from_text(zone_name, dns.name.root).to_text() + msg = dns.message.make_query(fqdn, dns.rdatatype.SOA) + msg.flags |= dns.flags.AA + msg.set_opcode(dns.opcode.NOTIFY) + + soa_rdata = dns.rdata.from_text( + dns.rdataclass.IN, + dns.rdatatype.SOA, + f"invalid. invalid. {soa_serial} 60 10 1209600 0", + ) + msg.answer.append(dns.rrset.from_rdata(fqdn, 0, soa_rdata)) + msg.use_tsig(keyring={tsig_key.name: tsig_key}, keyname=tsig_key.name) + + for target in targets: + try: + if notify_over_tcp: + LOGGER.debug(f"Sending {log_prefix} over TCP") + dns.query.tcp(msg, target, port=port, timeout=2) + else: + LOGGER.debug(f"Sending {log_prefix} over UDP") + dns.query.udp(msg, target, port=port, timeout=2) + + LOGGER.info( + f"{log_prefix} sent: {target} {view_name}/{fqdn} {soa_serial}" + ) + except Exception as e: + LOGGER.error( + f"{log_prefix} failed: {target}:{port}" + f" {view_name}/{fqdn} {soa_serial}: {e}" + ) + + +class SendClientNotify(JobRunner): class Meta: name = "Send DNS NOTIFY" @@ -49,61 +93,25 @@ class SendClientDNSNotify(JobRunner): notify_over_tcp = SETTINGS.get("notify_over_tcp", False) port = SETTINGS.get("notify_client_port", 53) view_name = kwargs["view_name"] + zone_name = kwargs["zone_name"] + soa_serial = kwargs["soa_serial"] try: view = View.objects.get(name=view_name) - try: - catz_serial = view.catalog_zone.soa_serial - except CatalogZone.DoesNotExist as e: - LOGGER.error(f"Failed to get catalog zone serial: {e}") - return - - zones = [ - (kwargs["zone_name"], kwargs["soa_serial"]), - ("catz", catz_serial), - (f"{view_name}.catz", catz_serial), - ] - tsig_key = _load_tsig_key(view_name) cutoff_hours = SETTINGS.get("notify_client_alive_threshold_hours", 24) seen_cutoff = timezone.now() - timezone.timedelta(hours=cutoff_hours) - clients = SeenTransferClients.objects.filter( - view=view, last_seen__gte=seen_cutoff + clients = list( + SeenTransferClients.objects.filter( + view=view, last_seen__gte=seen_cutoff + ).values_list("source_ip", flat=True) ) - for zone_name, soa_serial in zones: - zone_name = dns.name.from_text(zone_name, dns.name.root).to_text() - msg = dns.message.make_query(zone_name, dns.rdatatype.SOA) - msg.flags |= dns.flags.AA # = 0 - msg.set_opcode(dns.opcode.NOTIFY) - - soa_rdata = dns.rdata.from_text( - dns.rdataclass.IN, - dns.rdatatype.SOA, - f"invalid. invalid. {soa_serial} 60 10 1209600 0", - ) - soa_rrset = dns.rrset.from_rdata(zone_name, 0, soa_rdata) - msg.answer.append(soa_rrset) - msg.use_tsig(keyring={tsig_key.name: tsig_key}, keyname=tsig_key.name) - - for client in clients: - try: - if notify_over_tcp: - LOGGER.debug("Sending NOTIFY over TCP") - dns.query.tcp(msg, client.source_ip, port=port, timeout=2) - else: - LOGGER.debug("Sending NOTIFY over UDP") - dns.query.udp(msg, client.source_ip, port=port, timeout=2) - - LOGGER.info( - f"NOTIFY sent: {client.source_ip} {view_name}/{zone_name} {soa_serial}" - ) - except Exception as e: - LOGGER.error( - f"NOTIFY failed: {client.source_ip}:{port}" - f" {view_name}/{zone_name} {soa_serial}: {e}" - ) + _send_notify( + zone_name, soa_serial, clients, tsig_key, + view_name, notify_over_tcp, port, log_prefix="NOTIFY", + ) except View.DoesNotExist as e: LOGGER.error(f"Failed to get view for NOTIFY: {e}") @@ -116,10 +124,10 @@ class SendClientDNSNotify(JobRunner): class SendNSNotify(JobRunner): """Send a NOTIFY straight to a zone's own nameservers. - Unlike ``SendClientDNSNotify`` (which notifies clients that previously queried the - transfer endpoint), this job resolves the addresses of the zone's configured - nameservers from NetBox DNS itself and notifies each of them so they pick up - the changed zone quickly. + Unlike ``SendClientNotify`` (which notifies clients that previously queried + the transfer endpoint), this job resolves the addresses of the zone's + configured nameservers from NetBox DNS itself and notifies each of them so + they pick up the changed zone quickly. """ class Meta: @@ -156,33 +164,10 @@ class SendNSNotify(JobRunner): LOGGER.error(f"NS NOTIFY aborted for {view_name}/{zone_name}: {e}") return - fqdn = dns.name.from_text(zone_name, dns.name.root).to_text() - msg = dns.message.make_query(fqdn, dns.rdatatype.SOA) - msg.flags |= dns.flags.AA - msg.set_opcode(dns.opcode.NOTIFY) - - soa_rdata = dns.rdata.from_text( - dns.rdataclass.IN, - dns.rdatatype.SOA, - f"invalid. invalid. {soa_serial} 60 10 1209600 0", + _send_notify( + zone_name, soa_serial, ns_targets, tsig_key, + view_name, notify_over_tcp, port, log_prefix="NS NOTIFY", ) - msg.answer.append(dns.rrset.from_rdata(fqdn, 0, soa_rdata)) - msg.use_tsig(keyring={tsig_key.name: tsig_key}, keyname=tsig_key.name) - - for ip in ns_targets: - try: - if notify_over_tcp: - LOGGER.debug("Sending NS NOTIFY over TCP") - dns.query.tcp(msg, ip, port=port, timeout=2) - else: - LOGGER.debug("Sending NS NOTIFY over UDP") - dns.query.udp(msg, ip, port=port, timeout=2) - - LOGGER.info(f"NS NOTIFY sent: {ip} {view_name}/{fqdn} {soa_serial}") - except Exception as e: - LOGGER.error( - f"NS NOTIFY failed: {ip} {view_name}/{fqdn} {soa_serial}: {e}" - ) def _get_soa_serial(zone: Zone): @@ -239,12 +224,33 @@ def _resolve_zone_nameserver_ips(zone: Zone) -> list: return ips +def _notify_ns_enabled(zone) -> bool: + """Return True if a NOTIFY to the zone's nameservers should be sent. + + Enabled either globally for every zone via ``notify_ns_all_zones`` or per + zone when the boolean custom field named by ``notify_ns_custom_field_name`` + is set to True on the zone. + """ + if SETTINGS.get("notify_ns_all_zones", False): + return True + + cf_name = SETTINGS.get("notify_ns_custom_field_name") + if not cf_name: + return False + + custom_fields = getattr(zone, "custom_field_data", None) or {} + return custom_fields.get(cf_name) is True + + def schedule_client_notify(zone: Zone): + if not SETTINGS.get("notify_clients", False): + return + soa_serial = _get_soa_serial(zone) if soa_serial is None: return - SendClientDNSNotify.enqueue( + SendClientNotify.enqueue( zone_name=zone.name, view_name=zone.view.name, soa_serial=soa_serial, @@ -252,6 +258,9 @@ def schedule_client_notify(zone: Zone): def schedule_ns_notify(zone: Zone): + if not _notify_ns_enabled(zone): + return + soa_serial = _get_soa_serial(zone) if soa_serial is None: return @@ -262,3 +271,24 @@ def schedule_ns_notify(zone: Zone): view_name=zone.view.name, soa_serial=soa_serial, ) + + +def schedule_catalog_zone_notify(view: View): + if not SETTINGS.get("notify_clients", False): + return + + try: + serial = view.catalog_zone.soa_serial + except CatalogZone.DoesNotExist: + return + + SendClientNotify.enqueue( + zone_name="catz", + soa_serial=serial, + view_name=view.name, + ) + SendClientNotify.enqueue( + zone_name=f"{view.name}.catz", + soa_serial=serial, + view_name=view.name, + ) diff --git a/netbox_dns_bridge/management/commands/dns_transfer_endpoint/catalog_zone_manager.py b/netbox_dns_bridge/management/commands/dns_transfer_endpoint/catalog_zone_manager.py index 0617268..e0dbe78 100644 --- a/netbox_dns_bridge/management/commands/dns_transfer_endpoint/catalog_zone_manager.py +++ b/netbox_dns_bridge/management/commands/dns_transfer_endpoint/catalog_zone_manager.py @@ -5,6 +5,7 @@ import dns.rdataclass from netbox_dns_bridge.utils import get_logger from netbox_dns.models import Zone as NBZone, View as NBView from netbox_dns_bridge.models import CatalogZone, CatalogZoneMemberIdentifier +from netbox_dns_bridge.jobs import notify from uuid import uuid4 from base64 import b32encode from django.db import transaction, close_old_connections, OperationalError @@ -38,12 +39,16 @@ def increment_soa_serial(view: NBView) -> int: f"is now {soa_serial_obj.soa_serial}" ) - return soa_serial_obj.soa_serial + serial = soa_serial_obj.soa_serial except OperationalError as e: LOGGER.error( f"ERROR: Failed to increment the Catalog Zone's SOA serial: {e}" ) close_old_connections() + return None + + notify.schedule_catalog_zone_notify(view) + return serial def create_zone(name, view_name) -> dns.zone.Zone: diff --git a/netbox_dns_bridge/signals/netbox_dns_zones.py b/netbox_dns_bridge/signals/netbox_dns_zones.py index f8c33be..f335254 100644 --- a/netbox_dns_bridge/signals/netbox_dns_zones.py +++ b/netbox_dns_bridge/signals/netbox_dns_zones.py @@ -1,13 +1,11 @@ from django.db.models.signals import pre_save, post_save, post_delete from django.dispatch import receiver from django.db import transaction -from django.conf import settings from netbox.context import current_request from netbox_dns.models import Zone, View from netbox_dns_bridge.management.commands.dns_transfer_endpoint import catalog_zone_manager as catzm -from . import notify_handler +from netbox_dns_bridge.jobs import notify -SETTINGS = settings.PLUGINS_CONFIG.get("netbox_dns_bridge", {}) @receiver(pre_save, sender=Zone) def zone_pre_save(sender, instance, **kwargs): @@ -61,7 +59,8 @@ def zone_post_save(sender, instance, created, **kwargs): and old_view_id != zone.view_id ) - # On commit, increment soa serial and if notify is enabled, schedule bg job. + # On commit, increment soa serial and schedule notify jobs. All settings + # gating happens inside the scheduling functions. def _on_commit(): if view_changed: try: @@ -71,13 +70,9 @@ def zone_post_save(sender, instance, created, **kwargs): pass catzm.increment_soa_serial(zone.view) - if SETTINGS.get("notify_clients", False): - notify_handler.schedule_client_notify(zone) - - # Send a NOTIFY to the zone's own nameservers when its SOA serial changed - # and the feature is enabled — globally or per zone via a custom field. - if soa_serial_changed and _notify_ns_enabled(zone): - notify_handler.schedule_ns_notify(zone) + if soa_serial_changed: + notify.schedule_client_notify(zone) + notify.schedule_ns_notify(zone) transaction.on_commit(_on_commit) @@ -99,21 +94,3 @@ def zone_post_delete(sender, instance, **kwargs): catzm.increment_soa_serial(instance.view) transaction.on_commit(_on_commit) - - -def _notify_ns_enabled(zone) -> bool: - """Return True if a NOTIFY to the zone's nameservers should be sent. - - Enabled either globally for every zone via ``notify_ns_all_zones`` or per - zone when the boolean custom field named by ``notify_ns_custom_field_name`` - is set to True on the zone. - """ - if SETTINGS.get("notify_ns_all_zones", False): - return True - - cf_name = SETTINGS.get("notify_ns_custom_field_name") - if not cf_name: - return False - - custom_fields = getattr(zone, "custom_field_data", None) or {} - return custom_fields.get(cf_name) is True diff --git a/netbox_dns_bridge/tests/conftest.py b/netbox_dns_bridge/tests/conftest.py index 29de1ac..9a39229 100644 --- a/netbox_dns_bridge/tests/conftest.py +++ b/netbox_dns_bridge/tests/conftest.py @@ -27,11 +27,10 @@ def default_view(db): def _settings_modules(): from netbox_dns_bridge.management.commands.dns_transfer_endpoint import ( request_handler, - catalog_zone_manager as catzm, # noqa: F401 ) - from netbox_dns_bridge.signals import netbox_dns_zones, notify_handler + from netbox_dns_bridge.jobs import notify - return [netbox_dns_zones, notify_handler, request_handler] + return [notify, request_handler] @pytest.fixture diff --git a/netbox_dns_bridge/tests/test_notify_handler.py b/netbox_dns_bridge/tests/test_notify.py similarity index 63% rename from netbox_dns_bridge/tests/test_notify_handler.py rename to netbox_dns_bridge/tests/test_notify.py index e128f09..e03d8d7 100644 --- a/netbox_dns_bridge/tests/test_notify_handler.py +++ b/netbox_dns_bridge/tests/test_notify.py @@ -1,5 +1,5 @@ -"""Tests for notify_handler: _load_tsig_key, _get_soa_serial, _resolve_zone_nameserver_ips, -schedule_client_notify, schedule_ns_notify, and job classes.""" +"""Tests for notify: _load_tsig_key, _get_soa_serial, _resolve_zone_nameserver_ips, +schedule_client_notify, schedule_ns_notify, schedule_catalog_zone_notify, and job classes.""" from unittest.mock import patch, MagicMock import dns.name @@ -7,7 +7,7 @@ import dns.rdatatype import dns.tsig import pytest -import netbox_dns_bridge.signals.notify_handler as nh +import netbox_dns_bridge.jobs.notify as nh class TestLoadTsigKey: @@ -156,9 +156,10 @@ class TestResolveZoneNameserverIps: @pytest.mark.django_db class TestScheduleClientNotify: - def test_enqueues_job_when_soa_exists(self, make_zone): + def test_enqueues_job_when_enabled(self, make_zone, monkeypatch): zone = make_zone(name="sched-client.test", soa_serial=77) - with patch.object(nh.SendClientDNSNotify, "enqueue") as mock_enq: + monkeypatch.setattr(nh, "SETTINGS", {"notify_clients": True}) + with patch.object(nh.SendClientNotify, "enqueue") as mock_enq: nh.schedule_client_notify(zone) mock_enq.assert_called_once_with( zone_name="sched-client.test", @@ -166,20 +167,29 @@ class TestScheduleClientNotify: soa_serial=77, ) - def test_does_not_enqueue_when_no_soa(self, make_zone): + def test_does_not_enqueue_when_no_soa(self, make_zone, monkeypatch): 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: + monkeypatch.setattr(nh, "SETTINGS", {"notify_clients": True}) + with patch.object(nh.SendClientNotify, "enqueue") as mock_enq: + nh.schedule_client_notify(zone) + mock_enq.assert_not_called() + + def test_does_not_enqueue_when_notify_clients_disabled(self, make_zone, monkeypatch): + zone = make_zone(name="sched-client-disabled.test", soa_serial=77) + monkeypatch.setattr(nh, "SETTINGS", {"notify_clients": False}) + with patch.object(nh.SendClientNotify, "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): + def test_enqueues_job_when_enabled(self, make_zone, monkeypatch): zone = make_zone(name="sched-ns.test", soa_serial=88) + monkeypatch.setattr(nh, "SETTINGS", {"notify_ns_all_zones": True}) with patch.object(nh.SendNSNotify, "enqueue") as mock_enq: nh.schedule_ns_notify(zone) mock_enq.assert_called_once_with( @@ -189,33 +199,75 @@ class TestScheduleNsNotify: soa_serial=88, ) - def test_does_not_enqueue_when_no_soa(self, make_zone): + def test_does_not_enqueue_when_no_soa(self, make_zone, monkeypatch): from netbox_dns.models import Record zone = make_zone(name="sched-ns-nosoa.test") Record.objects.filter(zone=zone, type="SOA").delete() + monkeypatch.setattr(nh, "SETTINGS", {"notify_ns_all_zones": True}) + with patch.object(nh.SendNSNotify, "enqueue") as mock_enq: + nh.schedule_ns_notify(zone) + mock_enq.assert_not_called() + + def test_does_not_enqueue_when_ns_notify_disabled(self, make_zone, monkeypatch): + zone = make_zone(name="sched-ns-disabled.test", soa_serial=88) + monkeypatch.setattr(nh, "SETTINGS", {}) 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, CatalogZone +class TestScheduleCatalogZoneNotify: + def test_enqueues_two_jobs_when_enabled(self, make_view, monkeypatch): + from netbox_dns_bridge.models import CatalogZone + + view = make_view(name="catz-notify-view") + CatalogZone.objects.create(view=view, soa_serial=42) + monkeypatch.setattr(nh, "SETTINGS", {"notify_clients": True}) + with patch.object(nh.SendClientNotify, "enqueue") as mock_enq: + nh.schedule_catalog_zone_notify(view) + assert mock_enq.call_count == 2 + mock_enq.assert_any_call( + zone_name="catz", soa_serial=42, view_name="catz-notify-view" + ) + mock_enq.assert_any_call( + zone_name="catz-notify-view.catz", + soa_serial=42, + view_name="catz-notify-view", + ) + + def test_does_not_enqueue_when_notify_clients_disabled(self, make_view, monkeypatch): + from netbox_dns_bridge.models import CatalogZone + + view = make_view(name="catz-notify-disabled") + CatalogZone.objects.create(view=view, soa_serial=42) + monkeypatch.setattr(nh, "SETTINGS", {"notify_clients": False}) + with patch.object(nh.SendClientNotify, "enqueue") as mock_enq: + nh.schedule_catalog_zone_notify(view) + mock_enq.assert_not_called() + + def test_does_not_enqueue_when_no_catalog_zone(self, make_view, monkeypatch): + view = make_view(name="catz-notify-no-cz") + monkeypatch.setattr(nh, "SETTINGS", {"notify_clients": True}) + with patch.object(nh.SendClientNotify, "enqueue") as mock_enq: + nh.schedule_catalog_zone_notify(view) + mock_enq.assert_not_called() + + +@pytest.mark.django_db +class TestSendClientNotifyRun: + def test_sends_single_notify_to_seen_clients(self, make_zone, make_view, monkeypatch): + import netbox_dns_bridge.jobs.notify as nh + from netbox_dns_bridge.models import SeenTransferClients from django.utils import timezone view = make_view(name="notify-run-view") - CatalogZone.objects.update_or_create( - view=view, defaults={"soa_serial": 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.", @@ -229,21 +281,23 @@ class TestSendClientDNSNotifyRun: }) job = MagicMock() - job.run.__wrapped__ = nh.SendClientDNSNotify.run with patch("dns.query.udp") as mock_udp: - nh.SendClientDNSNotify.run(job, + nh.SendClientNotify.run(job, zone_name="z.test", soa_serial=10, view_name="notify-run-view") - assert mock_udp.call_count >= 3 # zone + catz + view.catz + assert mock_udp.call_count == 1 - def test_handles_missing_serial_setting(self, monkeypatch, make_view): - # When the CatalogZone for the view doesn't exist, - # the job should log an error and not send any notifies - view = make_view(name="missing-serial-view") + def test_sends_notify_without_catalog_zone(self, monkeypatch, make_view): + from netbox_dns_bridge.models import SeenTransferClients + from django.utils import timezone + + view = make_view(name="no-catz-view") + SeenTransferClients.objects.create( + source_ip="192.0.2.1", view=view, last_seen=timezone.now() + ) monkeypatch.setattr(nh, "SETTINGS", { - "notify_clients": True, "tsig_keys": { - "missing-serial-view": { + "no-catz-view": { "keyname": "nk.", "secret": "YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWE=", "algorithm": "hmac-sha256", @@ -256,15 +310,15 @@ class TestSendClientDNSNotifyRun: 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() + nh.SendClientNotify.run(job, + zone_name="z.test", soa_serial=1, view_name="no-catz-view") + assert mock_udp.call_count == 1 @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 + import netbox_dns_bridge.jobs.notify as nh zone = make_zone(name="ns-notify-run.test", soa_serial=55) ns = make_nameserver("ns1.ns-notify-run.test") @@ -319,3 +373,44 @@ class TestSendNSNotifyRun: zone_id=zone.pk, zone_name="ns-notify-empty.test", view_name=zone.view.name, soa_serial=1) mock_udp.assert_not_called() + + +@pytest.mark.django_db +class TestNotifyNsEnabled: + def test_enabled_by_all_zones_flag(self, make_zone, monkeypatch): + monkeypatch.setattr(nh, "SETTINGS", {"notify_ns_all_zones": True}) + from netbox_dns_bridge.jobs.notify import _notify_ns_enabled + + zone = make_zone(name="nsflag.test") + assert _notify_ns_enabled(zone) is True + + def test_disabled_by_default(self, make_zone, monkeypatch): + monkeypatch.setattr(nh, "SETTINGS", {}) + from netbox_dns_bridge.jobs.notify import _notify_ns_enabled + + zone = make_zone(name="nsdisabled.test") + assert _notify_ns_enabled(zone) is False + + def test_enabled_by_custom_field(self, make_zone, monkeypatch): + monkeypatch.setattr(nh, "SETTINGS", {"notify_ns_custom_field_name": "enable_notify"}) + from netbox_dns_bridge.jobs.notify import _notify_ns_enabled + + zone = make_zone(name="nscf.test") + zone.custom_field_data = {"enable_notify": True} + assert _notify_ns_enabled(zone) is True + + def test_disabled_when_custom_field_false(self, make_zone, monkeypatch): + monkeypatch.setattr(nh, "SETTINGS", {"notify_ns_custom_field_name": "enable_notify"}) + from netbox_dns_bridge.jobs.notify import _notify_ns_enabled + + zone = make_zone(name="nscf-false.test") + zone.custom_field_data = {"enable_notify": False} + assert _notify_ns_enabled(zone) is False + + def test_disabled_when_custom_field_missing(self, make_zone, monkeypatch): + monkeypatch.setattr(nh, "SETTINGS", {"notify_ns_custom_field_name": "enable_notify"}) + from netbox_dns_bridge.jobs.notify import _notify_ns_enabled + + zone = make_zone(name="nscf-missing.test") + zone.custom_field_data = {} + assert _notify_ns_enabled(zone) is False diff --git a/netbox_dns_bridge/tests/test_signals.py b/netbox_dns_bridge/tests/test_signals.py index 03e80b3..02e3e0e 100644 --- a/netbox_dns_bridge/tests/test_signals.py +++ b/netbox_dns_bridge/tests/test_signals.py @@ -50,41 +50,55 @@ class TestZonePostSaveCreate: obj = CatalogZone.objects.get(view=zone.view) assert obj.soa_serial == catz_serial - def test_schedule_client_notify_called_when_enabled(self, make_zone, patch_settings): + def test_schedule_client_notify_always_called(self, make_zone, patch_settings): patch_settings({"notify_clients": True}) with patch( - "netbox_dns_bridge.signals.netbox_dns_zones.notify_handler.schedule_client_notify" + "netbox_dns_bridge.jobs.notify.schedule_client_notify" ) as mock_notify: - with patch( - "netbox_dns_bridge.signals.netbox_dns_zones.notify_handler.schedule_ns_notify" - ): - with captureOnCommitCallbacks(execute=True): - make_zone(name="notify.test") - mock_notify.assert_called_once() + with patch("netbox_dns_bridge.jobs.notify.schedule_ns_notify"): + with patch( + "netbox_dns_bridge.management.commands.dns_transfer_endpoint." + "catalog_zone_manager.notify.schedule_catalog_zone_notify" + ): + with captureOnCommitCallbacks(execute=True): + make_zone(name="notify.test") + mock_notify.assert_called_once() - def test_schedule_ns_notify_called_when_serial_changed(self, make_zone, patch_settings): + def test_schedule_ns_notify_called_when_serial_changed( + self, make_zone, patch_settings + ): patch_settings({ "notify_clients": False, "notify_ns_all_zones": True, }) with patch( - "netbox_dns_bridge.signals.netbox_dns_zones.notify_handler.schedule_ns_notify" + "netbox_dns_bridge.jobs.notify.schedule_ns_notify" ) as mock_ns_notify: - with captureOnCommitCallbacks(execute=True): - make_zone(name="nsnotify.test") - mock_ns_notify.assert_called_once() + with patch("netbox_dns_bridge.jobs.notify.schedule_client_notify"): + with patch( + "netbox_dns_bridge.management.commands.dns_transfer_endpoint." + "catalog_zone_manager.notify.schedule_catalog_zone_notify" + ): + with captureOnCommitCallbacks(execute=True): + make_zone(name="nsnotify.test") + mock_ns_notify.assert_called_once() - def test_schedule_ns_notify_not_called_when_disabled(self, make_zone, patch_settings): - patch_settings({ - "notify_clients": False, - "notify_ns_all_zones": False, - }) + def test_schedule_ns_notify_not_called_when_serial_unchanged( + self, make_zone, patch_settings + ): + patch_settings({"notify_ns_all_zones": True}) with patch( - "netbox_dns_bridge.signals.netbox_dns_zones.notify_handler.schedule_ns_notify" + "netbox_dns_bridge.jobs.notify.schedule_ns_notify" ) as mock_ns_notify: - with captureOnCommitCallbacks(execute=True): - make_zone(name="nonsnotify.test") - mock_ns_notify.assert_not_called() + with patch("netbox_dns_bridge.jobs.notify.schedule_client_notify"): + with patch( + "netbox_dns_bridge.management.commands.dns_transfer_endpoint." + "catalog_zone_manager.notify.schedule_catalog_zone_notify" + ): + zone = make_zone(name="noserialchange.test") + with captureOnCommitCallbacks(execute=True): + zone.save() + mock_ns_notify.assert_not_called() @pytest.mark.django_db @@ -224,44 +238,3 @@ class TestSignalDedup: view=zone.view ).soa_serial assert new_serial == original_serial + 2 - - -@pytest.mark.django_db -class TestNotifyNsEnabled: - def test_enabled_by_all_zones_flag(self, make_zone, patch_settings): - patch_settings({"notify_ns_all_zones": True}) - from netbox_dns_bridge.signals.netbox_dns_zones import _notify_ns_enabled - - zone = make_zone(name="nsflag.test") - assert _notify_ns_enabled(zone) is True - - def test_disabled_by_default(self, make_zone, patch_settings): - patch_settings({}) - from netbox_dns_bridge.signals.netbox_dns_zones import _notify_ns_enabled - - zone = make_zone(name="nsdisabled.test") - assert _notify_ns_enabled(zone) is False - - def test_enabled_by_custom_field(self, make_zone, patch_settings): - patch_settings({"notify_ns_custom_field_name": "enable_notify"}) - from netbox_dns_bridge.signals.netbox_dns_zones import _notify_ns_enabled - - zone = make_zone(name="nscf.test") - zone.custom_field_data = {"enable_notify": True} - assert _notify_ns_enabled(zone) is True - - def test_disabled_when_custom_field_false(self, make_zone, patch_settings): - patch_settings({"notify_ns_custom_field_name": "enable_notify"}) - from netbox_dns_bridge.signals.netbox_dns_zones import _notify_ns_enabled - - zone = make_zone(name="nscf-false.test") - zone.custom_field_data = {"enable_notify": False} - assert _notify_ns_enabled(zone) is False - - def test_disabled_when_custom_field_missing(self, make_zone, patch_settings): - patch_settings({"notify_ns_custom_field_name": "enable_notify"}) - from netbox_dns_bridge.signals.netbox_dns_zones import _notify_ns_enabled - - zone = make_zone(name="nscf-missing.test") - zone.custom_field_data = {} - assert _notify_ns_enabled(zone) is False