Compare commits

...

3 Commits

Author SHA1 Message Date
Evan
1de6aa1c69 test(charts): add boundary assertions and window metadata bounds
Add inclusive boundary assertions for periods (0, 10000) and window
(1, 10000) to guard against off-by-one / inclusiveness regressions, and
add min/max metadata to the window field so the OpenAPI spec reflects the
actual Range validator (parity with periods).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 14:19:11 -07:00
Evan
67953e2441 fix(charts): add missing max:10000 to periods field metadata
The `periods` field already enforces Range(max=10000) at validation time,
but the OpenAPI metadata only documented the lower bound. Aligns the
`max` key in metadata with the actual validator.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-01 18:20:38 -07:00
Claude Code
162d3753e7 fix(charts): tighten chart schema input validation
Three small validation gaps in superset/charts/schemas.py:

- ChartPutSchema.query_context lacked the validate=utils.validate_json validator
  that ChartPostSchema.query_context already has, allowing invalid JSON to be
  stored on chart update and fail later at render time. Added it (allow_none and
  empty values remain valid, matching POST).
- ChartDataProphetOptionsSchema.periods documented "min: 0" but enforced no
  bound. Added Range(min=0, max=10000) to prevent unbounded forecast horizons.
- ChartDataRollingOptionsSchema.window had no bound. Added Range(min=1,
  max=10000), consistent with the downstream window > 0 requirement.

These bound user-supplied values that flow into Prophet/rolling computations,
reducing a resource-exhaustion surface. Added unit tests for each.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 16:51:47 -07:00
2 changed files with 94 additions and 2 deletions

View File

@@ -274,7 +274,9 @@ class ChartPutSchema(Schema):
validate=utils.validate_json,
)
query_context = fields.String(
metadata={"description": query_context_description}, allow_none=True
metadata={"description": query_context_description},
allow_none=True,
validate=utils.validate_json,
)
query_context_generation = fields.Boolean(
metadata={"description": query_context_generation_description}, allow_none=True
@@ -516,8 +518,20 @@ class ChartDataRollingOptionsSchema(ChartDataPostProcessingOperationOptionsSchem
required=True,
)
window = fields.Integer(
metadata={"description": "Size of the rolling window in days.", "example": 7},
metadata={
"description": "Size of the rolling window in days.",
"example": 7,
"min": 1,
"max": 10000,
},
required=True,
validate=[
Range(
min=1,
max=10000,
error=_("`window` must be between 1 and 10000"),
)
],
)
rolling_type_options = fields.Dict(
metadata={
@@ -657,8 +671,16 @@ class ChartDataProphetOptionsSchema(ChartDataPostProcessingOperationOptionsSchem
"the future",
"example": 7,
"min": 0,
"max": 10000,
},
required=True,
validate=[
Range(
min=0,
max=10000,
error=_("`periods` must be between 0 and 10000"),
)
],
)
confidence_interval = fields.Float(
metadata={

View File

@@ -22,6 +22,8 @@ from marshmallow import ValidationError
from superset.charts.schemas import (
ChartDataProphetOptionsSchema,
ChartDataQueryObjectSchema,
ChartDataRollingOptionsSchema,
ChartPutSchema,
get_time_grain_choices,
)
@@ -91,6 +93,74 @@ def test_chart_data_prophet_options_schema_time_grain_validation(
assert "time_grain" in exc_info.value.messages
def test_chart_put_schema_query_context_json_validation(
app_context: None,
) -> None:
"""ChartPutSchema.query_context must reject invalid JSON (parity with POST)."""
schema = ChartPutSchema()
# Valid JSON passes
assert schema.load({"query_context": '{"a": 1}'})["query_context"] == '{"a": 1}'
# None is allowed (allow_none)
assert schema.load({"query_context": None})["query_context"] is None
# Invalid JSON is rejected
with pytest.raises(ValidationError) as exc_info:
schema.load({"query_context": "{not valid json"})
assert "query_context" in exc_info.value.messages
def test_chart_data_prophet_options_schema_periods_range(
app_context: None,
) -> None:
"""`periods` must be a bounded non-negative integer."""
schema = ChartDataProphetOptionsSchema()
base = {"time_grain": "P1D", "confidence_interval": 0.8}
# Valid value passes
assert schema.load({**base, "periods": 7})["periods"] == 7
# Inclusive boundaries are accepted
assert schema.load({**base, "periods": 0})["periods"] == 0
assert schema.load({**base, "periods": 10000})["periods"] == 10000
# Negative value rejected
with pytest.raises(ValidationError) as exc_info:
schema.load({**base, "periods": -1})
assert "periods" in exc_info.value.messages
# Excessively large value rejected (resource-exhaustion guard)
with pytest.raises(ValidationError) as exc_info:
schema.load({**base, "periods": 1_000_000})
assert "periods" in exc_info.value.messages
def test_chart_data_rolling_options_schema_window_range(
app_context: None,
) -> None:
"""`window` must be a bounded positive integer."""
schema = ChartDataRollingOptionsSchema()
base = {"rolling_type": "mean"}
# Valid value passes
assert schema.load({**base, "window": 7})["window"] == 7
# Inclusive boundaries are accepted
assert schema.load({**base, "window": 1})["window"] == 1
assert schema.load({**base, "window": 10000})["window"] == 10000
# Zero window rejected (rolling requires window > 0)
with pytest.raises(ValidationError) as exc_info:
schema.load({**base, "window": 0})
assert "window" in exc_info.value.messages
# Excessively large window rejected
with pytest.raises(ValidationError) as exc_info:
schema.load({**base, "window": 1_000_000})
assert "window" in exc_info.value.messages
def test_chart_data_query_object_schema_time_grain_sqla_validation(
app_context: None,
) -> None: