Skip to content

Commit bf6871f

Browse files
committed
fix(ai): validate ai_expectations_for_source returns a list (#295)
http_get is typed as Union[Dict, requests.Response] and can return a non-list JSON shape. Guard the response in ai_expectations_for_source: raise OpenAEVParsingError when the payload is not a JSON list so the documented List[Dict] contract holds instead of silently returning the wrong shape. Add manager-level tests for the success and parsing-error paths.
1 parent 86147e6 commit bf6871f

2 files changed

Lines changed: 57 additions & 0 deletions

File tree

pyoaev/apis/inject_expectation/inject_expectation.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,11 +49,19 @@ def ai_expectations_for_source(
4949
5050
:param source_id: the identifier of the collector requesting expectations
5151
:type source_id: str
52+
:raises OpenAEVParsingError: if the server does not return a JSON list
5253
:return: a list of agentless detection/prevention expectation dicts
5354
:rtype: list[dict]
5455
"""
5556
path = f"{self.path}/ai/{source_id}"
5657
result = self.openaev.http_get(path, **kwargs)
58+
if not isinstance(result, list):
59+
raise exc.OpenAEVParsingError(
60+
error_message=(
61+
f"Expected a list of AI expectations from {path}, "
62+
f"got {type(result).__name__}"
63+
)
64+
)
5765
return result
5866

5967
def expectations_models_for_source(self, source_id: str, **kwargs: Any):
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
from unittest import TestCase, main, mock
2+
3+
from pyoaev import OpenAEV
4+
from pyoaev.exceptions import OpenAEVParsingError
5+
6+
7+
def make_json_response(payload):
8+
class MockResponse:
9+
def __init__(self):
10+
self.status_code = 200
11+
self.history = None
12+
self.content = None
13+
self.headers = {"Content-Type": "application/json"}
14+
15+
def json(self):
16+
return payload
17+
18+
return MockResponse()
19+
20+
21+
class TestAiExpectationsForSource(TestCase):
22+
@mock.patch("requests.Session.request")
23+
def test_returns_list_of_expectations(self, mock_request):
24+
expectations = [
25+
{"inject_expectation_id": "exp-1"},
26+
{"inject_expectation_id": "exp-2"},
27+
]
28+
mock_request.return_value = make_json_response(expectations)
29+
api_client = OpenAEV("url", "token")
30+
31+
result = api_client.inject_expectation.ai_expectations_for_source("collector-1")
32+
33+
mock_request.assert_called_once()
34+
_, kwargs = mock_request.call_args
35+
self.assertEqual(kwargs["method"], "get")
36+
self.assertEqual(kwargs["url"], "url/api/injects/expectations/ai/collector-1")
37+
self.assertEqual(result, expectations)
38+
39+
@mock.patch("requests.Session.request")
40+
def test_raises_parsing_error_when_not_a_list(self, mock_request):
41+
mock_request.return_value = make_json_response({"unexpected": "shape"})
42+
api_client = OpenAEV("url", "token")
43+
44+
with self.assertRaises(OpenAEVParsingError):
45+
api_client.inject_expectation.ai_expectations_for_source("collector-1")
46+
47+
48+
if __name__ == "__main__":
49+
main()

0 commit comments

Comments
 (0)