open-metadata--openmetadata
bf2343b7e4
Integration Tests - MySQL + Elasticsearch / Detect Changes (push) Has been cancelled
Integration Tests - MySQL + Elasticsearch / integration-tests-mysql-elasticsearch (push) Has been cancelled
Integration Tests - PostgreSQL + Elasticsearch + Redis / Detect Changes (push) Has been cancelled
Integration Tests - PostgreSQL + Elasticsearch + Redis / integration-tests-postgres-elasticsearch-redis (push) Has been cancelled
Integration Tests - PostgreSQL + OpenSearch / Detect Changes (push) Has been cancelled
Integration Tests - PostgreSQL + OpenSearch / integration-tests-postgres-opensearch (push) Has been cancelled
Java Checkstyle / java-checkstyle (push) Has been cancelled
Maven Collate Tests / maven-collate-ci (push) Has been cancelled
OpenMetadata Service Unit Tests / openmetadata-service-unit-tests-status (push) Has been cancelled
Publish Package to Maven Central Repository / publish-maven-packages (push) Has been cancelled
OpenMetadata Service Unit Tests / Detect Changes (push) Has been cancelled
OpenMetadata Service Unit Tests / openmetadata-service-unit-tests (push) Has been cancelled
OpenMetadata Service Unit Tests / k8s_operator-unit-tests (push) Has been cancelled
202 行
7.8 KiB
Python
202 行
7.8 KiB
Python
# Copyright 2025 Collate
|
|
# Licensed under the Collate Community License, Version 1.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
# https://github.com/open-metadata/OpenMetadata/blob/main/ingestion/LICENSE
|
|
# 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.
|
|
"""Unit tests for Power BI test-connection checks."""
|
|
|
|
import socket
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
import pytest
|
|
from requests.exceptions import HTTPError
|
|
|
|
from metadata.core.connections.test_connection.check import CheckError, collect_checks
|
|
from metadata.core.connections.test_connection.checks.dashboard import DashboardStep
|
|
from metadata.ingestion.api.steps import InvalidSourceException
|
|
from metadata.ingestion.connections.connection import BaseConnection
|
|
from metadata.ingestion.ometa.client import APIError
|
|
from metadata.ingestion.source.dashboard.powerbi.connection import (
|
|
POWERBI_ERRORS,
|
|
PowerBIChecks,
|
|
PowerBIConnection,
|
|
)
|
|
|
|
CONNECTION_MODULE = "metadata.ingestion.source.dashboard.powerbi.connection"
|
|
|
|
|
|
def _api_error(status_code: int, message: str = "boom") -> APIError:
|
|
http_error = MagicMock()
|
|
http_error.response.status_code = status_code
|
|
return APIError({"message": message, "code": "x"}, http_error)
|
|
|
|
|
|
def _raw_http_error(status_code: int) -> HTTPError:
|
|
"""A bare requests.HTTPError as the client re-raises when the body has no
|
|
'code' field - status lives on .response.status_code, not .status_code."""
|
|
response = MagicMock()
|
|
response.status_code = status_code
|
|
return HTTPError("server said no", response=response)
|
|
|
|
|
|
def _checks(api_client_cls) -> tuple[PowerBIChecks, MagicMock]:
|
|
"""A provider whose lazily-built REST client is the returned mock."""
|
|
api_client = MagicMock()
|
|
api_client_cls.return_value = api_client
|
|
return PowerBIChecks(connection=MagicMock()), api_client
|
|
|
|
|
|
def test_powerbi_connection_is_base_connection():
|
|
assert issubclass(PowerBIConnection, BaseConnection)
|
|
|
|
|
|
def test_checks_does_not_touch_the_network():
|
|
with patch(f"{CONNECTION_MODULE}.PowerBiApiClient") as mock_client:
|
|
conn = PowerBIConnection(MagicMock())
|
|
provider = conn.checks()
|
|
|
|
assert isinstance(provider, PowerBIChecks)
|
|
mock_client.assert_not_called()
|
|
|
|
|
|
def test_collect_checks_maps_every_step():
|
|
with patch(f"{CONNECTION_MODULE}.PowerBiApiClient") as mock_client:
|
|
provider, _ = _checks(mock_client)
|
|
collected = collect_checks(provider)
|
|
|
|
assert set(collected) == {DashboardStep.CheckAccess, DashboardStep.GetDashboards}
|
|
|
|
|
|
def test_check_access_authenticates():
|
|
with patch(f"{CONNECTION_MODULE}.PowerBiApiClient") as mock_client:
|
|
provider, client = _checks(mock_client)
|
|
client.get_auth_token.return_value = ("token", "3600")
|
|
|
|
evidence = provider.check_access()
|
|
|
|
client.get_auth_token.assert_called_once_with()
|
|
assert evidence.summary == "authenticated"
|
|
assert evidence.command == "acquire OAuth token"
|
|
|
|
|
|
def test_check_access_wraps_failure_as_check_error():
|
|
with patch(f"{CONNECTION_MODULE}.PowerBiApiClient") as mock_client:
|
|
provider, client = _checks(mock_client)
|
|
client.get_auth_token.side_effect = InvalidSourceException("no token")
|
|
|
|
with pytest.raises(CheckError) as exc_info:
|
|
provider.check_access()
|
|
|
|
assert isinstance(exc_info.value.cause, InvalidSourceException)
|
|
assert exc_info.value.evidence.command == "acquire OAuth token"
|
|
|
|
|
|
def test_get_dashboards_counts_results():
|
|
with patch(f"{CONNECTION_MODULE}.PowerBiApiClient") as mock_client:
|
|
provider, client = _checks(mock_client)
|
|
client.fetch_dashboards.return_value = [object(), object(), object()]
|
|
|
|
evidence = provider.get_dashboards()
|
|
|
|
assert evidence.summary == "3 dashboards enumerated"
|
|
assert evidence.command == "fetch dashboards"
|
|
|
|
|
|
def test_get_dashboards_caps_the_count():
|
|
with patch(f"{CONNECTION_MODULE}.PowerBiApiClient") as mock_client:
|
|
provider, client = _checks(mock_client)
|
|
client.fetch_dashboards.return_value = [object()] * 250
|
|
|
|
evidence = provider.get_dashboards()
|
|
|
|
assert evidence.summary == "100+ dashboards enumerated"
|
|
assert evidence.caveat is None
|
|
|
|
|
|
def test_get_dashboards_at_cap_shows_plus():
|
|
with patch(f"{CONNECTION_MODULE}.PowerBiApiClient") as mock_client:
|
|
provider, client = _checks(mock_client)
|
|
client.fetch_dashboards.return_value = [object()] * 100
|
|
|
|
evidence = provider.get_dashboards()
|
|
|
|
assert evidence.summary == "100+ dashboards enumerated"
|
|
|
|
|
|
def test_get_dashboards_empty_passes_with_caveat():
|
|
with patch(f"{CONNECTION_MODULE}.PowerBiApiClient") as mock_client:
|
|
provider, client = _checks(mock_client)
|
|
client.fetch_dashboards.return_value = None
|
|
|
|
evidence = provider.get_dashboards()
|
|
|
|
assert evidence.summary == "0 dashboards enumerated"
|
|
assert evidence.caveat is not None
|
|
assert evidence.caveat.title == "No dashboards visible"
|
|
|
|
|
|
def test_get_dashboards_wraps_failure_as_check_error():
|
|
with patch(f"{CONNECTION_MODULE}.PowerBiApiClient") as mock_client:
|
|
provider, client = _checks(mock_client)
|
|
client.fetch_dashboards.side_effect = _api_error(403)
|
|
|
|
with pytest.raises(CheckError) as exc_info:
|
|
provider.get_dashboards()
|
|
|
|
assert exc_info.value.evidence.command == "fetch dashboards"
|
|
|
|
|
|
def test_get_dashboards_wraps_client_build_failure():
|
|
# Client construction (MSAL discovery) happens inside fetch_list's try, so a
|
|
# build failure still reports the step's command rather than escaping raw.
|
|
with patch(f"{CONNECTION_MODULE}.PowerBiApiClient") as mock_client:
|
|
provider = PowerBIChecks(connection=MagicMock())
|
|
mock_client.side_effect = ValueError("authority discovery failed")
|
|
|
|
with pytest.raises(CheckError) as exc_info:
|
|
provider.get_dashboards()
|
|
|
|
assert exc_info.value.evidence.command == "fetch dashboards"
|
|
assert isinstance(exc_info.value.cause, ValueError)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("error", "title"),
|
|
[
|
|
(_api_error(401), "Power BI did not authorize the service principal"),
|
|
(InvalidSourceException("bad creds"), "Authentication failed"),
|
|
(_api_error(403), "Insufficient permissions"),
|
|
(_api_error(404), "Resource not found"),
|
|
(_raw_http_error(401), "Power BI did not authorize the service principal"),
|
|
(_raw_http_error(403), "Insufficient permissions"),
|
|
(_raw_http_error(404), "Resource not found"),
|
|
# A status-coded 401 whose message echoes the authority URL must classify
|
|
# by its status, not be shadowed by the broad 'authority' matcher.
|
|
(
|
|
_api_error(401, "AADSTS700016 https://login.microsoftonline.com/<tenant> authority"),
|
|
"Power BI did not authorize the service principal",
|
|
),
|
|
(ValueError("Unable to get authority configuration for https://login..."), "Invalid tenant or authority"),
|
|
(ValueError("invalid_instance: The authority you provided is not known"), "Invalid tenant or authority"),
|
|
(
|
|
ValueError("Your given address (https://login...) should consist of an https url"),
|
|
"Invalid tenant or authority",
|
|
),
|
|
(socket.gaierror("name resolution failed"), "Host could not be resolved"),
|
|
],
|
|
)
|
|
def test_error_pack_classifies(error, title):
|
|
diagnosis = POWERBI_ERRORS.classify(error)
|
|
|
|
assert diagnosis is not None
|
|
assert diagnosis.title == title
|
|
|
|
|
|
def test_error_pack_ignores_unknown_error():
|
|
assert POWERBI_ERRORS.classify(ValueError("something else")) is None
|