mirror of
https://github.com/open-metadata/OpenMetadata.git
synced 2025-10-01 03:46:24 +00:00
775 lines
30 KiB
Python
775 lines
30 KiB
Python
"""
|
|
Test Tableau Dashboard
|
|
"""
|
|
import uuid
|
|
from datetime import datetime, timedelta
|
|
from types import SimpleNamespace
|
|
from unittest import TestCase
|
|
from unittest.mock import MagicMock, 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.data.dashboardDataModel import DashboardDataModel
|
|
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 (
|
|
DataSource,
|
|
TableauBaseModel,
|
|
TableauChart,
|
|
TableauOwner,
|
|
UpstreamTable,
|
|
)
|
|
|
|
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"
|
|
"6NjM5Mzg0NjIsImVtYWlsIjoiYWRtaW5Ab3Blbm1ldGFkYXRhLm9yZyJ9.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",
|
|
)
|
|
|
|
def _setup_ssl_config(self, verify_ssl_value="no-ssl", ssl_config=None):
|
|
"""
|
|
Helper method to set up SSL configuration for testing
|
|
"""
|
|
from types import SimpleNamespace
|
|
|
|
from pydantic import SecretStr
|
|
|
|
# Set up verifySSL
|
|
self.tableau.config.serviceConnection.root.config.verifySSL = SimpleNamespace()
|
|
self.tableau.config.serviceConnection.root.config.verifySSL.value = (
|
|
verify_ssl_value
|
|
)
|
|
|
|
# Set up sslConfig if provided
|
|
if ssl_config:
|
|
self.tableau.config.serviceConnection.root.config.sslConfig = (
|
|
SimpleNamespace()
|
|
)
|
|
self.tableau.config.serviceConnection.root.config.sslConfig.root = (
|
|
SimpleNamespace()
|
|
)
|
|
|
|
if "caCertificate" in ssl_config:
|
|
self.tableau.config.serviceConnection.root.config.sslConfig.root.caCertificate = SecretStr(
|
|
ssl_config["caCertificate"]
|
|
)
|
|
else:
|
|
self.tableau.config.serviceConnection.root.config.sslConfig.root.caCertificate = (
|
|
None
|
|
)
|
|
|
|
if "sslCertificate" in ssl_config:
|
|
self.tableau.config.serviceConnection.root.config.sslConfig.root.sslCertificate = SecretStr(
|
|
ssl_config["sslCertificate"]
|
|
)
|
|
else:
|
|
self.tableau.config.serviceConnection.root.config.sslConfig.root.sslCertificate = (
|
|
None
|
|
)
|
|
|
|
if "sslKey" in ssl_config:
|
|
self.tableau.config.serviceConnection.root.config.sslConfig.root.sslKey = SecretStr(
|
|
ssl_config["sslKey"]
|
|
)
|
|
else:
|
|
self.tableau.config.serviceConnection.root.config.sslConfig.root.sslKey = (
|
|
None
|
|
)
|
|
else:
|
|
self.tableau.config.serviceConnection.root.config.sslConfig = None
|
|
|
|
def test_tableau_ssl_auth(self):
|
|
"""
|
|
Test that Tableau SSL authentication works correctly
|
|
"""
|
|
# Set up SSL configuration with all certificates
|
|
self._setup_ssl_config(
|
|
verify_ssl_value="validate",
|
|
ssl_config={
|
|
"caCertificate": "/path/to/ca.pem",
|
|
"sslCertificate": "/path/to/cert.pem",
|
|
"sslKey": "/path/to/key.pem",
|
|
},
|
|
)
|
|
|
|
# Test that SSL configuration was set correctly
|
|
self.assertEqual(
|
|
self.tableau.config.serviceConnection.root.config.sslConfig.root.sslCertificate.get_secret_value(),
|
|
"/path/to/cert.pem",
|
|
)
|
|
self.assertEqual(
|
|
self.tableau.config.serviceConnection.root.config.sslConfig.root.sslKey.get_secret_value(),
|
|
"/path/to/key.pem",
|
|
)
|
|
self.assertEqual(
|
|
self.tableau.config.serviceConnection.root.config.sslConfig.root.caCertificate.get_secret_value(),
|
|
"/path/to/ca.pem",
|
|
)
|
|
|
|
# Test SSL connection establishment
|
|
with patch.object(
|
|
self.tableau, "get_dashboards_list", return_value=[]
|
|
) as mock_get_dashboards:
|
|
list(self.tableau.get_dashboard())
|
|
mock_get_dashboards.assert_called_once()
|
|
|
|
def test_tableau_ssl_auth_without_cert(self):
|
|
"""
|
|
Test that Tableau SSL authentication works without client certificates
|
|
"""
|
|
# Set up SSL configuration with only CA certificate
|
|
self._setup_ssl_config(
|
|
verify_ssl_value="validate", ssl_config={"caCertificate": "/path/to/ca.pem"}
|
|
)
|
|
|
|
# Verify SSL configuration was set correctly
|
|
self.assertEqual(
|
|
self.tableau.config.serviceConnection.root.config.sslConfig.root.caCertificate.get_secret_value(),
|
|
"/path/to/ca.pem",
|
|
)
|
|
self.assertIsNone(
|
|
self.tableau.config.serviceConnection.root.config.sslConfig.root.sslCertificate
|
|
)
|
|
self.assertIsNone(
|
|
self.tableau.config.serviceConnection.root.config.sslConfig.root.sslKey
|
|
)
|
|
|
|
# Test SSL connection establishment
|
|
with patch.object(
|
|
self.tableau, "get_dashboards_list", return_value=[]
|
|
) as mock_get_dashboards:
|
|
list(self.tableau.get_dashboard())
|
|
mock_get_dashboards.assert_called_once()
|
|
|
|
def test_tableau_ssl_auth_disabled(self):
|
|
"""
|
|
Test that Tableau works correctly when SSL is disabled
|
|
"""
|
|
# Set up SSL configuration with SSL disabled
|
|
self._setup_ssl_config(verify_ssl_value="ignore")
|
|
|
|
# Verify SSL verification is disabled
|
|
self.assertEqual(
|
|
self.tableau.config.serviceConnection.root.config.verifySSL.value, "ignore"
|
|
)
|
|
|
|
# Test SSL connection establishment
|
|
with patch.object(
|
|
self.tableau, "get_dashboards_list", return_value=[]
|
|
) as mock_get_dashboards:
|
|
list(self.tableau.get_dashboard())
|
|
mock_get_dashboards.assert_called_once()
|
|
|
|
def test_get_datamodel_table_lineage_with_empty_from_entities(self):
|
|
"""
|
|
Test that _get_datamodel_table_lineage handles empty from_entities gracefully
|
|
"""
|
|
# Mock data for the test
|
|
mock_datamodel = DataSource(
|
|
id="datasource1",
|
|
name="Test Datasource",
|
|
upstreamDatasources=[
|
|
DataSource(
|
|
id="upstream_datasource1",
|
|
name="Upstream Datasource",
|
|
upstreamTables=[
|
|
UpstreamTable(
|
|
id="table1",
|
|
luid="table1_luid",
|
|
name="test_table",
|
|
referencedByQueries=[
|
|
{
|
|
"id": "query1",
|
|
"name": "test_query",
|
|
"query": "SELECT * FROM test_table",
|
|
}
|
|
],
|
|
)
|
|
],
|
|
)
|
|
],
|
|
)
|
|
|
|
mock_data_model_entity = DashboardDataModel(
|
|
id=uuid.uuid4(),
|
|
name="Test Data Model",
|
|
service=EntityReference(id=uuid.uuid4(), type="dashboardService"),
|
|
dataModelType="TableauDataModel",
|
|
columns=[],
|
|
)
|
|
|
|
mock_upstream_data_model_entity = DashboardDataModel(
|
|
id=uuid.uuid4(),
|
|
name="Upstream Data Model",
|
|
service=EntityReference(id=uuid.uuid4(), type="dashboardService"),
|
|
dataModelType="TableauDataModel",
|
|
columns=[],
|
|
)
|
|
|
|
# Mock the client to return custom SQL queries
|
|
self.tableau.client.get_custom_sql_table_queries = MagicMock(
|
|
return_value=["SELECT * FROM test_table"]
|
|
)
|
|
|
|
# Mock the _get_datamodel method
|
|
with patch.object(
|
|
self.tableau, "_get_datamodel", return_value=mock_upstream_data_model_entity
|
|
):
|
|
# Mock the metadata search to return empty results (simulating no table entities found)
|
|
with patch.object(
|
|
self.tableau.metadata, "search_in_any_service", return_value=[]
|
|
):
|
|
# Mock the _get_add_lineage_request method to avoid actual lineage creation
|
|
with patch.object(
|
|
self.tableau, "_get_add_lineage_request"
|
|
) as mock_lineage_request:
|
|
# Call the method under test
|
|
lineage_results = list(
|
|
self.tableau._get_datamodel_table_lineage(
|
|
datamodel=mock_datamodel,
|
|
data_model_entity=mock_data_model_entity,
|
|
db_service_prefix=None,
|
|
)
|
|
)
|
|
|
|
# Verify that the method completes without throwing an error
|
|
# Even though from_entities is empty, the method should handle it gracefully
|
|
self.assertIsInstance(lineage_results, list)
|
|
|
|
# Verify that the lineage request was called for the datamodel lineage
|
|
# (but not for table lineage since from_entities was empty)
|
|
mock_lineage_request.assert_called()
|
|
|
|
# Verify that the method didn't throw any exceptions
|
|
# The test passes if we reach this point without exceptions
|
|
|
|
def test_get_datamodel_table_lineage_with_none_from_entities(self):
|
|
"""
|
|
Test that _get_datamodel_table_lineage handles None from_entities gracefully
|
|
"""
|
|
# Mock data for the test
|
|
mock_datamodel = DataSource(
|
|
id="datasource2",
|
|
name="Test Datasource 2",
|
|
upstreamDatasources=[
|
|
DataSource(
|
|
id="upstream_datasource2",
|
|
name="Upstream Datasource 2",
|
|
upstreamTables=[
|
|
UpstreamTable(
|
|
id="table2",
|
|
luid="table2_luid",
|
|
name="test_table_2",
|
|
referencedByQueries=[
|
|
{
|
|
"id": "query2",
|
|
"name": "test_query_2",
|
|
"query": "SELECT * FROM test_table_2",
|
|
}
|
|
],
|
|
)
|
|
],
|
|
)
|
|
],
|
|
)
|
|
|
|
mock_data_model_entity = DashboardDataModel(
|
|
id=uuid.uuid4(),
|
|
name="Test Data Model 2",
|
|
service=EntityReference(id=uuid.uuid4(), type="dashboardService"),
|
|
dataModelType="TableauDataModel",
|
|
columns=[],
|
|
)
|
|
|
|
mock_upstream_data_model_entity = DashboardDataModel(
|
|
id=uuid.uuid4(),
|
|
name="Upstream Data Model 2",
|
|
service=EntityReference(id=uuid.uuid4(), type="dashboardService"),
|
|
dataModelType="TableauDataModel",
|
|
columns=[],
|
|
)
|
|
|
|
# Mock the client to return custom SQL queries
|
|
self.tableau.client.get_custom_sql_table_queries = MagicMock(
|
|
return_value=["SELECT * FROM test_table_2"]
|
|
)
|
|
|
|
# Mock the _get_datamodel method
|
|
with patch.object(
|
|
self.tableau, "_get_datamodel", return_value=mock_upstream_data_model_entity
|
|
):
|
|
# Mock the metadata search to return None (simulating search failure)
|
|
with patch.object(
|
|
self.tableau.metadata, "search_in_any_service", return_value=None
|
|
):
|
|
# Mock the _get_add_lineage_request method to avoid actual lineage creation
|
|
with patch.object(
|
|
self.tableau, "_get_add_lineage_request"
|
|
) as mock_lineage_request:
|
|
# Call the method under test
|
|
lineage_results = list(
|
|
self.tableau._get_datamodel_table_lineage(
|
|
datamodel=mock_datamodel,
|
|
data_model_entity=mock_data_model_entity,
|
|
db_service_prefix=None,
|
|
)
|
|
)
|
|
|
|
# Verify that the method completes without throwing an error
|
|
# Even though from_entities is None, the method should handle it gracefully
|
|
self.assertIsInstance(lineage_results, list)
|
|
|
|
# Verify that the lineage request was called for the datamodel lineage
|
|
# (but not for table lineage since from_entities was None)
|
|
mock_lineage_request.assert_called()
|
|
|
|
# Verify that the method didn't throw any exceptions
|
|
# The test passes if we reach this point without exceptions
|