# Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # 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 semantic layer models.""" from __future__ import annotations import uuid from typing import Any from unittest.mock import MagicMock, patch import pyarrow as pa import pytest from superset_core.semantic_layers.types import ( Dimension, Grains, Metric, ) from superset.semantic_layers.models import ( ColumnMetadata, get_column_type, MetricMetadata, SemanticLayer, SemanticView, ) from superset.utils.core import GenericDataType # ============================================================================= # get_column_type tests # ============================================================================= def test_get_column_type_temporal_date() -> None: """Test that date types map to TEMPORAL.""" assert get_column_type(pa.date32()) == GenericDataType.TEMPORAL assert get_column_type(pa.date64()) == GenericDataType.TEMPORAL def test_get_column_type_temporal_timestamp() -> None: """Test that timestamp types map to TEMPORAL.""" assert get_column_type(pa.timestamp("us")) == GenericDataType.TEMPORAL def test_get_column_type_temporal_time() -> None: """Test that time types map to TEMPORAL.""" assert get_column_type(pa.time64("us")) == GenericDataType.TEMPORAL assert get_column_type(pa.time32("ms")) == GenericDataType.TEMPORAL def test_get_column_type_numeric_integer() -> None: """Test that integer types map to NUMERIC.""" assert get_column_type(pa.int64()) == GenericDataType.NUMERIC assert get_column_type(pa.int32()) == GenericDataType.NUMERIC def test_get_column_type_numeric_float() -> None: """Test that float types map to NUMERIC.""" assert get_column_type(pa.float64()) == GenericDataType.NUMERIC def test_get_column_type_numeric_decimal() -> None: """Test that decimal types map to NUMERIC.""" assert get_column_type(pa.decimal128(38, 10)) == GenericDataType.NUMERIC def test_get_column_type_numeric_duration() -> None: """Test that duration types map to NUMERIC.""" assert get_column_type(pa.duration("us")) == GenericDataType.NUMERIC def test_get_column_type_boolean() -> None: """Test that boolean types map to BOOLEAN.""" assert get_column_type(pa.bool_()) == GenericDataType.BOOLEAN def test_get_column_type_string() -> None: """Test that string types map to STRING.""" assert get_column_type(pa.utf8()) == GenericDataType.STRING assert get_column_type(pa.large_utf8()) == GenericDataType.STRING def test_get_column_type_binary() -> None: """Test that binary types map to STRING.""" assert get_column_type(pa.binary()) == GenericDataType.STRING def test_get_column_type_unknown() -> None: """Test that unknown types default to STRING.""" assert get_column_type(pa.null()) == GenericDataType.STRING # ============================================================================= # MetricMetadata tests # ============================================================================= def test_metric_metadata_required_fields() -> None: """Test MetricMetadata with required fields only.""" metadata = MetricMetadata( metric_name="revenue", expression="SUM(amount)", ) assert metadata.metric_name == "revenue" assert metadata.expression == "SUM(amount)" assert metadata.verbose_name is None assert metadata.description is None assert metadata.d3format is None assert metadata.currency is None assert metadata.warning_text is None assert metadata.certified_by is None assert metadata.certification_details is None def test_metric_metadata_all_fields() -> None: """Test MetricMetadata with all fields.""" metadata = MetricMetadata( metric_name="revenue", expression="SUM(amount)", verbose_name="Total Revenue", description="Sum of all revenue", d3format="$,.2f", currency={"symbol": "$", "symbolPosition": "prefix"}, warning_text="Data may be incomplete", certified_by="Data Team", certification_details="Verified Q1 2024", ) assert metadata.metric_name == "revenue" assert metadata.expression == "SUM(amount)" assert metadata.verbose_name == "Total Revenue" assert metadata.description == "Sum of all revenue" assert metadata.d3format == "$,.2f" assert metadata.currency == {"symbol": "$", "symbolPosition": "prefix"} assert metadata.warning_text == "Data may be incomplete" assert metadata.certified_by == "Data Team" assert metadata.certification_details == "Verified Q1 2024" # ============================================================================= # ColumnMetadata tests # ============================================================================= def test_column_metadata_required_fields() -> None: """Test ColumnMetadata with required fields only.""" metadata = ColumnMetadata( column_name="order_date", type="DATE", is_dttm=True, ) assert metadata.column_name == "order_date" assert metadata.type == "DATE" assert metadata.is_dttm is True assert metadata.verbose_name is None assert metadata.description is None assert metadata.groupby is True assert metadata.filterable is True assert metadata.expression is None assert metadata.python_date_format is None assert metadata.advanced_data_type is None assert metadata.extra is None def test_column_metadata_all_fields() -> None: """Test ColumnMetadata with all fields.""" metadata = ColumnMetadata( column_name="order_date", type="DATE", is_dttm=True, verbose_name="Order Date", description="Date of the order", groupby=True, filterable=True, expression="DATE(order_timestamp)", python_date_format="%Y-%m-%d", advanced_data_type="date", extra='{"grain": "day"}', ) assert metadata.column_name == "order_date" assert metadata.type == "DATE" assert metadata.is_dttm is True assert metadata.verbose_name == "Order Date" assert metadata.description == "Date of the order" assert metadata.groupby is True assert metadata.filterable is True assert metadata.expression == "DATE(order_timestamp)" assert metadata.python_date_format == "%Y-%m-%d" assert metadata.advanced_data_type == "date" assert metadata.extra == '{"grain": "day"}' # ============================================================================= # SemanticLayer tests # ============================================================================= def test_semantic_layer_repr_with_name() -> None: """Test SemanticLayer __repr__ with name.""" layer = SemanticLayer() layer.name = "My Semantic Layer" layer.uuid = uuid.uuid4() assert repr(layer) == "My Semantic Layer" def test_semantic_layer_repr_without_name() -> None: """Test SemanticLayer __repr__ without name (uses uuid).""" layer = SemanticLayer() layer.name = None test_uuid = uuid.uuid4() layer.uuid = test_uuid assert repr(layer) == str(test_uuid) def test_semantic_layer_implementation_not_implemented() -> None: """Test that implementation raises KeyError for unregistered type.""" layer = SemanticLayer() with pytest.raises(KeyError): _ = layer.implementation def test_semantic_layer_implementation() -> None: """Test that implementation returns a configured semantic layer.""" layer = SemanticLayer() layer.type = "test_type" layer.configuration = '{"key": "value"}' mock_class = MagicMock() mock_impl = MagicMock() mock_class.from_configuration.return_value = mock_impl with patch.dict( "superset.semantic_layers.models.registry", {"test_type": mock_class}, ): # Clear cached property if it exists if "implementation" in layer.__dict__: del layer.__dict__["implementation"] result = layer.implementation mock_class.from_configuration.assert_called_once_with({"key": "value"}) assert result == mock_impl # ============================================================================= # SemanticView tests # ============================================================================= @pytest.fixture def mock_dimensions() -> list[Dimension]: """Create mock dimensions for testing.""" return [ Dimension( id="orders.order_date", name="order_date", type=pa.date32(), definition="orders.order_date", description="Date of the order", grain=Grains.DAY, ), Dimension( id="products.category", name="category", type=pa.utf8(), definition="products.category", description="Product category", grain=None, ), ] @pytest.fixture def mock_metrics() -> list[Metric]: """Create mock metrics for testing.""" return [ Metric( id="orders.revenue", name="revenue", type=pa.float64(), definition="SUM(orders.amount)", description="Total revenue", ), Metric( id="orders.count", name="order_count", type=pa.int64(), definition="COUNT(*)", description="Number of orders", ), ] @pytest.fixture def mock_implementation( mock_dimensions: list[Dimension], mock_metrics: list[Metric], ) -> MagicMock: """Create a mock implementation.""" impl = MagicMock() impl.get_dimensions.return_value = mock_dimensions impl.get_metrics.return_value = mock_metrics impl.uid.return_value = "semantic_view_uid_123" return impl @pytest.fixture def semantic_view(mock_implementation: MagicMock) -> SemanticView: """Create a SemanticView with mocked implementation.""" layer = SemanticLayer() layer.name = "Test Layer" layer.uuid = uuid.UUID("87654321-4321-8765-4321-876543218765") layer.perm = "[Test Layer](id:87654321432187654321876543218765)" view = SemanticView() view.name = "Orders View" view.description = "View of order data" view.id = 1 view.uuid = uuid.UUID("12345678-1234-5678-1234-567812345678") view.semantic_layer_uuid = uuid.UUID("87654321-4321-8765-4321-876543218765") view.semantic_layer = layer view.cache_timeout = 3600 view.configuration = "{}" view.perm = "[Test Layer].[Orders View](id:1)" # Persist mocked implementation on this instance view.__dict__["implementation"] = mock_implementation return view def test_semantic_view_repr_with_name() -> None: """Test SemanticView __repr__ with name.""" view = SemanticView() view.name = "My View" view.uuid = uuid.uuid4() assert repr(view) == "My View" def test_semantic_view_repr_without_name() -> None: """Test SemanticView __repr__ without name (uses uuid).""" view = SemanticView() view.name = None test_uuid = uuid.uuid4() view.uuid = test_uuid assert repr(view) == str(test_uuid) def test_semantic_view_type() -> None: """Test SemanticView type property.""" view = SemanticView() assert view.type == "semantic_view" def test_semantic_view_table_name() -> None: """Test SemanticView table_name property.""" view = SemanticView() view.name = "Orders View" assert view.table_name == "Orders View" def test_semantic_view_kind() -> None: """Test SemanticView kind property.""" view = SemanticView() assert view.kind == "semantic_view" def test_semantic_view_offset() -> None: """Test SemanticView offset property.""" view = SemanticView() assert view.offset == 0 def test_semantic_view_is_rls_supported() -> None: """Test SemanticView is_rls_supported property.""" view = SemanticView() assert view.is_rls_supported is False def test_semantic_view_query_language() -> None: """Test SemanticView query_language property.""" view = SemanticView() assert view.query_language is None def test_semantic_view_get_query_str() -> None: """Test SemanticView get_query_str method.""" view = SemanticView() result = view.get_query_str({}) assert result == "Not implemented for semantic layers" def test_semantic_view_get_extra_cache_keys() -> None: """Test SemanticView get_extra_cache_keys method.""" view = SemanticView() result = view.get_extra_cache_keys({}) assert result == [] def test_semantic_view_perm() -> None: """Test SemanticView perm stores the view-level permission string.""" view = SemanticView() view.perm = "[My Layer].[My View](id:42)" assert view.perm == "[My Layer].[My View](id:42)" def test_semantic_view_perm_none_by_default() -> None: """Test SemanticView perm is None when not set.""" view = SemanticView() assert view.perm is None def test_semantic_view_get_perm() -> None: """Test SemanticView.get_perm() format: [layer].[view](id:N).""" layer = SemanticLayer() layer.name = "My Layer" layer.uuid = uuid.UUID("87654321-4321-8765-4321-876543218765") view = SemanticView() view.id = 42 view.name = "My View" view.semantic_layer = layer assert view.get_perm() == "[My Layer].[My View](id:42)" def test_semantic_view_get_perm_without_layer() -> None: """Test get_perm uses 'unknown' when no semantic_layer.""" view = SemanticView() view.id = 1 view.name = "Orphan View" view.semantic_layer = None # type: ignore assert view.get_perm() == "[unknown].[Orphan View](id:1)" def test_semantic_view_uid( mock_implementation: MagicMock, mock_dimensions: list[Dimension], mock_metrics: list[Metric], ) -> None: """Test SemanticView uid property.""" view = SemanticView() view.name = "Test View" view.uuid = uuid.uuid4() view.semantic_layer_uuid = uuid.uuid4() with patch.object( SemanticView, "implementation", new_callable=lambda: property(lambda s: mock_implementation), ): assert view.uid == "semantic_view_uid_123" def test_semantic_view_metrics( mock_implementation: MagicMock, mock_metrics: list[Metric], ) -> None: """Test SemanticView metrics property.""" view = SemanticView() with patch.object( SemanticView, "implementation", new_callable=lambda: property(lambda s: mock_implementation), ): metrics = view.metrics assert len(metrics) == 2 assert metrics[0].metric_name == "revenue" assert metrics[0].expression == "SUM(orders.amount)" assert metrics[0].description == "Total revenue" assert metrics[1].metric_name == "order_count" def test_semantic_view_columns( mock_implementation: MagicMock, mock_dimensions: list[Dimension], ) -> None: """Test SemanticView columns property.""" view = SemanticView() with patch.object( SemanticView, "implementation", new_callable=lambda: property(lambda s: mock_implementation), ): columns = view.columns assert len(columns) == 2 assert columns[0].column_name == "order_date" assert columns[0].type == "date32[day]" assert columns[0].is_dttm is True assert columns[0].description == "Date of the order" assert columns[1].column_name == "category" assert columns[1].type == "string" assert columns[1].is_dttm is False def test_semantic_view_column_names( mock_implementation: MagicMock, mock_dimensions: list[Dimension], ) -> None: """Test SemanticView column_names property.""" view = SemanticView() with patch.object( SemanticView, "implementation", new_callable=lambda: property(lambda s: mock_implementation), ): column_names = view.column_names assert column_names == ["order_date", "category"] def test_semantic_view_get_time_grains( mock_implementation: MagicMock, mock_dimensions: list[Dimension], ) -> None: """Test SemanticView get_time_grains property.""" view = SemanticView() with patch.object( SemanticView, "implementation", new_callable=lambda: property(lambda s: mock_implementation), ): time_grains = view.get_time_grains() assert len(time_grains) == 1 assert time_grains[0]["name"] == "Day" assert time_grains[0]["duration"] == "P1D" def test_semantic_view_has_drill_by_columns_all_exist( mock_implementation: MagicMock, mock_dimensions: list[Dimension], ) -> None: """Test has_drill_by_columns when all columns exist.""" view = SemanticView() with patch.object( SemanticView, "implementation", new_callable=lambda: property(lambda s: mock_implementation), ): assert view.has_drill_by_columns(["order_date", "category"]) is True def test_semantic_view_has_drill_by_columns_some_missing( mock_implementation: MagicMock, mock_dimensions: list[Dimension], ) -> None: """Test has_drill_by_columns when some columns are missing.""" view = SemanticView() with patch.object( SemanticView, "implementation", new_callable=lambda: property(lambda s: mock_implementation), ): assert view.has_drill_by_columns(["order_date", "nonexistent"]) is False def test_semantic_view_has_drill_by_columns_empty( mock_implementation: MagicMock, mock_dimensions: list[Dimension], ) -> None: """Test has_drill_by_columns with empty list.""" view = SemanticView() with patch.object( SemanticView, "implementation", new_callable=lambda: property(lambda s: mock_implementation), ): assert view.has_drill_by_columns([]) is True def test_semantic_view_data( mock_implementation: MagicMock, mock_dimensions: list[Dimension], mock_metrics: list[Metric], ) -> None: """Test SemanticView data property.""" from superset.semantic_layers.models import SemanticLayer layer = SemanticLayer() layer.name = "My Semantic Layer" layer.uuid = uuid.UUID("87654321-4321-8765-4321-876543218765") layer.perm = "[My Semantic Layer](id:87654321432187654321876543218765)" view = SemanticView() view.name = "Orders View" view.description = "View of order data" view.id = 1 view.uuid = uuid.UUID("12345678-1234-5678-1234-567812345678") view.semantic_layer_uuid = uuid.UUID("87654321-4321-8765-4321-876543218765") view.semantic_layer = layer view.cache_timeout = 3600 with patch.object( SemanticView, "implementation", new_callable=lambda: property(lambda s: mock_implementation), ): data = view.data # Check core fields assert data["id"] == 1 assert data["uid"] == "semantic_view_uid_123" assert data["type"] == "semantic_view" assert data["name"] == "Orders View" assert data["description"] == "View of order data" assert data["cache_timeout"] == 3600 assert data["database"] == {} assert data["parent"] == {"name": "My Semantic Layer"} # Check columns assert len(data["columns"]) == 2 assert data["columns"][0]["column_name"] == "order_date" assert data["columns"][0]["type"] == "date32[day]" assert data["columns"][0]["is_dttm"] is True assert data["columns"][0]["type_generic"] == GenericDataType.TEMPORAL assert data["columns"][1]["column_name"] == "category" assert data["columns"][1]["type"] == "string" assert data["columns"][1]["type_generic"] == GenericDataType.STRING # Check metrics assert len(data["metrics"]) == 2 assert data["metrics"][0]["metric_name"] == "revenue" assert data["metrics"][0]["expression"] == "SUM(orders.amount)" assert data["metrics"][1]["metric_name"] == "order_count" # Check column_types and column_names assert data["column_types"] == [ GenericDataType.TEMPORAL, GenericDataType.STRING, ] assert data["column_names"] == ["order_date", "category"] # Check other fields assert data["table_name"] == "Orders View" assert data["datasource_name"] == "Orders View" assert data["offset"] == 0 def test_semantic_view_get_query_result( mock_implementation: MagicMock, ) -> None: """Test SemanticView get_query_result method.""" view = SemanticView() mock_query_object = MagicMock() mock_result = MagicMock() with patch( "superset.semantic_layers.models.get_results", return_value=mock_result, ) as mock_get_results: result = view.get_query_result(mock_query_object) mock_get_results.assert_called_once_with(mock_query_object) assert result == mock_result def test_semantic_view_data_for_slices( mock_implementation: MagicMock, mock_dimensions: list[Dimension], mock_metrics: list[Metric], ) -> None: """Test SemanticView data_for_slices returns same as data.""" from superset.semantic_layers.models import SemanticLayer layer = SemanticLayer() layer.name = "My Semantic Layer" layer.uuid = uuid.UUID("87654321-4321-8765-4321-876543218765") layer.perm = "[My Semantic Layer](id:87654321432187654321876543218765)" view = SemanticView() view.name = "Orders View" view.description = "View of order data" view.id = 1 view.uuid = uuid.UUID("12345678-1234-5678-1234-567812345678") view.semantic_layer_uuid = uuid.UUID("87654321-4321-8765-4321-876543218765") view.semantic_layer = layer view.cache_timeout = 3600 with patch.object( SemanticView, "implementation", new_callable=lambda: property(lambda s: mock_implementation), ): assert view.data_for_slices([]) == view.data def test_semantic_view_catalog_perm() -> None: """Test SemanticView catalog_perm returns None.""" view = SemanticView() assert view.catalog_perm is None def test_semantic_view_schema_perm() -> None: """Test SemanticView schema_perm returns None.""" view = SemanticView() assert view.schema_perm is None def test_semantic_view_schema() -> None: """Test SemanticView schema returns None.""" view = SemanticView() assert view.schema is None def test_semantic_view_url() -> None: """Test SemanticView url property.""" view = SemanticView() view.uuid = uuid.UUID("12345678-1234-5678-1234-567812345678") assert view.url == "/semantic_view/12345678-1234-5678-1234-567812345678/" def test_semantic_view_explore_url() -> None: """Test SemanticView explore_url property.""" view = SemanticView() view.id = 42 assert ( view.explore_url == "/explore/?datasource_type=semantic_view&datasource_id=42" ) def test_semantic_view_implementation() -> None: """Test SemanticView implementation property.""" view = SemanticView() view.name = "Test View" view.configuration = '{"key": "value"}' mock_semantic_layer = MagicMock() mock_semantic_view_impl = MagicMock() mock_semantic_layer.implementation.get_semantic_view.return_value = ( mock_semantic_view_impl ) view.semantic_layer = mock_semantic_layer # Clear cached property if it exists if "implementation" in view.__dict__: del view.__dict__["implementation"] result = view.implementation mock_semantic_layer.implementation.get_semantic_view.assert_called_once_with( "Test View", {"key": "value"}, ) assert result == mock_semantic_view_impl def test_semantic_view_get_compatible_metrics( mock_implementation: MagicMock, mock_dimensions: list[Dimension], mock_metrics: list[Metric], ) -> None: """Test SemanticView get_compatible_metrics maps names to objects and back.""" view = SemanticView() mock_implementation.get_compatible_metrics.return_value = {mock_metrics[0]} with patch.object( SemanticView, "implementation", new_callable=lambda: property(lambda s: mock_implementation), ): result = view.get_compatible_metrics( selected_metrics=["revenue", "missing_metric"], selected_dimensions=["order_date", "missing_dimension"], ) assert result == ["revenue"] args = mock_implementation.get_compatible_metrics.call_args.args assert args[0] == {mock_metrics[0]} assert args[1] == {mock_dimensions[0]} def test_semantic_view_get_compatible_dimensions( mock_implementation: MagicMock, mock_dimensions: list[Dimension], mock_metrics: list[Metric], ) -> None: """Test SemanticView get_compatible_dimensions maps names to objects and back.""" view = SemanticView() mock_implementation.get_compatible_dimensions.return_value = {mock_dimensions[1]} with patch.object( SemanticView, "implementation", new_callable=lambda: property(lambda s: mock_implementation), ): result = view.get_compatible_dimensions( selected_metrics=["order_count", "missing_metric"], selected_dimensions=["category", "missing_dimension"], ) assert result == ["category"] args = mock_implementation.get_compatible_dimensions.call_args.args assert args[0] == {mock_metrics[1]} assert args[1] == {mock_dimensions[1]} # ============================================================================= # SemanticLayer.get_perm tests # ============================================================================= def test_semantic_layer_get_perm() -> None: """Test SemanticLayer.get_perm() format.""" layer = SemanticLayer() layer.name = "My Layer" layer.uuid = uuid.UUID("abcdef12-3456-7890-abcd-ef1234567890") assert layer.get_perm() == "[My Layer](id:abcdef1234567890abcdef1234567890)" def test_semantic_layer_get_perm_special_characters() -> None: """Test get_perm with special characters in the layer name.""" layer = SemanticLayer() layer.name = "Layer [with] (parens)" layer.uuid = uuid.UUID("11111111-1111-1111-1111-111111111111") assert ( layer.get_perm() == "[Layer [with] (parens)](id:11111111111111111111111111111111)" ) # ============================================================================= # SemanticView.raise_for_access tests # ============================================================================= def test_semantic_view_raise_for_access_all_datasources(app: Any) -> None: """Test raise_for_access passes when user has all_datasource_access.""" from superset import security_manager layer = SemanticLayer() layer.name = "Layer" layer.uuid = uuid.UUID("abcdef12-3456-7890-abcd-ef1234567890") layer.perm = layer.get_perm() view = SemanticView() view.semantic_layer = layer with patch.object( security_manager, "can_access_all_datasources", return_value=True ): view.raise_for_access() def test_semantic_view_raise_for_access_view_perm(app: Any) -> None: """Test raise_for_access passes when user has view-level perm.""" from superset import security_manager layer = SemanticLayer() layer.name = "Layer" layer.uuid = uuid.UUID("abcdef12-3456-7890-abcd-ef1234567890") layer.perm = layer.get_perm() view = SemanticView() view.id = 1 view.name = "My View" view.semantic_layer = layer view.perm = "[Layer].[My View](id:1)" with ( patch.object( security_manager, "can_access_all_datasources", return_value=False ), patch.object( security_manager, "can_access", return_value=True ) as mock_can_access, ): view.raise_for_access() mock_can_access.assert_called_once_with( "datasource_access", "[Layer].[My View](id:1)" ) def test_semantic_view_raise_for_access_layer_perm(app: Any) -> None: """Test raise_for_access passes via layer perm when view perm is denied.""" from superset import security_manager layer = SemanticLayer() layer.name = "Layer" layer.uuid = uuid.UUID("abcdef12-3456-7890-abcd-ef1234567890") layer.perm = layer.get_perm() view = SemanticView() view.id = 1 view.name = "My View" view.semantic_layer = layer view.perm = "[Layer].[My View](id:1)" def side_effect(permission: str, perm: str) -> bool: # Deny view perm, allow layer perm return perm == layer.perm with ( patch.object( security_manager, "can_access_all_datasources", return_value=False ), patch.object( security_manager, "can_access", side_effect=side_effect ) as mock_can_access, ): view.raise_for_access() assert mock_can_access.call_count == 2 def test_semantic_view_raise_for_access_denied(app: Any) -> None: """Test raise_for_access raises SupersetSecurityException when denied.""" from superset import security_manager from superset.exceptions import SupersetSecurityException layer = SemanticLayer() layer.name = "Layer" layer.uuid = uuid.UUID("abcdef12-3456-7890-abcd-ef1234567890") layer.perm = layer.get_perm() view = SemanticView() view.id = 1 view.name = "My View" view.semantic_layer = layer view.perm = "[Layer].[My View](id:1)" with ( patch.object( security_manager, "can_access_all_datasources", return_value=False ), patch.object(security_manager, "can_access", return_value=False), ): with pytest.raises(SupersetSecurityException): view.raise_for_access() # ============================================================================= # create_missing_perms backfill tests # ============================================================================= def test_create_missing_perms_backfills_semantic_layer_perm(app: Any) -> None: """Test that create_missing_perms sets perm on layers with perm=NULL.""" from superset import security_manager from superset.extensions import db layer = SemanticLayer() layer.name = "Backfill Layer" layer.uuid = uuid.UUID("aaaa1111-2222-3333-4444-555566667777") layer.type = "test" layer.perm = None # simulate pre-existing layer without perm db.session.add(layer) db.session.flush() try: with ( patch.object(security_manager, "_get_all_pvms", return_value=[]), patch.object(security_manager, "add_permission_view_menu") as mock_add_pvm, ): security_manager.create_missing_perms() expected_perm = "[Backfill Layer](id:aaaa1111222233334444555566667777)" assert layer.perm == expected_perm mock_add_pvm.assert_any_call("datasource_access", expected_perm) finally: db.session.rollback() def test_create_missing_perms_backfills_semantic_view_perm(app: Any) -> None: """Test that create_missing_perms sets perm on views with perm=NULL.""" from superset import security_manager from superset.extensions import db layer = SemanticLayer() layer.name = "Backfill Layer" layer.uuid = uuid.UUID("aaaa1111-2222-3333-4444-555566667777") layer.type = "test" layer.perm = "[Backfill Layer](id:aaaa1111222233334444555566667777)" view = SemanticView() view.name = "Backfill View" view.semantic_layer_uuid = layer.uuid view.perm = None # simulate pre-existing view without perm db.session.add(layer) db.session.add(view) db.session.flush() try: with ( patch.object(security_manager, "_get_all_pvms", return_value=[]), patch.object(security_manager, "add_permission_view_menu") as mock_add_pvm, ): security_manager.create_missing_perms() expected_perm = f"[Backfill Layer].[Backfill View](id:{view.id})" assert view.perm == expected_perm mock_add_pvm.assert_any_call("datasource_access", expected_perm) finally: db.session.rollback() # ============================================================================= # SemanticView.get_perm with explicit layer_name # ============================================================================= def test_semantic_view_get_perm_explicit_layer_name() -> None: """Test get_perm with explicit layer_name parameter.""" view = SemanticView() view.id = 5 view.name = "My View" view.semantic_layer = None # type: ignore assert ( view.get_perm(layer_name="Explicit Layer") == "[Explicit Layer].[My View](id:5)" ) # ============================================================================= # Event listener tests # ============================================================================= def test_semantic_view_after_insert_sets_perm(app: Any) -> None: """Test that the after_insert event listener sets the perm column.""" from superset.extensions import db layer = SemanticLayer() layer.name = "Event Layer" layer.uuid = uuid.UUID("eeee1111-2222-3333-4444-555566667777") layer.type = "test" view = SemanticView() view.name = "Event View" view.semantic_layer_uuid = layer.uuid db.session.add(layer) db.session.add(view) db.session.flush() try: assert view.perm == f"[Event Layer].[Event View](id:{view.id})" finally: db.session.rollback() def test_semantic_view_before_update_updates_perm(app: Any) -> None: """Test that renaming a view updates its perm via the before_update event.""" from superset.extensions import db layer = SemanticLayer() layer.name = "Update Layer" layer.uuid = uuid.UUID("dddd1111-2222-3333-4444-555566667777") layer.type = "test" view = SemanticView() view.name = "Old Name" view.semantic_layer_uuid = layer.uuid db.session.add(layer) db.session.add(view) db.session.flush() try: view.name = "New Name" db.session.flush() assert view.perm == f"[Update Layer].[New Name](id:{view.id})" finally: db.session.rollback() def test_semantic_layer_after_delete_calls_security_manager() -> None: """Test SemanticLayer.after_delete delegates to security manager.""" from superset import security_manager mapper = MagicMock() connection = MagicMock() target = MagicMock(spec=SemanticLayer) with patch.object(security_manager, "semantic_layer_after_delete") as mock_hook: SemanticLayer.after_delete(mapper, connection, target) mock_hook.assert_called_once_with(mapper, connection, target) def test_semantic_view_after_delete_calls_security_manager() -> None: """Test SemanticView.after_delete delegates to security manager.""" from superset import security_manager mapper = MagicMock() connection = MagicMock() target = MagicMock(spec=SemanticView) with patch.object(security_manager, "semantic_view_after_delete") as mock_hook: SemanticView.after_delete(mapper, connection, target) mock_hook.assert_called_once_with(mapper, connection, target) def test_semantic_layer_rename_cascades_to_view_perms(app: Any) -> None: """Test that renaming a layer cascades the perm update to its views.""" from superset.extensions import db layer = SemanticLayer() layer.name = "Old Layer" layer.uuid = uuid.UUID("cccc1111-2222-3333-4444-555566667777") layer.type = "test" view = SemanticView() view.name = "Cascade View" view.semantic_layer_uuid = layer.uuid db.session.add(layer) db.session.add(view) db.session.flush() assert view.perm == f"[Old Layer].[Cascade View](id:{view.id})" try: layer.name = "New Layer" db.session.flush() # Cascade update is via raw SQL, so refresh the ORM object db.session.refresh(view) assert view.perm == f"[New Layer].[Cascade View](id:{view.id})" finally: db.session.rollback() # ============================================================================= # build_semantic_view_query dual perm tests # ============================================================================= def test_build_semantic_view_query_view_perm_grants_access(app: Any) -> None: """Test that view-level perm grants access in build_semantic_view_query.""" from superset import security_manager from superset.daos.datasource import DatasourceDAO from superset.extensions import db layer = SemanticLayer() layer.name = "Query Layer" layer.uuid = uuid.UUID("bbbb1111-2222-3333-4444-555566667777") layer.type = "test" view = SemanticView() view.name = "Query View" view.semantic_layer_uuid = layer.uuid db.session.add(layer) db.session.add(view) db.session.flush() try: # Only grant the view-level perm (not the layer perm) with ( patch.object( security_manager, "can_access_all_datasources", return_value=False ), patch.object( security_manager, "user_view_menu_names", return_value={view.perm}, ), ): query = DatasourceDAO.build_semantic_view_query(name_filter=None) results = db.session.execute(query).fetchall() item_ids = [row.item_id for row in results] assert view.id in item_ids finally: db.session.rollback() def test_build_semantic_view_query_layer_perm_grants_access(app: Any) -> None: """Test that layer-level perm grants access in build_semantic_view_query.""" from superset import security_manager from superset.daos.datasource import DatasourceDAO from superset.extensions import db layer = SemanticLayer() layer.name = "Query Layer 2" layer.uuid = uuid.UUID("aaaa2222-3333-4444-5555-666677778888") layer.type = "test" view = SemanticView() view.name = "Query View 2" view.semantic_layer_uuid = layer.uuid db.session.add(layer) db.session.add(view) db.session.flush() try: # Only grant the layer-level perm (not the view perm) with ( patch.object( security_manager, "can_access_all_datasources", return_value=False ), patch.object( security_manager, "user_view_menu_names", return_value={layer.perm}, ), ): query = DatasourceDAO.build_semantic_view_query(name_filter=None) results = db.session.execute(query).fetchall() item_ids = [row.item_id for row in results] assert view.id in item_ids finally: db.session.rollback() def test_build_semantic_view_query_no_perm_excludes(app: Any) -> None: """Test that views are excluded when user has neither view nor layer perm.""" from superset import security_manager from superset.daos.datasource import DatasourceDAO from superset.extensions import db layer = SemanticLayer() layer.name = "Query Layer 3" layer.uuid = uuid.UUID("aaaa3333-4444-5555-6666-777788889999") layer.type = "test" view = SemanticView() view.name = "Query View 3" view.semantic_layer_uuid = layer.uuid db.session.add(layer) db.session.add(view) db.session.flush() try: with ( patch.object( security_manager, "can_access_all_datasources", return_value=False ), patch.object( security_manager, "user_view_menu_names", return_value={"[unrelated](id:xxx)"}, ), ): query = DatasourceDAO.build_semantic_view_query(name_filter=None) results = db.session.execute(query).fetchall() item_ids = [row.item_id for row in results] assert view.id not in item_ids finally: db.session.rollback()