Repository: nautobot/nautobot

Issue:
Unable to Edit/Delete/Add Objects in v1.5.6
<!--
    NOTE: IF YOUR ISSUE DOES NOT FOLLOW THIS TEMPLATE, IT WILL BE CLOSED.

    This form is only for reporting reproducible bugs. If you need assistance
    with Nautobot installation, or if you have a general question, please start a
    discussion instead: https://github.com/nautobot/nautobot/discussions

    Please describe the environment in which you are running Nautobot. Be sure
    that you are running an unmodified instance of the latest stable release
    before submitting a bug report, and that any plugins have been disabled.
-->
### Environment
* Nautobot version (Docker tag too if applicable): 1.5.6
* Python version: 3.8.10
* Database platform, version: postgresql, Ubuntu 12.12-0ubuntu0.20.04.1
* Middleware(s): -

<!--
    Describe in detail the exact steps that someone else can take to reproduce
    this bug using the current stable release of Nautobot. Begin with the
    creation of any necessary database objects and call out every operation
    being performed explicitly. If reporting a bug in the REST API, be sure to
    reconstruct the raw HTTP request(s) being made: Don't rely on a client
    library such as pynautobot.
-->
### Steps to Reproduce
1. Create/Edit/Delete an object (e.g. Interface/IP-Address) in Nautobot either via Web GUI, API-Page or pynautobot

<!-- What did you expect to happen? -->
### Expected Behavior
The creation / edit / deletion of the object will be made without an error. Worked on 1.4.10 Release until upgrade to 1.5.6

<!-- What happened instead? -->
### Observed Behavior
**Web GUI:** An HTTP 400 error (Bad Request (400)) will be shown and no change will be made. The following log will be created in the syslog
```
[...] nautobot-server[2528]: ERROR:django.security.DisallowedHost:Invalid HTTP_HOST header: 'testserver'. You may need to add 'testserver' to ALLOWED_HOSTS.
[...] nautobot-server[2528]: WARNING:django.request:Bad Request: /dcim/interfaces/add/
[...] nautobot-server[2528]: [pid: 2528|app: 0|req: 154/154] x.x.x.x () {76 vars in 1973 bytes}  POST /dcim/interfaces/add/?device=397a12bb-db12-4b87-9283-a3c7cf231c91&return_url=/dcim/devices/397a12bb-db12-4b87-9283-a3c7cf231c91/interfaces/ => generated 143 bytes in 69 msecs (HTTP/1.1 400) 6 headers in 182 bytes (1 switches on core 0)
```
**API-Page:**
_Request_
```
curl -X 'DELETE' \
  'https://***********/api/dcim/interfaces/da7c8120-5da4-42aa-b724-79760c900587/' \
  -H 'accept: */*' \
  -H 'X-CSRFTOKEN: SsqqVYngkfktBhknV2cChj9vDSU91nOsjgp70RSpd0MwU0BbGFakLA5pTNH9oM3u'
```

_Response body_
```
Download
{
  "error": "Invalid HTTP_HOST header: 'testserver'. You may need to add 'testserver' to ALLOWED_HOSTS.",
  "exception": "DisallowedHost",
  "nautobot_version": "1.5.6",
  "python_version": "3.8.10"
}
```

_Response headers_
```
connection: keep-alive 
 content-length: 191 
 content-type: application/json 
 date: Tue,03 Jan 2023 09:09:15 GMT 
 referrer-policy: same-origin 
 server: nginx/1.18.0 (Ubuntu) 
 strict-transport-security: max-age=157680000 
 vary: Cookie,Origin 
 x-content-type-options: nosniff 
 x-frame-options: DENY 
```

**pynautobot**
```
 File "C:\Users\***********\AppData\Local\Packages\PythonSoftwareFoundation.Python.3.10_qbz5n2kfra8p0\LocalCache\local-packages\Python310\site-packages\pynautobot\core\response.py", line 436, in delete
    return True if req.delete() else False
  File "C:\Users\***********\AppData\Local\Packages\PythonSoftwareFoundation.Python.3.10_qbz5n2kfra8p0\LocalCache\local-packages\Python310\site-packages\pynautobot\core\query.py", line 368, in delete
    return self._make_call(verb="delete")
  File "C:\Users\***********\AppData\Local\Packages\PythonSoftwareFoundation.Python.3.10_qbz5n2kfra8p0\LocalCache\local-packages\Python310\site-packages\pynautobot\core\query.py", line 250, in _make_call
    raise RequestError(req)
pynautobot.core.query.RequestError: The request failed with code 500 Internal Server Error: {'error': "Invalid HTTP_HOST header: 'testserver'. You may need to add 'testserver' to ALLOWED_HOSTS.", 'exception': 'DisallowedHost', 'nautobot_version': '1.5.6', 'python_version': '3.8.10'}

```


