nautobot 2.2.1__py3-none-any.whl → 2.2.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- nautobot/apps/jobs.py +2 -0
- nautobot/core/api/utils.py +12 -9
- nautobot/core/apps/__init__.py +2 -2
- nautobot/core/celery/__init__.py +79 -68
- nautobot/core/celery/backends.py +9 -1
- nautobot/core/celery/control.py +4 -7
- nautobot/core/celery/schedulers.py +4 -2
- nautobot/core/celery/task.py +78 -5
- nautobot/core/graphql/schema.py +2 -1
- nautobot/core/jobs/__init__.py +2 -1
- nautobot/core/templates/generic/object_list.html +3 -3
- nautobot/core/templatetags/helpers.py +66 -9
- nautobot/core/testing/__init__.py +6 -1
- nautobot/core/testing/api.py +12 -13
- nautobot/core/testing/mixins.py +2 -2
- nautobot/core/testing/views.py +50 -51
- nautobot/core/tests/test_api.py +23 -2
- nautobot/core/tests/test_templatetags_helpers.py +32 -0
- nautobot/core/tests/test_views.py +21 -1
- nautobot/core/tests/test_views_utils.py +22 -1
- nautobot/core/utils/module_loading.py +89 -0
- nautobot/core/views/generic.py +4 -4
- nautobot/core/views/mixins.py +4 -3
- nautobot/core/views/utils.py +3 -2
- nautobot/core/wsgi.py +9 -2
- nautobot/dcim/choices.py +14 -0
- nautobot/dcim/forms.py +59 -4
- nautobot/dcim/models/device_components.py +9 -5
- nautobot/dcim/templates/dcim/device/lldp_neighbors.html +2 -2
- nautobot/dcim/templates/dcim/devicefamily_retrieve.html +1 -1
- nautobot/dcim/templates/dcim/location.html +32 -13
- nautobot/dcim/templates/dcim/location_migrate_data_to_contact.html +102 -0
- nautobot/dcim/tests/test_forms.py +49 -2
- nautobot/dcim/tests/test_views.py +137 -0
- nautobot/dcim/urls.py +5 -0
- nautobot/dcim/views.py +149 -1
- nautobot/extras/api/views.py +21 -10
- nautobot/extras/constants.py +3 -3
- nautobot/extras/context_managers.py +56 -0
- nautobot/extras/datasources/git.py +47 -58
- nautobot/extras/forms/forms.py +3 -1
- nautobot/extras/jobs.py +79 -146
- nautobot/extras/models/datasources.py +0 -2
- nautobot/extras/models/jobs.py +36 -18
- nautobot/extras/plugins/__init__.py +1 -20
- nautobot/extras/signals.py +88 -57
- nautobot/extras/test_jobs/__init__.py +8 -0
- nautobot/extras/test_jobs/dry_run.py +3 -2
- nautobot/extras/test_jobs/fail.py +43 -0
- nautobot/extras/test_jobs/ipaddress_vars.py +40 -1
- nautobot/extras/test_jobs/jobs_module/__init__.py +5 -0
- nautobot/extras/test_jobs/jobs_module/jobs_submodule/__init__.py +1 -0
- nautobot/extras/test_jobs/jobs_module/jobs_submodule/jobs.py +6 -0
- nautobot/extras/test_jobs/pass.py +40 -0
- nautobot/extras/test_jobs/relative_import.py +11 -0
- nautobot/extras/tests/test_api.py +3 -0
- nautobot/extras/tests/test_context_managers.py +98 -1
- nautobot/extras/tests/test_datasources.py +125 -118
- nautobot/extras/tests/test_job_variables.py +57 -15
- nautobot/extras/tests/test_jobs.py +135 -1
- nautobot/extras/tests/test_models.py +26 -19
- nautobot/extras/tests/test_plugins.py +1 -3
- nautobot/extras/tests/test_views.py +2 -4
- nautobot/extras/utils.py +37 -0
- nautobot/extras/views.py +47 -95
- nautobot/ipam/api/views.py +8 -1
- nautobot/ipam/graphql/types.py +11 -0
- nautobot/ipam/mixins.py +32 -0
- nautobot/ipam/models.py +2 -1
- nautobot/ipam/querysets.py +6 -1
- nautobot/ipam/tables.py +1 -1
- nautobot/ipam/tests/test_models.py +82 -0
- nautobot/project-static/docs/assets/extra.css +4 -0
- nautobot/project-static/docs/code-reference/nautobot/apps/api.html +1 -1
- nautobot/project-static/docs/code-reference/nautobot/apps/jobs.html +180 -211
- nautobot/project-static/docs/development/apps/api/platform-features/jobs.html +1 -1
- nautobot/project-static/docs/development/core/application-registry.html +126 -84
- nautobot/project-static/docs/development/core/model-checklist.html +49 -1
- nautobot/project-static/docs/development/core/model-features.html +1 -1
- nautobot/project-static/docs/development/jobs/index.html +334 -58
- nautobot/project-static/docs/development/jobs/migration/from-v1.html +1 -1
- nautobot/project-static/docs/objects.inv +0 -0
- nautobot/project-static/docs/release-notes/version-1.6.html +504 -201
- nautobot/project-static/docs/release-notes/version-2.2.html +392 -43
- nautobot/project-static/docs/search/search_index.json +1 -1
- nautobot/project-static/docs/sitemap.xml +254 -254
- nautobot/project-static/docs/sitemap.xml.gz +0 -0
- nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/upgrading-from-nautobot-v1.html +7 -4
- nautobot/project-static/docs/user-guide/core-data-model/ipam/vlan.html +111 -0
- nautobot/project-static/docs/user-guide/platform-functionality/jobs/index.html +15 -28
- nautobot/project-static/docs/user-guide/platform-functionality/jobs/models.html +4 -4
- nautobot/project-static/js/forms.js +18 -11
- {nautobot-2.2.1.dist-info → nautobot-2.2.3.dist-info}/METADATA +3 -3
- {nautobot-2.2.1.dist-info → nautobot-2.2.3.dist-info}/RECORD +98 -92
- nautobot/extras/test_jobs/job_variables.py +0 -93
- {nautobot-2.2.1.dist-info → nautobot-2.2.3.dist-info}/LICENSE.txt +0 -0
- {nautobot-2.2.1.dist-info → nautobot-2.2.3.dist-info}/NOTICE +0 -0
- {nautobot-2.2.1.dist-info → nautobot-2.2.3.dist-info}/WHEEL +0 -0
- {nautobot-2.2.1.dist-info → nautobot-2.2.3.dist-info}/entry_points.txt +0 -0
nautobot/core/testing/api.py
CHANGED
|
@@ -13,12 +13,11 @@ from rest_framework import status
|
|
|
13
13
|
from rest_framework.relations import ManyRelatedField
|
|
14
14
|
from rest_framework.test import APITransactionTestCase as _APITransactionTestCase
|
|
15
15
|
|
|
16
|
-
from nautobot.core import testing
|
|
17
16
|
from nautobot.core.api.utils import get_serializer_for_model
|
|
18
17
|
from nautobot.core.models import fields as core_fields
|
|
19
18
|
from nautobot.core.models.tree_queries import TreeModel
|
|
20
19
|
from nautobot.core.templatetags.helpers import bettertitle
|
|
21
|
-
from nautobot.core.testing import mixins, views
|
|
20
|
+
from nautobot.core.testing import mixins, utils, views
|
|
22
21
|
from nautobot.core.utils import lookup
|
|
23
22
|
from nautobot.core.utils.data import is_uuid
|
|
24
23
|
from nautobot.extras import choices as extras_choices, models as extras_models, registry
|
|
@@ -111,7 +110,7 @@ class APIViewTestCases:
|
|
|
111
110
|
self.model._meta.model_name,
|
|
112
111
|
) in settings.EXEMPT_EXCLUDE_MODELS:
|
|
113
112
|
# Models listed in EXEMPT_EXCLUDE_MODELS should not be accessible to anonymous users
|
|
114
|
-
with
|
|
113
|
+
with utils.disable_warnings("django.request"):
|
|
115
114
|
self.assertHttpStatus(self.client.get(url, **self.header), status.HTTP_403_FORBIDDEN)
|
|
116
115
|
else:
|
|
117
116
|
response = self.client.get(url, **self.header)
|
|
@@ -125,7 +124,7 @@ class APIViewTestCases:
|
|
|
125
124
|
url = self._get_detail_url(self._get_queryset().first())
|
|
126
125
|
|
|
127
126
|
# Try GET without permission
|
|
128
|
-
with
|
|
127
|
+
with utils.disable_warnings("django.request"):
|
|
129
128
|
self.assertHttpStatus(self.client.get(url, **self.header), status.HTTP_403_FORBIDDEN)
|
|
130
129
|
|
|
131
130
|
@override_settings(EXEMPT_VIEW_PERMISSIONS=[])
|
|
@@ -303,7 +302,7 @@ class APIViewTestCases:
|
|
|
303
302
|
self.model._meta.model_name,
|
|
304
303
|
) in settings.EXEMPT_EXCLUDE_MODELS:
|
|
305
304
|
# Models listed in EXEMPT_EXCLUDE_MODELS should not be accessible to anonymous users
|
|
306
|
-
with
|
|
305
|
+
with utils.disable_warnings("django.request"):
|
|
307
306
|
self.assertHttpStatus(self.client.get(url, **self.header), status.HTTP_403_FORBIDDEN)
|
|
308
307
|
else:
|
|
309
308
|
# TODO(Glenn): if we're passing **self.header, we are *by definition* **NOT** anonymous!!
|
|
@@ -389,7 +388,7 @@ class APIViewTestCases:
|
|
|
389
388
|
url = self._get_list_url()
|
|
390
389
|
|
|
391
390
|
# Try GET without permission
|
|
392
|
-
with
|
|
391
|
+
with utils.disable_warnings("django.request"):
|
|
393
392
|
self.assertHttpStatus(self.client.get(url, **self.header), status.HTTP_403_FORBIDDEN)
|
|
394
393
|
|
|
395
394
|
@override_settings(EXEMPT_VIEW_PERMISSIONS=[])
|
|
@@ -490,7 +489,7 @@ class APIViewTestCases:
|
|
|
490
489
|
GET a list of objects with an unknown filter parameter and strict filtering, expect a 400 response.
|
|
491
490
|
"""
|
|
492
491
|
self.add_permissions(f"{self.model._meta.app_label}.view_{self.model._meta.model_name}")
|
|
493
|
-
with
|
|
492
|
+
with utils.disable_warnings("django.request"):
|
|
494
493
|
response = self.client.get(f"{self._get_list_url()}?ice_cream_flavor=rocky-road", **self.header)
|
|
495
494
|
self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
|
|
496
495
|
self.assertIsInstance(response.data, dict)
|
|
@@ -606,7 +605,7 @@ class APIViewTestCases:
|
|
|
606
605
|
url = self._get_list_url()
|
|
607
606
|
|
|
608
607
|
# Try POST without permission
|
|
609
|
-
with
|
|
608
|
+
with utils.disable_warnings("django.request"):
|
|
610
609
|
response = self.client.post(url, self.create_data[0], format="json", **self.header)
|
|
611
610
|
self.assertHttpStatus(response, status.HTTP_403_FORBIDDEN)
|
|
612
611
|
|
|
@@ -679,7 +678,7 @@ class APIViewTestCases:
|
|
|
679
678
|
instance = self.get_deletable_object()
|
|
680
679
|
else:
|
|
681
680
|
# try to do it ourselves
|
|
682
|
-
instance =
|
|
681
|
+
instance = utils.get_deletable_objects(self.model, self._get_queryset()).first()
|
|
683
682
|
if instance is None:
|
|
684
683
|
self.fail("Couldn't find a single deletable object!")
|
|
685
684
|
|
|
@@ -776,7 +775,7 @@ class APIViewTestCases:
|
|
|
776
775
|
update_data = self.update_data or getattr(self, "create_data")[0]
|
|
777
776
|
|
|
778
777
|
# Try PATCH without permission
|
|
779
|
-
with
|
|
778
|
+
with utils.disable_warnings("django.request"):
|
|
780
779
|
response = self.client.patch(url, update_data, format="json", **self.header)
|
|
781
780
|
self.assertHttpStatus(response, status.HTTP_403_FORBIDDEN)
|
|
782
781
|
|
|
@@ -983,7 +982,7 @@ class APIViewTestCases:
|
|
|
983
982
|
For some models this may just be any random object, but when we have FKs with `on_delete=models.PROTECT`
|
|
984
983
|
(as is often the case) we need to find or create an instance that doesn't have such entanglements.
|
|
985
984
|
"""
|
|
986
|
-
instance =
|
|
985
|
+
instance = utils.get_deletable_objects(self.model, self._get_queryset()).first()
|
|
987
986
|
if instance is None:
|
|
988
987
|
self.fail("Couldn't find a single deletable object!")
|
|
989
988
|
return instance
|
|
@@ -995,7 +994,7 @@ class APIViewTestCases:
|
|
|
995
994
|
For some models this may just be any random objects, but when we have FKs with `on_delete=models.PROTECT`
|
|
996
995
|
(as is often the case) we need to find or create an instance that doesn't have such entanglements.
|
|
997
996
|
"""
|
|
998
|
-
instances =
|
|
997
|
+
instances = utils.get_deletable_objects(self.model, self._get_queryset()).values_list("pk", flat=True)[:3]
|
|
999
998
|
if len(instances) < 3:
|
|
1000
999
|
self.fail(f"Couldn't find 3 deletable objects, only found {len(instances)}!")
|
|
1001
1000
|
return instances
|
|
@@ -1007,7 +1006,7 @@ class APIViewTestCases:
|
|
|
1007
1006
|
url = self._get_detail_url(self.get_deletable_object())
|
|
1008
1007
|
|
|
1009
1008
|
# Try DELETE without permission
|
|
1010
|
-
with
|
|
1009
|
+
with utils.disable_warnings("django.request"):
|
|
1011
1010
|
response = self.client.delete(url, **self.header)
|
|
1012
1011
|
self.assertHttpStatus(response, status.HTTP_403_FORBIDDEN)
|
|
1013
1012
|
|
nautobot/core/testing/mixins.py
CHANGED
|
@@ -11,8 +11,8 @@ from django.forms.models import model_to_dict
|
|
|
11
11
|
from netaddr import IPNetwork
|
|
12
12
|
from rest_framework.test import APIClient, APIRequestFactory
|
|
13
13
|
|
|
14
|
-
from nautobot.core import testing
|
|
15
14
|
from nautobot.core.models import fields as core_fields
|
|
15
|
+
from nautobot.core.testing import utils
|
|
16
16
|
from nautobot.core.utils import permissions
|
|
17
17
|
from nautobot.extras import management, models as extras_models
|
|
18
18
|
from nautobot.users import models as users_models
|
|
@@ -169,7 +169,7 @@ class NautobotTestCaseMixin:
|
|
|
169
169
|
# REST API response; pass the response data through directly
|
|
170
170
|
err_message += f"\n{response.data}"
|
|
171
171
|
# Attempt to extract form validation errors from the response HTML
|
|
172
|
-
form_errors =
|
|
172
|
+
form_errors = utils.extract_form_failures(response.content.decode(response.charset))
|
|
173
173
|
err_message += "\n" + str(form_errors or response.content.decode(response.charset) or "No data")
|
|
174
174
|
if msg:
|
|
175
175
|
err_message = f"{msg}\n{err_message}"
|
nautobot/core/testing/views.py
CHANGED
|
@@ -15,11 +15,10 @@ from django.utils.http import urlencode
|
|
|
15
15
|
from django.utils.text import slugify
|
|
16
16
|
from tree_queries.models import TreeNode
|
|
17
17
|
|
|
18
|
-
from nautobot.core import testing
|
|
19
18
|
from nautobot.core.models.generics import PrimaryModel
|
|
20
19
|
from nautobot.core.models.tree_queries import TreeModel
|
|
21
20
|
from nautobot.core.templatetags import helpers
|
|
22
|
-
from nautobot.core.testing import mixins
|
|
21
|
+
from nautobot.core.testing import mixins, utils
|
|
23
22
|
from nautobot.core.utils import lookup
|
|
24
23
|
from nautobot.extras import choices as extras_choices, models as extras_models, querysets as extras_querysets
|
|
25
24
|
from nautobot.extras.forms import CustomFieldModelFormMixin, RelationshipModelFormMixin
|
|
@@ -152,7 +151,7 @@ class ViewTestCases:
|
|
|
152
151
|
|
|
153
152
|
# The "Change Log" tab should appear in the response since we have all exempt permissions
|
|
154
153
|
if issubclass(self.model, extras_models.ChangeLoggedModel):
|
|
155
|
-
response_body =
|
|
154
|
+
response_body = utils.extract_page_body(response.content.decode(response.charset))
|
|
156
155
|
self.assertIn("Change Log", response_body, msg=response_body)
|
|
157
156
|
|
|
158
157
|
@override_settings(EXEMPT_VIEW_PERMISSIONS=[])
|
|
@@ -160,7 +159,7 @@ class ViewTestCases:
|
|
|
160
159
|
instance = self._get_queryset().first()
|
|
161
160
|
|
|
162
161
|
# Try GET without permission
|
|
163
|
-
with
|
|
162
|
+
with utils.disable_warnings("django.request"):
|
|
164
163
|
response = self.client.get(instance.get_absolute_url())
|
|
165
164
|
self.assertHttpStatus(response, [403, 404])
|
|
166
165
|
response_body = response.content.decode(response.charset)
|
|
@@ -180,7 +179,7 @@ class ViewTestCases:
|
|
|
180
179
|
response = self.client.get(instance.get_absolute_url())
|
|
181
180
|
self.assertHttpStatus(response, 200)
|
|
182
181
|
|
|
183
|
-
response_body =
|
|
182
|
+
response_body = utils.extract_page_body(response.content.decode(response.charset))
|
|
184
183
|
|
|
185
184
|
# The object's display name or string representation should appear in the response
|
|
186
185
|
self.assertIn(escape(getattr(instance, "display", str(instance))), response_body, msg=response_body)
|
|
@@ -247,7 +246,7 @@ class ViewTestCases:
|
|
|
247
246
|
obj_perm.object_types.add(ContentType.objects.get_for_model(self.model))
|
|
248
247
|
|
|
249
248
|
response = self.client.get(instance.get_absolute_url())
|
|
250
|
-
response_body =
|
|
249
|
+
response_body = utils.extract_page_body(response.content.decode(response.charset))
|
|
251
250
|
advanced_tab_href = f"{instance.get_absolute_url()}#advanced"
|
|
252
251
|
|
|
253
252
|
self.assertIn(advanced_tab_href, response_body)
|
|
@@ -304,16 +303,16 @@ class ViewTestCases:
|
|
|
304
303
|
|
|
305
304
|
def test_create_object_without_permission(self):
|
|
306
305
|
# Try GET without permission
|
|
307
|
-
with
|
|
306
|
+
with utils.disable_warnings("django.request"):
|
|
308
307
|
self.assertHttpStatus(self.client.get(self._get_url("add")), 403)
|
|
309
308
|
|
|
310
309
|
# Try POST without permission
|
|
311
310
|
request = {
|
|
312
311
|
"path": self._get_url("add"),
|
|
313
|
-
"data":
|
|
312
|
+
"data": utils.post_data(self.form_data),
|
|
314
313
|
}
|
|
315
314
|
response = self.client.post(**request)
|
|
316
|
-
with
|
|
315
|
+
with utils.disable_warnings("django.request"):
|
|
317
316
|
self.assertHttpStatus(response, 403)
|
|
318
317
|
|
|
319
318
|
@override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
|
|
@@ -332,7 +331,7 @@ class ViewTestCases:
|
|
|
332
331
|
# Try POST with model-level permission
|
|
333
332
|
request = {
|
|
334
333
|
"path": self._get_url("add"),
|
|
335
|
-
"data":
|
|
334
|
+
"data": utils.post_data(self.form_data),
|
|
336
335
|
}
|
|
337
336
|
self.assertHttpStatus(self.client.post(**request), 302)
|
|
338
337
|
self.assertEqual(initial_count + 1, self._get_queryset().count())
|
|
@@ -362,7 +361,7 @@ class ViewTestCases:
|
|
|
362
361
|
detail_url = instance.get_absolute_url()
|
|
363
362
|
validate(detail_url)
|
|
364
363
|
response = self.client.get(detail_url)
|
|
365
|
-
response_body =
|
|
364
|
+
response_body = utils.extract_page_body(response.content.decode(response.charset))
|
|
366
365
|
advanced_tab_href = f"{detail_url}#advanced"
|
|
367
366
|
self.assertIn(advanced_tab_href, response_body)
|
|
368
367
|
self.assertIn("<td>Created By</td>", response_body)
|
|
@@ -391,7 +390,7 @@ class ViewTestCases:
|
|
|
391
390
|
# Try to create an object (not permitted)
|
|
392
391
|
request = {
|
|
393
392
|
"path": self._get_url("add"),
|
|
394
|
-
"data":
|
|
393
|
+
"data": utils.post_data(self.form_data),
|
|
395
394
|
}
|
|
396
395
|
self.assertHttpStatus(self.client.post(**request), 200)
|
|
397
396
|
self.assertEqual(initial_count, self._get_queryset().count()) # Check that no object was created
|
|
@@ -403,7 +402,7 @@ class ViewTestCases:
|
|
|
403
402
|
# Try to create an object (permitted)
|
|
404
403
|
request = {
|
|
405
404
|
"path": self._get_url("add"),
|
|
406
|
-
"data":
|
|
405
|
+
"data": utils.post_data(self.form_data),
|
|
407
406
|
}
|
|
408
407
|
self.assertHttpStatus(self.client.post(**request), 302)
|
|
409
408
|
self.assertEqual(initial_count + 1, self._get_queryset().count())
|
|
@@ -472,15 +471,15 @@ class ViewTestCases:
|
|
|
472
471
|
instance = self._get_queryset().first()
|
|
473
472
|
|
|
474
473
|
# Try GET without permission
|
|
475
|
-
with
|
|
474
|
+
with utils.disable_warnings("django.request"):
|
|
476
475
|
self.assertHttpStatus(self.client.get(self._get_url("edit", instance)), [403, 404])
|
|
477
476
|
|
|
478
477
|
# Try POST without permission
|
|
479
478
|
request = {
|
|
480
479
|
"path": self._get_url("edit", instance),
|
|
481
|
-
"data":
|
|
480
|
+
"data": utils.post_data(self.form_data),
|
|
482
481
|
}
|
|
483
|
-
with
|
|
482
|
+
with utils.disable_warnings("django.request"):
|
|
484
483
|
self.assertHttpStatus(self.client.post(**request), [403, 404])
|
|
485
484
|
|
|
486
485
|
@override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
|
|
@@ -499,7 +498,7 @@ class ViewTestCases:
|
|
|
499
498
|
# Try POST with model-level permission
|
|
500
499
|
request = {
|
|
501
500
|
"path": self._get_url("edit", instance),
|
|
502
|
-
"data":
|
|
501
|
+
"data": utils.post_data(self.form_data),
|
|
503
502
|
}
|
|
504
503
|
self.assertHttpStatus(self.client.post(**request), 302)
|
|
505
504
|
self.assertInstanceEqual(self._get_queryset().get(pk=instance.pk), self.form_data)
|
|
@@ -515,7 +514,7 @@ class ViewTestCases:
|
|
|
515
514
|
detail_url = instance.get_absolute_url()
|
|
516
515
|
validate(detail_url)
|
|
517
516
|
response = self.client.get(detail_url)
|
|
518
|
-
response_body =
|
|
517
|
+
response_body = utils.extract_page_body(response.content.decode(response.charset))
|
|
519
518
|
advanced_tab_href = f"{detail_url}#advanced"
|
|
520
519
|
self.assertIn(advanced_tab_href, response_body)
|
|
521
520
|
self.assertIn("<td>Last Updated By</td>", response_body)
|
|
@@ -547,7 +546,7 @@ class ViewTestCases:
|
|
|
547
546
|
# Try to edit a permitted object
|
|
548
547
|
request = {
|
|
549
548
|
"path": self._get_url("edit", instance1),
|
|
550
|
-
"data":
|
|
549
|
+
"data": utils.post_data(self.form_data),
|
|
551
550
|
}
|
|
552
551
|
self.assertHttpStatus(self.client.post(**request), 302)
|
|
553
552
|
self.assertInstanceEqual(self._get_queryset().get(pk=instance1.pk), self.form_data)
|
|
@@ -555,7 +554,7 @@ class ViewTestCases:
|
|
|
555
554
|
# Try to edit a non-permitted object
|
|
556
555
|
request = {
|
|
557
556
|
"path": self._get_url("edit", instance2),
|
|
558
|
-
"data":
|
|
557
|
+
"data": utils.post_data(self.form_data),
|
|
559
558
|
}
|
|
560
559
|
self.assertHttpStatus(self.client.post(**request), 404)
|
|
561
560
|
|
|
@@ -571,7 +570,7 @@ class ViewTestCases:
|
|
|
571
570
|
For some models this may just be any random object, but when we have FKs with `on_delete=models.PROTECT`
|
|
572
571
|
(as is often the case) we need to find or create an instance that doesn't have such entanglements.
|
|
573
572
|
"""
|
|
574
|
-
instance =
|
|
573
|
+
instance = utils.get_deletable_objects(self.model, self._get_queryset()).first()
|
|
575
574
|
if instance is None:
|
|
576
575
|
self.fail("Couldn't find a single deletable object!")
|
|
577
576
|
return instance
|
|
@@ -580,15 +579,15 @@ class ViewTestCases:
|
|
|
580
579
|
instance = self.get_deletable_object()
|
|
581
580
|
|
|
582
581
|
# Try GET without permission
|
|
583
|
-
with
|
|
582
|
+
with utils.disable_warnings("django.request"):
|
|
584
583
|
self.assertHttpStatus(self.client.get(self._get_url("delete", instance)), [403, 404])
|
|
585
584
|
|
|
586
585
|
# Try POST without permission
|
|
587
586
|
request = {
|
|
588
587
|
"path": self._get_url("delete", instance),
|
|
589
|
-
"data":
|
|
588
|
+
"data": utils.post_data({"confirm": True}),
|
|
590
589
|
}
|
|
591
|
-
with
|
|
590
|
+
with utils.disable_warnings("django.request"):
|
|
592
591
|
self.assertHttpStatus(self.client.post(**request), [403, 404])
|
|
593
592
|
|
|
594
593
|
@override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
|
|
@@ -624,7 +623,7 @@ class ViewTestCases:
|
|
|
624
623
|
# Try POST with model-level permission
|
|
625
624
|
request = {
|
|
626
625
|
"path": self._get_url("delete", instance),
|
|
627
|
-
"data":
|
|
626
|
+
"data": utils.post_data({"confirm": True}),
|
|
628
627
|
}
|
|
629
628
|
self.assertHttpStatus(self.client.post(**request), 302)
|
|
630
629
|
with self.assertRaises(ObjectDoesNotExist):
|
|
@@ -701,7 +700,7 @@ class ViewTestCases:
|
|
|
701
700
|
# Try to delete a permitted object
|
|
702
701
|
request = {
|
|
703
702
|
"path": self._get_url("delete", instance1),
|
|
704
|
-
"data":
|
|
703
|
+
"data": utils.post_data({"confirm": True}),
|
|
705
704
|
}
|
|
706
705
|
self.assertHttpStatus(self.client.post(**request), 302)
|
|
707
706
|
with self.assertRaises(ObjectDoesNotExist):
|
|
@@ -713,7 +712,7 @@ class ViewTestCases:
|
|
|
713
712
|
instance3 = self._get_queryset().first()
|
|
714
713
|
request = {
|
|
715
714
|
"path": self._get_url("delete", instance3),
|
|
716
|
-
"data":
|
|
715
|
+
"data": utils.post_data({"confirm": True}),
|
|
717
716
|
}
|
|
718
717
|
self.assertHttpStatus(self.client.post(**request), 404)
|
|
719
718
|
self.assertTrue(self._get_queryset().filter(pk=instance3.pk).exists())
|
|
@@ -781,7 +780,7 @@ class ViewTestCases:
|
|
|
781
780
|
instance1, instance2 = self._get_queryset().all()[:2]
|
|
782
781
|
response = self.client.get(f"{self._get_url('list')}?id={instance1.pk}")
|
|
783
782
|
self.assertHttpStatus(response, 200)
|
|
784
|
-
content =
|
|
783
|
+
content = utils.extract_page_body(response.content.decode(response.charset))
|
|
785
784
|
# TODO: it'd make test failures more readable if we strip the page headers/footers from the content
|
|
786
785
|
if hasattr(self.model, "name"):
|
|
787
786
|
self.assertRegex(content, r">\s*" + re.escape(escape(instance1.name)) + r"\s*<", msg=content)
|
|
@@ -794,7 +793,7 @@ class ViewTestCases:
|
|
|
794
793
|
"""Verify that with STRICT_FILTERING, an unknown filter results in an error message and no matches."""
|
|
795
794
|
response = self.client.get(f"{self._get_url('list')}?ice_cream_flavor=chocolate")
|
|
796
795
|
self.assertHttpStatus(response, 200)
|
|
797
|
-
content =
|
|
796
|
+
content = utils.extract_page_body(response.content.decode(response.charset))
|
|
798
797
|
# TODO: it'd make test failures more readable if we strip the page headers/footers from the content
|
|
799
798
|
self.assertIn("Unknown filter field", content, msg=content)
|
|
800
799
|
# There should be no table rows displayed except for the empty results row
|
|
@@ -820,7 +819,7 @@ class ViewTestCases:
|
|
|
820
819
|
],
|
|
821
820
|
)
|
|
822
821
|
self.assertHttpStatus(response, 200)
|
|
823
|
-
content =
|
|
822
|
+
content = utils.extract_page_body(response.content.decode(response.charset))
|
|
824
823
|
# TODO: it'd make test failures more readable if we strip the page headers/footers from the content
|
|
825
824
|
self.assertNotIn("Unknown filter field", content, msg=content)
|
|
826
825
|
self.assertIn("None", content, msg=content)
|
|
@@ -833,7 +832,7 @@ class ViewTestCases:
|
|
|
833
832
|
@override_settings(EXEMPT_VIEW_PERMISSIONS=[])
|
|
834
833
|
def test_list_objects_without_permission(self):
|
|
835
834
|
# Try GET without permission
|
|
836
|
-
with
|
|
835
|
+
with utils.disable_warnings("django.request"):
|
|
837
836
|
response = self.client.get(self._get_url("list"))
|
|
838
837
|
self.assertHttpStatus(response, 403)
|
|
839
838
|
response_body = response.content.decode(response.charset)
|
|
@@ -884,7 +883,7 @@ class ViewTestCases:
|
|
|
884
883
|
# Try GET with object-level permission
|
|
885
884
|
response = self.client.get(self._get_url("list"))
|
|
886
885
|
self.assertHttpStatus(response, 200)
|
|
887
|
-
content =
|
|
886
|
+
content = utils.extract_page_body(response.content.decode(response.charset))
|
|
888
887
|
# TODO: it'd make test failures more readable if we strip the page headers/footers from the content
|
|
889
888
|
if hasattr(self.model, "name"):
|
|
890
889
|
self.assertRegex(content, r">\s*" + re.escape(escape(instance1.name)) + r"\s*<", msg=content)
|
|
@@ -952,11 +951,11 @@ class ViewTestCases:
|
|
|
952
951
|
def test_create_multiple_objects_without_permission(self):
|
|
953
952
|
request = {
|
|
954
953
|
"path": self._get_url("add"),
|
|
955
|
-
"data":
|
|
954
|
+
"data": utils.post_data(self.bulk_create_data),
|
|
956
955
|
}
|
|
957
956
|
|
|
958
957
|
# Try POST without permission
|
|
959
|
-
with
|
|
958
|
+
with utils.disable_warnings("django.request"):
|
|
960
959
|
self.assertHttpStatus(self.client.post(**request), 403)
|
|
961
960
|
|
|
962
961
|
@override_settings(EXEMPT_VIEW_PERMISSIONS=[])
|
|
@@ -964,7 +963,7 @@ class ViewTestCases:
|
|
|
964
963
|
initial_count = self._get_queryset().count()
|
|
965
964
|
request = {
|
|
966
965
|
"path": self._get_url("add"),
|
|
967
|
-
"data":
|
|
966
|
+
"data": utils.post_data(self.bulk_create_data),
|
|
968
967
|
}
|
|
969
968
|
|
|
970
969
|
# Assign non-constrained permission
|
|
@@ -994,7 +993,7 @@ class ViewTestCases:
|
|
|
994
993
|
initial_count = self._get_queryset().count()
|
|
995
994
|
request = {
|
|
996
995
|
"path": self._get_url("add"),
|
|
997
|
-
"data":
|
|
996
|
+
"data": utils.post_data(self.bulk_create_data),
|
|
998
997
|
}
|
|
999
998
|
|
|
1000
999
|
# Assign constrained permission
|
|
@@ -1078,7 +1077,7 @@ class ViewTestCases:
|
|
|
1078
1077
|
}
|
|
1079
1078
|
|
|
1080
1079
|
# Try POST without permission
|
|
1081
|
-
with
|
|
1080
|
+
with utils.disable_warnings("django.request"):
|
|
1082
1081
|
self.assertHttpStatus(self.client.post(self._get_url("bulk_edit"), data), 403)
|
|
1083
1082
|
|
|
1084
1083
|
@override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
|
|
@@ -1090,7 +1089,7 @@ class ViewTestCases:
|
|
|
1090
1089
|
}
|
|
1091
1090
|
|
|
1092
1091
|
# Append the form data to the request
|
|
1093
|
-
data.update(
|
|
1092
|
+
data.update(utils.post_data(self.bulk_edit_data))
|
|
1094
1093
|
|
|
1095
1094
|
# Assign model-level permission
|
|
1096
1095
|
obj_perm = users_models.ObjectPermission(name="Test permission", actions=["change"])
|
|
@@ -1124,7 +1123,7 @@ class ViewTestCases:
|
|
|
1124
1123
|
# Expect a 200 status cause we are only rendering the bulk edit table.
|
|
1125
1124
|
# after pressing Edit Selected button.
|
|
1126
1125
|
self.assertHttpStatus(response, 200)
|
|
1127
|
-
response_body =
|
|
1126
|
+
response_body = utils.extract_page_body(response.content.decode(response.charset))
|
|
1128
1127
|
# Check if all the pks are passed into the BulkEditForm/BulkUpdateForm
|
|
1129
1128
|
for pk in pk_list:
|
|
1130
1129
|
self.assertIn(f'<input type="hidden" name="pk" value="{pk}"', response_body)
|
|
@@ -1143,7 +1142,7 @@ class ViewTestCases:
|
|
|
1143
1142
|
except StopIteration:
|
|
1144
1143
|
self.fail(f"Test requires at least three instances of {self.model._meta.model_name} to be defined.")
|
|
1145
1144
|
|
|
1146
|
-
post_data =
|
|
1145
|
+
post_data = utils.post_data(self.bulk_edit_data)
|
|
1147
1146
|
|
|
1148
1147
|
# Open bulk update form with first two objects
|
|
1149
1148
|
selected_data = {
|
|
@@ -1155,7 +1154,7 @@ class ViewTestCases:
|
|
|
1155
1154
|
response = self.client.post(f"{self._get_url('bulk_edit')}?{query_string}", selected_data)
|
|
1156
1155
|
# Expect a 200 status cause we are only rendering the bulk edit table after pressing Edit Selected button.
|
|
1157
1156
|
self.assertHttpStatus(response, 200)
|
|
1158
|
-
response_body =
|
|
1157
|
+
response_body = utils.extract_page_body(response.content.decode(response.charset))
|
|
1159
1158
|
# Check if the first and second pk is passed into the form.
|
|
1160
1159
|
self.assertIn(f'<input type="hidden" name="pk" value="{first_pk}"', response_body)
|
|
1161
1160
|
self.assertIn(f'<input type="hidden" name="pk" value="{second_pk}"', response_body)
|
|
@@ -1195,7 +1194,7 @@ class ViewTestCases:
|
|
|
1195
1194
|
"pk": pk_list,
|
|
1196
1195
|
"_apply": True, # Form button
|
|
1197
1196
|
}
|
|
1198
|
-
data.update(
|
|
1197
|
+
data.update(utils.post_data(self.bulk_edit_data))
|
|
1199
1198
|
|
|
1200
1199
|
# Attempt to bulk edit permitted objects into a non-permitted state
|
|
1201
1200
|
response = self.client.post(self._get_url("bulk_edit"), data)
|
|
@@ -1227,7 +1226,7 @@ class ViewTestCases:
|
|
|
1227
1226
|
For some models this may just be any random objects, but when we have FKs with `on_delete=models.PROTECT`
|
|
1228
1227
|
(as is often the case) we need to find or create an instance that doesn't have such entanglements.
|
|
1229
1228
|
"""
|
|
1230
|
-
return
|
|
1229
|
+
return utils.get_deletable_objects(self.model, self._get_queryset()).values_list("pk", flat=True)[:3]
|
|
1231
1230
|
|
|
1232
1231
|
@override_settings(EXEMPT_VIEW_PERMISSIONS=[])
|
|
1233
1232
|
def test_bulk_delete_objects_without_permission(self):
|
|
@@ -1239,7 +1238,7 @@ class ViewTestCases:
|
|
|
1239
1238
|
}
|
|
1240
1239
|
|
|
1241
1240
|
# Try POST without permission
|
|
1242
|
-
with
|
|
1241
|
+
with utils.disable_warnings("django.request"):
|
|
1243
1242
|
self.assertHttpStatus(self.client.post(self._get_url("bulk_delete"), data), 403)
|
|
1244
1243
|
|
|
1245
1244
|
@override_settings(EXEMPT_VIEW_PERMISSIONS=[])
|
|
@@ -1284,7 +1283,7 @@ class ViewTestCases:
|
|
|
1284
1283
|
# Try POST with the selected data first. Emulating selecting all -> pressing Delete Selected button.
|
|
1285
1284
|
response = self.client.post(self._get_url("bulk_delete"), selected_data)
|
|
1286
1285
|
self.assertHttpStatus(response, 200)
|
|
1287
|
-
response_body =
|
|
1286
|
+
response_body = utils.extract_page_body(response.content.decode(response.charset))
|
|
1288
1287
|
# Check if all the pks are passed into the BulkDeleteForm/BulkDestroyForm
|
|
1289
1288
|
for pk in pk_list:
|
|
1290
1289
|
self.assertIn(f'<input type="hidden" name="pk" value="{pk}"', response_body)
|
|
@@ -1312,7 +1311,7 @@ class ViewTestCases:
|
|
|
1312
1311
|
response = self.client.post(f"{self._get_url('bulk_delete')}?{query_string}", selected_data)
|
|
1313
1312
|
# Expect a 200 status cause we are only rendering the bulk delete table after pressing Delete Selected button.
|
|
1314
1313
|
self.assertHttpStatus(response, 200)
|
|
1315
|
-
response_body =
|
|
1314
|
+
response_body = utils.extract_page_body(response.content.decode(response.charset))
|
|
1316
1315
|
# Check if the first and second pk is passed into the form.
|
|
1317
1316
|
self.assertIn(f'<input type="hidden" name="pk" value="{first_pk}"', response_body)
|
|
1318
1317
|
self.assertIn(f'<input type="hidden" name="pk" value="{second_pk}"', response_body)
|
|
@@ -1372,11 +1371,11 @@ class ViewTestCases:
|
|
|
1372
1371
|
data.update(self.rename_data)
|
|
1373
1372
|
|
|
1374
1373
|
# Test GET without permission
|
|
1375
|
-
with
|
|
1374
|
+
with utils.disable_warnings("django.request"):
|
|
1376
1375
|
self.assertHttpStatus(self.client.get(self._get_url("bulk_rename")), 403)
|
|
1377
1376
|
|
|
1378
1377
|
# Try POST without permission
|
|
1379
|
-
with
|
|
1378
|
+
with utils.disable_warnings("django.request"):
|
|
1380
1379
|
self.assertHttpStatus(self.client.post(self._get_url("bulk_rename"), data), 403)
|
|
1381
1380
|
|
|
1382
1381
|
@override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
|
|
@@ -1517,13 +1516,13 @@ class ViewTestCases:
|
|
|
1517
1516
|
url = reverse(f"dcim:device_bulk_add_{self.model._meta.model_name}")
|
|
1518
1517
|
request = {
|
|
1519
1518
|
"path": url,
|
|
1520
|
-
"data":
|
|
1519
|
+
"data": utils.post_data({"pk": data["device"]}),
|
|
1521
1520
|
}
|
|
1522
1521
|
else:
|
|
1523
1522
|
url = reverse(f"virtualization:virtualmachine_bulk_add_{self.model._meta.model_name}")
|
|
1524
1523
|
request = {
|
|
1525
1524
|
"path": url,
|
|
1526
|
-
"data":
|
|
1525
|
+
"data": utils.post_data({"pk": data["virtual_machine"]}),
|
|
1527
1526
|
}
|
|
1528
1527
|
self.assertHttpStatus(self.client.post(**request), 200)
|
|
1529
1528
|
|
|
@@ -1533,7 +1532,7 @@ class ViewTestCases:
|
|
|
1533
1532
|
else:
|
|
1534
1533
|
data["pk"] = data.pop("virtual_machine")
|
|
1535
1534
|
data["_create"] = ""
|
|
1536
|
-
request["data"] =
|
|
1535
|
+
request["data"] = utils.post_data(data)
|
|
1537
1536
|
self.assertHttpStatus(self.client.post(**request), 302)
|
|
1538
1537
|
|
|
1539
1538
|
updated_count = self._get_queryset().count()
|
nautobot/core/tests/test_api.py
CHANGED
|
@@ -20,7 +20,7 @@ from nautobot.circuits.models import Provider
|
|
|
20
20
|
from nautobot.core import testing
|
|
21
21
|
from nautobot.core.api.parsers import NautobotCSVParser
|
|
22
22
|
from nautobot.core.api.renderers import NautobotCSVRenderer
|
|
23
|
-
from nautobot.core.api.utils import get_serializer_for_model
|
|
23
|
+
from nautobot.core.api.utils import get_serializer_for_model, get_view_name
|
|
24
24
|
from nautobot.core.api.versioning import NautobotAPIVersioning
|
|
25
25
|
from nautobot.core.constants import COMPOSITE_KEY_SEPARATOR
|
|
26
26
|
from nautobot.core.utils.lookup import get_route_for_model
|
|
@@ -28,7 +28,7 @@ from nautobot.dcim import models as dcim_models
|
|
|
28
28
|
from nautobot.dcim.api import serializers as dcim_serializers
|
|
29
29
|
from nautobot.extras import choices, models as extras_models
|
|
30
30
|
from nautobot.ipam import models as ipam_models
|
|
31
|
-
from nautobot.ipam.api import serializers as ipam_serializers
|
|
31
|
+
from nautobot.ipam.api import serializers as ipam_serializers, views as ipam_api_views
|
|
32
32
|
from nautobot.tenancy import models as tenancy_models
|
|
33
33
|
|
|
34
34
|
User = get_user_model()
|
|
@@ -997,3 +997,24 @@ class NewUIGetMenuAPIViewTestCase(testing.APITestCase):
|
|
|
997
997
|
|
|
998
998
|
self.assertEqual(response.status_code, 200)
|
|
999
999
|
self.assertEqual(response.data, expected_response)
|
|
1000
|
+
|
|
1001
|
+
|
|
1002
|
+
class NautobotGetViewNameTest(TestCase):
|
|
1003
|
+
"""
|
|
1004
|
+
Some unit tests for the get_view_name() functionality.
|
|
1005
|
+
"""
|
|
1006
|
+
|
|
1007
|
+
@override_settings(ALLOWED_HOSTS=["*"])
|
|
1008
|
+
def test_get(self):
|
|
1009
|
+
"""Assert that the proper view name is displayed for the correct view."""
|
|
1010
|
+
viewset = ipam_api_views.PrefixViewSet
|
|
1011
|
+
# We need to get a specific view, so we need to set the class kwargs
|
|
1012
|
+
view_kwargs = {
|
|
1013
|
+
"Prefixes": {"suffix": "List", "basename": "prefix", "detail": False},
|
|
1014
|
+
"Prefix": {"suffix": "Instance", "basename": "prefix", "detail": True},
|
|
1015
|
+
"Available IPs": {"name": "Available IPs"},
|
|
1016
|
+
"Available Prefixes": {"name": "Available Prefixes"},
|
|
1017
|
+
"Notes": {"name": "Notes"},
|
|
1018
|
+
}
|
|
1019
|
+
for view_name, view_kwarg in view_kwargs.items():
|
|
1020
|
+
self.assertEqual(view_name, get_view_name(viewset(**view_kwarg)))
|
|
@@ -283,3 +283,35 @@ class NautobotTemplatetagsHelperTest(TestCase):
|
|
|
283
283
|
helpers.support_message(),
|
|
284
284
|
"<p>Settings <strong>support</strong> message:</p><ul><li>Item 1</li><li>Item 2</li></ul>",
|
|
285
285
|
)
|
|
286
|
+
|
|
287
|
+
def test_hyperlinked_object_target_new_tab(self):
|
|
288
|
+
# None gives a placeholder
|
|
289
|
+
self.assertEqual(helpers.hyperlinked_object_target_new_tab(None), helpers.placeholder(None))
|
|
290
|
+
# An object without get_absolute_url gives a string
|
|
291
|
+
self.assertEqual(helpers.hyperlinked_object_target_new_tab("hello"), "hello")
|
|
292
|
+
# An object with get_absolute_url gives a hyperlink
|
|
293
|
+
location = models.Location.objects.first()
|
|
294
|
+
# Initially remove description if any
|
|
295
|
+
location.description = ""
|
|
296
|
+
location.save()
|
|
297
|
+
self.assertEqual(
|
|
298
|
+
helpers.hyperlinked_object_target_new_tab(location),
|
|
299
|
+
f'<a href="/dcim/locations/{location.pk}/" target="_blank" rel="noreferrer">{location.name}</a>',
|
|
300
|
+
)
|
|
301
|
+
# An object with get_absolute_url and a description gives a titled hyperlink
|
|
302
|
+
location.description = "An important location"
|
|
303
|
+
location.save()
|
|
304
|
+
self.assertEqual(
|
|
305
|
+
helpers.hyperlinked_object_target_new_tab(location),
|
|
306
|
+
f'<a href="/dcim/locations/{location.pk}/" title="An important location" target="_blank" rel="noreferrer">{location.name}</a>',
|
|
307
|
+
)
|
|
308
|
+
# Optionally you can request a field other than the object's display string
|
|
309
|
+
self.assertEqual(
|
|
310
|
+
helpers.hyperlinked_object_target_new_tab(location, "name"),
|
|
311
|
+
f'<a href="/dcim/locations/{location.pk}/" title="An important location" target="_blank" rel="noreferrer">{location.name}</a>',
|
|
312
|
+
)
|
|
313
|
+
# If you request a nonexistent field, it defaults to the string representation
|
|
314
|
+
self.assertEqual(
|
|
315
|
+
helpers.hyperlinked_object_target_new_tab(location, "foo"),
|
|
316
|
+
f'<a href="/dcim/locations/{location.pk}/" title="An important location" target="_blank" rel="noreferrer">{location!s}</a>',
|
|
317
|
+
)
|
|
@@ -90,7 +90,8 @@ class HomeViewTestCase(TestCase):
|
|
|
90
90
|
difference = [model for model in existing_models if model not in global_searchable_models]
|
|
91
91
|
if difference:
|
|
92
92
|
self.fail(
|
|
93
|
-
f'Existing model/models {",".join(difference)} are not included in the searchable_models attribute of the app config.\
|
|
93
|
+
f'Existing model/models {",".join(difference)} are not included in the searchable_models attribute of the app config.\n'
|
|
94
|
+
'If you do not want the models to be searchable, please include them in the GLOBAL_SEARCH_EXCLUDE_LIST constant in nautobot.core.constants.'
|
|
94
95
|
)
|
|
95
96
|
|
|
96
97
|
def make_request(self):
|
|
@@ -241,6 +242,25 @@ class FilterFormsTestCase(TestCase):
|
|
|
241
242
|
self.assertInHTML(locations[0].name, response_content)
|
|
242
243
|
self.assertInHTML(locations[1].name, response_content)
|
|
243
244
|
|
|
245
|
+
def test_filtering_crafted_query_params(self):
|
|
246
|
+
"""Test for reflected-XSS vulnerability GHSA-jxgr-gcj5-cqqg."""
|
|
247
|
+
self.add_permissions("dcim.view_location")
|
|
248
|
+
query_param = "?location_type=1 onmouseover=alert('hi') foo=bar"
|
|
249
|
+
url = reverse("dcim:location_list") + query_param
|
|
250
|
+
response = self.client.get(url)
|
|
251
|
+
self.assertHttpStatus(response, 200)
|
|
252
|
+
response_content = response.content.decode(response.charset)
|
|
253
|
+
# The important thing here is that the data-field-parent and data-field-value are correctly quoted
|
|
254
|
+
self.assertInHTML(
|
|
255
|
+
"""
|
|
256
|
+
<span class="filter-selection-choice-remove remove-filter-param"
|
|
257
|
+
data-field-type="child"
|
|
258
|
+
data-field-parent="location_type"
|
|
259
|
+
data-field-value="1 onmouseover=alert('hi') foo=bar"
|
|
260
|
+
>×</span>""", # noqa: RUF001 - ambiguous-unicode-character-string
|
|
261
|
+
response_content,
|
|
262
|
+
)
|
|
263
|
+
|
|
244
264
|
|
|
245
265
|
class ForceScriptNameTestcase(TestCase):
|
|
246
266
|
"""Basic test to assert that `settings.FORCE_SCRIPT_NAME` works as intended."""
|