From ad10aa3381f26b63f548e0156aa9f3b2dd03e679 Mon Sep 17 00:00:00 2001 From: Jason Novinger Date: Fri, 11 Sep 2026 01:45:11 -0500 Subject: [PATCH 1/9] Fixes #23046: Install dulwich in CI so Git backend tests run dulwich is the optional 'git' extra and is not in requirements.txt, so CI never had it installed. The four GitBackendCredentialIntegrationTestCase tests added in #21252 have therefore been skipped since they were written, leaving the Git data source credential handling without CI coverage. These tests mock dulwich.porcelain.clone, so they only need the module to be importable. Installing it in the test environment does not change what a NetBox deployment pulls in: dulwich stays optional at runtime, as decided in #12906. --- .github/workflows/ci.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9a7220c21..340c2f4f3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -122,6 +122,10 @@ jobs: python -m pip install --upgrade pip pip install -r requirements.txt pip install coverage tblib + # dulwich is the optional 'git' extra, so it is absent from requirements.txt. + # The Git data backend tests mock dulwich.porcelain.clone and only need the + # module to be importable; without it they skip silently. + pip install dulwich - name: Check for missing migrations run: python netbox/manage.py makemigrations --check From 5959f915fa46328dcf8fbb95bae676e862a29a83 Mon Sep 17 00:00:00 2001 From: Jason Novinger Date: Fri, 11 Sep 2026 01:47:59 -0500 Subject: [PATCH 2/9] Note the CI dulwich install in the Git backend test docstring The docstring said these tests require dulwich without saying where that comes from. Point at the CI install now that one exists. --- netbox/core/tests/test_data_backends.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/netbox/core/tests/test_data_backends.py b/netbox/core/tests/test_data_backends.py index 9f96a0cb3..0789c2981 100644 --- a/netbox/core/tests/test_data_backends.py +++ b/netbox/core/tests/test_data_backends.py @@ -62,7 +62,8 @@ class GitBackendCredentialIntegrationTestCase(TestCase): Integration tests that verify GitBackend correctly applies credential logic. These tests require dulwich to be installed and verify the full integration - of the credential handling in GitBackend.fetch(). + of the credential handling in GitBackend.fetch(). dulwich is the optional + 'git' extra, so CI installs it explicitly; see .github/workflows/ci.yml. """ def _get_clone_kwargs(self, url, **params): From ad8730151fb810e645dcc155dfd2a3a268059041 Mon Sep 17 00:00:00 2001 From: Jason Novinger Date: Fri, 11 Sep 2026 02:27:10 -0500 Subject: [PATCH 3/9] Populate bulk_update_invalid_data for dcim API tests test_bulk_update_objects_validation_error skips unless a test class sets bulk_update_invalid_data, and only SiteTestCase did, so the test has been skipping for nearly every model since it was added. Give each dcim test class an invalid payload, preferring a real choice field where the model has one, then a validated ColorField, then a nonexistent FK pk. Note that component template serializers have no owner field, so those classes use an FK instead. All 53 dcim API test classes now run the test. --- netbox/dcim/tests/test_api.py | 153 ++++++++++++++++++++++++++++++++++ 1 file changed, 153 insertions(+) diff --git a/netbox/dcim/tests/test_api.py b/netbox/dcim/tests/test_api.py index 5ba406ee5..5e3319afc 100644 --- a/netbox/dcim/tests/test_api.py +++ b/netbox/dcim/tests/test_api.py @@ -106,6 +106,9 @@ class RegionTestCase(APIViewTestCases.APIViewTestCase): 'description': 'New description', 'comments': 'New comments', } + bulk_update_invalid_data = { + 'owner': 99999, + } @classmethod def setUpTestData(cls): @@ -139,6 +142,9 @@ class SiteGroupTestCase(APIViewTestCases.APIViewTestCase): 'description': 'New description', 'comments': 'I do exist!', } + bulk_update_invalid_data = { + 'owner': 99999, + } @classmethod def setUpTestData(cls): @@ -973,6 +979,9 @@ class LocationTestCase(APIViewTestCases.APIViewTestCase): bulk_update_data = { 'description': 'New description', } + bulk_update_invalid_data = { + 'status': 'not-a-valid-status', + } user_permissions = ('dcim.view_site',) graphql_filter_tests = ( GraphQLFilterTest( @@ -1095,6 +1104,9 @@ class RackGroupTestCase(APIViewTestCases.APIViewTestCase): bulk_update_data = { 'description': 'New description', } + bulk_update_invalid_data = { + 'owner': 99999, + } @classmethod def setUpTestData(cls): @@ -1130,6 +1142,9 @@ class RackRoleTestCase(APIViewTestCases.APIViewTestCase): bulk_update_data = { 'description': 'New description', } + bulk_update_invalid_data = { + 'color': 'not-a-color', + } @classmethod def setUpTestData(cls): @@ -1150,6 +1165,9 @@ class RackTypeTestCase(APIViewTestCases.APIViewTestCase): 'cooling_capability': RackCoolingCapabilityChoices.CAPABILITY_HYBRID, 'cooling_capacity': 50, } + bulk_update_invalid_data = { + 'form_factor': 'not-a-valid-form-factor', + } user_permissions = ('dcim.view_manufacturer',) @classmethod @@ -1213,6 +1231,9 @@ class RackTestCase(APIViewTestCases.APIViewTestCase): bulk_update_data = { 'status': 'planned', } + bulk_update_invalid_data = { + 'status': 'not-a-valid-status', + } user_permissions = ('dcim.view_site', ) @classmethod @@ -1339,6 +1360,9 @@ class RackReservationTestCase(APIViewTestCases.APIViewTestCase): bulk_update_data = { 'description': 'New description', } + bulk_update_invalid_data = { + 'status': 'not-a-valid-status', + } user_permissions = ('dcim.view_rack', 'users.view_user') @classmethod @@ -1428,6 +1452,9 @@ class ManufacturerTestCase(APIViewTestCases.APIViewTestCase): bulk_update_data = { 'description': 'New description', } + bulk_update_invalid_data = { + 'owner': 99999, + } @classmethod def setUpTestData(cls): @@ -1447,6 +1474,9 @@ class DeviceTypeTestCase(APIViewTestCases.APIViewTestCase): 'part_number': 'ABC123', 'end_of_life': '2030-01-01', } + bulk_update_invalid_data = { + 'airflow': 'not-a-valid-airflow', + } user_permissions = ('dcim.view_manufacturer', ) @classmethod @@ -1495,6 +1525,9 @@ class ModuleTypeTestCase(APIViewTestCases.APIViewTestCase): 'part_number': 'ABC123', 'end_of_life': '2030-01-01', } + bulk_update_invalid_data = { + 'airflow': 'not-a-valid-airflow', + } user_permissions = ('dcim.view_manufacturer', ) @classmethod @@ -1584,6 +1617,9 @@ class ModuleTypeProfileTestCase(APIViewTestCases.APIViewTestCase): 'description': 'New description', 'comments': 'New comments', } + bulk_update_invalid_data = { + 'owner': 99999, + } @classmethod def setUpTestData(cls): @@ -1610,6 +1646,9 @@ class ModuleBayTypeTestCase(APIViewTestCases.APIViewTestCase): bulk_update_data = { 'description': 'New description', } + bulk_update_invalid_data = { + 'color': 'not-a-color', + } @classmethod def setUpTestData(cls): @@ -1651,6 +1690,9 @@ class ConsolePortTemplateTestCase(APIViewTestCases.APIViewTestCase): bulk_update_data = { 'description': 'New description', } + bulk_update_invalid_data = { + 'type': 'not-a-valid-type', + } @classmethod def setUpTestData(cls): @@ -1695,6 +1737,9 @@ class ConsoleServerPortTemplateTestCase(APIViewTestCases.APIViewTestCase): bulk_update_data = { 'description': 'New description', } + bulk_update_invalid_data = { + 'type': 'not-a-valid-type', + } @classmethod def setUpTestData(cls): @@ -1739,6 +1784,9 @@ class PowerPortTemplateTestCase(APIViewTestCases.APIViewTestCase): bulk_update_data = { 'description': 'New description', } + bulk_update_invalid_data = { + 'type': 'not-a-valid-type', + } @classmethod def setUpTestData(cls): @@ -1783,6 +1831,9 @@ class PowerOutletTemplateTestCase(APIViewTestCases.APIViewTestCase): bulk_update_data = { 'description': 'New description', } + bulk_update_invalid_data = { + 'type': 'not-a-valid-type', + } user_permissions = ('dcim.view_devicetype', ) @classmethod @@ -1841,6 +1892,9 @@ class InterfaceTemplateTestCase(APIViewTestCases.APIViewTestCase): bulk_update_data = { 'description': 'New description', } + bulk_update_invalid_data = { + 'poe_mode': 'not-a-valid-poe-mode', + } @classmethod def setUpTestData(cls): @@ -1906,6 +1960,9 @@ class FrontPortTemplateTestCase(APIViewTestCases.APIViewTestCase): bulk_update_data = { 'description': 'New description', } + bulk_update_invalid_data = { + 'color': 'not-a-color', + } user_permissions = ('dcim.view_rearporttemplate', ) @classmethod @@ -2023,6 +2080,9 @@ class RearPortTemplateTestCase(APIViewTestCases.APIViewTestCase): bulk_update_data = { 'description': 'New description', } + bulk_update_invalid_data = { + 'color': 'not-a-color', + } @classmethod def setUpTestData(cls): @@ -2139,6 +2199,9 @@ class ModuleBayTemplateTestCase(APIViewTestCases.APIViewTestCase): bulk_update_data = { 'description': 'New description', } + bulk_update_invalid_data = { + 'device_type': 99999, + } user_permissions = ('dcim.view_devicetype', ) @classmethod @@ -2191,6 +2254,9 @@ class DeviceBayTemplateTestCase(APIViewTestCases.APIViewTestCase): bulk_update_data = { 'description': 'New description', } + bulk_update_invalid_data = { + 'device_type': 99999, + } user_permissions = ('dcim.view_devicetype', ) @classmethod @@ -2233,6 +2299,9 @@ class InventoryItemTemplateTestCase(APIViewTestCases.APIViewTestCase): bulk_update_data = { 'description': 'New description', } + bulk_update_invalid_data = { + 'role': 99999, + } user_permissions = ('dcim.view_devicetype', 'dcim.view_manufacturer',) @classmethod @@ -2312,6 +2381,9 @@ class DeviceRoleTestCase(APIViewTestCases.APIViewTestCase): bulk_update_data = { 'description': 'New description', } + bulk_update_invalid_data = { + 'color': 'not-a-color', + } @classmethod def setUpTestData(cls): @@ -2343,6 +2415,9 @@ class PlatformTestCase(APIViewTestCases.APIViewTestCase): bulk_update_data = { 'description': 'New description', } + bulk_update_invalid_data = { + 'owner': 99999, + } @classmethod def setUpTestData(cls): @@ -2362,6 +2437,9 @@ class DeviceTestCase(APIViewTestCases.APIViewTestCase): bulk_update_data = { 'status': 'failed', } + bulk_update_invalid_data = { + 'status': 'not-a-valid-status', + } user_permissions = ( 'dcim.view_site', 'dcim.view_rack', 'dcim.view_location', 'dcim.view_devicerole', 'dcim.view_devicetype', ) @@ -2821,6 +2899,9 @@ class ModuleTestCase(APIViewTestCases.APIViewTestCase): bulk_update_data = { 'serial': '1234ABCD', } + bulk_update_invalid_data = { + 'status': 'not-a-valid-status', + } user_permissions = ( 'dcim.view_modulebay', 'dcim.view_moduletype', 'dcim.view_moduletypeprofile', 'dcim.view_device' ) @@ -3317,6 +3398,9 @@ class ConsolePortTestCase(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTe bulk_update_data = { 'description': 'New description', } + bulk_update_invalid_data = { + 'type': 'not-a-valid-type', + } peer_termination_type = ConsoleServerPort user_permissions = ('dcim.view_device', ) @@ -3360,6 +3444,9 @@ class ConsoleServerPortTestCase(Mixins.ComponentTraceMixin, APIViewTestCases.API bulk_update_data = { 'description': 'New description', } + bulk_update_invalid_data = { + 'type': 'not-a-valid-type', + } peer_termination_type = ConsolePort user_permissions = ('dcim.view_device', ) @@ -3403,6 +3490,9 @@ class PowerPortTestCase(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTest bulk_update_data = { 'description': 'New description', } + bulk_update_invalid_data = { + 'type': 'not-a-valid-type', + } peer_termination_type = PowerOutlet user_permissions = ('dcim.view_device', ) @@ -3443,6 +3533,9 @@ class PowerOutletTestCase(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTe bulk_update_data = { 'description': 'New description', } + bulk_update_invalid_data = { + 'status': 'not-a-valid-status', + } peer_termination_type = PowerPort user_permissions = ('dcim.view_device', ) @@ -3492,6 +3585,9 @@ class InterfaceTestCase(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTest bulk_update_data = { 'description': 'New description', } + bulk_update_invalid_data = { + 'poe_mode': 'not-a-valid-poe-mode', + } peer_termination_type = Interface user_permissions = ('dcim.view_device', ) @@ -4051,6 +4147,9 @@ class FrontPortTestCase(APIViewTestCases.APIViewTestCase): bulk_update_data = { 'description': 'New description', } + bulk_update_invalid_data = { + 'color': 'not-a-color', + } peer_termination_type = Interface user_permissions = ('dcim.view_device', 'dcim.view_rearport') @@ -4170,6 +4269,9 @@ class RearPortTestCase(APIViewTestCases.APIViewTestCase): bulk_update_data = { 'description': 'New description', } + bulk_update_invalid_data = { + 'color': 'not-a-color', + } peer_termination_type = Interface user_permissions = ('dcim.view_device', ) @@ -4286,6 +4388,9 @@ class ModuleBayTestCase(APIViewTestCases.APIViewTestCase): bulk_update_data = { 'description': 'New description', } + bulk_update_invalid_data = { + 'owner': 99999, + } user_permissions = ('dcim.view_device', ) @classmethod @@ -4417,6 +4522,9 @@ class DeviceBayTestCase(APIViewTestCases.APIViewTestCase): bulk_update_data = { 'description': 'New description', } + bulk_update_invalid_data = { + 'owner': 99999, + } user_permissions = ('dcim.view_device', ) @classmethod @@ -4481,6 +4589,9 @@ class InventoryItemTestCase(APIViewTestCases.APIViewTestCase): bulk_update_data = { 'description': 'New description', } + bulk_update_invalid_data = { + 'status': 'not-a-valid-status', + } user_permissions = ('dcim.view_device', 'dcim.view_manufacturer') @classmethod @@ -4565,6 +4676,9 @@ class InventoryItemRoleTestCase(APIViewTestCases.APIViewTestCase): bulk_update_data = { 'description': 'New description', } + bulk_update_invalid_data = { + 'color': 'not-a-color', + } @classmethod def setUpTestData(cls): @@ -4588,6 +4702,9 @@ class CableBundleTestCase(APIViewTestCases.APIViewTestCase): bulk_update_data = { 'description': 'New description', } + bulk_update_invalid_data = { + 'owner': 99999, + } @classmethod def setUpTestData(cls): @@ -4638,6 +4755,9 @@ class CableTestCase(APIViewTestCases.APIViewTestCase): 'length': 100, 'length_unit': 'm', } + bulk_update_invalid_data = { + 'status': 'not-a-valid-status', + } # TODO: Allow updating cable terminations test_update_object = None @@ -5094,6 +5214,9 @@ class VirtualChassisTestCase(APIViewTestCases.APIViewTestCase): 'domain': 'newdomain', 'master': None } + cls.bulk_update_invalid_data = { + 'owner': 99999, + } class PowerPanelTestCase(APIViewTestCases.APIViewTestCase): @@ -5144,6 +5267,9 @@ class PowerPanelTestCase(APIViewTestCases.APIViewTestCase): 'site': sites[1].pk, 'location': locations[3].pk } + cls.bulk_update_invalid_data = { + 'owner': 99999, + } class PowerFeedTestCase(APIViewTestCases.APIViewTestCase): @@ -5152,6 +5278,9 @@ class PowerFeedTestCase(APIViewTestCases.APIViewTestCase): bulk_update_data = { 'status': 'planned', } + bulk_update_invalid_data = { + 'status': 'not-a-valid-status', + } user_permissions = ('dcim.view_powerpanel', ) @classmethod @@ -5208,6 +5337,9 @@ class CoolingIntakeTemplateTestCase(APIViewTestCases.APIViewTestCase): bulk_update_data = { 'description': 'New description', } + bulk_update_invalid_data = { + 'type': 'not-a-valid-type', + } @classmethod def setUpTestData(cls): @@ -5253,6 +5385,9 @@ class CoolingOutflowTemplateTestCase(APIViewTestCases.APIViewTestCase): bulk_update_data = { 'description': 'New description', } + bulk_update_invalid_data = { + 'type': 'not-a-valid-type', + } user_permissions = ('dcim.view_devicetype', ) @classmethod @@ -5312,6 +5447,9 @@ class CoolingIntakeTestCase(APIViewTestCases.APIViewTestCase): bulk_update_data = { 'description': 'New description', } + bulk_update_invalid_data = { + 'type': 'not-a-valid-type', + } user_permissions = ('dcim.view_device', ) @classmethod @@ -5356,6 +5494,9 @@ class CoolingOutflowTestCase(APIViewTestCases.APIViewTestCase): bulk_update_data = { 'description': 'New description', } + bulk_update_invalid_data = { + 'type': 'not-a-valid-type', + } user_permissions = ('dcim.view_device', ) @classmethod @@ -5459,6 +5600,9 @@ class CoolingSourceTestCase(APIViewTestCases.APIViewTestCase): 'site': sites[1].pk, 'location': locations[3].pk } + cls.bulk_update_invalid_data = { + 'status': 'not-a-valid-status', + } class CoolingFeedTestCase(APIViewTestCases.APIViewTestCase): @@ -5467,6 +5611,9 @@ class CoolingFeedTestCase(APIViewTestCases.APIViewTestCase): bulk_update_data = { 'status': 'planned', } + bulk_update_invalid_data = { + 'status': 'not-a-valid-status', + } user_permissions = ('dcim.view_coolingsource', ) @classmethod @@ -5528,6 +5675,9 @@ class VirtualDeviceContextTestCase(APIViewTestCases.APIViewTestCase): bulk_update_data = { 'status': 'planned', } + bulk_update_invalid_data = { + 'status': 'not-a-valid-status', + } @classmethod def setUpTestData(cls): @@ -5651,6 +5801,9 @@ class MACAddressTestCase(APIViewTestCases.APIViewTestCase): bulk_update_data = { 'description': 'New description', } + bulk_update_invalid_data = { + 'owner': 99999, + } @classmethod def setUpTestData(cls): From 17ebddd0f2615fa8fe3c2856db826ebfe84b73fe Mon Sep 17 00:00:00 2001 From: Jason Novinger Date: Fri, 11 Sep 2026 02:41:14 -0500 Subject: [PATCH 4/9] Populate bulk_update_invalid_data for ipam API tests Give each ipam test class an invalid payload so test_bulk_update_objects_validation_error runs instead of skipping, following the same preference order used for dcim. Three classes needed something other than the obvious choice: Aggregate's family is read-only on the serializer, and FHRPGroupAssignment and VLANTranslationRule do not mix in OwnerMixin, so all three use a required FK instead. All 18 ipam API test classes now run the test. --- netbox/ipam/tests/test_api.py | 54 +++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/netbox/ipam/tests/test_api.py b/netbox/ipam/tests/test_api.py index 2b04945fb..53b22f4fb 100644 --- a/netbox/ipam/tests/test_api.py +++ b/netbox/ipam/tests/test_api.py @@ -30,6 +30,9 @@ class ASNRangeTestCase(APIViewTestCases.APIViewTestCase): bulk_update_data = { 'description': 'New description', } + bulk_update_invalid_data = { + 'owner': 99999, + } @classmethod def setUpTestData(cls): @@ -142,6 +145,9 @@ class ASNTestCase(APIViewTestCases.APIViewTestCase): bulk_update_data = { 'description': 'New description', } + bulk_update_invalid_data = { + 'owner': 99999, + } @classmethod def setUpTestData(cls): @@ -220,6 +226,9 @@ class VRFTestCase(APIViewTestCases.APIViewTestCase): bulk_update_data = { 'description': 'New description', } + bulk_update_invalid_data = { + 'owner': 99999, + } @classmethod def setUpTestData(cls): @@ -263,6 +272,9 @@ class RouteTargetTestCase(APIViewTestCases.APIViewTestCase): bulk_update_data = { 'description': 'New description', } + bulk_update_invalid_data = { + 'owner': 99999, + } @classmethod def setUpTestData(cls): @@ -295,6 +307,9 @@ class RIRTestCase(APIViewTestCases.APIViewTestCase): bulk_update_data = { 'description': 'New description', } + bulk_update_invalid_data = { + 'owner': 99999, + } @classmethod def setUpTestData(cls): @@ -313,6 +328,9 @@ class AggregateTestCase(APIViewTestCases.APIViewTestCase): bulk_update_data = { 'description': 'New description', } + bulk_update_invalid_data = { + 'rir': 99999, + } @classmethod def setUpTestData(cls): @@ -415,6 +433,9 @@ class RoleTestCase(APIViewTestCases.APIViewTestCase): bulk_update_data = { 'description': 'New description', } + bulk_update_invalid_data = { + 'owner': 99999, + } @classmethod def setUpTestData(cls): @@ -455,6 +476,9 @@ class PrefixTestCase(APIViewTestCases.APIViewTestCase): bulk_update_data = { 'description': 'New description', } + bulk_update_invalid_data = { + 'status': 'not-a-valid-status', + } @classmethod def setUpTestData(cls): @@ -754,6 +778,9 @@ class IPRangeTestCase(APIViewTestCases.APIViewTestCase): bulk_update_data = { 'description': 'New description', } + bulk_update_invalid_data = { + 'status': 'not-a-valid-status', + } @classmethod def setUpTestData(cls): @@ -909,6 +936,9 @@ class IPAddressTestCase(APIViewTestCases.APIViewTestCase): bulk_update_data = { 'description': 'New description', } + bulk_update_invalid_data = { + 'status': 'not-a-valid-status', + } graphql_filter = { 'address': {'lookup': 'i_exact', 'value': '192.168.0.1/24'}, } @@ -1071,6 +1101,9 @@ class FHRPGroupTestCase(APIViewTestCases.APIViewTestCase): 'name': 'foobar-999', 'description': 'New description', } + bulk_update_invalid_data = { + 'protocol': 'not-a-valid-protocol', + } @classmethod def setUpTestData(cls): @@ -1117,6 +1150,9 @@ class FHRPGroupAssignmentTestCase(APIViewTestCases.APIViewTestCase): bulk_update_data = { 'priority': 100, } + bulk_update_invalid_data = { + 'group': 99999, + } user_permissions = ('ipam.view_fhrpgroup', ) @classmethod @@ -1214,6 +1250,9 @@ class VLANGroupTestCase(APIViewTestCases.APIViewTestCase): bulk_update_data = { 'description': 'New description', } + bulk_update_invalid_data = { + 'owner': 99999, + } @classmethod def setUpTestData(cls): @@ -1320,6 +1359,9 @@ class VLANTestCase(APIViewTestCases.APIViewTestCase): bulk_update_data = { 'description': 'New description', } + bulk_update_invalid_data = { + 'status': 'not-a-valid-status', + } @classmethod def setUpTestData(cls): @@ -1387,6 +1429,9 @@ class VLANTranslationPolicyTestCase(APIViewTestCases.APIViewTestCase): bulk_update_data = { 'description': 'New description', } + bulk_update_invalid_data = { + 'owner': 99999, + } @classmethod def setUpTestData(cls): @@ -1523,6 +1568,9 @@ class VLANTranslationRuleTestCase(APIViewTestCases.APIViewTestCase): 'policy': vlan_translation_policies[2].pk, 'description': 'New description', } + cls.bulk_update_invalid_data = { + 'policy': 99999, + } def test_standard_fields_in_representation(self): """The standard URL, tag, custom-field and change-tracking names appear in the representation.""" @@ -1554,6 +1602,9 @@ class ServiceTemplateTestCase(APIViewTestCases.APIViewTestCase): bulk_update_data = { 'description': 'New description', } + bulk_update_invalid_data = { + 'owner': 99999, + } graphql_base_name = 'service_template' @classmethod @@ -1838,6 +1889,9 @@ class ServiceTestCase(APIViewTestCases.APIViewTestCase): bulk_update_data = { 'description': 'New description', } + bulk_update_invalid_data = { + 'owner': 99999, + } graphql_base_name = 'service' @classmethod From de595f6a985a6522a39ba8f949132d11f00a691b Mon Sep 17 00:00:00 2001 From: Jason Novinger Date: Fri, 11 Sep 2026 02:54:42 -0500 Subject: [PATCH 5/9] Populate bulk_update_invalid_data for extras API tests Give each extras test class an invalid payload so test_bulk_update_objects_validation_error runs instead of skipping. extras serializers are more varied than dcim's, so fewer classes had a plain choice field to work with: the config context and template classes use a nonexistent data_source pk, and the saved filter, table config, export template and subscription classes use an invalid content type. Two fields were deliberately avoided. CustomField uses filter_logic rather than type, because validate_type rejects any type change on an existing instance and would fail for the wrong reason. EventRule uses action_type rather than event_types, whose ArrayField choices do not map to a reliable serializer error. All 16 extras API test classes now run the test. --- netbox/extras/tests/test_api.py | 48 +++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/netbox/extras/tests/test_api.py b/netbox/extras/tests/test_api.py index 6123dcc36..72454126e 100644 --- a/netbox/extras/tests/test_api.py +++ b/netbox/extras/tests/test_api.py @@ -62,6 +62,9 @@ class WebhookTestCase(APIViewTestCases.APIViewTestCase): 'description': 'New description', 'ssl_verification': False, } + bulk_update_invalid_data = { + 'http_method': 'not-a-valid-http-method', + } @classmethod def setUpTestData(cls): @@ -90,6 +93,9 @@ class EventRuleTestCase(APIViewTestCases.APIViewTestCase): 'enabled': False, 'description': 'New description', } + bulk_update_invalid_data = { + 'action_type': 'not-a-valid-action-type', + } update_data = { 'name': 'Event Rule X', 'enabled': False, @@ -308,6 +314,9 @@ class CustomFieldTestCase(APIViewTestCases.APIViewTestCase): 'description': 'New description', 'nulls_first': False, } + bulk_update_invalid_data = { + 'filter_logic': 'not-a-valid-filter-logic', + } update_data = { 'object_types': ['dcim.device'], 'name': 'New_Name', @@ -377,6 +386,9 @@ class CustomFieldChoiceSetTestCase(APIViewTestCases.APIViewTestCase): bulk_update_data = { 'description': 'New description', } + bulk_update_invalid_data = { + 'base_choices': 'not-a-valid-base-choices', + } update_data = { 'name': 'Choice Set X', 'extra_choices': [ @@ -577,6 +589,9 @@ class CustomLinkTestCase(APIViewTestCases.APIViewTestCase): 'new_window': True, 'enabled': False, } + bulk_update_invalid_data = { + 'button_class': 'not-a-valid-button-class', + } @classmethod def setUpTestData(cls): @@ -661,6 +676,9 @@ class SavedFilterTestCase(SharedObjectAPITestMixin, APIViewTestCases.APIViewTest 'enabled': False, 'shared': False, } + bulk_update_invalid_data = { + 'object_types': ['dcim.notamodel'], + } @classmethod def setUpTestData(cls): @@ -753,6 +771,9 @@ class TableConfigTestCase(SharedObjectAPITestMixin, APIViewTestCases.APIViewTest 'enabled': False, 'shared': False, } + bulk_update_invalid_data = { + 'object_type': 'dcim.notamodel', + } @classmethod def setUpTestData(cls): @@ -942,6 +963,9 @@ class ExportTemplateTestCase(APIViewTestCases.APIViewTestCase): bulk_update_data = { 'description': 'New description', } + bulk_update_invalid_data = { + 'object_types': ['dcim.notamodel'], + } @classmethod def setUpTestData(cls): @@ -989,6 +1013,9 @@ class TagTestCase(APIViewTestCases.APIViewTestCase): bulk_update_data = { 'description': 'New description', } + bulk_update_invalid_data = { + 'color': 'not-a-color', + } @classmethod def setUpTestData(cls): @@ -1083,6 +1110,9 @@ class JournalEntryTestCase(APIViewTestCases.APIViewTestCase): bulk_update_data = { 'comments': 'Overwritten', } + bulk_update_invalid_data = { + 'kind': 'not-a-valid-kind', + } @classmethod def setUpTestData(cls): @@ -1169,6 +1199,9 @@ class ConfigContextProfileTestCase(APIViewTestCases.APIViewTestCase): bulk_update_data = { 'description': 'New description', } + bulk_update_invalid_data = { + 'data_source': 99999, + } @classmethod def setUpTestData(cls): @@ -1281,6 +1314,9 @@ class ConfigContextTestCase(APIViewTestCases.APIViewTestCase): bulk_update_data = { 'description': 'New description', } + bulk_update_invalid_data = { + 'data_source': 99999, + } @classmethod def setUpTestData(cls): @@ -1411,6 +1447,9 @@ class ConfigTemplateTestCase(APIViewTestCases.APIViewTestCase): bulk_update_data = { 'description': 'New description', } + bulk_update_invalid_data = { + 'data_source': 99999, + } @classmethod def setUpTestData(cls): @@ -2226,6 +2265,9 @@ class SubscriptionTestCase(APIViewTestCases.APIViewTestCase): cls.bulk_update_data = { 'user': users[3].pk, } + cls.bulk_update_invalid_data = { + 'object_type': 'dcim.notamodel', + } class NotificationGroupTestCase(APIViewTestCases.APIViewTestCase): @@ -2257,6 +2299,9 @@ class NotificationGroupTestCase(APIViewTestCases.APIViewTestCase): bulk_update_data = { 'description': 'New description', } + bulk_update_invalid_data = { + 'users': [99999], + } @classmethod def setUpTestData(cls): @@ -2311,6 +2356,9 @@ class NotificationTestCase(APIViewTestCases.APIViewTestCase): bulk_update_data = { 'read': now(), } + bulk_update_invalid_data = { + 'event_type': 'not-a-valid-event-type', + } graphql_filter = { 'event_type': {'lookup': 'exact', 'value': OBJECT_CREATED}, } From 1ec29cfc028d6a18a89783555bc2018d69367a48 Mon Sep 17 00:00:00 2001 From: Jason Novinger Date: Fri, 11 Sep 2026 02:56:55 -0500 Subject: [PATCH 6/9] Populate bulk_update_invalid_data for vpn API tests Give each vpn test class an invalid payload so test_bulk_update_objects_validation_error runs instead of skipping. Most vpn models carry several choice fields, so these mostly use one of those. TunnelGroup has none and uses a nonexistent owner pk, matching the organizational models in dcim, and L2VPNTermination uses a nonexistent l2vpn pk because its other relations are read-only. IPSecPolicy uses a non-numeric pfs_group value, since the Diffie-Hellman group choices are integers. All 10 vpn API test classes now run the test. --- netbox/vpn/tests/test_api.py | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/netbox/vpn/tests/test_api.py b/netbox/vpn/tests/test_api.py index 25695bbf0..dc7e14a77 100644 --- a/netbox/vpn/tests/test_api.py +++ b/netbox/vpn/tests/test_api.py @@ -38,6 +38,9 @@ class TunnelGroupTestCase(APIViewTestCases.APIViewTestCase): bulk_update_data = { 'description': 'New description', } + bulk_update_invalid_data = { + 'owner': 99999, + } @classmethod def setUpTestData(cls): @@ -58,6 +61,9 @@ class TunnelTestCase(APIViewTestCases.APIViewTestCase): 'encapsulation': TunnelEncapsulationChoices.ENCAP_GRE, 'description': 'New description', } + bulk_update_invalid_data = { + 'status': 'not-a-valid-status', + } @classmethod def setUpTestData(cls): @@ -117,6 +123,9 @@ class TunnelTerminationTestCase(APIViewTestCases.APIViewTestCase): bulk_update_data = { 'role': TunnelTerminationRoleChoices.ROLE_PEER, } + bulk_update_invalid_data = { + 'role': 'not-a-valid-role', + } user_permissions = ('vpn.view_tunnel', ) @classmethod @@ -189,6 +198,9 @@ class IKEProposalTestCase(APIViewTestCases.APIViewTestCase): 'group': DHGroupChoices.GROUP_19, 'description': 'New description', } + bulk_update_invalid_data = { + 'authentication_algorithm': 'not-a-valid-authentication-algorithm', + } @classmethod def setUpTestData(cls): @@ -252,6 +264,9 @@ class IKEPolicyTestCase(APIViewTestCases.APIViewTestCase): 'description': 'New description', 'preshared_key': 'New key', } + bulk_update_invalid_data = { + 'version': 'not-a-valid-version', + } @classmethod def setUpTestData(cls): @@ -325,6 +340,9 @@ class IPSecProposalTestCase(APIViewTestCases.APIViewTestCase): 'authentication_algorithm': AuthenticationAlgorithmChoices.AUTH_HMAC_MD5, 'description': 'New description', } + bulk_update_invalid_data = { + 'encryption_algorithm': 'not-a-valid-encryption-algorithm', + } @classmethod def setUpTestData(cls): @@ -374,6 +392,9 @@ class IPSecPolicyTestCase(APIViewTestCases.APIViewTestCase): 'pfs_group': DHGroupChoices.GROUP_5, 'description': 'New description', } + bulk_update_invalid_data = { + 'pfs_group': 'not-a-valid-pfs-group', + } @classmethod def setUpTestData(cls): @@ -519,6 +540,10 @@ class IPSecProfileTestCase(APIViewTestCases.APIViewTestCase): 'description': 'New description', } + cls.bulk_update_invalid_data = { + 'mode': 'not-a-valid-mode', + } + class L2VPNTestCase(APIViewTestCases.APIViewTestCase): model = L2VPN @@ -549,6 +574,9 @@ class L2VPNTestCase(APIViewTestCases.APIViewTestCase): bulk_update_data = { 'description': 'New description', } + bulk_update_invalid_data = { + 'status': 'not-a-valid-status', + } @classmethod def setUpTestData(cls): @@ -656,3 +684,7 @@ class L2VPNTerminationTestCase(APIViewTestCases.APIViewTestCase): cls.bulk_update_data = { 'l2vpn': l2vpns[2].pk } + + cls.bulk_update_invalid_data = { + 'l2vpn': 99999, + } From d23b8a2d07bfc9a205e2244b88177d35a5a0b3d6 Mon Sep 17 00:00:00 2001 From: Jason Novinger Date: Fri, 11 Sep 2026 02:57:44 -0500 Subject: [PATCH 7/9] Populate bulk_update_invalid_data for circuits API tests Give each circuits test class an invalid payload so test_bulk_update_objects_validation_error runs instead of skipping. Four classes have neither a choice field nor a colour field on their serializer and use a nonexistent FK pk instead. owner is absent from the circuit termination, circuit group assignment and virtual circuit termination serializers, so it was only used where it is actually present. All 11 circuits API test classes now run the test. --- netbox/circuits/tests/test_api.py | 33 +++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/netbox/circuits/tests/test_api.py b/netbox/circuits/tests/test_api.py index 81d609847..3fe724b6b 100644 --- a/netbox/circuits/tests/test_api.py +++ b/netbox/circuits/tests/test_api.py @@ -23,6 +23,9 @@ class ProviderTestCase(APIViewTestCases.APIViewTestCase): bulk_update_data = { 'comments': 'New comments', } + bulk_update_invalid_data = { + 'owner': 99999, + } @classmethod def setUpTestData(cls): @@ -79,6 +82,9 @@ class CircuitTypeTestCase(APIViewTestCases.APIViewTestCase): bulk_update_data = { 'description': 'New description', } + bulk_update_invalid_data = { + 'color': 'not-a-color', + } @classmethod def setUpTestData(cls): @@ -97,6 +103,9 @@ class CircuitTestCase(APIViewTestCases.APIViewTestCase): bulk_update_data = { 'status': 'planned', } + bulk_update_invalid_data = { + 'status': 'not-a-valid-status', + } user_permissions = ('circuits.view_provider', 'circuits.view_circuittype') @classmethod @@ -215,6 +224,9 @@ class CircuitTerminationTestCase(APIViewTestCases.APIViewTestCase): cls.bulk_update_data = { 'port_speed': 123456 } + cls.bulk_update_invalid_data = { + 'term_side': 'not-a-valid-term-side', + } class CircuitGroupTestCase(APIViewTestCases.APIViewTestCase): @@ -223,6 +235,9 @@ class CircuitGroupTestCase(APIViewTestCases.APIViewTestCase): bulk_update_data = { 'description': 'New description', } + bulk_update_invalid_data = { + 'tenant': 99999, + } @classmethod def setUpTestData(cls): @@ -291,6 +306,9 @@ class ProviderAccountTestCase(APIViewTestCases.APIViewTestCase): 'provider': providers[1].pk, 'description': 'New description', } + cls.bulk_update_invalid_data = { + 'provider': 99999, + } class CircuitGroupAssignmentTestCase(APIViewTestCases.APIViewTestCase): @@ -299,6 +317,9 @@ class CircuitGroupAssignmentTestCase(APIViewTestCases.APIViewTestCase): bulk_update_data = { 'priority': CircuitPriorityChoices.PRIORITY_INACTIVE, } + bulk_update_invalid_data = { + 'priority': 'not-a-valid-priority', + } user_permissions = ('circuits.view_circuit', 'circuits.view_circuitgroup') @classmethod @@ -407,6 +428,9 @@ class ProviderNetworkTestCase(APIViewTestCases.APIViewTestCase): 'provider': providers[1].pk, 'description': 'New description', } + cls.bulk_update_invalid_data = { + 'provider': 99999, + } class VirtualCircuitTypeTestCase(APIViewTestCases.APIViewTestCase): @@ -429,6 +453,9 @@ class VirtualCircuitTypeTestCase(APIViewTestCases.APIViewTestCase): bulk_update_data = { 'description': 'New description', } + bulk_update_invalid_data = { + 'color': 'not-a-color', + } @classmethod def setUpTestData(cls): @@ -447,6 +474,9 @@ class VirtualCircuitTestCase(APIViewTestCases.APIViewTestCase): bulk_update_data = { 'status': 'planned', } + bulk_update_invalid_data = { + 'status': 'not-a-valid-status', + } @classmethod def setUpTestData(cls): @@ -511,6 +541,9 @@ class VirtualCircuitTerminationTestCase(APIViewTestCases.APIViewTestCase): bulk_update_data = { 'description': 'New description', } + bulk_update_invalid_data = { + 'role': 'not-a-valid-role', + } @classmethod def setUpTestData(cls): From 5816e25b81e490108cf50cda850658567eb3dc98 Mon Sep 17 00:00:00 2001 From: Jason Novinger Date: Fri, 11 Sep 2026 03:00:39 -0500 Subject: [PATCH 8/9] Populate bulk_update_invalid_data for the remaining API tests Cover virtualization, tenancy, users, wireless and core so test_bulk_update_objects_validation_error runs instead of skipping. Every class in the suite that sets bulk_update_data now sets an invalid payload alongside it. Two cases needed care. DataSource's status is read-only on its serializer, so a bogus status would have been ignored rather than rejected, and it uses a nonexistent owner pk instead. OwnerGroup exposes only name, description and a read-only member_count, so an over-length description is the only value that can fail validation there. A comment records why. --- netbox/core/tests/test_api.py | 3 +++ netbox/tenancy/tests/test_api.py | 18 ++++++++++++++++++ netbox/users/tests/test_api.py | 18 ++++++++++++++++++ netbox/virtualization/tests/test_api.py | 21 +++++++++++++++++++++ netbox/wireless/tests/test_api.py | 10 ++++++++++ 5 files changed, 70 insertions(+) diff --git a/netbox/core/tests/test_api.py b/netbox/core/tests/test_api.py index 0c852636b..bf3d03c30 100644 --- a/netbox/core/tests/test_api.py +++ b/netbox/core/tests/test_api.py @@ -35,6 +35,9 @@ class DataSourceTestCase(APIViewTestCases.APIViewTestCase): 'enabled': False, 'description': 'foo bar baz', } + bulk_update_invalid_data = { + 'owner': 99999, + } @classmethod def setUpTestData(cls): diff --git a/netbox/tenancy/tests/test_api.py b/netbox/tenancy/tests/test_api.py index dcba0025b..f53b19810 100644 --- a/netbox/tenancy/tests/test_api.py +++ b/netbox/tenancy/tests/test_api.py @@ -28,6 +28,9 @@ class TenantGroupTestCase(APIViewTestCases.APIViewTestCase): 'description': 'New description', 'comments': 'New Comment', } + bulk_update_invalid_data = { + 'owner': 99999, + } @classmethod def setUpTestData(cls): @@ -119,6 +122,9 @@ class TenantTestCase(APIViewTestCases.APIViewTestCase): 'group': None, 'description': 'New description', } + bulk_update_invalid_data = { + 'owner': 99999, + } @classmethod def setUpTestData(cls): @@ -160,6 +166,9 @@ class ContactGroupTestCase(APIViewTestCases.APIViewTestCase): bulk_update_data = { 'description': 'New description', } + bulk_update_invalid_data = { + 'owner': 99999, + } @classmethod def setUpTestData(cls): @@ -219,6 +228,9 @@ class ContactRoleTestCase(APIViewTestCases.APIViewTestCase): bulk_update_data = { 'description': 'New description', } + bulk_update_invalid_data = { + 'owner': 99999, + } @classmethod def setUpTestData(cls): @@ -238,6 +250,9 @@ class ContactTestCase(APIViewTestCases.APIViewTestCase): 'groups': [], 'comments': 'New comments', } + bulk_update_invalid_data = { + 'owner': 99999, + } @classmethod def setUpTestData(cls): @@ -277,6 +292,9 @@ class ContactAssignmentTestCase(APIViewTestCases.APIViewTestCase): bulk_update_data = { 'priority': ContactPriorityChoices.PRIORITY_INACTIVE, } + bulk_update_invalid_data = { + 'priority': 'not-a-valid-priority', + } user_permissions = ('tenancy.view_contact', ) @classmethod diff --git a/netbox/users/tests/test_api.py b/netbox/users/tests/test_api.py index 1e5fd5ccf..22d6adf8b 100644 --- a/netbox/users/tests/test_api.py +++ b/netbox/users/tests/test_api.py @@ -24,6 +24,9 @@ class UserTestCase(APIViewTestCases.APIViewTestCase): bulk_update_data = { 'email': 'test@example.com', } + bulk_update_invalid_data = { + 'email': 'not-an-email', + } @classmethod def setUpTestData(cls): @@ -202,6 +205,9 @@ class TokenTestCase( bulk_update_data = { 'description': 'New description', } + bulk_update_invalid_data = { + 'expires': 'not-a-date', + } def setUp(self): super().setUp() @@ -588,6 +594,9 @@ class ObjectPermissionTestCase( cls.bulk_update_data = { 'description': 'New description', } + cls.bulk_update_invalid_data = { + 'users': [99999], + } class UserConfigTestCase(APITestCase): @@ -649,6 +658,11 @@ class OwnerGroupTestCase(APIViewTestCases.APIViewTestCase): bulk_update_data = { 'description': 'New description', } + # OwnerGroupSerializer exposes only name, description, and a read-only member_count, so an + # over-length description is the only value available to trigger a validation error. + bulk_update_invalid_data = { + 'description': 'a' * 201, + } @classmethod def setUpTestData(cls): @@ -750,3 +764,7 @@ class OwnerTestCase(APIViewTestCases.APIViewTestCase): 'users': [users[3].pk], 'description': 'New description', } + + cls.bulk_update_invalid_data = { + 'group': 99999, + } diff --git a/netbox/virtualization/tests/test_api.py b/netbox/virtualization/tests/test_api.py index f20c58653..9f00d79aa 100644 --- a/netbox/virtualization/tests/test_api.py +++ b/netbox/virtualization/tests/test_api.py @@ -56,6 +56,9 @@ class ClusterTypeTestCase(APIViewTestCases.APIViewTestCase): bulk_update_data = { 'description': 'New description', } + bulk_update_invalid_data = { + 'owner': 99999, + } @classmethod def setUpTestData(cls): @@ -88,6 +91,9 @@ class ClusterGroupTestCase(APIViewTestCases.APIViewTestCase): bulk_update_data = { 'description': 'New description', } + bulk_update_invalid_data = { + 'owner': 99999, + } @classmethod def setUpTestData(cls): @@ -107,6 +113,9 @@ class ClusterTestCase(APIViewTestCases.APIViewTestCase): 'status': 'offline', 'comments': 'New comment', } + bulk_update_invalid_data = { + 'status': 'not-a-valid-status', + } @classmethod def setUpTestData(cls): @@ -235,6 +244,9 @@ class VirtualMachineTypeTestCase(APIViewTestCases.APIViewTestCase): 'default_memory': 8192, 'description': 'New description', } + cls.bulk_update_invalid_data = { + 'owner': 99999, + } class VirtualMachineTestCase(APIViewTestCases.APIViewTestCase): @@ -243,6 +255,9 @@ class VirtualMachineTestCase(APIViewTestCases.APIViewTestCase): bulk_update_data = { 'status': 'staged', } + bulk_update_invalid_data = { + 'status': 'not-a-valid-status', + } user_permissions = ('dcim.view_platform', 'virtualization.view_virtualmachinetype') @classmethod @@ -624,6 +639,9 @@ class VMInterfaceTestCase(APIViewTestCases.APIViewTestCase): bulk_update_data = { 'description': 'New description', } + bulk_update_invalid_data = { + 'mode': 'not-a-valid-mode', + } graphql_base_name = 'vm_interface' user_permissions = ('virtualization.view_virtualmachine', ) @@ -915,6 +933,9 @@ class VirtualDiskTestCase(APIViewTestCases.APIViewTestCase): bulk_update_data = { 'size': 888, } + bulk_update_invalid_data = { + 'virtual_machine': 99999, + } graphql_base_name = 'virtual_disk' user_permissions = ('virtualization.view_virtualmachine', ) diff --git a/netbox/wireless/tests/test_api.py b/netbox/wireless/tests/test_api.py index b5671db32..bbfed9327 100644 --- a/netbox/wireless/tests/test_api.py +++ b/netbox/wireless/tests/test_api.py @@ -40,6 +40,9 @@ class WirelessLANGroupTestCase(APIViewTestCases.APIViewTestCase): 'description': 'New description', 'comments': 'New comment', } + bulk_update_invalid_data = { + 'owner': 99999, + } @classmethod def setUpTestData(cls): @@ -118,6 +121,10 @@ class WirelessLANTestCase(APIViewTestCases.APIViewTestCase): 'auth_psk': 'abc123def456', } + cls.bulk_update_invalid_data = { + 'auth_type': 'not-a-valid-auth-type', + } + class WirelessLinkTestCase(APIViewTestCases.APIViewTestCase): model = WirelessLink @@ -127,6 +134,9 @@ class WirelessLinkTestCase(APIViewTestCases.APIViewTestCase): 'distance': 100, 'distance_unit': 'm', } + bulk_update_invalid_data = { + 'status': 'not-a-valid-status', + } user_permissions = ('dcim.view_interface', ) @classmethod From 97b717f55292df2203d65a759adaad597d337765 Mon Sep 17 00:00:00 2001 From: Jason Novinger Date: Fri, 11 Sep 2026 03:17:00 -0500 Subject: [PATCH 9/9] Revert "Note the CI dulwich install in the Git backend test docstring" This reverts commit 5959f915fa46328dcf8fbb95bae676e862a29a83. That commit existed only to touch a .py file so CI's path filter would run the test suite against the dulwich install in the preceding commit. The docstring change itself is not worth keeping. --- netbox/core/tests/test_data_backends.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/netbox/core/tests/test_data_backends.py b/netbox/core/tests/test_data_backends.py index 0789c2981..9f96a0cb3 100644 --- a/netbox/core/tests/test_data_backends.py +++ b/netbox/core/tests/test_data_backends.py @@ -62,8 +62,7 @@ class GitBackendCredentialIntegrationTestCase(TestCase): Integration tests that verify GitBackend correctly applies credential logic. These tests require dulwich to be installed and verify the full integration - of the credential handling in GitBackend.fetch(). dulwich is the optional - 'git' extra, so CI installs it explicitly; see .github/workflows/ci.yml. + of the credential handling in GitBackend.fetch(). """ def _get_clone_kwargs(self, url, **params):