OpenMetadata/ingestion/tests/unit/test_owner_utils.py
Yourton Ma 07012db685
Fixes #22392: Add to herarchical owner config for database ingestion (#23709)
* feat: add owner assignment support at metadata ingestion level

* docs: Translate comments to English in test_owner

* refactor: move the test_owner-related files into correct positions

* feat: Add support for more source types

* Revert "feat: Add support for more source types"

This reverts commit a7649dcb3204cf98b7f4f9be02fbb982d2532193.

* feat: Add owner field support in sourceConfig for Database and Dashboard ingestion (fixes #22392)

* refactor code with the required style

* add owner field in related json file

* feat: add topology-based owner config for database/schema/table

* Format the code by the pre-commit tools

* fix some errors

* add a doc to explain this feature

* translate all Chinese comments to English and consolidate documentation

* remove redundant code

* refactor code

* refactor code

* refactor code

* refactor code

* Add some tests for owner-config and enhance this feat

* Add some tests for owner-config and enhance this feat

* fix some error

* fix some error

* refactor code

* Remove the yaml and bash test files and test owner config with pytest style

* format the python code

* refactor ingestion code

* refactor code

* fix some error in test_owner_utils

---------

Co-authored-by: Ma,Yutao <yutao.ma@sap.com>
2025-10-23 07:24:45 +02:00

427 lines
15 KiB
Python

# SPDX-License-Identifier: Apache-2.0
"""
Unit tests for owner_utils module
"""
import unittest
from unittest.mock import MagicMock
from metadata.generated.schema.type.entityReference import EntityReference
from metadata.generated.schema.type.entityReferenceList import EntityReferenceList
from metadata.utils.owner_utils import OwnerResolver, get_owner_from_config
class TestOwnerResolver(unittest.TestCase):
"""Test cases for OwnerResolver class"""
def setUp(self):
"""Set up test fixtures"""
self.mock_metadata = MagicMock()
# Mock successful owner lookup
mock_owner = EntityReference(
id="123e4567-e89b-12d3-a456-426614174000",
type="user",
name="test-user",
fullyQualifiedName="test-user",
)
self.mock_owner_list = EntityReferenceList(root=[mock_owner])
def test_simple_default_owner(self):
"""Test simple default owner configuration"""
config = {"default": "data-team"}
self.mock_metadata.get_reference_by_name.return_value = self.mock_owner_list
resolver = OwnerResolver(self.mock_metadata, config)
result = resolver.resolve_owner(entity_type="table", entity_name="test_table")
self.assertIsNotNone(result)
self.mock_metadata.get_reference_by_name.assert_called_with(
name="data-team", is_owner=True
)
def test_level_specific_owner(self):
"""Test level-specific owner configuration"""
config = {
"default": "default-team",
"database": "db-team",
"databaseSchema": "schema-team",
"table": "table-team",
}
self.mock_metadata.get_reference_by_name.return_value = self.mock_owner_list
resolver = OwnerResolver(self.mock_metadata, config)
# Test database level
result = resolver.resolve_owner(entity_type="database", entity_name="test_db")
self.assertIsNotNone(result)
self.mock_metadata.get_reference_by_name.assert_called_with(
name="db-team", is_owner=True
)
# Test databaseSchema level
result = resolver.resolve_owner(
entity_type="databaseSchema", entity_name="test_schema"
)
self.assertIsNotNone(result)
self.mock_metadata.get_reference_by_name.assert_called_with(
name="schema-team", is_owner=True
)
# Test table level
result = resolver.resolve_owner(entity_type="table", entity_name="test_table")
self.assertIsNotNone(result)
self.mock_metadata.get_reference_by_name.assert_called_with(
name="table-team", is_owner=True
)
def test_specific_entity_mapping(self):
"""Test specific entity name mapping"""
config = {
"default": "default-team",
"table": {"orders": "sales-team", "customers": "customer-team"},
}
self.mock_metadata.get_reference_by_name.return_value = self.mock_owner_list
resolver = OwnerResolver(self.mock_metadata, config)
# Test specific table mapping
result = resolver.resolve_owner(entity_type="table", entity_name="orders")
self.assertIsNotNone(result)
self.mock_metadata.get_reference_by_name.assert_called_with(
name="sales-team", is_owner=True
)
# Test unmapped table falls back to default
result = resolver.resolve_owner(entity_type="table", entity_name="products")
self.assertIsNotNone(result)
self.mock_metadata.get_reference_by_name.assert_called_with(
name="default-team", is_owner=True
)
def test_fqn_matching(self):
"""Test FQN matching for entities"""
config = {
"default": "default-team",
"table": {
"sales_db.public.orders": "sales-team",
"analytics_db.public.reports": "analytics-team",
},
}
self.mock_metadata.get_reference_by_name.return_value = self.mock_owner_list
resolver = OwnerResolver(self.mock_metadata, config)
# Test FQN match
result = resolver.resolve_owner(
entity_type="table", entity_name="sales_db.public.orders"
)
self.assertIsNotNone(result)
self.mock_metadata.get_reference_by_name.assert_called_with(
name="sales-team", is_owner=True
)
def test_simple_name_fallback(self):
"""Test fallback to simple name when FQN doesn't match"""
config = {"default": "default-team", "table": {"orders": "sales-team"}}
self.mock_metadata.get_reference_by_name.return_value = self.mock_owner_list
resolver = OwnerResolver(self.mock_metadata, config)
# Test FQN that falls back to simple name
result = resolver.resolve_owner(
entity_type="table", entity_name="sales_db.public.orders"
)
self.assertIsNotNone(result)
# Should match on simple name "orders"
self.mock_metadata.get_reference_by_name.assert_called_with(
name="sales-team", is_owner=True
)
def test_inheritance_enabled(self):
"""Test owner inheritance from parent"""
config = {"default": "default-team", "enableInheritance": True, "table": {}}
self.mock_metadata.get_reference_by_name.return_value = self.mock_owner_list
resolver = OwnerResolver(self.mock_metadata, config)
# Table should inherit from schema owner
result = resolver.resolve_owner(
entity_type="table", entity_name="test_table", parent_owner="schema-team"
)
self.assertIsNotNone(result)
self.mock_metadata.get_reference_by_name.assert_called_with(
name="schema-team", is_owner=True
)
def test_inheritance_disabled(self):
"""Test that inheritance can be disabled"""
config = {"default": "default-team", "enableInheritance": False, "table": {}}
self.mock_metadata.get_reference_by_name.return_value = self.mock_owner_list
resolver = OwnerResolver(self.mock_metadata, config)
# Table should NOT inherit, should use default
result = resolver.resolve_owner(
entity_type="table", entity_name="test_table", parent_owner="schema-team"
)
self.assertIsNotNone(result)
# Should use default, not parent
self.mock_metadata.get_reference_by_name.assert_called_with(
name="default-team", is_owner=True
)
def test_priority_order(self):
"""Test priority order: specific > level > inheritance > default"""
config = {
"default": "default-team",
"enableInheritance": True,
"table": {"orders": "specific-team"},
}
self.mock_metadata.get_reference_by_name.return_value = self.mock_owner_list
resolver = OwnerResolver(self.mock_metadata, config)
# Specific configuration should have highest priority
result = resolver.resolve_owner(
entity_type="table", entity_name="orders", parent_owner="parent-team"
)
self.assertIsNotNone(result)
# Should use specific, not parent or default
self.mock_metadata.get_reference_by_name.assert_called_with(
name="specific-team", is_owner=True
)
def test_owner_not_found(self):
"""Test handling when owner is not found"""
config = {"default": "nonexistent-team"}
self.mock_metadata.get_reference_by_name.return_value = None
resolver = OwnerResolver(self.mock_metadata, config)
result = resolver.resolve_owner(entity_type="table", entity_name="test_table")
self.assertIsNone(result)
def test_empty_config(self):
"""Test with empty configuration"""
resolver = OwnerResolver(self.mock_metadata, {})
result = resolver.resolve_owner(entity_type="table", entity_name="test_table")
self.assertIsNone(result)
def test_email_lookup(self):
"""Test owner lookup by email"""
config = {"default": "admin@company.com"}
# First call (by name) returns None, second call (by email) succeeds
self.mock_metadata.get_reference_by_name.return_value = None
self.mock_metadata.get_reference_by_email.return_value = self.mock_owner_list
resolver = OwnerResolver(self.mock_metadata, config)
result = resolver.resolve_owner(entity_type="table", entity_name="test_table")
self.assertIsNotNone(result)
self.mock_metadata.get_reference_by_email.assert_called_with(
"admin@company.com"
)
def test_multiple_owners_array(self):
"""Test multiple owners specified as array (users, not teams)"""
config = {
"default": "default-team",
"table": {"orders": ["john.doe", "jane.smith"]},
}
mock_john_owner = EntityReference(
id="923e4567-e89b-12d3-a456-426614174008",
type="user",
name="john.doe",
fullyQualifiedName="john.doe",
)
mock_jane_owner = EntityReference(
id="a23e4567-e89b-12d3-a456-426614174009",
type="user",
name="jane.smith",
fullyQualifiedName="jane.smith",
)
def mock_get_reference(name, is_owner=False):
if name == "john.doe":
return EntityReferenceList(root=[mock_john_owner])
elif name == "jane.smith":
return EntityReferenceList(root=[mock_jane_owner])
return None
self.mock_metadata.get_reference_by_name.side_effect = mock_get_reference
resolver = OwnerResolver(self.mock_metadata, config)
result = resolver.resolve_owner(entity_type="table", entity_name="orders")
self.assertIsNotNone(result)
self.assertEqual(len(result.root), 2)
self.assertEqual(result.root[0].name, "john.doe")
self.assertEqual(result.root[1].name, "jane.smith")
def test_multiple_owners_partial_success(self):
"""Test that partial success works when some owners are not found (users)"""
config = {"table": {"orders": ["john.doe", "nonexistent-user", "jane.smith"]}}
mock_john_owner = EntityReference(
id="423e4567-e89b-12d3-a456-426614174003",
type="user",
name="john.doe",
fullyQualifiedName="john.doe",
)
mock_jane_owner = EntityReference(
id="523e4567-e89b-12d3-a456-426614174004",
type="user",
name="jane.smith",
fullyQualifiedName="jane.smith",
)
def mock_get_reference(name, is_owner=False):
if name == "john.doe":
return EntityReferenceList(root=[mock_john_owner])
elif name == "jane.smith":
return EntityReferenceList(root=[mock_jane_owner])
return None
self.mock_metadata.get_reference_by_name.side_effect = mock_get_reference
resolver = OwnerResolver(self.mock_metadata, config)
result = resolver.resolve_owner(entity_type="table", entity_name="orders")
self.assertIsNotNone(result)
self.assertEqual(len(result.root), 2)
def test_multiple_owners_all_fail(self):
"""Test that None is returned when all owners fail"""
config = {"table": {"orders": ["nonexistent-1", "nonexistent-2"]}}
self.mock_metadata.get_reference_by_name.return_value = None
resolver = OwnerResolver(self.mock_metadata, config)
result = resolver.resolve_owner(entity_type="table", entity_name="orders")
self.assertIsNone(result)
def test_backward_compatibility_single_string(self):
"""Test backward compatibility with single string owner"""
config = {"table": {"orders": "sales-team"}}
self.mock_metadata.get_reference_by_name.return_value = self.mock_owner_list
resolver = OwnerResolver(self.mock_metadata, config)
result = resolver.resolve_owner(entity_type="table", entity_name="orders")
self.assertIsNotNone(result)
self.mock_metadata.get_reference_by_name.assert_called_with(
name="sales-team", is_owner=True
)
def test_multiple_owners_with_fqn(self):
"""Test multiple owners with FQN matching (users)"""
config = {"table": {"sales_db.public.orders": ["john.doe", "jane.smith"]}}
mock_john_owner = EntityReference(
id="623e4567-e89b-12d3-a456-426614174005",
type="user",
name="john.doe",
fullyQualifiedName="john.doe",
)
mock_jane_owner = EntityReference(
id="723e4567-e89b-12d3-a456-426614174006",
type="user",
name="jane.smith",
fullyQualifiedName="jane.smith",
)
def mock_get_reference(name, is_owner=False):
if name == "john.doe":
return EntityReferenceList(root=[mock_john_owner])
elif name == "jane.smith":
return EntityReferenceList(root=[mock_jane_owner])
return None
self.mock_metadata.get_reference_by_name.side_effect = mock_get_reference
resolver = OwnerResolver(self.mock_metadata, config)
result = resolver.resolve_owner(
entity_type="table", entity_name="sales_db.public.orders"
)
self.assertIsNotNone(result)
self.assertEqual(len(result.root), 2)
class TestGetOwnerFromConfig(unittest.TestCase):
"""Test cases for get_owner_from_config function"""
def setUp(self):
"""Set up test fixtures"""
self.mock_metadata = MagicMock()
mock_owner = EntityReference(
id="823e4567-e89b-12d3-a456-426614174007",
type="user",
name="test-user",
fullyQualifiedName="test-user",
)
self.mock_owner_list = EntityReferenceList(root=[mock_owner])
def test_string_config(self):
"""Test with string configuration (simple mode)"""
self.mock_metadata.get_reference_by_name.return_value = self.mock_owner_list
result = get_owner_from_config(
metadata=self.mock_metadata,
owner_config="data-team",
entity_type="table",
entity_name="test_table",
)
self.assertIsNotNone(result)
self.mock_metadata.get_reference_by_name.assert_called_with(
name="data-team", is_owner=True
)
def test_dict_config(self):
"""Test with dict configuration"""
config = {"default": "data-team"}
self.mock_metadata.get_reference_by_name.return_value = self.mock_owner_list
result = get_owner_from_config(
metadata=self.mock_metadata,
owner_config=config,
entity_type="table",
entity_name="test_table",
)
self.assertIsNotNone(result)
self.mock_metadata.get_reference_by_name.assert_called_with(
name="data-team", is_owner=True
)
def test_none_config(self):
"""Test with None configuration"""
result = get_owner_from_config(
metadata=self.mock_metadata,
owner_config=None,
entity_type="table",
entity_name="test_table",
)
self.assertIsNone(result)
if __name__ == "__main__":
unittest.main()