예제 #1
0
    def test_delete_object(self):
        site = Site(
            name="Test Site 1",
            slug="test-site-1",
            custom_field_data={"my_field": "ABC", "my_field_select": "Bar"},
        )
        site.save()
        self.create_tags("Tag 1", "Tag 2")
        site.tags.set("Tag 1", "Tag 2")

        request = {
            "path": self._get_url("delete", instance=site),
            "data": post_data({"confirm": True}),
        }
        self.add_permissions("dcim.delete_site")
        response = self.client.post(**request)
        self.assertHttpStatus(response, 302)

        oc = ObjectChange.objects.first()
        self.assertEqual(oc.changed_object, None)
        self.assertEqual(oc.object_repr, site.name)
        self.assertEqual(oc.action, ObjectChangeActionChoices.ACTION_DELETE)
        self.assertEqual(oc.object_data["custom_fields"]["my_field"], "ABC")
        self.assertEqual(oc.object_data["custom_fields"]["my_field_select"], "Bar")
        self.assertEqual(oc.object_data["tags"], ["Tag 1", "Tag 2"])
예제 #2
0
    def test_delete_object(self):
        site = Site(
            name="Test Site 1",
            slug="test-site-1",
            status=self.statuses.get(slug="active"),
            _custom_field_data={
                "my_field": "ABC",
                "my_field_select": "Bar"
            },
        )
        site.save()
        site.tags.set(*Tag.objects.all()[:2])
        self.assertEqual(ObjectChange.objects.count(), 0)
        self.add_permissions("dcim.delete_site", "extras.view_status")
        url = reverse("dcim-api:site-detail", kwargs={"pk": site.pk})

        response = self.client.delete(url, **self.header)
        self.assertHttpStatus(response, status.HTTP_204_NO_CONTENT)
        self.assertEqual(Site.objects.count(), 0)

        oc = ObjectChange.objects.first()
        self.assertEqual(oc.changed_object, None)
        self.assertEqual(oc.object_repr, site.name)
        self.assertEqual(oc.action, ObjectChangeActionChoices.ACTION_DELETE)
        self.assertEqual(oc.object_data["custom_fields"]["my_field"], "ABC")
        self.assertEqual(oc.object_data["custom_fields"]["my_field_select"],
                         "Bar")
        self.assertEqual(oc.object_data["tags"], ["Tag 1", "Tag 2"])
예제 #3
0
    def test_update_object(self):
        site = Site(
            name="Test Site 1",
            slug="test-site-1",
            status=self.statuses.get(slug="planned"),
        )
        site.save()

        data = {
            "name": "Test Site X",
            "slug": "test-site-x",
            "status": "active",
            "custom_fields": {
                "my_field": "DEF",
                "my_field_select": "Foo",
            },
            "tags": [{"name": "Tag 3"}],
        }
        self.assertEqual(ObjectChange.objects.count(), 0)
        self.add_permissions("dcim.change_site", "extras.view_status")
        url = reverse("dcim-api:site-detail", kwargs={"pk": site.pk})

        response = self.client.put(url, data, format="json", **self.header)
        self.assertHttpStatus(response, status.HTTP_200_OK)

        site = Site.objects.get(pk=response.data["id"])
        # Get only the most recent OC
        oc = ObjectChange.objects.filter(
            changed_object_type=ContentType.objects.get_for_model(Site),
            changed_object_id=site.pk,
        ).first()
        self.assertEqual(oc.changed_object, site)
        self.assertEqual(oc.action, ObjectChangeActionChoices.ACTION_UPDATE)
        self.assertEqual(oc.object_data["custom_fields"], data["custom_fields"])
        self.assertEqual(oc.object_data["tags"], ["Tag 3"])
예제 #4
0
    def setUpTestData(cls):

        site = Site(name="Site 1", slug="site-1")
        site.save()

        # Create three ObjectChanges
        user = User.objects.create_user(username="******")
        for i in range(1, 4):
            oc = site.to_objectchange(action=ObjectChangeActionChoices.ACTION_UPDATE)
            oc.user = user
            oc.request_id = uuid.uuid4()
            oc.save()
