mirror of
https://github.com/apache/superset.git
synced 2026-08-04 04:52:32 +00:00
Co-authored-by: Mike Bridge <michael.bridge@ext.preset.io> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
134 lines
5.0 KiB
Python
134 lines
5.0 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.
|
|
"""Unit-level coverage for the version-restore engine control flow.
|
|
|
|
The happy path is exercised end-to-end in the per-entity integration
|
|
suites (``version_restore_tests.py``); here we pin the cheap, DB-free
|
|
guard branches with mocks: unknown entity, missing target transaction,
|
|
DELETE-row target, unregistered model, and the ``single_flush_scope``
|
|
flush contract.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from unittest.mock import MagicMock, patch
|
|
from uuid import UUID
|
|
|
|
import pytest
|
|
|
|
from superset.versioning.baseline import OPERATION_DELETE
|
|
from superset.versioning.restore import restore_version
|
|
from superset.versioning.utils import single_flush_scope
|
|
|
|
_UUID = UUID("00000000-0000-0000-0000-000000000000")
|
|
|
|
|
|
@patch("superset.versioning.restore.find_active_by_uuid", return_value=None)
|
|
def test_restore_version_returns_none_for_unknown_entity(mock_find) -> None:
|
|
"""Unknown entity UUID → engine returns None (caller raises 404)."""
|
|
result = restore_version(MagicMock(__name__="Dashboard"), _UUID, 123)
|
|
assert result is None
|
|
mock_find.assert_called_once()
|
|
|
|
|
|
def _engine_with_target(target: object) -> MagicMock:
|
|
"""A db.session mock whose version query resolves to *target*."""
|
|
session = MagicMock()
|
|
session.query.return_value.filter.return_value.one_or_none.return_value = target
|
|
return session
|
|
|
|
|
|
@patch("superset.versioning.restore.version_class")
|
|
@patch("superset.versioning.restore.db")
|
|
def test_restore_version_returns_none_for_missing_transaction(
|
|
mock_db, mock_version_class
|
|
) -> None:
|
|
"""No version row at the resolved transaction_id → None (404), e.g.
|
|
the row was retention-pruned between resolve and restore."""
|
|
mock_db.session = _engine_with_target(None)
|
|
result = restore_version(
|
|
MagicMock(__name__="Slice"), _UUID, 123, entity=MagicMock(id=1, uuid=_UUID)
|
|
)
|
|
assert result is None
|
|
|
|
|
|
@patch("superset.versioning.restore.version_class")
|
|
@patch("superset.versioning.restore.db")
|
|
def test_restore_version_refuses_delete_row_target(mock_db, mock_version_class) -> None:
|
|
"""A DELETE version row is never a valid target: Continuum's Reverter
|
|
would delete the live entity and report success. Engine treats it as
|
|
not-found."""
|
|
target = MagicMock(operation_type=OPERATION_DELETE)
|
|
mock_db.session = _engine_with_target(target)
|
|
result = restore_version(
|
|
MagicMock(__name__="Slice"), _UUID, 123, entity=MagicMock(id=1, uuid=_UUID)
|
|
)
|
|
assert result is None
|
|
target.revert.assert_not_called()
|
|
|
|
|
|
@patch("superset.versioning.restore.version_class")
|
|
@patch("superset.versioning.restore.db")
|
|
def test_restore_version_fails_closed_for_unregistered_model(
|
|
mock_db, mock_version_class
|
|
) -> None:
|
|
"""An unregistered model must raise, not silently restore without its
|
|
child relations (mirrors _RAISE_FOR_ACCESS_KWARG's fail-closed
|
|
dispatch)."""
|
|
mock_db.session = _engine_with_target(MagicMock(operation_type=0))
|
|
with pytest.raises(LookupError, match="SomeNewModel"):
|
|
restore_version(
|
|
MagicMock(__name__="SomeNewModel"),
|
|
_UUID,
|
|
123,
|
|
entity=MagicMock(id=1, uuid=_UUID),
|
|
)
|
|
|
|
|
|
@patch("superset.versioning.restore.version_class")
|
|
@patch("superset.versioning.restore.db")
|
|
def test_restore_version_rejects_entity_uuid_mismatch(
|
|
mock_db, mock_version_class
|
|
) -> None:
|
|
"""A preloaded *entity* must be the row *entity_uuid* names. If they
|
|
disagree the engine would restore one entity while the caller logs
|
|
another, so it raises instead of guessing."""
|
|
mock_db.session = _engine_with_target(MagicMock(operation_type=0))
|
|
other_uuid = UUID("00000000-0000-0000-0000-0000000000ff")
|
|
with pytest.raises(ValueError, match="does not match entity_uuid"):
|
|
restore_version(
|
|
MagicMock(__name__="Slice"),
|
|
_UUID,
|
|
123,
|
|
entity=MagicMock(id=1, uuid=other_uuid),
|
|
)
|
|
|
|
|
|
def test_single_flush_scope_flushes_once_on_clean_exit() -> None:
|
|
session = MagicMock()
|
|
with single_flush_scope(session):
|
|
session.flush.assert_not_called()
|
|
session.flush.assert_called_once()
|
|
|
|
|
|
def test_single_flush_scope_skips_flush_on_exception() -> None:
|
|
session = MagicMock()
|
|
with pytest.raises(RuntimeError):
|
|
with single_flush_scope(session):
|
|
raise RuntimeError("boom")
|
|
session.flush.assert_not_called()
|