Suman Maharana 3a90b38a26
Fix: Tableau ca cert auth (#22041)
* Fix: Tableau ca cert auth

* py_format

* Added ssl tests

* fix lint errors
2025-07-30 09:38:47 +05:30

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