Files
open-metadata--openmetadata/ingestion/tests/unit/source/storage/s3/test_connection.py
T
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

237 lines
8.1 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.
"""S3 test-connection: its checks match its shipped definition."""
import json
from pathlib import Path
from unittest.mock import MagicMock, patch
import pytest
from botocore.exceptions import ClientError, EndpointConnectionError, NoCredentialsError
from metadata.core.connections.test_connection.check import CheckError, collect_checks
from metadata.core.connections.test_connection.checks.storage import list_buckets
from metadata.ingestion.connections.connection import BaseConnection
from metadata.ingestion.source.storage.s3.connection import (
S3_ERRORS,
S3Checks,
S3Connection,
)
CONNECTION_MODULE = "metadata.ingestion.source.storage.s3.connection"
_SEED = (
Path(__file__).parents[6] / "openmetadata-service/src/main/resources/json/data" / "testConnections/storage/s3.json"
)
def _checks(client=None, bucket_names=None):
return S3Checks(connect=lambda: client, bucket_names=bucket_names)
def _check_names():
return {step.value for step in collect_checks(_checks())}
def test_s3_connection_is_base_connection():
assert issubclass(S3Connection, BaseConnection)
def test_get_client_delegates_to_get_connection():
with patch(f"{CONNECTION_MODULE}.get_connection") as mock_get:
conn = S3Connection(MagicMock())
client = conn.client
assert client is mock_get.return_value
mock_get.assert_called_once_with(conn.service_connection)
def test_s3_checks_cover_expected_steps():
assert _check_names() == {"ListBuckets", "GetMetrics"}
def test_s3_checks_match_definition_seed():
definition_steps = {step["name"] for step in json.loads(_SEED.read_text())["steps"]}
assert _check_names() == definition_steps
def test_building_checks_does_not_build_the_client():
with patch.object(S3Connection, "_get_client") as mock_build:
S3Connection(MagicMock()).checks()
mock_build.assert_not_called()
def test_check_buckets_lists_all_buckets_when_none_configured():
client = MagicMock()
client.s3_client.list_buckets.return_value = {"Buckets": [{"Name": "a"}, {"Name": "b"}]}
evidence = _checks(client).check_buckets()
assert evidence.summary == "2 buckets enumerated"
assert evidence.command == "s3:ListBuckets"
assert evidence.caveat is None
client.s3_client.list_buckets.assert_called_once_with()
client.s3_client.list_objects.assert_not_called()
def test_list_buckets_caps_reported_count_and_flags_more():
client = MagicMock()
client.list_buckets.return_value = {"Buckets": [{"Name": "a"}, {"Name": "b"}, {"Name": "c"}]}
evidence = list_buckets(client, limit=2)
assert evidence.summary == "2 buckets enumerated (showing first 2; more exist)"
client.list_buckets.assert_called_once_with()
def test_check_buckets_warns_when_no_buckets_visible():
client = MagicMock()
client.s3_client.list_buckets.return_value = {"Buckets": []}
evidence = _checks(client).check_buckets()
assert evidence.caveat is not None
assert "No buckets visible" in evidence.caveat.title
def test_check_buckets_probes_each_configured_bucket():
client = MagicMock()
evidence = _checks(client, bucket_names=["one", "two"]).check_buckets()
assert evidence.summary == "2 configured buckets accessible"
client.s3_client.list_objects.assert_any_call(Bucket="one", MaxKeys=1)
client.s3_client.list_objects.assert_any_call(Bucket="two", MaxKeys=1)
client.s3_client.list_buckets.assert_not_called()
def test_check_buckets_failure_reports_the_bucket_it_probed():
client = MagicMock()
cause = _client_error("NoSuchBucket", "ListObjects")
client.s3_client.list_objects.side_effect = cause
with pytest.raises(CheckError) as failure:
_checks(client, bucket_names=["missing"]).check_buckets()
assert failure.value.cause is cause
assert failure.value.evidence.command == "s3:ListBucket (missing)"
def test_get_metrics_lists_the_s3_namespace():
client = MagicMock()
client.cloudwatch_client.list_metrics.return_value = {"Metrics": [{}]}
evidence = _checks(client).get_metrics()
assert evidence.summary == "1 metric visible in namespace 'AWS/S3'"
client.cloudwatch_client.list_metrics.assert_called_once_with(Namespace="AWS/S3")
def test_get_metrics_flags_truncated_results():
client = MagicMock()
client.cloudwatch_client.list_metrics.return_value = {"Metrics": [{}], "NextToken": "more"}
evidence = _checks(client).get_metrics()
assert "more exist" in evidence.summary
def _client_error(code: str, operation: str) -> ClientError:
return ClientError({"Error": {"Code": code, "Message": "denied"}}, operation)
def test_error_pack_invalid_access_key():
diagnosis = S3_ERRORS.classify(_client_error("InvalidAccessKeyId", "ListBuckets"))
assert diagnosis is not None
assert "access key" in diagnosis.title.lower()
def test_error_pack_signature_mismatch():
diagnosis = S3_ERRORS.classify(_client_error("SignatureDoesNotMatch", "ListBuckets"))
assert diagnosis is not None
assert "secret key" in diagnosis.title.lower()
def test_error_pack_unrecognized_client():
diagnosis = S3_ERRORS.classify(_client_error("UnrecognizedClientException", "ListMetrics"))
assert diagnosis is not None
assert diagnosis.title == "AWS access key not recognized"
def test_error_pack_invalid_client_token():
# STS's code for an unknown access key ID.
diagnosis = S3_ERRORS.classify(_client_error("InvalidClientTokenId", "ListBuckets"))
assert diagnosis is not None
assert diagnosis.title == "AWS access key not recognized"
def test_error_pack_expired_token():
diagnosis = S3_ERRORS.classify(_client_error("ExpiredToken", "ListBuckets"))
assert diagnosis is not None
assert "expired" in diagnosis.title.lower()
def test_error_pack_bucket_not_found():
diagnosis = S3_ERRORS.classify(_client_error("NoSuchBucket", "ListObjects"))
assert diagnosis is not None
assert "not found" in diagnosis.title.lower()
def test_error_pack_access_denied():
diagnosis = S3_ERRORS.classify(
ClientError(
{
"Error": {
"Code": "AccessDenied",
"Message": "User is not authorized to perform: s3:ListAllMyBuckets",
}
},
"ListBuckets",
)
)
assert diagnosis is not None
assert "authorized" in diagnosis.title.lower()
def test_error_pack_access_denied_exception_variant():
diagnosis = S3_ERRORS.classify(_client_error("AccessDeniedException", "ListMetrics"))
assert diagnosis is not None
assert "authorized" in diagnosis.title.lower()
def test_error_pack_no_credentials():
diagnosis = S3_ERRORS.classify(NoCredentialsError())
assert diagnosis is not None
assert "credentials" in diagnosis.title.lower()
def test_error_pack_endpoint_unreachable():
diagnosis = S3_ERRORS.classify(EndpointConnectionError(endpoint_url="https://s3.bad-region.amazonaws.com"))
assert diagnosis is not None
assert "endpoint" in diagnosis.title.lower()
def test_error_pack_matches_structured_code_not_message_text():
# The structured code is NoSuchBucket; the message echoes "AccessDenied".
# Matching the code (not the text) must classify it as the bucket error.
error = ClientError(
{"Error": {"Code": "NoSuchBucket", "Message": "bucket 'AccessDenied-logs' does not exist"}},
"ListObjects",
)
diagnosis = S3_ERRORS.classify(error)
assert diagnosis is not None
assert "not found" in diagnosis.title.lower()
def test_error_pack_unmatched_returns_none():
assert S3_ERRORS.classify(Exception("novel error")) is None