Files
nvi/tests/test_pick_shape.py

123 lines
4.2 KiB
Python

"""Pick / Filter / OrderBy validation — round-trips and rejection cases."""
import pytest
from api.composer.types import Filter, OrderBy, Pick, PickValidationError
# ── Filter ──────────────────────────────────────────────────
def test_filter_equals_roundtrip():
f = Filter.from_dict({"column": "status", "equals": "B"})
assert f.kind() == "equals"
assert f.to_dict() == {"column": "status", "equals": "B"}
def test_filter_in_values_roundtrip():
f = Filter.from_dict({"column": "status", "in_values": ["A", "B"]})
assert f.kind() == "in_values"
assert f.to_dict() == {"column": "status", "in_values": ["A", "B"]}
def test_filter_between_normalised_to_tuple():
f = Filter.from_dict({"column": "amount", "between": [1000, 5000]})
assert f.kind() == "between"
assert f.between == (1000, 5000)
# round-trip emits a list, not a tuple
assert f.to_dict() == {"column": "amount", "between": [1000, 5000]}
def test_filter_date_range_roundtrip():
f = Filter.from_dict({"column": "date", "date_range": "1996"})
assert f.kind() == "date_range"
assert f.to_dict() == {"column": "date", "date_range": "1996"}
def test_filter_missing_value_raises():
with pytest.raises(PickValidationError, match="no value field"):
Filter(column="x").kind()
def test_filter_multiple_value_fields_raises():
f = Filter(column="x", equals="A", in_values=["A", "B"])
with pytest.raises(PickValidationError, match="multiple value fields"):
f.kind()
def test_filter_missing_column_raises():
with pytest.raises(PickValidationError, match="missing 'column'"):
Filter.from_dict({"equals": "B"})
def test_filter_between_wrong_arity_raises():
with pytest.raises(PickValidationError, match="2-element"):
Filter.from_dict({"column": "x", "between": [1, 2, 3]})
# ── OrderBy ─────────────────────────────────────────────────
def test_order_by_metric():
ob = OrderBy.from_dict({"by": "metric", "direction": "desc"})
assert ob.by == "metric"
assert ob.direction == "desc"
assert ob.to_dict() == {"by": "metric", "direction": "desc"}
def test_order_by_dimension_requires_dimension():
with pytest.raises(PickValidationError, match="requires order_by.dimension"):
OrderBy.from_dict({"by": "dimension"})
def test_order_by_invalid_direction_raises():
with pytest.raises(PickValidationError, match="direction"):
OrderBy.from_dict({"by": "metric", "direction": "sideways"})
# ── Pick ────────────────────────────────────────────────────
def test_pick_minimal():
p = Pick.from_dict({"kind": "aggregate", "metric": "loan_volume"})
assert p.metric == "loan_volume"
assert p.group_by == []
assert p.where == []
assert p.order_by is None
assert p.limit is None
def test_pick_full_roundtrip():
raw = {
"kind": "aggregate",
"metric": "loan_default_rate",
"group_by": ["A2"],
"where": [{"column": "date", "date_range": "1996"}],
"order_by": {"by": "metric", "direction": "desc"},
"limit": 10,
}
p = Pick.from_dict(raw)
assert p.to_dict() == raw
def test_pick_rejects_non_aggregate_kind():
with pytest.raises(PickValidationError, match="aggregate"):
Pick.from_dict({"kind": "ratio", "metric": "x"})
def test_pick_missing_metric_raises():
with pytest.raises(PickValidationError, match="metric"):
Pick.from_dict({"kind": "aggregate"})
def test_pick_group_by_must_be_list_of_strings():
with pytest.raises(PickValidationError, match="group_by"):
Pick.from_dict({"kind": "aggregate", "metric": "x", "group_by": [1, 2]})
def test_pick_where_must_be_list():
with pytest.raises(PickValidationError, match="where must be a list"):
Pick.from_dict({"kind": "aggregate", "metric": "x", "where": {"foo": "bar"}})
def test_pick_limit_must_be_int():
with pytest.raises(PickValidationError, match="limit"):
Pick.from_dict({"kind": "aggregate", "metric": "x", "limit": "ten"})