Files
superset2/tests/unit_tests/semantic_layers/api_test.py
Beto Dealmeida 41a938d4d3 Address semantic layer review nits
Improve semantic layer schema refresh error handling and connections endpoint behavior to reduce noisy failures while keeping this feature branch focused. Also restore frontend typing consistency and add debounce coverage for dynamic schema refresh.
2026-05-05 09:06:09 -04:00

1474 lines
42 KiB
Python

# 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.
import uuid as uuid_lib
from typing import Any
from unittest.mock import MagicMock
import pytest
from pytest_mock import MockerFixture
from superset.commands.semantic_layer.exceptions import (
SemanticLayerCreateFailedError,
SemanticLayerDeleteFailedError,
SemanticLayerInvalidError,
SemanticLayerNotFoundError,
SemanticLayerUpdateFailedError,
SemanticViewForbiddenError,
SemanticViewInvalidError,
SemanticViewNotFoundError,
SemanticViewUpdateFailedError,
)
SEMANTIC_LAYERS_APP = pytest.mark.parametrize(
"app",
[{"FEATURE_FLAGS": {"SEMANTIC_LAYERS": True}}],
indirect=True,
)
@SEMANTIC_LAYERS_APP
def test_put_semantic_view(
client: Any,
full_api_access: None,
mocker: MockerFixture,
) -> None:
"""Test successful PUT updates a semantic view."""
changed_model = MagicMock()
changed_model.id = 1
mock_command = mocker.patch(
"superset.semantic_layers.api.UpdateSemanticViewCommand",
)
mock_command.return_value.run.return_value = changed_model
payload = {"description": "Updated description", "cache_timeout": 300}
response = client.put(
"/api/v1/semantic_view/1",
json=payload,
)
assert response.status_code == 200
assert response.json["id"] == 1
assert response.json["result"] == payload
mock_command.assert_called_once_with("1", payload)
@SEMANTIC_LAYERS_APP
def test_put_semantic_view_not_found(
client: Any,
full_api_access: None,
mocker: MockerFixture,
) -> None:
"""Test PUT returns 404 when semantic view does not exist."""
mock_command = mocker.patch(
"superset.semantic_layers.api.UpdateSemanticViewCommand",
)
mock_command.return_value.run.side_effect = SemanticViewNotFoundError()
response = client.put(
"/api/v1/semantic_view/999",
json={"description": "Updated"},
)
assert response.status_code == 404
@SEMANTIC_LAYERS_APP
def test_put_semantic_view_forbidden(
client: Any,
full_api_access: None,
mocker: MockerFixture,
) -> None:
"""Test PUT returns 403 when user lacks ownership."""
mock_command = mocker.patch(
"superset.semantic_layers.api.UpdateSemanticViewCommand",
)
mock_command.return_value.run.side_effect = SemanticViewForbiddenError()
response = client.put(
"/api/v1/semantic_view/1",
json={"description": "Updated"},
)
assert response.status_code == 403
@SEMANTIC_LAYERS_APP
def test_put_semantic_view_invalid(
client: Any,
full_api_access: None,
mocker: MockerFixture,
) -> None:
"""Test PUT returns 422 when validation fails."""
mock_command = mocker.patch(
"superset.semantic_layers.api.UpdateSemanticViewCommand",
)
mock_command.return_value.run.side_effect = SemanticViewInvalidError()
response = client.put(
"/api/v1/semantic_view/1",
json={"description": "Updated"},
)
assert response.status_code == 422
@SEMANTIC_LAYERS_APP
def test_put_semantic_view_update_failed(
client: Any,
full_api_access: None,
mocker: MockerFixture,
) -> None:
"""Test PUT returns 422 when the update operation fails."""
mock_command = mocker.patch(
"superset.semantic_layers.api.UpdateSemanticViewCommand",
)
mock_command.return_value.run.side_effect = SemanticViewUpdateFailedError()
response = client.put(
"/api/v1/semantic_view/1",
json={"description": "Updated"},
)
assert response.status_code == 422
@SEMANTIC_LAYERS_APP
def test_put_semantic_view_bad_request(
client: Any,
full_api_access: None,
mocker: MockerFixture,
) -> None:
"""Test PUT returns 400 when the request payload has invalid fields."""
# Marshmallow raises ValidationError for unknown fields
mocker.patch(
"superset.semantic_layers.api.UpdateSemanticViewCommand",
)
response = client.put(
"/api/v1/semantic_view/1",
json={"invalid_field": "value"},
)
assert response.status_code == 400
@SEMANTIC_LAYERS_APP
def test_put_semantic_view_description_only(
client: Any,
full_api_access: None,
mocker: MockerFixture,
) -> None:
"""Test PUT with only description field."""
changed_model = MagicMock()
changed_model.id = 1
mock_command = mocker.patch(
"superset.semantic_layers.api.UpdateSemanticViewCommand",
)
mock_command.return_value.run.return_value = changed_model
payload = {"description": "New description"}
response = client.put(
"/api/v1/semantic_view/1",
json=payload,
)
assert response.status_code == 200
assert response.json["result"] == payload
@SEMANTIC_LAYERS_APP
def test_put_semantic_view_cache_timeout_only(
client: Any,
full_api_access: None,
mocker: MockerFixture,
) -> None:
"""Test PUT with only cache_timeout field."""
changed_model = MagicMock()
changed_model.id = 2
mock_command = mocker.patch(
"superset.semantic_layers.api.UpdateSemanticViewCommand",
)
mock_command.return_value.run.return_value = changed_model
payload = {"cache_timeout": 600}
response = client.put(
"/api/v1/semantic_view/2",
json=payload,
)
assert response.status_code == 200
assert response.json["id"] == 2
assert response.json["result"] == payload
@SEMANTIC_LAYERS_APP
def test_put_semantic_view_null_values(
client: Any,
full_api_access: None,
mocker: MockerFixture,
) -> None:
"""Test PUT with null values for both fields."""
changed_model = MagicMock()
changed_model.id = 1
mock_command = mocker.patch(
"superset.semantic_layers.api.UpdateSemanticViewCommand",
)
mock_command.return_value.run.return_value = changed_model
payload = {"description": None, "cache_timeout": None}
response = client.put(
"/api/v1/semantic_view/1",
json=payload,
)
assert response.status_code == 200
assert response.json["result"] == payload
@SEMANTIC_LAYERS_APP
def test_put_semantic_view_empty_payload(
client: Any,
full_api_access: None,
mocker: MockerFixture,
) -> None:
"""Test PUT with empty payload."""
changed_model = MagicMock()
changed_model.id = 1
mock_command = mocker.patch(
"superset.semantic_layers.api.UpdateSemanticViewCommand",
)
mock_command.return_value.run.return_value = changed_model
response = client.put(
"/api/v1/semantic_view/1",
json={},
)
assert response.status_code == 200
# =============================================================================
# SemanticLayerRestApi tests
# =============================================================================
@SEMANTIC_LAYERS_APP
def test_get_types(
client: Any,
full_api_access: None,
mocker: MockerFixture,
) -> None:
"""Test GET /types returns registered semantic layer types."""
mock_cls = MagicMock()
mock_cls.name = "Snowflake Semantic Layer"
mock_cls.description = "Connect to Snowflake."
mocker.patch.dict(
"superset.semantic_layers.api.registry",
{"snowflake": mock_cls},
clear=True,
)
response = client.get("/api/v1/semantic_layer/types")
assert response.status_code == 200
result = response.json["result"]
assert len(result) == 1
assert result[0] == {
"id": "snowflake",
"name": "Snowflake Semantic Layer",
"description": "Connect to Snowflake.",
}
@SEMANTIC_LAYERS_APP
def test_get_types_empty(
client: Any,
full_api_access: None,
mocker: MockerFixture,
) -> None:
"""Test GET /types returns empty list when no types registered."""
mocker.patch.dict(
"superset.semantic_layers.api.registry",
{},
clear=True,
)
response = client.get("/api/v1/semantic_layer/types")
assert response.status_code == 200
assert response.json["result"] == []
@SEMANTIC_LAYERS_APP
def test_configuration_schema(
client: Any,
full_api_access: None,
mocker: MockerFixture,
) -> None:
"""Test POST /schema/configuration returns schema without partial config."""
mock_cls = MagicMock()
mock_cls.get_configuration_schema.return_value = {"type": "object"}
mocker.patch.dict(
"superset.semantic_layers.api.registry",
{"snowflake": mock_cls},
clear=True,
)
response = client.post(
"/api/v1/semantic_layer/schema/configuration",
json={"type": "snowflake"},
)
assert response.status_code == 200
assert response.json["result"] == {"type": "object"}
mock_cls.get_configuration_schema.assert_called_once_with(None)
@SEMANTIC_LAYERS_APP
def test_configuration_schema_with_partial_config(
client: Any,
full_api_access: None,
mocker: MockerFixture,
) -> None:
"""Test POST /schema/configuration enriches schema with partial config."""
mock_config_obj = MagicMock()
mock_cls = MagicMock()
mock_cls.configuration_class.model_json_schema.return_value = {
"type": "object",
"properties": {"account": {"type": "string"}},
}
mock_cls.configuration_class.model_validate.return_value = mock_config_obj
mock_cls.get_configuration_schema.return_value = {
"type": "object",
"properties": {"database": {"enum": ["db1", "db2"]}},
}
mocker.patch.dict(
"superset.semantic_layers.api.registry",
{"snowflake": mock_cls},
clear=True,
)
response = client.post(
"/api/v1/semantic_layer/schema/configuration",
json={"type": "snowflake", "configuration": {"account": "test"}},
)
assert response.status_code == 200
mock_cls.get_configuration_schema.assert_called_once_with(mock_config_obj)
@SEMANTIC_LAYERS_APP
def test_configuration_schema_with_invalid_partial_config(
client: Any,
full_api_access: None,
mocker: MockerFixture,
) -> None:
"""Test /schema/configuration returns schema when partial config fails."""
from pydantic import ValidationError as PydanticValidationError
mock_cls = MagicMock()
mock_cls.configuration_class.model_json_schema.return_value = {
"type": "object",
"properties": {},
}
mock_cls.configuration_class.model_validate.side_effect = (
PydanticValidationError.from_exception_data(
title="test",
line_errors=[],
)
)
mock_cls.get_configuration_schema.return_value = {"type": "object"}
mocker.patch.dict(
"superset.semantic_layers.api.registry",
{"snowflake": mock_cls},
clear=True,
)
response = client.post(
"/api/v1/semantic_layer/schema/configuration",
json={"type": "snowflake", "configuration": {"bad": "data"}},
)
assert response.status_code == 200
mock_cls.get_configuration_schema.assert_called_once_with(None)
@SEMANTIC_LAYERS_APP
def test_configuration_schema_unknown_type(
client: Any,
full_api_access: None,
mocker: MockerFixture,
) -> None:
"""Test POST /schema/configuration returns 400 for unknown type."""
mocker.patch.dict(
"superset.semantic_layers.api.registry",
{},
clear=True,
)
response = client.post(
"/api/v1/semantic_layer/schema/configuration",
json={"type": "nonexistent"},
)
assert response.status_code == 400
@SEMANTIC_LAYERS_APP
def test_runtime_schema(
client: Any,
full_api_access: None,
mocker: MockerFixture,
) -> None:
"""Test POST /<uuid>/schema/runtime returns runtime schema."""
test_uuid = str(uuid_lib.uuid4())
mock_layer = MagicMock()
mock_layer.type = "snowflake"
mock_layer.implementation.configuration = {"account": "test"}
mock_dao = mocker.patch("superset.semantic_layers.api.SemanticLayerDAO")
mock_dao.find_by_uuid.return_value = mock_layer
mock_cls = MagicMock()
mock_cls.get_runtime_schema.return_value = {"type": "object"}
mocker.patch.dict(
"superset.semantic_layers.api.registry",
{"snowflake": mock_cls},
clear=True,
)
response = client.post(
f"/api/v1/semantic_layer/{test_uuid}/schema/runtime",
json={"runtime_data": {"database": "mydb"}},
)
assert response.status_code == 200
assert response.json["result"] == {"type": "object"}
mock_cls.get_runtime_schema.assert_called_once_with(
{"account": "test"}, {"database": "mydb"}
)
@SEMANTIC_LAYERS_APP
def test_runtime_schema_no_body(
client: Any,
full_api_access: None,
mocker: MockerFixture,
) -> None:
"""Test POST /<uuid>/schema/runtime works without a request body."""
test_uuid = str(uuid_lib.uuid4())
mock_layer = MagicMock()
mock_layer.type = "snowflake"
mock_layer.implementation.configuration = {"account": "test"}
mock_dao = mocker.patch("superset.semantic_layers.api.SemanticLayerDAO")
mock_dao.find_by_uuid.return_value = mock_layer
mock_cls = MagicMock()
mock_cls.get_runtime_schema.return_value = {"type": "object"}
mocker.patch.dict(
"superset.semantic_layers.api.registry",
{"snowflake": mock_cls},
clear=True,
)
response = client.post(
f"/api/v1/semantic_layer/{test_uuid}/schema/runtime",
)
assert response.status_code == 200
mock_cls.get_runtime_schema.assert_called_once_with({"account": "test"}, None)
@SEMANTIC_LAYERS_APP
def test_runtime_schema_not_found(
client: Any,
full_api_access: None,
mocker: MockerFixture,
) -> None:
"""Test POST /<uuid>/schema/runtime returns 404 when layer not found."""
mock_dao = mocker.patch("superset.semantic_layers.api.SemanticLayerDAO")
mock_dao.find_by_uuid.return_value = None
response = client.post(
f"/api/v1/semantic_layer/{uuid_lib.uuid4()}/schema/runtime",
)
assert response.status_code == 404
@SEMANTIC_LAYERS_APP
def test_runtime_schema_unknown_type(
client: Any,
full_api_access: None,
mocker: MockerFixture,
) -> None:
"""Test POST /<uuid>/schema/runtime returns 400 for unknown type."""
test_uuid = str(uuid_lib.uuid4())
mock_layer = MagicMock()
mock_layer.type = "unknown_type"
mock_dao = mocker.patch("superset.semantic_layers.api.SemanticLayerDAO")
mock_dao.find_by_uuid.return_value = mock_layer
mocker.patch.dict(
"superset.semantic_layers.api.registry",
{},
clear=True,
)
response = client.post(
f"/api/v1/semantic_layer/{test_uuid}/schema/runtime",
)
assert response.status_code == 400
assert "Unknown type" in response.json["message"]
@SEMANTIC_LAYERS_APP
def test_runtime_schema_exception(
client: Any,
full_api_access: None,
mocker: MockerFixture,
) -> None:
"""Test POST /<uuid>/schema/runtime returns 400 when schema raises."""
test_uuid = str(uuid_lib.uuid4())
mock_layer = MagicMock()
mock_layer.type = "snowflake"
mock_layer.implementation.configuration = {"account": "test"}
mock_dao = mocker.patch("superset.semantic_layers.api.SemanticLayerDAO")
mock_dao.find_by_uuid.return_value = mock_layer
mock_cls = MagicMock()
mock_cls.get_runtime_schema.side_effect = ValueError("Bad config")
mocker.patch.dict(
"superset.semantic_layers.api.registry",
{"snowflake": mock_cls},
clear=True,
)
response = client.post(
f"/api/v1/semantic_layer/{test_uuid}/schema/runtime",
)
assert response.status_code == 400
assert "Bad config" in response.json["message"]
@SEMANTIC_LAYERS_APP
def test_post_semantic_layer(
client: Any,
full_api_access: None,
mocker: MockerFixture,
) -> None:
"""Test POST / creates a semantic layer."""
test_uuid = uuid_lib.uuid4()
new_model = MagicMock()
new_model.uuid = test_uuid
mock_command = mocker.patch(
"superset.semantic_layers.api.CreateSemanticLayerCommand",
)
mock_command.return_value.run.return_value = new_model
payload = {
"name": "My Layer",
"type": "snowflake",
"configuration": {"account": "test"},
}
response = client.post("/api/v1/semantic_layer/", json=payload)
assert response.status_code == 201
assert response.json["result"]["uuid"] == str(test_uuid)
mock_command.assert_called_once_with(payload)
@SEMANTIC_LAYERS_APP
def test_post_semantic_layer_invalid(
client: Any,
full_api_access: None,
mocker: MockerFixture,
) -> None:
"""Test POST / returns 422 when validation fails."""
mock_command = mocker.patch(
"superset.semantic_layers.api.CreateSemanticLayerCommand",
)
mock_command.return_value.run.side_effect = SemanticLayerInvalidError(
"Unknown type: bad"
)
payload = {
"name": "My Layer",
"type": "bad",
"configuration": {},
}
response = client.post("/api/v1/semantic_layer/", json=payload)
assert response.status_code == 422
@SEMANTIC_LAYERS_APP
def test_post_semantic_layer_create_failed(
client: Any,
full_api_access: None,
mocker: MockerFixture,
) -> None:
"""Test POST / returns 422 when creation fails."""
mock_command = mocker.patch(
"superset.semantic_layers.api.CreateSemanticLayerCommand",
)
mock_command.return_value.run.side_effect = SemanticLayerCreateFailedError()
payload = {
"name": "My Layer",
"type": "snowflake",
"configuration": {"account": "test"},
}
response = client.post("/api/v1/semantic_layer/", json=payload)
assert response.status_code == 422
@SEMANTIC_LAYERS_APP
def test_post_semantic_layer_missing_required_fields(
client: Any,
full_api_access: None,
mocker: MockerFixture,
) -> None:
"""Test POST / returns 400 when required fields are missing."""
mocker.patch(
"superset.semantic_layers.api.CreateSemanticLayerCommand",
)
response = client.post(
"/api/v1/semantic_layer/",
json={"name": "Only name"},
)
assert response.status_code == 400
@SEMANTIC_LAYERS_APP
def test_put_semantic_layer(
client: Any,
full_api_access: None,
mocker: MockerFixture,
) -> None:
"""Test PUT /<uuid> updates a semantic layer."""
test_uuid = uuid_lib.uuid4()
changed_model = MagicMock()
changed_model.uuid = test_uuid
mock_command = mocker.patch(
"superset.semantic_layers.api.UpdateSemanticLayerCommand",
)
mock_command.return_value.run.return_value = changed_model
payload = {"name": "Updated Name"}
response = client.put(
f"/api/v1/semantic_layer/{test_uuid}",
json=payload,
)
assert response.status_code == 200
assert response.json["result"]["uuid"] == str(test_uuid)
mock_command.assert_called_once_with(str(test_uuid), payload)
@SEMANTIC_LAYERS_APP
def test_put_semantic_layer_not_found(
client: Any,
full_api_access: None,
mocker: MockerFixture,
) -> None:
"""Test PUT /<uuid> returns 404 when layer not found."""
mock_command = mocker.patch(
"superset.semantic_layers.api.UpdateSemanticLayerCommand",
)
mock_command.return_value.run.side_effect = SemanticLayerNotFoundError()
response = client.put(
f"/api/v1/semantic_layer/{uuid_lib.uuid4()}",
json={"name": "New"},
)
assert response.status_code == 404
@SEMANTIC_LAYERS_APP
def test_put_semantic_layer_invalid(
client: Any,
full_api_access: None,
mocker: MockerFixture,
) -> None:
"""Test PUT /<uuid> returns 422 when validation fails."""
mock_command = mocker.patch(
"superset.semantic_layers.api.UpdateSemanticLayerCommand",
)
mock_command.return_value.run.side_effect = SemanticLayerInvalidError(
"Name already exists"
)
response = client.put(
f"/api/v1/semantic_layer/{uuid_lib.uuid4()}",
json={"name": "Duplicate"},
)
assert response.status_code == 422
@SEMANTIC_LAYERS_APP
def test_put_semantic_layer_update_failed(
client: Any,
full_api_access: None,
mocker: MockerFixture,
) -> None:
"""Test PUT /<uuid> returns 422 when update fails."""
mock_command = mocker.patch(
"superset.semantic_layers.api.UpdateSemanticLayerCommand",
)
mock_command.return_value.run.side_effect = SemanticLayerUpdateFailedError()
response = client.put(
f"/api/v1/semantic_layer/{uuid_lib.uuid4()}",
json={"name": "Test"},
)
assert response.status_code == 422
@SEMANTIC_LAYERS_APP
def test_put_semantic_layer_validation_error(
client: Any,
full_api_access: None,
) -> None:
"""Test PUT /<uuid> returns 400 when payload fails schema validation."""
response = client.put(
f"/api/v1/semantic_layer/{uuid_lib.uuid4()}",
json={"cache_timeout": "not_a_number"},
)
assert response.status_code == 400
@SEMANTIC_LAYERS_APP
def test_delete_semantic_layer(
client: Any,
full_api_access: None,
mocker: MockerFixture,
) -> None:
"""Test DELETE /<uuid> deletes a semantic layer."""
test_uuid = str(uuid_lib.uuid4())
mock_command = mocker.patch(
"superset.semantic_layers.api.DeleteSemanticLayerCommand",
)
mock_command.return_value.run.return_value = None
response = client.delete(f"/api/v1/semantic_layer/{test_uuid}")
assert response.status_code == 200
mock_command.assert_called_once_with(test_uuid)
@SEMANTIC_LAYERS_APP
def test_delete_semantic_layer_not_found(
client: Any,
full_api_access: None,
mocker: MockerFixture,
) -> None:
"""Test DELETE /<uuid> returns 404 when layer not found."""
mock_command = mocker.patch(
"superset.semantic_layers.api.DeleteSemanticLayerCommand",
)
mock_command.return_value.run.side_effect = SemanticLayerNotFoundError()
response = client.delete(f"/api/v1/semantic_layer/{uuid_lib.uuid4()}")
assert response.status_code == 404
@SEMANTIC_LAYERS_APP
def test_delete_semantic_layer_failed(
client: Any,
full_api_access: None,
mocker: MockerFixture,
) -> None:
"""Test DELETE /<uuid> returns 422 when deletion fails."""
mock_command = mocker.patch(
"superset.semantic_layers.api.DeleteSemanticLayerCommand",
)
mock_command.return_value.run.side_effect = SemanticLayerDeleteFailedError()
response = client.delete(f"/api/v1/semantic_layer/{uuid_lib.uuid4()}")
assert response.status_code == 422
@SEMANTIC_LAYERS_APP
def test_get_list_semantic_layers(
client: Any,
full_api_access: None,
mocker: MockerFixture,
) -> None:
"""Test GET / returns list of semantic layers."""
layer1 = MagicMock()
layer1.uuid = uuid_lib.uuid4()
layer1.name = "Layer 1"
layer1.description = "First"
layer1.type = "snowflake"
layer1.cache_timeout = None
layer1.configuration = "{}"
layer1.changed_on_delta_humanized.return_value = "1 day ago"
layer2 = MagicMock()
layer2.uuid = uuid_lib.uuid4()
layer2.name = "Layer 2"
layer2.description = None
layer2.type = "snowflake"
layer2.cache_timeout = 300
layer2.configuration = '{"account": "test"}'
layer2.changed_on_delta_humanized.return_value = "2 hours ago"
mock_dao = mocker.patch("superset.semantic_layers.api.SemanticLayerDAO")
mock_dao.find_all.return_value = [layer1, layer2]
response = client.get("/api/v1/semantic_layer/")
assert response.status_code == 200
result = response.json["result"]
assert len(result) == 2
assert result[0]["name"] == "Layer 1"
assert result[1]["name"] == "Layer 2"
assert result[1]["cache_timeout"] == 300
assert result[1]["configuration"] == {"account": "test"}
@SEMANTIC_LAYERS_APP
def test_get_list_semantic_layers_empty(
client: Any,
full_api_access: None,
mocker: MockerFixture,
) -> None:
"""Test GET / returns empty list when no layers exist."""
mock_dao = mocker.patch("superset.semantic_layers.api.SemanticLayerDAO")
mock_dao.find_all.return_value = []
response = client.get("/api/v1/semantic_layer/")
assert response.status_code == 200
assert response.json["result"] == []
@SEMANTIC_LAYERS_APP
def test_get_semantic_layer(
client: Any,
full_api_access: None,
mocker: MockerFixture,
) -> None:
"""Test GET /<uuid> returns a single semantic layer."""
test_uuid = uuid_lib.uuid4()
layer = MagicMock()
layer.uuid = test_uuid
layer.name = "My Layer"
layer.description = "A layer"
layer.type = "snowflake"
layer.cache_timeout = 600
layer.configuration = '{"account": "test"}'
layer.changed_on_delta_humanized.return_value = "1 day ago"
mock_dao = mocker.patch("superset.semantic_layers.api.SemanticLayerDAO")
mock_dao.find_by_uuid.return_value = layer
response = client.get(f"/api/v1/semantic_layer/{test_uuid}")
assert response.status_code == 200
result = response.json["result"]
assert result["uuid"] == str(test_uuid)
assert result["name"] == "My Layer"
assert result["type"] == "snowflake"
assert result["cache_timeout"] == 600
assert result["configuration"] == {"account": "test"}
@SEMANTIC_LAYERS_APP
def test_get_semantic_layer_not_found(
client: Any,
full_api_access: None,
mocker: MockerFixture,
) -> None:
"""Test GET /<uuid> returns 404 when layer not found."""
mock_dao = mocker.patch("superset.semantic_layers.api.SemanticLayerDAO")
mock_dao.find_by_uuid.return_value = None
response = client.get(f"/api/v1/semantic_layer/{uuid_lib.uuid4()}")
assert response.status_code == 404
@SEMANTIC_LAYERS_APP
def test_serialize_layer_string_config(
client: Any,
full_api_access: None,
mocker: MockerFixture,
) -> None:
"""Test _serialize_layer handles string configuration (JSON)."""
layer = MagicMock()
layer.uuid = uuid_lib.uuid4()
layer.name = "Layer"
layer.description = None
layer.type = "snowflake"
layer.cache_timeout = None
layer.configuration = '{"account": "test"}'
layer.changed_on_delta_humanized.return_value = "1 day ago"
mock_dao = mocker.patch("superset.semantic_layers.api.SemanticLayerDAO")
mock_dao.find_by_uuid.return_value = layer
response = client.get(f"/api/v1/semantic_layer/{layer.uuid}")
assert response.status_code == 200
assert response.json["result"]["configuration"] == {"account": "test"}
@SEMANTIC_LAYERS_APP
def test_serialize_layer_dict_config(
client: Any,
full_api_access: None,
mocker: MockerFixture,
) -> None:
"""Test _serialize_layer handles dict configuration."""
layer = MagicMock()
layer.uuid = uuid_lib.uuid4()
layer.name = "Layer"
layer.description = None
layer.type = "snowflake"
layer.cache_timeout = None
layer.configuration = {"account": "test"}
layer.changed_on_delta_humanized.return_value = "1 day ago"
mock_dao = mocker.patch("superset.semantic_layers.api.SemanticLayerDAO")
mock_dao.find_by_uuid.return_value = layer
response = client.get(f"/api/v1/semantic_layer/{layer.uuid}")
assert response.status_code == 200
assert response.json["result"]["configuration"] == {"account": "test"}
@SEMANTIC_LAYERS_APP
def test_serialize_layer_none_config(
client: Any,
full_api_access: None,
mocker: MockerFixture,
) -> None:
"""Test _serialize_layer handles None configuration."""
layer = MagicMock()
layer.uuid = uuid_lib.uuid4()
layer.name = "Layer"
layer.description = None
layer.type = "snowflake"
layer.cache_timeout = None
layer.configuration = None
layer.changed_on_delta_humanized.return_value = "1 day ago"
mock_dao = mocker.patch("superset.semantic_layers.api.SemanticLayerDAO")
mock_dao.find_by_uuid.return_value = layer
response = client.get(f"/api/v1/semantic_layer/{layer.uuid}")
assert response.status_code == 200
assert response.json["result"]["configuration"] == {}
def test_infer_discriminators_injects_discriminator() -> None:
"""Test _infer_discriminators injects discriminator values."""
from superset.semantic_layers.api import _infer_discriminators
schema = {
"$defs": {
"VariantA": {"required": ["disc", "field_a"]},
},
"properties": {
"auth": {
"discriminator": {
"propertyName": "disc",
"mapping": {"a": "#/$defs/VariantA"},
},
},
},
}
data = {"auth": {"field_a": "value"}}
result = _infer_discriminators(schema, data)
assert result["auth"]["disc"] == "a"
def test_infer_discriminators_no_match() -> None:
"""Test _infer_discriminators returns data unchanged when no match."""
from superset.semantic_layers.api import _infer_discriminators
schema = {
"$defs": {
"VariantA": {"required": ["disc", "field_a"]},
},
"properties": {
"auth": {
"discriminator": {
"propertyName": "disc",
"mapping": {"a": "#/$defs/VariantA"},
},
},
},
}
data = {"auth": {"other": "value"}}
result = _infer_discriminators(schema, data)
assert "disc" not in result["auth"]
def test_infer_discriminators_skips_non_dict() -> None:
"""Test _infer_discriminators skips non-dict values."""
from superset.semantic_layers.api import _infer_discriminators
schema = {
"$defs": {},
"properties": {"auth": {"discriminator": {"propertyName": "disc"}}},
}
data = {"auth": "a string"}
result = _infer_discriminators(schema, data)
assert result == data
def test_infer_discriminators_skips_if_discriminator_present() -> None:
"""Test _infer_discriminators skips when discriminator already set."""
from superset.semantic_layers.api import _infer_discriminators
schema = {
"$defs": {},
"properties": {
"auth": {
"discriminator": {
"propertyName": "disc",
"mapping": {"a": "#/$defs/VariantA"},
},
},
},
}
data = {"auth": {"disc": "a", "field_a": "value"}}
result = _infer_discriminators(schema, data)
assert result["auth"]["disc"] == "a"
def test_infer_discriminators_no_discriminator() -> None:
"""Test _infer_discriminators skips properties without discriminator."""
from superset.semantic_layers.api import _infer_discriminators
schema = {
"$defs": {},
"properties": {"auth": {"type": "object"}},
}
data = {"auth": {"key": "val"}}
result = _infer_discriminators(schema, data)
assert result == data
def test_parse_partial_config_strict_success() -> None:
"""Test _parse_partial_config returns config on strict validation."""
from superset.semantic_layers.api import _parse_partial_config
mock_cls = MagicMock()
mock_cls.configuration_class.model_json_schema.return_value = {
"properties": {},
}
validated = MagicMock()
mock_cls.configuration_class.model_validate.return_value = validated
result = _parse_partial_config(mock_cls, {"key": "val"})
assert result == validated
def test_parse_partial_config_falls_back_to_partial() -> None:
"""Test _parse_partial_config falls back to partial validation."""
from pydantic import ValidationError as PydanticValidationError
from superset.semantic_layers.api import _parse_partial_config
mock_cls = MagicMock()
mock_cls.configuration_class.model_json_schema.return_value = {
"properties": {},
}
partial_result = MagicMock()
mock_cls.configuration_class.model_validate.side_effect = [
PydanticValidationError.from_exception_data(title="test", line_errors=[]),
partial_result,
]
result = _parse_partial_config(mock_cls, {"key": "val"})
assert result == partial_result
def test_parse_partial_config_returns_none_on_failure() -> None:
"""Test _parse_partial_config returns None when all validation fails."""
from pydantic import ValidationError as PydanticValidationError
from superset.semantic_layers.api import _parse_partial_config
mock_cls = MagicMock()
mock_cls.configuration_class.model_json_schema.return_value = {
"properties": {},
}
err = PydanticValidationError.from_exception_data(title="test", line_errors=[])
mock_cls.configuration_class.model_validate.side_effect = err
result = _parse_partial_config(mock_cls, {"key": "val"})
assert result is None
@SEMANTIC_LAYERS_APP
def test_configuration_schema_enrichment_error_fallback(
client: Any,
full_api_access: None,
mocker: MockerFixture,
) -> None:
"""Test configuration_schema falls back when enrichment raises."""
mock_cls = MagicMock()
mock_cls.configuration_class.model_json_schema.return_value = {
"properties": {},
}
mock_cls.configuration_class.model_validate.return_value = MagicMock()
mock_cls.get_configuration_schema.side_effect = [
RuntimeError("connection failed"),
{"type": "object"},
]
mocker.patch.dict(
"superset.semantic_layers.api.registry",
{"snowflake": mock_cls},
clear=True,
)
response = client.post(
"/api/v1/semantic_layer/schema/configuration",
json={"type": "snowflake", "configuration": {"account": "test"}},
)
assert response.status_code == 200
assert response.json["result"] == {"type": "object"}
assert response.json["warning"] == "connection failed"
assert mock_cls.get_configuration_schema.call_count == 2
@SEMANTIC_LAYERS_APP
def test_connections_list(
client: Any,
full_api_access: None,
mocker: MockerFixture,
) -> None:
"""Test GET /connections/ returns combined database and layer list."""
from datetime import datetime
mock_db = MagicMock()
mock_db.id = 1
mock_db.uuid = uuid_lib.uuid4()
mock_db.database_name = "PostgreSQL"
mock_db.backend = "postgresql"
mock_db.allow_run_async = False
mock_db.allow_dml = False
mock_db.allow_file_upload = False
mock_db.expose_in_sqllab = True
mock_db.changed_on = datetime(2026, 1, 1)
mock_db.changed_on_delta_humanized.return_value = "1 month ago"
mock_db.changed_by = None
mock_layer = MagicMock()
mock_layer.uuid = uuid_lib.uuid4()
mock_layer.name = "My Layer"
mock_layer.type = "snowflake"
mock_layer.description = "A layer"
mock_layer.cache_timeout = None
mock_layer.changed_on = datetime(2026, 2, 1)
mock_layer.changed_on_delta_humanized.return_value = "1 day ago"
mock_layer.changed_by = None
mock_db_session = mocker.patch("superset.semantic_layers.api.db.session")
db_query = MagicMock()
db_query.options.return_value = db_query
db_query.all.return_value = [mock_db]
db_query.filter.return_value = db_query
sl_query = MagicMock()
sl_query.options.return_value = sl_query
sl_query.all.return_value = [mock_layer]
sl_query.filter.return_value = sl_query
mock_db_session.query.side_effect = [db_query, sl_query]
mock_cls = MagicMock()
mock_cls.name = "Snowflake"
mocker.patch.dict(
"superset.semantic_layers.api.registry",
{"snowflake": mock_cls},
clear=True,
)
mocker.patch(
"superset.semantic_layers.api.is_feature_enabled",
return_value=True,
)
response = client.get("/api/v1/semantic_layer/connections/")
assert response.status_code == 200
assert response.json["count"] == 2
result = response.json["result"]
assert len(result) == 2
@SEMANTIC_LAYERS_APP
def test_connections_database_only(
client: Any,
full_api_access: None,
mocker: MockerFixture,
) -> None:
"""Test GET /connections/ returns 404 when feature flag is disabled."""
mocker.patch(
"superset.semantic_layers.api.is_feature_enabled",
return_value=False,
)
response = client.get("/api/v1/semantic_layer/connections/")
assert response.status_code == 404
@SEMANTIC_LAYERS_APP
def test_connections_name_filter(
client: Any,
full_api_access: None,
mocker: MockerFixture,
) -> None:
"""Test GET /connections/ with name filter."""
mock_db_session = mocker.patch("superset.semantic_layers.api.db.session")
db_query = MagicMock()
db_query.options.return_value = db_query
db_query.all.return_value = []
db_query.filter.return_value = db_query
sl_query = MagicMock()
sl_query.options.return_value = sl_query
sl_query.all.return_value = []
sl_query.filter.return_value = sl_query
mock_db_session.query.side_effect = [db_query, sl_query]
mocker.patch(
"superset.semantic_layers.api.is_feature_enabled",
return_value=True,
)
import prison as rison_lib
q = rison_lib.dumps(
{"filters": [{"col": "database_name", "opr": "ct", "value": "post"}]}
)
response = client.get(f"/api/v1/semantic_layer/connections/?q={q}")
assert response.status_code == 200
assert response.json["count"] == 0
# Verify filter was applied to both queries
db_query.filter.assert_called_once()
sl_query.filter.assert_called_once()
@SEMANTIC_LAYERS_APP
def test_connections_sort_by_name(
client: Any,
full_api_access: None,
mocker: MockerFixture,
) -> None:
"""Test GET /connections/ sorts by database_name."""
from datetime import datetime
mock_db = MagicMock()
mock_db.id = 1
mock_db.uuid = uuid_lib.uuid4()
mock_db.database_name = "Zebra DB"
mock_db.backend = "postgresql"
mock_db.allow_run_async = False
mock_db.allow_dml = False
mock_db.allow_file_upload = False
mock_db.expose_in_sqllab = True
mock_db.changed_on = datetime(2026, 1, 1)
mock_db.changed_on_delta_humanized.return_value = "1 month ago"
mock_db.changed_by = None
mock_layer = MagicMock()
mock_layer.uuid = uuid_lib.uuid4()
mock_layer.name = "Alpha Layer"
mock_layer.type = "snowflake"
mock_layer.description = None
mock_layer.cache_timeout = None
mock_layer.changed_on = datetime(2026, 2, 1)
mock_layer.changed_on_delta_humanized.return_value = "1 day ago"
mock_layer.changed_by = None
mock_db_session = mocker.patch("superset.semantic_layers.api.db.session")
db_query = MagicMock()
db_query.options.return_value = db_query
db_query.all.return_value = [mock_db]
sl_query = MagicMock()
sl_query.options.return_value = sl_query
sl_query.all.return_value = [mock_layer]
mock_db_session.query.side_effect = [db_query, sl_query]
mock_cls = MagicMock()
mock_cls.name = "Snowflake"
mocker.patch.dict(
"superset.semantic_layers.api.registry",
{"snowflake": mock_cls},
clear=True,
)
mocker.patch(
"superset.semantic_layers.api.is_feature_enabled",
return_value=True,
)
import prison as rison_lib
q = rison_lib.dumps({"order_column": "database_name", "order_direction": "asc"})
response = client.get(f"/api/v1/semantic_layer/connections/?q={q}")
assert response.status_code == 200
result = response.json["result"]
assert result[0]["database_name"] == "Alpha Layer"
assert result[1]["database_name"] == "Zebra DB"
@SEMANTIC_LAYERS_APP
def test_connections_source_type_filter(
client: Any,
full_api_access: None,
mocker: MockerFixture,
) -> None:
"""Test GET /connections/ with source_type filter."""
from datetime import datetime
mock_db = MagicMock()
mock_db.id = 1
mock_db.uuid = uuid_lib.uuid4()
mock_db.database_name = "PostgreSQL"
mock_db.backend = "postgresql"
mock_db.allow_run_async = False
mock_db.allow_dml = False
mock_db.allow_file_upload = False
mock_db.expose_in_sqllab = True
mock_db.changed_on = datetime(2026, 1, 1)
mock_db.changed_on_delta_humanized.return_value = "1 month ago"
mock_db.changed_by = None
mock_db_session = mocker.patch("superset.semantic_layers.api.db.session")
db_query = MagicMock()
db_query.options.return_value = db_query
db_query.all.return_value = [mock_db]
mock_db_session.query.return_value = db_query
mocker.patch(
"superset.semantic_layers.api.is_feature_enabled",
return_value=True,
)
import prison as rison_lib
q = rison_lib.dumps(
{"filters": [{"col": "source_type", "opr": "eq", "value": "database"}]}
)
response = client.get(f"/api/v1/semantic_layer/connections/?q={q}")
assert response.status_code == 200
assert response.json["count"] == 1
# Only one query call (for Database), not two
mock_db_session.query.assert_called_once()
@SEMANTIC_LAYERS_APP
def test_connections_source_type_semantic_layer_only(
client: Any,
full_api_access: None,
mocker: MockerFixture,
) -> None:
"""Test GET /connections/ with source_type=semantic_layer filter."""
from datetime import datetime
mock_layer = MagicMock()
mock_layer.uuid = uuid_lib.uuid4()
mock_layer.name = "My Layer"
mock_layer.type = "snowflake"
mock_layer.description = None
mock_layer.cache_timeout = None
mock_layer.changed_on = datetime(2026, 1, 1)
mock_layer.changed_on_delta_humanized.return_value = "1 day ago"
mock_layer.changed_by = None
mock_db_session = mocker.patch("superset.semantic_layers.api.db.session")
sl_query = MagicMock()
sl_query.options.return_value = sl_query
sl_query.all.return_value = [mock_layer]
mock_db_session.query.return_value = sl_query
mock_cls = MagicMock()
mock_cls.name = "Snowflake"
mocker.patch.dict(
"superset.semantic_layers.api.registry",
{"snowflake": mock_cls},
clear=True,
)
mocker.patch(
"superset.semantic_layers.api.is_feature_enabled",
return_value=True,
)
import prison as rison_lib
q = rison_lib.dumps(
{
"filters": [
{"col": "source_type", "opr": "eq", "value": "semantic_layer"},
{"col": "other_col", "opr": "eq", "value": "ignored"},
]
}
)
response = client.get(f"/api/v1/semantic_layer/connections/?q={q}")
assert response.status_code == 200
assert response.json["count"] == 1
result = response.json["result"][0]
assert result["source_type"] == "semantic_layer"
# Only one query (SemanticLayer), no Database query
mock_db_session.query.assert_called_once()