Files
superset2/tests/unit_tests/reports/schemas_test.py
2026-04-02 11:55:24 -07:00

259 lines
8.4 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.
import pytest
from marshmallow import ValidationError
from pytest_mock import MockerFixture
from superset.reports.schemas import ReportSchedulePostSchema, ReportSchedulePutSchema
def test_report_post_schema_custom_width_validation(mocker: MockerFixture) -> None:
"""
Test the custom width validation.
"""
mocker.patch(
"flask.current_app.config",
{
"ALERT_REPORTS_MIN_CUSTOM_SCREENSHOT_WIDTH": 100,
"ALERT_REPORTS_MAX_CUSTOM_SCREENSHOT_WIDTH": 200,
},
)
schema = ReportSchedulePostSchema()
schema.load(
{
"type": "Report",
"name": "A report",
"description": "My report",
"active": True,
"crontab": "* * * * *",
"timezone": "America/Los_Angeles",
"custom_width": 100,
}
)
# not required
schema.load(
{
"type": "Report",
"name": "A report",
"description": "My report",
"active": True,
"crontab": "* * * * *",
"timezone": "America/Los_Angeles",
}
)
with pytest.raises(ValidationError) as excinfo:
schema.load(
{
"type": "Report",
"name": "A report",
"description": "My report",
"active": True,
"crontab": "* * * * *",
"timezone": "America/Los_Angeles",
"custom_width": 1000,
}
)
assert excinfo.value.messages == {
"custom_width": ["Screenshot width must be between 100px and 200px"]
}
MINIMAL_POST_PAYLOAD = {
"type": "Report",
"name": "A report",
"crontab": "* * * * *",
"timezone": "America/Los_Angeles",
}
CUSTOM_WIDTH_CONFIG = {
"ALERT_REPORTS_MIN_CUSTOM_SCREENSHOT_WIDTH": 600,
"ALERT_REPORTS_MAX_CUSTOM_SCREENSHOT_WIDTH": 2400,
}
@pytest.mark.parametrize(
"schema_class,payload_base",
[
(ReportSchedulePostSchema, MINIMAL_POST_PAYLOAD),
(ReportSchedulePutSchema, {}),
],
ids=["post", "put"],
)
@pytest.mark.parametrize(
"width,should_pass",
[
(599, False),
(600, True),
(2400, True),
(2401, False),
(None, True),
],
)
def test_custom_width_boundary_values(
mocker: MockerFixture,
schema_class: type,
payload_base: dict[str, object],
width: int | None,
should_pass: bool,
) -> None:
mocker.patch("flask.current_app.config", CUSTOM_WIDTH_CONFIG)
schema = schema_class()
payload = {**payload_base, "custom_width": width}
if should_pass:
schema.load(payload)
else:
with pytest.raises(ValidationError) as exc:
schema.load(payload)
assert "custom_width" in exc.value.messages
def test_working_timeout_validation(mocker: MockerFixture) -> None:
mocker.patch("flask.current_app.config", CUSTOM_WIDTH_CONFIG)
post_schema = ReportSchedulePostSchema()
put_schema = ReportSchedulePutSchema()
# POST: working_timeout=0 and -1 are invalid (min=1)
with pytest.raises(ValidationError) as exc:
post_schema.load({**MINIMAL_POST_PAYLOAD, "working_timeout": 0})
assert "working_timeout" in exc.value.messages
with pytest.raises(ValidationError) as exc:
post_schema.load({**MINIMAL_POST_PAYLOAD, "working_timeout": -1})
assert "working_timeout" in exc.value.messages
# POST: working_timeout=1 is valid
post_schema.load({**MINIMAL_POST_PAYLOAD, "working_timeout": 1})
# PUT: working_timeout=None is valid (allow_none=True)
put_schema.load({"working_timeout": None})
def test_log_retention_post_vs_put_parity(mocker: MockerFixture) -> None:
mocker.patch("flask.current_app.config", CUSTOM_WIDTH_CONFIG)
post_schema = ReportSchedulePostSchema()
put_schema = ReportSchedulePutSchema()
# POST: log_retention=0 is invalid (min=1)
with pytest.raises(ValidationError) as exc:
post_schema.load({**MINIMAL_POST_PAYLOAD, "log_retention": 0})
assert "log_retention" in exc.value.messages
# POST: log_retention=1 is valid
post_schema.load({**MINIMAL_POST_PAYLOAD, "log_retention": 1})
# PUT: log_retention=0 is valid (min=0)
put_schema.load({"log_retention": 0})
def test_report_type_disallows_database(mocker: MockerFixture) -> None:
mocker.patch("flask.current_app.config", CUSTOM_WIDTH_CONFIG)
schema = ReportSchedulePostSchema()
with pytest.raises(ValidationError) as exc:
schema.load({**MINIMAL_POST_PAYLOAD, "database": 1})
assert "database" in exc.value.messages
def test_alert_type_allows_database(mocker: MockerFixture) -> None:
"""Alert type should accept database; only Report type blocks it."""
mocker.patch("flask.current_app.config", CUSTOM_WIDTH_CONFIG)
schema = ReportSchedulePostSchema()
result = schema.load({**MINIMAL_POST_PAYLOAD, "type": "Alert", "database": 1})
assert result["database"] == 1
# ---------------------------------------------------------------------------
# Phase 1b gap closure: crontab validator, name length, PUT parity
# ---------------------------------------------------------------------------
@pytest.mark.parametrize(
"crontab,should_pass",
[
("* * * * *", True),
("0 0 * * 0", True),
("*/5 * * * *", True),
("not a cron", False),
("* * * *", False), # too few fields
("", False),
],
ids=["every-min", "weekly", "every-5", "invalid-text", "too-few-fields", "empty"],
)
def test_crontab_validation(
mocker: MockerFixture,
crontab: str,
should_pass: bool,
) -> None:
mocker.patch("flask.current_app.config", CUSTOM_WIDTH_CONFIG)
schema = ReportSchedulePostSchema()
payload = {**MINIMAL_POST_PAYLOAD, "crontab": crontab}
if should_pass:
result = schema.load(payload)
assert result["crontab"] == crontab
else:
with pytest.raises(ValidationError) as exc:
schema.load(payload)
assert "crontab" in exc.value.messages
def test_name_empty_rejected(mocker: MockerFixture) -> None:
mocker.patch("flask.current_app.config", CUSTOM_WIDTH_CONFIG)
schema = ReportSchedulePostSchema()
with pytest.raises(ValidationError) as exc:
schema.load({**MINIMAL_POST_PAYLOAD, "name": ""})
assert "name" in exc.value.messages
def test_name_at_max_length_accepted(mocker: MockerFixture) -> None:
mocker.patch("flask.current_app.config", CUSTOM_WIDTH_CONFIG)
schema = ReportSchedulePostSchema()
long_name = "x" * 150
result = schema.load({**MINIMAL_POST_PAYLOAD, "name": long_name})
assert result["name"] == long_name
def test_name_over_max_length_rejected(mocker: MockerFixture) -> None:
mocker.patch("flask.current_app.config", CUSTOM_WIDTH_CONFIG)
schema = ReportSchedulePostSchema()
with pytest.raises(ValidationError) as exc:
schema.load({**MINIMAL_POST_PAYLOAD, "name": "x" * 151})
assert "name" in exc.value.messages
def test_put_schema_allows_database_on_report_type(mocker: MockerFixture) -> None:
"""PUT schema lacks validate_report_references — database on Report type is
accepted (documents current behavior; POST schema correctly rejects this)."""
mocker.patch("flask.current_app.config", CUSTOM_WIDTH_CONFIG)
put_schema = ReportSchedulePutSchema()
result = put_schema.load({"type": "Report", "database": 1})
assert result["database"] == 1
# POST schema rejects it (verify the asymmetry)
post_schema = ReportSchedulePostSchema()
with pytest.raises(ValidationError) as exc:
post_schema.load({**MINIMAL_POST_PAYLOAD, "database": 1})
assert "database" in exc.value.messages