test_api.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464
  1. import datetime
  2. from unittest import skipIf
  3. from django.contrib.contenttypes.models import ContentType
  4. from django.test import override_settings
  5. from django.urls import reverse
  6. from django.utils.timezone import make_aware
  7. from django_rq.queues import get_connection
  8. from rest_framework import status
  9. from rq import Worker
  10. from dcim.models import Device, DeviceRole, DeviceType, Manufacturer, Rack, Location, RackRole, Site
  11. from extras.api.views import ReportViewSet, ScriptViewSet
  12. from extras.models import ConfigContext, CustomField, ExportTemplate, ImageAttachment, Tag
  13. from extras.reports import Report
  14. from extras.scripts import BooleanVar, IntegerVar, Script, StringVar
  15. from utilities.testing import APITestCase, APIViewTestCases
  16. rq_worker_running = Worker.count(get_connection('default'))
  17. class AppTest(APITestCase):
  18. def test_root(self):
  19. url = reverse('extras-api:api-root')
  20. response = self.client.get('{}?format=api'.format(url), **self.header)
  21. self.assertEqual(response.status_code, 200)
  22. class CustomFieldTest(APIViewTestCases.APIViewTestCase):
  23. model = CustomField
  24. brief_fields = ['id', 'name', 'url']
  25. create_data = [
  26. {
  27. 'content_types': ['dcim.site'],
  28. 'name': 'cf4',
  29. 'type': 'date',
  30. },
  31. {
  32. 'content_types': ['dcim.site'],
  33. 'name': 'cf5',
  34. 'type': 'url',
  35. },
  36. {
  37. 'content_types': ['dcim.site'],
  38. 'name': 'cf6',
  39. 'type': 'select',
  40. },
  41. ]
  42. bulk_update_data = {
  43. 'description': 'New description',
  44. }
  45. @classmethod
  46. def setUpTestData(cls):
  47. site_ct = ContentType.objects.get_for_model(Site)
  48. custom_fields = (
  49. CustomField(
  50. name='cf1',
  51. type='text'
  52. ),
  53. CustomField(
  54. name='cf2',
  55. type='integer'
  56. ),
  57. CustomField(
  58. name='cf3',
  59. type='boolean'
  60. ),
  61. )
  62. CustomField.objects.bulk_create(custom_fields)
  63. for cf in custom_fields:
  64. cf.content_types.add(site_ct)
  65. class ExportTemplateTest(APIViewTestCases.APIViewTestCase):
  66. model = ExportTemplate
  67. brief_fields = ['id', 'name', 'url']
  68. create_data = [
  69. {
  70. 'content_type': 'dcim.device',
  71. 'name': 'Test Export Template 4',
  72. 'template_code': '{% for obj in queryset %}{{ obj.name }}\n{% endfor %}',
  73. },
  74. {
  75. 'content_type': 'dcim.device',
  76. 'name': 'Test Export Template 5',
  77. 'template_code': '{% for obj in queryset %}{{ obj.name }}\n{% endfor %}',
  78. },
  79. {
  80. 'content_type': 'dcim.device',
  81. 'name': 'Test Export Template 6',
  82. 'template_code': '{% for obj in queryset %}{{ obj.name }}\n{% endfor %}',
  83. },
  84. ]
  85. bulk_update_data = {
  86. 'description': 'New description',
  87. }
  88. @classmethod
  89. def setUpTestData(cls):
  90. ct = ContentType.objects.get_for_model(Device)
  91. export_templates = (
  92. ExportTemplate(
  93. content_type=ct,
  94. name='Export Template 1',
  95. template_code='{% for obj in queryset %}{{ obj.name }}\n{% endfor %}'
  96. ),
  97. ExportTemplate(
  98. content_type=ct,
  99. name='Export Template 2',
  100. template_code='{% for obj in queryset %}{{ obj.name }}\n{% endfor %}'
  101. ),
  102. ExportTemplate(
  103. content_type=ct,
  104. name='Export Template 3',
  105. template_code='{% for obj in queryset %}{{ obj.name }}\n{% endfor %}'
  106. ),
  107. )
  108. ExportTemplate.objects.bulk_create(export_templates)
  109. class TagTest(APIViewTestCases.APIViewTestCase):
  110. model = Tag
  111. brief_fields = ['color', 'id', 'name', 'slug', 'url']
  112. create_data = [
  113. {
  114. 'name': 'Tag 4',
  115. 'slug': 'tag-4',
  116. },
  117. {
  118. 'name': 'Tag 5',
  119. 'slug': 'tag-5',
  120. },
  121. {
  122. 'name': 'Tag 6',
  123. 'slug': 'tag-6',
  124. },
  125. ]
  126. bulk_update_data = {
  127. 'description': 'New description',
  128. }
  129. @classmethod
  130. def setUpTestData(cls):
  131. tags = (
  132. Tag(name='Tag 1', slug='tag-1'),
  133. Tag(name='Tag 2', slug='tag-2'),
  134. Tag(name='Tag 3', slug='tag-3'),
  135. )
  136. Tag.objects.bulk_create(tags)
  137. # TODO: Standardize to APIViewTestCase (needs create & update tests)
  138. class ImageAttachmentTest(
  139. APIViewTestCases.GetObjectViewTestCase,
  140. APIViewTestCases.ListObjectsViewTestCase,
  141. APIViewTestCases.DeleteObjectViewTestCase
  142. ):
  143. model = ImageAttachment
  144. brief_fields = ['id', 'image', 'name', 'url']
  145. @classmethod
  146. def setUpTestData(cls):
  147. ct = ContentType.objects.get_for_model(Site)
  148. site = Site.objects.create(name='Site 1', slug='site-1')
  149. image_attachments = (
  150. ImageAttachment(
  151. content_type=ct,
  152. object_id=site.pk,
  153. name='Image Attachment 1',
  154. image='http://example.com/image1.png',
  155. image_height=100,
  156. image_width=100
  157. ),
  158. ImageAttachment(
  159. content_type=ct,
  160. object_id=site.pk,
  161. name='Image Attachment 2',
  162. image='http://example.com/image2.png',
  163. image_height=100,
  164. image_width=100
  165. ),
  166. ImageAttachment(
  167. content_type=ct,
  168. object_id=site.pk,
  169. name='Image Attachment 3',
  170. image='http://example.com/image3.png',
  171. image_height=100,
  172. image_width=100
  173. )
  174. )
  175. ImageAttachment.objects.bulk_create(image_attachments)
  176. class ConfigContextTest(APIViewTestCases.APIViewTestCase):
  177. model = ConfigContext
  178. brief_fields = ['id', 'name', 'url']
  179. create_data = [
  180. {
  181. 'name': 'Config Context 4',
  182. 'data': {'more_foo': True},
  183. },
  184. {
  185. 'name': 'Config Context 5',
  186. 'data': {'more_bar': False},
  187. },
  188. {
  189. 'name': 'Config Context 6',
  190. 'data': {'more_baz': None},
  191. },
  192. ]
  193. bulk_update_data = {
  194. 'description': 'New description',
  195. }
  196. @classmethod
  197. def setUpTestData(cls):
  198. config_contexts = (
  199. ConfigContext(name='Config Context 1', weight=100, data={'foo': 123}),
  200. ConfigContext(name='Config Context 2', weight=200, data={'bar': 456}),
  201. ConfigContext(name='Config Context 3', weight=300, data={'baz': 789}),
  202. )
  203. ConfigContext.objects.bulk_create(config_contexts)
  204. def test_render_configcontext_for_object(self):
  205. """
  206. Test rendering config context data for a device.
  207. """
  208. manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
  209. devicetype = DeviceType.objects.create(manufacturer=manufacturer, model='Device Type 1', slug='device-type-1')
  210. devicerole = DeviceRole.objects.create(name='Device Role 1', slug='device-role-1')
  211. site = Site.objects.create(name='Site-1', slug='site-1')
  212. device = Device.objects.create(name='Device 1', device_type=devicetype, device_role=devicerole, site=site)
  213. # Test default config contexts (created at test setup)
  214. rendered_context = device.get_config_context()
  215. self.assertEqual(rendered_context['foo'], 123)
  216. self.assertEqual(rendered_context['bar'], 456)
  217. self.assertEqual(rendered_context['baz'], 789)
  218. # Add another context specific to the site
  219. configcontext4 = ConfigContext(
  220. name='Config Context 4',
  221. data={'site_data': 'ABC'}
  222. )
  223. configcontext4.save()
  224. configcontext4.sites.add(site)
  225. rendered_context = device.get_config_context()
  226. self.assertEqual(rendered_context['site_data'], 'ABC')
  227. # Override one of the default contexts
  228. configcontext5 = ConfigContext(
  229. name='Config Context 5',
  230. weight=2000,
  231. data={'foo': 999}
  232. )
  233. configcontext5.save()
  234. configcontext5.sites.add(site)
  235. rendered_context = device.get_config_context()
  236. self.assertEqual(rendered_context['foo'], 999)
  237. # Add a context which does NOT match our device and ensure it does not apply
  238. site2 = Site.objects.create(name='Site 2', slug='site-2')
  239. configcontext6 = ConfigContext(
  240. name='Config Context 6',
  241. weight=2000,
  242. data={'bar': 999}
  243. )
  244. configcontext6.save()
  245. configcontext6.sites.add(site2)
  246. rendered_context = device.get_config_context()
  247. self.assertEqual(rendered_context['bar'], 456)
  248. class ReportTest(APITestCase):
  249. class TestReport(Report):
  250. def test_foo(self):
  251. self.log_success(None, "Report completed")
  252. def get_test_report(self, *args):
  253. return self.TestReport()
  254. def setUp(self):
  255. super().setUp()
  256. # Monkey-patch the API viewset's _get_script method to return our test script above
  257. ReportViewSet._retrieve_report = self.get_test_report
  258. def test_get_report(self):
  259. url = reverse('extras-api:report-detail', kwargs={'pk': None})
  260. response = self.client.get(url, **self.header)
  261. self.assertEqual(response.data['name'], self.TestReport.__name__)
  262. @skipIf(not rq_worker_running, "RQ worker not running")
  263. def test_run_report(self):
  264. self.add_permissions('extras.run_script')
  265. url = reverse('extras-api:report-run', kwargs={'pk': None})
  266. response = self.client.post(url, {}, format='json', **self.header)
  267. self.assertHttpStatus(response, status.HTTP_200_OK)
  268. self.assertEqual(response.data['result']['status']['value'], 'pending')
  269. class ScriptTest(APITestCase):
  270. class TestScript(Script):
  271. class Meta:
  272. name = "Test script"
  273. var1 = StringVar()
  274. var2 = IntegerVar()
  275. var3 = BooleanVar()
  276. def run(self, data, commit=True):
  277. self.log_info(data['var1'])
  278. self.log_success(data['var2'])
  279. self.log_failure(data['var3'])
  280. return 'Script complete'
  281. def get_test_script(self, *args):
  282. return self.TestScript
  283. def setUp(self):
  284. super().setUp()
  285. # Monkey-patch the API viewset's _get_script method to return our test script above
  286. ScriptViewSet._get_script = self.get_test_script
  287. def test_get_script(self):
  288. url = reverse('extras-api:script-detail', kwargs={'pk': None})
  289. response = self.client.get(url, **self.header)
  290. self.assertEqual(response.data['name'], self.TestScript.Meta.name)
  291. self.assertEqual(response.data['vars']['var1'], 'StringVar')
  292. self.assertEqual(response.data['vars']['var2'], 'IntegerVar')
  293. self.assertEqual(response.data['vars']['var3'], 'BooleanVar')
  294. @skipIf(not rq_worker_running, "RQ worker not running")
  295. def test_run_script(self):
  296. script_data = {
  297. 'var1': 'FooBar',
  298. 'var2': 123,
  299. 'var3': False,
  300. }
  301. data = {
  302. 'data': script_data,
  303. 'commit': True,
  304. }
  305. url = reverse('extras-api:script-detail', kwargs={'pk': None})
  306. response = self.client.post(url, data, format='json', **self.header)
  307. self.assertHttpStatus(response, status.HTTP_200_OK)
  308. self.assertEqual(response.data['result']['status']['value'], 'pending')
  309. class CreatedUpdatedFilterTest(APITestCase):
  310. def setUp(self):
  311. super().setUp()
  312. self.site1 = Site.objects.create(name='Test Site 1', slug='test-site-1')
  313. self.location1 = Location.objects.create(site=self.site1, name='Test Location 1', slug='test-location-1')
  314. self.rackrole1 = RackRole.objects.create(name='Test Rack Role 1', slug='test-rack-role-1', color='ff0000')
  315. self.rack1 = Rack.objects.create(
  316. site=self.site1, location=self.location1, role=self.rackrole1, name='Test Rack 1', u_height=42,
  317. )
  318. self.rack2 = Rack.objects.create(
  319. site=self.site1, location=self.location1, role=self.rackrole1, name='Test Rack 2', u_height=42,
  320. )
  321. # change the created and last_updated of one
  322. Rack.objects.filter(pk=self.rack2.pk).update(
  323. last_updated=make_aware(datetime.datetime(2001, 2, 3, 1, 2, 3, 4)),
  324. created=make_aware(datetime.datetime(2001, 2, 3))
  325. )
  326. def test_get_rack_created(self):
  327. self.add_permissions('dcim.view_rack')
  328. url = reverse('dcim-api:rack-list')
  329. response = self.client.get('{}?created=2001-02-03'.format(url), **self.header)
  330. self.assertEqual(response.data['count'], 1)
  331. self.assertEqual(response.data['results'][0]['id'], self.rack2.pk)
  332. def test_get_rack_created_gte(self):
  333. self.add_permissions('dcim.view_rack')
  334. url = reverse('dcim-api:rack-list')
  335. response = self.client.get('{}?created__gte=2001-02-04'.format(url), **self.header)
  336. self.assertEqual(response.data['count'], 1)
  337. self.assertEqual(response.data['results'][0]['id'], self.rack1.pk)
  338. def test_get_rack_created_lte(self):
  339. self.add_permissions('dcim.view_rack')
  340. url = reverse('dcim-api:rack-list')
  341. response = self.client.get('{}?created__lte=2001-02-04'.format(url), **self.header)
  342. self.assertEqual(response.data['count'], 1)
  343. self.assertEqual(response.data['results'][0]['id'], self.rack2.pk)
  344. def test_get_rack_last_updated(self):
  345. self.add_permissions('dcim.view_rack')
  346. url = reverse('dcim-api:rack-list')
  347. response = self.client.get('{}?last_updated=2001-02-03%2001:02:03.000004'.format(url), **self.header)
  348. self.assertEqual(response.data['count'], 1)
  349. self.assertEqual(response.data['results'][0]['id'], self.rack2.pk)
  350. def test_get_rack_last_updated_gte(self):
  351. self.add_permissions('dcim.view_rack')
  352. url = reverse('dcim-api:rack-list')
  353. response = self.client.get('{}?last_updated__gte=2001-02-04%2001:02:03.000004'.format(url), **self.header)
  354. self.assertEqual(response.data['count'], 1)
  355. self.assertEqual(response.data['results'][0]['id'], self.rack1.pk)
  356. def test_get_rack_last_updated_lte(self):
  357. self.add_permissions('dcim.view_rack')
  358. url = reverse('dcim-api:rack-list')
  359. response = self.client.get('{}?last_updated__lte=2001-02-04%2001:02:03.000004'.format(url), **self.header)
  360. self.assertEqual(response.data['count'], 1)
  361. self.assertEqual(response.data['results'][0]['id'], self.rack2.pk)
  362. class ContentTypeTest(APITestCase):
  363. @override_settings(EXEMPT_VIEW_PERMISSIONS=['contenttypes.contenttype'])
  364. def test_list_objects(self):
  365. contenttype_count = ContentType.objects.count()
  366. response = self.client.get(reverse('extras-api:contenttype-list'), **self.header)
  367. self.assertHttpStatus(response, status.HTTP_200_OK)
  368. self.assertEqual(response.data['count'], contenttype_count)
  369. @override_settings(EXEMPT_VIEW_PERMISSIONS=['contenttypes.contenttype'])
  370. def test_get_object(self):
  371. contenttype = ContentType.objects.first()
  372. url = reverse('extras-api:contenttype-detail', kwargs={'pk': contenttype.pk})
  373. self.assertHttpStatus(self.client.get(url, **self.header), status.HTTP_200_OK)