mirror of
https://github.com/apache/superset.git
synced 2026-09-01 04:51:23 +00:00
187 lines
7.5 KiB
Python
187 lines
7.5 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.
|
|
"""Regression test for #38012.
|
|
|
|
``RefreshDatasetCommand`` runs after ``UpdateDatasetCommand`` when the
|
|
Dataset Editor modal saves with ``override_columns=True``. Historically
|
|
it re-parsed the dataset's SQL with an empty runtime template context;
|
|
Jinja-templated virtual datasets like
|
|
``SELECT * FROM t {% if from_dttm %}WHERE ds > '{{ from_dttm }}'{% endif %}``
|
|
render to malformed SQL, sqlglot rejects it, and the whole PUT surfaces
|
|
as an "Invalid SQL" toast — even though the dataset row was already
|
|
committed successfully by ``UpdateDatasetCommand``.
|
|
|
|
``get_virtual_table_metadata`` now raises a dedicated
|
|
``SupersetVirtualTableParseException`` for the specific template/parse
|
|
failure paths, and ``RefreshDatasetCommand`` catches only that
|
|
subclass. Genuine driver, connection, and permission errors still raise
|
|
the broader ``SupersetGenericDBErrorException`` (from
|
|
``get_columns_description``'s catch-all) and bubble up unchanged.
|
|
|
|
This module covers:
|
|
|
|
1. Template/parse failures become a warning (no re-raise).
|
|
2. Generic DB errors (connection, permission, driver) still propagate.
|
|
3. Security failures still propagate.
|
|
4. Pre-existing validation paths (not-found, forbidden) still work.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
|
|
import pytest
|
|
from pytest_mock import MockerFixture
|
|
|
|
from superset.commands.dataset.exceptions import (
|
|
DatasetForbiddenError,
|
|
DatasetNotFoundError,
|
|
)
|
|
from superset.commands.dataset.refresh import RefreshDatasetCommand
|
|
from superset.errors import ErrorLevel, SupersetError, SupersetErrorType
|
|
from superset.exceptions import (
|
|
SupersetGenericDBErrorException,
|
|
SupersetSecurityException,
|
|
SupersetVirtualTableParseException,
|
|
)
|
|
|
|
|
|
def test_refresh_swallows_virtual_table_parse_exception(
|
|
mocker: MockerFixture, caplog: pytest.LogCaptureFixture
|
|
) -> None:
|
|
"""Template/parse failures during metadata refresh must NOT bubble
|
|
up as a hard error — the dataset has already been persisted by the
|
|
caller (``UpdateDatasetCommand``), so the refresh is best-effort for
|
|
SQL that cannot be validated at save time. See #38012.
|
|
"""
|
|
mock_dataset_dao = mocker.patch("superset.commands.dataset.refresh.DatasetDAO")
|
|
mock_model = mocker.MagicMock()
|
|
mock_model.table_name = "jinja_dataset"
|
|
mock_model.fetch_metadata.side_effect = SupersetVirtualTableParseException(
|
|
message="Invalid SQL: unexpected token"
|
|
)
|
|
mock_dataset_dao.find_by_id.return_value = mock_model
|
|
mocker.patch(
|
|
"superset.commands.dataset.refresh.security_manager.raise_for_editorship"
|
|
)
|
|
# Skip datetime-format detection to keep the test focused.
|
|
mocker.patch(
|
|
"superset.commands.dataset.refresh.current_app.config.get",
|
|
return_value=False,
|
|
)
|
|
|
|
with caplog.at_level(logging.WARNING, logger="superset.commands.dataset.refresh"):
|
|
result = RefreshDatasetCommand(model_id=1).run()
|
|
|
|
assert result is mock_model, "command should return the model, not re-raise"
|
|
assert any(
|
|
"Dataset column refresh skipped for jinja_dataset" in rec.message
|
|
for rec in caplog.records
|
|
), "expected a warning naming the dataset"
|
|
|
|
|
|
def test_refresh_still_raises_generic_db_error(mocker: MockerFixture) -> None:
|
|
"""A plain ``SupersetGenericDBErrorException`` from ``fetch_metadata``
|
|
(raised by ``get_columns_description`` for connection / permission /
|
|
driver failures) must still bubble up. The subclass-based catch in
|
|
the refresh command must NOT silence these — even when the same
|
|
dataset also contains Jinja markers. See #38012."""
|
|
mock_dataset_dao = mocker.patch("superset.commands.dataset.refresh.DatasetDAO")
|
|
mock_model = mocker.MagicMock()
|
|
mock_model.table_name = "jinja_dataset_with_bad_connection"
|
|
mock_model.sql = (
|
|
"SELECT * FROM foo {% if from_dttm %}WHERE ds > '{{ from_dttm }}'{% endif %}"
|
|
)
|
|
mock_model.fetch_metadata.side_effect = SupersetGenericDBErrorException(
|
|
message="Connection refused"
|
|
)
|
|
mock_dataset_dao.find_by_id.return_value = mock_model
|
|
mocker.patch(
|
|
"superset.commands.dataset.refresh.security_manager.raise_for_editorship"
|
|
)
|
|
mocker.patch(
|
|
"superset.commands.dataset.refresh.current_app.config.get",
|
|
return_value=False,
|
|
)
|
|
|
|
with pytest.raises(SupersetGenericDBErrorException):
|
|
RefreshDatasetCommand(model_id=1).run()
|
|
|
|
|
|
def test_refresh_still_raises_on_security_exception(
|
|
mocker: MockerFixture,
|
|
) -> None:
|
|
"""``SupersetSecurityException`` must still fail hard — softening the
|
|
refresh path must not become a bypass for security checks. See #38012.
|
|
"""
|
|
mock_dataset_dao = mocker.patch("superset.commands.dataset.refresh.DatasetDAO")
|
|
mock_model = mocker.MagicMock()
|
|
mock_model.table_name = "restricted_dataset"
|
|
mock_model.fetch_metadata.side_effect = SupersetSecurityException(
|
|
SupersetError(
|
|
error_type=SupersetErrorType.DATASOURCE_SECURITY_ACCESS_ERROR,
|
|
message="Access denied",
|
|
level=ErrorLevel.ERROR,
|
|
)
|
|
)
|
|
mock_dataset_dao.find_by_id.return_value = mock_model
|
|
mocker.patch(
|
|
"superset.commands.dataset.refresh.security_manager.raise_for_editorship"
|
|
)
|
|
mocker.patch(
|
|
"superset.commands.dataset.refresh.current_app.config.get",
|
|
return_value=False,
|
|
)
|
|
|
|
# ``on_error`` only wraps ``SQLAlchemyError`` into
|
|
# ``DatasetRefreshFailedError``; other exception classes propagate
|
|
# unchanged. We just need to prove the softening does NOT swallow
|
|
# this one.
|
|
with pytest.raises(SupersetSecurityException):
|
|
RefreshDatasetCommand(model_id=1).run()
|
|
|
|
|
|
def test_refresh_dataset_not_found(mocker: MockerFixture) -> None:
|
|
"""Sanity check that the pre-existing not-found path still works.
|
|
Guards against the softening change accidentally masking validation
|
|
errors.
|
|
"""
|
|
mock_dataset_dao = mocker.patch("superset.commands.dataset.refresh.DatasetDAO")
|
|
mock_dataset_dao.find_by_id.return_value = None
|
|
|
|
with pytest.raises(DatasetNotFoundError):
|
|
RefreshDatasetCommand(model_id=999).run()
|
|
|
|
|
|
def test_refresh_forbidden(mocker: MockerFixture) -> None:
|
|
"""Sanity check that the pre-existing forbidden path still works."""
|
|
mock_dataset_dao = mocker.patch("superset.commands.dataset.refresh.DatasetDAO")
|
|
mock_dataset_dao.find_by_id.return_value = mocker.MagicMock()
|
|
mocker.patch(
|
|
"superset.commands.dataset.refresh.security_manager.raise_for_editorship",
|
|
side_effect=SupersetSecurityException(
|
|
SupersetError(
|
|
error_type=SupersetErrorType.DATASOURCE_SECURITY_ACCESS_ERROR,
|
|
message="Access denied",
|
|
level=ErrorLevel.ERROR,
|
|
)
|
|
),
|
|
)
|
|
|
|
with pytest.raises(DatasetForbiddenError):
|
|
RefreshDatasetCommand(model_id=1).run()
|