| # 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) |
| self.assertIn("[ untitled dashboard ]", resp) |
| 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_dashboard_modes(self): |
| self.login(username="admin") |
| dash = db.session.query(Dashboard).filter_by(slug="births").first() |
| url = dash.url |
| if dash.url.find("?") == -1: |
| url += "?" |
| else: |
| url += "&" |
| resp = self.get_resp(url + "edit=true&standalone=true") |
| self.assertIn("editMode": true", resp) |
| self.assertIn("standalone_mode": true", resp) |
| self.assertIn('<body class="standalone">', resp) |
| |
| @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) |
| |
| resp = self.get_resp(new_url) |
| self.assertIn("North America", resp) |
| |
| @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) |
| |
| self.assertIn("Births", self.get_resp("/superset/dashboard/births/")) |
| |
| # 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() |