views.py 36 KB

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