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

561 lines
21 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.
"""
Test Hierarchical Owner Configuration for Database Ingestion
This test suite validates the owner configuration feature that allows automatic
assignment of owners to metadata entities (databases, schemas, tables) during
ingestion based on flexible, hierarchical rules.
Replaces the bash/YAML-based tests previously in owner_config_tests/ directory.
"""
import uuid
from typing import Any, Dict, List, Optional, Union # noqa: UP035
from unittest import TestCase
from unittest.mock import Mock
from metadata.generated.schema.entity.teams.team import Team
from metadata.generated.schema.entity.teams.user import User
from metadata.generated.schema.metadataIngestion.workflow import (
OpenMetadataWorkflowConfig,
)
from metadata.generated.schema.type.basic import (
Email,
EntityName,
FullyQualifiedEntityName,
)
def build_owner_config(
default: Optional[str] = None, # noqa: UP045
enable_inheritance: bool = True,
database: Optional[Union[str, Dict[str, Any]]] = None, # noqa: UP006, UP007, UP045
database_schema: Optional[Union[str, Dict[str, Any]]] = None, # noqa: UP006, UP007, UP045
table: Optional[Union[str, Dict[str, Any]]] = None, # noqa: UP006, UP007, UP045
) -> Dict[str, Any]: # noqa: UP006
"""
Build owner configuration dictionary for testing.
Args:
default: Default owner name
enable_inheritance: Whether to enable inheritance
database: Database-level owner config (string or dict)
database_schema: Schema-level owner config (string or dict)
table: Table-level owner config (string or dict)
Returns:
Owner configuration dictionary
"""
config: Dict[str, Any] = {"enableInheritance": enable_inheritance} # noqa: UP006
if default:
config["default"] = default
if database is not None:
config["database"] = database
if database_schema is not None:
config["databaseSchema"] = database_schema
if table is not None:
config["table"] = table
return config
def build_test_workflow_config(
service_name: str,
owner_config: Dict[str, Any], # noqa: UP006
host_port: str = "localhost:5432",
database: str = "finance_db",
) -> Dict[str, Any]: # noqa: UP006
"""
Build complete workflow configuration for testing.
Args:
service_name: Name of the database service
owner_config: Owner configuration dict from build_owner_config
host_port: Database host and port
database: Database name
Returns:
Complete workflow configuration dictionary
"""
return {
"source": {
"type": "postgres",
"serviceName": service_name,
"serviceConnection": {
"config": {
"type": "Postgres",
"username": "admin",
"authType": {"password": "admin123"},
"hostPort": host_port,
"database": database,
}
},
"sourceConfig": {
"config": {
"type": "DatabaseMetadata",
"ownerConfig": owner_config,
"includeTables": True,
"includeViews": True,
"markDeletedTables": True,
"useFqnForFiltering": True,
"overrideMetadata": True,
}
},
},
"sink": {"type": "metadata-rest", "config": {}},
"workflowConfig": {
"loggerLevel": "DEBUG",
"openMetadataServerConfig": {
"hostPort": "http://localhost:8585/api",
"authProvider": "openmetadata",
"securityConfig": {"jwtToken": "test-token"},
},
},
}
def unwrap_owner_value(value: Any) -> Any:
"""
Unwrap OwnerValue Pydantic model to get actual value.
OwnerValue wraps the actual values in nested root attributes:
OwnerValue(root=OwnerValue1(root=['alice', 'bob']))
Args:
value: Potentially wrapped OwnerValue object
Returns:
Unwrapped actual value (string, list, or dict)
"""
if hasattr(value, "root"):
if hasattr(value.root, "root"):
return value.root.root
return value.root
return value
class TestOwnerConfig(TestCase):
"""
Test suite for hierarchical owner configuration in database ingestion.
Tests cover:
- Basic hierarchical owner assignment
- FQN vs simple name matching
- Multiple users as owners
- Owner type validation
- Inheritance enabled/disabled
- Partial success when owners don't exist
- Complex mixed scenarios
"""
def setUp(self) -> None:
"""Set up test fixtures before each test"""
self.mock_metadata = self._create_mock_metadata()
def _create_mock_metadata(self) -> Mock:
"""Create mock OpenMetadata API with test users and teams"""
mock_om = Mock()
mock_users = {
"alice": self._create_mock_user("alice", "alice@example.com"),
"bob": self._create_mock_user("bob", "bob@example.com"),
"charlie": self._create_mock_user("charlie", "charlie@example.com"),
"david": self._create_mock_user("david", "david@example.com"),
"emma": self._create_mock_user("emma", "emma@example.com"),
"frank": self._create_mock_user("frank", "frank@example.com"),
"marketing-user-1": self._create_mock_user("marketing-user-1", "marketing1@example.com"),
"marketing-user-2": self._create_mock_user("marketing-user-2", "marketing2@example.com"),
}
mock_teams = {
"data-platform-team": self._create_mock_team("data-platform-team", "Data Platform Team"),
"finance-team": self._create_mock_team("finance-team", "Finance Team"),
"marketing-team": self._create_mock_team("marketing-team", "Marketing Team"),
"accounting-team": self._create_mock_team("accounting-team", "Accounting Team"),
"treasury-team": self._create_mock_team("treasury-team", "Treasury Team"),
"revenue-team": self._create_mock_team("revenue-team", "Revenue Team"),
"expense-team": self._create_mock_team("expense-team", "Expense Team"),
"investment-team": self._create_mock_team("investment-team", "Investment Team"),
"audit-team": self._create_mock_team("audit-team", "Audit Team"),
"compliance-team": self._create_mock_team("compliance-team", "Compliance Team"),
"treasury-ops-team": self._create_mock_team("treasury-ops-team", "Treasury Operations Team"),
}
def get_by_name_side_effect(
entity: Any,
fqn: str,
fields: Optional[List[str]] = None, # noqa: UP006, UP045
) -> Optional[Union[User, Team]]: # noqa: UP007, UP045
"""Mock get_by_name to return users/teams or None"""
if fqn in mock_users:
return mock_users[fqn]
if fqn in mock_teams:
return mock_teams[fqn]
return None
mock_om.get_by_name.side_effect = get_by_name_side_effect
return mock_om
def _create_mock_user(self, name: str, email: str) -> User:
"""Create a mock User entity"""
return User(
id=uuid.uuid4(),
name=EntityName(name),
fullyQualifiedName=FullyQualifiedEntityName(name),
email=Email(email),
displayName=name.capitalize(),
)
def _create_mock_team(self, name: str, display_name: str) -> Team:
"""Create a mock Team entity"""
return Team(
id=uuid.uuid4(),
name=EntityName(name),
fullyQualifiedName=FullyQualifiedEntityName(name),
displayName=display_name,
teamType="Group",
)
def test_01_basic_configuration(self) -> None:
"""
Test Case 01: Basic hierarchical owner assignment
Validates:
- Default owner assignment
- Database-level specific owners
- Schema-level specific owners
- Table-level specific owners
- Inheritance enabled
"""
owner_config = build_owner_config(
default="data-platform-team",
enable_inheritance=True,
database={
"finance_db": "finance-team",
"marketing_db": "marketing-team",
},
database_schema={
"finance_db.accounting": "accounting-team",
},
table={
"finance_db.accounting.revenue": "revenue-team",
},
)
workflow_config = build_test_workflow_config("postgres-test-01-basic", owner_config)
config = OpenMetadataWorkflowConfig.model_validate(workflow_config)
assert config.source.sourceConfig.config.ownerConfig is not None
assert config.source.sourceConfig.config.ownerConfig.default == "data-platform-team"
assert config.source.sourceConfig.config.ownerConfig.enableInheritance is True
assert config.source.sourceConfig.config.ownerConfig.database is not None
assert config.source.sourceConfig.config.ownerConfig.databaseSchema is not None
assert config.source.sourceConfig.config.ownerConfig.table is not None
def test_02_fqn_matching(self) -> None:
"""
Test Case 02: FQN exact match vs simple name fallback
Validates:
- FQN exact match has priority
- Simple name match as fallback
- Appropriate INFO logging for simple name matches
"""
owner_config = build_owner_config(
default="data-platform-team",
enable_inheritance=True,
database_schema={
"finance_db.accounting": "accounting-team",
"treasury": "treasury-team",
},
table={
"finance_db.accounting.expenses": "expense-team",
"investments": "investment-team",
},
)
workflow_config = build_test_workflow_config("postgres-test-02-fqn", owner_config)
config = OpenMetadataWorkflowConfig.model_validate(workflow_config)
assert config.source.sourceConfig.config.ownerConfig is not None
schema_config = config.source.sourceConfig.config.ownerConfig.databaseSchema
assert schema_config is not None
if isinstance(schema_config, dict):
assert "finance_db.accounting" in schema_config
assert "treasury" in schema_config
def test_03_multiple_users(self) -> None:
"""
Test Case 03: Multiple users as owners (valid scenario)
Validates:
- Multiple users can be assigned as owners
- All owners must be type="user"
- List format works correctly
"""
owner_config = build_owner_config(
default="data-platform-team",
enable_inheritance=True,
database={
"finance_db": ["alice", "bob"],
},
table={
"finance_db.accounting.revenue": ["charlie", "david", "emma"],
"finance_db.accounting.expenses": ["frank"],
},
)
workflow_config = build_test_workflow_config("postgres-test-03-multiple-users", owner_config)
config = OpenMetadataWorkflowConfig.model_validate(workflow_config)
assert config.source.sourceConfig.config.ownerConfig is not None
db_config = config.source.sourceConfig.config.ownerConfig.database
if isinstance(db_config, dict):
finance_owners = unwrap_owner_value(db_config.get("finance_db"))
assert finance_owners is not None
assert isinstance(finance_owners, list)
assert len(finance_owners) == 2
assert "alice" in finance_owners
assert "bob" in finance_owners
def test_04_validation_errors(self) -> None:
"""
Test Case 04: Owner type validation
Validates:
- Multiple teams (INVALID) - should log WARNING
- Mixed users and teams (INVALID) - should log WARNING
- Single team as string (VALID) - works normally
"""
owner_config = build_owner_config(
default="data-platform-team",
enable_inheritance=True,
database={
"finance_db": ["finance-team", "audit-team", "compliance-team"],
},
table={
"finance_db.accounting.revenue": ["alice", "bob", "finance-team"],
"finance_db.accounting.expenses": "expense-team",
},
)
workflow_config = build_test_workflow_config("postgres-test-04-validation", owner_config)
config = OpenMetadataWorkflowConfig.model_validate(workflow_config)
assert config.source.sourceConfig.config.ownerConfig is not None
def test_05_inheritance_enabled(self) -> None:
"""
Test Case 05: Inheritance mechanism (enableInheritance: true)
Validates:
- Child entities inherit owner from parent
- Priority: Specific Config > Inherited Owner > Default
- finance_db → "finance-team"
- accounting schema → "finance-team" (INHERITED from database, NOT default)
- revenue table → "finance-team" (INHERITED from schema, NOT default)
"""
owner_config = build_owner_config(
default="data-platform-team",
enable_inheritance=True,
database={
"finance_db": "finance-team",
},
database_schema={
"finance_db.treasury": "treasury-team",
},
table={
"finance_db.accounting.expenses": "expense-team",
},
)
workflow_config = build_test_workflow_config("postgres-test-05-inheritance-on", owner_config)
config = OpenMetadataWorkflowConfig.model_validate(workflow_config)
assert config.source.sourceConfig.config.ownerConfig is not None
assert config.source.sourceConfig.config.ownerConfig.enableInheritance is True
def test_06_inheritance_disabled(self) -> None:
"""
Test Case 06: Inheritance disabled (enableInheritance: false)
Validates:
- Child entities use default instead of inheriting
- Priority: Specific Config > Default (skip inheritance)
- finance_db → "finance-team"
- accounting schema → "data-platform-team" (DEFAULT, not inherited)
- revenue table → "data-platform-team" (DEFAULT, not inherited)
"""
owner_config = build_owner_config(
default="data-platform-team",
enable_inheritance=False,
database={
"finance_db": "finance-team",
},
database_schema={
"finance_db.treasury": "treasury-team",
},
table={
"finance_db.accounting.expenses": "expense-team",
},
)
workflow_config = build_test_workflow_config("postgres-test-06-inheritance-off", owner_config)
config = OpenMetadataWorkflowConfig.model_validate(workflow_config)
assert config.source.sourceConfig.config.ownerConfig is not None
assert config.source.sourceConfig.config.ownerConfig.enableInheritance is False
def test_07_partial_success(self) -> None:
"""
Test Case 07: Partial success when some owners don't exist
Validates:
- Ingestion continues when some owners are not found
- Found owners are assigned successfully
- WARNING logged for missing owners
- revenue table: Should have 2 owners (alice, bob), skip nonexistent users
"""
owner_config = build_owner_config(
default="data-platform-team",
enable_inheritance=True,
table={
"finance_db.accounting.revenue": [
"alice",
"nonexistent-user-1",
"bob",
"nonexistent-user-2",
],
"finance_db.accounting.expenses": ["charlie", "david"],
},
)
workflow_config = build_test_workflow_config("postgres-test-07-partial", owner_config)
config = OpenMetadataWorkflowConfig.model_validate(workflow_config)
assert config.source.sourceConfig.config.ownerConfig is not None
table_config = config.source.sourceConfig.config.ownerConfig.table
if isinstance(table_config, dict):
revenue_owners = unwrap_owner_value(table_config.get("finance_db.accounting.revenue"))
assert revenue_owners is not None
assert isinstance(revenue_owners, list)
assert len(revenue_owners) == 4
def test_08_complex_mixed(self) -> None:
"""
Test Case 08: Complex mixed scenario
Validates comprehensive combination of:
- FQN vs simple name matching
- Multiple users vs single team
- Inheritance for unconfigured entities
- All validation rules working together
"""
owner_config = build_owner_config(
default="data-platform-team",
enable_inheritance=True,
database={
"finance_db": "finance-team",
"marketing_db": ["marketing-user-1", "marketing-user-2"],
},
database_schema={
"finance_db.accounting": ["alice", "bob"],
"treasury": "treasury-team",
},
table={
"finance_db.accounting.revenue": ["charlie", "david", "emma"],
"expenses": "expense-team",
"finance_db.treasury.cash_flow": "treasury-ops-team",
},
)
workflow_config = build_test_workflow_config("postgres-test-08-complex", owner_config)
config = OpenMetadataWorkflowConfig.model_validate(workflow_config)
assert config.source.sourceConfig.config.ownerConfig is not None
assert config.source.sourceConfig.config.ownerConfig.enableInheritance is True
db_config = config.source.sourceConfig.config.ownerConfig.database
if isinstance(db_config, dict):
assert "finance_db" in db_config
assert "marketing_db" in db_config
marketing_owners = unwrap_owner_value(db_config.get("marketing_db"))
if isinstance(marketing_owners, list):
assert len(marketing_owners) == 2
def test_config_validation_with_all_formats(self) -> None:
"""
Test configuration validation with different formats
Validates:
- String format (simple)
- Dictionary format (specific)
- Mixed format (flexible)
"""
owner_config = build_owner_config(
default="data-platform-team",
enable_inheritance=True,
database="default-db-owner",
database_schema={
"finance_db.accounting": "accounting-team",
"marketing_db.campaigns": ["alice", "bob"],
},
table={
"revenue": "revenue-team",
"finance_db.accounting.expenses": ["charlie"],
},
)
workflow_config = build_test_workflow_config("postgres-test-formats", owner_config)
config = OpenMetadataWorkflowConfig.model_validate(workflow_config)
assert config.source.sourceConfig.config.ownerConfig is not None
db_config = unwrap_owner_value(config.source.sourceConfig.config.ownerConfig.database)
assert db_config == "default-db-owner"
schema_config = config.source.sourceConfig.config.ownerConfig.databaseSchema
assert isinstance(schema_config, dict)
def test_empty_owner_config(self) -> None:
"""
Test with minimal/empty owner configuration
Validates:
- System handles missing owner config gracefully
- No exceptions thrown
"""
workflow_config = build_test_workflow_config(
"postgres-test-empty",
{},
)
config = OpenMetadataWorkflowConfig.model_validate(workflow_config)
assert config.source.sourceConfig.config is not None