dashboard_tests.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496
  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. """Unit tests for Superset"""
  19. import json
  20. import unittest
  21. from random import random
  22. from flask import escape
  23. from sqlalchemy import func
  24. import tests.test_app
  25. from superset import db, security_manager
  26. from superset.connectors.sqla.models import SqlaTable
  27. from superset.models import core as models
  28. from superset.models.dashboard import Dashboard
  29. from superset.models.slice import Slice
  30. from .base_tests import SupersetTestCase
  31. class DashboardTests(SupersetTestCase):
  32. def __init__(self, *args, **kwargs):
  33. super(DashboardTests, self).__init__(*args, **kwargs)
  34. @classmethod
  35. def setUpClass(cls):
  36. pass
  37. def setUp(self):
  38. pass
  39. def tearDown(self):
  40. pass
  41. def get_mock_positions(self, dash):
  42. positions = {"DASHBOARD_VERSION_KEY": "v2"}
  43. for i, slc in enumerate(dash.slices):
  44. id = "DASHBOARD_CHART_TYPE-{}".format(i)
  45. d = {
  46. "type": "DASHBOARD_CHART_TYPE",
  47. "id": id,
  48. "children": [],
  49. "meta": {"width": 4, "height": 50, "chartId": slc.id},
  50. }
  51. positions[id] = d
  52. return positions
  53. def test_dashboard(self):
  54. self.login(username="admin")
  55. urls = {}
  56. for dash in db.session.query(Dashboard).all():
  57. urls[dash.dashboard_title] = dash.url
  58. for title, url in urls.items():
  59. assert escape(title) in self.client.get(url).data.decode("utf-8")
  60. def test_new_dashboard(self):
  61. self.login(username="admin")
  62. dash_count_before = db.session.query(func.count(Dashboard.id)).first()[0]
  63. url = "/dashboard/new/"
  64. resp = self.get_resp(url)
  65. self.assertIn("[ untitled dashboard ]", resp)
  66. dash_count_after = db.session.query(func.count(Dashboard.id)).first()[0]
  67. self.assertEqual(dash_count_before + 1, dash_count_after)
  68. def test_dashboard_modes(self):
  69. self.login(username="admin")
  70. dash = db.session.query(Dashboard).filter_by(slug="births").first()
  71. url = dash.url
  72. if dash.url.find("?") == -1:
  73. url += "?"
  74. else:
  75. url += "&"
  76. resp = self.get_resp(url + "edit=true&standalone=true")
  77. self.assertIn("editMode": true", resp)
  78. self.assertIn("standalone_mode": true", resp)
  79. self.assertIn('<body class="standalone">', resp)
  80. def test_save_dash(self, username="admin"):
  81. self.login(username=username)
  82. dash = db.session.query(Dashboard).filter_by(slug="births").first()
  83. positions = self.get_mock_positions(dash)
  84. data = {
  85. "css": "",
  86. "expanded_slices": {},
  87. "positions": positions,
  88. "dashboard_title": dash.dashboard_title,
  89. }
  90. url = "/superset/save_dash/{}/".format(dash.id)
  91. resp = self.get_resp(url, data=dict(data=json.dumps(data)))
  92. self.assertIn("SUCCESS", resp)
  93. def test_save_dash_with_filter(self, username="admin"):
  94. self.login(username=username)
  95. dash = db.session.query(Dashboard).filter_by(slug="world_health").first()
  96. positions = self.get_mock_positions(dash)
  97. filters = {str(dash.slices[0].id): {"region": ["North America"]}}
  98. default_filters = json.dumps(filters)
  99. data = {
  100. "css": "",
  101. "expanded_slices": {},
  102. "positions": positions,
  103. "dashboard_title": dash.dashboard_title,
  104. "default_filters": default_filters,
  105. }
  106. url = "/superset/save_dash/{}/".format(dash.id)
  107. resp = self.get_resp(url, data=dict(data=json.dumps(data)))
  108. self.assertIn("SUCCESS", resp)
  109. updatedDash = db.session.query(Dashboard).filter_by(slug="world_health").first()
  110. new_url = updatedDash.url
  111. self.assertIn("region", new_url)
  112. resp = self.get_resp(new_url)
  113. self.assertIn("North America", resp)
  114. def test_save_dash_with_invalid_filters(self, username="admin"):
  115. self.login(username=username)
  116. dash = db.session.query(Dashboard).filter_by(slug="world_health").first()
  117. # add an invalid filter slice
  118. positions = self.get_mock_positions(dash)
  119. filters = {str(99999): {"region": ["North America"]}}
  120. default_filters = json.dumps(filters)
  121. data = {
  122. "css": "",
  123. "expanded_slices": {},
  124. "positions": positions,
  125. "dashboard_title": dash.dashboard_title,
  126. "default_filters": default_filters,
  127. }
  128. url = "/superset/save_dash/{}/".format(dash.id)
  129. resp = self.get_resp(url, data=dict(data=json.dumps(data)))
  130. self.assertIn("SUCCESS", resp)
  131. updatedDash = db.session.query(Dashboard).filter_by(slug="world_health").first()
  132. new_url = updatedDash.url
  133. self.assertNotIn("region", new_url)
  134. def test_save_dash_with_dashboard_title(self, username="admin"):
  135. self.login(username=username)
  136. dash = db.session.query(Dashboard).filter_by(slug="births").first()
  137. origin_title = dash.dashboard_title
  138. positions = self.get_mock_positions(dash)
  139. data = {
  140. "css": "",
  141. "expanded_slices": {},
  142. "positions": positions,
  143. "dashboard_title": "new title",
  144. }
  145. url = "/superset/save_dash/{}/".format(dash.id)
  146. self.get_resp(url, data=dict(data=json.dumps(data)))
  147. updatedDash = db.session.query(Dashboard).filter_by(slug="births").first()
  148. self.assertEqual(updatedDash.dashboard_title, "new title")
  149. # bring back dashboard original title
  150. data["dashboard_title"] = origin_title
  151. self.get_resp(url, data=dict(data=json.dumps(data)))
  152. def test_save_dash_with_colors(self, username="admin"):
  153. self.login(username=username)
  154. dash = db.session.query(Dashboard).filter_by(slug="births").first()
  155. positions = self.get_mock_positions(dash)
  156. new_label_colors = {"data value": "random color"}
  157. data = {
  158. "css": "",
  159. "expanded_slices": {},
  160. "positions": positions,
  161. "dashboard_title": dash.dashboard_title,
  162. "color_namespace": "Color Namespace Test",
  163. "color_scheme": "Color Scheme Test",
  164. "label_colors": new_label_colors,
  165. }
  166. url = "/superset/save_dash/{}/".format(dash.id)
  167. self.get_resp(url, data=dict(data=json.dumps(data)))
  168. updatedDash = db.session.query(Dashboard).filter_by(slug="births").first()
  169. self.assertIn("color_namespace", updatedDash.json_metadata)
  170. self.assertIn("color_scheme", updatedDash.json_metadata)
  171. self.assertIn("label_colors", updatedDash.json_metadata)
  172. # bring back original dashboard
  173. del data["color_namespace"]
  174. del data["color_scheme"]
  175. del data["label_colors"]
  176. self.get_resp(url, data=dict(data=json.dumps(data)))
  177. def test_copy_dash(self, username="admin"):
  178. self.login(username=username)
  179. dash = db.session.query(Dashboard).filter_by(slug="births").first()
  180. positions = self.get_mock_positions(dash)
  181. new_label_colors = {"data value": "random color"}
  182. data = {
  183. "css": "",
  184. "duplicate_slices": False,
  185. "expanded_slices": {},
  186. "positions": positions,
  187. "dashboard_title": "Copy Of Births",
  188. "color_namespace": "Color Namespace Test",
  189. "color_scheme": "Color Scheme Test",
  190. "label_colors": new_label_colors,
  191. }
  192. # Save changes to Births dashboard and retrieve updated dash
  193. dash_id = dash.id
  194. url = "/superset/save_dash/{}/".format(dash_id)
  195. self.client.post(url, data=dict(data=json.dumps(data)))
  196. dash = db.session.query(Dashboard).filter_by(id=dash_id).first()
  197. orig_json_data = dash.data
  198. # Verify that copy matches original
  199. url = "/superset/copy_dash/{}/".format(dash_id)
  200. resp = self.get_json_resp(url, data=dict(data=json.dumps(data)))
  201. self.assertEqual(resp["dashboard_title"], "Copy Of Births")
  202. self.assertEqual(resp["position_json"], orig_json_data["position_json"])
  203. self.assertEqual(resp["metadata"], orig_json_data["metadata"])
  204. # check every attribute in each dashboard's slices list,
  205. # exclude modified and changed_on attribute
  206. for index, slc in enumerate(orig_json_data["slices"]):
  207. for key in slc:
  208. if key not in ["modified", "changed_on"]:
  209. self.assertEqual(slc[key], resp["slices"][index][key])
  210. def test_add_slices(self, username="admin"):
  211. self.login(username=username)
  212. dash = db.session.query(Dashboard).filter_by(slug="births").first()
  213. new_slice = (
  214. db.session.query(Slice).filter_by(slice_name="Energy Force Layout").first()
  215. )
  216. existing_slice = (
  217. db.session.query(Slice).filter_by(slice_name="Girl Name Cloud").first()
  218. )
  219. data = {
  220. "slice_ids": [new_slice.data["slice_id"], existing_slice.data["slice_id"]]
  221. }
  222. url = "/superset/add_slices/{}/".format(dash.id)
  223. resp = self.client.post(url, data=dict(data=json.dumps(data)))
  224. assert "SLICES ADDED" in resp.data.decode("utf-8")
  225. dash = db.session.query(Dashboard).filter_by(slug="births").first()
  226. new_slice = (
  227. db.session.query(Slice).filter_by(slice_name="Energy Force Layout").first()
  228. )
  229. assert new_slice in dash.slices
  230. assert len(set(dash.slices)) == len(dash.slices)
  231. # cleaning up
  232. dash = db.session.query(Dashboard).filter_by(slug="births").first()
  233. dash.slices = [o for o in dash.slices if o.slice_name != "Energy Force Layout"]
  234. db.session.commit()
  235. def test_remove_slices(self, username="admin"):
  236. self.login(username=username)
  237. dash = db.session.query(Dashboard).filter_by(slug="births").first()
  238. origin_slices_length = len(dash.slices)
  239. positions = self.get_mock_positions(dash)
  240. # remove one chart
  241. chart_keys = []
  242. for key in positions.keys():
  243. if key.startswith("DASHBOARD_CHART_TYPE"):
  244. chart_keys.append(key)
  245. positions.pop(chart_keys[0])
  246. data = {
  247. "css": "",
  248. "expanded_slices": {},
  249. "positions": positions,
  250. "dashboard_title": dash.dashboard_title,
  251. }
  252. # save dash
  253. dash_id = dash.id
  254. url = "/superset/save_dash/{}/".format(dash_id)
  255. self.client.post(url, data=dict(data=json.dumps(data)))
  256. dash = db.session.query(Dashboard).filter_by(id=dash_id).first()
  257. # verify slices data
  258. data = dash.data
  259. self.assertEqual(len(data["slices"]), origin_slices_length - 1)
  260. def test_public_user_dashboard_access(self):
  261. table = db.session.query(SqlaTable).filter_by(table_name="birth_names").one()
  262. # Make the births dash published so it can be seen
  263. births_dash = db.session.query(Dashboard).filter_by(slug="births").one()
  264. births_dash.published = True
  265. db.session.merge(births_dash)
  266. db.session.commit()
  267. # Try access before adding appropriate permissions.
  268. self.revoke_public_access_to_table(table)
  269. self.logout()
  270. resp = self.get_resp("/api/v1/chart/")
  271. self.assertNotIn("birth_names", resp)
  272. resp = self.get_resp("/api/v1/dashboard/")
  273. self.assertNotIn("/superset/dashboard/births/", resp)
  274. self.grant_public_access_to_table(table)
  275. # Try access after adding appropriate permissions.
  276. self.assertIn("birth_names", self.get_resp("/api/v1/chart/"))
  277. resp = self.get_resp("/api/v1/dashboard/")
  278. self.assertIn("/superset/dashboard/births/", resp)
  279. self.assertIn("Births", self.get_resp("/superset/dashboard/births/"))
  280. # Confirm that public doesn't have access to other datasets.
  281. resp = self.get_resp("/api/v1/chart/")
  282. self.assertNotIn("wb_health_population", resp)
  283. resp = self.get_resp("/api/v1/dashboard/")
  284. self.assertNotIn("/superset/dashboard/world_health/", resp)
  285. def test_dashboard_with_created_by_can_be_accessed_by_public_users(self):
  286. self.logout()
  287. table = db.session.query(SqlaTable).filter_by(table_name="birth_names").one()
  288. self.grant_public_access_to_table(table)
  289. dash = db.session.query(Dashboard).filter_by(slug="births").first()
  290. dash.owners = [security_manager.find_user("admin")]
  291. dash.created_by = security_manager.find_user("admin")
  292. db.session.merge(dash)
  293. db.session.commit()
  294. assert "Births" in self.get_resp("/superset/dashboard/births/")
  295. def test_only_owners_can_save(self):
  296. dash = db.session.query(Dashboard).filter_by(slug="births").first()
  297. dash.owners = []
  298. db.session.merge(dash)
  299. db.session.commit()
  300. self.test_save_dash("admin")
  301. self.logout()
  302. self.assertRaises(Exception, self.test_save_dash, "alpha")
  303. alpha = security_manager.find_user("alpha")
  304. dash = db.session.query(Dashboard).filter_by(slug="births").first()
  305. dash.owners = [alpha]
  306. db.session.merge(dash)
  307. db.session.commit()
  308. self.test_save_dash("alpha")
  309. def test_owners_can_view_empty_dashboard(self):
  310. dash = db.session.query(Dashboard).filter_by(slug="empty_dashboard").first()
  311. if not dash:
  312. dash = Dashboard()
  313. dash.dashboard_title = "Empty Dashboard"
  314. dash.slug = "empty_dashboard"
  315. else:
  316. dash.slices = []
  317. dash.owners = []
  318. db.session.merge(dash)
  319. db.session.commit()
  320. gamma_user = security_manager.find_user("gamma")
  321. self.login(gamma_user.username)
  322. resp = self.get_resp("/api/v1/dashboard/")
  323. self.assertNotIn("/superset/dashboard/empty_dashboard/", resp)
  324. def test_users_can_view_published_dashboard(self):
  325. table = db.session.query(SqlaTable).filter_by(table_name="energy_usage").one()
  326. # get a slice from the allowed table
  327. slice = db.session.query(Slice).filter_by(slice_name="Energy Sankey").one()
  328. self.grant_public_access_to_table(table)
  329. hidden_dash_slug = f"hidden_dash_{random()}"
  330. published_dash_slug = f"published_dash_{random()}"
  331. # Create a published and hidden dashboard and add them to the database
  332. published_dash = Dashboard()
  333. published_dash.dashboard_title = "Published Dashboard"
  334. published_dash.slug = published_dash_slug
  335. published_dash.slices = [slice]
  336. published_dash.published = True
  337. hidden_dash = Dashboard()
  338. hidden_dash.dashboard_title = "Hidden Dashboard"
  339. hidden_dash.slug = hidden_dash_slug
  340. hidden_dash.slices = [slice]
  341. hidden_dash.published = False
  342. db.session.merge(published_dash)
  343. db.session.merge(hidden_dash)
  344. db.session.commit()
  345. resp = self.get_resp("/api/v1/dashboard/")
  346. self.assertNotIn(f"/superset/dashboard/{hidden_dash_slug}/", resp)
  347. self.assertIn(f"/superset/dashboard/{published_dash_slug}/", resp)
  348. def test_users_can_view_own_dashboard(self):
  349. user = security_manager.find_user("gamma")
  350. my_dash_slug = f"my_dash_{random()}"
  351. not_my_dash_slug = f"not_my_dash_{random()}"
  352. # Create one dashboard I own and another that I don't
  353. dash = Dashboard()
  354. dash.dashboard_title = "My Dashboard"
  355. dash.slug = my_dash_slug
  356. dash.owners = [user]
  357. dash.slices = []
  358. hidden_dash = Dashboard()
  359. hidden_dash.dashboard_title = "Not My Dashboard"
  360. hidden_dash.slug = not_my_dash_slug
  361. hidden_dash.slices = []
  362. hidden_dash.owners = []
  363. db.session.merge(dash)
  364. db.session.merge(hidden_dash)
  365. db.session.commit()
  366. self.login(user.username)
  367. resp = self.get_resp("/api/v1/dashboard/")
  368. self.assertIn(f"/superset/dashboard/{my_dash_slug}/", resp)
  369. self.assertNotIn(f"/superset/dashboard/{not_my_dash_slug}/", resp)
  370. def test_users_can_view_favorited_dashboards(self):
  371. user = security_manager.find_user("gamma")
  372. fav_dash_slug = f"my_favorite_dash_{random()}"
  373. regular_dash_slug = f"regular_dash_{random()}"
  374. favorite_dash = Dashboard()
  375. favorite_dash.dashboard_title = "My Favorite Dashboard"
  376. favorite_dash.slug = fav_dash_slug
  377. regular_dash = Dashboard()
  378. regular_dash.dashboard_title = "A Plain Ol Dashboard"
  379. regular_dash.slug = regular_dash_slug
  380. db.session.merge(favorite_dash)
  381. db.session.merge(regular_dash)
  382. db.session.commit()
  383. dash = db.session.query(Dashboard).filter_by(slug=fav_dash_slug).first()
  384. favorites = models.FavStar()
  385. favorites.obj_id = dash.id
  386. favorites.class_name = "Dashboard"
  387. favorites.user_id = user.id
  388. db.session.merge(favorites)
  389. db.session.commit()
  390. self.login(user.username)
  391. resp = self.get_resp("/api/v1/dashboard/")
  392. self.assertIn(f"/superset/dashboard/{fav_dash_slug}/", resp)
  393. def test_user_can_not_view_unpublished_dash(self):
  394. admin_user = security_manager.find_user("admin")
  395. gamma_user = security_manager.find_user("gamma")
  396. slug = f"admin_owned_unpublished_dash_{random()}"
  397. # Create a dashboard owned by admin and unpublished
  398. dash = Dashboard()
  399. dash.dashboard_title = "My Dashboard"
  400. dash.slug = slug
  401. dash.owners = [admin_user]
  402. dash.slices = []
  403. dash.published = False
  404. db.session.merge(dash)
  405. db.session.commit()
  406. # list dashboards as a gamma user
  407. self.login(gamma_user.username)
  408. resp = self.get_resp("/api/v1/dashboard/")
  409. self.assertNotIn(f"/superset/dashboard/{slug}/", resp)
  410. if __name__ == "__main__":
  411. unittest.main()