This is an automated email from the ASF dual-hosted git repository.
jli pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/superset.git
The following commit(s) were added to refs/heads/master by this push:
new 5eb35a4795 fix(reports): validate database field on PUT report
schedule (#38084)
5eb35a4795 is described below
commit 5eb35a47957d95abc13d6391b58c8cc6df3e518c
Author: Joe Li <[email protected]>
AuthorDate: Tue Feb 24 16:58:19 2026 -0800
fix(reports): validate database field on PUT report schedule (#38084)
Co-authored-by: Claude Opus 4.6 <[email protected]>
---
superset/commands/report/exceptions.py | 12 +
superset/commands/report/update.py | 18 +-
superset/reports/schemas.py | 2 +-
tests/integration_tests/reports/api_tests.py | 294 +++++++++++++++++++++++-
tests/unit_tests/commands/report/__init__.py | 16 ++
tests/unit_tests/commands/report/update_test.py | 254 ++++++++++++++++++++
6 files changed, 592 insertions(+), 4 deletions(-)
diff --git a/superset/commands/report/exceptions.py
b/superset/commands/report/exceptions.py
index 27966e6f09..b51c2d1cd1 100644
--- a/superset/commands/report/exceptions.py
+++ b/superset/commands/report/exceptions.py
@@ -39,6 +39,18 @@ class DatabaseNotFoundValidationError(ValidationError):
super().__init__(_("Database does not exist"), field_name="database")
+class ReportScheduleDatabaseNotAllowedValidationError(ValidationError):
+ """
+ Marshmallow validation error for database reference on a Report type
schedule
+ """
+
+ def __init__(self) -> None:
+ super().__init__(
+ _("Database reference is not allowed on a report"),
+ field_name="database",
+ )
+
+
class DashboardNotFoundValidationError(ValidationError):
"""
Marshmallow validation error for dashboard does not exist
diff --git a/superset/commands/report/update.py
b/superset/commands/report/update.py
index abae62cadd..1ee09ea7a7 100644
--- a/superset/commands/report/update.py
+++ b/superset/commands/report/update.py
@@ -26,6 +26,8 @@ from superset.commands.base import UpdateMixin
from superset.commands.report.base import BaseReportScheduleCommand
from superset.commands.report.exceptions import (
DatabaseNotFoundValidationError,
+ ReportScheduleAlertRequiredDatabaseValidationError,
+ ReportScheduleDatabaseNotAllowedValidationError,
ReportScheduleForbiddenError,
ReportScheduleInvalidError,
ReportScheduleNameUniquenessValidationError,
@@ -98,8 +100,22 @@ class UpdateReportScheduleCommand(UpdateMixin,
BaseReportScheduleCommand):
)
)
+ # Determine effective database state (payload overrides model)
+ if "database" in self._properties:
+ has_database = self._properties["database"] is not None
+ else:
+ has_database = self._model.database_id is not None
+
+ # Validate database is not allowed on Report type
+ if report_type == ReportScheduleType.REPORT and has_database:
+
exceptions.append(ReportScheduleDatabaseNotAllowedValidationError())
+
+ # Validate Alert has a database
+ if report_type == ReportScheduleType.ALERT and not has_database:
+
exceptions.append(ReportScheduleAlertRequiredDatabaseValidationError())
+
# Validate if DB exists (for alerts)
- if report_type == ReportScheduleType.ALERT and database_id:
+ if report_type == ReportScheduleType.ALERT and database_id is not None:
if not (database := DatabaseDAO.find_by_id(database_id)):
exceptions.append(DatabaseNotFoundValidationError())
self._properties["database"] = database
diff --git a/superset/reports/schemas.py b/superset/reports/schemas.py
index 76108f976f..055ebc75d2 100644
--- a/superset/reports/schemas.py
+++ b/superset/reports/schemas.py
@@ -334,7 +334,7 @@ class ReportSchedulePutSchema(Schema):
metadata={"description": creation_method_description},
)
dashboard = fields.Integer(required=False, allow_none=True)
- database = fields.Integer(required=False)
+ database = fields.Integer(required=False, allow_none=True)
owners = fields.List(
fields.Integer(metadata={"description": owners_description}),
required=False
)
diff --git a/tests/integration_tests/reports/api_tests.py
b/tests/integration_tests/reports/api_tests.py
index 41edf71702..87fb8ccf04 100644
--- a/tests/integration_tests/reports/api_tests.py
+++ b/tests/integration_tests/reports/api_tests.py
@@ -18,6 +18,7 @@
"""Unit tests for Superset"""
from datetime import datetime, timedelta
+from typing import Any
from unittest.mock import patch
import pytz
@@ -1392,7 +1393,7 @@ class TestReportSchedulesApi(SupersetTestCase):
)
assert report_schedule.type == ReportScheduleType.ALERT
previous_cron = report_schedule.crontab
- update_payload = {
+ update_payload: dict[str, Any] = {
"crontab": "5,10 * * * *",
}
with patch.dict(
@@ -1410,6 +1411,7 @@ class TestReportSchedulesApi(SupersetTestCase):
# Test report minimum interval
update_payload["crontab"] = "5,8 * * * *"
update_payload["type"] = ReportScheduleType.REPORT
+ update_payload["database"] = None
uri = f"api/v1/report/{report_schedule.id}"
rv = self.put_assert_metric(uri, update_payload, "put")
assert rv.status_code == 200
@@ -1424,6 +1426,7 @@ class TestReportSchedulesApi(SupersetTestCase):
# Undo changes
update_payload["crontab"] = previous_cron
update_payload["type"] = ReportScheduleType.ALERT
+ update_payload["database"] = get_example_database().id
uri = f"api/v1/report/{report_schedule.id}"
rv = self.put_assert_metric(uri, update_payload, "put")
assert rv.status_code == 200
@@ -1441,7 +1444,7 @@ class TestReportSchedulesApi(SupersetTestCase):
.one_or_none()
)
assert report_schedule.type == ReportScheduleType.ALERT
- update_payload = {
+ update_payload: dict[str, Any] = {
"crontab": "5,10 * * * *",
}
with patch.dict(
@@ -1468,6 +1471,7 @@ class TestReportSchedulesApi(SupersetTestCase):
# Exceed report minimum interval
update_payload["crontab"] = "5,8 * * * *"
update_payload["type"] = ReportScheduleType.REPORT
+ update_payload["database"] = None
uri = f"api/v1/report/{report_schedule.id}"
rv = self.put_assert_metric(uri, update_payload, "put")
assert rv.status_code == 422
@@ -1607,6 +1611,292 @@ class TestReportSchedulesApi(SupersetTestCase):
data = json.loads(rv.data.decode("utf-8"))
assert data == {"message": {"chart": "Choose a chart or dashboard not
both"}}
+ @pytest.mark.usefixtures("create_report_schedules")
+ def test_update_report_schedule_database_not_allowed_on_report(self):
+ """
+ ReportSchedule API: Test update report schedule rejects database on
Report type
+ """
+ self.login(ADMIN_USERNAME)
+ example_db = get_example_database()
+
+ # Create a Report-type schedule (name1 is an Alert, so create one)
+ report_schedule = (
+ db.session.query(ReportSchedule)
+ .filter(ReportSchedule.name == "name1")
+ .one_or_none()
+ )
+ # Change to Report type first (clearing database)
+ uri = f"api/v1/report/{report_schedule.id}"
+ rv = self.put_assert_metric(
+ uri,
+ {"type": ReportScheduleType.REPORT, "database": None},
+ "put",
+ )
+ assert rv.status_code == 200
+
+ # Test 1: Report + database (no type in payload) → 422
+ rv = self.put_assert_metric(uri, {"database": example_db.id}, "put")
+ assert rv.status_code == 422
+ data = json.loads(rv.data.decode("utf-8"))
+ assert data == {
+ "message": {"database": "Database reference is not allowed on a
report"}
+ }
+
+ # Test 2: Report + database + explicit type=Report → 422
+ rv = self.put_assert_metric(
+ uri,
+ {"type": ReportScheduleType.REPORT, "database": example_db.id},
+ "put",
+ )
+ assert rv.status_code == 422
+ data = json.loads(rv.data.decode("utf-8"))
+ assert data == {
+ "message": {"database": "Database reference is not allowed on a
report"}
+ }
+
+ @pytest.mark.usefixtures("create_report_schedules")
+ def
test_update_report_schedule_nonexistent_database_returns_not_allowed(self):
+ """
+ ReportSchedule API: Test Report + nonexistent DB returns 'not allowed',
+ not 'does not exist' — type invariant takes precedence.
+ """
+ self.login(ADMIN_USERNAME)
+
+ report_schedule = (
+ db.session.query(ReportSchedule)
+ .filter(ReportSchedule.name == "name1")
+ .one_or_none()
+ )
+ uri = f"api/v1/report/{report_schedule.id}"
+
+ # Transition to Report type first
+ rv = self.put_assert_metric(
+ uri,
+ {"type": ReportScheduleType.REPORT, "database": None},
+ "put",
+ )
+ assert rv.status_code == 200
+
+ # Report + nonexistent DB → 422 "not allowed" (not "does not exist")
+ database_max_id = db.session.query(func.max(Database.id)).scalar()
+ rv = self.put_assert_metric(uri, {"database": database_max_id + 1},
"put")
+ assert rv.status_code == 422
+ data = json.loads(rv.data.decode("utf-8"))
+ assert data == {
+ "message": {"database": "Database reference is not allowed on a
report"}
+ }
+
+ @pytest.mark.usefixtures("create_report_schedules")
+ def test_update_alert_schedule_database_allowed(self):
+ """
+ ReportSchedule API: Test update alert schedule accepts database
+ """
+ self.login(ADMIN_USERNAME)
+ example_db = get_example_database()
+
+ report_schedule = (
+ db.session.query(ReportSchedule)
+ .filter(ReportSchedule.name == "name2")
+ .one_or_none()
+ )
+ assert report_schedule.type == ReportScheduleType.ALERT
+
+ # Test 3: Alert + database (no type in payload) → 200
+ uri = f"api/v1/report/{report_schedule.id}"
+ rv = self.put_assert_metric(uri, {"database": example_db.id}, "put")
+ assert rv.status_code == 200
+
+ @pytest.mark.usefixtures("create_report_schedules")
+ def test_update_report_schedule_type_transitions(self):
+ """
+ ReportSchedule API: Test type transitions with database validation
+ """
+ self.login(ADMIN_USERNAME)
+ example_db = get_example_database()
+
+ report_schedule = (
+ db.session.query(ReportSchedule)
+ .filter(ReportSchedule.name == "name3")
+ .one_or_none()
+ )
+ assert report_schedule.type == ReportScheduleType.ALERT
+ assert report_schedule.database_id is not None
+ uri = f"api/v1/report/{report_schedule.id}"
+
+ # Test 4: Alert + database update (same type) → 200
+ rv = self.put_assert_metric(
+ uri,
+ {"database": example_db.id},
+ "put",
+ )
+ assert rv.status_code == 200
+
+ # Test 5: Alert → Report + database → 422
+ rv = self.put_assert_metric(
+ uri,
+ {
+ "type": ReportScheduleType.REPORT,
+ "database": example_db.id,
+ },
+ "put",
+ )
+ assert rv.status_code == 422
+ data = json.loads(rv.data.decode("utf-8"))
+ assert data == {
+ "message": {"database": "Database reference is not allowed on a
report"}
+ }
+
+ # Test 6: Alert → Report without clearing database → 422
+ rv = self.put_assert_metric(uri, {"type": ReportScheduleType.REPORT},
"put")
+ assert rv.status_code == 422
+ data = json.loads(rv.data.decode("utf-8"))
+ assert data == {
+ "message": {"database": "Database reference is not allowed on a
report"}
+ }
+
+ # Test 7: Alert → Report with database: null (explicit clear) → 200
+ rv = self.put_assert_metric(
+ uri,
+ {"type": ReportScheduleType.REPORT, "database": None},
+ "put",
+ )
+ assert rv.status_code == 200
+
+ # Now schedule is a Report with no database.
+ # Test 8: Report → Alert without providing database → 422
+ rv = self.put_assert_metric(
+ uri,
+ {"type": ReportScheduleType.ALERT},
+ "put",
+ )
+ assert rv.status_code == 422
+ data = json.loads(rv.data.decode("utf-8"))
+ assert data == {"message": {"database": "Database is required for
alerts"}}
+
+ # Test 9: Report → Alert with database → 200 (valid transition)
+ rv = self.put_assert_metric(
+ uri,
+ {"type": ReportScheduleType.ALERT, "database": example_db.id},
+ "put",
+ )
+ assert rv.status_code == 200
+
+ @pytest.mark.usefixtures("create_report_schedules")
+ def test_update_alert_schedule_database_null_rejected(self):
+ """
+ ReportSchedule API: Test alert schedule rejects null database
+ """
+ self.login(ADMIN_USERNAME)
+
+ report_schedule = (
+ db.session.query(ReportSchedule)
+ .filter(ReportSchedule.name == "name2")
+ .one_or_none()
+ )
+ assert report_schedule.type == ReportScheduleType.ALERT
+ uri = f"api/v1/report/{report_schedule.id}"
+
+ # Test 8: Alert + database: null → 422
+ rv = self.put_assert_metric(uri, {"database": None}, "put")
+ assert rv.status_code == 422
+ data = json.loads(rv.data.decode("utf-8"))
+ assert data == {"message": {"database": "Database is required for
alerts"}}
+
+ @pytest.mark.usefixtures("create_report_schedules")
+ def test_update_report_schedule_422_does_not_mutate(self):
+ """
+ ReportSchedule API: Test that a rejected PUT does not mutate the model
+ """
+ self.login(ADMIN_USERNAME)
+
+ report_schedule = (
+ db.session.query(ReportSchedule)
+ .filter(ReportSchedule.name == "name2")
+ .one_or_none()
+ )
+ assert report_schedule.type == ReportScheduleType.ALERT
+ original_type = report_schedule.type
+ original_database_id = report_schedule.database_id
+ assert original_database_id is not None
+ uri = f"api/v1/report/{report_schedule.id}"
+
+ # Alert→Report without clearing database → 422
+ rv = self.put_assert_metric(uri, {"type": ReportScheduleType.REPORT},
"put")
+ assert rv.status_code == 422
+
+ # Re-query and verify no mutation
+ db.session.expire(report_schedule)
+ report_schedule = (
+ db.session.query(ReportSchedule)
+ .filter(ReportSchedule.id == report_schedule.id)
+ .one_or_none()
+ )
+ assert report_schedule.type == original_type
+ assert report_schedule.database_id == original_database_id
+
+ @pytest.mark.usefixtures(
+ "load_birth_names_dashboard_with_slices", "create_report_schedules"
+ )
+ def test_create_report_schedule_database_not_allowed(self):
+ """
+ ReportSchedule API: Test POST rejects database on Report type at
schema level
+ """
+ self.login(ADMIN_USERNAME)
+
+ chart = db.session.query(Slice).first()
+ example_db = get_example_database()
+ report_schedule_data = {
+ "type": ReportScheduleType.REPORT,
+ "name": "report_with_db",
+ "description": "should fail",
+ "crontab": "0 9 * * *",
+ "creation_method": ReportCreationMethod.ALERTS_REPORTS,
+ "chart": chart.id,
+ "database": example_db.id,
+ }
+ uri = "api/v1/report/"
+ rv = self.post_assert_metric(uri, report_schedule_data, "post")
+ assert rv.status_code == 400
+ data = json.loads(rv.data.decode("utf-8"))
+ assert "database" in data.get("message", {})
+
+ @pytest.mark.usefixtures("create_report_schedules")
+ def test_update_report_to_alert_nonexistent_database(self):
+ """
+ ReportSchedule API: Test Report→Alert with nonexistent database
returns 422
+ """
+ self.login(ADMIN_USERNAME)
+
+ report_schedule = (
+ db.session.query(ReportSchedule)
+ .filter(ReportSchedule.name == "name4")
+ .one_or_none()
+ )
+ assert report_schedule.type == ReportScheduleType.ALERT
+ uri = f"api/v1/report/{report_schedule.id}"
+
+ # First transition to Report (clearing database)
+ rv = self.put_assert_metric(
+ uri,
+ {"type": ReportScheduleType.REPORT, "database": None},
+ "put",
+ )
+ assert rv.status_code == 200
+
+ # Now transition back to Alert with nonexistent database
+ database_max_id = db.session.query(func.max(Database.id)).scalar()
+ rv = self.put_assert_metric(
+ uri,
+ {
+ "type": ReportScheduleType.ALERT,
+ "database": database_max_id + 1,
+ },
+ "put",
+ )
+ assert rv.status_code == 422
+ data = json.loads(rv.data.decode("utf-8"))
+ assert data == {"message": {"database": "Database does not exist"}}
+
@pytest.mark.usefixtures(
"load_birth_names_dashboard_with_slices", "create_report_schedules"
)
diff --git a/tests/unit_tests/commands/report/__init__.py
b/tests/unit_tests/commands/report/__init__.py
new file mode 100644
index 0000000000..13a83393a9
--- /dev/null
+++ b/tests/unit_tests/commands/report/__init__.py
@@ -0,0 +1,16 @@
+# 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.
diff --git a/tests/unit_tests/commands/report/update_test.py
b/tests/unit_tests/commands/report/update_test.py
new file mode 100644
index 0000000000..6b515781b4
--- /dev/null
+++ b/tests/unit_tests/commands/report/update_test.py
@@ -0,0 +1,254 @@
+# 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 tests for UpdateReportScheduleCommand.validate() database
invariants."""
+
+from __future__ import annotations
+
+from unittest.mock import Mock
+
+import pytest
+from pytest_mock import MockerFixture
+
+from superset.commands.report.exceptions import (
+ ReportScheduleInvalidError,
+)
+from superset.commands.report.update import UpdateReportScheduleCommand
+from superset.reports.models import ReportScheduleType
+
+
+def _make_model(
+ mocker: MockerFixture,
+ *,
+ model_type: ReportScheduleType | str,
+ database_id: int | None,
+) -> Mock:
+ model = mocker.Mock()
+ model.type = model_type
+ model.database_id = database_id
+ model.name = "test_schedule"
+ model.crontab = "0 9 * * *"
+ model.last_state = "noop"
+ model.owners = []
+ return model
+
+
+def _setup_mocks(mocker: MockerFixture, model: Mock) -> None:
+ mocker.patch(
+ "superset.commands.report.update.ReportScheduleDAO.find_by_id",
+ return_value=model,
+ )
+ mocker.patch(
+
"superset.commands.report.update.ReportScheduleDAO.validate_update_uniqueness",
+ return_value=True,
+ )
+ mocker.patch(
+ "superset.commands.report.update.security_manager.raise_for_ownership",
+ )
+ mocker.patch(
+ "superset.commands.report.update.DatabaseDAO.find_by_id",
+ return_value=mocker.Mock(),
+ )
+ mocker.patch.object(
+ UpdateReportScheduleCommand,
+ "validate_chart_dashboard",
+ )
+ mocker.patch.object(
+ UpdateReportScheduleCommand,
+ "validate_report_frequency",
+ )
+ mocker.patch.object(
+ UpdateReportScheduleCommand,
+ "compute_owners",
+ return_value=[],
+ )
+
+
+def _get_validation_messages(
+ exc_info: pytest.ExceptionInfo[ReportScheduleInvalidError],
+) -> dict[str, str]:
+ """Extract field→first message string from ReportScheduleInvalidError."""
+ raw = exc_info.value.normalized_messages()
+ result = {}
+ for field, msgs in raw.items():
+ if isinstance(msgs, list):
+ result[field] = str(msgs[0])
+ else:
+ result[field] = str(msgs)
+ return result
+
+
+# --- Report type: database must NOT be set ---
+
+
+def test_report_with_database_in_payload_rejected(mocker: MockerFixture) ->
None:
+ model = _make_model(mocker, model_type=ReportScheduleType.REPORT,
database_id=None)
+ _setup_mocks(mocker, model)
+
+ cmd = UpdateReportScheduleCommand(model_id=1, data={"database": 5})
+ with pytest.raises(ReportScheduleInvalidError) as exc_info:
+ cmd.validate()
+ messages = _get_validation_messages(exc_info)
+ assert "database" in messages
+ assert "not allowed" in messages["database"].lower()
+
+
+def test_report_with_database_none_in_payload_accepted(mocker: MockerFixture)
-> None:
+ model = _make_model(mocker, model_type=ReportScheduleType.REPORT,
database_id=None)
+ _setup_mocks(mocker, model)
+
+ cmd = UpdateReportScheduleCommand(model_id=1, data={"database": None})
+ cmd.validate() # should not raise
+
+
+def test_report_no_database_in_payload_model_has_db_rejected(
+ mocker: MockerFixture,
+) -> None:
+ model = _make_model(mocker, model_type=ReportScheduleType.REPORT,
database_id=5)
+ _setup_mocks(mocker, model)
+
+ cmd = UpdateReportScheduleCommand(model_id=1, data={})
+ with pytest.raises(ReportScheduleInvalidError) as exc_info:
+ cmd.validate()
+ messages = _get_validation_messages(exc_info)
+ assert "database" in messages
+ assert "not allowed" in messages["database"].lower()
+
+
+def test_report_no_database_anywhere_accepted(mocker: MockerFixture) -> None:
+ model = _make_model(mocker, model_type=ReportScheduleType.REPORT,
database_id=None)
+ _setup_mocks(mocker, model)
+
+ cmd = UpdateReportScheduleCommand(model_id=1, data={})
+ cmd.validate() # should not raise
+
+
+# --- Alert type: database MUST be set ---
+
+
+def test_alert_with_database_in_payload_accepted(mocker: MockerFixture) ->
None:
+ model = _make_model(mocker, model_type=ReportScheduleType.ALERT,
database_id=None)
+ _setup_mocks(mocker, model)
+
+ cmd = UpdateReportScheduleCommand(model_id=1, data={"database": 5})
+ cmd.validate() # should not raise
+
+
+def test_alert_with_database_none_in_payload_rejected(mocker: MockerFixture)
-> None:
+ model = _make_model(mocker, model_type=ReportScheduleType.ALERT,
database_id=5)
+ _setup_mocks(mocker, model)
+
+ cmd = UpdateReportScheduleCommand(model_id=1, data={"database": None})
+ with pytest.raises(ReportScheduleInvalidError) as exc_info:
+ cmd.validate()
+ messages = _get_validation_messages(exc_info)
+ assert "database" in messages
+ assert "required" in messages["database"].lower()
+
+
+def test_alert_no_database_in_payload_model_has_db_accepted(
+ mocker: MockerFixture,
+) -> None:
+ model = _make_model(mocker, model_type=ReportScheduleType.ALERT,
database_id=5)
+ _setup_mocks(mocker, model)
+
+ cmd = UpdateReportScheduleCommand(model_id=1, data={})
+ cmd.validate() # should not raise
+
+
+def test_alert_no_database_anywhere_rejected(mocker: MockerFixture) -> None:
+ model = _make_model(mocker, model_type=ReportScheduleType.ALERT,
database_id=None)
+ _setup_mocks(mocker, model)
+
+ cmd = UpdateReportScheduleCommand(model_id=1, data={})
+ with pytest.raises(ReportScheduleInvalidError) as exc_info:
+ cmd.validate()
+ messages = _get_validation_messages(exc_info)
+ assert "database" in messages
+ assert "required" in messages["database"].lower()
+
+
+# --- Type transitions ---
+
+
+def test_alert_to_report_without_clearing_db_rejected(mocker: MockerFixture)
-> None:
+ model = _make_model(mocker, model_type=ReportScheduleType.ALERT,
database_id=5)
+ _setup_mocks(mocker, model)
+
+ cmd = UpdateReportScheduleCommand(
+ model_id=1, data={"type": ReportScheduleType.REPORT}
+ )
+ with pytest.raises(ReportScheduleInvalidError) as exc_info:
+ cmd.validate()
+ messages = _get_validation_messages(exc_info)
+ assert "database" in messages
+ assert "not allowed" in messages["database"].lower()
+
+
+def test_alert_to_report_with_db_cleared_accepted(mocker: MockerFixture) ->
None:
+ model = _make_model(mocker, model_type=ReportScheduleType.ALERT,
database_id=5)
+ _setup_mocks(mocker, model)
+
+ cmd = UpdateReportScheduleCommand(
+ model_id=1,
+ data={"type": ReportScheduleType.REPORT, "database": None},
+ )
+ cmd.validate() # should not raise
+
+
+def test_report_to_alert_without_db_rejected(mocker: MockerFixture) -> None:
+ model = _make_model(mocker, model_type=ReportScheduleType.REPORT,
database_id=None)
+ _setup_mocks(mocker, model)
+
+ cmd = UpdateReportScheduleCommand(
+ model_id=1, data={"type": ReportScheduleType.ALERT}
+ )
+ with pytest.raises(ReportScheduleInvalidError) as exc_info:
+ cmd.validate()
+ messages = _get_validation_messages(exc_info)
+ assert "database" in messages
+ assert "required" in messages["database"].lower()
+
+
+def test_report_with_nonexistent_database_returns_not_allowed(
+ mocker: MockerFixture,
+) -> None:
+ """Report + nonexistent DB must return 'not allowed', not 'does not
exist'."""
+ model = _make_model(mocker, model_type=ReportScheduleType.REPORT,
database_id=None)
+ _setup_mocks(mocker, model)
+ mocker.patch(
+ "superset.commands.report.update.DatabaseDAO.find_by_id",
+ return_value=None,
+ )
+
+ cmd = UpdateReportScheduleCommand(model_id=1, data={"database": 99999})
+ with pytest.raises(ReportScheduleInvalidError) as exc_info:
+ cmd.validate()
+ messages = _get_validation_messages(exc_info)
+ assert "database" in messages
+ assert "not allowed" in messages["database"].lower()
+ assert "does not exist" not in messages["database"].lower()
+
+
+def test_report_to_alert_with_db_accepted(mocker: MockerFixture) -> None:
+ model = _make_model(mocker, model_type=ReportScheduleType.REPORT,
database_id=None)
+ _setup_mocks(mocker, model)
+
+ cmd = UpdateReportScheduleCommand(
+ model_id=1,
+ data={"type": ReportScheduleType.ALERT, "database": 5},
+ )
+ cmd.validate() # should not raise