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

606 lines
20 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.
"""
Tests for the connector scaffold CLI tool.
"""
import argparse
import json
from unittest.mock import patch
import pytest
from metadata.cli.scaffold import (
AUTH_CHOICES,
CAPABILITY_CHOICES,
CONNECTION_TYPES,
REFERENCE_CONNECTORS,
SERVICE_TYPES,
ConnectorProfile,
_build_auth_refs,
_has_ref_auth,
_has_token_auth,
_prompt,
_prompt_multi,
_prompt_multiline,
_prompt_optional,
generate_connection_schema,
generate_test_connection_json,
get_repo_root,
run_scaffold_cli,
)
# ---------------------------------------------------------------------------
# ConnectorProfile
# ---------------------------------------------------------------------------
class TestConnectorProfile:
def test_camel_single_word(self):
p = ConnectorProfile()
p.name = "mysql"
assert p.camel == "Mysql"
def test_camel_multi_word(self):
p = ConnectorProfile()
p.name = "big_query"
assert p.camel == "BigQuery"
def test_camel_three_words(self):
p = ConnectorProfile()
p.name = "azure_data_lake"
assert p.camel == "AzureDataLake"
def test_module_name_single_word(self):
p = ConnectorProfile()
p.name = "mysql"
assert p.module_name == "mysql"
def test_module_name_multi_word(self):
p = ConnectorProfile()
p.name = "big_query"
assert p.module_name == "bigQuery"
def test_module_name_three_words(self):
p = ConnectorProfile()
p.name = "qlik_cloud"
assert p.module_name == "qlikCloud"
def test_defaults(self):
p = ConnectorProfile()
assert p.name == ""
assert p.service_type == ""
assert p.connection_type == "rest_api"
assert p.auth_types == ["basic"]
assert p.capabilities == ["metadata"]
assert p.scheme is None
assert p.default_port is None
# ---------------------------------------------------------------------------
# Auth helpers
# ---------------------------------------------------------------------------
class TestAuthHelpers:
def test_build_auth_refs_basic(self):
refs = _build_auth_refs(["basic"])
assert refs == [{"$ref": "./common/basicAuth.json"}]
def test_build_auth_refs_multiple(self):
refs = _build_auth_refs(["basic", "iam"])
assert len(refs) == 2
assert refs[0]["$ref"] == "./common/basicAuth.json"
assert refs[1]["$ref"] == "./common/iamAuthConfig.json"
def test_build_auth_refs_ignores_token(self):
refs = _build_auth_refs(["token", "oauth"])
assert refs == []
def test_build_auth_refs_mixed(self):
refs = _build_auth_refs(["jwt", "token"])
assert len(refs) == 1
assert refs[0]["$ref"] == "./common/jwtAuth.json"
def test_has_ref_auth_true(self):
assert _has_ref_auth(["basic"]) is True
assert _has_ref_auth(["iam", "token"]) is True
def test_has_ref_auth_false(self):
assert _has_ref_auth(["token"]) is False
assert _has_ref_auth(["oauth"]) is False
assert _has_ref_auth([]) is False
def test_has_token_auth_true(self):
assert _has_token_auth(["token"]) is True
assert _has_token_auth(["oauth"]) is True
assert _has_token_auth(["basic", "token"]) is True
def test_has_token_auth_false(self):
assert _has_token_auth(["basic"]) is False
assert _has_token_auth([]) is False
# ---------------------------------------------------------------------------
# generate_connection_schema
# ---------------------------------------------------------------------------
class TestGenerateConnectionSchema:
@staticmethod
def _make_profile(
name="test_db",
service_type="database",
connection_type="sqlalchemy",
scheme="testdb+pytest",
auth_types=None,
capabilities=None,
description="",
) -> ConnectorProfile:
p = ConnectorProfile()
p.name = name
p.service_type = service_type
p.connection_type = connection_type
p.scheme = scheme
p.auth_types = auth_types or ["basic"]
p.capabilities = capabilities or ["metadata"]
p.description = description
return p
def test_schema_structure(self):
p = self._make_profile()
schema = generate_connection_schema(p)
assert schema["$schema"] == "http://json-schema.org/draft-07/schema#"
assert schema["type"] == "object"
assert schema["additionalProperties"] is False
assert "definitions" in schema
assert "properties" in schema
def test_schema_ids(self):
p = self._make_profile()
schema = generate_connection_schema(p)
assert "testDbConnection" in schema["$id"]
assert "database" in schema["$id"]
assert schema["title"] == "TestDbConnection"
assert "TestDbConnection" in schema["javaType"]
def test_schema_type_definition(self):
p = self._make_profile()
schema = generate_connection_schema(p)
assert "testDbType" in schema["definitions"]
type_def = schema["definitions"]["testDbType"]
assert type_def["enum"] == ["TestDb"]
assert type_def["default"] == "TestDb"
def test_database_sqlalchemy_has_scheme(self):
p = self._make_profile(scheme="testdb+pytest")
schema = generate_connection_schema(p)
assert "scheme" in schema["properties"]
assert "testDbScheme" in schema["definitions"]
scheme_def = schema["definitions"]["testDbScheme"]
assert "testdb+pytest" in scheme_def["enum"]
def test_database_sqlalchemy_has_host_port(self):
p = self._make_profile()
schema = generate_connection_schema(p)
assert "hostPort" in schema["properties"]
assert "hostPort" in schema["required"]
def test_database_sqlalchemy_has_database_fields(self):
p = self._make_profile()
schema = generate_connection_schema(p)
assert "databaseName" in schema["properties"]
assert "databaseSchema" in schema["properties"]
def test_database_sqlalchemy_basic_auth(self):
p = self._make_profile(auth_types=["basic"])
schema = generate_connection_schema(p)
assert "username" in schema["properties"]
assert "authType" in schema["properties"]
assert "username" in schema["required"]
def test_database_sqlalchemy_token_auth(self):
p = self._make_profile(auth_types=["token"])
schema = generate_connection_schema(p)
assert "token" in schema["properties"]
assert "authType" not in schema["properties"]
def test_database_sqlalchemy_with_lineage_caps(self):
p = self._make_profile(capabilities=["metadata", "lineage"])
schema = generate_connection_schema(p)
props = schema["properties"]
assert "supportsMetadataExtraction" in props
assert "supportsLineageExtraction" in props
def test_database_sqlalchemy_with_profiler_caps(self):
p = self._make_profile(capabilities=["metadata", "profiler"])
schema = generate_connection_schema(p)
assert "supportsProfiler" in schema["properties"]
def test_schema_is_valid_json(self):
p = self._make_profile()
schema = generate_connection_schema(p)
serialized = json.dumps(schema, indent=2)
reparsed = json.loads(serialized)
assert reparsed == schema
def test_database_non_sqlalchemy_host_port_required(self):
p = self._make_profile(
name="test_rest_db",
service_type="database",
connection_type="rest_api",
scheme=None,
)
schema = generate_connection_schema(p)
assert "hostPort" in schema["properties"]
assert "hostPort" in schema["required"]
def test_dashboard_schema(self):
p = self._make_profile(
name="my_dash",
service_type="dashboard",
connection_type="rest_api",
scheme=None,
)
schema = generate_connection_schema(p)
assert "dashboard" in schema["$id"]
assert "hostPort" in schema["properties"]
assert "hostPort" in schema["required"]
assert "supportsMetadataExtraction" in schema["properties"]
def test_pipeline_schema(self):
p = self._make_profile(
name="my_pipe",
service_type="pipeline",
connection_type="rest_api",
scheme=None,
)
schema = generate_connection_schema(p)
assert "pipeline" in schema["$id"]
assert "hostPort" in schema["properties"]
def test_messaging_schema(self):
p = self._make_profile(
name="my_queue",
service_type="messaging",
connection_type="rest_api",
scheme=None,
)
schema = generate_connection_schema(p)
assert "messaging" in schema["$id"]
assert "bootstrapServers" in schema["properties"]
def test_custom_description(self):
p = self._make_profile(description="My custom database connector")
schema = generate_connection_schema(p)
assert schema["description"] == "My custom database connector"
def test_default_description(self):
p = self._make_profile(description="")
schema = generate_connection_schema(p)
assert schema["description"] == "TestDb Connection Config"
# ---------------------------------------------------------------------------
# generate_test_connection_json
# ---------------------------------------------------------------------------
class TestGenerateTestConnectionJson:
@staticmethod
def _make_profile(name="test_db", service_type="database", capabilities=None) -> ConnectorProfile:
p = ConnectorProfile()
p.name = name
p.service_type = service_type
p.capabilities = capabilities or ["metadata"]
return p
def test_database_steps(self):
p = self._make_profile()
result = generate_test_connection_json(p)
assert result["name"] == "TestDb"
step_names = [s["name"] for s in result["steps"]]
assert "CheckAccess" in step_names
assert "GetSchemas" in step_names
assert "GetTables" in step_names
assert "GetViews" in step_names
def test_database_check_access_is_mandatory_and_short_circuit(self):
p = self._make_profile()
result = generate_test_connection_json(p)
check_access = result["steps"][0]
assert check_access["name"] == "CheckAccess"
assert check_access["mandatory"] is True
assert check_access["shortCircuit"] is True
def test_database_with_lineage_has_get_queries(self):
p = self._make_profile(capabilities=["metadata", "lineage"])
result = generate_test_connection_json(p)
step_names = [s["name"] for s in result["steps"]]
assert "GetQueries" in step_names
def test_database_with_usage_has_get_queries(self):
p = self._make_profile(capabilities=["metadata", "usage"])
result = generate_test_connection_json(p)
step_names = [s["name"] for s in result["steps"]]
assert "GetQueries" in step_names
def test_database_without_lineage_usage_no_get_queries(self):
p = self._make_profile(capabilities=["metadata"])
result = generate_test_connection_json(p)
step_names = [s["name"] for s in result["steps"]]
assert "GetQueries" not in step_names
def test_dashboard_steps(self):
p = self._make_profile(name="my_dash", service_type="dashboard")
result = generate_test_connection_json(p)
step_names = [s["name"] for s in result["steps"]]
assert "CheckAccess" in step_names
assert "GetDashboards" in step_names
assert "GetCharts" in step_names
assert "GetSchemas" not in step_names
def test_pipeline_steps(self):
p = self._make_profile(name="my_pipe", service_type="pipeline")
result = generate_test_connection_json(p)
step_names = [s["name"] for s in result["steps"]]
assert "CheckAccess" in step_names
assert "GetPipelines" in step_names
def test_messaging_steps(self):
p = self._make_profile(name="my_queue", service_type="messaging")
result = generate_test_connection_json(p)
step_names = [s["name"] for s in result["steps"]]
assert "CheckAccess" in step_names
assert "GetTopics" in step_names
def test_storage_steps(self):
p = self._make_profile(name="my_store", service_type="storage")
result = generate_test_connection_json(p)
step_names = [s["name"] for s in result["steps"]]
assert "CheckAccess" in step_names
assert "GetContainers" in step_names
def test_search_steps(self):
p = self._make_profile(name="my_search", service_type="search")
result = generate_test_connection_json(p)
step_names = [s["name"] for s in result["steps"]]
assert "CheckAccess" in step_names
assert "GetSearchIndexes" in step_names
def test_api_steps(self):
p = self._make_profile(name="my_api", service_type="api")
result = generate_test_connection_json(p)
step_names = [s["name"] for s in result["steps"]]
assert "CheckAccess" in step_names
assert "GetCollections" in step_names
def test_mlmodel_steps(self):
p = self._make_profile(name="my_ml", service_type="mlmodel")
result = generate_test_connection_json(p)
step_names = [s["name"] for s in result["steps"]]
assert "CheckAccess" in step_names
assert "GetModels" in step_names
# ---------------------------------------------------------------------------
# Interactive prompts — EOF/interrupt handling
# ---------------------------------------------------------------------------
class TestPromptEofHandling:
def test_prompt_multiline_eof_returns_partial(self):
with patch("builtins.input", side_effect=["line1", "line2", EOFError]):
result = _prompt_multiline("Test")
assert result == "line1\nline2"
def test_prompt_multiline_keyboard_interrupt(self):
with patch("builtins.input", side_effect=[KeyboardInterrupt]):
result = _prompt_multiline("Test")
assert result == ""
def test_prompt_multiline_empty_line_stops(self):
with patch("builtins.input", side_effect=["hello", ""]):
result = _prompt_multiline("Test")
assert result == "hello"
def test_prompt_eof_with_default(self):
with patch("builtins.input", side_effect=EOFError):
result = _prompt("Test", default="fallback")
assert result == "fallback"
def test_prompt_eof_without_default_exits(self):
with patch("builtins.input", side_effect=EOFError): # noqa: SIM117
with pytest.raises(SystemExit):
_prompt("Test")
def test_prompt_keyboard_interrupt_with_default(self):
with patch("builtins.input", side_effect=KeyboardInterrupt):
result = _prompt("Test", default="fallback")
assert result == "fallback"
def test_prompt_keyboard_interrupt_without_default_exits(self):
with patch("builtins.input", side_effect=KeyboardInterrupt): # noqa: SIM117
with pytest.raises(SystemExit):
_prompt("Test")
def test_prompt_multi_eof_with_defaults(self):
with patch("builtins.input", side_effect=EOFError):
result = _prompt_multi("Test", ["a", "b"], defaults=["a"])
assert result == ["a"]
def test_prompt_multi_eof_without_defaults_exits(self):
with patch("builtins.input", side_effect=EOFError): # noqa: SIM117
with pytest.raises(SystemExit):
_prompt_multi("Test", ["a", "b"])
def test_prompt_optional_eof_returns_empty(self):
with patch("builtins.input", side_effect=EOFError):
result = _prompt_optional("Test")
assert result == ""
def test_prompt_optional_keyboard_interrupt_returns_empty(self):
with patch("builtins.input", side_effect=KeyboardInterrupt):
result = _prompt_optional("Test")
assert result == ""
# ---------------------------------------------------------------------------
# run_scaffold_cli — name validation
# ---------------------------------------------------------------------------
class TestRunScaffoldCliValidation:
@staticmethod
def _make_args(**kwargs) -> argparse.Namespace:
defaults = {
"name": "my_connector",
"service_type": "database",
"connection_type": "sqlalchemy",
"scheme": "mydb+pymydb",
"default_port": 5432,
"auth_types": ["basic"],
"capabilities": ["metadata"],
"display_name": None,
"description": None,
"docs_url": None,
"sdk_package": None,
"api_endpoints": None,
"docs_notes": None,
"docker_image": None,
"docker_port": None,
}
defaults.update(kwargs)
return argparse.Namespace(**defaults)
def test_rejects_uppercase_name(self):
args = self._make_args(name="MyConnector")
with pytest.raises(SystemExit):
run_scaffold_cli(args)
def test_rejects_name_starting_with_number(self):
args = self._make_args(name="1bad_name")
with pytest.raises(SystemExit):
run_scaffold_cli(args)
def test_rejects_name_with_dashes(self):
args = self._make_args(name="my-connector")
with pytest.raises(SystemExit):
run_scaffold_cli(args)
def test_rejects_name_with_spaces(self):
args = self._make_args(name="my connector")
with pytest.raises(SystemExit):
run_scaffold_cli(args)
def test_rejects_sqlalchemy_for_non_database(self):
args = self._make_args(
name="my_dash",
service_type="dashboard",
connection_type="sqlalchemy",
)
with pytest.raises(SystemExit):
run_scaffold_cli(args)
def test_allows_rest_api_for_non_database(self):
args = self._make_args(
name="my_dash",
service_type="dashboard",
connection_type="rest_api",
)
# Passes validation, then proceeds to run_scaffold (which writes files).
# We just verify it doesn't exit during validation.
with patch("metadata.cli.scaffold.run_scaffold"):
run_scaffold_cli(args)
# ---------------------------------------------------------------------------
# get_repo_root
# ---------------------------------------------------------------------------
class TestGetRepoRoot:
def test_finds_repo_root(self):
root = get_repo_root()
assert (root / "openmetadata-spec").is_dir()
assert (root / "ingestion").is_dir()
def test_returns_path_object(self):
root = get_repo_root()
from pathlib import Path
assert isinstance(root, Path)
# ---------------------------------------------------------------------------
# Constants
# ---------------------------------------------------------------------------
class TestConstants:
def test_service_types_complete(self):
expected = {
"database",
"dashboard",
"pipeline",
"messaging",
"mlmodel",
"storage",
"search",
"api",
}
assert set(SERVICE_TYPES) == expected
def test_connection_types(self):
assert "sqlalchemy" in CONNECTION_TYPES
assert "rest_api" in CONNECTION_TYPES
assert "sdk_client" in CONNECTION_TYPES
def test_auth_choices(self):
assert "basic" in AUTH_CHOICES
assert "token" in AUTH_CHOICES
assert "oauth" in AUTH_CHOICES
def test_capability_choices(self):
assert "metadata" in CAPABILITY_CHOICES
assert "lineage" in CAPABILITY_CHOICES
assert "profiler" in CAPABILITY_CHOICES
def test_reference_connectors_cover_all_service_types(self):
for st in SERVICE_TYPES:
assert st in REFERENCE_CONNECTORS