import logging import urllib.error import pytest from dnscms import revalidation from events.models import EventCategory from tests.conftest import GenericPageFactory WEBHOOK_URL = "https://frontend.example.com/api/revalidate" class InlineThread: def __init__(self, target=None, args=(), kwargs=None, daemon=None): self._target = target self._args = args self._kwargs = kwargs or {} def start(self): self._target(*self._args, **self._kwargs) @pytest.fixture def webhook_calls(monkeypatch, settings): settings.REVALIDATE_WEBHOOK_SECRET = "s3cret" settings.FRONTEND_BASE_URL = "https://frontend.example.com" calls = [] monkeypatch.setattr(revalidation.threading, "Thread", InlineThread) monkeypatch.setattr( revalidation, "_send_webhook", lambda url, secret, reason: calls.append((url, secret)) ) return calls def test_page_publish_triggers_webhook( home_page, webhook_calls, django_capture_on_commit_callbacks ): page = GenericPageFactory(parent=home_page) with django_capture_on_commit_callbacks(execute=True): page.save_revision().publish() assert webhook_calls == [(WEBHOOK_URL, "s3cret")] def test_page_unpublish_triggers_webhook( home_page, webhook_calls, django_capture_on_commit_callbacks ): page = GenericPageFactory(parent=home_page) page.save_revision().publish() webhook_calls.clear() with django_capture_on_commit_callbacks(execute=True): page.unpublish() assert webhook_calls == [(WEBHOOK_URL, "s3cret")] def test_page_delete_triggers_webhook( home_page, webhook_calls, django_capture_on_commit_callbacks ): # live=False isolates the post_delete path: deleting a live page would # also fire page_unpublished (a harmless duplicate purge in production) page = GenericPageFactory(parent=home_page, live=False) webhook_calls.clear() with django_capture_on_commit_callbacks(execute=True): page.delete() assert webhook_calls == [(WEBHOOK_URL, "s3cret")] @pytest.mark.django_db def test_snippet_save_triggers_webhook(webhook_calls, django_capture_on_commit_callbacks): with django_capture_on_commit_callbacks(execute=True): EventCategory.objects.create(name="Konsert", slug="konsert") assert webhook_calls == [(WEBHOOK_URL, "s3cret")] def test_unset_secret_skips_and_warns_once(settings, monkeypatch, caplog): settings.REVALIDATE_WEBHOOK_SECRET = "" monkeypatch.setattr(revalidation, "_warned_unconfigured", False) with caplog.at_level(logging.WARNING, logger="dnscms.revalidation"): revalidation.trigger_frontend_revalidation("test") revalidation.trigger_frontend_revalidation("test") warnings = [r for r in caplog.records if r.levelno == logging.WARNING] assert len(warnings) == 1 def test_send_webhook_failure_is_swallowed(monkeypatch, caplog): def raise_urlerror(request, timeout): raise urllib.error.URLError("connection refused") monkeypatch.setattr(revalidation.urllib.request, "urlopen", raise_urlerror) with caplog.at_level(logging.ERROR, logger="dnscms.revalidation"): revalidation._send_webhook(WEBHOOK_URL, "s3cret", "test") assert any("frontend revalidation failed" in r.message for r in caplog.records) def test_send_webhook_request_shape(monkeypatch): seen = {} class FakeResponse: status = 200 def __enter__(self): return self def __exit__(self, *exc): return False def fake_urlopen(request, timeout): seen["request"] = request seen["timeout"] = timeout return FakeResponse() monkeypatch.setattr(revalidation.urllib.request, "urlopen", fake_urlopen) revalidation._send_webhook(WEBHOOK_URL, "s3cret", "test") request = seen["request"] assert request.get_method() == "POST" assert request.get_header("X-revalidate-secret") == "s3cret" assert seen["timeout"] == revalidation.WEBHOOK_TIMEOUT_SECONDS