views.py 40 KB

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