-
Notifications
You must be signed in to change notification settings - Fork 3
Expand file tree
/
Copy pathtest_metadata_error_handling.py
More file actions
255 lines (206 loc) · 10.5 KB
/
Copy pathtest_metadata_error_handling.py
File metadata and controls
255 lines (206 loc) · 10.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
"""Test metadata filtering error handling."""
import pytest
from fastmcp import Context as MockFastMCPContext
from app.server import search_context
@pytest.mark.asyncio
class TestMetadataErrorHandling:
"""Test error handling for metadata filtering."""
@pytest.mark.usefixtures('initialized_server')
async def test_invalid_operator_returns_error(self, mock_context: MockFastMCPContext) -> None:
"""Test that invalid operators return proper error responses."""
# Try to use an invalid operator
result = await search_context.fn(
thread_id='test',
metadata_filters=[
{'key': 'status', 'operator': 'invalid_operator', 'value': 'active'},
],
ctx=mock_context,
)
# Should return an error response
assert isinstance(result, dict)
assert 'entries' in result
assert result['entries'] == []
assert 'error' in result
assert 'Metadata filter validation failed' in result['error']
assert 'validation_errors' in result
assert len(result['validation_errors']) > 0
assert 'invalid_operator' in result['validation_errors'][0]
@pytest.mark.usefixtures('initialized_server')
async def test_empty_in_list_returns_error(self, mock_context: MockFastMCPContext) -> None:
"""Test that empty IN operator lists return proper error responses."""
# Try to use an empty list with IN operator
result = await search_context.fn(
thread_id='test',
metadata_filters=[
{'key': 'status', 'operator': 'in', 'value': []},
],
ctx=mock_context,
)
# Should return an error response
assert isinstance(result, dict)
assert 'entries' in result
assert result['entries'] == []
assert 'error' in result
assert 'Metadata filter validation failed' in result['error']
assert 'validation_errors' in result
assert len(result['validation_errors']) > 0
assert 'non-empty list' in result['validation_errors'][0]
@pytest.mark.usefixtures('initialized_server')
async def test_multiple_invalid_filters_collect_all_errors(self, mock_context: MockFastMCPContext) -> None:
"""Test that multiple invalid filters collect all errors."""
# Try multiple invalid filters
result = await search_context.fn(
thread_id='test',
metadata_filters=[
{'key': 'status', 'operator': 'invalid_op', 'value': 'active'},
{'key': 'priority', 'operator': 'in', 'value': []},
{'key': 'type', 'operator': 'another_invalid', 'value': 5},
],
ctx=mock_context,
)
# Should return an error response with all validation errors
assert isinstance(result, dict)
assert 'entries' in result
assert result['entries'] == []
assert 'error' in result
assert 'validation_errors' in result
assert len(result['validation_errors']) == 3 # All three errors collected
@pytest.mark.usefixtures('initialized_server')
async def test_valid_filters_work_correctly(self, mock_context: MockFastMCPContext) -> None:
"""Test that valid filters still work correctly after error handling changes."""
# Use valid filters
result = await search_context.fn(
thread_id='test',
metadata_filters=[
{'key': 'status', 'operator': 'eq', 'value': 'active'},
{'key': 'priority', 'operator': 'gt', 'value': 5},
{'key': 'tags', 'operator': 'in', 'value': ['urgent', 'important']},
],
ctx=mock_context,
)
# Should NOT return an error
assert isinstance(result, dict)
assert 'entries' in result
assert 'error' not in result
assert 'validation_errors' not in result
assert 'stats' in result
@pytest.mark.usefixtures('initialized_server')
async def test_case_sensitivity_flag_works(self, mock_context: MockFastMCPContext) -> None:
"""Test that case_sensitive flag is properly handled."""
# Store test data first
from app.server import store_context
await store_context.fn(
thread_id='test_case',
source='user',
text='Test entry',
metadata={'name': 'TestCase'},
ctx=mock_context,
)
# Search with case-insensitive (default)
result1 = await search_context.fn(
thread_id='test_case',
metadata_filters=[
{'key': 'name', 'operator': 'eq', 'value': 'testcase', 'case_sensitive': False},
],
ctx=mock_context,
)
assert 'entries' in result1
assert len(result1['entries']) == 1 # Should find the entry
# Search with case-sensitive
result2 = await search_context.fn(
thread_id='test_case',
metadata_filters=[
{'key': 'name', 'operator': 'eq', 'value': 'testcase', 'case_sensitive': True},
],
ctx=mock_context,
)
assert 'entries' in result2
assert len(result2['entries']) == 0 # Should NOT find the entry (case mismatch)
# Search with correct case
result3 = await search_context.fn(
thread_id='test_case',
metadata_filters=[
{'key': 'name', 'operator': 'eq', 'value': 'TestCase', 'case_sensitive': True},
],
ctx=mock_context,
)
assert 'entries' in result3
assert len(result3['entries']) == 1 # Should find the entry (exact match)
class TestMetadataFilterValidation:
"""Test MetadataFilter pydantic model validation directly."""
def test_empty_key_raises_validation_error(self) -> None:
"""Test that empty metadata key raises validation error."""
from pydantic import ValidationError
from app.metadata_types import MetadataFilter
from app.metadata_types import MetadataOperator
with pytest.raises(ValidationError, match='Metadata key cannot be empty'):
MetadataFilter(key='', operator=MetadataOperator.EQ, value='test')
def test_whitespace_only_key_raises_validation_error(self) -> None:
"""Test that whitespace-only metadata key raises validation error."""
from pydantic import ValidationError
from app.metadata_types import MetadataFilter
from app.metadata_types import MetadataOperator
with pytest.raises(ValidationError, match='Metadata key cannot be empty'):
MetadataFilter(key=' ', operator=MetadataOperator.EQ, value='test')
def test_invalid_key_pattern_raises_validation_error(self) -> None:
"""Test that invalid key pattern raises validation error."""
from pydantic import ValidationError
from app.metadata_types import MetadataFilter
from app.metadata_types import MetadataOperator
# Special characters not allowed
with pytest.raises(ValidationError, match='Invalid metadata key'):
MetadataFilter(key='status@field', operator=MetadataOperator.EQ, value='test')
# Spaces not allowed
with pytest.raises(ValidationError, match='Invalid metadata key'):
MetadataFilter(key='my field', operator=MetadataOperator.EQ, value='test')
# SQL injection attempts blocked
with pytest.raises(ValidationError, match='Invalid metadata key'):
MetadataFilter(key="status'; DROP TABLE", operator=MetadataOperator.EQ, value='test')
def test_valid_key_patterns_accepted(self) -> None:
"""Test that valid key patterns are accepted."""
from app.metadata_types import MetadataFilter
from app.metadata_types import MetadataOperator
# Simple keys
f1 = MetadataFilter(key='status', operator=MetadataOperator.EQ, value='active')
assert f1.key == 'status'
# Nested paths with dots
f2 = MetadataFilter(key='user.preferences.theme', operator=MetadataOperator.EQ, value='dark')
assert f2.key == 'user.preferences.theme'
# Underscores and hyphens
f3 = MetadataFilter(key='task_name', operator=MetadataOperator.EQ, value='test')
assert f3.key == 'task_name'
f4 = MetadataFilter(key='agent-name', operator=MetadataOperator.EQ, value='test')
assert f4.key == 'agent-name'
def test_in_operator_requires_list(self) -> None:
"""Test that IN operator requires list value."""
from pydantic import ValidationError
from app.metadata_types import MetadataFilter
from app.metadata_types import MetadataOperator
with pytest.raises(ValidationError, match='requires a list value'):
MetadataFilter(key='status', operator=MetadataOperator.IN, value='active')
with pytest.raises(ValidationError, match='requires a list value'):
MetadataFilter(key='priority', operator=MetadataOperator.NOT_IN, value=5)
def test_string_operators_require_string(self) -> None:
"""Test that string operators require string value."""
from pydantic import ValidationError
from app.metadata_types import MetadataFilter
from app.metadata_types import MetadataOperator
with pytest.raises(ValidationError, match='requires a string value'):
MetadataFilter(key='name', operator=MetadataOperator.CONTAINS, value=123)
with pytest.raises(ValidationError, match='requires a string value'):
MetadataFilter(key='name', operator=MetadataOperator.STARTS_WITH, value=['list'])
with pytest.raises(ValidationError, match='requires a string value'):
MetadataFilter(key='name', operator=MetadataOperator.ENDS_WITH, value=True)
def test_existence_operators_ignore_value(self) -> None:
"""Test that existence operators ignore provided value."""
from app.metadata_types import MetadataFilter
from app.metadata_types import MetadataOperator
# Value is set to None for existence operators
f1 = MetadataFilter(key='status', operator=MetadataOperator.EXISTS, value='ignored')
assert f1.value is None
f2 = MetadataFilter(key='status', operator=MetadataOperator.NOT_EXISTS, value=123)
assert f2.value is None
f3 = MetadataFilter(key='status', operator=MetadataOperator.IS_NULL)
assert f3.value is None
f4 = MetadataFilter(key='status', operator=MetadataOperator.IS_NOT_NULL, value=['ignored'])
assert f4.value is None