예제 #5
0
    def test_change_webhook_enqueued(self):

        with web_request_context(self.user):
            site = Site(name="Test Site 2")
            site.save()

        # Verify that a job was queued for the object creation webhook
        site = Site.objects.get(name="Test Site 2")
        self.assertEqual(self.queue.count, 1)
        job = self.queue.jobs[0]
        self.assertEqual(job.args[0], Webhook.objects.get(type_create=True))
        self.assertEqual(job.args[1]["id"], str(site.pk))
        self.assertEqual(job.args[2], "site")
예제 #6
0
    def test_change_log_created(self):

        with web_request_context(self.user):
            site = Site(name="Test Site 1")
            site.save()

        site = Site.objects.get(name="Test Site 1")
        oc_list = ObjectChange.objects.filter(
            changed_object_type=ContentType.objects.get_for_model(Site),
            changed_object_id=site.pk,
        ).order_by("pk")
        self.assertEqual(len(oc_list), 1)
        self.assertEqual(oc_list[0].changed_object, site)
        self.assertEqual(oc_list[0].action, ObjectChangeActionChoices.ACTION_CREATE)
예제 #7
0
    def test_cf_data(self):
        """
        Check that custom field data is present on the instance immediately after being set and after being fetched
        from the database.
        """
        site = Site(name="Test Site", slug="test-site")

        # Check custom field data on new instance
        site.cf["foo"] = "abc"
        self.assertEqual(site.cf["foo"], "abc")

        # Check custom field data from database
        site.save()
        site = Site.objects.get(name="Test Site")
        self.assertEqual(site.cf["foo"], "abc")
예제 #8
0
    def test_view_object_with_custom_link(self):
        customlink = CustomLink(
            content_type=ContentType.objects.get_for_model(Site),
            name="Test",
            text="FOO {{ obj.name }} BAR",
            target_url="http://example.com/?site={{ obj.slug }}",
            new_window=False,
        )
        customlink.save()

        site = Site(name="Test Site", slug="test-site")
        site.save()

        response = self.client.get(site.get_absolute_url(), follow=True)
        self.assertEqual(response.status_code, 200)
        self.assertIn(f"FOO {site.name} BAR", str(response.content))
예제 #9
0
    def test_webhooks_snapshot_on_create(self):
        request_id = uuid.uuid4()
        webhook = Webhook.objects.get(type_create=True)
        timestamp = str(timezone.now())

        def mock_send(_, request, **kwargs):
            # Validate the outgoing request body
            body = json.loads(request.body)
            self.assertEqual(body["data"]["name"], "Site 1")
            self.assertEqual(body["snapshots"]["prechange"], None)
            self.assertEqual(body["snapshots"]["postchange"]["name"], "Site 1")
            self.assertEqual(body["snapshots"]["differences"]["removed"], None)
            self.assertEqual(body["snapshots"]["differences"]["added"]["name"],
                             "Site 1")

            class FakeResponse:
                ok = True
                status_code = 200

            return FakeResponse()

        # Patch the Session object with our mock_send() method, then process the webhook for sending
        with patch.object(Session, "send", mock_send):

            request = mock.MagicMock()
            request.user = self.user
            request.id = request_id

            with change_logging(request):
                site = Site(name="Site 1", slug="site-1")
                site.save()

                serializer = SiteSerializer(site, context={"request": None})
                snapshots = get_snapshots(
                    site, ObjectChangeActionChoices.ACTION_CREATE)

                process_webhook(
                    webhook.pk,
                    serializer.data,
                    Site._meta.model_name,
                    ObjectChangeActionChoices.ACTION_CREATE,
                    timestamp,
                    self.user.username,
                    request_id,
                    snapshots,
                )
