"""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"})