test_app.py 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650
  1. # -*- coding: utf-8 -*-
  2. # Import Python Libs
  3. from __future__ import absolute_import, print_function, unicode_literals
  4. import time
  5. import threading
  6. # Import Salt Libs
  7. import salt.utils.json
  8. import salt.utils.stringutils
  9. from salt.netapi.rest_tornado import saltnado
  10. from salt.utils.versions import StrictVersion
  11. # Import Salt Testing Libs
  12. from tests.unit.netapi.test_rest_tornado import SaltnadoTestCase
  13. from tests.support.helpers import flaky
  14. from tests.support.unit import skipIf
  15. # Import 3rd-party libs
  16. from salt.ext import six
  17. from salt.utils.zeromq import zmq, ZMQDefaultLoop as ZMQIOLoop
  18. HAS_ZMQ_IOLOOP = bool(zmq)
  19. class _SaltnadoIntegrationTestCase(SaltnadoTestCase): # pylint: disable=abstract-method
  20. @property
  21. def opts(self):
  22. return self.get_config('client_config', from_scratch=True)
  23. @property
  24. def mod_opts(self):
  25. return self.get_config('minion', from_scratch=True)
  26. @skipIf(HAS_ZMQ_IOLOOP is False, 'PyZMQ version must be >= 14.0.1 to run these tests.')
  27. @skipIf(StrictVersion(zmq.__version__) < StrictVersion('14.0.1'), 'PyZMQ must be >= 14.0.1 to run these tests.')
  28. class TestSaltAPIHandler(_SaltnadoIntegrationTestCase):
  29. def get_app(self):
  30. urls = [('/', saltnado.SaltAPIHandler)]
  31. application = self.build_tornado_app(urls)
  32. application.event_listener = saltnado.EventListener({}, self.opts)
  33. self.application = application
  34. return application
  35. def test_root(self):
  36. '''
  37. Test the root path which returns the list of clients we support
  38. '''
  39. response = self.fetch('/',
  40. connect_timeout=30,
  41. request_timeout=30,
  42. )
  43. self.assertEqual(response.code, 200)
  44. response_obj = salt.utils.json.loads(response.body)
  45. self.assertEqual(sorted(response_obj['clients']),
  46. ['local', 'local_async', 'runner', 'runner_async'])
  47. self.assertEqual(response_obj['return'], 'Welcome')
  48. def test_post_no_auth(self):
  49. '''
  50. Test post with no auth token, should 401
  51. '''
  52. # get a token for this test
  53. low = [{'client': 'local',
  54. 'tgt': '*',
  55. 'fun': 'test.ping',
  56. }]
  57. response = self.fetch('/',
  58. method='POST',
  59. body=salt.utils.json.dumps(low),
  60. headers={'Content-Type': self.content_type_map['json']},
  61. follow_redirects=False,
  62. connect_timeout=30,
  63. request_timeout=30,
  64. )
  65. self.assertEqual(response.code, 302)
  66. self.assertEqual(response.headers['Location'], '/login')
  67. # Local client tests
  68. @skipIf(True, 'to be re-enabled when #23623 is merged')
  69. def test_simple_local_post(self):
  70. '''
  71. Test a basic API of /
  72. '''
  73. low = [{'client': 'local',
  74. 'tgt': '*',
  75. 'fun': 'test.ping',
  76. }]
  77. response = self.fetch('/',
  78. method='POST',
  79. body=salt.utils.json.dumps(low),
  80. headers={'Content-Type': self.content_type_map['json'],
  81. saltnado.AUTH_TOKEN_HEADER: self.token['token']},
  82. connect_timeout=30,
  83. request_timeout=30,
  84. )
  85. response_obj = salt.utils.json.loads(response.body)
  86. self.assertEqual(len(response_obj['return']), 1)
  87. # If --proxy is set, it will cause an extra minion_id to be in the
  88. # response. Since there's not a great way to know if the test
  89. # runner's proxy minion is running, and we're not testing proxy
  90. # minions here anyway, just remove it from the response.
  91. response_obj['return'][0].pop('proxytest', None)
  92. self.assertEqual(response_obj['return'][0], {'minion': True, 'sub_minion': True})
  93. def test_simple_local_post_no_tgt(self):
  94. '''
  95. POST job with invalid tgt
  96. '''
  97. low = [{'client': 'local',
  98. 'tgt': 'minion_we_dont_have',
  99. 'fun': 'test.ping',
  100. }]
  101. response = self.fetch('/',
  102. method='POST',
  103. body=salt.utils.json.dumps(low),
  104. headers={'Content-Type': self.content_type_map['json'],
  105. saltnado.AUTH_TOKEN_HEADER: self.token['token']},
  106. connect_timeout=30,
  107. request_timeout=30,
  108. )
  109. response_obj = salt.utils.json.loads(response.body)
  110. self.assertEqual(response_obj['return'], ["No minions matched the target. No command was sent, no jid was assigned."])
  111. # local client request body test
  112. @skipIf(True, 'Undetermined race condition in test. Temporarily disabled.')
  113. def test_simple_local_post_only_dictionary_request(self):
  114. '''
  115. Test a basic API of /
  116. '''
  117. low = {'client': 'local',
  118. 'tgt': '*',
  119. 'fun': 'test.ping',
  120. }
  121. response = self.fetch('/',
  122. method='POST',
  123. body=salt.utils.json.dumps(low),
  124. headers={'Content-Type': self.content_type_map['json'],
  125. saltnado.AUTH_TOKEN_HEADER: self.token['token']},
  126. connect_timeout=30,
  127. request_timeout=30,
  128. )
  129. response_obj = salt.utils.json.loads(response.body)
  130. self.assertEqual(len(response_obj['return']), 1)
  131. # If --proxy is set, it will cause an extra minion_id to be in the
  132. # response. Since there's not a great way to know if the test
  133. # runner's proxy minion is running, and we're not testing proxy
  134. # minions here anyway, just remove it from the response.
  135. response_obj['return'][0].pop('proxytest', None)
  136. self.assertEqual(response_obj['return'][0], {'minion': True, 'sub_minion': True})
  137. def test_simple_local_post_invalid_request(self):
  138. '''
  139. Test a basic API of /
  140. '''
  141. low = ["invalid request"]
  142. response = self.fetch('/',
  143. method='POST',
  144. body=salt.utils.json.dumps(low),
  145. headers={'Content-Type': self.content_type_map['json'],
  146. saltnado.AUTH_TOKEN_HEADER: self.token['token']},
  147. connect_timeout=30,
  148. request_timeout=30,
  149. )
  150. self.assertEqual(response.code, 400)
  151. # local_async tests
  152. def test_simple_local_async_post(self):
  153. low = [{'client': 'local_async',
  154. 'tgt': '*',
  155. 'fun': 'test.ping',
  156. }]
  157. response = self.fetch('/',
  158. method='POST',
  159. body=salt.utils.json.dumps(low),
  160. headers={'Content-Type': self.content_type_map['json'],
  161. saltnado.AUTH_TOKEN_HEADER: self.token['token']},
  162. )
  163. response_obj = salt.utils.json.loads(response.body)
  164. ret = response_obj['return']
  165. ret[0]['minions'] = sorted(ret[0]['minions'])
  166. try:
  167. # If --proxy is set, it will cause an extra minion_id to be in the
  168. # response. Since there's not a great way to know if the test
  169. # runner's proxy minion is running, and we're not testing proxy
  170. # minions here anyway, just remove it from the response.
  171. ret[0]['minions'].remove('proxytest')
  172. except ValueError:
  173. pass
  174. # TODO: verify pub function? Maybe look at how we test the publisher
  175. self.assertEqual(len(ret), 1)
  176. self.assertIn('jid', ret[0])
  177. self.assertEqual(ret[0]['minions'], sorted(['minion', 'sub_minion']))
  178. def test_multi_local_async_post(self):
  179. low = [{'client': 'local_async',
  180. 'tgt': '*',
  181. 'fun': 'test.ping',
  182. },
  183. {'client': 'local_async',
  184. 'tgt': '*',
  185. 'fun': 'test.ping',
  186. }]
  187. response = self.fetch('/',
  188. method='POST',
  189. body=salt.utils.json.dumps(low),
  190. headers={'Content-Type': self.content_type_map['json'],
  191. saltnado.AUTH_TOKEN_HEADER: self.token['token']},
  192. )
  193. response_obj = salt.utils.json.loads(response.body)
  194. ret = response_obj['return']
  195. ret[0]['minions'] = sorted(ret[0]['minions'])
  196. ret[1]['minions'] = sorted(ret[1]['minions'])
  197. try:
  198. # If --proxy is set, it will cause an extra minion_id to be in the
  199. # response. Since there's not a great way to know if the test
  200. # runner's proxy minion is running, and we're not testing proxy
  201. # minions here anyway, just remove it from the response.
  202. ret[0]['minions'].remove('proxytest')
  203. ret[1]['minions'].remove('proxytest')
  204. except ValueError:
  205. pass
  206. self.assertEqual(len(ret), 2)
  207. self.assertIn('jid', ret[0])
  208. self.assertIn('jid', ret[1])
  209. self.assertEqual(ret[0]['minions'], sorted(['minion', 'sub_minion']))
  210. self.assertEqual(ret[1]['minions'], sorted(['minion', 'sub_minion']))
  211. def test_multi_local_async_post_multitoken(self):
  212. low = [{'client': 'local_async',
  213. 'tgt': '*',
  214. 'fun': 'test.ping',
  215. },
  216. {'client': 'local_async',
  217. 'tgt': '*',
  218. 'fun': 'test.ping',
  219. 'token': self.token['token'], # send a different (but still valid token)
  220. },
  221. {'client': 'local_async',
  222. 'tgt': '*',
  223. 'fun': 'test.ping',
  224. 'token': 'BAD_TOKEN', # send a bad token
  225. },
  226. ]
  227. response = self.fetch('/',
  228. method='POST',
  229. body=salt.utils.json.dumps(low),
  230. headers={'Content-Type': self.content_type_map['json'],
  231. saltnado.AUTH_TOKEN_HEADER: self.token['token']},
  232. )
  233. response_obj = salt.utils.json.loads(response.body)
  234. ret = response_obj['return']
  235. ret[0]['minions'] = sorted(ret[0]['minions'])
  236. ret[1]['minions'] = sorted(ret[1]['minions'])
  237. try:
  238. # If --proxy is set, it will cause an extra minion_id to be in the
  239. # response. Since there's not a great way to know if the test
  240. # runner's proxy minion is running, and we're not testing proxy
  241. # minions here anyway, just remove it from the response.
  242. ret[0]['minions'].remove('proxytest')
  243. ret[1]['minions'].remove('proxytest')
  244. except ValueError:
  245. pass
  246. self.assertEqual(len(ret), 3) # make sure we got 3 responses
  247. self.assertIn('jid', ret[0]) # the first 2 are regular returns
  248. self.assertIn('jid', ret[1])
  249. self.assertIn('Failed to authenticate', ret[2]) # bad auth
  250. self.assertEqual(ret[0]['minions'], sorted(['minion', 'sub_minion']))
  251. self.assertEqual(ret[1]['minions'], sorted(['minion', 'sub_minion']))
  252. def test_simple_local_async_post_no_tgt(self):
  253. low = [{'client': 'local_async',
  254. 'tgt': 'minion_we_dont_have',
  255. 'fun': 'test.ping',
  256. }]
  257. response = self.fetch('/',
  258. method='POST',
  259. body=salt.utils.json.dumps(low),
  260. headers={'Content-Type': self.content_type_map['json'],
  261. saltnado.AUTH_TOKEN_HEADER: self.token['token']},
  262. )
  263. response_obj = salt.utils.json.loads(response.body)
  264. self.assertEqual(response_obj['return'], [{}])
  265. @skipIf(True, 'Undetermined race condition in test. Temporarily disabled.')
  266. def test_simple_local_post_only_dictionary_request_with_order_masters(self):
  267. '''
  268. Test a basic API of /
  269. '''
  270. low = {'client': 'local',
  271. 'tgt': '*',
  272. 'fun': 'test.ping',
  273. }
  274. self.application.opts['order_masters'] = True
  275. self.application.opts['syndic_wait'] = 5
  276. response = self.fetch('/',
  277. method='POST',
  278. body=salt.utils.json.dumps(low),
  279. headers={'Content-Type': self.content_type_map['json'],
  280. saltnado.AUTH_TOKEN_HEADER: self.token['token']},
  281. connect_timeout=30,
  282. request_timeout=30,
  283. )
  284. response_obj = salt.utils.json.loads(response.body)
  285. self.application.opts['order_masters'] = []
  286. self.application.opts['syndic_wait'] = 5
  287. # If --proxy is set, it will cause an extra minion_id to be in the
  288. # response. Since there's not a great way to know if the test runner's
  289. # proxy minion is running, and we're not testing proxy minions here
  290. # anyway, just remove it from the response.
  291. response_obj[0]['return'].pop('proxytest', None)
  292. self.assertEqual(response_obj['return'], [{'minion': True, 'sub_minion': True}])
  293. # runner tests
  294. def test_simple_local_runner_post(self):
  295. low = [{'client': 'runner',
  296. 'fun': 'manage.up',
  297. }]
  298. response = self.fetch('/',
  299. method='POST',
  300. body=salt.utils.json.dumps(low),
  301. headers={'Content-Type': self.content_type_map['json'],
  302. saltnado.AUTH_TOKEN_HEADER: self.token['token']},
  303. connect_timeout=30,
  304. request_timeout=30,
  305. )
  306. response_obj = salt.utils.json.loads(response.body)
  307. self.assertEqual(len(response_obj['return']), 1)
  308. try:
  309. # If --proxy is set, it will cause an extra minion_id to be in the
  310. # response. Since there's not a great way to know if the test
  311. # runner's proxy minion is running, and we're not testing proxy
  312. # minions here anyway, just remove it from the response.
  313. response_obj['return'][0].remove('proxytest')
  314. except ValueError:
  315. pass
  316. self.assertEqual(sorted(response_obj['return'][0]), sorted(['minion', 'sub_minion']))
  317. # runner_async tests
  318. def test_simple_local_runner_async_post(self):
  319. low = [{'client': 'runner_async',
  320. 'fun': 'manage.up',
  321. }]
  322. response = self.fetch('/',
  323. method='POST',
  324. body=salt.utils.json.dumps(low),
  325. headers={'Content-Type': self.content_type_map['json'],
  326. saltnado.AUTH_TOKEN_HEADER: self.token['token']},
  327. connect_timeout=10,
  328. request_timeout=10,
  329. )
  330. response_obj = salt.utils.json.loads(response.body)
  331. self.assertIn('return', response_obj)
  332. self.assertEqual(1, len(response_obj['return']))
  333. self.assertIn('jid', response_obj['return'][0])
  334. self.assertIn('tag', response_obj['return'][0])
  335. @flaky
  336. @skipIf(HAS_ZMQ_IOLOOP is False, 'PyZMQ version must be >= 14.0.1 to run these tests.')
  337. class TestMinionSaltAPIHandler(_SaltnadoIntegrationTestCase):
  338. def get_app(self):
  339. urls = [(r"/minions/(.*)", saltnado.MinionSaltAPIHandler),
  340. (r"/minions", saltnado.MinionSaltAPIHandler),
  341. ]
  342. application = self.build_tornado_app(urls)
  343. application.event_listener = saltnado.EventListener({}, self.opts)
  344. return application
  345. @skipIf(True, 'issue #34753')
  346. def test_get_no_mid(self):
  347. response = self.fetch('/minions',
  348. method='GET',
  349. headers={saltnado.AUTH_TOKEN_HEADER: self.token['token']},
  350. follow_redirects=False,
  351. )
  352. response_obj = salt.utils.json.loads(response.body)
  353. self.assertEqual(len(response_obj['return']), 1)
  354. # one per minion
  355. self.assertEqual(len(response_obj['return'][0]), 2)
  356. # check a single grain
  357. for minion_id, grains in six.iteritems(response_obj['return'][0]):
  358. self.assertEqual(minion_id, grains['id'])
  359. @skipIf(True, 'to be re-enabled when #23623 is merged')
  360. def test_get(self):
  361. response = self.fetch('/minions/minion',
  362. method='GET',
  363. headers={saltnado.AUTH_TOKEN_HEADER: self.token['token']},
  364. follow_redirects=False,
  365. )
  366. response_obj = salt.utils.json.loads(response.body)
  367. self.assertEqual(len(response_obj['return']), 1)
  368. self.assertEqual(len(response_obj['return'][0]), 1)
  369. # check a single grain
  370. self.assertEqual(response_obj['return'][0]['minion']['id'], 'minion')
  371. def test_post(self):
  372. low = [{'tgt': '*minion',
  373. 'fun': 'test.ping',
  374. }]
  375. response = self.fetch('/minions',
  376. method='POST',
  377. body=salt.utils.json.dumps(low),
  378. headers={'Content-Type': self.content_type_map['json'],
  379. saltnado.AUTH_TOKEN_HEADER: self.token['token']},
  380. )
  381. response_obj = salt.utils.json.loads(response.body)
  382. ret = response_obj['return']
  383. ret[0]['minions'] = sorted(ret[0]['minions'])
  384. # TODO: verify pub function? Maybe look at how we test the publisher
  385. self.assertEqual(len(ret), 1)
  386. self.assertIn('jid', ret[0])
  387. self.assertEqual(ret[0]['minions'], sorted(['minion', 'sub_minion']))
  388. def test_post_with_client(self):
  389. # get a token for this test
  390. low = [{'client': 'local_async',
  391. 'tgt': '*minion',
  392. 'fun': 'test.ping',
  393. }]
  394. response = self.fetch('/minions',
  395. method='POST',
  396. body=salt.utils.json.dumps(low),
  397. headers={'Content-Type': self.content_type_map['json'],
  398. saltnado.AUTH_TOKEN_HEADER: self.token['token']},
  399. )
  400. response_obj = salt.utils.json.loads(response.body)
  401. ret = response_obj['return']
  402. ret[0]['minions'] = sorted(ret[0]['minions'])
  403. # TODO: verify pub function? Maybe look at how we test the publisher
  404. self.assertEqual(len(ret), 1)
  405. self.assertIn('jid', ret[0])
  406. self.assertEqual(ret[0]['minions'], sorted(['minion', 'sub_minion']))
  407. def test_post_with_incorrect_client(self):
  408. '''
  409. The /minions endpoint is asynchronous only, so if you try something else
  410. make sure you get an error
  411. '''
  412. # get a token for this test
  413. low = [{'client': 'local',
  414. 'tgt': '*',
  415. 'fun': 'test.ping',
  416. }]
  417. response = self.fetch('/minions',
  418. method='POST',
  419. body=salt.utils.json.dumps(low),
  420. headers={'Content-Type': self.content_type_map['json'],
  421. saltnado.AUTH_TOKEN_HEADER: self.token['token']},
  422. )
  423. self.assertEqual(response.code, 400)
  424. @skipIf(HAS_ZMQ_IOLOOP is False, 'PyZMQ version must be >= 14.0.1 to run these tests.')
  425. class TestJobsSaltAPIHandler(_SaltnadoIntegrationTestCase):
  426. def get_app(self):
  427. urls = [(r"/jobs/(.*)", saltnado.JobsSaltAPIHandler),
  428. (r"/jobs", saltnado.JobsSaltAPIHandler),
  429. ]
  430. application = self.build_tornado_app(urls)
  431. application.event_listener = saltnado.EventListener({}, self.opts)
  432. return application
  433. @skipIf(True, 'to be re-enabled when #23623 is merged')
  434. def test_get(self):
  435. # test with no JID
  436. self.http_client.fetch(self.get_url('/jobs'),
  437. self.stop,
  438. method='GET',
  439. headers={saltnado.AUTH_TOKEN_HEADER: self.token['token']},
  440. follow_redirects=False,
  441. )
  442. response = self.wait(timeout=30)
  443. response_obj = salt.utils.json.loads(response.body)['return'][0]
  444. try:
  445. for jid, ret in six.iteritems(response_obj):
  446. self.assertIn('Function', ret)
  447. self.assertIn('Target', ret)
  448. self.assertIn('Target-type', ret)
  449. self.assertIn('User', ret)
  450. self.assertIn('StartTime', ret)
  451. self.assertIn('Arguments', ret)
  452. except AttributeError as attribute_error:
  453. print(salt.utils.json.loads(response.body))
  454. raise
  455. # test with a specific JID passed in
  456. jid = next(six.iterkeys(response_obj))
  457. self.http_client.fetch(self.get_url('/jobs/{0}'.format(jid)),
  458. self.stop,
  459. method='GET',
  460. headers={saltnado.AUTH_TOKEN_HEADER: self.token['token']},
  461. follow_redirects=False,
  462. )
  463. response = self.wait(timeout=30)
  464. response_obj = salt.utils.json.loads(response.body)['return'][0]
  465. self.assertIn('Function', response_obj)
  466. self.assertIn('Target', response_obj)
  467. self.assertIn('Target-type', response_obj)
  468. self.assertIn('User', response_obj)
  469. self.assertIn('StartTime', response_obj)
  470. self.assertIn('Arguments', response_obj)
  471. self.assertIn('Result', response_obj)
  472. # TODO: run all the same tests from the root handler, but for now since they are
  473. # the same code, we'll just sanity check
  474. @skipIf(HAS_ZMQ_IOLOOP is False, 'PyZMQ version must be >= 14.0.1 to run these tests.')
  475. class TestRunSaltAPIHandler(_SaltnadoIntegrationTestCase):
  476. def get_app(self):
  477. urls = [("/run", saltnado.RunSaltAPIHandler),
  478. ]
  479. application = self.build_tornado_app(urls)
  480. application.event_listener = saltnado.EventListener({}, self.opts)
  481. return application
  482. @skipIf(True, 'to be re-enabled when #23623 is merged')
  483. def test_get(self):
  484. low = [{'client': 'local',
  485. 'tgt': '*',
  486. 'fun': 'test.ping',
  487. }]
  488. response = self.fetch('/run',
  489. method='POST',
  490. body=salt.utils.json.dumps(low),
  491. headers={'Content-Type': self.content_type_map['json'],
  492. saltnado.AUTH_TOKEN_HEADER: self.token['token']},
  493. )
  494. response_obj = salt.utils.json.loads(response.body)
  495. self.assertEqual(response_obj['return'], [{'minion': True, 'sub_minion': True}])
  496. @skipIf(HAS_ZMQ_IOLOOP is False, 'PyZMQ version must be >= 14.0.1 to run these tests.')
  497. class TestEventsSaltAPIHandler(_SaltnadoIntegrationTestCase):
  498. def get_app(self):
  499. urls = [(r"/events", saltnado.EventsSaltAPIHandler),
  500. ]
  501. application = self.build_tornado_app(urls)
  502. application.event_listener = saltnado.EventListener({}, self.opts)
  503. # store a reference, for magic later!
  504. self.application = application
  505. self.events_to_fire = 0
  506. return application
  507. def test_get(self):
  508. self.events_to_fire = 5
  509. response = self.fetch('/events',
  510. headers={saltnado.AUTH_TOKEN_HEADER: self.token['token']},
  511. streaming_callback=self.on_event,
  512. )
  513. def _stop(self):
  514. self.stop()
  515. def on_event(self, event):
  516. if six.PY3:
  517. event = event.decode('utf-8')
  518. if self.events_to_fire > 0:
  519. self.application.event_listener.event.fire_event({
  520. 'foo': 'bar',
  521. 'baz': 'qux',
  522. }, 'salt/netapi/test')
  523. self.events_to_fire -= 1
  524. # once we've fired all the events, lets call it a day
  525. else:
  526. # wait so that we can ensure that the next future is ready to go
  527. # to make sure we don't explode if the next one is ready
  528. ZMQIOLoop.current().add_timeout(time.time() + 0.5, self._stop)
  529. event = event.strip()
  530. # if we got a retry, just continue
  531. if event != 'retry: 400':
  532. tag, data = event.splitlines()
  533. self.assertTrue(tag.startswith('tag: '))
  534. self.assertTrue(data.startswith('data: '))
  535. @skipIf(HAS_ZMQ_IOLOOP is False, 'PyZMQ version must be >= 14.0.1 to run these tests.')
  536. class TestWebhookSaltAPIHandler(_SaltnadoIntegrationTestCase):
  537. def get_app(self):
  538. urls = [(r"/hook(/.*)?", saltnado.WebhookSaltAPIHandler),
  539. ]
  540. application = self.build_tornado_app(urls)
  541. self.application = application
  542. application.event_listener = saltnado.EventListener({}, self.opts)
  543. return application
  544. @skipIf(True, 'Skipping until we can devote more resources to debugging this test.')
  545. def test_post(self):
  546. self._future_resolved = threading.Event()
  547. try:
  548. def verify_event(future):
  549. '''
  550. Notify the threading event that the future is resolved
  551. '''
  552. self._future_resolved.set()
  553. self._finished = False # TODO: remove after some cleanup of the event listener
  554. # get an event future
  555. future = self.application.event_listener.get_event(self,
  556. tag='salt/netapi/hook',
  557. callback=verify_event)
  558. # fire the event
  559. response = self.fetch('/hook',
  560. method='POST',
  561. body='foo=bar',
  562. headers={saltnado.AUTH_TOKEN_HEADER: self.token['token']},
  563. )
  564. response_obj = salt.utils.json.loads(response.body)
  565. self.assertTrue(response_obj['success'])
  566. resolve_future_timeout = 60
  567. self._future_resolved.wait(resolve_future_timeout)
  568. try:
  569. event = future.result()
  570. except Exception as exc:
  571. self.fail('Failed to resolve future under {} secs: {}'.format(resolve_future_timeout, exc))
  572. self.assertEqual(event['tag'], 'salt/netapi/hook')
  573. self.assertIn('headers', event['data'])
  574. self.assertEqual(
  575. event['data']['post'],
  576. {'foo': salt.utils.stringutils.to_bytes('bar')}
  577. )
  578. finally:
  579. self._future_resolved.clear()
  580. del self._future_resolved