예제 #10
0
    def setUp(self):
        obj_type = ContentType.objects.get_for_model(Site)
        self.cf = CustomField(
            name="cf1",
            type=CustomFieldTypeChoices.TYPE_SELECT,
        )
        self.cf.save()
        self.cf.content_types.set([obj_type])

        self.choice = CustomFieldChoice(field=self.cf, value="Foo")
        self.choice.save()

        self.site = Site(
            name="Site 1",
            slug="site-1",
            _custom_field_data={
                "cf1": "Foo",
            },
        )
        self.site.save()
예제 #11
0
class CustomFieldChoiceTest(TestCase):
    def setUp(self):
        obj_type = ContentType.objects.get_for_model(Site)
        self.cf = CustomField(
            name="cf1",
            type=CustomFieldTypeChoices.TYPE_SELECT,
        )
        self.cf.save()
        self.cf.content_types.set([obj_type])

        self.choice = CustomFieldChoice(field=self.cf, value="Foo")
        self.choice.save()

        self.site = Site(
            name="Site 1",
            slug="site-1",
            _custom_field_data={
                "cf1": "Foo",
            },
        )
        self.site.save()

    def test_default_value_must_be_valid_choice_sad_path(self):
        self.cf.default = "invalid value"
        with self.assertRaises(ValidationError):
            self.cf.full_clean()

    def test_default_value_must_be_valid_choice_happy_path(self):
        self.cf.default = "Foo"
        self.cf.full_clean()
        self.cf.save()
        self.assertEqual(self.cf.default, "Foo")

    def test_active_choice_cannot_be_deleted(self):
        with self.assertRaises(ProtectedError):
            self.choice.delete()

    def test_custom_choice_deleted_with_field(self):
        self.cf.delete()
        self.assertEqual(CustomField.objects.count(), 0)
        self.assertEqual(CustomFieldChoice.objects.count(), 0)
예제 #12
0
    def test_update_custom_field_choice_data_task(self):
        self.clear_worker()

        obj_type = ContentType.objects.get_for_model(Site)
        cf = CustomField(
            name="cf1",
            type=CustomFieldTypeChoices.TYPE_SELECT,
        )
        cf.save()
        cf.content_types.set([obj_type])

        self.wait_on_active_tasks()

        choice = CustomFieldChoice(field=cf, value="Foo")
        choice.save()

        site = Site(name="Site 1", slug="site-1", _custom_field_data={"cf1": "Foo"})
        site.save()

        choice.value = "Bar"
        choice.save()

        self.wait_on_active_tasks()

        site.refresh_from_db()

        self.assertEqual(site.cf["cf1"], "Bar")
예제 #13
0
    def test_delete_custom_field_data_task(self):
        obj_type = ContentType.objects.get_for_model(Site)
        cf = CustomField(
            name="cf1",
            type=CustomFieldTypeChoices.TYPE_TEXT,
        )
        cf.save()
        cf.content_types.set([obj_type])

        # Synchronously process all jobs on the queue in this process
        self.get_worker()

        site = Site(name="Site 1",
                    slug="site-1",
                    _custom_field_data={"cf1": "foo"})
        site.save()

        cf.delete()

        # Synchronously process all jobs on the queue in this process
        self.get_worker()

        site.refresh_from_db()

        self.assertTrue("cf1" not in site.cf)
예제 #14
0
    def test_invalid_data(self):
        """
        Setting custom field data for a non-applicable (or non-existent) CustomField should raise a ValidationError.
        """
        site = Site(name="Test Site", slug="test-site")

        # Set custom field data
        site.cf["foo"] = "abc"
        site.cf["bar"] = "def"
        with self.assertRaises(ValidationError):
            site.clean()

        del site.cf["bar"]
        site.clean()
