# Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. # isort:skip_file """Unit tests for Superset""" from datetime import datetime import json import unittest from random import random import pytest from flask import escape, url_for from sqlalchemy import func from tests.test_app import app from superset import db, security_manager from superset.connectors.sqla.models import SqlaTable from superset.models import core as models from superset.models.dashboard import Dashboard from superset.models.slice import Slice from tests.fixtures.birth_names_dashboard import load_birth_names_dashboard_with_slices from tests.fixtures.energy_dashboard import load_energy_table_with_slice from tests.fixtures.public_role import public_role_like_gamma from tests.fixtures.unicode_dashboard import load_unicode_dashboard_with_position from tests.fixtures.world_bank_dashboard import load_world_bank_dashboard_with_slices from .base_tests import SupersetTestCase class TestDashboard(SupersetTestCase): @pytest.fixture def cleanup_copied_dash(self): with app.app_context(): original_dashboard = ( db.session.query(Dashboard).filter_by(slug="births").first() ) original_dashboard_id = original_dashboard.id yield copied_dashboard = ( db.session.query(Dashboard) .filter( Dashboard.dashboard_title == "Copy Of Births", Dashboard.id != original_dashboard_id, ) .first() ) db.session.merge(original_dashboard) if copied_dashboard: db.session.delete(copied_dashboard) db.session.commit() @pytest.fixture def load_dashboard(self): with app.app_context(): table = ( db.session.query(SqlaTable).filter_by(table_name="energy_usage").one() ) # get a slice from the allowed table slice = db.session.query(Slice).filter_by(slice_name="Energy Sankey").one() self.grant_public_access_to_table(table) pytest.hidden_dash_slug = f"hidden_dash_{random()}" pytest.published_dash_slug = f"published_dash_{random()}" # Create a published and hidden dashboard and add them to the database published_dash = Dashboard() published_dash.dashboard_title = "Published Dashboard" published_dash.slug = pytest.published_dash_slug published_dash.slices = [slice] published_dash.published = True hidden_dash = Dashboard() hidden_dash.dashboard_title = "Hidden Dashboard" hidden_dash.slug = pytest.hidden_dash_slug hidden_dash.slices = [slice] hidden_dash.published = False db.session.merge(published_dash) db.session.merge(hidden_dash) yield db.session.commit() self.revoke_public_access_to_table(table) db.session.delete(published_dash) db.session.delete(hidden_dash) db.session.commit() def get_mock_positions(self, dash): positions = {"DASHBOARD_VERSION_KEY": "v2"} for i, slc in enumerate(dash.slices): id = "DASHBOARD_CHART_TYPE-{}".format(i) d = { "type": "CHART", "id": id, "children": [], "meta": {"width": 4, "height": 50, "chartId": slc.id}, } positions[id] = d return positions def test_dashboard(self): self.login(username="admin") urls = {} for dash in db.session.query(Dashboard).all(): urls[dash.dashboard_title] = dash.url for title, url in urls.items(): assert escape(title) in self.client.get(url).data.decode("utf-8") def test_superset_dashboard_url(self): url_for("Superset.dashboard", dashboard_id_or_slug=1) def test_new_dashboard(self): self.login(username="admin") dash_count_before = db.session.query(func.count(Dashboard.id)).first()[0] url = "/dashboard/new/" resp = self.get_resp(url) dash_count_after = db.session.query(func.count(Dashboard.id)).first()[0] self.assertEqual(dash_count_before + 1, dash_count_after) @pytest.mark.usefixtures("load_birth_names_dashboard_with_slices") def test_save_dash(self, username="admin"): self.login(username=username) dash = db.session.query(Dashboard).filter_by(slug="births").first() positions = self.get_mock_positions(dash) data = { "css": "", "expanded_slices": {}, "positions": positions, "dashboard_title": dash.dashboard_title, # set a further modified_time for unit test "last_modified_time": datetime.now().timestamp() + 1000, } url = "/superset/save_dash/{}/".format(dash.id) resp = self.get_resp(url, data=dict(data=json.dumps(data))) self.assertIn("SUCCESS", resp) @pytest.mark.usefixtures("load_world_bank_dashboard_with_slices") def test_save_dash_with_filter(self, username="admin"): self.login(username=username) dash = db.session.query(Dashboard).filter_by(slug="world_health").first() positions = self.get_mock_positions(dash) filters = {str(dash.slices[0].id): {"region": ["North America"]}} default_filters = json.dumps(filters) data = { "css": "", "expanded_slices": {}, "positions": positions, "dashboard_title": dash.dashboard_title, "default_filters": default_filters, # set a further modified_time for unit test "last_modified_time": datetime.now().timestamp() + 1000, } url = "/superset/save_dash/{}/".format(dash.id) resp = self.get_resp(url, data=dict(data=json.dumps(data))) self.assertIn("SUCCESS", resp) updatedDash = db.session.query(Dashboard).filter_by(slug="world_health").first() new_url = updatedDash.url self.assertIn("world_health", new_url) self.assertNotIn("preselect_filters", new_url) @pytest.mark.usefixtures("load_world_bank_dashboard_with_slices") def test_save_dash_with_invalid_filters(self, username="admin"): self.login(username=username) dash = db.session.query(Dashboard).filter_by(slug="world_health").first() # add an invalid filter slice positions = self.get_mock_positions(dash) filters = {str(99999): {"region": ["North America"]}} default_filters = json.dumps(filters) data = { "css": "", "expanded_slices": {}, "positions": positions, "dashboard_title": dash.dashboard_title, "default_filters": default_filters, # set a further modified_time for unit test "last_modified_time": datetime.now().timestamp() + 1000, } url = "/superset/save_dash/{}/".format(dash.id) resp = self.get_resp(url, data=dict(data=json.dumps(data))) self.assertIn("SUCCESS", resp) updatedDash = db.session.query(Dashboard).filter_by(slug="world_health").first() new_url = updatedDash.url self.assertNotIn("region", new_url) @pytest.mark.usefixtures("load_birth_names_dashboard_with_slices") def test_save_dash_with_dashboard_title(self, username="admin"): self.login(username=username) dash = db.session.query(Dashboard).filter_by(slug="births").first() origin_title = dash.dashboard_title positions = self.get_mock_positions(dash) data = { "css": "", "expanded_slices": {}, "positions": positions, "dashboard_title": "new title", # set a further modified_time for unit test "last_modified_time": datetime.now().timestamp() + 1000, } url = "/superset/save_dash/{}/".format(dash.id) self.get_resp(url, data=dict(data=json.dumps(data))) updatedDash = db.session.query(Dashboard).filter_by(slug="births").first() self.assertEqual(updatedDash.dashboard_title, "new title") # bring back dashboard original title data["dashboard_title"] = origin_title self.get_resp(url, data=dict(data=json.dumps(data))) @pytest.mark.usefixtures("load_birth_names_dashboard_with_slices") def test_save_dash_with_colors(self, username="admin"): self.login(username=username) dash = db.session.query(Dashboard).filter_by(slug="births").first() positions = self.get_mock_positions(dash) new_label_colors = {"data value": "random color"} data = { "css": "", "expanded_slices": {}, "positions": positions, "dashboard_title": dash.dashboard_title, "color_namespace": "Color Namespace Test", "color_scheme": "Color Scheme Test", "label_colors": new_label_colors, # set a further modified_time for unit test "last_modified_time": datetime.now().timestamp() + 1000, } url = "/superset/save_dash/{}/".format(dash.id) self.get_resp(url, data=dict(data=json.dumps(data))) updatedDash = db.session.query(Dashboard).filter_by(slug="births").first() self.assertIn("color_namespace", updatedDash.json_metadata) self.assertIn("color_scheme", updatedDash.json_metadata) self.assertIn("label_colors", updatedDash.json_metadata) # bring back original dashboard del data["color_namespace"] del data["color_scheme"] del data["label_colors"] self.get_resp(url, data=dict(data=json.dumps(data))) @pytest.mark.usefixtures( "load_birth_names_dashboard_with_slices", "cleanup_copied_dash", "load_unicode_dashboard_with_position", ) def test_copy_dash(self, username="admin"): self.login(username=username) dash = db.session.query(Dashboard).filter_by(slug="births").first() positions = self.get_mock_positions(dash) new_label_colors = {"data value": "random color"} data = { "css": "", "duplicate_slices": False, "expanded_slices": {}, "positions": positions, "dashboard_title": "Copy Of Births", "color_namespace": "Color Namespace Test", "color_scheme": "Color Scheme Test", "label_colors": new_label_colors, # set a further modified_time for unit test "last_modified_time": datetime.now().timestamp() + 1000, } # Save changes to Births dashboard and retrieve updated dash dash_id = dash.id url = "/superset/save_dash/{}/".format(dash_id) self.client.post(url, data=dict(data=json.dumps(data))) dash = db.session.query(Dashboard).filter_by(id=dash_id).first() orig_json_data = dash.data # Verify that copy matches original url = "/superset/copy_dash/{}/".format(dash_id) resp = self.get_json_resp(url, data=dict(data=json.dumps(data))) self.assertEqual(resp["dashboard_title"], "Copy Of Births") self.assertEqual(resp["position_json"], orig_json_data["position_json"]) self.assertEqual(resp["metadata"], orig_json_data["metadata"]) # check every attribute in each dashboard's slices list, # exclude modified and changed_on attribute for index, slc in enumerate(orig_json_data["slices"]): for key in slc: if key not in ["modified", "changed_on", "changed_on_humanized"]: self.assertEqual(slc[key], resp["slices"][index][key]) @pytest.mark.usefixtures( "load_energy_table_with_slice", "load_birth_names_dashboard_with_slices" ) def test_add_slices(self, username="admin"): self.login(username=username) dash = db.session.query(Dashboard).filter_by(slug="births").first() new_slice = ( db.session.query(Slice).filter_by(slice_name="Energy Force Layout").first() ) existing_slice = ( db.session.query(Slice).filter_by(slice_name="Girl Name Cloud").first() ) data = { "slice_ids": [new_slice.data["slice_id"], existing_slice.data["slice_id"]] } url = "/superset/add_slices/{}/".format(dash.id) resp = self.client.post(url, data=dict(data=json.dumps(data))) assert "SLICES ADDED" in resp.data.decode("utf-8") dash = db.session.query(Dashboard).filter_by(slug="births").first() new_slice = ( db.session.query(Slice).filter_by(slice_name="Energy Force Layout").first() ) assert new_slice in dash.slices assert len(set(dash.slices)) == len(dash.slices) # cleaning up dash = db.session.query(Dashboard).filter_by(slug="births").first() dash.slices = [o for o in dash.slices if o.slice_name != "Energy Force Layout"] db.session.commit() @pytest.mark.usefixtures("load_birth_names_dashboard_with_slices") def test_remove_slices(self, username="admin"): self.login(username=username) dash = db.session.query(Dashboard).filter_by(slug="births").first() origin_slices_length = len(dash.slices) positions = self.get_mock_positions(dash) # remove one chart chart_keys = [] for key in positions.keys(): if key.startswith("DASHBOARD_CHART_TYPE"): chart_keys.append(key) positions.pop(chart_keys[0]) data = { "css": "", "expanded_slices": {}, "positions": positions, "dashboard_title": dash.dashboard_title, # set a further modified_time for unit test "last_modified_time": datetime.now().timestamp() + 1000, } # save dash dash_id = dash.id url = "/superset/save_dash/{}/".format(dash_id) self.client.post(url, data=dict(data=json.dumps(data))) dash = db.session.query(Dashboard).filter_by(id=dash_id).first() # verify slices data data = dash.data self.assertEqual(len(data["slices"]), origin_slices_length - 1) @pytest.mark.usefixtures("load_birth_names_dashboard_with_slices") @pytest.mark.usefixtures("public_role_like_gamma") def test_public_user_dashboard_access(self): table = db.session.query(SqlaTable).filter_by(table_name="birth_names").one() # Make the births dash published so it can be seen births_dash = db.session.query(Dashboard).filter_by(slug="births").one() births_dash.published = True db.session.merge(births_dash) db.session.commit() # Try access before adding appropriate permissions. self.revoke_public_access_to_table(table) self.logout() resp = self.get_resp("/api/v1/chart/") self.assertNotIn("birth_names", resp) resp = self.get_resp("/api/v1/dashboard/") self.assertNotIn("/superset/dashboard/births/", resp) self.grant_public_access_to_table(table) # Try access after adding appropriate permissions. self.assertIn("birth_names", self.get_resp("/api/v1/chart/")) resp = self.get_resp("/api/v1/dashboard/") self.assertIn("/superset/dashboard/births/", resp) # Confirm that public doesn't have access to other datasets. resp = self.get_resp("/api/v1/chart/") self.assertNotIn("wb_health_population", resp) resp = self.get_resp("/api/v1/dashboard/") self.assertNotIn("/superset/dashboard/world_health/", resp) # Cleanup self.revoke_public_access_to_table(table) @pytest.mark.usefixtures("load_birth_names_dashboard_with_slices") @pytest.mark.usefixtures("public_role_like_gamma") def test_dashboard_with_created_by_can_be_accessed_by_public_users(self): self.logout() table = db.session.query(SqlaTable).filter_by(table_name="birth_names").one() self.grant_public_access_to_table(table) dash = db.session.query(Dashboard).filter_by(slug="births").first() dash.owners = [security_manager.find_user("admin")] dash.created_by = security_manager.find_user("admin") db.session.merge(dash) db.session.commit() assert "Births" in self.get_resp("/superset/dashboard/births/") # Cleanup self.revoke_public_access_to_table(table) @pytest.mark.usefixtures("load_birth_names_dashboard_with_slices") def test_only_owners_can_save(self): dash = db.session.query(Dashboard).filter_by(slug="births").first() dash.owners = [] db.session.merge(dash) db.session.commit() self.test_save_dash("admin") self.logout() self.assertRaises(Exception, self.test_save_dash, "alpha") alpha = security_manager.find_user("alpha") dash = db.session.query(Dashboard).filter_by(slug="births").first() dash.owners = [alpha] db.session.merge(dash) db.session.commit() self.test_save_dash("alpha") @pytest.mark.usefixtures("load_energy_table_with_slice", "load_dashboard") def test_users_can_view_published_dashboard(self): self.login("alpha") resp = self.get_resp("/api/v1/dashboard/") self.assertNotIn(f"/superset/dashboard/{pytest.hidden_dash_slug}/", resp) self.assertIn(f"/superset/dashboard/{pytest.published_dash_slug}/", resp) def test_users_can_view_own_dashboard(self): user = security_manager.find_user("gamma") my_dash_slug = f"my_dash_{random()}" not_my_dash_slug = f"not_my_dash_{random()}" # Create one dashboard I own and another that I don't dash = Dashboard() dash.dashboard_title = "My Dashboard" dash.slug = my_dash_slug dash.owners = [user] dash.slices = [] hidden_dash = Dashboard() hidden_dash.dashboard_title = "Not My Dashboard" hidden_dash.slug = not_my_dash_slug hidden_dash.slices = [] hidden_dash.owners = [] db.session.merge(dash) db.session.merge(hidden_dash) db.session.commit() self.login(user.username) resp = self.get_resp("/api/v1/dashboard/") self.assertIn(f"/superset/dashboard/{my_dash_slug}/", resp) self.assertNotIn(f"/superset/dashboard/{not_my_dash_slug}/", resp) def test_users_can_view_favorited_dashboards(self): user = security_manager.find_user("gamma") fav_dash_slug = f"my_favorite_dash_{random()}" regular_dash_slug = f"regular_dash_{random()}" favorite_dash = Dashboard() favorite_dash.dashboard_title = "My Favorite Dashboard" favorite_dash.slug = fav_dash_slug regular_dash = Dashboard() regular_dash.dashboard_title = "A Plain Ol Dashboard" regular_dash.slug = regular_dash_slug db.session.merge(favorite_dash) db.session.merge(regular_dash) db.session.commit() dash = db.session.query(Dashboard).filter_by(slug=fav_dash_slug).first() favorites = models.FavStar() favorites.obj_id = dash.id favorites.class_name = "Dashboard" favorites.user_id = user.id db.session.merge(favorites) db.session.commit() self.login(user.username) resp = self.get_resp("/api/v1/dashboard/") self.assertIn(f"/superset/dashboard/{fav_dash_slug}/", resp) def test_user_can_not_view_unpublished_dash(self): admin_user = security_manager.find_user("admin") gamma_user = security_manager.find_user("gamma") slug = f"admin_owned_unpublished_dash_{random()}" # Create a dashboard owned by admin and unpublished dash = Dashboard() dash.dashboard_title = "My Dashboard" dash.slug = slug dash.owners = [admin_user] dash.slices = [] dash.published = False db.session.merge(dash) db.session.commit() # list dashboards as a gamma user self.login(gamma_user.username) resp = self.get_resp("/api/v1/dashboard/") self.assertNotIn(f"/superset/dashboard/{slug}/", resp) if __name__ == "__main__": unittest.main()