Discussion:
Update:

I added "testserver" to NAUTOBOT_ALLOWED_HOSTS and the problem seems to be gone. Did I miss something during the upgrade process?

_http://testserver_ was defined in the following unit tests:
https://github.com/nautobot/nautobot/blob/3aa950a2a84cdcc1a4655882489f900c20d64d3f/nautobot/extras/tests/test_api.py
https://github.com/nautobot/nautobot/blob/4ff1ef04bc15073ddbfdf30c182a89e5351f04f2/nautobot/extras/tests/test_relationships.py

To my understanding, those classes won't be called during normal operation 

Hi, we hit the same issue after upgrade from 1.5.2. We tested `Relationship`, `RelationshipAssociation`, `IPaddress` and `Device`, neither can be created at all with v1.5.6.
IMHO the reason is in PR https://github.com/nautobot/nautobot/pull/3016 as those changes are recent and the ones in `test_api.py` are over 5 moths old according to `git blame`. It is now creating fake request during `serialize_object_v2()` via `APIRequestFactory` so I suspect this is the reason, but I miss the deeper understanding of your tests to understand exactly *why* :) 
~Just contributing a test case here. I have no relationships and basic set up in my environment (devices, sites, circuits). I am able to create and delete devices without issue. Perhaps something when relationships are defined?~
I encountered the same problem after upgrading from 1.5.5 to 1.5.6.
Here's a stack trace

```
Traceback (most recent call last):
  File "/usr/local/lib/python3.7/site-packages/django/core/handlers/exception.py", line 47, in inner
    response = get_response(request)
  File "/usr/local/lib/python3.7/site-packages/django/core/handlers/base.py", line 181, in _get_response
    response = wrapped_callback(request, *callback_args, **callback_kwargs)
  File "/usr/local/lib/python3.7/site-packages/django/views/generic/base.py", line 70, in view
    return self.dispatch(request, *args, **kwargs)
  File "/source/nautobot/core/views/generic.py", line 412, in dispatch
    return super().dispatch(request, *args, **kwargs)
  File "/source/nautobot/utilities/views.py", line 116, in dispatch
    return super().dispatch(request, *args, **kwargs)
  File "/usr/local/lib/python3.7/site-packages/django/views/generic/base.py", line 98, in dispatch
    return handler(request, *args, **kwargs)
  File "/source/nautobot/core/views/generic.py", line 446, in post
    obj = form.save()
  File "/source/nautobot/extras/forms/mixins.py", line 640, in save
    obj = super().save(commit)
  File "/usr/local/lib/python3.7/site-packages/django/forms/models.py", line 468, in save
    self.instance.save()
  File "/usr/local/lib/python3.7/site-packages/django/db/models/base.py", line 740, in save
    force_update=force_update, update_fields=update_fields)
  File "/usr/local/lib/python3.7/site-packages/django/db/models/base.py", line 789, in save_base
    update_fields=update_fields, raw=raw, using=using,
  File "/usr/local/lib/python3.7/site-packages/django/dispatch/dispatcher.py", line 182, in send
    for receiver in self._live_receivers(sender)
  File "/usr/local/lib/python3.7/site-packages/django/dispatch/dispatcher.py", line 182, in <listcomp>
    for receiver in self._live_receivers(sender)
  File "/source/nautobot/utilities/utils.py", line 453, in _curried
    return _curried_func(*args, *moreargs, **{**kwargs, **morekwargs})
  File "/source/nautobot/extras/signals.py", line 87, in _handle_changed_object
    objectchange = instance.to_objectchange(action)
  File "/source/nautobot/extras/models/change_logging.py", line 44, in to_objectchange
    object_data_v2=serialize_object_v2(self),
  File "/source/nautobot/utilities/utils.py", line 237, in serialize_object_v2
    data = serializer_class(obj, context={"request": request}).data
  File "/usr/local/lib/python3.7/site-packages/rest_framework/serializers.py", line 555, in data
    ret = super().data
  File "/usr/local/lib/python3.7/site-packages/rest_framework/serializers.py", line 253, in data
    self._data = self.to_representation(self.instance)
  File "/usr/local/lib/python3.7/site-packages/rest_framework/serializers.py", line 522, in to_representation
    ret[field.field_name] = field.to_representation(attribute)
  File "/usr/local/lib/python3.7/site-packages/rest_framework/relations.py", line 401, in to_representation
    url = self.get_url(value, self.view_name, request, format)
  File "/usr/local/lib/python3.7/site-packages/rest_framework/relations.py", line 339, in get_url
    return self.reverse(view_name, kwargs=kwargs, request=request, format=format)
  File "/usr/local/lib/python3.7/site-packages/rest_framework/reverse.py", line 47, in reverse
    url = _reverse(viewname, args, kwargs, request, format, **extra)
  File "/usr/local/lib/python3.7/site-packages/rest_framework/reverse.py", line 62, in _reverse
    return request.build_absolute_uri(url)
  File "/usr/local/lib/python3.7/site-packages/django/http/request.py", line 236, in build_absolute_uri
    location = self._current_scheme_host + location
  File "/usr/local/lib/python3.7/site-packages/django/utils/functional.py", line 48, in __get__
    res = instance.__dict__[self.name] = self.func(instance)
  File "/usr/local/lib/python3.7/site-packages/django/http/request.py", line 246, in _current_scheme_host
    return '{}://{}'.format(self.scheme, self.get_host())
  File "/usr/local/lib/python3.7/site-packages/django/http/request.py", line 149, in get_host
    raise DisallowedHost(msg)

Exception Type: DisallowedHost at /dcim/sites/add/
Exception Value: Invalid HTTP_HOST header: 'testserver'. You may need to add 'testserver' to ALLOWED_HOSTS.
```