예제 #15
0
    def test_update_custom_field_choice_data_task(self):
        obj_type = ContentType.objects.get_for_model(Site)
        cf = CustomField(
            name="cf1",
            type=CustomFieldTypeChoices.TYPE_SELECT,
        )
        cf.save()
        cf.content_types.set([obj_type])

        # Synchronously process all jobs on the queue in this process
        self.get_worker()

        choice = CustomFieldChoice(field=cf, value="Foo")
        choice.save()

        site = Site(name="Site 1",
                    slug="site-1",
                    _custom_field_data={"cf1": "Foo"})
        site.save()

        choice.value = "Bar"
        choice.save()

        # Synchronously process all jobs on the queue in this process
        self.get_worker()

        site.refresh_from_db()

        self.assertEqual(site.cf["cf1"], "Bar")
예제 #16
0
    def test_missing_required_field(self):
        """
        Check that a ValidationError is raised if any required custom fields are not present.
        """
        cf3 = CustomField(type=CustomFieldTypeChoices.TYPE_TEXT, name="baz", required=True)
        cf3.save()
        cf3.content_types.set([ContentType.objects.get_for_model(Site)])

        site = Site(name="Test Site", slug="test-site")

        # Set custom field data with a required field omitted
        site.cf["foo"] = "abc"
        with self.assertRaises(ValidationError):
            site.clean()

        site.cf["baz"] = "def"
        site.clean()
예제 #17
0
    def evaluate_ordering(self, names):

        # Create the Sites
        Site.objects.bulk_create(
            Site(name=name, slug=name.lower()) for name in names)

        # Validate forward ordering
        self.assertEqual(names,
                         list(Site.objects.values_list("name", flat=True)))

        # Validate reverse ordering
        self.assertEqual(
            list(reversed(names)),
            list(Site.objects.reverse().values_list("name", flat=True)),
        )
예제 #18
0
    def setUpTestData(cls):
        obj_type = ContentType.objects.get_for_model(Site)

        computed_fields = (
            ComputedField(
                content_type=obj_type,
                label="Computed Field One",
                slug="computed_field_one",
                template="Site name is {{ obj.name }}",
                fallback_value="Template error",
                weight=100,
            ),
            ComputedField(
                content_type=obj_type,
                slug="computed_field_two",
                label="Computed Field Two",
                template="Site name is {{ obj.name }}",
                fallback_value="Template error",
                weight=100,
            ),
            ComputedField(
                content_type=obj_type,
                slug="computed_field_three",
                label="Computed Field Three",
                template="Site name is {{ obj.name }}",
                fallback_value="Template error",
                weight=100,
            ),
        )

        cls.site1 = Site(name="NYC")
        cls.site1.save()

        for cf in computed_fields:
            cf.save()

        cls.form_data = {
            "content_type": obj_type.pk,
            "slug": "computed_field_four",
            "label": "Computed Field Four",
            "template": "{{ obj.name }} is the best Site!",
            "fallback_value": ":skull_emoji:",
            "weight": 100,
        }
예제 #19
0
    def test_provision_field_task(self):
        self.clear_worker()

        site = Site(
            name="Site 1",
            slug="site-1",
        )
        site.save()

        obj_type = ContentType.objects.get_for_model(Site)
        cf = CustomField(name="cf1", type=CustomFieldTypeChoices.TYPE_TEXT, default="Foo")
        cf.save()
        cf.content_types.set([obj_type])

        self.wait_on_active_tasks()

        site.refresh_from_db()

        self.assertEqual(site.cf["cf1"], "Foo")
