Files
superset2/tests/integration_tests/versioning/skip_unmodified_tests.py
Mike Bridge 7ce5f1d0e7 test(versioning): integration tests for SkipUnmodifiedPlugin (FR-026)
Locks in the no-op-suppression behavior implemented by
``SkipUnmodifiedPlugin`` (which lives in ``superset/versioning/factory.py``
shipping with the foundation commit). Five integration tests:

1. Owners-only edit doesn't mint a version row — exercises the
   case where every dirty column is an excluded relationship.
2. Re-save with identical scalar values doesn't mint a row —
   exercises the json_metadata re-serialise path where
   ``set_dash_metadata`` rewrites the column to a different byte
   sequence with identical parsed content; the plugin must compare
   post-flush values against the prior shadow row to detect this.
3. Real scalar change DOES mint a row — guards against the plugin
   over-suppressing.
4. Same assertion on a Slice (covers the ``String`` column path on
   a different entity type).
5. ``json_metadata`` sub-key edit DOES mint a row — covers the
   ``MediumText`` column path past the plugin's content-equality
   check.

Tests are designed so a column-type change in the parent entities
(e.g. flipping ``json_metadata`` from ``MediumText`` to ``JSON``)
will fail one of these if the plugin's Python ``!=`` comparison
breaks for the new type.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 18:42:06 -06:00

