views.py 41 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062
  1. from django.contrib.auth.models import User
  2. from django.contrib.contenttypes.models import ContentType
  3. from django.contrib.postgres.fields import ArrayField
  4. from django.core.exceptions import FieldDoesNotExist, ObjectDoesNotExist
  5. from django.db.models import ManyToManyField
  6. from django.forms.models import model_to_dict
  7. from django.test import Client, TestCase as _TestCase, override_settings
  8. from django.urls import reverse
  9. from django.utils.text import slugify
  10. from netaddr import IPNetwork
  11. from taggit.managers import TaggableManager
  12. from extras.models import Tag
  13. from users.models import ObjectPermission
  14. from utilities.permissions import resolve_permission_ct
  15. from .utils import disable_warnings, extract_form_failures, post_data
  16. __all__ = (
  17. 'TestCase',
  18. 'ModelTestCase',
  19. 'ModelViewTestCase',
  20. 'ViewTestCases',
  21. )
  22. class TestCase(_TestCase):
  23. user_permissions = ()
  24. def setUp(self):
  25. # Create the test user and assign permissions
  26. self.user = User.objects.create_user(username='testuser')
  27. self.add_permissions(*self.user_permissions)
  28. # Initialize the test client
  29. self.client = Client()
  30. self.client.force_login(self.user)
  31. def prepare_instance(self, instance):
  32. """
  33. Test cases can override this method to perform any necessary manipulation of an instance prior to its evaluation
  34. against test data. For example, it can be used to decrypt a Secret's plaintext attribute.
  35. """
  36. return instance
  37. def model_to_dict(self, instance, fields, api=False):
  38. """
  39. Return a dictionary representation of an instance.
  40. """
  41. # Prepare the instance and call Django's model_to_dict() to extract all fields
  42. model_dict = model_to_dict(self.prepare_instance(instance), fields=fields)
  43. # Map any additional (non-field) instance attributes that were specified
  44. for attr in fields:
  45. if hasattr(instance, attr) and attr not in model_dict:
  46. model_dict[attr] = getattr(instance, attr)
  47. for key, value in list(model_dict.items()):
  48. try:
  49. field = instance._meta.get_field(key)
  50. except FieldDoesNotExist:
  51. # Attribute is not a model field
  52. continue
  53. # Handle ManyToManyFields
  54. if value and type(field) in (ManyToManyField, TaggableManager):
  55. if field.related_model is ContentType:
  56. model_dict[key] = sorted([f'{ct.app_label}.{ct.model}' for ct in value])
  57. else:
  58. model_dict[key] = sorted([obj.pk for obj in value])
  59. if api:
  60. # Replace ContentType numeric IDs with <app_label>.<model>
  61. if type(getattr(instance, key)) is ContentType:
  62. ct = ContentType.objects.get(pk=value)
  63. model_dict[key] = f'{ct.app_label}.{ct.model}'
  64. # Convert IPNetwork instances to strings
  65. elif type(value) is IPNetwork:
  66. model_dict[key] = str(value)
  67. else:
  68. # Convert ArrayFields to CSV strings
  69. if type(instance._meta.get_field(key)) is ArrayField:
  70. model_dict[key] = ','.join([str(v) for v in value])
  71. return model_dict
  72. #
  73. # Permissions management
  74. #
  75. def add_permissions(self, *names):
  76. """
  77. Assign a set of permissions to the test user. Accepts permission names in the form <app>.<action>_<model>.
  78. """
  79. for name in names:
  80. ct, action = resolve_permission_ct(name)
  81. obj_perm = ObjectPermission(name=name, actions=[action])
  82. obj_perm.save()
  83. obj_perm.users.add(self.user)
  84. obj_perm.object_types.add(ct)
  85. #
  86. # Custom assertions
  87. #
  88. def assertHttpStatus(self, response, expected_status):
  89. """
  90. TestCase method. Provide more detail in the event of an unexpected HTTP response.
  91. """
  92. err_message = None
  93. # Construct an error message only if we know the test is going to fail
  94. if response.status_code != expected_status:
  95. if hasattr(response, 'data'):
  96. # REST API response; pass the response data through directly
  97. err = response.data
  98. else:
  99. # Attempt to extract form validation errors from the response HTML
  100. form_errors = extract_form_failures(response.content)
  101. err = form_errors or response.content or 'No data'
  102. err_message = f"Expected HTTP status {expected_status}; received {response.status_code}: {err}"
  103. self.assertEqual(response.status_code, expected_status, err_message)
  104. def assertInstanceEqual(self, instance, data, exclude=None, api=False):
  105. """
  106. Compare a model instance to a dictionary, checking that its attribute values match those specified
  107. in the dictionary.
  108. :param instance: Python object instance
  109. :param data: Dictionary of test data used to define the instance
  110. :param exclude: List of fields to exclude from comparison (e.g. passwords, which get hashed)
  111. :param api: Set to True is the data is a JSON representation of the instance
  112. """
  113. if exclude is None:
  114. exclude = []
  115. fields = [k for k in data.keys() if k not in exclude]
  116. model_dict = self.model_to_dict(instance, fields=fields, api=api)
  117. # Omit any dictionary keys which are not instance attributes or have been excluded
  118. relevant_data = {
  119. k: v for k, v in data.items() if hasattr(instance, k) and k not in exclude
  120. }
  121. self.assertDictEqual(model_dict, relevant_data)
  122. #
  123. # Convenience methods
  124. #
  125. @classmethod
  126. def create_tags(cls, *names):
  127. """
  128. Create and return a Tag instance for each name given.
  129. """
  130. tags = [Tag(name=name, slug=slugify(name)) for name in names]
  131. Tag.objects.bulk_create(tags)
  132. return tags
  133. class ModelTestCase(TestCase):
  134. """
  135. Parent class for TestCases which deal with models.
  136. """
  137. model = None
  138. def _get_queryset(self):
  139. """
  140. Return a base queryset suitable for use in test methods.
  141. """
  142. return self.model.objects.all()
  143. #
  144. # UI Tests
  145. #
  146. class ModelViewTestCase(ModelTestCase):
  147. """
  148. Base TestCase for model views. Subclass to test individual views.
  149. """
  150. def _get_base_url(self):
  151. """
  152. Return the base format for a URL for the test's model. Override this to test for a model which belongs
  153. to a different app (e.g. testing Interfaces within the virtualization app).
  154. """
  155. return '{}:{}_{{}}'.format(
  156. self.model._meta.app_label,
  157. self.model._meta.model_name
  158. )
  159. def _get_url(self, action, instance=None):
  160. """
  161. Return the URL name for a specific action and optionally a specific instance
  162. """
  163. url_format = self._get_base_url()
  164. # If no instance was provided, assume we don't need a unique identifier
  165. if instance is None:
  166. return reverse(url_format.format(action))
  167. return reverse(url_format.format(action), kwargs={'pk': instance.pk})
  168. class ViewTestCases:
  169. """
  170. We keep any TestCases with test_* methods inside a class to prevent unittest from trying to run them.
  171. """
  172. class GetObjectViewTestCase(ModelViewTestCase):
  173. """
  174. Retrieve a single instance.
  175. """
  176. @override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
  177. def test_get_object_anonymous(self):
  178. # Make the request as an unauthenticated user
  179. self.client.logout()
  180. response = self.client.get(self._get_queryset().first().get_absolute_url())
  181. self.assertHttpStatus(response, 200)
  182. @override_settings(EXEMPT_VIEW_PERMISSIONS=[])
  183. def test_get_object_without_permission(self):
  184. instance = self._get_queryset().first()
  185. # Try GET without permission
  186. with disable_warnings('django.request'):
  187. self.assertHttpStatus(self.client.get(instance.get_absolute_url()), 403)
  188. @override_settings(EXEMPT_VIEW_PERMISSIONS=[])
  189. def test_get_object_with_permission(self):
  190. instance = self._get_queryset().first()
  191. # Add model-level permission
  192. obj_perm = ObjectPermission(
  193. name='Test permission',
  194. actions=['view']
  195. )
  196. obj_perm.save()
  197. obj_perm.users.add(self.user)
  198. obj_perm.object_types.add(ContentType.objects.get_for_model(self.model))
  199. # Try GET with model-level permission
  200. self.assertHttpStatus(self.client.get(instance.get_absolute_url()), 200)
  201. @override_settings(EXEMPT_VIEW_PERMISSIONS=[])
  202. def test_get_object_with_constrained_permission(self):
  203. instance1, instance2 = self._get_queryset().all()[:2]
  204. # Add object-level permission
  205. obj_perm = ObjectPermission(
  206. name='Test permission',
  207. constraints={'pk': instance1.pk},
  208. actions=['view']
  209. )
  210. obj_perm.save()
  211. obj_perm.users.add(self.user)
  212. obj_perm.object_types.add(ContentType.objects.get_for_model(self.model))
  213. # Try GET to permitted object
  214. self.assertHttpStatus(self.client.get(instance1.get_absolute_url()), 200)
  215. # Try GET to non-permitted object
  216. self.assertHttpStatus(self.client.get(instance2.get_absolute_url()), 404)
  217. class GetObjectChangelogViewTestCase(ModelViewTestCase):
  218. """
  219. View the changelog for an instance.
  220. """
  221. @override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
  222. def test_get_object_changelog(self):
  223. url = self._get_url('changelog', self._get_queryset().first())
  224. response = self.client.get(url)
  225. self.assertHttpStatus(response, 200)
  226. class CreateObjectViewTestCase(ModelViewTestCase):
  227. """
  228. Create a single new instance.
  229. :form_data: Data to be used when creating a new object.
  230. """
  231. form_data = {}
  232. def test_create_object_without_permission(self):
  233. # Try GET without permission
  234. with disable_warnings('django.request'):
  235. self.assertHttpStatus(self.client.get(self._get_url('add')), 403)
  236. # Try POST without permission
  237. request = {
  238. 'path': self._get_url('add'),
  239. 'data': post_data(self.form_data),
  240. }
  241. response = self.client.post(**request)
  242. with disable_warnings('django.request'):
  243. self.assertHttpStatus(response, 403)
  244. @override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
  245. def test_create_object_with_permission(self):
  246. initial_count = self._get_queryset().count()
  247. # Assign unconstrained permission
  248. obj_perm = ObjectPermission(
  249. name='Test permission',
  250. actions=['add']
  251. )
  252. obj_perm.save()
  253. obj_perm.users.add(self.user)
  254. obj_perm.object_types.add(ContentType.objects.get_for_model(self.model))
  255. # Try GET with model-level permission
  256. self.assertHttpStatus(self.client.get(self._get_url('add')), 200)
  257. # Try POST with model-level permission
  258. request = {
  259. 'path': self._get_url('add'),
  260. 'data': post_data(self.form_data),
  261. }
  262. self.assertHttpStatus(self.client.post(**request), 302)
  263. self.assertEqual(initial_count + 1, self._get_queryset().count())
  264. self.assertInstanceEqual(self._get_queryset().order_by('pk').last(), self.form_data)
  265. @override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
  266. def test_create_object_with_constrained_permission(self):
  267. initial_count = self._get_queryset().count()
  268. # Assign constrained permission
  269. obj_perm = ObjectPermission(
  270. name='Test permission',
  271. constraints={'pk': 0}, # Dummy permission to deny all
  272. actions=['add']
  273. )
  274. obj_perm.save()
  275. obj_perm.users.add(self.user)
  276. obj_perm.object_types.add(ContentType.objects.get_for_model(self.model))
  277. # Try GET with object-level permission
  278. self.assertHttpStatus(self.client.get(self._get_url('add')), 200)
  279. # Try to create an object (not permitted)
  280. request = {
  281. 'path': self._get_url('add'),
  282. 'data': post_data(self.form_data),
  283. }
  284. self.assertHttpStatus(self.client.post(**request), 200)
  285. self.assertEqual(initial_count, self._get_queryset().count()) # Check that no object was created
  286. # Update the ObjectPermission to allow creation
  287. obj_perm.constraints = {'pk__gt': 0}
  288. obj_perm.save()
  289. # Try to create an object (permitted)
  290. request = {
  291. 'path': self._get_url('add'),
  292. 'data': post_data(self.form_data),
  293. }
  294. self.assertHttpStatus(self.client.post(**request), 302)
  295. self.assertEqual(initial_count + 1, self._get_queryset().count())
  296. self.assertInstanceEqual(self._get_queryset().order_by('pk').last(), self.form_data)
  297. class EditObjectViewTestCase(ModelViewTestCase):
  298. """
  299. Edit a single existing instance.
  300. :form_data: Data to be used when updating the first existing object.
  301. """
  302. form_data = {}
  303. def test_edit_object_without_permission(self):
  304. instance = self._get_queryset().first()
  305. # Try GET without permission
  306. with disable_warnings('django.request'):
  307. self.assertHttpStatus(self.client.get(self._get_url('edit', instance)), 403)
  308. # Try POST without permission
  309. request = {
  310. 'path': self._get_url('edit', instance),
  311. 'data': post_data(self.form_data),
  312. }
  313. with disable_warnings('django.request'):
  314. self.assertHttpStatus(self.client.post(**request), 403)
  315. @override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
  316. def test_edit_object_with_permission(self):
  317. instance = self._get_queryset().first()
  318. # Assign model-level permission
  319. obj_perm = ObjectPermission(
  320. name='Test permission',
  321. actions=['change']
  322. )
  323. obj_perm.save()
  324. obj_perm.users.add(self.user)
  325. obj_perm.object_types.add(ContentType.objects.get_for_model(self.model))
  326. # Try GET with model-level permission
  327. self.assertHttpStatus(self.client.get(self._get_url('edit', instance)), 200)
  328. # Try POST with model-level permission
  329. request = {
  330. 'path': self._get_url('edit', instance),
  331. 'data': post_data(self.form_data),
  332. }
  333. self.assertHttpStatus(self.client.post(**request), 302)
  334. self.assertInstanceEqual(self._get_queryset().get(pk=instance.pk), self.form_data)
  335. @override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
  336. def test_edit_object_with_constrained_permission(self):
  337. instance1, instance2 = self._get_queryset().all()[:2]
  338. # Assign constrained permission
  339. obj_perm = ObjectPermission(
  340. name='Test permission',
  341. constraints={'pk': instance1.pk},
  342. actions=['change']
  343. )
  344. obj_perm.save()
  345. obj_perm.users.add(self.user)
  346. obj_perm.object_types.add(ContentType.objects.get_for_model(self.model))
  347. # Try GET with a permitted object
  348. self.assertHttpStatus(self.client.get(self._get_url('edit', instance1)), 200)
  349. # Try GET with a non-permitted object
  350. self.assertHttpStatus(self.client.get(self._get_url('edit', instance2)), 404)
  351. # Try to edit a permitted object
  352. request = {
  353. 'path': self._get_url('edit', instance1),
  354. 'data': post_data(self.form_data),
  355. }
  356. self.assertHttpStatus(self.client.post(**request), 302)
  357. self.assertInstanceEqual(self._get_queryset().get(pk=instance1.pk), self.form_data)
  358. # Try to edit a non-permitted object
  359. request = {
  360. 'path': self._get_url('edit', instance2),
  361. 'data': post_data(self.form_data),
  362. }
  363. self.assertHttpStatus(self.client.post(**request), 404)
  364. class DeleteObjectViewTestCase(ModelViewTestCase):
  365. """
  366. Delete a single instance.
  367. """
  368. def test_delete_object_without_permission(self):
  369. instance = self._get_queryset().first()
  370. # Try GET without permission
  371. with disable_warnings('django.request'):
  372. self.assertHttpStatus(self.client.get(self._get_url('delete', instance)), 403)
  373. # Try POST without permission
  374. request = {
  375. 'path': self._get_url('delete', instance),
  376. 'data': post_data({'confirm': True}),
  377. }
  378. with disable_warnings('django.request'):
  379. self.assertHttpStatus(self.client.post(**request), 403)
  380. @override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
  381. def test_delete_object_with_permission(self):
  382. instance = self._get_queryset().first()
  383. # Assign model-level permission
  384. obj_perm = ObjectPermission(
  385. name='Test permission',
  386. actions=['delete']
  387. )
  388. obj_perm.save()
  389. obj_perm.users.add(self.user)
  390. obj_perm.object_types.add(ContentType.objects.get_for_model(self.model))
  391. # Try GET with model-level permission
  392. self.assertHttpStatus(self.client.get(self._get_url('delete', instance)), 200)
  393. # Try POST with model-level permission
  394. request = {
  395. 'path': self._get_url('delete', instance),
  396. 'data': post_data({'confirm': True}),
  397. }
  398. self.assertHttpStatus(self.client.post(**request), 302)
  399. with self.assertRaises(ObjectDoesNotExist):
  400. self._get_queryset().get(pk=instance.pk)
  401. @override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
  402. def test_delete_object_with_constrained_permission(self):
  403. instance1, instance2 = self._get_queryset().all()[:2]
  404. # Assign object-level permission
  405. obj_perm = ObjectPermission(
  406. name='Test permission',
  407. constraints={'pk': instance1.pk},
  408. actions=['delete']
  409. )
  410. obj_perm.save()
  411. obj_perm.users.add(self.user)
  412. obj_perm.object_types.add(ContentType.objects.get_for_model(self.model))
  413. # Try GET with a permitted object
  414. self.assertHttpStatus(self.client.get(self._get_url('delete', instance1)), 200)
  415. # Try GET with a non-permitted object
  416. self.assertHttpStatus(self.client.get(self._get_url('delete', instance2)), 404)
  417. # Try to delete a permitted object
  418. request = {
  419. 'path': self._get_url('delete', instance1),
  420. 'data': post_data({'confirm': True}),
  421. }
  422. self.assertHttpStatus(self.client.post(**request), 302)
  423. with self.assertRaises(ObjectDoesNotExist):
  424. self._get_queryset().get(pk=instance1.pk)
  425. # Try to delete a non-permitted object
  426. request = {
  427. 'path': self._get_url('delete', instance2),
  428. 'data': post_data({'confirm': True}),
  429. }
  430. self.assertHttpStatus(self.client.post(**request), 404)
  431. self.assertTrue(self._get_queryset().filter(pk=instance2.pk).exists())
  432. class ListObjectsViewTestCase(ModelViewTestCase):
  433. """
  434. Retrieve multiple instances.
  435. """
  436. @override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
  437. def test_list_objects_anonymous(self):
  438. # Make the request as an unauthenticated user
  439. self.client.logout()
  440. response = self.client.get(self._get_url('list'))
  441. self.assertHttpStatus(response, 200)
  442. @override_settings(EXEMPT_VIEW_PERMISSIONS=[])
  443. def test_list_objects_without_permission(self):
  444. # Try GET without permission
  445. with disable_warnings('django.request'):
  446. self.assertHttpStatus(self.client.get(self._get_url('list')), 403)
  447. @override_settings(EXEMPT_VIEW_PERMISSIONS=[])
  448. def test_list_objects_with_permission(self):
  449. # Add model-level permission
  450. obj_perm = ObjectPermission(
  451. name='Test permission',
  452. actions=['view']
  453. )
  454. obj_perm.save()
  455. obj_perm.users.add(self.user)
  456. obj_perm.object_types.add(ContentType.objects.get_for_model(self.model))
  457. # Try GET with model-level permission
  458. self.assertHttpStatus(self.client.get(self._get_url('list')), 200)
  459. @override_settings(EXEMPT_VIEW_PERMISSIONS=[])
  460. def test_list_objects_with_constrained_permission(self):
  461. instance1, instance2 = self._get_queryset().all()[:2]
  462. # Add object-level permission
  463. obj_perm = ObjectPermission(
  464. name='Test permission',
  465. constraints={'pk': instance1.pk},
  466. actions=['view']
  467. )
  468. obj_perm.save()
  469. obj_perm.users.add(self.user)
  470. obj_perm.object_types.add(ContentType.objects.get_for_model(self.model))
  471. # Try GET with object-level permission
  472. response = self.client.get(self._get_url('list'))
  473. self.assertHttpStatus(response, 200)
  474. content = str(response.content)
  475. if hasattr(self.model, 'name'):
  476. self.assertIn(instance1.name, content)
  477. self.assertNotIn(instance2.name, content)
  478. else:
  479. self.assertIn(instance1.get_absolute_url(), content)
  480. self.assertNotIn(instance2.get_absolute_url(), content)
  481. @override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
  482. def test_export_objects(self):
  483. url = self._get_url('list')
  484. # Test default CSV export
  485. response = self.client.get(f'{url}?export')
  486. self.assertHttpStatus(response, 200)
  487. if hasattr(self.model, 'csv_headers'):
  488. self.assertEqual(response.get('Content-Type'), 'text/csv')
  489. content = response.content.decode('utf-8')
  490. self.assertEqual(content.splitlines()[0], ','.join(self.model.csv_headers))
  491. # Test table-based export
  492. response = self.client.get(f'{url}?export=table')
  493. self.assertHttpStatus(response, 200)
  494. self.assertEqual(response.get('Content-Type'), 'text/csv; charset=utf-8')
  495. class CreateMultipleObjectsViewTestCase(ModelViewTestCase):
  496. """
  497. Create multiple instances using a single form. Expects the creation of three new instances by default.
  498. :bulk_create_count: The number of objects expected to be created (default: 3).
  499. :bulk_create_data: A dictionary of data to be used for bulk object creation.
  500. """
  501. bulk_create_count = 3
  502. bulk_create_data = {}
  503. @override_settings(EXEMPT_VIEW_PERMISSIONS=[])
  504. def test_create_multiple_objects_without_permission(self):
  505. request = {
  506. 'path': self._get_url('add'),
  507. 'data': post_data(self.bulk_create_data),
  508. }
  509. # Try POST without permission
  510. with disable_warnings('django.request'):
  511. self.assertHttpStatus(self.client.post(**request), 403)
  512. @override_settings(EXEMPT_VIEW_PERMISSIONS=[])
  513. def test_create_multiple_objects_with_permission(self):
  514. initial_count = self._get_queryset().count()
  515. request = {
  516. 'path': self._get_url('add'),
  517. 'data': post_data(self.bulk_create_data),
  518. }
  519. # Assign non-constrained permission
  520. obj_perm = ObjectPermission(
  521. name='Test permission',
  522. actions=['add'],
  523. )
  524. obj_perm.save()
  525. obj_perm.users.add(self.user)
  526. obj_perm.object_types.add(ContentType.objects.get_for_model(self.model))
  527. # Bulk create objects
  528. response = self.client.post(**request)
  529. self.assertHttpStatus(response, 302)
  530. self.assertEqual(initial_count + self.bulk_create_count, self._get_queryset().count())
  531. for instance in self._get_queryset().order_by('-pk')[:self.bulk_create_count]:
  532. self.assertInstanceEqual(instance, self.bulk_create_data)
  533. @override_settings(EXEMPT_VIEW_PERMISSIONS=[])
  534. def test_create_multiple_objects_with_constrained_permission(self):
  535. initial_count = self._get_queryset().count()
  536. request = {
  537. 'path': self._get_url('add'),
  538. 'data': post_data(self.bulk_create_data),
  539. }
  540. # Assign constrained permission
  541. obj_perm = ObjectPermission(
  542. name='Test permission',
  543. actions=['add'],
  544. constraints={'pk': 0} # Dummy constraint to deny all
  545. )
  546. obj_perm.save()
  547. obj_perm.users.add(self.user)
  548. obj_perm.object_types.add(ContentType.objects.get_for_model(self.model))
  549. # Attempt to make the request with unmet constraints
  550. self.assertHttpStatus(self.client.post(**request), 200)
  551. self.assertEqual(self._get_queryset().count(), initial_count)
  552. # Update the ObjectPermission to allow creation
  553. obj_perm.constraints = {'pk__gt': 0} # Dummy constraint to allow all
  554. obj_perm.save()
  555. response = self.client.post(**request)
  556. self.assertHttpStatus(response, 302)
  557. self.assertEqual(initial_count + self.bulk_create_count, self._get_queryset().count())
  558. for instance in self._get_queryset().order_by('-pk')[:self.bulk_create_count]:
  559. self.assertInstanceEqual(instance, self.bulk_create_data)
  560. class BulkImportObjectsViewTestCase(ModelViewTestCase):
  561. """
  562. Create multiple instances from imported data.
  563. :csv_data: A list of CSV-formatted lines (starting with the headers) to be used for bulk object import.
  564. """
  565. csv_data = ()
  566. def _get_csv_data(self):
  567. return '\n'.join(self.csv_data)
  568. def test_bulk_import_objects_without_permission(self):
  569. data = {
  570. 'csv': self._get_csv_data(),
  571. }
  572. # Test GET without permission
  573. with disable_warnings('django.request'):
  574. self.assertHttpStatus(self.client.get(self._get_url('import')), 403)
  575. # Try POST without permission
  576. response = self.client.post(self._get_url('import'), data)
  577. with disable_warnings('django.request'):
  578. self.assertHttpStatus(response, 403)
  579. @override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
  580. def test_bulk_import_objects_with_permission(self):
  581. initial_count = self._get_queryset().count()
  582. data = {
  583. 'csv': self._get_csv_data(),
  584. }
  585. # Assign model-level permission
  586. obj_perm = ObjectPermission(
  587. name='Test permission',
  588. actions=['add']
  589. )
  590. obj_perm.save()
  591. obj_perm.users.add(self.user)
  592. obj_perm.object_types.add(ContentType.objects.get_for_model(self.model))
  593. # Try GET with model-level permission
  594. self.assertHttpStatus(self.client.get(self._get_url('import')), 200)
  595. # Test POST with permission
  596. self.assertHttpStatus(self.client.post(self._get_url('import'), data), 200)
  597. self.assertEqual(self._get_queryset().count(), initial_count + len(self.csv_data) - 1)
  598. @override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
  599. def test_bulk_import_objects_with_constrained_permission(self):
  600. initial_count = self._get_queryset().count()
  601. data = {
  602. 'csv': self._get_csv_data(),
  603. }
  604. # Assign constrained permission
  605. obj_perm = ObjectPermission(
  606. name='Test permission',
  607. constraints={'pk': 0}, # Dummy permission to deny all
  608. actions=['add']
  609. )
  610. obj_perm.save()
  611. obj_perm.users.add(self.user)
  612. obj_perm.object_types.add(ContentType.objects.get_for_model(self.model))
  613. # Attempt to import non-permitted objects
  614. self.assertHttpStatus(self.client.post(self._get_url('import'), data), 200)
  615. self.assertEqual(self._get_queryset().count(), initial_count)
  616. # Update permission constraints
  617. obj_perm.constraints = {'pk__gt': 0} # Dummy permission to allow all
  618. obj_perm.save()
  619. # Import permitted objects
  620. self.assertHttpStatus(self.client.post(self._get_url('import'), data), 200)
  621. self.assertEqual(self._get_queryset().count(), initial_count + len(self.csv_data) - 1)
  622. class BulkEditObjectsViewTestCase(ModelViewTestCase):
  623. """
  624. Edit multiple instances.
  625. :bulk_edit_data: A dictionary of data to be used when bulk editing a set of objects. This data should differ
  626. from that used for initial object creation within setUpTestData().
  627. """
  628. bulk_edit_data = {}
  629. def test_bulk_edit_objects_without_permission(self):
  630. pk_list = self._get_queryset().values_list('pk', flat=True)[:3]
  631. data = {
  632. 'pk': pk_list,
  633. '_apply': True, # Form button
  634. }
  635. # Test GET without permission
  636. with disable_warnings('django.request'):
  637. self.assertHttpStatus(self.client.get(self._get_url('bulk_edit')), 403)
  638. # Try POST without permission
  639. with disable_warnings('django.request'):
  640. self.assertHttpStatus(self.client.post(self._get_url('bulk_edit'), data), 403)
  641. @override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
  642. def test_bulk_edit_objects_with_permission(self):
  643. pk_list = self._get_queryset().values_list('pk', flat=True)[:3]
  644. data = {
  645. 'pk': pk_list,
  646. '_apply': True, # Form button
  647. }
  648. # Append the form data to the request
  649. data.update(post_data(self.bulk_edit_data))
  650. # Assign model-level permission
  651. obj_perm = ObjectPermission(
  652. name='Test permission',
  653. actions=['change']
  654. )
  655. obj_perm.save()
  656. obj_perm.users.add(self.user)
  657. obj_perm.object_types.add(ContentType.objects.get_for_model(self.model))
  658. # Try POST with model-level permission
  659. self.assertHttpStatus(self.client.post(self._get_url('bulk_edit'), data), 302)
  660. for i, instance in enumerate(self._get_queryset().filter(pk__in=pk_list)):
  661. self.assertInstanceEqual(instance, self.bulk_edit_data)
  662. @override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
  663. def test_bulk_edit_objects_with_constrained_permission(self):
  664. pk_list = list(self._get_queryset().values_list('pk', flat=True)[:3])
  665. data = {
  666. 'pk': pk_list,
  667. '_apply': True, # Form button
  668. }
  669. # Append the form data to the request
  670. data.update(post_data(self.bulk_edit_data))
  671. # Dynamically determine a constraint that will *not* be matched by the updated objects.
  672. attr_name = list(self.bulk_edit_data.keys())[0]
  673. field = self.model._meta.get_field(attr_name)
  674. value = field.value_from_object(self._get_queryset().first())
  675. # Assign constrained permission
  676. obj_perm = ObjectPermission(
  677. name='Test permission',
  678. constraints={attr_name: value},
  679. actions=['change']
  680. )
  681. obj_perm.save()
  682. obj_perm.users.add(self.user)
  683. obj_perm.object_types.add(ContentType.objects.get_for_model(self.model))
  684. # Attempt to bulk edit permitted objects into a non-permitted state
  685. response = self.client.post(self._get_url('bulk_edit'), data)
  686. self.assertHttpStatus(response, 200)
  687. # Update permission constraints
  688. obj_perm.constraints = {'pk__gt': 0}
  689. obj_perm.save()
  690. # Bulk edit permitted objects
  691. self.assertHttpStatus(self.client.post(self._get_url('bulk_edit'), data), 302)
  692. for i, instance in enumerate(self._get_queryset().filter(pk__in=pk_list)):
  693. self.assertInstanceEqual(instance, self.bulk_edit_data)
  694. class BulkDeleteObjectsViewTestCase(ModelViewTestCase):
  695. """
  696. Delete multiple instances.
  697. """
  698. @override_settings(EXEMPT_VIEW_PERMISSIONS=[])
  699. def test_bulk_delete_objects_without_permission(self):
  700. pk_list = self._get_queryset().values_list('pk', flat=True)[:3]
  701. data = {
  702. 'pk': pk_list,
  703. 'confirm': True,
  704. '_confirm': True, # Form button
  705. }
  706. # Test GET without permission
  707. with disable_warnings('django.request'):
  708. self.assertHttpStatus(self.client.get(self._get_url('bulk_delete')), 403)
  709. # Try POST without permission
  710. with disable_warnings('django.request'):
  711. self.assertHttpStatus(self.client.post(self._get_url('bulk_delete'), data), 403)
  712. @override_settings(EXEMPT_VIEW_PERMISSIONS=[])
  713. def test_bulk_delete_objects_with_permission(self):
  714. pk_list = self._get_queryset().values_list('pk', flat=True)
  715. data = {
  716. 'pk': pk_list,
  717. 'confirm': True,
  718. '_confirm': True, # Form button
  719. }
  720. # Assign unconstrained permission
  721. obj_perm = ObjectPermission(
  722. name='Test permission',
  723. actions=['delete']
  724. )
  725. obj_perm.save()
  726. obj_perm.users.add(self.user)
  727. obj_perm.object_types.add(ContentType.objects.get_for_model(self.model))
  728. # Try POST with model-level permission
  729. self.assertHttpStatus(self.client.post(self._get_url('bulk_delete'), data), 302)
  730. self.assertEqual(self._get_queryset().count(), 0)
  731. @override_settings(EXEMPT_VIEW_PERMISSIONS=[])
  732. def test_bulk_delete_objects_with_constrained_permission(self):
  733. initial_count = self._get_queryset().count()
  734. pk_list = self._get_queryset().values_list('pk', flat=True)
  735. data = {
  736. 'pk': pk_list,
  737. 'confirm': True,
  738. '_confirm': True, # Form button
  739. }
  740. # Assign constrained permission
  741. obj_perm = ObjectPermission(
  742. name='Test permission',
  743. constraints={'pk': 0}, # Dummy permission to deny all
  744. actions=['delete']
  745. )
  746. obj_perm.save()
  747. obj_perm.users.add(self.user)
  748. obj_perm.object_types.add(ContentType.objects.get_for_model(self.model))
  749. # Attempt to bulk delete non-permitted objects
  750. self.assertHttpStatus(self.client.post(self._get_url('bulk_delete'), data), 302)
  751. self.assertEqual(self._get_queryset().count(), initial_count)
  752. # Update permission constraints
  753. obj_perm.constraints = {'pk__gt': 0} # Dummy permission to allow all
  754. obj_perm.save()
  755. # Bulk delete permitted objects
  756. self.assertHttpStatus(self.client.post(self._get_url('bulk_delete'), data), 302)
  757. self.assertEqual(self._get_queryset().count(), 0)
  758. class BulkRenameObjectsViewTestCase(ModelViewTestCase):
  759. """
  760. Rename multiple instances.
  761. """
  762. rename_data = {
  763. 'find': '^(.*)$',
  764. 'replace': '\\1X', # Append an X to the original value
  765. 'use_regex': True,
  766. }
  767. def test_bulk_rename_objects_without_permission(self):
  768. pk_list = self._get_queryset().values_list('pk', flat=True)[:3]
  769. data = {
  770. 'pk': pk_list,
  771. '_apply': True, # Form button
  772. }
  773. data.update(self.rename_data)
  774. # Test GET without permission
  775. with disable_warnings('django.request'):
  776. self.assertHttpStatus(self.client.get(self._get_url('bulk_rename')), 403)
  777. # Try POST without permission
  778. with disable_warnings('django.request'):
  779. self.assertHttpStatus(self.client.post(self._get_url('bulk_rename'), data), 403)
  780. @override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
  781. def test_bulk_rename_objects_with_permission(self):
  782. objects = self._get_queryset().all()[:3]
  783. pk_list = [obj.pk for obj in objects]
  784. data = {
  785. 'pk': pk_list,
  786. '_apply': True, # Form button
  787. }
  788. data.update(self.rename_data)
  789. # Assign model-level permission
  790. obj_perm = ObjectPermission(
  791. name='Test permission',
  792. actions=['change']
  793. )
  794. obj_perm.save()
  795. obj_perm.users.add(self.user)
  796. obj_perm.object_types.add(ContentType.objects.get_for_model(self.model))
  797. # Try POST with model-level permission
  798. self.assertHttpStatus(self.client.post(self._get_url('bulk_rename'), data), 302)
  799. for i, instance in enumerate(self._get_queryset().filter(pk__in=pk_list)):
  800. self.assertEqual(instance.name, f'{objects[i].name}X')
  801. @override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
  802. def test_bulk_rename_objects_with_constrained_permission(self):
  803. objects = self._get_queryset().all()[:3]
  804. pk_list = [obj.pk for obj in objects]
  805. data = {
  806. 'pk': pk_list,
  807. '_apply': True, # Form button
  808. }
  809. data.update(self.rename_data)
  810. # Assign constrained permission
  811. obj_perm = ObjectPermission(
  812. name='Test permission',
  813. constraints={'name__regex': '[^X]$'},
  814. actions=['change']
  815. )
  816. obj_perm.save()
  817. obj_perm.users.add(self.user)
  818. obj_perm.object_types.add(ContentType.objects.get_for_model(self.model))
  819. # Attempt to bulk edit permitted objects into a non-permitted state
  820. response = self.client.post(self._get_url('bulk_rename'), data)
  821. self.assertHttpStatus(response, 200)
  822. # Update permission constraints
  823. obj_perm.constraints = {'pk__gt': 0}
  824. obj_perm.save()
  825. # Bulk rename permitted objects
  826. self.assertHttpStatus(self.client.post(self._get_url('bulk_rename'), data), 302)
  827. for i, instance in enumerate(self._get_queryset().filter(pk__in=pk_list)):
  828. self.assertEqual(instance.name, f'{objects[i].name}X')
  829. class PrimaryObjectViewTestCase(
  830. GetObjectViewTestCase,
  831. GetObjectChangelogViewTestCase,
  832. CreateObjectViewTestCase,
  833. EditObjectViewTestCase,
  834. DeleteObjectViewTestCase,
  835. ListObjectsViewTestCase,
  836. BulkImportObjectsViewTestCase,
  837. BulkEditObjectsViewTestCase,
  838. BulkDeleteObjectsViewTestCase,
  839. ):
  840. """
  841. TestCase suitable for testing all standard View functions for primary objects
  842. """
  843. maxDiff = None
  844. class OrganizationalObjectViewTestCase(
  845. GetObjectChangelogViewTestCase,
  846. CreateObjectViewTestCase,
  847. EditObjectViewTestCase,
  848. DeleteObjectViewTestCase,
  849. ListObjectsViewTestCase,
  850. BulkImportObjectsViewTestCase,
  851. BulkDeleteObjectsViewTestCase,
  852. ):
  853. """
  854. TestCase suitable for all organizational objects
  855. """
  856. maxDiff = None
  857. class DeviceComponentTemplateViewTestCase(
  858. EditObjectViewTestCase,
  859. DeleteObjectViewTestCase,
  860. CreateMultipleObjectsViewTestCase,
  861. BulkEditObjectsViewTestCase,
  862. BulkRenameObjectsViewTestCase,
  863. BulkDeleteObjectsViewTestCase,
  864. ):
  865. """
  866. TestCase suitable for testing device component template models (ConsolePortTemplates, InterfaceTemplates, etc.)
  867. """
  868. maxDiff = None
  869. class DeviceComponentViewTestCase(
  870. GetObjectViewTestCase,
  871. GetObjectChangelogViewTestCase,
  872. EditObjectViewTestCase,
  873. DeleteObjectViewTestCase,
  874. ListObjectsViewTestCase,
  875. CreateMultipleObjectsViewTestCase,
  876. BulkImportObjectsViewTestCase,
  877. BulkEditObjectsViewTestCase,
  878. BulkRenameObjectsViewTestCase,
  879. BulkDeleteObjectsViewTestCase,
  880. ):
  881. """
  882. TestCase suitable for testing device component models (ConsolePorts, Interfaces, etc.)
  883. """
  884. maxDiff = None