Resolving patch:
diff --git a/nautobot/extras/context_managers.py b/nautobot/extras/context_managers.py
--- a/nautobot/extras/context_managers.py
+++ b/nautobot/extras/context_managers.py
@@ -2,16 +2,11 @@
 from contextlib import contextmanager
 
 from django.contrib.auth import get_user_model
-from django.db.models.signals import m2m_changed, post_delete, post_save, pre_delete
+from django.db.models.signals import m2m_changed, pre_delete, post_save
 from django.test.client import RequestFactory
 
 from nautobot.extras.choices import ObjectChangeEventContextChoices
-from nautobot.extras.models import RelationshipAssociation
-from nautobot.extras.signals import (
-    _handle_changed_object,
-    _handle_deleted_object,
-    _handle_relationship_association_change,
-)
+from nautobot.extras.signals import _handle_changed_object, _handle_deleted_object
 from nautobot.utilities.utils import curry
 
 
@@ -92,22 +87,11 @@ def change_logging(change_context):
     # Curry signals receivers to pass the current request
     handle_changed_object = curry(_handle_changed_object, change_context)
     handle_deleted_object = curry(_handle_deleted_object, change_context)
-    handle_relationship_association_change = curry(_handle_relationship_association_change, change_context)
 
     # Connect our receivers to the post_save and post_delete signals.
     post_save.connect(handle_changed_object, dispatch_uid="handle_changed_object")
     m2m_changed.connect(handle_changed_object, dispatch_uid="handle_changed_object")
     pre_delete.connect(handle_deleted_object, dispatch_uid="handle_deleted_object")
-    post_save.connect(
-        handle_relationship_association_change,
-        dispatch_uid="handle_relationship_association_change",
-        sender=RelationshipAssociation,
-    )
-    post_delete.connect(
-        handle_relationship_association_change,
-        dispatch_uid="handle_relationship_association_change",
-        sender=RelationshipAssociation,
-    )
 
     yield
 
@@ -116,16 +100,6 @@ def change_logging(change_context):
     post_save.disconnect(handle_changed_object, dispatch_uid="handle_changed_object")
     m2m_changed.disconnect(handle_changed_object, dispatch_uid="handle_changed_object")
     pre_delete.disconnect(handle_deleted_object, dispatch_uid="handle_deleted_object")
-    post_save.disconnect(
-        handle_relationship_association_change,
-        dispatch_uid="handle_relationship_association_change",
-        sender=RelationshipAssociation,
-    )
-    post_delete.disconnect(
-        handle_relationship_association_change,
-        dispatch_uid="handle_relationship_association_change",
-        sender=RelationshipAssociation,
-    )
 
 
 @contextmanager
diff --git a/nautobot/extras/signals.py b/nautobot/extras/signals.py
--- a/nautobot/extras/signals.py
+++ b/nautobot/extras/signals.py
@@ -19,15 +19,8 @@
 from nautobot.extras.utils import refresh_job_model_from_job_class
 from nautobot.utilities.config import get_settings_or_config
 from nautobot.extras.constants import CHANGELOG_MAX_CHANGE_CONTEXT_DETAIL