218 lines
8.6 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.
"""FR-026 — ``SkipUnmodifiedPlugin`` integration tests.
Locks in the behavior that owners-only saves and content-equivalent
re-saves do *not* mint version rows. Exercises the plugin's
``_matches_previous_version`` comparator across the Dashboard's three
column-type families (String, Text, MediumText) so a future column-type
change can't silently regress to "always create version rows".
"""
from __future__ import annotations
from typing import Any
import pytest
from sqlalchemy_continuum import version_class
from superset.extensions import db
from superset.models.dashboard import Dashboard
from superset.models.slice import Slice
from superset.utils import json as _json
from tests.integration_tests.base_tests import SupersetTestCase
from tests.integration_tests.constants import ADMIN_USERNAME
from tests.integration_tests.fixtures.birth_names_dashboard import ( # noqa: F401
load_birth_names_dashboard_with_slices,
load_birth_names_data,
)
def _dashboard_version_count(dashboard_id: int) -> int:
ver_cls = version_class(Dashboard)
return db.session.query(ver_cls).filter(ver_cls.id == dashboard_id).count()
def _slice_version_count(slice_id: int) -> int:
ver_cls = version_class(Slice)
return db.session.query(ver_cls).filter(ver_cls.id == slice_id).count()
class TestSkipUnmodifiedPlugin(SupersetTestCase):
"""FR-026 — version rows are not minted for content-equivalent updates."""
@pytest.fixture(autouse=True)
def _load_data(self, load_birth_names_dashboard_with_slices): # noqa: PT004, F811
pass
def _get_dashboard(self) -> Dashboard:
db.session.commit()
dash = (
db.session.query(Dashboard)
.filter(Dashboard.dashboard_title == "USA Births Names")
.first()
)
assert dash is not None
return dash
def _put(self, pk: int, body: dict[str, Any]) -> None:
rv = self.client.put(f"/api/v1/dashboard/{pk}", json=body)
assert rv.status_code == 200, rv.data
def test_owners_only_edit_does_not_create_version(self) -> None:
"""Saving a dashboard with only owner changes is a no-op for
version-row creation."""
dash = self._get_dashboard()
dash_id = dash.id
title = dash.dashboard_title
original_owner_ids = [o.id for o in dash.owners]
self.login(ADMIN_USERNAME)
# Force a known baseline state with one save.
self._put(dash_id, {"dashboard_title": title})
db.session.expire_all()
before = _dashboard_version_count(dash_id)
try:
# Now save with only ``owners`` changed (toggle: drop one,
# then put it back). String / Text / MediumText columns are
# unchanged so the plugin should skip both saves.
new_owners = [oid for oid in original_owner_ids if oid != 1] or []
self._put(dash_id, {"dashboard_title": title, "owners": new_owners})
db.session.expire_all()
mid = _dashboard_version_count(dash_id)
assert mid == before, (
f"owners-only edit minted a version row (before={before}, after={mid})"
)
self._put(dash_id, {"dashboard_title": title, "owners": original_owner_ids})
db.session.expire_all()
after = _dashboard_version_count(dash_id)
assert after == before, (
f"second owners-only edit minted a version row "
f"(before={before}, after={after})"
)
finally:
# Always restore original ownership.
self._put(dash_id, {"dashboard_title": title, "owners": original_owner_ids})
def test_re_save_with_identical_values_does_not_create_version(self) -> None:
"""Submitting the same scalar values back through PUT is a no-op
for version creation — exercises the json_metadata re-serialize
case (``set_dash_metadata`` rewrites the column with a different
byte sequence; plugin must compare against the prior shadow row
and skip)."""
dash = self._get_dashboard()
dash_id = dash.id
title = dash.dashboard_title
existing_metadata = dash.json_metadata or "{}"
self.login(ADMIN_USERNAME)
# Prime: one real save to ensure the json_metadata is in canonical
# post-set_dash_metadata form.
self._put(
dash_id,
{"dashboard_title": title, "json_metadata": existing_metadata},
)
db.session.expire_all()
before = _dashboard_version_count(dash_id)
# Re-submit identical content. set_dash_metadata will round-trip
# the json — the resulting byte sequence might differ from the
# request body but must equal the previous stored value.
self._put(
dash_id,
{"dashboard_title": title, "json_metadata": existing_metadata},
)
db.session.expire_all()
after = _dashboard_version_count(dash_id)
assert after == before, (
f"identical re-save minted a version row (before={before}, after={after})"
)
def test_actual_change_creates_version(self) -> None:
"""A real scalar change MUST mint a version row — the plugin
only suppresses no-ops, never legitimate edits."""
dash = self._get_dashboard()
dash_id = dash.id
original_title = dash.dashboard_title
self.login(ADMIN_USERNAME)
before = _dashboard_version_count(dash_id)
try:
self._put(dash_id, {"dashboard_title": "fr-026-modified-title"})
db.session.expire_all()
after = _dashboard_version_count(dash_id)
assert after == before + 1, (
f"real edit failed to mint a version row "
f"(before={before}, after={after})"
)
finally:
self._put(dash_id, {"dashboard_title": original_title})
def test_chart_slice_name_change_creates_version(self) -> None:
"""Same assertion for ``Slice`` (covers the ``String`` column path
on a different entity type)."""
db.session.commit()
chart = db.session.query(Slice).filter(Slice.slice_name == "Girls").first()
assert chart is not None
chart_id = chart.id
self.login(ADMIN_USERNAME)
before = _slice_version_count(chart_id)
try:
rv = self.client.put(
f"/api/v1/chart/{chart_id}",
json={"slice_name": "fr-026-renamed"},
)
assert rv.status_code == 200
db.session.expire_all()
after = _slice_version_count(chart_id)
assert after == before + 1
finally:
self.client.put(f"/api/v1/chart/{chart_id}", json={"slice_name": "Girls"})
def test_dashboard_json_metadata_subkey_change_creates_version(self) -> None:
"""Editing a non-audit key inside ``json_metadata`` MUST mint a
version row — exercises the MediumText column path past the
plugin's content-equality check."""
dash = self._get_dashboard()
dash_id = dash.id
title = dash.dashboard_title
original_metadata = dash.json_metadata or "{}"
self.login(ADMIN_USERNAME)
before = _dashboard_version_count(dash_id)
try:
md = _json.loads(original_metadata)
md["color_scheme"] = "fr026TestPalette"
self._put(
dash_id,
{"dashboard_title": title, "json_metadata": _json.dumps(md)},
)
db.session.expire_all()
after = _dashboard_version_count(dash_id)
assert after == before + 1, (
f"json_metadata edit failed to mint a version row "
f"(before={before}, after={after})"
)
finally:
self._put(
dash_id,
{"dashboard_title": title, "json_metadata": original_metadata},
)