mirror of
https://github.com/open-metadata/OpenMetadata.git
synced 2025-10-01 20:04:52 +00:00
1167 lines
47 KiB
Python
1167 lines
47 KiB
Python
import uuid
|
|
from typing import List, Optional
|
|
from unittest import TestCase
|
|
|
|
from metadata.generated.schema.api.data.createDashboardDataModel import (
|
|
CreateDashboardDataModelRequest,
|
|
)
|
|
from metadata.generated.schema.api.data.createTable import CreateTableRequest
|
|
from metadata.generated.schema.api.services.createDatabaseService import (
|
|
CreateDatabaseServiceRequest,
|
|
)
|
|
from metadata.generated.schema.entity.data.dashboardDataModel import (
|
|
DashboardDataModel,
|
|
DataModelType,
|
|
)
|
|
from metadata.generated.schema.entity.data.table import (
|
|
Column,
|
|
ColumnName,
|
|
DataType,
|
|
Table,
|
|
TableConstraint,
|
|
TableType,
|
|
)
|
|
from metadata.generated.schema.type.basic import (
|
|
EntityExtension,
|
|
EntityName,
|
|
FullyQualifiedEntityName,
|
|
Markdown,
|
|
)
|
|
from metadata.generated.schema.type.entityReference import EntityReference
|
|
from metadata.ingestion.models.custom_pydantic import BaseModel, CustomSecretStr
|
|
|
|
|
|
class CustomPydanticValidationTest(TestCase):
|
|
|
|
create_request = CreateTableRequest(
|
|
name=EntityName("Sales::>Territory"),
|
|
displayName="SalesTerritory",
|
|
description=Markdown(root="Sales territory lookup table."),
|
|
tableType="Regular",
|
|
columns=[
|
|
Column(
|
|
name=ColumnName(root="Sales::Last>Year"),
|
|
displayName="SalesLastYear",
|
|
dataType="NUMBER",
|
|
dataTypeDisplay="NUMBER",
|
|
description=Markdown(root="Sales total of previous year."),
|
|
constraint="NOT_NULL",
|
|
ordinalPosition=7,
|
|
),
|
|
Column(
|
|
name=ColumnName(root="Bonus"),
|
|
displayName="Bonus",
|
|
dataType="NUMBER",
|
|
dataTypeDisplay="NUMBER",
|
|
description=Markdown(root="Bonus due if quota is met."),
|
|
constraint="NOT_NULL",
|
|
ordinalPosition=4,
|
|
),
|
|
Column(
|
|
name=ColumnName(root="ModifiedDate"),
|
|
displayName="ModifiedDate",
|
|
dataType="DATETIME",
|
|
dataTypeDisplay="DATETIME",
|
|
description=Markdown(root="Date and time the record was last updated."),
|
|
constraint="NOT_NULL",
|
|
ordinalPosition=9,
|
|
),
|
|
],
|
|
tableConstraints=[
|
|
TableConstraint(constraintType="PRIMARY_KEY", columns=["Sales::Last>Year"])
|
|
],
|
|
databaseSchema=FullyQualifiedEntityName(
|
|
root='New Gyro 360.New Gyro 360."AdventureWorks2017.HumanResources"'
|
|
),
|
|
extension=EntityExtension(
|
|
root={
|
|
"DataQuality": '<div><p><b>Last evaluation:</b> 07/24/2023<br><b>Interval: </b>30 days <br><b>Next run:</b> 08/23/2023, 10:44:20<br><b>Measurement unit:</b> percent [%]</p><br><table><tbody><tr><th>Metric</th><th>Target</th><th>Latest result</th></tr><tr><td><p class="text-success">Completeness</p></td><td>90%</td><td><div class="bar fabric" style="width: 100%;"><strong>100%</strong></div></td></tr><tr><td><p class="text-success">Integrity</p></td><td>90%</td><td><div class="bar fabric" style="width: 100%;"><strong>100%</strong></div></td></tr><tr><td><p class="text-warning">Timeliness</p></td><td>90%</td><td><div class="bar fabric" style="width: 25%;"><strong>25%</strong></div></td></tr><tr><td><p class="text-warning">Uniqueness</p></td><td>90%</td><td><div class="bar fabric" style="width: 60%;"><strong>60%</strong></div></td></tr><tr><td><p class="text-success">Validity</p></td><td>90%</td><td><div class="bar fabric" style="width: 100%;"><strong>100%</strong></div></td></tr></tbody></table><h3>Overall score of the table is: 77%</h3><hr style="border-width: 5px;"></div>'
|
|
}
|
|
),
|
|
)
|
|
|
|
create_request_dashboard_datamodel = CreateDashboardDataModelRequest(
|
|
name=EntityName('test"dashboarddatamodel"'),
|
|
displayName='test"dashboarddatamodel"',
|
|
description=Markdown(
|
|
root="test__reserved__quote__dashboarddatamodel__reserved__quote__"
|
|
),
|
|
dataModelType=DataModelType.PowerBIDataModel,
|
|
service=FullyQualifiedEntityName(
|
|
root='New Gyro 360.New Gyro 360."AdventureWorks2017.HumanResources"'
|
|
),
|
|
columns=[
|
|
Column(
|
|
name="struct",
|
|
dataType=DataType.STRUCT,
|
|
arrayDataType="UNKNOWN",
|
|
children=[
|
|
Column(name='test "struct_children"', dataType=DataType.BIGINT)
|
|
],
|
|
)
|
|
],
|
|
)
|
|
|
|
def test_replace_separator(self):
|
|
assert (
|
|
self.create_request.name.root
|
|
== "Sales__reserved__colon____reserved__arrow__Territory"
|
|
)
|
|
assert (
|
|
self.create_request.columns[0].name.root
|
|
== "Sales__reserved__colon__Last__reserved__arrow__Year"
|
|
)
|
|
assert (
|
|
self.create_request.tableConstraints[0].columns[0]
|
|
== "Sales__reserved__colon__Last__reserved__arrow__Year"
|
|
)
|
|
|
|
assert (
|
|
self.create_request_dashboard_datamodel.name.root
|
|
== "test__reserved__quote__dashboarddatamodel__reserved__quote__"
|
|
)
|
|
|
|
assert (
|
|
self.create_request_dashboard_datamodel.columns[0].children[0].name.root
|
|
== "test __reserved__quote__struct_children__reserved__quote__"
|
|
)
|
|
|
|
def test_revert_separator(self):
|
|
fetch_response_revert_separator = Table(
|
|
id=uuid.uuid4(),
|
|
name="test__reserved__colon__table",
|
|
databaseSchema=EntityReference(id=uuid.uuid4(), type="databaseSchema"),
|
|
fullyQualifiedName="test-service-table.test-db.test-schema.test",
|
|
columns=[Column(name="id", dataType=DataType.BIGINT)],
|
|
)
|
|
fetch_response_revert_separator_2 = Table(
|
|
id=uuid.uuid4(),
|
|
name="test__reserved__colon__table__reserved__arrow__",
|
|
databaseSchema=EntityReference(id=uuid.uuid4(), type="databaseSchema"),
|
|
fullyQualifiedName="test-service-table.test-db.test-schema.test",
|
|
columns=[Column(name="id", dataType=DataType.BIGINT)],
|
|
)
|
|
|
|
fetch_response_revert_separator_3 = DashboardDataModel(
|
|
id=uuid.uuid4(),
|
|
name="test__reserved__quote__dashboarddatamodel__reserved__quote__",
|
|
fullyQualifiedName="test-service-table.test-db.test-schema.test__reserved__quote__dashboarddatamodel__reserved__quote__",
|
|
dataModelType=DataModelType.PowerBIDataModel,
|
|
columns=[
|
|
Column(
|
|
name="struct",
|
|
dataType=DataType.STRUCT,
|
|
children=[
|
|
Column(name='test "struct_children"', dataType=DataType.BIGINT)
|
|
],
|
|
)
|
|
],
|
|
)
|
|
assert fetch_response_revert_separator_3.name.root == 'test"dashboarddatamodel"'
|
|
assert (
|
|
fetch_response_revert_separator_3.columns[0].children[0].name.root
|
|
== 'test "struct_children"'
|
|
)
|
|
assert fetch_response_revert_separator.name.root == "test::table"
|
|
assert fetch_response_revert_separator_2.name.root == "test::table>"
|
|
|
|
|
|
class NestedModel(BaseModel):
|
|
secret: CustomSecretStr
|
|
value: int
|
|
|
|
|
|
class RootModel(BaseModel):
|
|
root_secret: CustomSecretStr
|
|
nested: NestedModel
|
|
items: List[NestedModel]
|
|
|
|
|
|
data = {
|
|
"root_secret": "root_password",
|
|
"nested": {"secret": "nested_password", "value": 42},
|
|
"items": [
|
|
{"secret": "item1_password", "value": 1},
|
|
{"secret": "item2_password", "value": 2},
|
|
],
|
|
}
|
|
|
|
model = RootModel(**data)
|
|
masked_data = model.model_dump(mask_secrets=True)
|
|
|
|
|
|
def test_model_dump_secrets():
|
|
"""Test model_dump_masked with root, nested, and list structures."""
|
|
|
|
assert masked_data["root_secret"] == "**********"
|
|
assert masked_data["nested"]["secret"] == "**********"
|
|
assert masked_data["nested"]["value"] == 42
|
|
assert masked_data["items"][0]["secret"] == "**********"
|
|
assert masked_data["items"][0]["value"] == 1
|
|
assert masked_data["items"][1]["secret"] == "**********"
|
|
assert masked_data["items"][1]["value"] == 2
|
|
|
|
plain_data = model.model_dump(mask_secrets=False)
|
|
assert plain_data["root_secret"] == "root_password"
|
|
assert plain_data["nested"]["secret"] == "nested_password"
|
|
assert plain_data["items"][0]["secret"] == "item1_password"
|
|
|
|
default_dump = model.model_dump()
|
|
assert default_dump["root_secret"] == "root_password"
|
|
assert default_dump["nested"]["secret"] == "nested_password"
|
|
assert default_dump["items"][0]["secret"] == "item1_password"
|
|
|
|
|
|
def test_model_dump_json_secrets():
|
|
assert (
|
|
model.model_validate_json(
|
|
model.model_dump_json()
|
|
).root_secret.get_secret_value()
|
|
== "**********"
|
|
)
|
|
assert (
|
|
model.model_validate_json(
|
|
model.model_dump_json(mask_secrets=True)
|
|
).root_secret.get_secret_value()
|
|
== "**********"
|
|
)
|
|
assert (
|
|
model.model_validate_json(
|
|
model.model_dump_json(mask_secrets=False)
|
|
).root_secret.get_secret_value()
|
|
== "root_password"
|
|
)
|
|
|
|
|
|
# Additional comprehensive tests for enhanced functionality
|
|
class ExtendedCustomPydanticValidationTest(TestCase):
|
|
"""Extended test suite for comprehensive validation of custom Pydantic functionality."""
|
|
|
|
def setUp(self):
|
|
"""Set up test data for extended tests."""
|
|
self.sample_table_id = uuid.uuid4()
|
|
self.sample_schema_ref = EntityReference(id=uuid.uuid4(), type="databaseSchema")
|
|
|
|
def test_service_level_models_not_transformed(self):
|
|
"""Test that service-level Create models are not transformed."""
|
|
# Test database service creation (should NOT be transformed)
|
|
service_request = CreateDatabaseServiceRequest(
|
|
name=EntityName('my::database>service"with_separators'), serviceType="Mysql"
|
|
)
|
|
|
|
# Service names should remain unchanged (not transformed)
|
|
assert service_request.name.root == 'my::database>service"with_separators'
|
|
|
|
def test_edge_cases_empty_and_none_values(self):
|
|
"""Test handling of edge cases like empty strings and None values."""
|
|
# Test minimal name (empty string not allowed by EntityName validation)
|
|
table_empty = Table(
|
|
id=self.sample_table_id,
|
|
name=EntityName("a"),
|
|
databaseSchema=self.sample_schema_ref,
|
|
fullyQualifiedName="test.empty",
|
|
columns=[Column(name="id", dataType=DataType.BIGINT)],
|
|
)
|
|
assert table_empty.name.root == "a"
|
|
|
|
# Test table with no columns (edge case)
|
|
table_no_columns = Table(
|
|
id=self.sample_table_id,
|
|
name="test__reserved__colon__table",
|
|
databaseSchema=self.sample_schema_ref,
|
|
fullyQualifiedName="test.empty",
|
|
columns=[],
|
|
)
|
|
assert table_no_columns.name.root == "test::table"
|
|
assert len(table_no_columns.columns) == 0
|
|
|
|
def test_complex_nested_structures(self):
|
|
"""Test complex nested column structures with multiple levels."""
|
|
# Create deeply nested structure
|
|
level3_columns = [
|
|
Column(
|
|
name=ColumnName("deep__reserved__colon__field"),
|
|
dataType=DataType.STRING,
|
|
)
|
|
]
|
|
|
|
level2_columns = [
|
|
Column(
|
|
name=ColumnName("nested__reserved__arrow__struct"),
|
|
dataType=DataType.STRUCT,
|
|
children=level3_columns,
|
|
)
|
|
]
|
|
|
|
level1_column = Column(
|
|
name=ColumnName("root__reserved__quote__struct"),
|
|
dataType=DataType.STRUCT,
|
|
children=level2_columns,
|
|
)
|
|
|
|
table = Table(
|
|
id=self.sample_table_id,
|
|
name="complex__reserved__colon__table",
|
|
columns=[level1_column],
|
|
databaseSchema=self.sample_schema_ref,
|
|
fullyQualifiedName="test.complex",
|
|
)
|
|
|
|
# Verify transformations at all levels
|
|
assert table.name.root == "complex::table"
|
|
assert table.columns[0].name.root == 'root"struct'
|
|
assert table.columns[0].children[0].name.root == "nested>struct"
|
|
assert table.columns[0].children[0].children[0].name.root == "deep::field"
|
|
|
|
def test_unicode_and_special_characters(self):
|
|
"""Test handling of Unicode and international characters."""
|
|
# Test Unicode with separators
|
|
table_unicode = Table(
|
|
id=self.sample_table_id,
|
|
name="測試__reserved__colon__表格__reserved__arrow__名稱",
|
|
databaseSchema=self.sample_schema_ref,
|
|
fullyQualifiedName="test.unicode",
|
|
columns=[
|
|
Column(name="unicode__reserved__quote__列", dataType=DataType.STRING)
|
|
],
|
|
)
|
|
assert table_unicode.name.root == "測試::表格>名稱"
|
|
assert table_unicode.columns[0].name.root == 'unicode"列'
|
|
|
|
# Test emojis with separators
|
|
table_emoji = Table(
|
|
id=self.sample_table_id,
|
|
name="table🚀__reserved__colon__data📊",
|
|
databaseSchema=self.sample_schema_ref,
|
|
fullyQualifiedName="test.emoji",
|
|
columns=[
|
|
Column(name="emoji__reserved__arrow__field🎯", dataType=DataType.STRING)
|
|
],
|
|
)
|
|
assert table_emoji.name.root == "table🚀::data📊"
|
|
assert table_emoji.columns[0].name.root == "emoji>field🎯"
|
|
|
|
def test_all_separator_combinations(self):
|
|
"""Test all combinations of separators in various scenarios."""
|
|
# Test all separators together
|
|
complex_name = 'test::colon>arrow"quote__reserved__mixed'
|
|
create_request = CreateTableRequest(
|
|
name=EntityName(complex_name),
|
|
columns=[Column(name=ColumnName("simple_col"), dataType=DataType.STRING)],
|
|
databaseSchema=FullyQualifiedEntityName("db.schema"),
|
|
)
|
|
|
|
expected = "test__reserved__colon__colon__reserved__arrow__arrow__reserved__quote__quote__reserved__mixed"
|
|
assert create_request.name.root == expected
|
|
|
|
def test_table_types_and_properties(self):
|
|
"""Test different table types and properties with name transformations."""
|
|
# Test with comprehensive table properties
|
|
table_full = Table(
|
|
id=self.sample_table_id,
|
|
name="full__reserved__colon__table__reserved__arrow__test",
|
|
displayName="Full Test Table",
|
|
description=Markdown(root="A comprehensive test table"),
|
|
tableType=TableType.Regular,
|
|
databaseSchema=self.sample_schema_ref,
|
|
fullyQualifiedName="test.db.schema.full_table",
|
|
columns=[
|
|
Column(
|
|
name=ColumnName("id__reserved__quote__primary"),
|
|
displayName="ID Primary",
|
|
dataType=DataType.BIGINT,
|
|
description=Markdown(root="Primary key column"),
|
|
),
|
|
Column(
|
|
name=ColumnName("data__reserved__arrow__field"),
|
|
displayName="Data Field",
|
|
dataType=DataType.STRING,
|
|
description=Markdown(root="Data field column"),
|
|
),
|
|
],
|
|
tableConstraints=[
|
|
TableConstraint(
|
|
constraintType="PRIMARY_KEY",
|
|
columns=["id__reserved__quote__primary"],
|
|
)
|
|
],
|
|
)
|
|
|
|
# Verify all transformations
|
|
assert table_full.name.root == "full::table>test"
|
|
assert table_full.columns[0].name.root == 'id"primary'
|
|
assert table_full.columns[1].name.root == "data>field"
|
|
assert table_full.tableConstraints[0].columns[0] == 'id"primary'
|
|
|
|
def test_dashboard_data_model_comprehensive(self):
|
|
"""Test comprehensive DashboardDataModel scenarios."""
|
|
# Test with all data model types
|
|
data_model_types = [
|
|
DataModelType.TableauDataModel,
|
|
DataModelType.PowerBIDataModel,
|
|
DataModelType.SupersetDataModel,
|
|
DataModelType.MetabaseDataModel,
|
|
]
|
|
|
|
for model_type in data_model_types:
|
|
dashboard_model = DashboardDataModel(
|
|
id=uuid.uuid4(),
|
|
name=f"model__reserved__colon__{model_type.value.lower()}",
|
|
dataModelType=model_type,
|
|
columns=[
|
|
Column(
|
|
name=ColumnName(
|
|
f"metric__reserved__arrow__{model_type.value.lower()}"
|
|
),
|
|
dataType=DataType.DOUBLE,
|
|
)
|
|
],
|
|
)
|
|
|
|
expected_name = f"model::{model_type.value.lower()}"
|
|
expected_col = f"metric>{model_type.value.lower()}"
|
|
|
|
assert dashboard_model.name.root == expected_name
|
|
assert dashboard_model.columns[0].name.root == expected_col
|
|
|
|
def test_create_requests_comprehensive(self):
|
|
"""Test comprehensive CreateRequest scenarios."""
|
|
# Test CreateTableRequest with all possible fields
|
|
comprehensive_request = CreateTableRequest(
|
|
name=EntityName('comprehensive::table>name"test'),
|
|
displayName='Comprehensive"Table>Test::Name',
|
|
description=Markdown(root="A comprehensive test table with all fields"),
|
|
tableType=TableType.Regular,
|
|
columns=[
|
|
Column(
|
|
name=ColumnName("primary__reserved__quote__key"),
|
|
displayName="Primary Key",
|
|
dataType=DataType.BIGINT,
|
|
constraint="NOT_NULL",
|
|
ordinalPosition=1,
|
|
),
|
|
Column(
|
|
name=ColumnName("foreign__reserved__arrow__key"),
|
|
displayName="Foreign Key",
|
|
dataType=DataType.BIGINT,
|
|
constraint="NOT_NULL",
|
|
ordinalPosition=2,
|
|
),
|
|
Column(
|
|
name=ColumnName("nested__reserved__colon__struct"),
|
|
displayName="Nested Struct",
|
|
dataType=DataType.STRUCT,
|
|
children=[
|
|
Column(
|
|
name=ColumnName("child__reserved__quote__field"),
|
|
dataType=DataType.STRING,
|
|
)
|
|
],
|
|
),
|
|
],
|
|
tableConstraints=[
|
|
TableConstraint(
|
|
constraintType="PRIMARY_KEY",
|
|
columns=["primary__reserved__quote__key"],
|
|
),
|
|
TableConstraint(
|
|
constraintType="UNIQUE", columns=["foreign__reserved__arrow__key"]
|
|
),
|
|
],
|
|
databaseSchema=FullyQualifiedEntityName("test__reserved__colon__db.schema"),
|
|
)
|
|
|
|
# Verify transformations
|
|
assert (
|
|
comprehensive_request.name.root
|
|
== "comprehensive__reserved__colon__table__reserved__arrow__name__reserved__quote__test"
|
|
)
|
|
assert (
|
|
comprehensive_request.columns[0].name.root
|
|
== "primary__reserved__quote__key"
|
|
)
|
|
assert (
|
|
comprehensive_request.columns[1].name.root
|
|
== "foreign__reserved__arrow__key"
|
|
)
|
|
assert (
|
|
comprehensive_request.columns[2].name.root
|
|
== "nested__reserved__colon__struct"
|
|
)
|
|
assert (
|
|
comprehensive_request.columns[2].children[0].name.root
|
|
== "child__reserved__quote__field"
|
|
)
|
|
|
|
def test_mixed_separator_edge_cases(self):
|
|
"""Test edge cases with mixed separators."""
|
|
edge_cases = [
|
|
# Consecutive separators
|
|
(
|
|
'test::>>""name',
|
|
"test__reserved__colon____reserved__arrow____reserved__arrow____reserved__quote____reserved__quote__name",
|
|
),
|
|
# Separators at start and end
|
|
(
|
|
'::test>name"',
|
|
"__reserved__colon__test__reserved__arrow__name__reserved__quote__",
|
|
),
|
|
# Only separators
|
|
('::>"', "__reserved__colon____reserved__arrow____reserved__quote__"),
|
|
# Empty between separators
|
|
(
|
|
'test::>"name',
|
|
"test__reserved__colon____reserved__arrow____reserved__quote__name",
|
|
),
|
|
]
|
|
|
|
for input_name, expected in edge_cases:
|
|
create_request = CreateTableRequest(
|
|
name=EntityName(input_name),
|
|
columns=[Column(name=ColumnName("col"), dataType=DataType.STRING)],
|
|
databaseSchema=FullyQualifiedEntityName("db.schema"),
|
|
)
|
|
assert (
|
|
create_request.name.root == expected
|
|
), f"Failed for input: {input_name}"
|
|
|
|
def test_very_long_names_performance(self):
|
|
"""Test performance with very long names."""
|
|
# Create very long names to test performance
|
|
long_base_name = "very_long_table_name_" * 3
|
|
long_name_with_separators = (
|
|
f'{long_base_name}::separator>{long_base_name}"quote{long_base_name}'
|
|
)
|
|
|
|
create_request = CreateTableRequest(
|
|
name=EntityName(long_name_with_separators),
|
|
columns=[Column(name=ColumnName("col"), dataType=DataType.STRING)],
|
|
databaseSchema=FullyQualifiedEntityName("db.schema"),
|
|
)
|
|
|
|
# Should handle long names without issues
|
|
result_name = create_request.name.root
|
|
assert "__reserved__colon__" in result_name
|
|
assert "__reserved__arrow__" in result_name
|
|
assert "__reserved__quote__" in result_name
|
|
|
|
def test_happy_path_simple_names(self):
|
|
"""Test happy path with simple names that don't need transformation."""
|
|
# Test simple names without special characters
|
|
simple_create = CreateTableRequest(
|
|
name=EntityName("simple_table_name"),
|
|
columns=[
|
|
Column(name=ColumnName("simple_column"), dataType=DataType.STRING)
|
|
],
|
|
databaseSchema=FullyQualifiedEntityName("db.schema"),
|
|
)
|
|
|
|
# Names should remain unchanged
|
|
assert simple_create.name.root == "simple_table_name"
|
|
assert simple_create.columns[0].name.root == "simple_column"
|
|
|
|
# Test simple fetch model
|
|
simple_table = Table(
|
|
id=self.sample_table_id,
|
|
name="simple_table",
|
|
databaseSchema=self.sample_schema_ref,
|
|
fullyQualifiedName="db.schema.simple_table",
|
|
columns=[Column(name="simple_col", dataType=DataType.STRING)],
|
|
)
|
|
|
|
assert simple_table.name.root == "simple_table"
|
|
assert simple_table.columns[0].name.root == "simple_col"
|
|
|
|
def test_error_handling_invalid_models(self):
|
|
"""Test error handling with None and invalid models."""
|
|
# Test with None entity
|
|
result = None
|
|
# This would normally be called by the validation system
|
|
# Just ensure no exceptions are thrown
|
|
|
|
# Test with mock invalid object
|
|
class InvalidModel:
|
|
def __init__(self):
|
|
self.invalid_attr = "test"
|
|
|
|
invalid_obj = InvalidModel()
|
|
# Should handle gracefully without transformation
|
|
assert hasattr(invalid_obj, "invalid_attr")
|
|
|
|
def test_boundary_conditions(self):
|
|
"""Test boundary conditions and edge cases."""
|
|
# Test single character names
|
|
single_char_create = CreateTableRequest(
|
|
name=EntityName("a"),
|
|
columns=[Column(name=ColumnName("b"), dataType=DataType.STRING)],
|
|
databaseSchema=FullyQualifiedEntityName("db.schema"),
|
|
)
|
|
assert single_char_create.name.root == "a"
|
|
|
|
# Test names with only separators
|
|
separator_only = CreateTableRequest(
|
|
name=EntityName("::"),
|
|
columns=[Column(name=ColumnName(">"), dataType=DataType.STRING)],
|
|
databaseSchema=FullyQualifiedEntityName("db.schema"),
|
|
)
|
|
assert separator_only.name.root == "__reserved__colon__"
|
|
assert separator_only.columns[0].name.root == "__reserved__arrow__"
|
|
|
|
def test_whitespace_handling(self):
|
|
"""Test handling of whitespace in various scenarios."""
|
|
whitespace_cases = [
|
|
# Leading/trailing spaces
|
|
(" test::name ", " test__reserved__colon__name "),
|
|
# Spaces around separators
|
|
(" test :: name ", " test __reserved__colon__ name "),
|
|
# Multiple spaces
|
|
("test :: name", "test __reserved__colon__ name"),
|
|
# Tabs and newlines (should be preserved)
|
|
("test\t::\nname", "test\t__reserved__colon__\nname"),
|
|
]
|
|
|
|
for input_name, expected in whitespace_cases:
|
|
create_request = CreateTableRequest(
|
|
name=EntityName(input_name),
|
|
columns=[Column(name=ColumnName("col"), dataType=DataType.STRING)],
|
|
databaseSchema=FullyQualifiedEntityName("db.schema"),
|
|
)
|
|
assert (
|
|
create_request.name.root == expected
|
|
), f"Failed for input: '{input_name}'"
|
|
|
|
def test_table_constraints_comprehensive(self):
|
|
"""Test comprehensive table constraints scenarios."""
|
|
constraint_types = ["PRIMARY_KEY", "UNIQUE", "FOREIGN_KEY"]
|
|
constraints = []
|
|
columns = []
|
|
|
|
for i, constraint_type in enumerate(constraint_types):
|
|
col_name = f"col_{i}__reserved__colon__constraint"
|
|
columns.append(Column(name=ColumnName(col_name), dataType=DataType.STRING))
|
|
constraints.append(
|
|
TableConstraint(constraintType=constraint_type, columns=[col_name])
|
|
)
|
|
|
|
create_request = CreateTableRequest(
|
|
name=EntityName("constraints__reserved__arrow__test"),
|
|
columns=columns,
|
|
tableConstraints=constraints,
|
|
databaseSchema=FullyQualifiedEntityName("db.schema"),
|
|
)
|
|
|
|
# Verify all constraints have transformed column names
|
|
for i, constraint in enumerate(create_request.tableConstraints):
|
|
expected_col = f"col_{i}__reserved__colon__constraint"
|
|
assert constraint.columns[0] == expected_col
|
|
|
|
def test_entity_references_and_relationships(self):
|
|
"""Test entity references and relationship handling."""
|
|
# Test with complex entity references
|
|
table_with_refs = Table(
|
|
id=self.sample_table_id,
|
|
name="table__reserved__colon__with__reserved__arrow__refs",
|
|
databaseSchema=EntityReference(
|
|
id=uuid.uuid4(),
|
|
type="databaseSchema",
|
|
name="schema__reserved__quote__name",
|
|
),
|
|
fullyQualifiedName="service.db.schema__reserved__quote__name.table",
|
|
columns=[
|
|
Column(
|
|
name=ColumnName("ref__reserved__colon__column"),
|
|
dataType=DataType.STRING,
|
|
)
|
|
],
|
|
)
|
|
|
|
# Verify transformations
|
|
assert table_with_refs.name.root == "table::with>refs"
|
|
assert table_with_refs.columns[0].name.root == "ref::column"
|
|
# Entity references should not be transformed (they're separate entities)
|
|
assert table_with_refs.databaseSchema.name == "schema__reserved__quote__name"
|
|
|
|
|
|
class CustomSecretStrExtendedTest(TestCase):
|
|
"""Extended test suite for CustomSecretStr functionality."""
|
|
|
|
def test_secret_creation_and_access(self):
|
|
"""Test CustomSecretStr creation and value access."""
|
|
secret = CustomSecretStr("test_password")
|
|
assert secret.get_secret_value() == "test_password"
|
|
assert str(secret) == "**********"
|
|
assert repr(secret) == "SecretStr('**********')"
|
|
|
|
def test_empty_and_none_secrets(self):
|
|
"""Test handling of empty and None secret values."""
|
|
# Test empty secret
|
|
empty_secret = CustomSecretStr("")
|
|
assert empty_secret.get_secret_value() == ""
|
|
assert str(empty_secret) == ""
|
|
|
|
# Test None secret handling
|
|
try:
|
|
none_secret = CustomSecretStr(None)
|
|
assert none_secret.get_secret_value() is None
|
|
except (TypeError, ValueError, AttributeError):
|
|
# This is acceptable behavior for None values
|
|
pass
|
|
|
|
def test_long_secrets(self):
|
|
"""Test handling of very long secret values."""
|
|
long_secret_value = "a" * 1000
|
|
long_secret = CustomSecretStr(long_secret_value)
|
|
assert long_secret.get_secret_value() == long_secret_value
|
|
assert (
|
|
str(long_secret) == "**********"
|
|
) # Should still mask regardless of length
|
|
|
|
def test_special_character_secrets(self):
|
|
"""Test secrets with special characters."""
|
|
special_chars = "!@#$%^&*()_+-=[]{}|;':,.<>?/~`"
|
|
special_secret = CustomSecretStr(special_chars)
|
|
assert special_secret.get_secret_value() == special_chars
|
|
assert str(special_secret) == "**********"
|
|
|
|
def test_unicode_secrets(self):
|
|
"""Test secrets with Unicode characters."""
|
|
unicode_secret = CustomSecretStr("密码测试🔒")
|
|
assert unicode_secret.get_secret_value() == "密码测试🔒"
|
|
assert str(unicode_secret) == "**********"
|
|
|
|
def test_secret_equality_and_hashing(self):
|
|
"""Test secret equality and hashing behavior."""
|
|
secret1 = CustomSecretStr("password123")
|
|
secret2 = CustomSecretStr("password123")
|
|
secret3 = CustomSecretStr("different_password")
|
|
|
|
# Test equality
|
|
assert secret1.get_secret_value() == secret2.get_secret_value()
|
|
assert secret1.get_secret_value() != secret3.get_secret_value()
|
|
|
|
# Test that string representation is always masked
|
|
assert str(secret1) == str(secret2) == str(secret3) == "**********"
|
|
|
|
def test_secret_in_nested_models_deep(self):
|
|
"""Test secrets in deeply nested model structures."""
|
|
|
|
class Level3Model(BaseModel):
|
|
deep_secret: CustomSecretStr
|
|
deep_value: str
|
|
|
|
class Level2Model(BaseModel):
|
|
mid_secret: CustomSecretStr
|
|
level3: Level3Model
|
|
|
|
class Level1Model(BaseModel):
|
|
top_secret: CustomSecretStr
|
|
level2: Level2Model
|
|
|
|
deep_data = {
|
|
"top_secret": "top_password",
|
|
"level2": {
|
|
"mid_secret": "mid_password",
|
|
"level3": {"deep_secret": "deep_password", "deep_value": "not_secret"},
|
|
},
|
|
}
|
|
|
|
deep_model = Level1Model(**deep_data)
|
|
|
|
# Test masked dump
|
|
masked = deep_model.model_dump(mask_secrets=True)
|
|
assert masked["top_secret"] == "**********"
|
|
assert masked["level2"]["mid_secret"] == "**********"
|
|
assert masked["level2"]["level3"]["deep_secret"] == "**********"
|
|
assert masked["level2"]["level3"]["deep_value"] == "not_secret"
|
|
|
|
# Test unmasked dump
|
|
unmasked = deep_model.model_dump(mask_secrets=False)
|
|
assert unmasked["top_secret"] == "top_password"
|
|
assert unmasked["level2"]["mid_secret"] == "mid_password"
|
|
assert unmasked["level2"]["level3"]["deep_secret"] == "deep_password"
|
|
|
|
def test_secret_with_optional_fields(self):
|
|
"""Test secrets with optional fields."""
|
|
|
|
class OptionalSecretModel(BaseModel):
|
|
required_secret: CustomSecretStr
|
|
optional_secret: Optional[CustomSecretStr] = None
|
|
optional_value: Optional[str] = None
|
|
|
|
# Test with all fields
|
|
full_model = OptionalSecretModel(
|
|
required_secret="required_pass",
|
|
optional_secret="optional_pass",
|
|
optional_value="some_value",
|
|
)
|
|
|
|
masked_full = full_model.model_dump(mask_secrets=True)
|
|
assert masked_full["required_secret"] == "**********"
|
|
assert masked_full["optional_secret"] == "**********"
|
|
assert masked_full["optional_value"] == "some_value"
|
|
|
|
# Test with only required fields
|
|
minimal_model = OptionalSecretModel(required_secret="required_pass")
|
|
|
|
masked_minimal = minimal_model.model_dump(mask_secrets=True)
|
|
assert masked_minimal["required_secret"] == "**********"
|
|
assert masked_minimal["optional_secret"] is None
|
|
assert masked_minimal["optional_value"] is None
|
|
|
|
def test_secret_lists_and_dictionaries(self):
|
|
"""Test secrets in lists and dictionaries."""
|
|
|
|
class ComplexSecretModel(BaseModel):
|
|
secret_list: List[CustomSecretStr]
|
|
nested_secrets: List[dict]
|
|
|
|
complex_data = {
|
|
"secret_list": ["password1", "password2", "password3"],
|
|
"nested_secrets": [
|
|
{"name": "config1", "secret": CustomSecretStr("secret1")},
|
|
{"name": "config2", "secret": CustomSecretStr("secret2")},
|
|
],
|
|
}
|
|
|
|
complex_model = ComplexSecretModel(**complex_data)
|
|
|
|
# Test that list secrets are handled
|
|
assert len(complex_model.secret_list) == 3
|
|
assert all(str(secret) == "**********" for secret in complex_model.secret_list)
|
|
assert all(
|
|
secret.get_secret_value() in ["password1", "password2", "password3"]
|
|
for secret in complex_model.secret_list
|
|
)
|
|
|
|
|
|
class DashboardDataModelTransformationTest(TestCase):
|
|
"""Test DashboardDataModel transformations with nested children and reserved keywords."""
|
|
|
|
def setUp(self):
|
|
"""Set up test data."""
|
|
self.sample_service = FullyQualifiedEntityName(
|
|
root='TestService.PowerBI."Analysis>Services::Environment"'
|
|
)
|
|
|
|
def test_create_dashboard_datamodel_with_nested_children(self):
|
|
"""Test CreateDashboardDataModelRequest with nested children containing reserved keywords."""
|
|
create_request = CreateDashboardDataModelRequest(
|
|
name=EntityName('financial::report>model"quarterly'),
|
|
displayName="Financial Report Model",
|
|
description=Markdown(
|
|
root="Financial reporting model with special characters"
|
|
),
|
|
dataModelType=DataModelType.PowerBIDataModel,
|
|
service=self.sample_service,
|
|
columns=[
|
|
Column(
|
|
name=ColumnName("revenue::metrics>summary"),
|
|
displayName="Revenue Metrics",
|
|
dataType=DataType.STRUCT,
|
|
description=Markdown(root="Revenue metrics structure"),
|
|
children=[
|
|
Column(
|
|
name=ColumnName("total::revenue>amount"),
|
|
displayName="Total Revenue",
|
|
dataType=DataType.DECIMAL,
|
|
description=Markdown(root="Total revenue amount"),
|
|
),
|
|
Column(
|
|
name=ColumnName('currency::code>"USD"'),
|
|
displayName="Currency Code",
|
|
dataType=DataType.STRING,
|
|
description=Markdown(root="Currency code with quotes"),
|
|
),
|
|
Column(
|
|
name=ColumnName("nested::struct>data"),
|
|
displayName="Nested Structure",
|
|
dataType=DataType.STRUCT,
|
|
children=[
|
|
Column(
|
|
name=ColumnName('deep::field>"value"'),
|
|
displayName="Deep Field",
|
|
dataType=DataType.STRING,
|
|
)
|
|
],
|
|
),
|
|
],
|
|
),
|
|
Column(
|
|
name=ColumnName("expenses::breakdown>categories"),
|
|
displayName="Expense Breakdown",
|
|
dataType=DataType.ARRAY,
|
|
arrayDataType=DataType.STRUCT,
|
|
children=[
|
|
Column(
|
|
name=ColumnName('category::name>"operations"'),
|
|
displayName="Category Name",
|
|
dataType=DataType.STRING,
|
|
),
|
|
Column(
|
|
name=ColumnName("amount::value>total"),
|
|
displayName="Amount Value",
|
|
dataType=DataType.DECIMAL,
|
|
),
|
|
],
|
|
),
|
|
],
|
|
)
|
|
|
|
# Verify main entity name transformation (ENCODE for Create operations)
|
|
assert (
|
|
create_request.name.root
|
|
== "financial__reserved__colon__report__reserved__arrow__model__reserved__quote__quarterly"
|
|
)
|
|
|
|
# Verify top-level column name transformations
|
|
assert (
|
|
create_request.columns[0].name.root
|
|
== "revenue__reserved__colon__metrics__reserved__arrow__summary"
|
|
)
|
|
assert (
|
|
create_request.columns[1].name.root
|
|
== "expenses__reserved__colon__breakdown__reserved__arrow__categories"
|
|
)
|
|
|
|
# Verify nested children transformations (first level)
|
|
revenue_column = create_request.columns[0]
|
|
assert (
|
|
revenue_column.children[0].name.root
|
|
== "total__reserved__colon__revenue__reserved__arrow__amount"
|
|
)
|
|
assert (
|
|
revenue_column.children[1].name.root
|
|
== "currency__reserved__colon__code__reserved__arrow____reserved__quote__USD__reserved__quote__"
|
|
)
|
|
assert (
|
|
revenue_column.children[2].name.root
|
|
== "nested__reserved__colon__struct__reserved__arrow__data"
|
|
)
|
|
|
|
# Verify deeply nested children transformations (second level)
|
|
nested_struct = revenue_column.children[2]
|
|
assert (
|
|
nested_struct.children[0].name.root
|
|
== "deep__reserved__colon__field__reserved__arrow____reserved__quote__value__reserved__quote__"
|
|
)
|
|
|
|
# Verify array children transformations
|
|
expenses_column = create_request.columns[1]
|
|
assert (
|
|
expenses_column.children[0].name.root
|
|
== "category__reserved__colon__name__reserved__arrow____reserved__quote__operations__reserved__quote__"
|
|
)
|
|
assert (
|
|
expenses_column.children[1].name.root
|
|
== "amount__reserved__colon__value__reserved__arrow__total"
|
|
)
|
|
|
|
def test_fetch_dashboard_datamodel_with_nested_children(self):
|
|
"""Test DashboardDataModel fetch with nested children containing encoded reserved keywords."""
|
|
dashboard_model = DashboardDataModel(
|
|
id=uuid.uuid4(),
|
|
name="financial__reserved__colon__report__reserved__arrow__model__reserved__quote__quarterly",
|
|
displayName="Financial Report Model",
|
|
dataModelType=DataModelType.PowerBIDataModel,
|
|
service=EntityReference(id=uuid.uuid4(), type="dashboardService"),
|
|
fullyQualifiedName="service.financial__reserved__colon__report__reserved__arrow__model__reserved__quote__quarterly",
|
|
columns=[
|
|
Column(
|
|
name=ColumnName(
|
|
"revenue__reserved__colon__metrics__reserved__arrow__summary"
|
|
),
|
|
displayName="Revenue Metrics",
|
|
dataType=DataType.STRUCT,
|
|
children=[
|
|
Column(
|
|
name=ColumnName(
|
|
"total__reserved__colon__revenue__reserved__arrow__amount"
|
|
),
|
|
displayName="Total Revenue",
|
|
dataType=DataType.DECIMAL,
|
|
),
|
|
Column(
|
|
name=ColumnName(
|
|
"currency__reserved__colon__code__reserved__arrow____reserved__quote__USD__reserved__quote__"
|
|
),
|
|
displayName="Currency Code",
|
|
dataType=DataType.STRING,
|
|
),
|
|
Column(
|
|
name=ColumnName(
|
|
"nested__reserved__colon__struct__reserved__arrow__data"
|
|
),
|
|
displayName="Nested Structure",
|
|
dataType=DataType.STRUCT,
|
|
children=[
|
|
Column(
|
|
name=ColumnName(
|
|
"deep__reserved__colon__field__reserved__arrow____reserved__quote__value__reserved__quote__"
|
|
),
|
|
displayName="Deep Field",
|
|
dataType=DataType.STRING,
|
|
)
|
|
],
|
|
),
|
|
],
|
|
),
|
|
Column(
|
|
name=ColumnName(
|
|
"expenses__reserved__colon__breakdown__reserved__arrow__categories"
|
|
),
|
|
displayName="Expense Breakdown",
|
|
dataType=DataType.ARRAY,
|
|
arrayDataType=DataType.STRUCT,
|
|
children=[
|
|
Column(
|
|
name=ColumnName(
|
|
"category__reserved__colon__name__reserved__arrow____reserved__quote__operations__reserved__quote__"
|
|
),
|
|
displayName="Category Name",
|
|
dataType=DataType.STRING,
|
|
),
|
|
Column(
|
|
name=ColumnName(
|
|
"amount__reserved__colon__value__reserved__arrow__total"
|
|
),
|
|
displayName="Amount Value",
|
|
dataType=DataType.DECIMAL,
|
|
),
|
|
],
|
|
),
|
|
],
|
|
)
|
|
|
|
# Verify main entity name transformation (DECODE for fetch operations)
|
|
assert dashboard_model.name.root == 'financial::report>model"quarterly'
|
|
|
|
# Verify top-level column name transformations
|
|
assert dashboard_model.columns[0].name.root == "revenue::metrics>summary"
|
|
assert dashboard_model.columns[1].name.root == "expenses::breakdown>categories"
|
|
|
|
# Verify nested children transformations (first level)
|
|
revenue_column = dashboard_model.columns[0]
|
|
assert revenue_column.children[0].name.root == "total::revenue>amount"
|
|
assert revenue_column.children[1].name.root == 'currency::code>"USD"'
|
|
assert revenue_column.children[2].name.root == "nested::struct>data"
|
|
|
|
# Verify deeply nested children transformations (second level)
|
|
nested_struct = revenue_column.children[2]
|
|
assert nested_struct.children[0].name.root == 'deep::field>"value"'
|
|
|
|
# Verify array children transformations
|
|
expenses_column = dashboard_model.columns[1]
|
|
assert expenses_column.children[0].name.root == 'category::name>"operations"'
|
|
assert expenses_column.children[1].name.root == "amount::value>total"
|
|
|
|
def test_dashboard_datamodel_round_trip_transformation(self):
|
|
"""Test round-trip transformation: Create -> Fetch -> Create maintains data integrity."""
|
|
# Start with create request containing special characters
|
|
original_create = CreateDashboardDataModelRequest(
|
|
name=EntityName('analytics::dashboard>model"test'),
|
|
displayName="Analytics Dashboard Model",
|
|
dataModelType=DataModelType.PowerBIDataModel,
|
|
service=self.sample_service,
|
|
columns=[
|
|
Column(
|
|
name=ColumnName("metrics::summary>report"),
|
|
dataType=DataType.STRUCT,
|
|
children=[
|
|
Column(
|
|
name=ColumnName('total::count>"records"'),
|
|
dataType=DataType.INT,
|
|
)
|
|
],
|
|
)
|
|
],
|
|
)
|
|
|
|
# Simulate storage (encoded form)
|
|
stored_name = original_create.name.root # Should be encoded
|
|
stored_column_name = original_create.columns[0].name.root # Should be encoded
|
|
stored_nested_name = (
|
|
original_create.columns[0].children[0].name.root
|
|
) # Should be encoded
|
|
|
|
# Simulate fetch operation (create DashboardDataModel with stored values)
|
|
fetched_model = DashboardDataModel(
|
|
id=uuid.uuid4(),
|
|
name=stored_name,
|
|
displayName="Analytics Dashboard Model",
|
|
dataModelType=DataModelType.PowerBIDataModel,
|
|
service=EntityReference(id=uuid.uuid4(), type="dashboardService"),
|
|
fullyQualifiedName=f"service.{stored_name}",
|
|
columns=[
|
|
Column(
|
|
name=ColumnName(stored_column_name),
|
|
dataType=DataType.STRUCT,
|
|
children=[
|
|
Column(
|
|
name=ColumnName(stored_nested_name), dataType=DataType.INT
|
|
)
|
|
],
|
|
)
|
|
],
|
|
)
|
|
|
|
# Verify fetch operation decodes correctly
|
|
assert fetched_model.name.root == 'analytics::dashboard>model"test'
|
|
assert fetched_model.columns[0].name.root == "metrics::summary>report"
|
|
assert (
|
|
fetched_model.columns[0].children[0].name.root == 'total::count>"records"'
|
|
)
|
|
|
|
# Verify create operation encodes correctly
|
|
assert (
|
|
stored_name
|
|
== "analytics__reserved__colon__dashboard__reserved__arrow__model__reserved__quote__test"
|
|
)
|
|
assert (
|
|
stored_column_name
|
|
== "metrics__reserved__colon__summary__reserved__arrow__report"
|
|
)
|
|
assert (
|
|
stored_nested_name
|
|
== "total__reserved__colon__count__reserved__arrow____reserved__quote__records__reserved__quote__"
|
|
)
|
|
|
|
def test_dashboard_datamodel_edge_cases(self):
|
|
"""Test edge cases for DashboardDataModel transformations."""
|
|
# Test with empty children
|
|
model_empty_children = DashboardDataModel(
|
|
id=uuid.uuid4(),
|
|
name="test__reserved__colon__model",
|
|
dataModelType=DataModelType.PowerBIDataModel,
|
|
service=EntityReference(id=uuid.uuid4(), type="dashboardService"),
|
|
fullyQualifiedName="service.test__reserved__colon__model",
|
|
columns=[
|
|
Column(
|
|
name=ColumnName("parent__reserved__arrow__column"),
|
|
dataType=DataType.STRUCT,
|
|
children=[], # Empty children list
|
|
)
|
|
],
|
|
)
|
|
|
|
assert model_empty_children.name.root == "test::model"
|
|
assert model_empty_children.columns[0].name.root == "parent>column"
|
|
|
|
# Test with None children
|
|
model_none_children = DashboardDataModel(
|
|
id=uuid.uuid4(),
|
|
name="test__reserved__quote__model",
|
|
dataModelType=DataModelType.PowerBIDataModel,
|
|
service=EntityReference(id=uuid.uuid4(), type="dashboardService"),
|
|
fullyQualifiedName="service.test__reserved__quote__model",
|
|
columns=[
|
|
Column(
|
|
name=ColumnName("parent__reserved__quote__column"),
|
|
dataType=DataType.STRING,
|
|
children=None, # None children
|
|
)
|
|
],
|
|
)
|
|
|
|
assert model_none_children.name.root == 'test"model'
|
|
assert model_none_children.columns[0].name.root == 'parent"column'
|