mirror of
https://github.com/open-metadata/OpenMetadata.git
synced 2025-07-10 10:40:42 +00:00
472 lines
18 KiB
Python
472 lines
18 KiB
Python
"""
|
|
Test Tableau Dashboard
|
|
"""
|
|
import uuid
|
|
from datetime import datetime, timedelta
|
|
from types import SimpleNamespace
|
|
from unittest import TestCase
|
|
from unittest.mock import patch
|
|
|
|
from metadata.generated.schema.api.data.createChart import CreateChartRequest
|
|
from metadata.generated.schema.api.data.createDashboard import CreateDashboardRequest
|
|
from metadata.generated.schema.entity.data.dashboard import Dashboard
|
|
from metadata.generated.schema.entity.services.dashboardService import (
|
|
DashboardConnection,
|
|
DashboardService,
|
|
DashboardServiceType,
|
|
)
|
|
from metadata.generated.schema.metadataIngestion.workflow import (
|
|
OpenMetadataWorkflowConfig,
|
|
)
|
|
from metadata.generated.schema.type.basic import FullyQualifiedEntityName
|
|
from metadata.generated.schema.type.entityReference import EntityReference
|
|
from metadata.generated.schema.type.filterPattern import FilterPattern
|
|
from metadata.generated.schema.type.usageDetails import UsageDetails, UsageStats
|
|
from metadata.generated.schema.type.usageRequest import UsageRequest
|
|
from metadata.ingestion.ometa.ometa_api import OpenMetadata
|
|
from metadata.ingestion.source.dashboard.dashboard_service import DashboardUsage
|
|
from metadata.ingestion.source.dashboard.tableau.metadata import (
|
|
TableauDashboard,
|
|
TableauSource,
|
|
)
|
|
from metadata.ingestion.source.dashboard.tableau.models import (
|
|
TableauBaseModel,
|
|
TableauChart,
|
|
TableauOwner,
|
|
)
|
|
|
|
MOCK_DASHBOARD_SERVICE = DashboardService(
|
|
id="c3eb265f-5445-4ad3-ba5e-797d3a3071bb",
|
|
fullyQualifiedName=FullyQualifiedEntityName("tableau_source_test"),
|
|
name="tableau_source_test",
|
|
connection=DashboardConnection(),
|
|
serviceType=DashboardServiceType.Tableau,
|
|
)
|
|
|
|
mock_tableau_config = {
|
|
"source": {
|
|
"type": "tableau",
|
|
"serviceName": "test2",
|
|
"serviceConnection": {
|
|
"config": {
|
|
"type": "Tableau",
|
|
"authType": {"username": "username", "password": "abcdefg"},
|
|
"hostPort": "http://tableauHost.com",
|
|
"siteName": "tableauSiteName",
|
|
}
|
|
},
|
|
"sourceConfig": {
|
|
"config": {"dashboardFilterPattern": {}, "chartFilterPattern": {}}
|
|
},
|
|
},
|
|
"sink": {"type": "metadata-rest", "config": {}},
|
|
"workflowConfig": {
|
|
"openMetadataServerConfig": {
|
|
"hostPort": "http://localhost:8585/api",
|
|
"authProvider": "openmetadata",
|
|
"securityConfig": {
|
|
"jwtToken": "eyJraWQiOiJHYjM4OWEtOWY3Ni1nZGpzLWE5MmotMDI0MmJrOTQzNTYiLCJ0eXAiOiJKV1QiLCJhbGc"
|
|
"iOiJSUzI1NiJ9.eyJzdWIiOiJhZG1pbiIsImlzQm90IjpmYWxzZSwiaXNzIjoib3Blbi1tZXRhZGF0YS5vcmciLCJpYXQiOjE"
|
|
"2NjM5Mzg0NjIsImVtYWlsIjoiYWRtaW5Ab3Blbm1ldGFkYXRhLm9yZyJ9.tS8um_5DKu7HgzGBzS1VTA5uUjKWOCU0B_j08WXB"
|
|
"iEC0mr0zNREkqVfwFDD-d24HlNEbrqioLsBuFRiwIWKc1m_ZlVQbG7P36RUxhuv2vbSp80FKyNM-Tj93FDzq91jsyNmsQhyNv_fN"
|
|
"r3TXfzzSPjHt8Go0FMMP66weoKMgW2PbXlhVKwEuXUHyakLLzewm9UMeQaEiRzhiTMU3UkLXcKbYEJJvfNFcLwSl9W8JCO_l0Yj3u"
|
|
"d-qt_nQYEZwqW6u5nfdQllN133iikV4fM5QZsMCnm8Rq1mvLR0y9bmJiD7fwM1tmJ791TUWqmKaTnP49U493VanKpUAfzIiOiIbhg"
|
|
},
|
|
}
|
|
},
|
|
}
|
|
|
|
MOCK_DASHBOARD = TableauDashboard(
|
|
id="42a5b706-739d-4d62-94a2-faedf33950a5",
|
|
name="Regional",
|
|
webpageUrl="http://tableauHost.com/#/site/hidarsite/workbooks/897790",
|
|
description="tableau dashboard description",
|
|
user_views=10,
|
|
tags=[],
|
|
owner=TableauOwner(
|
|
id="1234", name="Dashboard Owner", email="samplemail@sample.com"
|
|
),
|
|
charts=[
|
|
TableauChart(
|
|
id="b05695a2-d1ea-428e-96b2-858809809da4",
|
|
name="Obesity",
|
|
workbook=TableauBaseModel(id="42a5b706-739d-4d62-94a2-faedf33950a5"),
|
|
sheetType="dashboard",
|
|
viewUrlName="Obesity",
|
|
contentUrl="Regional/sheets/Obesity",
|
|
tags=[],
|
|
),
|
|
TableauChart(
|
|
id="106ff64d-537b-4534-8140-5d08c586e077",
|
|
name="College",
|
|
workbook=TableauBaseModel(id="42a5b706-739d-4d62-94a2-faedf33950a5"),
|
|
sheetType="view",
|
|
viewUrlName="College",
|
|
contentUrl="Regional/sheets/College",
|
|
tags=[],
|
|
),
|
|
TableauChart(
|
|
id="c1493abc-9057-4bdf-9061-c6d2908e4eaa",
|
|
name="Global Temperatures",
|
|
workbook=TableauBaseModel(id="42a5b706-739d-4d62-94a2-faedf33950a5"),
|
|
sheetType="dashboard",
|
|
viewUrlName="GlobalTemperatures",
|
|
contentUrl="Regional/sheets/GlobalTemperatures",
|
|
tags=[],
|
|
),
|
|
],
|
|
)
|
|
|
|
EXPECTED_DASHBOARD = [
|
|
CreateDashboardRequest(
|
|
name="42a5b706-739d-4d62-94a2-faedf33950a5",
|
|
displayName="Regional",
|
|
description="tableau dashboard description",
|
|
sourceUrl="http://tableauHost.com/#/site/hidarsite/workbooks/897790/views",
|
|
charts=[],
|
|
tags=[],
|
|
owners=None,
|
|
service=FullyQualifiedEntityName("tableau_source_test"),
|
|
extension=None,
|
|
)
|
|
]
|
|
|
|
EXPECTED_CHARTS = [
|
|
CreateChartRequest(
|
|
name="b05695a2-d1ea-428e-96b2-858809809da4",
|
|
displayName="Obesity",
|
|
description=None,
|
|
chartType="Other",
|
|
sourceUrl="http://tableauHost.com/#/site/tableauSiteUrl/views/Regional/Obesity",
|
|
tags=None,
|
|
owners=None,
|
|
service=FullyQualifiedEntityName("tableau_source_test"),
|
|
),
|
|
CreateChartRequest(
|
|
name="106ff64d-537b-4534-8140-5d08c586e077",
|
|
displayName="College",
|
|
description=None,
|
|
chartType="Other",
|
|
sourceUrl="http://tableauHost.com/#/site/tableauSiteUrl/views/Regional/College",
|
|
tags=None,
|
|
owners=None,
|
|
service=FullyQualifiedEntityName("tableau_source_test"),
|
|
),
|
|
CreateChartRequest(
|
|
name="c1493abc-9057-4bdf-9061-c6d2908e4eaa",
|
|
displayName="Global Temperatures",
|
|
description=None,
|
|
chartType="Other",
|
|
sourceUrl="http://tableauHost.com/#/site/tableauSiteUrl/views/Regional/GlobalTemperatures",
|
|
tags=None,
|
|
owners=None,
|
|
service=FullyQualifiedEntityName("tableau_source_test"),
|
|
),
|
|
]
|
|
|
|
|
|
class TableauUnitTest(TestCase):
|
|
"""
|
|
Implements the necessary methods to extract
|
|
Domo Dashboard Unit Test
|
|
"""
|
|
|
|
@patch(
|
|
"metadata.ingestion.source.dashboard.dashboard_service.DashboardServiceSource.test_connection"
|
|
)
|
|
@patch("metadata.ingestion.source.dashboard.tableau.connection.get_connection")
|
|
def __init__(self, methodName, get_connection, test_connection) -> None:
|
|
super().__init__(methodName)
|
|
get_connection.return_value = False
|
|
test_connection.return_value = False
|
|
self.config = OpenMetadataWorkflowConfig.model_validate(mock_tableau_config)
|
|
self.tableau = TableauSource.create(
|
|
mock_tableau_config["source"],
|
|
OpenMetadata(self.config.workflowConfig.openMetadataServerConfig),
|
|
)
|
|
self.tableau.client = SimpleNamespace()
|
|
self.tableau.context.get().__dict__[
|
|
"dashboard_service"
|
|
] = MOCK_DASHBOARD_SERVICE.fullyQualifiedName.root
|
|
|
|
def test_dashboard_name(self):
|
|
assert self.tableau.get_dashboard_name(MOCK_DASHBOARD) == MOCK_DASHBOARD.name
|
|
|
|
def test_yield_chart(self):
|
|
"""
|
|
Function for testing charts
|
|
"""
|
|
chart_list = []
|
|
results = self.tableau.yield_dashboard_chart(MOCK_DASHBOARD)
|
|
for result in results:
|
|
if isinstance(result, CreateChartRequest):
|
|
chart_list.append(result)
|
|
|
|
for _, (exptected, original) in enumerate(zip(EXPECTED_CHARTS, chart_list)):
|
|
self.assertEqual(exptected, original)
|
|
|
|
def test_yield_dashboard_usage(self):
|
|
"""
|
|
Validate the logic for existing or new usage
|
|
"""
|
|
self.tableau.context.get().__dict__["dashboard"] = "dashboard_name"
|
|
|
|
# Start checking dashboard without usage
|
|
# and a view count
|
|
return_value = Dashboard(
|
|
id=uuid.uuid4(),
|
|
name="dashboard_name",
|
|
fullyQualifiedName="dashboard_service.dashboard_name",
|
|
service=EntityReference(id=uuid.uuid4(), type="dashboardService"),
|
|
)
|
|
with patch.object(OpenMetadata, "get_by_name", return_value=return_value):
|
|
got_usage = next(self.tableau.yield_dashboard_usage(MOCK_DASHBOARD))
|
|
self.assertEqual(
|
|
got_usage.right,
|
|
DashboardUsage(
|
|
dashboard=return_value,
|
|
usage=UsageRequest(date=self.tableau.today, count=10),
|
|
),
|
|
)
|
|
|
|
# Now check what happens if we already have some summary data for today
|
|
return_value = Dashboard(
|
|
id=uuid.uuid4(),
|
|
name="dashboard_name",
|
|
fullyQualifiedName="dashboard_service.dashboard_name",
|
|
service=EntityReference(id=uuid.uuid4(), type="dashboardService"),
|
|
usageSummary=UsageDetails(
|
|
dailyStats=UsageStats(count=10), date=self.tableau.today
|
|
),
|
|
)
|
|
with patch.object(OpenMetadata, "get_by_name", return_value=return_value):
|
|
# Nothing is returned
|
|
self.assertEqual(
|
|
len(list(self.tableau.yield_dashboard_usage(MOCK_DASHBOARD))), 0
|
|
)
|
|
|
|
# But if we have usage for today but the count is 0, we'll return the details
|
|
return_value = Dashboard(
|
|
id=uuid.uuid4(),
|
|
name="dashboard_name",
|
|
fullyQualifiedName="dashboard_service.dashboard_name",
|
|
service=EntityReference(id=uuid.uuid4(), type="dashboardService"),
|
|
usageSummary=UsageDetails(
|
|
dailyStats=UsageStats(count=0), date=self.tableau.today
|
|
),
|
|
)
|
|
with patch.object(OpenMetadata, "get_by_name", return_value=return_value):
|
|
self.assertEqual(
|
|
next(self.tableau.yield_dashboard_usage(MOCK_DASHBOARD)).right,
|
|
DashboardUsage(
|
|
dashboard=return_value,
|
|
usage=UsageRequest(date=self.tableau.today, count=10),
|
|
),
|
|
)
|
|
|
|
# But if we have usage for another day, then we do the difference
|
|
return_value = Dashboard(
|
|
id=uuid.uuid4(),
|
|
name="dashboard_name",
|
|
fullyQualifiedName="dashboard_service.dashboard_name",
|
|
service=EntityReference(id=uuid.uuid4(), type="dashboardService"),
|
|
usageSummary=UsageDetails(
|
|
dailyStats=UsageStats(count=5),
|
|
date=datetime.strftime(datetime.now() - timedelta(1), "%Y-%m-%d"),
|
|
),
|
|
)
|
|
with patch.object(OpenMetadata, "get_by_name", return_value=return_value):
|
|
self.assertEqual(
|
|
next(self.tableau.yield_dashboard_usage(MOCK_DASHBOARD)).right,
|
|
DashboardUsage(
|
|
dashboard=return_value,
|
|
usage=UsageRequest(date=self.tableau.today, count=5),
|
|
),
|
|
)
|
|
|
|
# If the past usage is higher than what we have today, something weird is going on
|
|
# we don't return usage but don't explode
|
|
return_value = Dashboard(
|
|
id=uuid.uuid4(),
|
|
name="dashboard_name",
|
|
fullyQualifiedName="dashboard_service.dashboard_name",
|
|
service=EntityReference(id=uuid.uuid4(), type="dashboardService"),
|
|
usageSummary=UsageDetails(
|
|
dailyStats=UsageStats(count=1000),
|
|
date=datetime.strftime(datetime.now() - timedelta(1), "%Y-%m-%d"),
|
|
),
|
|
)
|
|
with patch.object(OpenMetadata, "get_by_name", return_value=return_value):
|
|
self.assertEqual(
|
|
len(list(self.tableau.yield_dashboard_usage(MOCK_DASHBOARD))), 1
|
|
)
|
|
|
|
self.assertIsNotNone(
|
|
list(self.tableau.yield_dashboard_usage(MOCK_DASHBOARD))[0].left
|
|
)
|
|
|
|
def test_check_basemodel_returns_id_as_string(self):
|
|
"""
|
|
Test that the basemodel returns the id as a string
|
|
"""
|
|
base_model = TableauBaseModel(id=uuid.uuid4())
|
|
self.assertEqual(base_model.id, str(base_model.id))
|
|
|
|
base_model = TableauBaseModel(id="1234")
|
|
self.assertEqual(base_model.id, "1234")
|
|
|
|
def test_get_dashboard_project_filter(self):
|
|
"""
|
|
Test get_dashboard filters dashboards based on projectFilterPattern
|
|
"""
|
|
|
|
mock_dashboard_details_list = [
|
|
TableauDashboard(
|
|
id="dashboard1",
|
|
name="dashboard1",
|
|
project=TableauBaseModel(id="p1", name="FilteredProject"),
|
|
charts=[],
|
|
dataModels=[],
|
|
tags=[],
|
|
),
|
|
TableauDashboard(
|
|
id="dashboard2",
|
|
name="dashboard2",
|
|
project=TableauBaseModel(id="p2", name="OtherProject"),
|
|
charts=[],
|
|
dataModels=[],
|
|
tags=[],
|
|
),
|
|
TableauDashboard(
|
|
id="dashboard3",
|
|
name="dashboard3",
|
|
project=TableauBaseModel(id="p3", name="excludedDashboard"),
|
|
charts=[],
|
|
dataModels=[],
|
|
tags=[],
|
|
),
|
|
TableauDashboard(
|
|
id="dashboard4",
|
|
name="dashboard4",
|
|
project=TableauBaseModel(id="p4", name="excludedDashboard"),
|
|
charts=[],
|
|
dataModels=[],
|
|
tags=[],
|
|
),
|
|
]
|
|
|
|
project_names_return_map = {
|
|
"dashboard1": "FilteredProject.OtherProject",
|
|
"dashboard2": "FilteredProject.OtherProject.ChildProject",
|
|
"dashboard3": "AnFilteredProject.OtherProject.ChildProject",
|
|
"dashboard4": "AnFilteredProject.OtherProject1.ChildProject2.ExcludedProject2",
|
|
}
|
|
|
|
self.tableau.source_config.projectFilterPattern = FilterPattern(
|
|
includes=["^FilteredProject.OtherProject$"]
|
|
)
|
|
|
|
with patch.object(
|
|
self.tableau,
|
|
"get_dashboards_list",
|
|
return_value=mock_dashboard_details_list,
|
|
):
|
|
|
|
with patch.object(
|
|
self.tableau,
|
|
"get_project_names",
|
|
side_effect=lambda dashboard_details: project_names_return_map[
|
|
dashboard_details.name
|
|
],
|
|
), patch.object(
|
|
self.tableau,
|
|
"get_dashboards_list",
|
|
return_value=mock_dashboard_details_list,
|
|
), patch.object(
|
|
self.tableau,
|
|
"get_dashboard_details",
|
|
side_effect=lambda x: x,
|
|
):
|
|
dashboards = list(self.tableau.get_dashboard())
|
|
self.assertEqual(len(dashboards), 1)
|
|
self.assertEqual(dashboards[0].name, "dashboard1")
|
|
|
|
# Test with other project names
|
|
self.tableau.source_config.projectFilterPattern = FilterPattern(
|
|
includes=[
|
|
"^FilteredProject.OtherProject.*",
|
|
"^AnFilteredProject.OtherProject.ChildProject$",
|
|
]
|
|
)
|
|
|
|
with patch.object(
|
|
self.tableau,
|
|
"get_dashboards_list",
|
|
return_value=mock_dashboard_details_list,
|
|
):
|
|
|
|
with patch.object(
|
|
self.tableau,
|
|
"get_project_names",
|
|
side_effect=lambda dashboard_details: project_names_return_map[
|
|
dashboard_details.name
|
|
],
|
|
), patch.object(
|
|
self.tableau,
|
|
"get_dashboards_list",
|
|
return_value=mock_dashboard_details_list,
|
|
), patch.object(
|
|
self.tableau,
|
|
"get_dashboard_details",
|
|
side_effect=lambda x: x,
|
|
):
|
|
dashboards = list(self.tableau.get_dashboard())
|
|
self.assertEqual(len(dashboards), 3)
|
|
self.assertEqual(dashboards[0].name, "dashboard1")
|
|
self.assertEqual(dashboards[1].name, "dashboard2")
|
|
self.assertEqual(dashboards[2].name, "dashboard3")
|
|
|
|
# Test with includes and excludes
|
|
|
|
self.tableau.source_config.projectFilterPattern = FilterPattern(
|
|
includes=["^AnFilteredProject.OtherProject1.*"],
|
|
excludes=[".*ExcludedProject2.*"],
|
|
)
|
|
|
|
with patch.object(
|
|
self.tableau,
|
|
"get_dashboards_list",
|
|
return_value=mock_dashboard_details_list,
|
|
):
|
|
|
|
with patch.object(
|
|
self.tableau,
|
|
"get_project_names",
|
|
side_effect=lambda dashboard_details: project_names_return_map[
|
|
dashboard_details.name
|
|
],
|
|
), patch.object(
|
|
self.tableau,
|
|
"get_dashboards_list",
|
|
return_value=mock_dashboard_details_list,
|
|
), patch.object(
|
|
self.tableau,
|
|
"get_dashboard_details",
|
|
side_effect=lambda x: x,
|
|
):
|
|
dashboards = list(self.tableau.get_dashboard())
|
|
self.assertEqual(len(dashboards), 0)
|
|
|
|
def test_generate_dashboard_url(self):
|
|
"""
|
|
Test that the dashboard url is generated correctly with proxyURL
|
|
"""
|
|
self.tableau.config.serviceConnection.root.config.proxyURL = (
|
|
"http://mockTableauServer.com"
|
|
)
|
|
result = list(self.tableau.yield_dashboard(MOCK_DASHBOARD))
|
|
self.assertEqual(
|
|
result[0].right.sourceUrl.root,
|
|
"http://mockTableauServer.com/#/site/hidarsite/workbooks/897790/views",
|
|
)
|