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

345 lines
14 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 Snowflake test-connection checks and error classification.
Cover the wiring (steps resolve to checks, the network pack is folded in, nothing
connects at construction) and the error-pack mapping (each scenario classifies to
the intended diagnosis).
"""
from unittest.mock import MagicMock, patch
import pytest
from metadata.core.connections.test_connection import Evidence
from metadata.core.connections.test_connection.check import CheckError, collect_checks
from metadata.core.connections.test_connection.checks.database import (
DEFAULT_SAMPLE_ROWS,
DatabaseStep,
)
from metadata.core.connections.test_connection.network import NetworkUnreachableError
from metadata.generated.schema.entity.services.connections.database.snowflakeConnection import (
SnowflakeConnection as SnowflakeConnectionConfig,
)
from metadata.ingestion.source.database.snowflake.connection import (
SNOWFLAKE_ERRORS,
SNOWFLAKE_PORT,
SnowflakeChecks,
_count_summary,
)
def _config(**overrides) -> SnowflakeConnectionConfig:
base = {"username": "user", "account": "ue18849.us-east-2.aws", "warehouse": "wh"}
base.update(overrides)
return SnowflakeConnectionConfig(**base)
class _SnowflakeError(Exception):
"""Mirror a snowflake.connector error: the code lives on ``.errno`` (not in
``args[0]`` as for PyMySQL), the message is the first arg."""
def __init__(self, message: str = "", errno: int | None = None) -> None:
super().__init__(message)
self.errno = errno
class _SqlAlchemyError(Exception):
"""Mirror ``sqlalchemy.exc.DBAPIError``: wraps the driver error on ``.orig``."""
def __init__(self, orig: Exception) -> None:
super().__init__(str(orig))
self.orig = orig
def test_auth_failure_errno_is_classified():
error = _SqlAlchemyError(_SnowflakeError("250001 (08001): Failed to connect to DB", errno=250001))
assert SNOWFLAKE_ERRORS.classify(error).title == "Authentication failed"
def test_auth_failure_message_is_classified():
error = _SqlAlchemyError(_SnowflakeError("Incorrect username or password were specified."))
assert SNOWFLAKE_ERRORS.classify(error).title == "Authentication failed"
def test_bad_account_login_404_is_classified():
error = _SqlAlchemyError(
_SnowflakeError(
"None: 404 Not Found: post nope99999.us-east-1.snowflakecomputing.com:443/session/v1/login-request",
errno=290404,
)
)
assert SNOWFLAKE_ERRORS.classify(error).title == "Snowflake account not found"
def test_mfa_required_beats_generic_auth():
# errno 250001 alone would read as "Authentication failed"; the MFA message is
# ordered first so the user gets an actionable, specific diagnosis.
error = _SqlAlchemyError(
_SnowflakeError(
"Failed to connect to DB: acc.snowflakecomputing.com:443. "
"Multi-factor authentication is required for this account. Log in to Snowsight to enroll.",
errno=250001,
)
)
assert SNOWFLAKE_ERRORS.classify(error).title == "Multi-factor authentication required"
def test_missing_role_beats_generic_auth():
# A missing role also raises errno 250001; match the role-specific token first.
error = _SqlAlchemyError(
_SnowflakeError(
"Failed to connect to DB: acc.snowflakecomputing.com:443. Role 'NO_SUCH_ROLE' specified in the "
"connect string is not granted to this user, or is not permitted for the credentials being used.",
errno=250001,
)
)
assert SNOWFLAKE_ERRORS.classify(error).title == "Role not granted"
def test_account_usage_denied_is_classified():
error = _SqlAlchemyError(
_SnowflakeError(
"Object 'SNOWFLAKE.ACCOUNT_USAGE.QUERY_HISTORY' does not exist or not authorized.",
errno=2003,
)
)
assert SNOWFLAKE_ERRORS.classify(error).title == "Account usage not accessible"
def test_account_usage_denied_wins_over_generic_object_not_found():
# The same message also matches the generic "does not exist or not authorized"
# rule; the account_usage rule is ordered first so the sharper diagnosis wins.
error = _SnowflakeError(
"SQL compilation error: Object 'SNOWFLAKE.ACCOUNT_USAGE.ACCESS_HISTORY' does not exist or not authorized.",
errno=2003,
)
assert SNOWFLAKE_ERRORS.classify(error).title != "Object not found"
def test_account_usage_denial_honors_custom_schema():
# The pack is built from the configured accountUsageSchema, so a denial on a
# custom schema is still diagnosed as an account_usage gap - not "Object not found".
schema = "MYDB.MY_USAGE"
checks = SnowflakeChecks(client=MagicMock(), service_connection=_config(accountUsageSchema=schema))
error = _SnowflakeError("Object 'MYDB.MY_USAGE.QUERY_HISTORY' does not exist or not authorized.", errno=2003)
assert checks.errors.classify(error).title == "Account usage not accessible"
# the default pack, keyed on snowflake.account_usage, does not recognize the custom schema
assert SNOWFLAKE_ERRORS.classify(error).title == "Object not found"
def test_insufficient_privileges_is_classified():
error = _SnowflakeError("SQL access control error: Insufficient privileges to operate on schema 'PUBLIC'")
assert SNOWFLAKE_ERRORS.classify(error).title == "Insufficient privileges"
def test_object_not_found_errno_is_classified():
error = _SqlAlchemyError(_SnowflakeError("Database 'NOPE' does not exist or not authorized.", errno=2003))
assert SNOWFLAKE_ERRORS.classify(error).title == "Object not found"
def test_missing_database_use_is_classified():
# USE DATABASE on a missing DB -> errno 2043 with a different message
# ("Object does not exist, or operation cannot be performed") than the 2003 form.
error = _SqlAlchemyError(
_SnowflakeError(
"SQL compilation error:\nObject does not exist, or operation cannot be performed.",
errno=2043,
)
)
assert SNOWFLAKE_ERRORS.classify(error).title == "Object not found"
def test_no_active_warehouse_is_classified():
error = _SnowflakeError("No active warehouse selected in the current session.")
assert SNOWFLAKE_ERRORS.classify(error).title == "No active warehouse"
def test_unknown_error_returns_no_diagnosis():
error = _SqlAlchemyError(_SnowflakeError("something unexpected", errno=99999))
assert SNOWFLAKE_ERRORS.classify(error) is None
def test_network_errors_classify_through_including():
error = NetworkUnreachableError("acc.snowflakecomputing.com:443 is not reachable")
error.__cause__ = TimeoutError("timed out")
assert SNOWFLAKE_ERRORS.classify(error).title == "Connection timed out"
def test_count_summary_marks_empty_count_and_cap():
assert _count_summary([], "table") == "no tables enumerated"
assert _count_summary([object()] * 3, "database") == "3 databases enumerated"
assert _count_summary([object()] * 3, "view") == "3 views enumerated"
assert _count_summary([object()] * 1, "stream") == "1 stream enumerated"
assert _count_summary([object()] * 1, "table") == "1 table enumerated"
capped = _count_summary([object()] * DEFAULT_SAMPLE_ROWS, "table")
assert capped == f"{DEFAULT_SAMPLE_ROWS}+ tables enumerated"
def _fake_run_sql(returned_rows):
def fake(client, statement, summarize, *args, **kwargs):
return Evidence(summary=summarize(returned_rows), command="cmd")
return fake
def test_get_tables_warns_when_no_user_tables():
# Empty probe (INFORMATION_SCHEMA filtered out) -> passing step with a caveat,
# which the runner records as a Warning.
checks = SnowflakeChecks(client=MagicMock(), service_connection=_config(database="MYDB"))
with patch(
"metadata.ingestion.source.database.snowflake.connection.run_sql",
side_effect=_fake_run_sql([]),
):
evidence = checks.get_tables()
assert evidence.summary == "no tables enumerated"
assert evidence.caveat is not None
assert evidence.caveat.title == "No tables visible in database 'MYDB'"
assert "privileges" in evidence.caveat.remediation.lower()
def test_get_tables_has_no_caveat_when_tables_exist():
checks = SnowflakeChecks(client=MagicMock(), service_connection=_config(database="MYDB"))
with patch(
"metadata.ingestion.source.database.snowflake.connection.run_sql",
side_effect=_fake_run_sql([object(), object()]),
):
evidence = checks.get_tables()
assert evidence.summary == "2 tables enumerated"
assert evidence.caveat is None
def test_table_and_view_probes_exclude_information_schema():
from metadata.ingestion.source.database.snowflake.queries import (
SNOWFLAKE_TEST_GET_TABLES,
SNOWFLAKE_TEST_GET_VIEWS,
)
for query in (SNOWFLAKE_TEST_GET_TABLES, SNOWFLAKE_TEST_GET_VIEWS):
assert "INFORMATION_SCHEMA" in query
assert "LIMIT 100" in query
def test_checks_cover_exactly_the_wired_steps():
checks = SnowflakeChecks(client=MagicMock(), service_connection=_config())
collected = collect_checks(checks)
assert set(collected.keys()) == {
DatabaseStep.CheckAccess,
DatabaseStep.GetDatabases,
DatabaseStep.GetSchemas,
DatabaseStep.GetTables,
DatabaseStep.GetViews,
DatabaseStep.GetStreams,
DatabaseStep.GetTags,
DatabaseStep.GetQueries,
DatabaseStep.GetAccessHistory,
}
def test_get_access_history_is_wired():
# ACCESS_HISTORY is the default lineage source; its probe is the GetAccessHistory step.
checks = SnowflakeChecks(client=MagicMock(), service_connection=_config())
assert DatabaseStep.GetAccessHistory in collect_checks(checks)
def test_construction_touches_no_network():
# Regression for gotcha #2: building the provider must not connect or resolve a
# database - that would run before the gate and bypass the preflight.
client = MagicMock()
SnowflakeChecks(client=client, service_connection=_config())
client.connect.assert_not_called()
client.execute.assert_not_called()
def test_check_access_probes_account_host_and_reports_network_failure():
# check_access -> tcp_probe(<account>.snowflakecomputing.com, 443); a probe
# failure is wrapped as a CheckError whose cause classifies via the network
# pack. tcp_probe is stubbed so the test is deterministic and fast.
checks = SnowflakeChecks(client=MagicMock(), service_connection=_config(account="acc"))
probe_error = NetworkUnreachableError("acc.snowflakecomputing.com:443 is not reachable")
probe_error.__cause__ = TimeoutError("timed out")
with (
patch(
"metadata.ingestion.source.database.snowflake.connection.tcp_probe",
side_effect=probe_error,
) as mock_probe,
pytest.raises(CheckError) as exc,
):
checks.check_access()
mock_probe.assert_called_once_with("acc.snowflakecomputing.com", SNOWFLAKE_PORT)
assert exc.value.cause is probe_error
assert SNOWFLAKE_ERRORS.classify(exc.value.cause).title == "Connection timed out"
def test_check_access_prefers_explicit_connection_argument_host():
# A proxy / load balancer / PrivateLink endpoint set as connectionArguments
# host is what the driver dials, so the gate probe must target it - not the
# synthesized public account host (which may be unreachable for that deployment).
checks = SnowflakeChecks(
client=MagicMock(),
service_connection=_config(account="acc", connectionArguments={"host": "proxy.internal"}),
)
probe_error = NetworkUnreachableError("proxy.internal:443 is not reachable")
with (
patch(
"metadata.ingestion.source.database.snowflake.connection.tcp_probe",
side_effect=probe_error,
) as mock_probe,
pytest.raises(CheckError),
):
checks.check_access()
mock_probe.assert_called_once_with("proxy.internal", SNOWFLAKE_PORT)
def test_check_access_honors_explicit_connection_argument_port():
# A custom host may listen on a non-443 port; the preflight must probe that
# port, not the default 443, or it false-gates the whole connection test.
checks = SnowflakeChecks(
client=MagicMock(),
service_connection=_config(account="acc", connectionArguments={"host": "proxy.internal", "port": 8443}),
)
with (
patch(
"metadata.ingestion.source.database.snowflake.connection.tcp_probe",
side_effect=NetworkUnreachableError("proxy.internal:8443 is not reachable"),
) as mock_probe,
pytest.raises(CheckError),
):
checks.check_access()
mock_probe.assert_called_once_with("proxy.internal", 8443)
def test_get_schemas_runs_show_schemas_with_command_and_count():
# GetSchemas goes through run_sql (like the other probes), so it reports the
# database-scoped SHOW SCHEMAS command and a counted summary - and a failure
# would surface as a CheckError carrying that command.
checks = SnowflakeChecks(client=MagicMock(), service_connection=_config(database="MYDB"))
captured = {}
def fake(client, statement, summarize, *args, **kwargs):
captured["statement"] = statement
return Evidence(summary=summarize([object(), object()]), command=statement)
with patch("metadata.ingestion.source.database.snowflake.connection.run_sql", side_effect=fake):
evidence = checks.get_schemas()
assert 'SHOW SCHEMAS IN DATABASE "MYDB"' in captured["statement"]
assert evidence.summary == "2 schemas enumerated"
assert evidence.command == captured["statement"]
def test_account_usage_queries_built_lazily_not_at_construction():
# The account_usage statements must be formatted inside their checks (behind
# the gate), never at construction; constructing must not read the engine.
client = MagicMock()
checks = SnowflakeChecks(client=client, service_connection=_config(account="acc"))
assert checks._engine_wrapper.database_name is None
client.connect.assert_not_called()