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
198 lines
7.5 KiB
Python
198 lines
7.5 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 the MSSQL BaseConnection wiring.
|
|
|
|
URL building (including the pyodbc delegation to azuresql) is covered in
|
|
tests/unit/test_source_url.py and tests/unit/test_source_connection.py.
|
|
"""
|
|
|
|
import socket
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
from metadata.core.connections.test_connection import collect_checks
|
|
from metadata.core.connections.test_connection.checks.database import DEFAULT_SAMPLE_ROWS, DatabaseStep
|
|
from metadata.generated.schema.entity.services.connections.database.mssqlConnection import (
|
|
MssqlConnection as MssqlConnectionConfig,
|
|
)
|
|
from metadata.generated.schema.entity.services.connections.database.mssqlConnection import (
|
|
MssqlScheme,
|
|
)
|
|
from metadata.ingestion.connections.connection import BaseConnection
|
|
from metadata.ingestion.source.database.mssql.connection import (
|
|
MSSQL_ERRORS,
|
|
MssqlChecks,
|
|
MssqlConnection,
|
|
_databases_enumerated,
|
|
get_connection_url,
|
|
)
|
|
from metadata.ingestion.source.database.mssql.queries import (
|
|
MSSQL_GET_CURRENT_DATABASE,
|
|
MSSQL_GET_DATABASE,
|
|
)
|
|
|
|
CONNECTION_MODULE = "metadata.ingestion.source.database.mssql.connection"
|
|
|
|
|
|
def _config(scheme: MssqlScheme = MssqlScheme.mssql_pytds) -> MssqlConnectionConfig:
|
|
return MssqlConnectionConfig(
|
|
scheme=scheme,
|
|
username="user",
|
|
password="pass",
|
|
hostPort="myhost:1433",
|
|
database="mydb",
|
|
)
|
|
|
|
|
|
def test_mssql_connection_is_base_connection():
|
|
assert issubclass(MssqlConnection, BaseConnection)
|
|
|
|
|
|
def test_get_client_uses_the_module_url_builder():
|
|
with patch(f"{CONNECTION_MODULE}.create_generic_db_connection") as mock_connection:
|
|
_ = MssqlConnection(_config()).client
|
|
assert mock_connection.call_args.kwargs["get_connection_url_fn"].__name__ == "get_connection_url"
|
|
|
|
|
|
def test_pyodbc_scheme_delegates_to_azuresql_url_builder():
|
|
with patch(f"{CONNECTION_MODULE}.get_pyodbc_connection_url", return_value="delegated") as mock_pyodbc:
|
|
result = get_connection_url(_config(scheme=MssqlScheme.mssql_pyodbc))
|
|
mock_pyodbc.assert_called_once()
|
|
assert result == "delegated"
|
|
|
|
|
|
def test_non_pyodbc_scheme_uses_common_url_builder():
|
|
url = get_connection_url(_config(scheme=MssqlScheme.mssql_pytds))
|
|
assert url.startswith("mssql+pytds://")
|
|
|
|
|
|
def _checks() -> MssqlChecks:
|
|
return MssqlChecks(client=MagicMock(), get_databases_statement="SELECT 1")
|
|
|
|
|
|
def test_every_definition_step_resolves_to_a_check():
|
|
collected = collect_checks(_checks())
|
|
assert set(collected) == {
|
|
DatabaseStep.CheckAccess,
|
|
DatabaseStep.GetDatabases,
|
|
DatabaseStep.GetSchemas,
|
|
DatabaseStep.GetTables,
|
|
DatabaseStep.GetViews,
|
|
DatabaseStep.GetQueries,
|
|
}
|
|
|
|
|
|
def test_close_disposes_the_engine():
|
|
with patch(f"{CONNECTION_MODULE}.create_generic_db_connection"):
|
|
connection = MssqlConnection(_config())
|
|
engine = connection.client
|
|
connection.close()
|
|
engine.dispose.assert_called_once()
|
|
|
|
|
|
def test_building_checks_does_not_touch_the_network():
|
|
with patch(f"{CONNECTION_MODULE}.create_generic_db_connection") as mock_engine:
|
|
provider = MssqlConnection(_config()).checks()
|
|
engine = mock_engine.return_value
|
|
engine.connect.assert_not_called()
|
|
engine.exec_driver_sql.assert_not_called()
|
|
assert isinstance(provider, MssqlChecks)
|
|
|
|
|
|
def test_get_databases_statement_uses_current_db_when_not_ingest_all():
|
|
config = _config()
|
|
config.ingestAllDatabases = False
|
|
handler = MssqlConnection(config)
|
|
handler._client = MagicMock()
|
|
assert handler._get_databases_statement() == MSSQL_GET_CURRENT_DATABASE
|
|
|
|
|
|
def test_get_databases_statement_uses_all_dbs_when_ingest_all():
|
|
config = _config()
|
|
config.ingestAllDatabases = True
|
|
handler = MssqlConnection(config)
|
|
handler._client = MagicMock()
|
|
assert handler._get_databases_statement() == MSSQL_GET_DATABASE
|
|
|
|
|
|
def _pymssql_error(number: int, message: str) -> Exception:
|
|
"""A pymssql-style driver error: numeric SQL Server code in ``args[0]``,
|
|
wrapped by SQLAlchemy which preserves the original on ``.orig``."""
|
|
orig = Exception(number, message)
|
|
wrapper = Exception(f"({number}) {message}")
|
|
wrapper.orig = orig # type: ignore[attr-defined]
|
|
return wrapper
|
|
|
|
|
|
def _pyodbc_error(sqlstate: str, message: str) -> Exception:
|
|
"""A pyodbc-style driver error: SQLSTATE + message text, no numeric code."""
|
|
return Exception(f"('{sqlstate}', \"[{sqlstate}] {message}\")")
|
|
|
|
|
|
def test_pymssql_login_failure_classifies_as_auth():
|
|
diagnosis = MSSQL_ERRORS.classify(_pymssql_error(18456, "Login failed for user 'x'"))
|
|
assert diagnosis is not None
|
|
assert diagnosis.title == "Authentication failed"
|
|
|
|
|
|
def test_pyodbc_login_failure_classifies_as_auth():
|
|
diagnosis = MSSQL_ERRORS.classify(_pyodbc_error("28000", "Login failed for user 'x'."))
|
|
assert diagnosis is not None
|
|
assert diagnosis.title == "Authentication failed"
|
|
|
|
|
|
def test_pymssql_database_not_found_classifies():
|
|
diagnosis = MSSQL_ERRORS.classify(_pymssql_error(4060, 'Cannot open database "x" requested by the login.'))
|
|
assert diagnosis is not None
|
|
assert diagnosis.title == "Database not found or not accessible"
|
|
|
|
|
|
def test_pyodbc_cannot_open_database_classifies():
|
|
diagnosis = MSSQL_ERRORS.classify(_pyodbc_error("42000", 'Cannot open database "x" requested by the login.'))
|
|
assert diagnosis is not None
|
|
assert diagnosis.title == "Database not found or not accessible"
|
|
|
|
|
|
def test_cannot_open_database_wins_over_login_failed():
|
|
"""The SQL Server 4060 message embeds 'The login failed.', so the database
|
|
rule must be ordered before the login rule (regression for live-found bug)."""
|
|
message = "Cannot open database \"x\" requested by the login. The login failed. Login failed for user 'y'."
|
|
assert MSSQL_ERRORS.classify(Exception(message)).title == "Database not found or not accessible"
|
|
|
|
|
|
def test_pymssql_permission_denied_classifies():
|
|
diagnosis = MSSQL_ERRORS.classify(_pymssql_error(229, "The SELECT permission was denied on the object 'x'."))
|
|
assert diagnosis is not None
|
|
assert diagnosis.title == "Insufficient privileges"
|
|
|
|
|
|
def test_pyodbc_permission_denied_classifies():
|
|
diagnosis = MSSQL_ERRORS.classify(_pyodbc_error("42000", "The SELECT permission was denied on the object 'x'."))
|
|
assert diagnosis is not None
|
|
assert diagnosis.title == "Insufficient privileges"
|
|
|
|
|
|
def test_network_pack_is_folded_in():
|
|
diagnosis = MSSQL_ERRORS.classify(socket.gaierror("Name or service not known"))
|
|
assert diagnosis is not None
|
|
assert diagnosis.title == "Host could not be resolved"
|
|
|
|
|
|
def test_unknown_error_is_not_classified():
|
|
assert MSSQL_ERRORS.classify(Exception("something unexpected")) is None
|
|
|
|
|
|
def test_databases_summary_reports_exact_count_below_cap():
|
|
assert _databases_enumerated([object()] * 3) == "3 databases enumerated"
|
|
|
|
|
|
def test_databases_summary_reports_floor_when_capped():
|
|
assert _databases_enumerated([object()] * DEFAULT_SAMPLE_ROWS) == f"{DEFAULT_SAMPLE_ROWS}+ databases enumerated"
|