Files
wehub-resource-sync 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
chore: import upstream snapshot with attribution
2026-07-13 13:35:45 +08:00

382 lines
15 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 Athena connection handling."""
import socket
from unittest.mock import MagicMock, patch
import pytest
from botocore.exceptions import ClientError
from metadata.core.connections.test_connection.check import collect_checks
from metadata.core.connections.test_connection.checks.database import DatabaseStep
from metadata.core.connections.test_connection.records import Evidence
from metadata.generated.schema.entity.services.connections.database.athenaConnection import (
AthenaConnection as AthenaConnectionConfig,
)
from metadata.generated.schema.entity.services.connections.database.athenaConnection import (
AthenaScheme,
)
from metadata.generated.schema.security.credentials.awsCredentials import AWSCredentials
from metadata.generated.schema.type.filterPattern import FilterPattern
from metadata.ingestion.connections.connection import BaseConnection
from metadata.ingestion.source.database.athena import connection as athena_connection
from metadata.ingestion.source.database.athena.connection import (
ATHENA_ERRORS,
AthenaChecks,
AthenaConnection,
)
CONNECTION_MODULE = "metadata.ingestion.source.database.athena.connection"
def _config(**kwargs) -> AthenaConnectionConfig:
base = {
"awsConfig": AWSCredentials(awsAccessKeyId="key", awsRegion="us-east-2", awsSecretAccessKey="secret_key"),
"s3StagingDir": "s3://postgres/input/",
"workgroup": "primary",
"scheme": AthenaScheme.awsathena_rest,
}
base.update(kwargs)
return AthenaConnectionConfig(**base)
def _checks(**config_kwargs) -> AthenaChecks:
conn = AthenaConnection(_config(**config_kwargs))
conn._client = MagicMock()
return conn.checks()
def _client_error(code: str, message: str = "denied") -> ClientError:
return ClientError({"Error": {"Code": code, "Message": message}}, "StartQueryExecution")
def _raising_factory(exc: BaseException):
def factory():
raise exc
return factory
def test_athena_connection_is_base_connection():
assert issubclass(AthenaConnection, BaseConnection)
def test_get_client_uses_the_class_url_builder():
with patch(f"{CONNECTION_MODULE}.create_generic_db_connection") as mock_connection:
_ = AthenaConnection(_config()).client
assert mock_connection.call_args.kwargs["get_connection_url_fn"].__name__ == "get_connection_url"
def test_get_client_registers_engine_disposal():
conn = AthenaConnection(_config())
with patch(f"{CONNECTION_MODULE}.create_generic_db_connection") as mock_connection:
engine = mock_connection.return_value
_ = conn.client
conn.close()
engine.dispose.assert_called_once_with()
def test_checks_returns_provider_over_the_client():
conn = AthenaConnection(_config())
conn._client = MagicMock()
provider = conn.checks()
assert isinstance(provider, AthenaChecks)
assert provider.client is conn._client
def test_checks_wires_filter_pattern_and_catalog_from_service_connection():
pattern = FilterPattern(includes=["db2"])
conn = AthenaConnection(_config(catalogId="my_catalog", schemaFilterPattern=pattern))
conn._client = MagicMock()
provider = conn.checks()
assert provider.schema_filter_pattern == pattern
assert provider.catalog_id == "my_catalog"
def test_every_athena_step_resolves_to_a_check():
provider = AthenaChecks(client_factory=MagicMock)
resolved = collect_checks(provider)
assert set(resolved) == {
DatabaseStep.CheckAccess,
DatabaseStep.GetSchemas,
DatabaseStep.GetTables,
DatabaseStep.GetViews,
}
def test_every_check_reports_the_command_it_ran():
# pyathena reflects via the AWS API, so the shared SQL capture yields nothing;
# each reflection step names its API operation instead.
provider = _checks()
inspector = MagicMock()
inspector.get_schema_names.return_value = ["ecommerce"]
inspector.get_table_names.return_value = ["orders"]
inspector.get_view_names.return_value = ["v_orders"]
with (
patch(f"{CONNECTION_MODULE}.list_schemas", return_value=Evidence(summary="1 schema enumerated")),
patch(f"{CONNECTION_MODULE}.inspect", return_value=inspector),
):
assert provider.get_schemas().command == "athena:ListDatabases (CatalogName=AwsDataCatalog)"
assert provider.get_tables().command == "athena:ListTableMetadata (CatalogName=AwsDataCatalog)"
assert provider.get_views().command == "athena:ListTableMetadata (CatalogName=AwsDataCatalog)"
def test_command_names_a_configured_catalog():
provider = _checks(catalogId="my_catalog")
with patch(f"{CONNECTION_MODULE}.list_schemas", return_value=Evidence(summary="ok")):
assert provider.get_schemas().command == "athena:ListDatabases (CatalogName=my_catalog)"
def test_error_pack_classifies_access_denied_as_not_authorized():
diagnosis = ATHENA_ERRORS.classify(_client_error("AccessDeniedException"))
assert diagnosis is not None
assert diagnosis.title == "Not authorized"
def test_error_pack_classifies_unrecognized_client_as_unknown_access_key():
# Athena is json-protocol: an unknown access key ID arrives as this.
diagnosis = ATHENA_ERRORS.classify(_client_error("UnrecognizedClientException"))
assert diagnosis is not None
assert diagnosis.title == "AWS access key not recognized"
def test_error_pack_classifies_a_signature_error_through_a_wrapping_cause():
wrapped = RuntimeError("query failed")
wrapped.__cause__ = _client_error("InvalidSignatureException")
diagnosis = ATHENA_ERRORS.classify(wrapped)
assert diagnosis is not None
assert diagnosis.title == "AWS secret key does not match"
def test_error_pack_classifies_missing_workgroup():
error = RuntimeError("WorkGroup primary is not found")
diagnosis = ATHENA_ERRORS.classify(error)
assert diagnosis is not None
assert diagnosis.title == "Workgroup not found"
def test_error_pack_classifies_missing_result_location():
error = RuntimeError("No output location provided for query")
diagnosis = ATHENA_ERRORS.classify(error)
assert diagnosis is not None
assert diagnosis.title == "Query result location not configured"
def test_error_pack_classifies_unwritable_result_location():
error = RuntimeError("Access denied when writing to location: s3://my-bucket/query-results/abc.csv")
diagnosis = ATHENA_ERRORS.classify(error)
assert diagnosis is not None
assert diagnosis.title == "Cannot write query results"
def test_error_pack_classifies_an_unusable_result_bucket():
error = RuntimeError("An error occurred (InvalidRequestException): Unable to verify/create output bucket my-bucket")
diagnosis = ATHENA_ERRORS.classify(error)
assert diagnosis is not None
assert diagnosis.title == "Query result bucket not usable"
def test_error_pack_classifies_unreachable_endpoint():
error = RuntimeError('Could not connect to the endpoint URL: "https://athena.bad.amazonaws.com/"')
diagnosis = ATHENA_ERRORS.classify(error)
assert diagnosis is not None
assert diagnosis.title == "Cannot reach the AWS Athena endpoint"
def test_error_pack_classifies_not_authorized():
diagnosis = ATHENA_ERRORS.classify(
_client_error("SomeOtherException", "User is not authorized to perform: glue:GetTables")
)
assert diagnosis is not None
assert diagnosis.title == "Not authorized"
def test_error_pack_folds_in_the_network_pack():
diagnosis = ATHENA_ERRORS.classify(socket.gaierror("name resolution failed"))
assert diagnosis is not None
assert diagnosis.title == "Host could not be resolved"
def test_error_pack_classifies_sts_assume_role_denied_as_not_authorized():
diagnosis = ATHENA_ERRORS.classify(_client_error("AccessDenied", "not authorized to perform: sts:AssumeRole"))
assert diagnosis is not None
assert diagnosis.title == "Not authorized"
def test_error_pack_classifies_invalid_client_token_as_unknown_access_key():
# STS's code, from the assume-role leg.
diagnosis = ATHENA_ERRORS.classify(_client_error("InvalidClientTokenId"))
assert diagnosis is not None
assert diagnosis.title == "AWS access key not recognized"
def test_error_pack_classifies_signature_mismatch_as_wrong_secret():
diagnosis = ATHENA_ERRORS.classify(_client_error("SignatureDoesNotMatch"))
assert diagnosis is not None
assert diagnosis.title == "AWS secret key does not match"
def test_error_pack_classifies_expired_token():
diagnosis = ATHENA_ERRORS.classify(_client_error("ExpiredToken"))
assert diagnosis is not None
assert diagnosis.title == "AWS session token expired"
def test_error_pack_prefers_an_authentication_code_over_not_authorized_text():
# The authorization rule sits above AWS_ERRORS; its message fallback must not
# swallow a rejected identity.
diagnosis = ATHENA_ERRORS.classify(_client_error("ExpiredToken", "not authorized: token expired"))
assert diagnosis is not None
assert diagnosis.title == "AWS session token expired"
def test_error_pack_returns_none_for_unknown_error():
assert ATHENA_ERRORS.classify(RuntimeError("something unrelated")) is None
def test_client_is_built_lazily_not_at_construction():
calls = []
provider = AthenaChecks(client_factory=lambda: calls.append(1) or MagicMock())
assert calls == []
_ = provider.client
_ = provider.client
assert calls == [1]
def test_check_access_surfaces_client_build_failure_for_classification():
# An assume-role / credential failure while building the engine now happens
# inside CheckAccess (the gate), so its exception propagates from the check and
# the runner can classify it - instead of raising at provider construction.
error = _client_error("InvalidClientTokenId", "The security token included in the request is invalid")
provider = AthenaChecks(client_factory=_raising_factory(error))
with pytest.raises(ClientError) as exc_info:
provider.check_access()
assert ATHENA_ERRORS.classify(exc_info.value).title == "AWS access key not recognized"
def test_athena_url():
expected = (
"awsathena+rest://key:secret_key@athena.us-east-2.amazonaws.com:443"
"?s3_staging_dir=s3%3A%2F%2Fpostgres%2Finput%2F&work_group=primary"
)
assert AthenaConnection.get_connection_url(_config()) == expected
def test_athena_url_other_staging_dir():
expected = (
"awsathena+rest://key:secret_key@athena.us-east-2.amazonaws.com:443"
"?s3_staging_dir=s3%3A%2F%2Fpostgres%2Fintput%2F&work_group=primary"
)
assert AthenaConnection.get_connection_url(_config(s3StagingDir="s3://postgres/intput/")) == expected
def test_get_tables_caveats_when_all_targeted_schemas_empty():
checks = _checks(catalogId="my_catalog")
inspector = MagicMock()
inspector.get_schema_names.return_value = ["db1", "db2"]
inspector.get_table_names.return_value = []
with patch(f"{CONNECTION_MODULE}.inspect", return_value=inspector):
evidence = checks.get_tables()
assert evidence.caveat is not None
assert evidence.caveat.title == "No readable tables"
def test_get_tables_passes_without_caveat_when_a_schema_has_tables():
checks = _checks()
inspector = MagicMock()
inspector.get_schema_names.return_value = ["db1", "db2"]
inspector.get_table_names.side_effect = lambda schema: ["t1"] if schema == "db2" else []
with patch(f"{CONNECTION_MODULE}.inspect", return_value=inspector):
evidence = checks.get_tables()
assert evidence.caveat is None
def test_get_tables_honors_schema_filter_pattern():
checks = _checks(schemaFilterPattern=FilterPattern(includes=["db2"]))
inspector = MagicMock()
inspector.get_schema_names.return_value = ["db1", "db2"]
inspector.get_table_names.side_effect = lambda schema: ["t1"] if schema == "db2" else []
with patch(f"{CONNECTION_MODULE}.inspect", return_value=inspector):
checks.get_tables()
probed = {call.args[0] for call in inspector.get_table_names.call_args_list}
assert probed == {"db2"}
def test_get_tables_caveats_when_filter_matches_nothing():
checks = _checks(catalogId="my_catalog", schemaFilterPattern=FilterPattern(includes=["nope"]))
inspector = MagicMock()
inspector.get_schema_names.return_value = ["db1", "db2"]
with patch(f"{CONNECTION_MODULE}.inspect", return_value=inspector):
evidence = checks.get_tables()
assert evidence.caveat is not None
assert evidence.caveat.title == "No schemas visible"
assert "my_catalog" in evidence.caveat.remediation
inspector.get_table_names.assert_not_called()
def test_get_views_does_not_raise_or_caveat_when_empty():
checks = _checks()
inspector = MagicMock()
inspector.get_schema_names.return_value = ["db1"]
inspector.get_view_names.return_value = []
with patch(f"{CONNECTION_MODULE}.inspect", return_value=inspector):
evidence = checks.get_views()
assert evidence.caveat is None
def test_get_views_reports_visible_views():
checks = _checks()
inspector = MagicMock()
inspector.get_schema_names.return_value = ["db1"]
inspector.get_view_names.return_value = ["v1"]
with patch(f"{CONNECTION_MODULE}.inspect", return_value=inspector):
evidence = checks.get_views()
assert evidence.caveat is None
assert "views visible" in evidence.summary
def test_get_tables_caveat_is_actionable():
checks = _checks(catalogId="my_catalog")
inspector = MagicMock()
inspector.get_schema_names.return_value = ["db1"]
inspector.get_table_names.return_value = []
with patch(f"{CONNECTION_MODULE}.inspect", return_value=inspector):
evidence = checks.get_tables()
remediation = evidence.caveat.remediation
assert "Lake Formation" in remediation
assert any(keyword in remediation.lower() for keyword in ("describe", "select", "grant"))
def test_get_tables_caps_probing_at_max_schemas():
checks = _checks(catalogId="my_catalog")
inspector = MagicMock()
inspector.get_schema_names.return_value = [f"db{i}" for i in range(500)]
inspector.get_table_names.return_value = []
with patch(f"{CONNECTION_MODULE}.inspect", return_value=inspector):
evidence = checks.get_tables()
assert evidence.caveat is not None
assert inspector.get_table_names.call_count <= athena_connection.MAX_SCHEMAS_TO_PROBE
def test_targeted_schemas_listed_once_across_table_and_view_steps():
checks = _checks()
inspector = MagicMock()
inspector.get_schema_names.return_value = ["db1"]
inspector.get_table_names.return_value = ["t1"]
inspector.get_view_names.return_value = []
with patch(f"{CONNECTION_MODULE}.inspect", return_value=inspector):
checks.get_tables()
checks.get_views()
assert inspector.get_schema_names.call_count == 1