feat: normalize filter values to array format for Grist API
The Grist API requires all filter values to be arrays. This change adds automatic normalization of filter values in get_records, wrapping single values in lists before sending to the API. This fixes 400 errors when filtering on Ref columns with single integer IDs. Changes: - Add filters.py module with normalize_filter function - Update get_records to normalize filters before API call - Add Orders table with Ref column to mock Grist server - Add filter validation to mock server (rejects non-array values) - Fix shell script shebangs for portability (#!/usr/bin/env bash)
This commit is contained in:
89
tests/unit/test_filters.py
Normal file
89
tests/unit/test_filters.py
Normal file
@@ -0,0 +1,89 @@
|
||||
"""Unit tests for filter normalization."""
|
||||
|
||||
import pytest
|
||||
|
||||
from grist_mcp.tools.filters import normalize_filter, normalize_filter_value
|
||||
|
||||
|
||||
class TestNormalizeFilterValue:
|
||||
"""Tests for normalize_filter_value function."""
|
||||
|
||||
def test_int_becomes_list(self):
|
||||
assert normalize_filter_value(5) == [5]
|
||||
|
||||
def test_string_becomes_list(self):
|
||||
assert normalize_filter_value("foo") == ["foo"]
|
||||
|
||||
def test_float_becomes_list(self):
|
||||
assert normalize_filter_value(3.14) == [3.14]
|
||||
|
||||
def test_list_unchanged(self):
|
||||
assert normalize_filter_value([1, 2, 3]) == [1, 2, 3]
|
||||
|
||||
def test_empty_list_unchanged(self):
|
||||
assert normalize_filter_value([]) == []
|
||||
|
||||
def test_single_item_list_unchanged(self):
|
||||
assert normalize_filter_value([42]) == [42]
|
||||
|
||||
def test_mixed_type_list_unchanged(self):
|
||||
assert normalize_filter_value([1, "foo", 3.14]) == [1, "foo", 3.14]
|
||||
|
||||
|
||||
class TestNormalizeFilter:
|
||||
"""Tests for normalize_filter function."""
|
||||
|
||||
def test_none_returns_none(self):
|
||||
assert normalize_filter(None) is None
|
||||
|
||||
def test_empty_dict_returns_empty_dict(self):
|
||||
assert normalize_filter({}) == {}
|
||||
|
||||
def test_single_int_value_wrapped(self):
|
||||
result = normalize_filter({"Transaction": 44})
|
||||
assert result == {"Transaction": [44]}
|
||||
|
||||
def test_single_string_value_wrapped(self):
|
||||
result = normalize_filter({"Status": "active"})
|
||||
assert result == {"Status": ["active"]}
|
||||
|
||||
def test_list_value_unchanged(self):
|
||||
result = normalize_filter({"Transaction": [44, 45, 46]})
|
||||
assert result == {"Transaction": [44, 45, 46]}
|
||||
|
||||
def test_mixed_columns_all_normalized(self):
|
||||
"""Both ref and non-ref columns are normalized to arrays."""
|
||||
result = normalize_filter({
|
||||
"Transaction": 44, # Ref column (int)
|
||||
"Debit": 500, # Non-ref column (int)
|
||||
"Memo": "test", # Non-ref column (str)
|
||||
})
|
||||
assert result == {
|
||||
"Transaction": [44],
|
||||
"Debit": [500],
|
||||
"Memo": ["test"],
|
||||
}
|
||||
|
||||
def test_multiple_values_list_unchanged(self):
|
||||
"""Filter with multiple values passes through."""
|
||||
result = normalize_filter({
|
||||
"Status": ["pending", "active"],
|
||||
"Priority": [1, 2, 3],
|
||||
})
|
||||
assert result == {
|
||||
"Status": ["pending", "active"],
|
||||
"Priority": [1, 2, 3],
|
||||
}
|
||||
|
||||
def test_mixed_single_and_list_values(self):
|
||||
"""Mix of single values and lists."""
|
||||
result = normalize_filter({
|
||||
"Transaction": 44, # Single int
|
||||
"Status": ["open", "closed"], # List
|
||||
"Amount": 100.50, # Single float
|
||||
})
|
||||
assert result == {
|
||||
"Transaction": [44],
|
||||
"Status": ["open", "closed"],
|
||||
"Amount": [100.50],
|
||||
}
|
||||
@@ -75,6 +75,45 @@ async def test_get_records(agent, auth, mock_client):
|
||||
assert result == {"records": [{"id": 1, "Name": "Alice"}]}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_records_normalizes_filter(agent, auth, mock_client):
|
||||
"""Test that filter values are normalized to array format for Grist API."""
|
||||
mock_client.get_records.return_value = [{"id": 1, "Customer": 5}]
|
||||
|
||||
await get_records(
|
||||
agent, auth, "budget", "Orders",
|
||||
filter={"Customer": 5, "Status": "active"},
|
||||
client=mock_client,
|
||||
)
|
||||
|
||||
# Verify filter was normalized: single values wrapped in lists
|
||||
mock_client.get_records.assert_called_once_with(
|
||||
"Orders",
|
||||
filter={"Customer": [5], "Status": ["active"]},
|
||||
sort=None,
|
||||
limit=None,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_records_preserves_list_filter(agent, auth, mock_client):
|
||||
"""Test that filter values already in list format are preserved."""
|
||||
mock_client.get_records.return_value = []
|
||||
|
||||
await get_records(
|
||||
agent, auth, "budget", "Orders",
|
||||
filter={"Customer": [5, 6, 7]},
|
||||
client=mock_client,
|
||||
)
|
||||
|
||||
mock_client.get_records.assert_called_once_with(
|
||||
"Orders",
|
||||
filter={"Customer": [5, 6, 7]},
|
||||
sort=None,
|
||||
limit=None,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_sql_query(agent, auth, mock_client):
|
||||
result = await sql_query(agent, auth, "budget", "SELECT * FROM Table1", client=mock_client)
|
||||
|
||||
Reference in New Issue
Block a user