Files
superset2/tests/unit_tests/models/owner_association_tables_test.py

107 lines
4.2 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.
"""Uniqueness contract for subject association tables.
The subject relationship tables must reject duplicate
``(subject_id, <resource>_id)`` rows. Duplicate relationship rows break
removal through the DAO relationship diff, and the subject-era tables enforce
the contract with a two-column unique constraint.
Unlike ``tests/unit_tests/migrations/composite_pk_association_tables_test.py``,
which rebuilds the post-migration shape synthetically, these tests assert
against the *live ORM* ``Table`` objects, so a regression in the model
definitions themselves is caught directly.
"""
import pytest
import sqlalchemy as sa
from sqlalchemy.exc import IntegrityError
from superset.subjects.models import (
chart_editors,
chart_viewers,
dashboard_editors,
dashboard_viewers,
report_schedule_editors,
sqlatable_editors,
)
# (association table, resource FK column name)
SUBJECT_TABLES: list[tuple[sa.Table, str]] = [
(dashboard_editors, "dashboard_id"),
(dashboard_viewers, "dashboard_id"),
(chart_editors, "chart_id"),
(chart_viewers, "chart_id"),
(sqlatable_editors, "table_id"),
(report_schedule_editors, "report_schedule_id"),
]
_TABLE_IDS = [table.name for table, _ in SUBJECT_TABLES]
def _create_standalone(table: sa.Table) -> sa.engine.Engine:
"""Create just the association table on in-memory SQLite.
SQLite does not enforce foreign keys unless explicitly enabled, so the
real ``Table`` object can be created without its parent tables while its
primary-key constraint remains fully enforced.
"""
engine = sa.create_engine("sqlite:///:memory:")
table.create(engine)
return engine
@pytest.mark.parametrize("table,resource_col", SUBJECT_TABLES, ids=_TABLE_IDS)
def test_subject_table_declares_pair_uniqueness(
table: sa.Table, resource_col: str
) -> None:
"""The subject/resource pair uniqueness is declared in model metadata."""
unique_constraints = [
{column.name for column in constraint.columns}
for constraint in table.constraints
if isinstance(constraint, sa.UniqueConstraint)
]
assert {"subject_id", resource_col} in unique_constraints
@pytest.mark.parametrize("table,resource_col", SUBJECT_TABLES, ids=_TABLE_IDS)
def test_subject_table_rejects_duplicate_row(
table: sa.Table, resource_col: str
) -> None:
"""Inserting the same (subject, resource) pair twice fails."""
engine = _create_standalone(table)
with engine.begin() as conn:
conn.execute(table.insert().values(subject_id=1, **{resource_col: 1}))
with pytest.raises(IntegrityError):
conn.execute(table.insert().values(subject_id=1, **{resource_col: 1}))
@pytest.mark.parametrize("table,resource_col", SUBJECT_TABLES, ids=_TABLE_IDS)
def test_subject_table_accepts_distinct_rows(
table: sa.Table, resource_col: str
) -> None:
"""Distinct pairs still insert fine — uniqueness is over the pair, not a
single column: the same subject may edit/view several resources and a
resource may have several subjects."""
engine = _create_standalone(table)
with engine.begin() as conn:
conn.execute(table.insert().values(subject_id=1, **{resource_col: 1}))
conn.execute(table.insert().values(subject_id=1, **{resource_col: 2}))
conn.execute(table.insert().values(subject_id=2, **{resource_col: 1}))
count = conn.execute(sa.select(sa.func.count()).select_from(table)).scalar_one()
assert count == 3