| # 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. |
| |
| from typing import Any, cast |
| |
| import pytest |
| from marshmallow import ValidationError |
| from pytest_mock import MockerFixture |
| |
| from superset.commands.dataset.exceptions import ( |
| DatabaseNotFoundValidationError, |
| DatasetExistsValidationError, |
| DatasetForbiddenError, |
| DatasetInvalidError, |
| DatasetNotFoundError, |
| MultiCatalogDisabledValidationError, |
| ) |
| from superset.commands.dataset.update import UpdateDatasetCommand, validate_folders |
| from superset.commands.exceptions import OwnersNotFoundValidationError |
| from superset.datasets.schemas import FolderSchema |
| from superset.errors import ErrorLevel, SupersetError, SupersetErrorType |
| from superset.exceptions import SupersetSecurityException |
| from tests.unit_tests.conftest import with_feature_flags |
| |
| |
| def test_update_dataset_not_found(mocker: MockerFixture) -> None: |
| """ |
| Test updating an unexisting ID raises a `DatasetNotFoundError`. |
| """ |
| mock_dataset_dao = mocker.patch("superset.commands.dataset.update.DatasetDAO") |
| mock_dataset_dao.find_by_id.return_value = None |
| |
| with pytest.raises(DatasetNotFoundError): |
| UpdateDatasetCommand(1, {"name": "test"}).run() |
| |
| |
| def test_update_dataset_forbidden(mocker: MockerFixture) -> None: |
| """ |
| Test try updating a dataset without permission raises a `DatasetForbiddenError`. |
| """ |
| mock_dataset_dao = mocker.patch("superset.commands.dataset.update.DatasetDAO") |
| mock_dataset_dao.find_by_id.return_value = mocker.MagicMock() |
| |
| mocker.patch( |
| "superset.commands.dataset.update.security_manager.raise_for_ownership", |
| side_effect=SupersetSecurityException( |
| SupersetError( |
| error_type=SupersetErrorType.MISSING_OWNERSHIP_ERROR, |
| message="Sample message", |
| level=ErrorLevel.ERROR, |
| ) |
| ), |
| ) |
| |
| with pytest.raises(DatasetForbiddenError): |
| UpdateDatasetCommand(1, {"name": "test"}).run() |
| |
| |
| @pytest.mark.parametrize( |
| ("payload, exception, error_msg"), |
| [ |
| ( |
| {"database_id": 2}, |
| DatabaseNotFoundValidationError, |
| "Database does not exist", |
| ), |
| ( |
| {"catalog": "test"}, |
| MultiCatalogDisabledValidationError, |
| "Only the default catalog is supported for this connection", |
| ), |
| ( |
| {"table_name": "table", "schema": "schema"}, |
| DatasetExistsValidationError, |
| "Dataset catalog.schema.table already exists", |
| ), |
| ( |
| {"owners": [1]}, |
| OwnersNotFoundValidationError, |
| "Owners are invalid", |
| ), |
| ], |
| ) |
| def test_update_dataset_validation_errors( |
| payload: dict[str, Any], |
| exception: Exception, |
| error_msg: str, |
| mocker: MockerFixture, |
| ) -> None: |
| """ |
| Test validation errors for the `UpdateDatasetCommand`. |
| """ |
| mock_dataset_dao = mocker.patch("superset.commands.dataset.update.DatasetDAO") |
| mocker.patch( |
| "superset.commands.dataset.update.security_manager.raise_for_ownership", |
| ) |
| mocker.patch("superset.commands.utils.security_manager.is_admin", return_value=True) |
| mocker.patch( |
| "superset.commands.utils.security_manager.get_user_by_id", return_value=None |
| ) |
| mock_database = mocker.MagicMock() |
| mock_database.id = 1 |
| mock_database.get_default_catalog.return_value = "catalog" |
| mock_database.allow_multi_catalog = False |
| mock_dataset = mocker.MagicMock() |
| mock_dataset.database = mock_database |
| mock_dataset.catalog = "catalog" |
| mock_dataset_dao.find_by_id.return_value = mock_dataset |
| |
| if exception == DatabaseNotFoundValidationError: |
| mock_dataset_dao.get_database_by_id.return_value = None |
| else: |
| mock_dataset_dao.get_database_by_id.return_value = mock_database |
| |
| if exception == DatasetExistsValidationError: |
| mock_dataset_dao.validate_update_uniqueness.return_value = False |
| else: |
| mock_dataset_dao.validate_update_uniqueness.return_value = True |
| |
| with pytest.raises(DatasetInvalidError) as excinfo: |
| UpdateDatasetCommand(1, payload).run() |
| assert any(error_msg in str(exc) for exc in excinfo.value._exceptions) |
| |
| |
| @with_feature_flags(DATASET_FOLDERS=True) |
| def test_validate_folders(mocker: MockerFixture) -> None: |
| """ |
| Test the folder validation. |
| """ |
| metrics = [mocker.MagicMock(metric_name="metric1", uuid="uuid1")] |
| columns = [ |
| mocker.MagicMock(column_name="column1", uuid="uuid2"), |
| mocker.MagicMock(column_name="column2", uuid="uuid3"), |
| ] |
| |
| validate_folders(folders=[], metrics=metrics, columns=columns) |
| |
| folders = cast( |
| list[FolderSchema], |
| [ |
| { |
| "uuid": "uuid4", |
| "type": "folder", |
| "name": "My folder", |
| "children": [ |
| { |
| "uuid": "uuid1", |
| "type": "metric", |
| "name": "metric1", |
| }, |
| { |
| "uuid": "uuid2", |
| "type": "column", |
| "name": "column1", |
| }, |
| { |
| "uuid": "uuid3", |
| "type": "column", |
| "name": "column2", |
| }, |
| ], |
| }, |
| ], |
| ) |
| validate_folders(folders=folders, metrics=metrics, columns=columns) |
| |
| |
| @with_feature_flags(DATASET_FOLDERS=True) |
| def test_validate_folders_cycle(mocker: MockerFixture) -> None: |
| """ |
| Test that we can detect cycles in the folder structure. |
| """ |
| folders = cast( |
| list[FolderSchema], |
| [ |
| { |
| "uuid": "uuid1", |
| "type": "folder", |
| "name": "My folder", |
| "children": [ |
| { |
| "uuid": "uuid2", |
| "type": "folder", |
| "name": "My other folder", |
| "children": [ |
| { |
| "uuid": "uuid1", |
| "type": "folder", |
| "name": "My folder", |
| "children": [], |
| }, |
| ], |
| }, |
| ], |
| }, |
| ], |
| ) |
| |
| with pytest.raises(ValidationError) as excinfo: |
| validate_folders(folders=folders, metrics=[], columns=[]) |
| assert str(excinfo.value) == "Cycle detected: uuid1 appears in its ancestry" |
| |
| |
| @with_feature_flags(DATASET_FOLDERS=True) |
| def test_validate_folders_inter_cycle(mocker: MockerFixture) -> None: |
| """ |
| Test that we can detect cycles between folders. |
| """ |
| folders = cast( |
| list[FolderSchema], |
| [ |
| { |
| "uuid": "uuid1", |
| "type": "folder", |
| "name": "My folder", |
| "children": [ |
| { |
| "uuid": "uuid2", |
| "type": "folder", |
| "name": "My other folder", |
| "children": [], |
| }, |
| ], |
| }, |
| { |
| "uuid": "uuid2", |
| "type": "folder", |
| "name": "My other folder", |
| "children": [ |
| { |
| "uuid": "uuid1", |
| "type": "folder", |
| "name": "My folder", |
| "children": [], |
| }, |
| ], |
| }, |
| ], |
| ) |
| |
| with pytest.raises(ValidationError) as excinfo: |
| validate_folders(folders=folders, metrics=[], columns=[]) |
| assert str(excinfo.value) == "Duplicate UUID in folder structure: uuid2" |
| |
| |
| @with_feature_flags(DATASET_FOLDERS=True) |
| def test_validate_folders_duplicates(mocker: MockerFixture) -> None: |
| """ |
| Test that metrics and columns belong to a single folder. |
| """ |
| metrics = [mocker.MagicMock(metric_name="count", uuid="uuid2")] |
| folders = cast( |
| list[FolderSchema], |
| [ |
| { |
| "uuid": "uuid1", |
| "type": "folder", |
| "name": "My folder", |
| "children": [ |
| { |
| "uuid": "uuid2", |
| "type": "metric", |
| "name": "count", |
| }, |
| ], |
| }, |
| { |
| "uuid": "uuid2", |
| "type": "folder", |
| "name": "My other folder", |
| "children": [ |
| { |
| "uuid": "uuid2", |
| "type": "metric", |
| "name": "count", |
| }, |
| ], |
| }, |
| ], |
| ) |
| |
| with pytest.raises(ValidationError) as excinfo: |
| validate_folders(folders=folders, metrics=metrics, columns=[]) |
| assert str(excinfo.value) == "Duplicate UUID in folder structure: uuid2" |
| |
| |
| @with_feature_flags(DATASET_FOLDERS=True) |
| def test_validate_folders_duplicate_name_not_siblings(mocker: MockerFixture) -> None: |
| """ |
| Duplicate folder names are allowed if folders are not siblings. |
| """ |
| folders = cast( |
| list[FolderSchema], |
| [ |
| { |
| "uuid": "uuid1", |
| "type": "folder", |
| "name": "Sales", |
| "children": [ |
| { |
| "uuid": "uuid2", |
| "type": "folder", |
| "name": "Core", |
| "children": [], |
| }, |
| ], |
| }, |
| { |
| "uuid": "uuid3", |
| "type": "folder", |
| "name": "Engineering", |
| "children": [ |
| { |
| "uuid": "uuid4", |
| "type": "folder", |
| "name": "Core", |
| "children": [], |
| }, |
| ], |
| }, |
| ], |
| ) |
| |
| validate_folders(folders=folders, metrics=[], columns=[]) |
| |
| |
| @with_feature_flags(DATASET_FOLDERS=True) |
| def test_validate_folders_duplicate_name_siblings(mocker: MockerFixture) -> None: |
| """ |
| Duplicate folder names are not allowed if folders are siblings. |
| """ |
| folders = cast( |
| list[FolderSchema], |
| [ |
| { |
| "uuid": "uuid1", |
| "type": "folder", |
| "name": "Sales", |
| "children": [ |
| { |
| "uuid": "uuid2", |
| "type": "folder", |
| "name": "Core", |
| "children": [], |
| }, |
| ], |
| }, |
| { |
| "uuid": "uuid3", |
| "type": "folder", |
| "name": "Sales", |
| "children": [ |
| { |
| "uuid": "uuid4", |
| "type": "folder", |
| "name": "Other", |
| "children": [], |
| }, |
| ], |
| }, |
| ], |
| ) |
| |
| with pytest.raises(ValidationError) as excinfo: |
| validate_folders(folders=folders, metrics=[], columns=[]) |
| assert str(excinfo.value) == "Duplicate folder name: Sales" |
| |
| |
| @with_feature_flags(DATASET_FOLDERS=True) |
| def test_validate_folders_invalid_names(mocker: MockerFixture) -> None: |
| """ |
| Test that we can detect reserved folder names. |
| """ |
| folders_with_metrics = cast( |
| list[FolderSchema], |
| [ |
| { |
| "uuid": "uuid1", |
| "type": "folder", |
| "name": "Metrics", |
| "children": [], |
| }, |
| ], |
| ) |
| folders_with_columns = cast( |
| list[FolderSchema], |
| [ |
| { |
| "uuid": "uuid1", |
| "type": "folder", |
| "name": "Columns", |
| "children": [], |
| }, |
| ], |
| ) |
| |
| with pytest.raises(ValidationError) as excinfo: |
| validate_folders(folders=folders_with_metrics, metrics=[], columns=[]) |
| assert str(excinfo.value) == "Folder cannot have name 'Metrics'" |
| |
| with pytest.raises(ValidationError) as excinfo: |
| validate_folders(folders=folders_with_columns, metrics=[], columns=[]) |
| assert str(excinfo.value) == "Folder cannot have name 'Columns'" |
| |
| |
| @with_feature_flags(DATASET_FOLDERS=True) |
| def test_validate_folders_invalid_uuid(mocker: MockerFixture) -> None: |
| """ |
| Test that we can detect invalid UUIDs. |
| """ |
| folders = cast( |
| list[FolderSchema], |
| [ |
| { |
| "uuid": "uuid4", |
| "type": "folder", |
| "name": "My folder", |
| "children": [ |
| { |
| "uuid": "uuid2", |
| "type": "metric", |
| "name": "metric1", |
| }, |
| ], |
| }, |
| ], |
| ) |
| |
| with pytest.raises(ValidationError): |
| FolderSchema(many=True).load(folders) |