test_api.py 47 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439
  1. import datetime
  2. import hashlib
  3. from unittest.mock import MagicMock, patch
  4. from django.contrib.contenttypes.models import ContentType
  5. from django.core.files.uploadedfile import SimpleUploadedFile
  6. from django.urls import reverse
  7. from django.utils.timezone import make_aware, now
  8. from rest_framework import status
  9. from core.choices import ManagedFileRootPathChoices
  10. from core.events import *
  11. from core.models import DataFile, DataSource, ObjectType
  12. from dcim.models import Device, DeviceRole, DeviceType, Location, Manufacturer, Rack, RackRole, Site
  13. from extras.choices import *
  14. from extras.models import *
  15. from extras.scripts import BooleanVar, IntegerVar, StringVar
  16. from extras.scripts import Script as PythonClass
  17. from users.constants import TOKEN_PREFIX
  18. from users.models import Group, Token, User
  19. from utilities.testing import APITestCase, APIViewTestCases
  20. class AppTest(APITestCase):
  21. def test_root(self):
  22. url = reverse('extras-api:api-root')
  23. response = self.client.get('{}?format=api'.format(url), **self.header)
  24. self.assertEqual(response.status_code, 200)
  25. class WebhookTest(APIViewTestCases.APIViewTestCase):
  26. model = Webhook
  27. brief_fields = ['description', 'display', 'id', 'name', 'url']
  28. create_data = [
  29. {
  30. 'name': 'Webhook 4',
  31. 'payload_url': 'http://example.com/?4',
  32. },
  33. {
  34. 'name': 'Webhook 5',
  35. 'payload_url': 'http://example.com/?5',
  36. },
  37. {
  38. 'name': 'Webhook 6',
  39. 'payload_url': 'http://example.com/?6',
  40. },
  41. ]
  42. bulk_update_data = {
  43. 'description': 'New description',
  44. 'ssl_verification': False,
  45. }
  46. @classmethod
  47. def setUpTestData(cls):
  48. webhooks = (
  49. Webhook(
  50. name='Webhook 1',
  51. payload_url='http://example.com/?1',
  52. ),
  53. Webhook(
  54. name='Webhook 2',
  55. payload_url='http://example.com/?1',
  56. ),
  57. Webhook(
  58. name='Webhook 3',
  59. payload_url='http://example.com/?1',
  60. ),
  61. )
  62. Webhook.objects.bulk_create(webhooks)
  63. class EventRuleTest(APIViewTestCases.APIViewTestCase):
  64. model = EventRule
  65. brief_fields = ['description', 'display', 'id', 'name', 'url']
  66. bulk_update_data = {
  67. 'enabled': False,
  68. 'description': 'New description',
  69. }
  70. update_data = {
  71. 'name': 'Event Rule X',
  72. 'enabled': False,
  73. 'description': 'New description',
  74. }
  75. @classmethod
  76. def setUpTestData(cls):
  77. webhooks = (
  78. Webhook(
  79. name='Webhook 1',
  80. payload_url='http://example.com/?1',
  81. ),
  82. Webhook(
  83. name='Webhook 2',
  84. payload_url='http://example.com/?1',
  85. ),
  86. Webhook(
  87. name='Webhook 3',
  88. payload_url='http://example.com/?1',
  89. ),
  90. Webhook(
  91. name='Webhook 4',
  92. payload_url='http://example.com/?1',
  93. ),
  94. Webhook(
  95. name='Webhook 5',
  96. payload_url='http://example.com/?1',
  97. ),
  98. Webhook(
  99. name='Webhook 6',
  100. payload_url='http://example.com/?1',
  101. ),
  102. )
  103. Webhook.objects.bulk_create(webhooks)
  104. event_rules = (
  105. EventRule(name='EventRule 1', event_types=[OBJECT_CREATED], action_object=webhooks[0]),
  106. EventRule(name='EventRule 2', event_types=[OBJECT_CREATED], action_object=webhooks[1]),
  107. EventRule(name='EventRule 3', event_types=[OBJECT_CREATED], action_object=webhooks[2]),
  108. )
  109. EventRule.objects.bulk_create(event_rules)
  110. cls.create_data = [
  111. {
  112. 'name': 'EventRule 4',
  113. 'object_types': ['dcim.device', 'dcim.devicetype'],
  114. 'event_types': [OBJECT_CREATED],
  115. 'action_type': EventRuleActionChoices.WEBHOOK,
  116. 'action_object_type': 'extras.webhook',
  117. 'action_object_id': webhooks[3].pk,
  118. },
  119. {
  120. 'name': 'EventRule 5',
  121. 'object_types': ['dcim.device', 'dcim.devicetype'],
  122. 'event_types': [OBJECT_CREATED],
  123. 'action_type': EventRuleActionChoices.WEBHOOK,
  124. 'action_object_type': 'extras.webhook',
  125. 'action_object_id': webhooks[4].pk,
  126. },
  127. {
  128. 'name': 'EventRule 6',
  129. 'object_types': ['dcim.device', 'dcim.devicetype'],
  130. 'event_types': [OBJECT_CREATED],
  131. 'action_type': EventRuleActionChoices.WEBHOOK,
  132. 'action_object_type': 'extras.webhook',
  133. 'action_object_id': webhooks[5].pk,
  134. },
  135. ]
  136. class CustomFieldTest(APIViewTestCases.APIViewTestCase):
  137. model = CustomField
  138. brief_fields = ['description', 'display', 'id', 'name', 'url']
  139. create_data = [
  140. {
  141. 'object_types': ['dcim.site'],
  142. 'name': 'cf4',
  143. 'type': 'date',
  144. },
  145. {
  146. 'object_types': ['dcim.site'],
  147. 'name': 'cf5',
  148. 'type': 'url',
  149. },
  150. {
  151. 'object_types': ['dcim.site'],
  152. 'name': 'cf6',
  153. 'type': 'text',
  154. },
  155. ]
  156. bulk_update_data = {
  157. 'description': 'New description',
  158. }
  159. update_data = {
  160. 'object_types': ['dcim.device'],
  161. 'name': 'New_Name',
  162. 'description': 'New description',
  163. }
  164. @classmethod
  165. def setUpTestData(cls):
  166. site_ct = ObjectType.objects.get_for_model(Site)
  167. custom_fields = (
  168. CustomField(
  169. name='cf1',
  170. type='text'
  171. ),
  172. CustomField(
  173. name='cf2',
  174. type='integer'
  175. ),
  176. CustomField(
  177. name='cf3',
  178. type='boolean'
  179. ),
  180. )
  181. CustomField.objects.bulk_create(custom_fields)
  182. for cf in custom_fields:
  183. cf.object_types.add(site_ct)
  184. class CustomFieldChoiceSetTest(APIViewTestCases.APIViewTestCase):
  185. model = CustomFieldChoiceSet
  186. brief_fields = ['choices_count', 'description', 'display', 'id', 'name', 'url']
  187. create_data = [
  188. {
  189. 'name': 'Choice Set 4',
  190. 'extra_choices': [
  191. ['4A', 'Choice 1'],
  192. ['4B', 'Choice 2'],
  193. ['4C', 'Choice 3'],
  194. ],
  195. },
  196. {
  197. 'name': 'Choice Set 5',
  198. 'extra_choices': [
  199. ['5A', 'Choice 1'],
  200. ['5B', 'Choice 2'],
  201. ['5C', 'Choice 3'],
  202. ],
  203. },
  204. {
  205. 'name': 'Choice Set 6',
  206. 'extra_choices': [
  207. ['6A', 'Choice 1'],
  208. ['6B', 'Choice 2'],
  209. ['6C', 'Choice 3'],
  210. ],
  211. },
  212. ]
  213. bulk_update_data = {
  214. 'description': 'New description',
  215. }
  216. update_data = {
  217. 'name': 'Choice Set X',
  218. 'extra_choices': [
  219. ['X1', 'Choice 1'],
  220. ['X2', 'Choice 2'],
  221. ['X3', 'Choice 3'],
  222. ],
  223. 'description': 'New description',
  224. }
  225. @classmethod
  226. def setUpTestData(cls):
  227. choice_sets = (
  228. CustomFieldChoiceSet(
  229. name='Choice Set 1',
  230. extra_choices=[['1A', '1A'], ['1B', '1B'], ['1C', '1C'], ['1D', '1D'], ['1E', '1E']],
  231. ),
  232. CustomFieldChoiceSet(
  233. name='Choice Set 2',
  234. extra_choices=[['2A', '2A'], ['2B', '2B'], ['2C', '2C'], ['2D', '2D'], ['2E', '2E']],
  235. ),
  236. CustomFieldChoiceSet(
  237. name='Choice Set 3',
  238. extra_choices=[['3A', '3A'], ['3B', '3B'], ['3C', '3C'], ['3D', '3D'], ['3E', '3E']],
  239. ),
  240. )
  241. CustomFieldChoiceSet.objects.bulk_create(choice_sets)
  242. def test_invalid_choice_items(self):
  243. """
  244. Attempting to define each choice as a single-item list should return a 400 error.
  245. """
  246. self.add_permissions('extras.add_customfieldchoiceset')
  247. data = {
  248. "name": "test",
  249. "extra_choices": [
  250. ["choice1"],
  251. ["choice2"],
  252. ["choice3"],
  253. ]
  254. }
  255. response = self.client.post(self._get_list_url(), data, format='json', **self.header)
  256. self.assertEqual(response.status_code, 400)
  257. class CustomLinkTest(APIViewTestCases.APIViewTestCase):
  258. model = CustomLink
  259. brief_fields = ['display', 'id', 'name', 'url']
  260. create_data = [
  261. {
  262. 'object_types': ['dcim.site'],
  263. 'name': 'Custom Link 4',
  264. 'enabled': True,
  265. 'link_text': 'Link 4',
  266. 'link_url': 'http://example.com/?4',
  267. },
  268. {
  269. 'object_types': ['dcim.site'],
  270. 'name': 'Custom Link 5',
  271. 'enabled': True,
  272. 'link_text': 'Link 5',
  273. 'link_url': 'http://example.com/?5',
  274. },
  275. {
  276. 'object_types': ['dcim.site'],
  277. 'name': 'Custom Link 6',
  278. 'enabled': False,
  279. 'link_text': 'Link 6',
  280. 'link_url': 'http://example.com/?6',
  281. },
  282. ]
  283. bulk_update_data = {
  284. 'new_window': True,
  285. 'enabled': False,
  286. }
  287. @classmethod
  288. def setUpTestData(cls):
  289. site_type = ObjectType.objects.get_for_model(Site)
  290. custom_links = (
  291. CustomLink(
  292. name='Custom Link 1',
  293. enabled=True,
  294. link_text='Link 1',
  295. link_url='http://example.com/?1',
  296. ),
  297. CustomLink(
  298. name='Custom Link 2',
  299. enabled=True,
  300. link_text='Link 2',
  301. link_url='http://example.com/?2',
  302. ),
  303. CustomLink(
  304. name='Custom Link 3',
  305. enabled=False,
  306. link_text='Link 3',
  307. link_url='http://example.com/?3',
  308. ),
  309. )
  310. CustomLink.objects.bulk_create(custom_links)
  311. for i, custom_link in enumerate(custom_links):
  312. custom_link.object_types.set([site_type])
  313. class SavedFilterTest(APIViewTestCases.APIViewTestCase):
  314. model = SavedFilter
  315. brief_fields = ['description', 'display', 'id', 'name', 'slug', 'url']
  316. create_data = [
  317. {
  318. 'object_types': ['dcim.site'],
  319. 'name': 'Saved Filter 4',
  320. 'slug': 'saved-filter-4',
  321. 'weight': 100,
  322. 'enabled': True,
  323. 'shared': True,
  324. 'parameters': {'status': ['active']},
  325. },
  326. {
  327. 'object_types': ['dcim.site'],
  328. 'name': 'Saved Filter 5',
  329. 'slug': 'saved-filter-5',
  330. 'weight': 200,
  331. 'enabled': True,
  332. 'shared': True,
  333. 'parameters': {'status': ['planned']},
  334. },
  335. {
  336. 'object_types': ['dcim.site'],
  337. 'name': 'Saved Filter 6',
  338. 'slug': 'saved-filter-6',
  339. 'weight': 300,
  340. 'enabled': True,
  341. 'shared': True,
  342. 'parameters': {'status': ['retired']},
  343. },
  344. ]
  345. bulk_update_data = {
  346. 'weight': 1000,
  347. 'enabled': False,
  348. 'shared': False,
  349. }
  350. @classmethod
  351. def setUpTestData(cls):
  352. site_type = ObjectType.objects.get_for_model(Site)
  353. saved_filters = (
  354. SavedFilter(
  355. name='Saved Filter 1',
  356. slug='saved-filter-1',
  357. weight=100,
  358. enabled=True,
  359. shared=True,
  360. parameters={'status': ['active']}
  361. ),
  362. SavedFilter(
  363. name='Saved Filter 2',
  364. slug='saved-filter-2',
  365. weight=200,
  366. enabled=True,
  367. shared=True,
  368. parameters={'status': ['planned']}
  369. ),
  370. SavedFilter(
  371. name='Saved Filter 3',
  372. slug='saved-filter-3',
  373. weight=300,
  374. enabled=True,
  375. shared=True,
  376. parameters={'status': ['retired']}
  377. ),
  378. )
  379. SavedFilter.objects.bulk_create(saved_filters)
  380. for i, savedfilter in enumerate(saved_filters):
  381. savedfilter.object_types.set([site_type])
  382. class BookmarkTest(
  383. APIViewTestCases.GetObjectViewTestCase,
  384. APIViewTestCases.ListObjectsViewTestCase,
  385. APIViewTestCases.CreateObjectViewTestCase,
  386. APIViewTestCases.DeleteObjectViewTestCase
  387. ):
  388. model = Bookmark
  389. brief_fields = ['display', 'id', 'object_id', 'object_type', 'url']
  390. @classmethod
  391. def setUpTestData(cls):
  392. sites = (
  393. Site(name='Site 1', slug='site-1'),
  394. Site(name='Site 2', slug='site-2'),
  395. Site(name='Site 3', slug='site-3'),
  396. Site(name='Site 4', slug='site-4'),
  397. Site(name='Site 5', slug='site-5'),
  398. Site(name='Site 6', slug='site-6'),
  399. )
  400. Site.objects.bulk_create(sites)
  401. def setUp(self):
  402. super().setUp()
  403. sites = Site.objects.all()
  404. bookmarks = (
  405. Bookmark(object=sites[0], user=self.user),
  406. Bookmark(object=sites[1], user=self.user),
  407. Bookmark(object=sites[2], user=self.user),
  408. )
  409. Bookmark.objects.bulk_create(bookmarks)
  410. self.create_data = [
  411. {
  412. 'object_type': 'dcim.site',
  413. 'object_id': sites[3].pk,
  414. 'user': self.user.pk,
  415. },
  416. {
  417. 'object_type': 'dcim.site',
  418. 'object_id': sites[4].pk,
  419. 'user': self.user.pk,
  420. },
  421. {
  422. 'object_type': 'dcim.site',
  423. 'object_id': sites[5].pk,
  424. 'user': self.user.pk,
  425. },
  426. ]
  427. class ExportTemplateTest(APIViewTestCases.APIViewTestCase):
  428. model = ExportTemplate
  429. brief_fields = ['description', 'display', 'id', 'name', 'url']
  430. create_data = [
  431. {
  432. 'object_types': ['dcim.device'],
  433. 'name': 'Test Export Template 4',
  434. 'template_code': '{% for obj in queryset %}{{ obj.name }}\n{% endfor %}',
  435. },
  436. {
  437. 'object_types': ['dcim.device'],
  438. 'name': 'Test Export Template 5',
  439. 'template_code': '{% for obj in queryset %}{{ obj.name }}\n{% endfor %}',
  440. },
  441. {
  442. 'object_types': ['dcim.device'],
  443. 'name': 'Test Export Template 6',
  444. 'template_code': '{% for obj in queryset %}{{ obj.name }}\n{% endfor %}',
  445. 'file_name': 'test_export_template_6',
  446. },
  447. ]
  448. bulk_update_data = {
  449. 'description': 'New description',
  450. }
  451. @classmethod
  452. def setUpTestData(cls):
  453. export_templates = (
  454. ExportTemplate(
  455. name='Export Template 1',
  456. template_code='{% for obj in queryset %}{{ obj.name }}\n{% endfor %}'
  457. ),
  458. ExportTemplate(
  459. name='Export Template 2',
  460. template_code='{% for obj in queryset %}{{ obj.name }}\n{% endfor %}',
  461. file_name='export_template_2',
  462. file_extension='test',
  463. ),
  464. ExportTemplate(
  465. name='Export Template 3',
  466. template_code='{% for obj in queryset %}{{ obj.name }}\n{% endfor %}'
  467. ),
  468. )
  469. ExportTemplate.objects.bulk_create(export_templates)
  470. device_object_type = ObjectType.objects.get_for_model(Device)
  471. for et in export_templates:
  472. et.object_types.set([device_object_type])
  473. class TagTest(APIViewTestCases.APIViewTestCase):
  474. model = Tag
  475. brief_fields = ['color', 'description', 'display', 'id', 'name', 'slug', 'url']
  476. create_data = [
  477. {
  478. 'name': 'Tag 4',
  479. 'slug': 'tag-4',
  480. 'weight': 1000,
  481. },
  482. {
  483. 'name': 'Tag 5',
  484. 'slug': 'tag-5',
  485. },
  486. {
  487. 'name': 'Tag 6',
  488. 'slug': 'tag-6',
  489. },
  490. ]
  491. bulk_update_data = {
  492. 'description': 'New description',
  493. }
  494. @classmethod
  495. def setUpTestData(cls):
  496. tags = (
  497. Tag(name='Tag 1', slug='tag-1'),
  498. Tag(name='Tag 2', slug='tag-2'),
  499. Tag(name='Tag 3', slug='tag-3', weight=26),
  500. )
  501. Tag.objects.bulk_create(tags)
  502. class TaggedItemTest(
  503. APIViewTestCases.GetObjectViewTestCase,
  504. APIViewTestCases.ListObjectsViewTestCase
  505. ):
  506. model = TaggedItem
  507. brief_fields = ['display', 'id', 'object', 'object_id', 'object_type', 'tag', 'url']
  508. @classmethod
  509. def setUpTestData(cls):
  510. tags = (
  511. Tag(name='Tag 1', slug='tag-1'),
  512. Tag(name='Tag 2', slug='tag-2'),
  513. Tag(name='Tag 3', slug='tag-3'),
  514. )
  515. Tag.objects.bulk_create(tags)
  516. sites = (
  517. Site(name='Site 1', slug='site-1'),
  518. Site(name='Site 2', slug='site-2'),
  519. Site(name='Site 3', slug='site-3'),
  520. )
  521. Site.objects.bulk_create(sites)
  522. sites[0].tags.set([tags[0], tags[1]])
  523. sites[1].tags.set([tags[1], tags[2]])
  524. sites[2].tags.set([tags[2], tags[0]])
  525. # TODO: Standardize to APIViewTestCase (needs create & update tests)
  526. class ImageAttachmentTest(
  527. APIViewTestCases.GetObjectViewTestCase,
  528. APIViewTestCases.ListObjectsViewTestCase,
  529. APIViewTestCases.DeleteObjectViewTestCase,
  530. APIViewTestCases.GraphQLTestCase
  531. ):
  532. model = ImageAttachment
  533. brief_fields = ['description', 'display', 'id', 'image', 'name', 'url']
  534. @classmethod
  535. def setUpTestData(cls):
  536. ct = ContentType.objects.get_for_model(Site)
  537. site = Site.objects.create(name='Site 1', slug='site-1')
  538. image_attachments = (
  539. ImageAttachment(
  540. object_type=ct,
  541. object_id=site.pk,
  542. name='Image Attachment 1',
  543. image='http://example.com/image1.png',
  544. image_height=100,
  545. image_width=100
  546. ),
  547. ImageAttachment(
  548. object_type=ct,
  549. object_id=site.pk,
  550. name='Image Attachment 2',
  551. image='http://example.com/image2.png',
  552. image_height=100,
  553. image_width=100
  554. ),
  555. ImageAttachment(
  556. object_type=ct,
  557. object_id=site.pk,
  558. name='Image Attachment 3',
  559. image='http://example.com/image3.png',
  560. image_height=100,
  561. image_width=100
  562. )
  563. )
  564. ImageAttachment.objects.bulk_create(image_attachments)
  565. class JournalEntryTest(APIViewTestCases.APIViewTestCase):
  566. model = JournalEntry
  567. brief_fields = ['created', 'display', 'id', 'url']
  568. bulk_update_data = {
  569. 'comments': 'Overwritten',
  570. }
  571. @classmethod
  572. def setUpTestData(cls):
  573. user = User.objects.first()
  574. site = Site.objects.create(name='Site 1', slug='site-1')
  575. journal_entries = (
  576. JournalEntry(
  577. created_by=user,
  578. assigned_object=site,
  579. comments='Fourth entry',
  580. ),
  581. JournalEntry(
  582. created_by=user,
  583. assigned_object=site,
  584. comments='Fifth entry',
  585. ),
  586. JournalEntry(
  587. created_by=user,
  588. assigned_object=site,
  589. comments='Sixth entry',
  590. ),
  591. )
  592. JournalEntry.objects.bulk_create(journal_entries)
  593. cls.create_data = [
  594. {
  595. 'assigned_object_type': 'dcim.site',
  596. 'assigned_object_id': site.pk,
  597. 'comments': 'First entry',
  598. },
  599. {
  600. 'assigned_object_type': 'dcim.site',
  601. 'assigned_object_id': site.pk,
  602. 'comments': 'Second entry',
  603. },
  604. {
  605. 'assigned_object_type': 'dcim.site',
  606. 'assigned_object_id': site.pk,
  607. 'comments': 'Third entry',
  608. },
  609. ]
  610. class ConfigContextProfileTest(APIViewTestCases.APIViewTestCase):
  611. model = ConfigContextProfile
  612. brief_fields = ['description', 'display', 'id', 'name', 'url']
  613. create_data = [
  614. {
  615. 'name': 'Config Context Profile 4',
  616. },
  617. {
  618. 'name': 'Config Context Profile 5',
  619. },
  620. {
  621. 'name': 'Config Context Profile 6',
  622. },
  623. ]
  624. bulk_update_data = {
  625. 'description': 'New description',
  626. }
  627. @classmethod
  628. def setUpTestData(cls):
  629. profiles = (
  630. ConfigContextProfile(
  631. name='Config Context Profile 1',
  632. schema={
  633. "properties": {
  634. "foo": {
  635. "type": "string"
  636. }
  637. },
  638. "required": [
  639. "foo"
  640. ]
  641. }
  642. ),
  643. ConfigContextProfile(
  644. name='Config Context Profile 2',
  645. schema={
  646. "properties": {
  647. "bar": {
  648. "type": "string"
  649. }
  650. },
  651. "required": [
  652. "bar"
  653. ]
  654. }
  655. ),
  656. ConfigContextProfile(
  657. name='Config Context Profile 3',
  658. schema={
  659. "properties": {
  660. "baz": {
  661. "type": "string"
  662. }
  663. },
  664. "required": [
  665. "baz"
  666. ]
  667. }
  668. ),
  669. )
  670. ConfigContextProfile.objects.bulk_create(profiles)
  671. def test_update_data_source_and_data_file(self):
  672. """
  673. Regression test: Ensure data_source and data_file can be assigned via the API.
  674. This specifically covers PATCHing a ConfigContext with integer IDs for both fields.
  675. """
  676. self.add_permissions(
  677. 'core.view_datafile',
  678. 'core.view_datasource',
  679. 'extras.view_configcontextprofile',
  680. 'extras.change_configcontextprofile',
  681. )
  682. config_context_profile = ConfigContextProfile.objects.first()
  683. # Create a data source and file
  684. datasource = DataSource.objects.create(
  685. name='Data Source 1',
  686. type='local',
  687. source_url='file:///tmp/netbox-datasource/',
  688. )
  689. # Generate a valid dummy YAML file
  690. file_data = b'profile: configcontext\n'
  691. datafile = DataFile.objects.create(
  692. source=datasource,
  693. path='dir1/file1.yml',
  694. last_updated=now(),
  695. size=len(file_data),
  696. hash=hashlib.sha256(file_data).hexdigest(),
  697. data=file_data,
  698. )
  699. url = self._get_detail_url(config_context_profile)
  700. payload = {
  701. 'data_source': datasource.pk,
  702. 'data_file': datafile.pk,
  703. }
  704. response = self.client.patch(url, payload, format='json', **self.header)
  705. self.assertHttpStatus(response, status.HTTP_200_OK)
  706. config_context_profile.refresh_from_db()
  707. self.assertEqual(config_context_profile.data_source_id, datasource.pk)
  708. self.assertEqual(config_context_profile.data_file_id, datafile.pk)
  709. self.assertEqual(response.data['data_source']['id'], datasource.pk)
  710. self.assertEqual(response.data['data_file']['id'], datafile.pk)
  711. class ConfigContextTest(APIViewTestCases.APIViewTestCase):
  712. model = ConfigContext
  713. brief_fields = ['description', 'display', 'id', 'name', 'url']
  714. create_data = [
  715. {
  716. 'name': 'Config Context 4',
  717. 'data': {'more_foo': True},
  718. },
  719. {
  720. 'name': 'Config Context 5',
  721. 'data': {'more_bar': False},
  722. },
  723. {
  724. 'name': 'Config Context 6',
  725. 'data': {'more_baz': None},
  726. },
  727. ]
  728. bulk_update_data = {
  729. 'description': 'New description',
  730. }
  731. @classmethod
  732. def setUpTestData(cls):
  733. config_contexts = (
  734. ConfigContext(name='Config Context 1', weight=100, data={'foo': 123}),
  735. ConfigContext(name='Config Context 2', weight=200, data={'bar': 456}),
  736. ConfigContext(name='Config Context 3', weight=300, data={'baz': 789}),
  737. )
  738. ConfigContext.objects.bulk_create(config_contexts)
  739. def test_render_configcontext_for_object(self):
  740. """
  741. Test rendering config context data for a device.
  742. """
  743. manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
  744. devicetype = DeviceType.objects.create(manufacturer=manufacturer, model='Device Type 1', slug='device-type-1')
  745. role = DeviceRole.objects.create(name='Device Role 1', slug='device-role-1')
  746. site = Site.objects.create(name='Site-1', slug='site-1')
  747. device = Device.objects.create(name='Device 1', device_type=devicetype, role=role, site=site)
  748. # Test default config contexts (created at test setup)
  749. rendered_context = device.get_config_context()
  750. self.assertEqual(rendered_context['foo'], 123)
  751. self.assertEqual(rendered_context['bar'], 456)
  752. self.assertEqual(rendered_context['baz'], 789)
  753. # Add another context specific to the site
  754. configcontext4 = ConfigContext(
  755. name='Config Context 4',
  756. data={'site_data': 'ABC'}
  757. )
  758. configcontext4.save()
  759. configcontext4.sites.add(site)
  760. rendered_context = device.get_config_context()
  761. self.assertEqual(rendered_context['site_data'], 'ABC')
  762. # Override one of the default contexts
  763. configcontext5 = ConfigContext(
  764. name='Config Context 5',
  765. weight=2000,
  766. data={'foo': 999}
  767. )
  768. configcontext5.save()
  769. configcontext5.sites.add(site)
  770. rendered_context = device.get_config_context()
  771. self.assertEqual(rendered_context['foo'], 999)
  772. # Add a context which does NOT match our device and ensure it does not apply
  773. site2 = Site.objects.create(name='Site 2', slug='site-2')
  774. configcontext6 = ConfigContext(
  775. name='Config Context 6',
  776. weight=2000,
  777. data={'bar': 999}
  778. )
  779. configcontext6.save()
  780. configcontext6.sites.add(site2)
  781. rendered_context = device.get_config_context()
  782. self.assertEqual(rendered_context['bar'], 456)
  783. def test_update_data_source_and_data_file(self):
  784. """
  785. Regression test: Ensure data_source and data_file can be assigned via the API.
  786. This specifically covers PATCHing a ConfigContext with integer IDs for both fields.
  787. """
  788. self.add_permissions(
  789. 'core.view_datafile',
  790. 'core.view_datasource',
  791. 'extras.view_configcontext',
  792. 'extras.change_configcontext',
  793. )
  794. config_context = ConfigContext.objects.first()
  795. # Create a data source and file
  796. datasource = DataSource.objects.create(
  797. name='Data Source 1',
  798. type='local',
  799. source_url='file:///tmp/netbox-datasource/',
  800. )
  801. # Generate a valid dummy YAML file
  802. file_data = b'context: config\n'
  803. datafile = DataFile.objects.create(
  804. source=datasource,
  805. path='dir1/file1.yml',
  806. last_updated=now(),
  807. size=len(file_data),
  808. hash=hashlib.sha256(file_data).hexdigest(),
  809. data=file_data,
  810. )
  811. url = self._get_detail_url(config_context)
  812. payload = {
  813. 'data_source': datasource.pk,
  814. 'data_file': datafile.pk,
  815. }
  816. response = self.client.patch(url, payload, format='json', **self.header)
  817. self.assertHttpStatus(response, status.HTTP_200_OK)
  818. config_context.refresh_from_db()
  819. self.assertEqual(config_context.data_source_id, datasource.pk)
  820. self.assertEqual(config_context.data_file_id, datafile.pk)
  821. self.assertEqual(response.data['data_source']['id'], datasource.pk)
  822. self.assertEqual(response.data['data_file']['id'], datafile.pk)
  823. class ConfigTemplateTest(APIViewTestCases.APIViewTestCase):
  824. model = ConfigTemplate
  825. brief_fields = ['description', 'display', 'id', 'name', 'url']
  826. create_data = [
  827. {
  828. 'name': 'Config Template 4',
  829. 'template_code': 'Foo: {{ foo }}',
  830. 'mime_type': 'text/plain',
  831. 'file_name': 'output4',
  832. 'file_extension': 'txt',
  833. 'as_attachment': True,
  834. },
  835. {
  836. 'name': 'Config Template 5',
  837. 'template_code': 'Bar: {{ bar }}',
  838. },
  839. {
  840. 'name': 'Config Template 6',
  841. 'template_code': 'Baz: {{ baz }}',
  842. },
  843. ]
  844. bulk_update_data = {
  845. 'description': 'New description',
  846. }
  847. @classmethod
  848. def setUpTestData(cls):
  849. config_templates = (
  850. ConfigTemplate(
  851. name='Config Template 1',
  852. template_code='Foo: {{ foo }}'
  853. ),
  854. ConfigTemplate(
  855. name='Config Template 2',
  856. template_code='Bar: {{ bar }}',
  857. ),
  858. ConfigTemplate(
  859. name='Config Template 3',
  860. template_code='Baz: {{ baz }}'
  861. ),
  862. )
  863. ConfigTemplate.objects.bulk_create(config_templates)
  864. def test_render(self):
  865. configtemplate = ConfigTemplate.objects.first()
  866. self.add_permissions('extras.render_configtemplate', 'extras.view_configtemplate')
  867. url = reverse('extras-api:configtemplate-render', kwargs={'pk': configtemplate.pk})
  868. response = self.client.post(url, {'foo': 'bar'}, format='json', **self.header)
  869. self.assertHttpStatus(response, status.HTTP_200_OK)
  870. self.assertEqual(response.data['content'], 'Foo: bar')
  871. def test_render_without_permission(self):
  872. configtemplate = ConfigTemplate.objects.first()
  873. # No permissions added - user has no render permission
  874. url = reverse('extras-api:configtemplate-render', kwargs={'pk': configtemplate.pk})
  875. response = self.client.post(url, {'foo': 'bar'}, format='json', **self.header)
  876. self.assertHttpStatus(response, status.HTTP_404_NOT_FOUND)
  877. def test_render_token_write_enabled(self):
  878. configtemplate = ConfigTemplate.objects.first()
  879. self.add_permissions('extras.render_configtemplate', 'extras.view_configtemplate')
  880. url = reverse('extras-api:configtemplate-render', kwargs={'pk': configtemplate.pk})
  881. # Request without token auth should fail with PermissionDenied
  882. response = self.client.post(url, {'foo': 'bar'}, format='json')
  883. self.assertHttpStatus(response, status.HTTP_403_FORBIDDEN)
  884. # Create token with write_enabled=False
  885. token = Token.objects.create(version=2, user=self.user, write_enabled=False)
  886. token_header = f'Bearer {TOKEN_PREFIX}{token.key}.{token.token}'
  887. # Request with write-disabled token should fail
  888. response = self.client.post(url, {'foo': 'bar'}, format='json', HTTP_AUTHORIZATION=token_header)
  889. self.assertHttpStatus(response, status.HTTP_403_FORBIDDEN)
  890. # Enable write and retry
  891. token.write_enabled = True
  892. token.save()
  893. response = self.client.post(url, {'foo': 'bar'}, format='json', HTTP_AUTHORIZATION=token_header)
  894. self.assertHttpStatus(response, status.HTTP_200_OK)
  895. class ScriptTest(APITestCase):
  896. class TestScriptClass(PythonClass):
  897. class Meta:
  898. name = 'Test script'
  899. commit = True
  900. scheduling_enabled = True
  901. var1 = StringVar()
  902. var2 = IntegerVar()
  903. var3 = BooleanVar()
  904. def run(self, data, commit=True):
  905. self.log_info(data['var1'])
  906. self.log_success(data['var2'])
  907. self.log_failure(data['var3'])
  908. return 'Script complete'
  909. @classmethod
  910. def setUpTestData(cls):
  911. module = ScriptModule.objects.create(
  912. file_root=ManagedFileRootPathChoices.SCRIPTS,
  913. file_path='script.py',
  914. )
  915. script = Script.objects.create(
  916. module=module,
  917. name='Test script',
  918. is_executable=True,
  919. )
  920. cls.url = reverse('extras-api:script-detail', kwargs={'pk': script.pk})
  921. @property
  922. def python_class(self):
  923. return self.TestScriptClass
  924. def setUp(self):
  925. super().setUp()
  926. self.add_permissions('extras.view_script')
  927. # Monkey-patch the Script model to return our TestScriptClass above
  928. Script.python_class = self.python_class
  929. def test_get_script(self):
  930. response = self.client.get(self.url, **self.header)
  931. self.assertEqual(response.data['name'], self.TestScriptClass.Meta.name)
  932. self.assertEqual(response.data['vars']['var1'], 'StringVar')
  933. self.assertEqual(response.data['vars']['var2'], 'IntegerVar')
  934. self.assertEqual(response.data['vars']['var3'], 'BooleanVar')
  935. def test_schedule_script_past_time_rejected(self):
  936. """
  937. Scheduling with past schedule_at should fail.
  938. """
  939. self.add_permissions('extras.run_script')
  940. payload = {
  941. 'data': {'var1': 'hello', 'var2': 1, 'var3': False},
  942. 'commit': True,
  943. 'schedule_at': now() - datetime.timedelta(hours=1),
  944. }
  945. response = self.client.post(self.url, payload, format='json', **self.header)
  946. self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
  947. self.assertIn('schedule_at', response.data)
  948. # Be tolerant of exact wording but ensure we failed on schedule_at being in the past
  949. self.assertIn('future', str(response.data['schedule_at']).lower())
  950. def test_schedule_script_interval_only(self):
  951. """
  952. Interval without schedule_at should auto-set schedule_at now.
  953. """
  954. self.add_permissions('extras.run_script')
  955. payload = {
  956. 'data': {'var1': 'hello', 'var2': 1, 'var3': False},
  957. 'commit': True,
  958. 'interval': 60,
  959. }
  960. response = self.client.post(self.url, payload, format='json', **self.header)
  961. self.assertHttpStatus(response, status.HTTP_200_OK)
  962. # The latest job is returned in the script detail serializer under "result"
  963. self.assertIn('result', response.data)
  964. self.assertEqual(response.data['result']['interval'], 60)
  965. # Ensure a start time was autopopulated
  966. self.assertIsNotNone(response.data['result']['scheduled'])
  967. def test_schedule_script_when_disabled(self):
  968. """
  969. Scheduling should fail when script.scheduling_enabled=False.
  970. """
  971. self.add_permissions('extras.run_script')
  972. # Temporarily disable scheduling on the in-test Python class
  973. original = getattr(self.TestScriptClass.Meta, 'scheduling_enabled', True)
  974. self.TestScriptClass.Meta.scheduling_enabled = False
  975. base = {
  976. 'data': {'var1': 'hello', 'var2': 1, 'var3': False},
  977. 'commit': True,
  978. }
  979. # Check both schedule_at and interval paths
  980. cases = [
  981. {**base, 'schedule_at': now() + datetime.timedelta(minutes=5)},
  982. {**base, 'interval': 60},
  983. ]
  984. try:
  985. for case in cases:
  986. with self.subTest(case=list(case.keys())):
  987. response = self.client.post(self.url, case, format='json', **self.header)
  988. self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
  989. # Error should be attached to whichever field we used
  990. key = 'schedule_at' if 'schedule_at' in case else 'interval'
  991. self.assertIn(key, response.data)
  992. self.assertIn('scheduling is not enabled', str(response.data[key]).lower())
  993. finally:
  994. # Restore the original setting for other tests
  995. self.TestScriptClass.Meta.scheduling_enabled = original
  996. class CreatedUpdatedFilterTest(APITestCase):
  997. @classmethod
  998. def setUpTestData(cls):
  999. site1 = Site.objects.create(name='Site 1', slug='site-1')
  1000. location1 = Location.objects.create(site=site1, name='Location 1', slug='location-1')
  1001. rackrole1 = RackRole.objects.create(name='Rack Role 1', slug='rack-role-1', color='ff0000')
  1002. racks = (
  1003. Rack(site=site1, location=location1, role=rackrole1, name='Rack 1', u_height=42),
  1004. Rack(site=site1, location=location1, role=rackrole1, name='Rack 2', u_height=42)
  1005. )
  1006. Rack.objects.bulk_create(racks)
  1007. # Change the created and last_updated of the second rack
  1008. Rack.objects.filter(pk=racks[1].pk).update(
  1009. last_updated=make_aware(datetime.datetime(2001, 2, 3, 1, 2, 3, 4)),
  1010. created=make_aware(datetime.datetime(2001, 2, 3))
  1011. )
  1012. def test_get_rack_created(self):
  1013. rack2 = Rack.objects.get(name='Rack 2')
  1014. self.add_permissions('dcim.view_rack')
  1015. url = reverse('dcim-api:rack-list')
  1016. response = self.client.get('{}?created=2001-02-03'.format(url), **self.header)
  1017. self.assertEqual(response.data['count'], 1)
  1018. self.assertEqual(response.data['results'][0]['id'], rack2.pk)
  1019. def test_get_rack_created_gte(self):
  1020. rack1 = Rack.objects.get(name='Rack 1')
  1021. self.add_permissions('dcim.view_rack')
  1022. url = reverse('dcim-api:rack-list')
  1023. response = self.client.get('{}?created__gte=2001-02-04'.format(url), **self.header)
  1024. self.assertEqual(response.data['count'], 1)
  1025. self.assertEqual(response.data['results'][0]['id'], rack1.pk)
  1026. def test_get_rack_created_lte(self):
  1027. rack2 = Rack.objects.get(name='Rack 2')
  1028. self.add_permissions('dcim.view_rack')
  1029. url = reverse('dcim-api:rack-list')
  1030. response = self.client.get('{}?created__lte=2001-02-04'.format(url), **self.header)
  1031. self.assertEqual(response.data['count'], 1)
  1032. self.assertEqual(response.data['results'][0]['id'], rack2.pk)
  1033. def test_get_rack_last_updated(self):
  1034. rack2 = Rack.objects.get(name='Rack 2')
  1035. self.add_permissions('dcim.view_rack')
  1036. url = reverse('dcim-api:rack-list')
  1037. response = self.client.get('{}?last_updated=2001-02-03%2001:02:03.000004'.format(url), **self.header)
  1038. self.assertEqual(response.data['count'], 1)
  1039. self.assertEqual(response.data['results'][0]['id'], rack2.pk)
  1040. def test_get_rack_last_updated_gte(self):
  1041. rack1 = Rack.objects.get(name='Rack 1')
  1042. self.add_permissions('dcim.view_rack')
  1043. url = reverse('dcim-api:rack-list')
  1044. response = self.client.get('{}?last_updated__gte=2001-02-04%2001:02:03.000004'.format(url), **self.header)
  1045. self.assertEqual(response.data['count'], 1)
  1046. self.assertEqual(response.data['results'][0]['id'], rack1.pk)
  1047. def test_get_rack_last_updated_lte(self):
  1048. rack2 = Rack.objects.get(name='Rack 2')
  1049. self.add_permissions('dcim.view_rack')
  1050. url = reverse('dcim-api:rack-list')
  1051. response = self.client.get('{}?last_updated__lte=2001-02-04%2001:02:03.000004'.format(url), **self.header)
  1052. self.assertEqual(response.data['count'], 1)
  1053. self.assertEqual(response.data['results'][0]['id'], rack2.pk)
  1054. class SubscriptionTest(APIViewTestCases.APIViewTestCase):
  1055. model = Subscription
  1056. brief_fields = ['display', 'id', 'object_id', 'object_type', 'url', 'user']
  1057. @classmethod
  1058. def setUpTestData(cls):
  1059. users = (
  1060. User(username='User 1'),
  1061. User(username='User 2'),
  1062. User(username='User 3'),
  1063. User(username='User 4'),
  1064. )
  1065. User.objects.bulk_create(users)
  1066. sites = (
  1067. Site(name='Site 1', slug='site-1'),
  1068. Site(name='Site 2', slug='site-2'),
  1069. Site(name='Site 3', slug='site-3'),
  1070. )
  1071. Site.objects.bulk_create(sites)
  1072. subscriptions = (
  1073. Subscription(
  1074. object=sites[0],
  1075. user=users[0],
  1076. ),
  1077. Subscription(
  1078. object=sites[1],
  1079. user=users[1],
  1080. ),
  1081. Subscription(
  1082. object=sites[2],
  1083. user=users[2],
  1084. ),
  1085. )
  1086. Subscription.objects.bulk_create(subscriptions)
  1087. cls.create_data = [
  1088. {
  1089. 'object_type': 'dcim.site',
  1090. 'object_id': sites[0].pk,
  1091. 'user': users[3].pk,
  1092. },
  1093. {
  1094. 'object_type': 'dcim.site',
  1095. 'object_id': sites[1].pk,
  1096. 'user': users[3].pk,
  1097. },
  1098. {
  1099. 'object_type': 'dcim.site',
  1100. 'object_id': sites[2].pk,
  1101. 'user': users[3].pk,
  1102. },
  1103. ]
  1104. cls.bulk_update_data = {
  1105. 'user': users[3].pk,
  1106. }
  1107. class NotificationGroupTest(APIViewTestCases.APIViewTestCase):
  1108. model = NotificationGroup
  1109. brief_fields = ['description', 'display', 'id', 'name', 'url']
  1110. create_data = [
  1111. {
  1112. 'object_types': ['dcim.site'],
  1113. 'name': 'Custom Link 4',
  1114. 'enabled': True,
  1115. 'link_text': 'Link 4',
  1116. 'link_url': 'http://example.com/?4',
  1117. },
  1118. {
  1119. 'object_types': ['dcim.site'],
  1120. 'name': 'Custom Link 5',
  1121. 'enabled': True,
  1122. 'link_text': 'Link 5',
  1123. 'link_url': 'http://example.com/?5',
  1124. },
  1125. {
  1126. 'object_types': ['dcim.site'],
  1127. 'name': 'Custom Link 6',
  1128. 'enabled': False,
  1129. 'link_text': 'Link 6',
  1130. 'link_url': 'http://example.com/?6',
  1131. },
  1132. ]
  1133. bulk_update_data = {
  1134. 'description': 'New description',
  1135. }
  1136. @classmethod
  1137. def setUpTestData(cls):
  1138. users = (
  1139. User(username='User 1'),
  1140. User(username='User 2'),
  1141. User(username='User 3'),
  1142. )
  1143. User.objects.bulk_create(users)
  1144. groups = (
  1145. Group(name='Group 1'),
  1146. Group(name='Group 2'),
  1147. Group(name='Group 3'),
  1148. )
  1149. Group.objects.bulk_create(groups)
  1150. notification_groups = (
  1151. NotificationGroup(name='Notification Group 1'),
  1152. NotificationGroup(name='Notification Group 2'),
  1153. NotificationGroup(name='Notification Group 3'),
  1154. )
  1155. NotificationGroup.objects.bulk_create(notification_groups)
  1156. for i, notification_group in enumerate(notification_groups):
  1157. notification_group.users.add(users[i])
  1158. notification_group.groups.add(groups[i])
  1159. cls.create_data = [
  1160. {
  1161. 'name': 'Notification Group 4',
  1162. 'description': 'Foo',
  1163. 'users': [users[0].pk],
  1164. 'groups': [groups[0].pk],
  1165. },
  1166. {
  1167. 'name': 'Notification Group 5',
  1168. 'description': 'Bar',
  1169. 'users': [users[1].pk],
  1170. 'groups': [groups[1].pk],
  1171. },
  1172. {
  1173. 'name': 'Notification Group 6',
  1174. 'description': 'Baz',
  1175. 'users': [users[2].pk],
  1176. 'groups': [groups[2].pk],
  1177. },
  1178. ]
  1179. class NotificationTest(APIViewTestCases.APIViewTestCase):
  1180. model = Notification
  1181. brief_fields = ['display', 'event_type', 'id', 'object_id', 'object_type', 'read', 'url', 'user']
  1182. bulk_update_data = {
  1183. 'read': now(),
  1184. }
  1185. @classmethod
  1186. def setUpTestData(cls):
  1187. users = (
  1188. User(username='User 1'),
  1189. User(username='User 2'),
  1190. User(username='User 3'),
  1191. User(username='User 4'),
  1192. )
  1193. User.objects.bulk_create(users)
  1194. sites = (
  1195. Site(name='Site 1', slug='site-1'),
  1196. Site(name='Site 2', slug='site-2'),
  1197. Site(name='Site 3', slug='site-3'),
  1198. )
  1199. Site.objects.bulk_create(sites)
  1200. notifications = (
  1201. Notification(
  1202. object=sites[0],
  1203. event_type=OBJECT_CREATED,
  1204. user=users[0],
  1205. ),
  1206. Notification(
  1207. object=sites[1],
  1208. event_type=OBJECT_UPDATED,
  1209. user=users[1],
  1210. ),
  1211. Notification(
  1212. object=sites[2],
  1213. event_type=OBJECT_DELETED,
  1214. user=users[2],
  1215. ),
  1216. )
  1217. Notification.objects.bulk_create(notifications)
  1218. cls.create_data = [
  1219. {
  1220. 'object_type': 'dcim.site',
  1221. 'object_id': sites[0].pk,
  1222. 'user': users[3].pk,
  1223. 'event_type': OBJECT_CREATED,
  1224. },
  1225. {
  1226. 'object_type': 'dcim.site',
  1227. 'object_id': sites[1].pk,
  1228. 'user': users[3].pk,
  1229. 'event_type': OBJECT_UPDATED,
  1230. },
  1231. {
  1232. 'object_type': 'dcim.site',
  1233. 'object_id': sites[2].pk,
  1234. 'user': users[3].pk,
  1235. 'event_type': OBJECT_DELETED,
  1236. },
  1237. ]
  1238. class ScriptModuleTest(APITestCase):
  1239. """
  1240. Tests for the POST /api/extras/scripts/upload/ endpoint.
  1241. ScriptModule is a proxy of core.ManagedFile (a different app) so the standard
  1242. APIViewTestCases mixins cannot be used directly. All tests use add_permissions()
  1243. with explicit Django model-level permissions.
  1244. """
  1245. def setUp(self):
  1246. super().setUp()
  1247. self.url = reverse('extras-api:scriptmodule-list') # /api/extras/scripts/upload/
  1248. def test_upload_script_module_without_permission(self):
  1249. script_content = b"from extras.scripts import Script\nclass TestScript(Script):\n pass\n"
  1250. upload_file = SimpleUploadedFile('test_upload.py', script_content, content_type='text/plain')
  1251. response = self.client.post(
  1252. self.url,
  1253. {'file': upload_file},
  1254. format='multipart',
  1255. **self.header,
  1256. )
  1257. self.assertHttpStatus(response, status.HTTP_403_FORBIDDEN)
  1258. def test_upload_script_module(self):
  1259. # ScriptModule is a proxy of core.ManagedFile; both permissions required.
  1260. self.add_permissions('extras.add_scriptmodule', 'core.add_managedfile')
  1261. script_content = b"from extras.scripts import Script\nclass TestScript(Script):\n pass\n"
  1262. upload_file = SimpleUploadedFile('test_upload.py', script_content, content_type='text/plain')
  1263. mock_storage = MagicMock()
  1264. mock_storage.save.return_value = 'test_upload.py'
  1265. with patch('extras.api.serializers_.scripts.storages') as mock_storages:
  1266. mock_storages.create_storage.return_value = mock_storage
  1267. mock_storages.backends = {'scripts': {}}
  1268. response = self.client.post(
  1269. self.url,
  1270. {'file': upload_file},
  1271. format='multipart',
  1272. **self.header,
  1273. )
  1274. self.assertHttpStatus(response, status.HTTP_201_CREATED)
  1275. self.assertEqual(response.data['file_path'], 'test_upload.py')
  1276. mock_storage.save.assert_called_once()
  1277. self.assertTrue(ScriptModule.objects.filter(file_path='test_upload.py').exists())
  1278. def test_upload_script_module_without_file_fails(self):
  1279. self.add_permissions('extras.add_scriptmodule', 'core.add_managedfile')
  1280. response = self.client.post(self.url, {}, format='json', **self.header)
  1281. self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)