Browse Source

test(backup): Improve sanitization tests (#69128)

We add some integration tests, and ensure coverage of all models by
them.
Alex Zaslavsky 10 months ago
parent
commit
8e4570c3d6

+ 2 - 2
src/sentry/backup/sanitize.py

@@ -1,7 +1,7 @@
 from collections.abc import Callable
 from copy import deepcopy
 from dataclasses import dataclass
-from datetime import datetime, timedelta, timezone
+from datetime import UTC, datetime, timedelta, timezone
 from random import choice, randint
 
 import petname
@@ -182,7 +182,7 @@ class Sanitizer:
         for model in self.json:
             for value in model["fields"].values():
                 try:
-                    datetimes.add(parse_datetime(value))
+                    datetimes.add(parse_datetime(value).replace(tzinfo=UTC))
                 except Exception:
                     continue
 

+ 632 - 0
tests/sentry/backup/snapshots/SanitizationExhaustiveTests/test_clean_pks.pysnap

@@ -0,0 +1,632 @@
+---
+created: '2024-04-16T17:28:56.859402+00:00'
+creator: sentry
+source: tests/sentry/backup/test_sanitize.py
+---
+- model_name: sentry.controloption
+  ordinal: 1
+  sanitized_fields:
+  - last_updated
+- model_name: sentry.integration
+  ordinal: 1
+  sanitized_fields:
+  - date_added
+  - date_updated
+  - name
+- model_name: sentry.option
+  ordinal: 1
+  sanitized_fields:
+  - last_updated
+- model_name: sentry.organization
+  ordinal: 1
+  sanitized_fields:
+  - date_added
+  - name
+  - slug
+- model_name: sentry.organization
+  ordinal: 2
+  sanitized_fields:
+  - date_added
+  - name
+  - slug
+- model_name: sentry.organizationintegration
+  ordinal: 1
+  sanitized_fields:
+  - date_added
+  - date_updated
+- model_name: sentry.organizationoption
+  ordinal: 1
+  sanitized_fields: []
+- model_name: sentry.project
+  ordinal: 1
+  sanitized_fields:
+  - date_added
+  - name
+  - slug
+- model_name: sentry.project
+  ordinal: 2
+  sanitized_fields:
+  - date_added
+  - name
+  - slug
+- model_name: sentry.project
+  ordinal: 3
+  sanitized_fields:
+  - date_added
+  - name
+  - slug
+- model_name: sentry.project
+  ordinal: 4
+  sanitized_fields:
+  - date_added
+  - name
+  - slug
+- model_name: sentry.projectintegration
+  ordinal: 1
+  sanitized_fields: []
+- model_name: sentry.projectkey
+  ordinal: 1
+  sanitized_fields:
+  - date_added
+- model_name: sentry.projectkey
+  ordinal: 2
+  sanitized_fields:
+  - date_added
+- model_name: sentry.projectkey
+  ordinal: 3
+  sanitized_fields:
+  - date_added
+- model_name: sentry.projectkey
+  ordinal: 4
+  sanitized_fields:
+  - date_added
+- model_name: sentry.projectoption
+  ordinal: 1
+  sanitized_fields: []
+- model_name: sentry.projectoption
+  ordinal: 2
+  sanitized_fields: []
+- model_name: sentry.projectoption
+  ordinal: 3
+  sanitized_fields: []
+- model_name: sentry.projectoption
+  ordinal: 4
+  sanitized_fields: []
+- model_name: sentry.projectoption
+  ordinal: 5
+  sanitized_fields: []
+- model_name: sentry.projectoption
+  ordinal: 6
+  sanitized_fields: []
+- model_name: sentry.projectoption
+  ordinal: 7
+  sanitized_fields: []
+- model_name: sentry.projectoption
+  ordinal: 8
+  sanitized_fields: []
+- model_name: sentry.projectoption
+  ordinal: 9
+  sanitized_fields: []
+- model_name: sentry.projectoption
+  ordinal: 10
+  sanitized_fields: []
+- model_name: sentry.projectoption
+  ordinal: 11
+  sanitized_fields: []
+- model_name: sentry.projectoption
+  ordinal: 12
+  sanitized_fields: []
+- model_name: sentry.projectownership
+  ordinal: 1
+  sanitized_fields:
+  - date_created
+  - last_updated
+- model_name: sentry.projectredirect
+  ordinal: 1
+  sanitized_fields:
+  - date_added
+- model_name: sentry.relay
+  ordinal: 1
+  sanitized_fields: []
+- model_name: sentry.relayusage
+  ordinal: 1
+  sanitized_fields:
+  - first_seen
+  - last_seen
+- model_name: sentry.repository
+  ordinal: 1
+  sanitized_fields:
+  - date_added
+  - name
+- model_name: sentry.team
+  ordinal: 1
+  sanitized_fields:
+  - date_added
+  - name
+  - slug
+- model_name: sentry.user
+  ordinal: 1
+  sanitized_fields:
+  - date_joined
+  - email
+  - last_active
+  - last_password_change
+  - name
+- model_name: sentry.user
+  ordinal: 2
+  sanitized_fields:
+  - date_joined
+  - email
+  - last_active
+  - last_password_change
+  - name
+- model_name: sentry.user
+  ordinal: 3
+  sanitized_fields:
+  - date_joined
+  - email
+  - last_active
+  - last_password_change
+  - name
+- model_name: sentry.user
+  ordinal: 4
+  sanitized_fields:
+  - date_joined
+  - email
+  - last_active
+  - last_password_change
+  - name
+- model_name: sentry.user
+  ordinal: 5
+  sanitized_fields:
+  - date_joined
+  - email
+  - last_active
+  - name
+- model_name: sentry.user
+  ordinal: 6
+  sanitized_fields:
+  - date_joined
+  - email
+  - last_active
+  - last_password_change
+  - name
+- model_name: sentry.userip
+  ordinal: 1
+  sanitized_fields:
+  - first_seen
+  - last_seen
+- model_name: sentry.userip
+  ordinal: 2
+  sanitized_fields:
+  - first_seen
+  - last_seen
+- model_name: sentry.useroption
+  ordinal: 1
+  sanitized_fields: []
+- model_name: sentry.useroption
+  ordinal: 2
+  sanitized_fields: []
+- model_name: sentry.userpermission
+  ordinal: 1
+  sanitized_fields: []
+- model_name: sentry.userrole
+  ordinal: 1
+  sanitized_fields:
+  - date_added
+  - date_updated
+  - name
+- model_name: sentry.userroleuser
+  ordinal: 1
+  sanitized_fields:
+  - date_added
+  - date_updated
+- model_name: sentry.savedsearch
+  ordinal: 1
+  sanitized_fields:
+  - date_added
+  - name
+- model_name: sentry.recentsearch
+  ordinal: 1
+  sanitized_fields:
+  - date_added
+  - last_seen
+- model_name: sentry.projectteam
+  ordinal: 1
+  sanitized_fields: []
+- model_name: sentry.projectteam
+  ordinal: 2
+  sanitized_fields: []
+- model_name: sentry.projectbookmark
+  ordinal: 1
+  sanitized_fields:
+  - date_added
+- model_name: sentry.orgauthtoken
+  ordinal: 1
+  sanitized_fields:
+  - date_added
+  - name
+- model_name: sentry.organizationmember
+  ordinal: 1
+  sanitized_fields:
+  - date_added
+- model_name: sentry.organizationmember
+  ordinal: 2
+  sanitized_fields:
+  - date_added
+- model_name: sentry.organizationaccessrequest
+  ordinal: 1
+  sanitized_fields: []
+- model_name: sentry.monitor
+  ordinal: 1
+  sanitized_fields:
+  - date_added
+  - name
+  - slug
+- model_name: sentry.environment
+  ordinal: 1
+  sanitized_fields:
+  - date_added
+  - name
+- model_name: sentry.email
+  ordinal: 1
+  sanitized_fields:
+  - date_added
+  - email
+- model_name: sentry.email
+  ordinal: 2
+  sanitized_fields:
+  - date_added
+  - email
+- model_name: sentry.email
+  ordinal: 3
+  sanitized_fields:
+  - date_added
+  - email
+- model_name: sentry.email
+  ordinal: 4
+  sanitized_fields:
+  - date_added
+  - email
+- model_name: sentry.email
+  ordinal: 5
+  sanitized_fields:
+  - date_added
+  - email
+- model_name: sentry.email
+  ordinal: 6
+  sanitized_fields:
+  - date_added
+  - email
+- model_name: sentry.dashboardtombstone
+  ordinal: 1
+  sanitized_fields:
+  - date_added
+- model_name: sentry.dashboard
+  ordinal: 1
+  sanitized_fields:
+  - date_added
+  - last_visited
+- model_name: sentry.customdynamicsamplingrule
+  ordinal: 1
+  sanitized_fields:
+  - date_added
+  - end_date
+  - start_date
+- model_name: sentry.counter
+  ordinal: 1
+  sanitized_fields: []
+- model_name: sentry.authprovider
+  ordinal: 1
+  sanitized_fields:
+  - date_added
+- model_name: sentry.authidentity
+  ordinal: 1
+  sanitized_fields:
+  - date_added
+  - last_synced
+  - last_verified
+- model_name: sentry.authenticator
+  ordinal: 1
+  sanitized_fields:
+  - created_at
+- model_name: sentry.authenticator
+  ordinal: 2
+  sanitized_fields:
+  - created_at
+- model_name: sentry.apikey
+  ordinal: 1
+  sanitized_fields:
+  - date_added
+- model_name: sentry.apiapplication
+  ordinal: 1
+  sanitized_fields:
+  - date_added
+  - name
+- model_name: sentry.actor
+  ordinal: 1
+  sanitized_fields: []
+- model_name: sentry.useremail
+  ordinal: 1
+  sanitized_fields:
+  - date_hash_added
+  - email
+- model_name: sentry.useremail
+  ordinal: 2
+  sanitized_fields:
+  - date_hash_added
+  - email
+- model_name: sentry.useremail
+  ordinal: 3
+  sanitized_fields:
+  - date_hash_added
+  - email
+- model_name: sentry.useremail
+  ordinal: 4
+  sanitized_fields:
+  - date_hash_added
+  - email
+- model_name: sentry.useremail
+  ordinal: 5
+  sanitized_fields:
+  - date_hash_added
+  - email
+- model_name: sentry.useremail
+  ordinal: 6
+  sanitized_fields:
+  - date_hash_added
+  - email
+- model_name: sentry.snubaquery
+  ordinal: 1
+  sanitized_fields:
+  - date_added
+- model_name: sentry.snubaquery
+  ordinal: 2
+  sanitized_fields:
+  - date_added
+- model_name: sentry.snubaquery
+  ordinal: 3
+  sanitized_fields:
+  - date_added
+- model_name: sentry.sentryapp
+  ordinal: 1
+  sanitized_fields:
+  - date_added
+  - date_updated
+  - name
+  - slug
+- model_name: sentry.rule
+  ordinal: 1
+  sanitized_fields:
+  - date_added
+- model_name: sentry.querysubscription
+  ordinal: 1
+  sanitized_fields:
+  - date_added
+  - date_updated
+- model_name: sentry.querysubscription
+  ordinal: 2
+  sanitized_fields:
+  - date_added
+  - date_updated
+- model_name: sentry.querysubscription
+  ordinal: 3
+  sanitized_fields:
+  - date_added
+  - date_updated
+- model_name: sentry.querysubscription
+  ordinal: 4
+  sanitized_fields:
+  - date_added
+  - date_updated
+- model_name: sentry.querysubscription
+  ordinal: 5
+  sanitized_fields:
+  - date_added
+  - date_updated
+- model_name: sentry.organizationmemberteam
+  ordinal: 1
+  sanitized_fields: []
+- model_name: sentry.notificationaction
+  ordinal: 1
+  sanitized_fields: []
+- model_name: sentry.notificationaction
+  ordinal: 2
+  sanitized_fields: []
+- model_name: sentry.neglectedrule
+  ordinal: 1
+  sanitized_fields:
+  - disable_date
+  - sent_final_email_date
+  - sent_initial_email_date
+- model_name: sentry.environmentproject
+  ordinal: 1
+  sanitized_fields: []
+- model_name: sentry.dashboardwidget
+  ordinal: 1
+  sanitized_fields:
+  - date_added
+- model_name: sentry.customdynamicsamplingruleproject
+  ordinal: 1
+  sanitized_fields: []
+- model_name: sentry.apitoken
+  ordinal: 1
+  sanitized_fields:
+  - date_added
+  - expires_at
+- model_name: sentry.apitoken
+  ordinal: 2
+  sanitized_fields:
+  - date_added
+  - name
+- model_name: sentry.apitoken
+  ordinal: 3
+  sanitized_fields:
+  - date_added
+  - name
+- model_name: sentry.apigrant
+  ordinal: 1
+  sanitized_fields:
+  - expires_at
+- model_name: sentry.apiauthorization
+  ordinal: 1
+  sanitized_fields:
+  - date_added
+- model_name: sentry.apiauthorization
+  ordinal: 2
+  sanitized_fields:
+  - date_added
+- model_name: sentry.alertrule
+  ordinal: 1
+  sanitized_fields:
+  - date_added
+  - date_modified
+  - name
+- model_name: sentry.alertrule
+  ordinal: 2
+  sanitized_fields:
+  - date_added
+  - date_modified
+  - name
+- model_name: sentry.alertrule
+  ordinal: 3
+  sanitized_fields:
+  - date_added
+  - date_modified
+  - name
+- model_name: sentry.snubaqueryeventtype
+  ordinal: 1
+  sanitized_fields: []
+- model_name: sentry.snubaqueryeventtype
+  ordinal: 2
+  sanitized_fields: []
+- model_name: sentry.snubaqueryeventtype
+  ordinal: 3
+  sanitized_fields: []
+- model_name: sentry.sentryappinstallation
+  ordinal: 1
+  sanitized_fields:
+  - date_added
+  - date_updated
+- model_name: sentry.sentryappcomponent
+  ordinal: 1
+  sanitized_fields: []
+- model_name: sentry.rulesnooze
+  ordinal: 1
+  sanitized_fields:
+  - date_added
+- model_name: sentry.ruleactivity
+  ordinal: 1
+  sanitized_fields:
+  - date_added
+- model_name: sentry.notificationactionproject
+  ordinal: 1
+  sanitized_fields: []
+- model_name: sentry.notificationactionproject
+  ordinal: 2
+  sanitized_fields: []
+- model_name: sentry.incident
+  ordinal: 1
+  sanitized_fields:
+  - date_added
+  - date_detected
+  - date_started
+- model_name: sentry.dashboardwidgetquery
+  ordinal: 1
+  sanitized_fields:
+  - date_added
+  - date_modified
+  - name
+- model_name: sentry.alertruletrigger
+  ordinal: 1
+  sanitized_fields:
+  - date_added
+- model_name: sentry.alertruletrigger
+  ordinal: 2
+  sanitized_fields:
+  - date_added
+- model_name: sentry.alertruleprojects
+  ordinal: 1
+  sanitized_fields:
+  - date_added
+- model_name: sentry.alertruleprojects
+  ordinal: 2
+  sanitized_fields:
+  - date_added
+- model_name: sentry.alertruleprojects
+  ordinal: 3
+  sanitized_fields:
+  - date_added
+- model_name: sentry.alertruleexcludedprojects
+  ordinal: 1
+  sanitized_fields:
+  - date_added
+- model_name: sentry.alertruleactivity
+  ordinal: 1
+  sanitized_fields:
+  - date_added
+- model_name: sentry.alertruleactivity
+  ordinal: 2
+  sanitized_fields:
+  - date_added
+- model_name: sentry.alertruleactivity
+  ordinal: 3
+  sanitized_fields:
+  - date_added
+- model_name: sentry.alertruleactivationcondition
+  ordinal: 1
+  sanitized_fields:
+  - date_added
+- model_name: sentry.timeseriessnapshot
+  ordinal: 1
+  sanitized_fields:
+  - date_added
+  - end
+  - start
+- model_name: sentry.servicehook
+  ordinal: 1
+  sanitized_fields:
+  - date_added
+- model_name: sentry.servicehook
+  ordinal: 2
+  sanitized_fields:
+  - date_added
+- model_name: sentry.pendingincidentsnapshot
+  ordinal: 1
+  sanitized_fields:
+  - date_added
+  - target_run_date
+- model_name: sentry.incidenttrigger
+  ordinal: 1
+  sanitized_fields:
+  - date_added
+  - date_modified
+- model_name: sentry.incidentsubscription
+  ordinal: 1
+  sanitized_fields:
+  - date_added
+- model_name: sentry.incidentsnapshot
+  ordinal: 1
+  sanitized_fields:
+  - date_added
+- model_name: sentry.incidentactivity
+  ordinal: 1
+  sanitized_fields:
+  - date_added
+- model_name: sentry.dashboardwidgetqueryondemand
+  ordinal: 1
+  sanitized_fields:
+  - date_added
+  - date_modified
+- model_name: sentry.alertruletriggerexclusion
+  ordinal: 1
+  sanitized_fields:
+  - date_added
+- model_name: sentry.alertruletriggeraction
+  ordinal: 1
+  sanitized_fields:
+  - date_added
+- model_name: sentry.alertruletriggeraction
+  ordinal: 2
+  sanitized_fields:
+  - date_added

+ 147 - 0
tests/sentry/backup/snapshots/SanitizationIntegrationTests/test_fresh_install.pysnap

@@ -0,0 +1,147 @@
+---
+created: '2024-04-17T17:41:21.647153+00:00'
+creator: sentry
+source: tests/sentry/backup/test_sanitize.py
+---
+- model_name: sentry.option
+  ordinal: 1
+  sanitized_fields:
+  - last_updated
+- model_name: sentry.option
+  ordinal: 2
+  sanitized_fields:
+  - last_updated
+- model_name: sentry.option
+  ordinal: 3
+  sanitized_fields:
+  - last_updated
+- model_name: sentry.option
+  ordinal: 4
+  sanitized_fields:
+  - last_updated
+- model_name: sentry.actor
+  ordinal: 1
+  sanitized_fields: []
+- model_name: sentry.email
+  ordinal: 1
+  sanitized_fields:
+  - date_added
+  - email
+- model_name: sentry.email
+  ordinal: 2
+  sanitized_fields:
+  - date_added
+  - email
+- model_name: sentry.organization
+  ordinal: 1
+  sanitized_fields:
+  - date_added
+  - name
+  - slug
+- model_name: sentry.user
+  ordinal: 1
+  sanitized_fields:
+  - date_joined
+  - email
+  - last_active
+  - last_password_change
+  - name
+- model_name: sentry.user
+  ordinal: 2
+  sanitized_fields:
+  - date_joined
+  - email
+  - last_active
+  - last_password_change
+  - name
+- model_name: sentry.relayusage
+  ordinal: 1
+  sanitized_fields:
+  - first_seen
+  - last_seen
+- model_name: sentry.relay
+  ordinal: 1
+  sanitized_fields: []
+- model_name: sentry.authenticator
+  ordinal: 1
+  sanitized_fields:
+  - created_at
+- model_name: sentry.authenticator
+  ordinal: 2
+  sanitized_fields:
+  - created_at
+- model_name: sentry.useremail
+  ordinal: 1
+  sanitized_fields:
+  - date_hash_added
+  - email
+- model_name: sentry.useremail
+  ordinal: 2
+  sanitized_fields:
+  - date_hash_added
+  - email
+- model_name: sentry.userrole
+  ordinal: 1
+  sanitized_fields:
+  - date_added
+  - date_updated
+  - name
+- model_name: sentry.userroleuser
+  ordinal: 1
+  sanitized_fields:
+  - date_added
+  - date_updated
+- model_name: sentry.team
+  ordinal: 1
+  sanitized_fields:
+  - date_added
+  - name
+  - slug
+- model_name: sentry.organizationmember
+  ordinal: 1
+  sanitized_fields:
+  - date_added
+- model_name: sentry.organizationmember
+  ordinal: 2
+  sanitized_fields:
+  - date_added
+- model_name: sentry.project
+  ordinal: 1
+  sanitized_fields:
+  - date_added
+  - name
+  - slug
+- model_name: sentry.projectkey
+  ordinal: 1
+  sanitized_fields:
+  - date_added
+- model_name: sentry.rule
+  ordinal: 1
+  sanitized_fields:
+  - date_added
+- model_name: sentry.projectteam
+  ordinal: 1
+  sanitized_fields: []
+- model_name: sentry.organizationmemberteam
+  ordinal: 1
+  sanitized_fields: []
+- model_name: sentry.organizationmemberteam
+  ordinal: 2
+  sanitized_fields: []
+- model_name: sentry.projectoption
+  ordinal: 1
+  sanitized_fields: []
+- model_name: sentry.projectoption
+  ordinal: 2
+  sanitized_fields: []
+- model_name: sentry.projectoption
+  ordinal: 3
+  sanitized_fields: []
+- model_name: sentry.projectoption
+  ordinal: 4
+  sanitized_fields: []
+- model_name: sentry.apiapplication
+  ordinal: 1
+  sanitized_fields:
+  - date_added
+  - name

+ 6 - 0
tests/sentry/backup/test_coverage.py

@@ -8,6 +8,7 @@ from tests.sentry.backup.test_exhaustive import EXHAUSTIVELY_TESTED, UNIQUENESS_
 from tests.sentry.backup.test_imports import COLLISION_TESTED
 from tests.sentry.backup.test_models import DYNAMIC_RELOCATION_SCOPE_TESTED
 from tests.sentry.backup.test_releases import RELEASE_TESTED
+from tests.sentry.backup.test_sanitize import SANITIZATION_TESTED
 
 ALL_EXPORTABLE_MODELS = {get_model_name(c) for c in get_exportable_sentry_models()}
 
@@ -78,6 +79,11 @@ def test_exportable_final_derivations_of_sentry_model_are_release_tested_at_head
     assert not {str(u) for u in untested}
 
 
+def test_exportable_final_derivations_of_sentry_model_are_sanitization_tested_at_head():
+    untested = ALL_EXPORTABLE_MODELS - SANITIZATION_TESTED
+    assert not {str(u) for u in untested}
+
+
 def test_exportable_final_derivations_of_sentry_model_are_uniqueness_tested():
     # No need to uniqueness test global models, since they assume a clean database anyway.
     all_non_global_sentry_models = {

+ 3 - 6
tests/sentry/backup/test_exhaustive.py

@@ -35,12 +35,9 @@ class ExhaustiveTests(BackupTestCase):
         clear_database(reset_pks=reset_pks)
         return tmp_path
 
-    @expect_models(EXHAUSTIVELY_TESTED, "__all__")
-    def test_exhaustive_clean_pks(self, expected_models: list[type[Model]]):
-        self.create_exhaustive_instance(is_superadmin=True)
-        actual = self.import_export_then_validate(self._testMethodName, reset_pks=True)
-        verify_models_in_output(expected_models, actual)
-
+    # Note: the "clean_pks" version of this test lives in
+    # `test_sanitize.py::SanitizationExhaustiveTests`. Because these tests are slow, we want to
+    # reduce duplication, so we only use that one in that particular location.
     @expect_models(EXHAUSTIVELY_TESTED, "__all__")
     def test_exhaustive_dirty_pks(self, expected_models: list[type[Model]]):
         self.create_exhaustive_instance(is_superadmin=True)

+ 83 - 1
tests/sentry/backup/test_sanitize.py

@@ -1,3 +1,5 @@
+import os
+from collections import defaultdict
 from collections.abc import Sequence
 from datetime import datetime, timedelta
 from unittest.mock import Mock, patch
@@ -6,6 +8,7 @@ import pytest
 from dateutil.parser import parse as parse_datetime
 from django.core.serializers import serialize
 from django.db import models
+from django.db.models import Model
 
 from sentry.backup.dependencies import NormalizedModelName, get_model_name
 from sentry.backup.helpers import DatetimeSafeDjangoJSONEncoder
@@ -22,8 +25,12 @@ from sentry.backup.scopes import RelocationScope
 from sentry.db.models.base import DefaultFieldsModel
 from sentry.db.models.fields.slug import SentrySlugField
 from sentry.testutils.cases import TestCase
+from sentry.testutils.factories import get_fixture_path
+from sentry.testutils.helpers.backups import BackupTestCase
+from sentry.testutils.silo import strip_silo_mode_test_suffix
 from sentry.utils import json
 from sentry.utils.json import JSONData
+from tests.sentry.backup import expect_models, verify_models_in_output
 
 FAKE_EMAIL = "test@fake.com"
 FAKE_NAME = "Fake Name"
@@ -64,7 +71,7 @@ class FakeSanitizableModel(DefaultFieldsModel):
 
 
 @patch("sentry.backup.dependencies.get_model", Mock(return_value=FakeSanitizableModel))
-class SanitizerTests(TestCase):
+class SanitizationUnitTests(TestCase):
     def serialize_to_json_data(self, models: Sequence[FakeSanitizableModel]) -> JSONData:
         json_string = serialize(
             "json",
@@ -336,3 +343,78 @@ class SanitizerTests(TestCase):
         )
         with pytest.raises(TypeError):
             sanitize([invalid])
+
+
+class IntegrationTestCase(TestCase):
+    def sanitize_and_compare(self, unsanitized_json: JSONData) -> JSONData:
+        root_dir = os.path.dirname(os.path.realpath(__file__))
+
+        # Use the same data for monolith and region mode.
+        class_name = strip_silo_mode_test_suffix(self.__class__.__name__)
+        snapshot_path = f"{root_dir}/snapshots/{class_name}/{self._testMethodName}.pysnap"
+
+        # Go roughly 10 years backward, to ensure that there is no collision in the data when we
+        # look for diffs.
+        sanitized_json = sanitize(unsanitized_json, timedelta(days=3650))
+        assert len(sanitized_json) == len(unsanitized_json)
+
+        # Walk the two JSONs simultaneously, taking note of any changed fields.
+        diffs = []
+        ordinals: dict[str, int] = defaultdict(lambda: 1)
+        for i, sanitized_model in enumerate(sanitized_json):
+            unsanitized_model = unsanitized_json[i]
+            assert sanitized_model["model"] == unsanitized_model["model"]
+
+            model_name = sanitized_model["model"]
+            ordinal = ordinals[model_name]
+            ordinals[model_name] = ordinal + 1
+            sanitized_fields = sorted(sanitized_model["fields"].keys())
+            unsanitized_fields = sorted(unsanitized_model["fields"].keys())
+            assert set(sanitized_fields) == set(unsanitized_fields)
+
+            diff = []
+            for field_name in sanitized_fields:
+                if sanitized_model["fields"][field_name] != unsanitized_model["fields"][field_name]:
+                    diff.append(field_name)
+
+            diffs.append(
+                {
+                    "model_name": model_name,
+                    "ordinal": ordinal,
+                    "sanitized_fields": diff,
+                }
+            )
+
+        # Perform the snapshot comparison.
+        self.insta_snapshot(diffs, reference_file=snapshot_path)
+
+        return sanitized_json
+
+
+class SanitizationIntegrationTests(IntegrationTestCase):
+    """
+    Use some of our existing export JSON fixtures as test cases, ensuring that a consistent set of
+    fields is altered by sanitization.
+    """
+
+    def test_fresh_install(self):
+        with open(get_fixture_path("backup", "fresh-install.json")) as backup_file:
+            unsanitized_json = json.load(backup_file)
+            self.sanitize_and_compare(unsanitized_json)
+
+
+SANITIZATION_TESTED: set[NormalizedModelName] = set()
+
+
+class SanitizationExhaustiveTests(BackupTestCase, IntegrationTestCase):
+    """
+    Take our exhaustive test case and sanitize it, ensuring that the specific fields that
+    were sanitized remain constant.
+    """
+
+    @expect_models(SANITIZATION_TESTED, "__all__")
+    def test_clean_pks(self, expected_models: list[type[Model]]):
+        self.create_exhaustive_instance(is_superadmin=True)
+        unsanitized_json = self.import_export_then_validate(self._testMethodName, reset_pks=True)
+        sanitized_json = self.sanitize_and_compare(unsanitized_json)
+        verify_models_in_output(expected_models, sanitized_json)