-from nautobot.extras.choices import JobResultStatusChoices, ObjectChangeActionChoices
-from nautobot.extras.models import (
-    CustomField,
-    DynamicGroup,
-    DynamicGroupMembership,
-    GitRepository,
-    JobResult,
-    ObjectChange,
-)
+from .choices import JobResultStatusChoices, ObjectChangeActionChoices
+from .models import CustomField, DynamicGroup, DynamicGroupMembership, GitRepository, JobResult, ObjectChange
 from .webhooks import enqueue_webhooks
 
 logger = logging.getLogger("nautobot.extras.signals")
@@ -305,43 +298,3 @@ def refresh_job_models(sender, *, apps, **kwargs):
             )
             job_model.installed = False
             job_model.save()
-
-
-def _handle_relationship_association_change(change_context, sender, instance, **kwargs):
-    """
-    Signal handler for RelationshipAssociation changes. When a RelationshipAssociation
-    is changed or deleted, also create ObjectChanges for related objects.
-    """
-    from .jobs import enqueue_job_hooks  # avoid circular import
-
-    for related_object in [instance.source, instance.destination]:
-        if related_object is None:
-            continue
-        if hasattr(related_object, "to_objectchange"):
-            updated_objectchange = related_object.to_objectchange(ObjectChangeActionChoices.ACTION_UPDATE)
-            related_changes = ObjectChange.objects.filter(
-                changed_object_type=ContentType.objects.get_for_model(related_object),
-                changed_object_id=related_object.pk,
-                request_id=change_context.change_id,
-            )
-            if related_changes.exists():
-                related_changes.update(
-                    object_data=updated_objectchange.object_data, object_data_v2=updated_objectchange.object_data_v2
-                )
-                enqueue_job_hooks(related_changes.first())
-            else:
-                updated_objectchange.user = _get_user_if_authenticated(change_context.get_user(), updated_objectchange)
-                updated_objectchange.request_id = change_context.change_id
-                updated_objectchange.change_context = change_context.context
-                updated_objectchange.change_context_detail = change_context.context_detail[
-                    :CHANGELOG_MAX_CHANGE_CONTEXT_DETAIL
-                ]
-                updated_objectchange.save()
-                enqueue_job_hooks(updated_objectchange)
-
-        enqueue_webhooks(
-            related_object,
-            change_context.get_user(),
-            change_context.change_id,
-            ObjectChangeActionChoices.ACTION_UPDATE,
-        )
diff --git a/nautobot/utilities/utils.py b/nautobot/utilities/utils.py
--- a/nautobot/utilities/utils.py
+++ b/nautobot/utilities/utils.py
@@ -32,7 +32,6 @@
     NumberFilter,
 )
 from django_filters.utils import verbose_lookup_expr
-from rest_framework.test import APIRequestFactory
 from taggit.managers import _TaggableManager
 
 from nautobot.dcim.choices import CableLengthUnitChoices
@@ -231,10 +230,7 @@ def serialize_object_v2(obj):
     # Try serializing obj(model instance) using its API Serializer
     try:
         serializer_class = get_serializer_for_model(obj.__class__)
-        # create a fake request to include opt-in `relationships` field if available
-        request = APIRequestFactory().get("/", {"include": ["relationships"]})
-        request.version = settings.REST_FRAMEWORK_VERSION
-        data = serializer_class(obj, context={"request": request}).data
+        data = serializer_class(obj, context={"request": None}).data
     except SerializerNotFound:
         # Fall back to generic JSON representation of obj
         data = serialize_object(obj)


Test patch:
diff --git a/nautobot/extras/tests/test_relationships.py b/nautobot/extras/tests/test_relationships.py
--- a/nautobot/extras/tests/test_relationships.py
+++ b/nautobot/extras/tests/test_relationships.py
@@ -1,4 +1,3 @@
-import copy
 import logging
 import uuid
 
@@ -11,22 +10,16 @@
 from nautobot.dcim.models import Device, Platform, Rack, Site
 from nautobot.dcim.tables import SiteTable
 from nautobot.dcim.tests.test_views import create_test_device