예제 #20
0
    def test_update_object(self):
        site = Site(
            name="Test Site 1",
            slug="test-site-1",
            status=Status.objects.get(slug="active"),
        )
        site.save()
        tags = self.create_tags("Tag 1", "Tag 2", "Tag 3")
        site.tags.set("Tag 1", "Tag 2")

        form_data = {
            "name": "Test Site X",
            "slug": "test-site-x",
            "status": Status.objects.get(slug="planned").pk,
            "cf_my_field": "DEF",
            "cf_my_field_select": "Foo",
            "tags": [tags[2].pk],
        }

        request = {
            "path": self._get_url("edit", instance=site),
            "data": post_data(form_data),
        }
        self.add_permissions("dcim.change_site", "extras.view_tag",
                             "extras.view_status")
        response = self.client.post(**request)
        self.assertHttpStatus(response, 302)

        # Verify the creation of a new ObjectChange record
        site.refresh_from_db()
        oc = ObjectChange.objects.filter(
            changed_object_type=ContentType.objects.get_for_model(Site),
            changed_object_id=site.pk,
        ).first()
        self.assertEqual(oc.changed_object, site)
        self.assertEqual(oc.action, ObjectChangeActionChoices.ACTION_UPDATE)
        self.assertEqual(oc.object_data["custom_fields"]["my_field"],
                         form_data["cf_my_field"])
        self.assertEqual(
            oc.object_data["custom_fields"]["my_field_select"],
            form_data["cf_my_field_select"],
        )
        self.assertEqual(oc.object_data["tags"], ["Tag 3"])
예제 #21
0
    def test_provision_field_task(self):
        site = Site(
            name="Site 1",
            slug="site-1",
        )
        site.save()

        obj_type = ContentType.objects.get_for_model(Site)
        cf = CustomField(name="cf1",
                         type=CustomFieldTypeChoices.TYPE_TEXT,
                         default="Foo")
        cf.save()
        cf.content_types.set([obj_type])

        # Synchronously process all jobs on the queue in this process
        self.get_worker()

        site.refresh_from_db()

        self.assertEqual(site.cf["cf1"], "Foo")
예제 #22
0
    def test_delete_custom_field_data_task(self):
        self.clear_worker()

        obj_type = ContentType.objects.get_for_model(Site)
        cf = CustomField(
            name="cf1",
            type=CustomFieldTypeChoices.TYPE_TEXT,
        )
        cf.save()
        cf.content_types.set([obj_type])

        site = Site(name="Site 1", slug="site-1", _custom_field_data={"cf1": "foo"})
        site.save()

        cf.delete()

        self.wait_on_active_tasks()

        site.refresh_from_db()

        self.assertTrue("cf1" not in site.cf)
