2
0

views.py 40 KB

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