-from nautobot.ipam.models import IPAddress, VLAN
-from nautobot.extras import context_managers
-from nautobot.extras.choices import (
-    ObjectChangeActionChoices,
-    RelationshipRequiredSideChoices,
-    RelationshipSideChoices,
-    RelationshipTypeChoices,
-)
-from nautobot.extras.models import ObjectChange, Relationship, RelationshipAssociation, Status
+from nautobot.ipam.models import VLAN
+from nautobot.extras.choices import RelationshipRequiredSideChoices, RelationshipSideChoices, RelationshipTypeChoices
+from nautobot.extras.models import Relationship, RelationshipAssociation, Status
 from nautobot.utilities.tables import RelationshipColumn
 from nautobot.utilities.testing import TestCase
 from nautobot.utilities.forms import (
     DynamicModelChoiceField,
     DynamicModelMultipleChoiceField,
 )
-from nautobot.utilities.utils import get_changes_for_model, get_route_for_model
+from nautobot.utilities.utils import get_route_for_model
 
 
 class RelationshipBaseTest(TestCase):
@@ -1370,179 +1363,3 @@ def required_relationships_test(self, interact_with="ui"):
                         }
                     }
                     self.assertEqual(expected_error_json, response.json())
-
-
-class RelationshipChangeLoggingTest(TestCase):
-    @classmethod
-    def setUpTestData(cls):
-        cls.site_status = Status.objects.get_for_model(Site).first()
-        site_ct = ContentType.objects.get_for_model(Site)
-        cls.ipaddress_ct = ContentType.objects.get_for_model(IPAddress)
-        cls.site_ip_relationship = Relationship(
-            name="Site to IP Address",
-            slug="sites-ipaddresses-m2m",
-            type="many-to-many",
-            source_type=site_ct,
-            destination_type=cls.ipaddress_ct,
-        )
-        cls.site_ip_relationship.validated_save()
-        cls.site = Site.objects.first()
-        cls.ip = IPAddress.objects.first()
-        cls.base_diff = {
-            "relationships": {
-                cls.site_ip_relationship.slug: {
-                    "id": str(cls.site_ip_relationship.pk),
-                    "name": cls.site_ip_relationship.name,
-                    "type": "many-to-many",
-                    "url": "http://testserver"
-                    + reverse("extras-api:relationship-detail", kwargs={"pk": cls.site_ip_relationship.pk}),
-                    "destination": {
-                        "label": "IP addresses",
-                        "object_type": "ipam.ipaddress",
-                        "objects": [],
-                    },
-                    "source": {
-                        "label": "sites",
-                        "object_type": "dcim.site",
-                        "objects": [],
-                    },
-                }
-            }
-        }
-
-    def test_change_logging_relationship_association(self):
-        """Test that an objectchange is generated for associated objects when a RelationshipAssociation is saved"""
-        # generate initial object change
-        with context_managers.web_request_context(self.user):
-            self.ip.validated_save()
-            self.site.validated_save()
-
-        with context_managers.web_request_context(self.user):
-            RelationshipAssociation.objects.create(
-                relationship=self.site_ip_relationship, source=self.site, destination=self.ip
-            )
-
-        with self.subTest("Source object change was generated"):
-            self.assertEqual(get_changes_for_model(self.site).count(), 2)
-            object_change = get_changes_for_model(self.site).first()
-            self.assertEqual(object_change.action, ObjectChangeActionChoices.ACTION_UPDATE)
-
-            # ensure diff only contains added relationship
-            snapshots = object_change.get_snapshots()
-            removed_diff = copy.deepcopy(self.base_diff)
-            removed_diff["relationships"][self.site_ip_relationship.slug].pop("source")
-            added_diff = copy.deepcopy(removed_diff)
-            added_diff["relationships"][self.site_ip_relationship.slug]["destination"]["objects"] = [
-                {
-                    "id": str(self.ip.pk),
-                    "url": "http://testserver" + reverse("ipam-api:ipaddress-detail", kwargs={"pk": self.ip.pk}),
-                    "address": str(self.ip),
-                    "display": getattr(self.ip, "display", str(self.ip)),
-                    "family": self.ip.family,
-                }
-            ]
-            self.assertEqual(snapshots["differences"]["removed"], removed_diff)
-            self.assertEqual(snapshots["differences"]["added"], added_diff)
-
-        with self.subTest("Destination object change was generated"):
-            self.assertEqual(get_changes_for_model(self.ip).count(), 2)
-            object_change = get_changes_for_model(self.ip).first()
-            self.assertEqual(object_change.action, ObjectChangeActionChoices.ACTION_UPDATE)
-
-            # ensure diff only contains added relationship
-            snapshots = object_change.get_snapshots()
-            removed_diff = copy.deepcopy(self.base_diff)
-            removed_diff["relationships"][self.site_ip_relationship.slug].pop("destination")
-            added_diff = copy.deepcopy(removed_diff)
-            added_diff["relationships"][self.site_ip_relationship.slug]["source"]["objects"] = [
-                {
-                    "id": str(self.site.pk),
-                    "url": "http://testserver" + reverse("dcim-api:site-detail", kwargs={"pk": self.site.pk}),
-                    "display": getattr(self.site, "display", str(self.site)),
-                    "name": self.site.name,
-                    "slug": self.site.slug,
-                }
-            ]
-            self.assertEqual(snapshots["differences"]["removed"], removed_diff)
-            self.assertEqual(snapshots["differences"]["added"], added_diff)
-
-    def test_change_logging_with_object_save(self):
-        """
-        Test that only a single objectchange is generated for associated objects when a RelationshipAssociation
-        is created and the associated object is saved in the same request
-        """
-        with context_managers.web_request_context(self.user):
-            self.ip.validated_save()
-            self.site.validated_save()
-            RelationshipAssociation.objects.create(
-                relationship=self.site_ip_relationship, source=self.site, destination=self.ip
-            )
-
-        with self.subTest("Source object change was generated"):
-            self.assertEqual(get_changes_for_model(self.site).count(), 1)
-            object_change = get_changes_for_model(self.site).first()
-            snapshots_added = object_change.get_snapshots()["differences"]["added"]
-            self.assertIn("relationships", snapshots_added)
-            self.assertEqual(
-                snapshots_added["relationships"]["sites-ipaddresses-m2m"]["destination"]["objects"][0]["id"],
-                str(self.ip.pk),
-            )
-
-        with self.subTest("Destination object change was generated"):
-            self.assertEqual(get_changes_for_model(self.ip).count(), 1)
-            object_change = get_changes_for_model(self.ip).first()
-            snapshots_added = object_change.get_snapshots()["differences"]["added"]
-            self.assertIn("relationships", snapshots_added)
-            self.assertEqual(
-                snapshots_added["relationships"]["sites-ipaddresses-m2m"]["source"]["objects"][0]["id"],
-                str(self.site.pk),
-            )
-
-    def test_change_logging_relationship_association_on_delete(self):
-        """Test that an objectchange is generated for related objects when an object with relationships is deleted"""
-        # generate initial object change
-        with context_managers.web_request_context(self.user):
-            self.ip.validated_save()
-            self.site.validated_save()
-
-        with context_managers.web_request_context(self.user):
-            RelationshipAssociation.objects.create(
-                relationship=self.site_ip_relationship, source=self.site, destination=self.ip
-            )
-
-        with context_managers.web_request_context(self.user):
-            ip_pk = str(self.ip.pk)
-            self.ip.delete()
-
-        with self.subTest("Source object change was generated"):
-            # object created, modified (relationship added), modified (relationship deleted)
-            site_changes = get_changes_for_model(self.site)
-            self.assertEqual(site_changes.count(), 3)
-
-            # ensure newest diff only contains removed relationship
-            object_change = site_changes.first()
-            self.assertEqual(object_change.action, ObjectChangeActionChoices.ACTION_UPDATE)
-            snapshots = object_change.get_snapshots()
-            added_diff = copy.deepcopy(self.base_diff)
-            added_diff["relationships"][self.site_ip_relationship.slug].pop("source")
-            removed_diff = copy.deepcopy(added_diff)
-            removed_diff["relationships"][self.site_ip_relationship.slug]["destination"]["objects"] = [
-                {
-                    "id": ip_pk,
-                    "url": "http://testserver" + reverse("ipam-api:ipaddress-detail", kwargs={"pk": ip_pk}),
-                    "address": str(self.ip),
-                    "display": getattr(self.ip, "display", str(self.ip)),
-                    "family": self.ip.family,
-                }
-            ]
-            self.assertEqual(snapshots["differences"]["removed"], removed_diff)
-            self.assertEqual(snapshots["differences"]["added"], added_diff)
-
-        with self.subTest("Object change was generated for object delete but not relationship change"):
-            # object created, modified (relationship added), object deleted
-            ip_changes = ObjectChange.objects.filter(
-                changed_object_type=self.ipaddress_ct,
-                changed_object_id=ip_pk,
-            )
-            self.assertEqual(ip_changes.count(), 3)
-            self.assertEqual(ip_changes.first().action, ObjectChangeActionChoices.ACTION_DELETE)