예제 #23
0
    def setUpTestData(cls):

        manufacturers = (
            Manufacturer(name="Manufacturer 1", slug="manufacturer-1"),
            Manufacturer(name="Manufacturer 2", slug="manufacturer-2"),
            Manufacturer(name="Manufacturer 3", slug="manufacturer-3"),
        )
        Manufacturer.objects.bulk_create(manufacturers)

        device_types = (
            DeviceType(
                manufacturer=manufacturers[0],
                model="Model 1",
                slug="model-1",
                is_full_depth=True,
            ),
            DeviceType(
                manufacturer=manufacturers[1],
                model="Model 2",
                slug="model-2",
                is_full_depth=True,
            ),
            DeviceType(
                manufacturer=manufacturers[2],
                model="Model 3",
                slug="model-3",
                is_full_depth=False,
            ),
        )
        DeviceType.objects.bulk_create(device_types)

        device_roles = (
            DeviceRole(name="Device Role 1", slug="device-role-1"),
            DeviceRole(name="Device Role 2", slug="device-role-2"),
            DeviceRole(name="Device Role 3", slug="device-role-3"),
        )
        DeviceRole.objects.bulk_create(device_roles)

        device_statuses = Status.objects.get_for_model(Device)
        device_status_map = {ds.slug: ds for ds in device_statuses.all()}

        platforms = (
            Platform(name="Platform 1", slug="platform-1"),
            Platform(name="Platform 2", slug="platform-2"),
            Platform(name="Platform 3", slug="platform-3"),
        )
        Platform.objects.bulk_create(platforms)

        regions = (
            Region(name="Region 1", slug="region-1"),
            Region(name="Region 2", slug="region-2"),
            Region(name="Region 3", slug="region-3"),
        )
        for region in regions:
            region.save()

        sites = (
            Site(name="Site 1",
                 slug="abc-site-1",
                 region=regions[0],
                 asn=65001),
            Site(name="Site 2",
                 slug="def-site-2",
                 region=regions[1],
                 asn=65101),
            Site(name="Site 3",
                 slug="ghi-site-3",
                 region=regions[2],
                 asn=65201),
        )
        Site.objects.bulk_create(sites)

        racks = (
            Rack(name="Rack 1", site=sites[0]),
            Rack(name="Rack 2", site=sites[1]),
            Rack(name="Rack 3", site=sites[2]),
        )
        Rack.objects.bulk_create(racks)

        devices = (
            Device(
                name="Device 1",
                device_type=device_types[0],
                device_role=device_roles[0],
                platform=platforms[0],
                serial="ABC",
                asset_tag="1001",
                site=sites[0],
                rack=racks[0],
                position=1,
                face=DeviceFaceChoices.FACE_FRONT,
                status=device_status_map["active"],
                local_context_data={"foo": 123},
                comments="Device 1 comments",
            ),
            Device(
                name="Device 2",
                device_type=device_types[1],
                device_role=device_roles[1],
                platform=platforms[1],
                serial="DEF",
                asset_tag="1002",
                site=sites[1],
                rack=racks[1],
                position=2,
                face=DeviceFaceChoices.FACE_FRONT,
                status=device_status_map["staged"],
                comments="Device 2 comments",
            ),
            Device(
                name="Device 3",
                device_type=device_types[2],
                device_role=device_roles[2],
                platform=platforms[2],
                serial="GHI",
                asset_tag="1003",
                site=sites[2],
                rack=racks[2],
                position=3,
                face=DeviceFaceChoices.FACE_REAR,
                status=device_status_map["failed"],
                comments="Device 3 comments",
            ),
        )
        Device.objects.bulk_create(devices)

        interfaces = (
            Interface(device=devices[0],
                      name="Interface 1",
                      mac_address="00-00-00-00-00-01"),
            Interface(device=devices[0],
                      name="Interface 2",
                      mac_address="aa-00-00-00-00-01"),
            Interface(device=devices[1],
                      name="Interface 3",
                      mac_address="00-00-00-00-00-02"),
            Interface(device=devices[1],
                      name="Interface 4",
                      mac_address="bb-00-00-00-00-02"),
            Interface(device=devices[2],
                      name="Interface 5",
                      mac_address="00-00-00-00-00-03"),
            Interface(device=devices[2],
                      name="Interface 6",
                      mac_address="cc-00-00-00-00-03"),
        )
        Interface.objects.bulk_create(interfaces)
예제 #24
0
    def test_custom_validator_raises_exception(self):

        site = Site(name="this site has a matching name", slug="site1")

        with self.assertRaises(ValidationError):
            site.clean()
예제 #25
0
    def test_webhooks_snapshot_without_model_api_serializer(
            self, get_serializer_for_model):
        def get_serializer(model_class):
            raise SerializerNotFound

        get_serializer_for_model.side_effect = get_serializer

        request_id = uuid.uuid4()
        webhook = Webhook.objects.get(type_create=True)
        timestamp = str(timezone.now())

        def mock_send(_, request, **kwargs):

            # Validate the outgoing request body
            body = json.loads(request.body)

            self.assertEqual(body["snapshots"]["prechange"]["status"],
                             str(self.active_status.id))
            self.assertEqual(body["snapshots"]["prechange"]["region"],
                             str(self.region_one.id))
            self.assertEqual(body["snapshots"]["postchange"]["name"],
                             "Site Update")
            self.assertEqual(body["snapshots"]["postchange"]["status"],
                             str(self.planned_status.id))
            self.assertEqual(body["snapshots"]["postchange"]["region"],
                             str(self.region_two.id))
            self.assertEqual(
                body["snapshots"]["differences"]["removed"]["name"], "Site 1")
            self.assertEqual(body["snapshots"]["differences"]["added"]["name"],
                             "Site Update")

            class FakeResponse:
                ok = True
                status_code = 200

            return FakeResponse()

        with patch.object(Session, "send", mock_send):
            self.client.force_login(self.user)

            request = mock.MagicMock()
            request.user = self.user
            request.id = request_id

            with change_logging(request):
                site = Site(name="Site 1",
                            slug="site-1",
                            status=self.active_status,
                            region=self.region_one)
                site.save()

                site.name = "Site Update"
                site.status = self.planned_status
                site.region = self.region_two
                site.save()

                serializer = SiteSerializer(site, context={"request": None})
                snapshots = get_snapshots(
                    site, ObjectChangeActionChoices.ACTION_UPDATE)

                process_webhook(
                    webhook.pk,
                    serializer.data,
                    Site._meta.model_name,
                    ObjectChangeActionChoices.ACTION_CREATE,
                    timestamp,
                    self.user.username,
                    request_id,
                    snapshots,
                )
