schedules_test.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423
  1. # Licensed to the Apache Software Foundation (ASF) under one
  2. # or more contributor license agreements. See the NOTICE file
  3. # distributed with this work for additional information
  4. # regarding copyright ownership. The ASF licenses this file
  5. # to you under the Apache License, Version 2.0 (the
  6. # "License"); you may not use this file except in compliance
  7. # with the License. You may obtain a copy of the License at
  8. #
  9. # http://www.apache.org/licenses/LICENSE-2.0
  10. #
  11. # Unless required by applicable law or agreed to in writing,
  12. # software distributed under the License is distributed on an
  13. # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
  14. # KIND, either express or implied. See the License for the
  15. # specific language governing permissions and limitations
  16. # under the License.
  17. # isort:skip_file
  18. from datetime import datetime, timedelta
  19. from unittest.mock import Mock, patch, PropertyMock
  20. from flask_babel import gettext as __
  21. from selenium.common.exceptions import WebDriverException
  22. from tests.test_app import app
  23. from superset import db
  24. from superset.models.dashboard import Dashboard
  25. from superset.models.schedules import (
  26. DashboardEmailSchedule,
  27. EmailDeliveryType,
  28. SliceEmailReportFormat,
  29. SliceEmailSchedule,
  30. )
  31. from superset.tasks.schedules import (
  32. create_webdriver,
  33. deliver_dashboard,
  34. deliver_slice,
  35. next_schedules,
  36. )
  37. from superset.models.slice import Slice
  38. from tests.base_tests import SupersetTestCase
  39. from .utils import read_fixture
  40. class SchedulesTestCase(SupersetTestCase):
  41. RECIPIENTS = "recipient1@superset.com, recipient2@superset.com"
  42. BCC = "bcc@superset.com"
  43. CSV = read_fixture("trends.csv")
  44. @classmethod
  45. def setUpClass(cls):
  46. with app.app_context():
  47. cls.common_data = dict(
  48. active=True,
  49. crontab="* * * * *",
  50. recipients=cls.RECIPIENTS,
  51. deliver_as_group=True,
  52. delivery_type=EmailDeliveryType.inline,
  53. )
  54. # Pick up a random slice and dashboard
  55. slce = db.session.query(Slice).all()[0]
  56. dashboard = db.session.query(Dashboard).all()[0]
  57. dashboard_schedule = DashboardEmailSchedule(**cls.common_data)
  58. dashboard_schedule.dashboard_id = dashboard.id
  59. dashboard_schedule.user_id = 1
  60. db.session.add(dashboard_schedule)
  61. slice_schedule = SliceEmailSchedule(**cls.common_data)
  62. slice_schedule.slice_id = slce.id
  63. slice_schedule.user_id = 1
  64. slice_schedule.email_format = SliceEmailReportFormat.data
  65. db.session.add(slice_schedule)
  66. db.session.commit()
  67. cls.slice_schedule = slice_schedule.id
  68. cls.dashboard_schedule = dashboard_schedule.id
  69. @classmethod
  70. def tearDownClass(cls):
  71. with app.app_context():
  72. db.session.query(SliceEmailSchedule).filter_by(
  73. id=cls.slice_schedule
  74. ).delete()
  75. db.session.query(DashboardEmailSchedule).filter_by(
  76. id=cls.dashboard_schedule
  77. ).delete()
  78. db.session.commit()
  79. def test_crontab_scheduler(self):
  80. crontab = "* * * * *"
  81. start_at = datetime.now().replace(microsecond=0, second=0, minute=0)
  82. stop_at = start_at + timedelta(seconds=3600)
  83. # Fire off the task every minute
  84. schedules = list(next_schedules(crontab, start_at, stop_at, resolution=0))
  85. self.assertEqual(schedules[0], start_at)
  86. self.assertEqual(schedules[-1], stop_at - timedelta(seconds=60))
  87. self.assertEqual(len(schedules), 60)
  88. # Fire off the task every 10 minutes, controlled via resolution
  89. schedules = list(next_schedules(crontab, start_at, stop_at, resolution=10 * 60))
  90. self.assertEqual(schedules[0], start_at)
  91. self.assertEqual(schedules[-1], stop_at - timedelta(seconds=10 * 60))
  92. self.assertEqual(len(schedules), 6)
  93. # Fire off the task every 12 minutes, controlled via resolution
  94. schedules = list(next_schedules(crontab, start_at, stop_at, resolution=12 * 60))
  95. self.assertEqual(schedules[0], start_at)
  96. self.assertEqual(schedules[-1], stop_at - timedelta(seconds=12 * 60))
  97. self.assertEqual(len(schedules), 5)
  98. def test_wider_schedules(self):
  99. crontab = "*/15 2,10 * * *"
  100. for hour in range(0, 24):
  101. start_at = datetime.now().replace(
  102. microsecond=0, second=0, minute=0, hour=hour
  103. )
  104. stop_at = start_at + timedelta(seconds=3600)
  105. schedules = list(next_schedules(crontab, start_at, stop_at, resolution=0))
  106. if hour in (2, 10):
  107. self.assertEqual(len(schedules), 4)
  108. else:
  109. self.assertEqual(len(schedules), 0)
  110. def test_complex_schedule(self):
  111. # Run the job on every Friday of March and May
  112. # On these days, run the job at
  113. # 5:10 pm
  114. # 5:11 pm
  115. # 5:12 pm
  116. # 5:13 pm
  117. # 5:14 pm
  118. # 5:15 pm
  119. # 5:25 pm
  120. # 5:28 pm
  121. # 5:31 pm
  122. # 5:34 pm
  123. # 5:37 pm
  124. # 5:40 pm
  125. crontab = "10-15,25-40/3 17 * 3,5 5"
  126. start_at = datetime.strptime("2018/01/01", "%Y/%m/%d")
  127. stop_at = datetime.strptime("2018/12/31", "%Y/%m/%d")
  128. schedules = list(next_schedules(crontab, start_at, stop_at, resolution=60))
  129. self.assertEqual(len(schedules), 108)
  130. fmt = "%Y-%m-%d %H:%M:%S"
  131. self.assertEqual(schedules[0], datetime.strptime("2018-03-02 17:10:00", fmt))
  132. self.assertEqual(schedules[-1], datetime.strptime("2018-05-25 17:40:00", fmt))
  133. self.assertEqual(schedules[59], datetime.strptime("2018-03-30 17:40:00", fmt))
  134. self.assertEqual(schedules[60], datetime.strptime("2018-05-04 17:10:00", fmt))
  135. @patch("superset.tasks.schedules.firefox.webdriver.WebDriver")
  136. def test_create_driver(self, mock_driver_class):
  137. mock_driver = Mock()
  138. mock_driver_class.return_value = mock_driver
  139. mock_driver.find_elements_by_id.side_effect = [True, False]
  140. create_webdriver()
  141. create_webdriver()
  142. mock_driver.add_cookie.assert_called_once()
  143. @patch("superset.tasks.schedules.firefox.webdriver.WebDriver")
  144. @patch("superset.tasks.schedules.send_email_smtp")
  145. @patch("superset.tasks.schedules.time")
  146. def test_deliver_dashboard_inline(self, mtime, send_email_smtp, driver_class):
  147. element = Mock()
  148. driver = Mock()
  149. mtime.sleep.return_value = None
  150. driver_class.return_value = driver
  151. # Ensure that we are able to login with the driver
  152. driver.find_elements_by_id.side_effect = [True, False]
  153. driver.find_element_by_class_name.return_value = element
  154. element.screenshot_as_png = read_fixture("sample.png")
  155. schedule = (
  156. db.session.query(DashboardEmailSchedule)
  157. .filter_by(id=self.dashboard_schedule)
  158. .all()[0]
  159. )
  160. deliver_dashboard(schedule)
  161. mtime.sleep.assert_called_once()
  162. driver.screenshot.assert_not_called()
  163. send_email_smtp.assert_called_once()
  164. @patch("superset.tasks.schedules.firefox.webdriver.WebDriver")
  165. @patch("superset.tasks.schedules.send_email_smtp")
  166. @patch("superset.tasks.schedules.time")
  167. def test_deliver_dashboard_as_attachment(
  168. self, mtime, send_email_smtp, driver_class
  169. ):
  170. element = Mock()
  171. driver = Mock()
  172. mtime.sleep.return_value = None
  173. driver_class.return_value = driver
  174. # Ensure that we are able to login with the driver
  175. driver.find_elements_by_id.side_effect = [True, False]
  176. driver.find_element_by_id.return_value = element
  177. driver.find_element_by_class_name.return_value = element
  178. element.screenshot_as_png = read_fixture("sample.png")
  179. schedule = (
  180. db.session.query(DashboardEmailSchedule)
  181. .filter_by(id=self.dashboard_schedule)
  182. .all()[0]
  183. )
  184. schedule.delivery_type = EmailDeliveryType.attachment
  185. deliver_dashboard(schedule)
  186. mtime.sleep.assert_called_once()
  187. driver.screenshot.assert_not_called()
  188. send_email_smtp.assert_called_once()
  189. self.assertIsNone(send_email_smtp.call_args[1]["images"])
  190. self.assertEqual(
  191. send_email_smtp.call_args[1]["data"]["screenshot.png"],
  192. element.screenshot_as_png,
  193. )
  194. @patch("superset.tasks.schedules.firefox.webdriver.WebDriver")
  195. @patch("superset.tasks.schedules.send_email_smtp")
  196. @patch("superset.tasks.schedules.time")
  197. def test_dashboard_chrome_like(self, mtime, send_email_smtp, driver_class):
  198. # Test functionality for chrome driver which does not support
  199. # element snapshots
  200. element = Mock()
  201. driver = Mock()
  202. mtime.sleep.return_value = None
  203. type(element).screenshot_as_png = PropertyMock(side_effect=WebDriverException)
  204. driver_class.return_value = driver
  205. # Ensure that we are able to login with the driver
  206. driver.find_elements_by_id.side_effect = [True, False]
  207. driver.find_element_by_id.return_value = element
  208. driver.find_element_by_class_name.return_value = element
  209. driver.screenshot.return_value = read_fixture("sample.png")
  210. schedule = (
  211. db.session.query(DashboardEmailSchedule)
  212. .filter_by(id=self.dashboard_schedule)
  213. .all()[0]
  214. )
  215. deliver_dashboard(schedule)
  216. mtime.sleep.assert_called_once()
  217. driver.screenshot.assert_called_once()
  218. send_email_smtp.assert_called_once()
  219. self.assertEqual(send_email_smtp.call_args[0][0], self.RECIPIENTS)
  220. self.assertEqual(
  221. list(send_email_smtp.call_args[1]["images"].values())[0],
  222. driver.screenshot.return_value,
  223. )
  224. @patch("superset.tasks.schedules.firefox.webdriver.WebDriver")
  225. @patch("superset.tasks.schedules.send_email_smtp")
  226. @patch("superset.tasks.schedules.time")
  227. def test_deliver_email_options(self, mtime, send_email_smtp, driver_class):
  228. element = Mock()
  229. driver = Mock()
  230. mtime.sleep.return_value = None
  231. driver_class.return_value = driver
  232. # Ensure that we are able to login with the driver
  233. driver.find_elements_by_id.side_effect = [True, False]
  234. driver.find_element_by_class_name.return_value = element
  235. element.screenshot_as_png = read_fixture("sample.png")
  236. schedule = (
  237. db.session.query(DashboardEmailSchedule)
  238. .filter_by(id=self.dashboard_schedule)
  239. .all()[0]
  240. )
  241. # Send individual mails to the group
  242. schedule.deliver_as_group = False
  243. # Set a bcc email address
  244. app.config["EMAIL_REPORT_BCC_ADDRESS"] = self.BCC
  245. deliver_dashboard(schedule)
  246. mtime.sleep.assert_called_once()
  247. driver.screenshot.assert_not_called()
  248. self.assertEqual(send_email_smtp.call_count, 2)
  249. self.assertEqual(send_email_smtp.call_args[1]["bcc"], self.BCC)
  250. @patch("superset.tasks.schedules.firefox.webdriver.WebDriver")
  251. @patch("superset.tasks.schedules.send_email_smtp")
  252. @patch("superset.tasks.schedules.time")
  253. def test_deliver_slice_inline_image(self, mtime, send_email_smtp, driver_class):
  254. element = Mock()
  255. driver = Mock()
  256. mtime.sleep.return_value = None
  257. driver_class.return_value = driver
  258. # Ensure that we are able to login with the driver
  259. driver.find_elements_by_id.side_effect = [True, False]
  260. driver.find_element_by_class_name.return_value = element
  261. element.screenshot_as_png = read_fixture("sample.png")
  262. schedule = (
  263. db.session.query(SliceEmailSchedule)
  264. .filter_by(id=self.slice_schedule)
  265. .all()[0]
  266. )
  267. schedule.email_format = SliceEmailReportFormat.visualization
  268. schedule.delivery_format = EmailDeliveryType.inline
  269. deliver_slice(schedule)
  270. mtime.sleep.assert_called_once()
  271. driver.screenshot.assert_not_called()
  272. send_email_smtp.assert_called_once()
  273. self.assertEqual(
  274. list(send_email_smtp.call_args[1]["images"].values())[0],
  275. element.screenshot_as_png,
  276. )
  277. @patch("superset.tasks.schedules.firefox.webdriver.WebDriver")
  278. @patch("superset.tasks.schedules.send_email_smtp")
  279. @patch("superset.tasks.schedules.time")
  280. def test_deliver_slice_attachment(self, mtime, send_email_smtp, driver_class):
  281. element = Mock()
  282. driver = Mock()
  283. mtime.sleep.return_value = None
  284. driver_class.return_value = driver
  285. # Ensure that we are able to login with the driver
  286. driver.find_elements_by_id.side_effect = [True, False]
  287. driver.find_element_by_class_name.return_value = element
  288. element.screenshot_as_png = read_fixture("sample.png")
  289. schedule = (
  290. db.session.query(SliceEmailSchedule)
  291. .filter_by(id=self.slice_schedule)
  292. .all()[0]
  293. )
  294. schedule.email_format = SliceEmailReportFormat.visualization
  295. schedule.delivery_type = EmailDeliveryType.attachment
  296. deliver_slice(schedule)
  297. mtime.sleep.assert_called_once()
  298. driver.screenshot.assert_not_called()
  299. send_email_smtp.assert_called_once()
  300. self.assertEqual(
  301. send_email_smtp.call_args[1]["data"]["screenshot.png"],
  302. element.screenshot_as_png,
  303. )
  304. @patch("superset.tasks.schedules.urllib.request.OpenerDirector.open")
  305. @patch("superset.tasks.schedules.urllib.request.urlopen")
  306. @patch("superset.tasks.schedules.send_email_smtp")
  307. def test_deliver_slice_csv_attachment(
  308. self, send_email_smtp, mock_open, mock_urlopen
  309. ):
  310. response = Mock()
  311. mock_open.return_value = response
  312. mock_urlopen.return_value = response
  313. mock_urlopen.return_value.getcode.return_value = 200
  314. response.read.return_value = self.CSV
  315. schedule = (
  316. db.session.query(SliceEmailSchedule)
  317. .filter_by(id=self.slice_schedule)
  318. .all()[0]
  319. )
  320. schedule.email_format = SliceEmailReportFormat.data
  321. schedule.delivery_type = EmailDeliveryType.attachment
  322. deliver_slice(schedule)
  323. send_email_smtp.assert_called_once()
  324. file_name = __("%(name)s.csv", name=schedule.slice.slice_name)
  325. self.assertEqual(send_email_smtp.call_args[1]["data"][file_name], self.CSV)
  326. @patch("superset.tasks.schedules.urllib.request.urlopen")
  327. @patch("superset.tasks.schedules.urllib.request.OpenerDirector.open")
  328. @patch("superset.tasks.schedules.send_email_smtp")
  329. def test_deliver_slice_csv_inline(self, send_email_smtp, mock_open, mock_urlopen):
  330. response = Mock()
  331. mock_open.return_value = response
  332. mock_urlopen.return_value = response
  333. mock_urlopen.return_value.getcode.return_value = 200
  334. response.read.return_value = self.CSV
  335. schedule = (
  336. db.session.query(SliceEmailSchedule)
  337. .filter_by(id=self.slice_schedule)
  338. .all()[0]
  339. )
  340. schedule.email_format = SliceEmailReportFormat.data
  341. schedule.delivery_type = EmailDeliveryType.inline
  342. deliver_slice(schedule)
  343. send_email_smtp.assert_called_once()
  344. self.assertIsNone(send_email_smtp.call_args[1]["data"])
  345. self.assertTrue("<table " in send_email_smtp.call_args[0][2])