test_api.py 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623
  1. import datetime
  2. from unittest import skipIf
  3. from django.contrib.auth.models import User
  4. from django.contrib.contenttypes.models import ContentType
  5. from django.test import override_settings
  6. from django.urls import reverse
  7. from django.utils.timezone import make_aware
  8. from django_rq.queues import get_connection
  9. from rest_framework import status
  10. from rq import Worker
  11. from dcim.models import Device, DeviceRole, DeviceType, Manufacturer, Rack, Location, RackRole, Site
  12. from extras.api.views import ReportViewSet, ScriptViewSet
  13. from extras.models import *
  14. from extras.reports import Report
  15. from extras.scripts import BooleanVar, IntegerVar, Script, StringVar
  16. from utilities.testing import APITestCase, APIViewTestCases
  17. rq_worker_running = Worker.count(get_connection('default'))
  18. class AppTest(APITestCase):
  19. def test_root(self):
  20. url = reverse('extras-api:api-root')
  21. response = self.client.get('{}?format=api'.format(url), **self.header)
  22. self.assertEqual(response.status_code, 200)
  23. class WebhookTest(APIViewTestCases.APIViewTestCase):
  24. model = Webhook
  25. brief_fields = ['display', 'id', 'name', 'url']
  26. create_data = [
  27. {
  28. 'content_types': ['dcim.device', 'dcim.devicetype'],
  29. 'name': 'Webhook 4',
  30. 'type_create': True,
  31. 'payload_url': 'http://example.com/?4',
  32. },
  33. {
  34. 'content_types': ['dcim.device', 'dcim.devicetype'],
  35. 'name': 'Webhook 5',
  36. 'type_update': True,
  37. 'payload_url': 'http://example.com/?5',
  38. },
  39. {
  40. 'content_types': ['dcim.device', 'dcim.devicetype'],
  41. 'name': 'Webhook 6',
  42. 'type_delete': True,
  43. 'payload_url': 'http://example.com/?6',
  44. },
  45. ]
  46. bulk_update_data = {
  47. 'ssl_verification': False,
  48. }
  49. @classmethod
  50. def setUpTestData(cls):
  51. site_ct = ContentType.objects.get_for_model(Site)
  52. rack_ct = ContentType.objects.get_for_model(Rack)
  53. webhooks = (
  54. Webhook(
  55. name='Webhook 1',
  56. type_create=True,
  57. payload_url='http://example.com/?1',
  58. ),
  59. Webhook(
  60. name='Webhook 2',
  61. type_update=True,
  62. payload_url='http://example.com/?1',
  63. ),
  64. Webhook(
  65. name='Webhook 3',
  66. type_delete=True,
  67. payload_url='http://example.com/?1',
  68. ),
  69. )
  70. Webhook.objects.bulk_create(webhooks)
  71. for webhook in webhooks:
  72. webhook.content_types.add(site_ct, rack_ct)
  73. class CustomFieldTest(APIViewTestCases.APIViewTestCase):
  74. model = CustomField
  75. brief_fields = ['display', 'id', 'name', 'url']
  76. create_data = [
  77. {
  78. 'content_types': ['dcim.site'],
  79. 'name': 'cf4',
  80. 'type': 'date',
  81. },
  82. {
  83. 'content_types': ['dcim.site'],
  84. 'name': 'cf5',
  85. 'type': 'url',
  86. },
  87. {
  88. 'content_types': ['dcim.site'],
  89. 'name': 'cf6',
  90. 'type': 'select',
  91. },
  92. ]
  93. bulk_update_data = {
  94. 'description': 'New description',
  95. }
  96. @classmethod
  97. def setUpTestData(cls):
  98. site_ct = ContentType.objects.get_for_model(Site)
  99. custom_fields = (
  100. CustomField(
  101. name='cf1',
  102. type='text'
  103. ),
  104. CustomField(
  105. name='cf2',
  106. type='integer'
  107. ),
  108. CustomField(
  109. name='cf3',
  110. type='boolean'
  111. ),
  112. )
  113. CustomField.objects.bulk_create(custom_fields)
  114. for cf in custom_fields:
  115. cf.content_types.add(site_ct)
  116. class CustomLinkTest(APIViewTestCases.APIViewTestCase):
  117. model = CustomLink
  118. brief_fields = ['display', 'id', 'name', 'url']
  119. create_data = [
  120. {
  121. 'content_type': 'dcim.site',
  122. 'name': 'Custom Link 4',
  123. 'link_text': 'Link 4',
  124. 'link_url': 'http://example.com/?4',
  125. },
  126. {
  127. 'content_type': 'dcim.site',
  128. 'name': 'Custom Link 5',
  129. 'link_text': 'Link 5',
  130. 'link_url': 'http://example.com/?5',
  131. },
  132. {
  133. 'content_type': 'dcim.site',
  134. 'name': 'Custom Link 6',
  135. 'link_text': 'Link 6',
  136. 'link_url': 'http://example.com/?6',
  137. },
  138. ]
  139. bulk_update_data = {
  140. 'new_window': True,
  141. }
  142. @classmethod
  143. def setUpTestData(cls):
  144. site_ct = ContentType.objects.get_for_model(Site)
  145. custom_links = (
  146. CustomLink(
  147. content_type=site_ct,
  148. name='Custom Link 1',
  149. link_text='Link 1',
  150. link_url='http://example.com/?1',
  151. ),
  152. CustomLink(
  153. content_type=site_ct,
  154. name='Custom Link 2',
  155. link_text='Link 2',
  156. link_url='http://example.com/?2',
  157. ),
  158. CustomLink(
  159. content_type=site_ct,
  160. name='Custom Link 3',
  161. link_text='Link 3',
  162. link_url='http://example.com/?3',
  163. ),
  164. )
  165. CustomLink.objects.bulk_create(custom_links)
  166. class ExportTemplateTest(APIViewTestCases.APIViewTestCase):
  167. model = ExportTemplate
  168. brief_fields = ['display', 'id', 'name', 'url']
  169. create_data = [
  170. {
  171. 'content_type': 'dcim.device',
  172. 'name': 'Test Export Template 4',
  173. 'template_code': '{% for obj in queryset %}{{ obj.name }}\n{% endfor %}',
  174. },
  175. {
  176. 'content_type': 'dcim.device',
  177. 'name': 'Test Export Template 5',
  178. 'template_code': '{% for obj in queryset %}{{ obj.name }}\n{% endfor %}',
  179. },
  180. {
  181. 'content_type': 'dcim.device',
  182. 'name': 'Test Export Template 6',
  183. 'template_code': '{% for obj in queryset %}{{ obj.name }}\n{% endfor %}',
  184. },
  185. ]
  186. bulk_update_data = {
  187. 'description': 'New description',
  188. }
  189. @classmethod
  190. def setUpTestData(cls):
  191. ct = ContentType.objects.get_for_model(Device)
  192. export_templates = (
  193. ExportTemplate(
  194. content_type=ct,
  195. name='Export Template 1',
  196. template_code='{% for obj in queryset %}{{ obj.name }}\n{% endfor %}'
  197. ),
  198. ExportTemplate(
  199. content_type=ct,
  200. name='Export Template 2',
  201. template_code='{% for obj in queryset %}{{ obj.name }}\n{% endfor %}'
  202. ),
  203. ExportTemplate(
  204. content_type=ct,
  205. name='Export Template 3',
  206. template_code='{% for obj in queryset %}{{ obj.name }}\n{% endfor %}'
  207. ),
  208. )
  209. ExportTemplate.objects.bulk_create(export_templates)
  210. class TagTest(APIViewTestCases.APIViewTestCase):
  211. model = Tag
  212. brief_fields = ['color', 'display', 'id', 'name', 'slug', 'url']
  213. create_data = [
  214. {
  215. 'name': 'Tag 4',
  216. 'slug': 'tag-4',
  217. },
  218. {
  219. 'name': 'Tag 5',
  220. 'slug': 'tag-5',
  221. },
  222. {
  223. 'name': 'Tag 6',
  224. 'slug': 'tag-6',
  225. },
  226. ]
  227. bulk_update_data = {
  228. 'description': 'New description',
  229. }
  230. @classmethod
  231. def setUpTestData(cls):
  232. tags = (
  233. Tag(name='Tag 1', slug='tag-1'),
  234. Tag(name='Tag 2', slug='tag-2'),
  235. Tag(name='Tag 3', slug='tag-3'),
  236. )
  237. Tag.objects.bulk_create(tags)
  238. # TODO: Standardize to APIViewTestCase (needs create & update tests)
  239. class ImageAttachmentTest(
  240. APIViewTestCases.GetObjectViewTestCase,
  241. APIViewTestCases.ListObjectsViewTestCase,
  242. APIViewTestCases.DeleteObjectViewTestCase
  243. ):
  244. model = ImageAttachment
  245. brief_fields = ['display', 'id', 'image', 'name', 'url']
  246. @classmethod
  247. def setUpTestData(cls):
  248. ct = ContentType.objects.get_for_model(Site)
  249. site = Site.objects.create(name='Site 1', slug='site-1')
  250. image_attachments = (
  251. ImageAttachment(
  252. content_type=ct,
  253. object_id=site.pk,
  254. name='Image Attachment 1',
  255. image='http://example.com/image1.png',
  256. image_height=100,
  257. image_width=100
  258. ),
  259. ImageAttachment(
  260. content_type=ct,
  261. object_id=site.pk,
  262. name='Image Attachment 2',
  263. image='http://example.com/image2.png',
  264. image_height=100,
  265. image_width=100
  266. ),
  267. ImageAttachment(
  268. content_type=ct,
  269. object_id=site.pk,
  270. name='Image Attachment 3',
  271. image='http://example.com/image3.png',
  272. image_height=100,
  273. image_width=100
  274. )
  275. )
  276. ImageAttachment.objects.bulk_create(image_attachments)
  277. class JournalEntryTest(APIViewTestCases.APIViewTestCase):
  278. model = JournalEntry
  279. brief_fields = ['created', 'display', 'id', 'url']
  280. bulk_update_data = {
  281. 'comments': 'Overwritten',
  282. }
  283. @classmethod
  284. def setUpTestData(cls):
  285. user = User.objects.first()
  286. site = Site.objects.create(name='Site 1', slug='site-1')
  287. journal_entries = (
  288. JournalEntry(
  289. created_by=user,
  290. assigned_object=site,
  291. comments='Fourth entry',
  292. ),
  293. JournalEntry(
  294. created_by=user,
  295. assigned_object=site,
  296. comments='Fifth entry',
  297. ),
  298. JournalEntry(
  299. created_by=user,
  300. assigned_object=site,
  301. comments='Sixth entry',
  302. ),
  303. )
  304. JournalEntry.objects.bulk_create(journal_entries)
  305. cls.create_data = [
  306. {
  307. 'assigned_object_type': 'dcim.site',
  308. 'assigned_object_id': site.pk,
  309. 'comments': 'First entry',
  310. },
  311. {
  312. 'assigned_object_type': 'dcim.site',
  313. 'assigned_object_id': site.pk,
  314. 'comments': 'Second entry',
  315. },
  316. {
  317. 'assigned_object_type': 'dcim.site',
  318. 'assigned_object_id': site.pk,
  319. 'comments': 'Third entry',
  320. },
  321. ]
  322. class ConfigContextTest(APIViewTestCases.APIViewTestCase):
  323. model = ConfigContext
  324. brief_fields = ['display', 'id', 'name', 'url']
  325. create_data = [
  326. {
  327. 'name': 'Config Context 4',
  328. 'data': {'more_foo': True},
  329. },
  330. {
  331. 'name': 'Config Context 5',
  332. 'data': {'more_bar': False},
  333. },
  334. {
  335. 'name': 'Config Context 6',
  336. 'data': {'more_baz': None},
  337. },
  338. ]
  339. bulk_update_data = {
  340. 'description': 'New description',
  341. }
  342. @classmethod
  343. def setUpTestData(cls):
  344. config_contexts = (
  345. ConfigContext(name='Config Context 1', weight=100, data={'foo': 123}),
  346. ConfigContext(name='Config Context 2', weight=200, data={'bar': 456}),
  347. ConfigContext(name='Config Context 3', weight=300, data={'baz': 789}),
  348. )
  349. ConfigContext.objects.bulk_create(config_contexts)
  350. def test_render_configcontext_for_object(self):
  351. """
  352. Test rendering config context data for a device.
  353. """
  354. manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
  355. devicetype = DeviceType.objects.create(manufacturer=manufacturer, model='Device Type 1', slug='device-type-1')
  356. devicerole = DeviceRole.objects.create(name='Device Role 1', slug='device-role-1')
  357. site = Site.objects.create(name='Site-1', slug='site-1')
  358. device = Device.objects.create(name='Device 1', device_type=devicetype, device_role=devicerole, site=site)
  359. # Test default config contexts (created at test setup)
  360. rendered_context = device.get_config_context()
  361. self.assertEqual(rendered_context['foo'], 123)
  362. self.assertEqual(rendered_context['bar'], 456)
  363. self.assertEqual(rendered_context['baz'], 789)
  364. # Add another context specific to the site
  365. configcontext4 = ConfigContext(
  366. name='Config Context 4',
  367. data={'site_data': 'ABC'}
  368. )
  369. configcontext4.save()
  370. configcontext4.sites.add(site)
  371. rendered_context = device.get_config_context()
  372. self.assertEqual(rendered_context['site_data'], 'ABC')
  373. # Override one of the default contexts
  374. configcontext5 = ConfigContext(
  375. name='Config Context 5',
  376. weight=2000,
  377. data={'foo': 999}
  378. )
  379. configcontext5.save()
  380. configcontext5.sites.add(site)
  381. rendered_context = device.get_config_context()
  382. self.assertEqual(rendered_context['foo'], 999)
  383. # Add a context which does NOT match our device and ensure it does not apply
  384. site2 = Site.objects.create(name='Site 2', slug='site-2')
  385. configcontext6 = ConfigContext(
  386. name='Config Context 6',
  387. weight=2000,
  388. data={'bar': 999}
  389. )
  390. configcontext6.save()
  391. configcontext6.sites.add(site2)
  392. rendered_context = device.get_config_context()
  393. self.assertEqual(rendered_context['bar'], 456)
  394. class ReportTest(APITestCase):
  395. class TestReport(Report):
  396. def test_foo(self):
  397. self.log_success(None, "Report completed")
  398. def get_test_report(self, *args):
  399. return self.TestReport()
  400. def setUp(self):
  401. super().setUp()
  402. # Monkey-patch the API viewset's _get_script method to return our test script above
  403. ReportViewSet._retrieve_report = self.get_test_report
  404. def test_get_report(self):
  405. url = reverse('extras-api:report-detail', kwargs={'pk': None})
  406. response = self.client.get(url, **self.header)
  407. self.assertEqual(response.data['name'], self.TestReport.__name__)
  408. @skipIf(not rq_worker_running, "RQ worker not running")
  409. def test_run_report(self):
  410. self.add_permissions('extras.run_script')
  411. url = reverse('extras-api:report-run', kwargs={'pk': None})
  412. response = self.client.post(url, {}, format='json', **self.header)
  413. self.assertHttpStatus(response, status.HTTP_200_OK)
  414. self.assertEqual(response.data['result']['status']['value'], 'pending')
  415. class ScriptTest(APITestCase):
  416. class TestScript(Script):
  417. class Meta:
  418. name = "Test script"
  419. var1 = StringVar()
  420. var2 = IntegerVar()
  421. var3 = BooleanVar()
  422. def run(self, data, commit=True):
  423. self.log_info(data['var1'])
  424. self.log_success(data['var2'])
  425. self.log_failure(data['var3'])
  426. return 'Script complete'
  427. def get_test_script(self, *args):
  428. return self.TestScript
  429. def setUp(self):
  430. super().setUp()
  431. # Monkey-patch the API viewset's _get_script method to return our test script above
  432. ScriptViewSet._get_script = self.get_test_script
  433. def test_get_script(self):
  434. url = reverse('extras-api:script-detail', kwargs={'pk': None})
  435. response = self.client.get(url, **self.header)
  436. self.assertEqual(response.data['name'], self.TestScript.Meta.name)
  437. self.assertEqual(response.data['vars']['var1'], 'StringVar')
  438. self.assertEqual(response.data['vars']['var2'], 'IntegerVar')
  439. self.assertEqual(response.data['vars']['var3'], 'BooleanVar')
  440. @skipIf(not rq_worker_running, "RQ worker not running")
  441. def test_run_script(self):
  442. script_data = {
  443. 'var1': 'FooBar',
  444. 'var2': 123,
  445. 'var3': False,
  446. }
  447. data = {
  448. 'data': script_data,
  449. 'commit': True,
  450. }
  451. url = reverse('extras-api:script-detail', kwargs={'pk': None})
  452. response = self.client.post(url, data, format='json', **self.header)
  453. self.assertHttpStatus(response, status.HTTP_200_OK)
  454. self.assertEqual(response.data['result']['status']['value'], 'pending')
  455. class CreatedUpdatedFilterTest(APITestCase):
  456. def setUp(self):
  457. super().setUp()
  458. self.site1 = Site.objects.create(name='Test Site 1', slug='test-site-1')
  459. self.location1 = Location.objects.create(site=self.site1, name='Test Location 1', slug='test-location-1')
  460. self.rackrole1 = RackRole.objects.create(name='Test Rack Role 1', slug='test-rack-role-1', color='ff0000')
  461. self.rack1 = Rack.objects.create(
  462. site=self.site1, location=self.location1, role=self.rackrole1, name='Test Rack 1', u_height=42,
  463. )
  464. self.rack2 = Rack.objects.create(
  465. site=self.site1, location=self.location1, role=self.rackrole1, name='Test Rack 2', u_height=42,
  466. )
  467. # change the created and last_updated of one
  468. Rack.objects.filter(pk=self.rack2.pk).update(
  469. last_updated=make_aware(datetime.datetime(2001, 2, 3, 1, 2, 3, 4)),
  470. created=make_aware(datetime.datetime(2001, 2, 3))
  471. )
  472. def test_get_rack_created(self):
  473. self.add_permissions('dcim.view_rack')
  474. url = reverse('dcim-api:rack-list')
  475. response = self.client.get('{}?created=2001-02-03'.format(url), **self.header)
  476. self.assertEqual(response.data['count'], 1)
  477. self.assertEqual(response.data['results'][0]['id'], self.rack2.pk)
  478. def test_get_rack_created_gte(self):
  479. self.add_permissions('dcim.view_rack')
  480. url = reverse('dcim-api:rack-list')
  481. response = self.client.get('{}?created__gte=2001-02-04'.format(url), **self.header)
  482. self.assertEqual(response.data['count'], 1)
  483. self.assertEqual(response.data['results'][0]['id'], self.rack1.pk)
  484. def test_get_rack_created_lte(self):
  485. self.add_permissions('dcim.view_rack')
  486. url = reverse('dcim-api:rack-list')
  487. response = self.client.get('{}?created__lte=2001-02-04'.format(url), **self.header)
  488. self.assertEqual(response.data['count'], 1)
  489. self.assertEqual(response.data['results'][0]['id'], self.rack2.pk)
  490. def test_get_rack_last_updated(self):
  491. self.add_permissions('dcim.view_rack')
  492. url = reverse('dcim-api:rack-list')
  493. response = self.client.get('{}?last_updated=2001-02-03%2001:02:03.000004'.format(url), **self.header)
  494. self.assertEqual(response.data['count'], 1)
  495. self.assertEqual(response.data['results'][0]['id'], self.rack2.pk)
  496. def test_get_rack_last_updated_gte(self):
  497. self.add_permissions('dcim.view_rack')
  498. url = reverse('dcim-api:rack-list')
  499. response = self.client.get('{}?last_updated__gte=2001-02-04%2001:02:03.000004'.format(url), **self.header)
  500. self.assertEqual(response.data['count'], 1)
  501. self.assertEqual(response.data['results'][0]['id'], self.rack1.pk)
  502. def test_get_rack_last_updated_lte(self):
  503. self.add_permissions('dcim.view_rack')
  504. url = reverse('dcim-api:rack-list')
  505. response = self.client.get('{}?last_updated__lte=2001-02-04%2001:02:03.000004'.format(url), **self.header)
  506. self.assertEqual(response.data['count'], 1)
  507. self.assertEqual(response.data['results'][0]['id'], self.rack2.pk)
  508. class ContentTypeTest(APITestCase):
  509. @override_settings(EXEMPT_VIEW_PERMISSIONS=['contenttypes.contenttype'])
  510. def test_list_objects(self):
  511. contenttype_count = ContentType.objects.count()
  512. response = self.client.get(reverse('extras-api:contenttype-list'), **self.header)
  513. self.assertHttpStatus(response, status.HTTP_200_OK)
  514. self.assertEqual(response.data['count'], contenttype_count)
  515. @override_settings(EXEMPT_VIEW_PERMISSIONS=['contenttypes.contenttype'])
  516. def test_get_object(self):
  517. contenttype = ContentType.objects.first()
  518. url = reverse('extras-api:contenttype-detail', kwargs={'pk': contenttype.pk})
  519. self.assertHttpStatus(self.client.get(url, **self.header), status.HTTP_200_OK)