예제 #26
0
    def test_webhooks_process_webhook_on_update(self):
        """
        Mock a Session.send to inspect the result of `process_webhook()`.
        Note that process_webhook is called directly, not via a celery task.
        """

        request_id = uuid.uuid4()
        webhook = Webhook.objects.get(type_create=True)
        timestamp = str(timezone.now())

        def mock_send(_, request, **kwargs):
            """
            A mock implementation of Session.send() to be used for testing.
            Always returns a 200 HTTP response.
            """
            signature = generate_signature(request.body, webhook.secret)

            # Validate the outgoing request headers
            self.assertEqual(request.headers["Content-Type"],
                             webhook.http_content_type)
            self.assertEqual(request.headers["X-Hook-Signature"], signature)
            self.assertEqual(request.headers["X-Foo"], "Bar")

            # Validate the outgoing request body
            body = json.loads(request.body)
            self.assertEqual(body["event"], "created")
            self.assertEqual(body["timestamp"], timestamp)
            self.assertEqual(body["model"], "site")
            self.assertEqual(body["username"], "testuser")
            self.assertEqual(body["request_id"], str(request_id))
            self.assertEqual(body["data"]["name"], "Site Update")
            self.assertEqual(body["data"]["status"]["value"],
                             self.planned_status.slug)
            self.assertEqual(body["data"]["region"]["slug"],
                             self.region_two.slug)
            self.assertEqual(body["snapshots"]["prechange"]["name"], "Site 1")
            self.assertEqual(body["snapshots"]["prechange"]["status"]["value"],
                             self.active_status.slug)
            self.assertEqual(body["snapshots"]["prechange"]["region"]["slug"],
                             self.region_one.slug)
            self.assertEqual(body["snapshots"]["postchange"]["name"],
                             "Site Update")
            self.assertEqual(
                body["snapshots"]["postchange"]["status"]["value"],
                self.planned_status.slug)
            self.assertEqual(body["snapshots"]["postchange"]["region"]["slug"],
                             self.region_two.slug)
            self.assertEqual(
                body["snapshots"]["differences"]["removed"]["name"], "Site 1")
            self.assertEqual(body["snapshots"]["differences"]["added"]["name"],
                             "Site Update")

            class FakeResponse:
                ok = True
                status_code = 200

            return FakeResponse()

        # Patch the Session object with our mock_send() method, then process the webhook for sending
        with patch.object(Session, "send", mock_send):
            self.client.force_login(self.user)

            request = mock.MagicMock()
            request.user = self.user
            request.id = request_id

            with change_logging(request):
                site = Site(name="Site 1",
                            slug="site-1",
                            status=self.active_status,
                            region=self.region_one)
                site.save()

                site.name = "Site Update"
                site.status = self.planned_status
                site.region = self.region_two
                site.save()

                serializer = SiteSerializer(site, context={"request": None})
                snapshots = get_snapshots(
                    site, ObjectChangeActionChoices.ACTION_UPDATE)

                process_webhook(
                    webhook.pk,
                    serializer.data,
                    Site._meta.model_name,
                    ObjectChangeActionChoices.ACTION_CREATE,
                    timestamp,
                    self.user.username,
                    request_id,
                    snapshots,
                )