Skip to content

Commit dd7c4d2

Browse files
Merge pull request #47 from netboxlabs/fix/search-pagination
fix: netbox_search_objects now extracts 'results' array from NetBox's paginated response structure instead of returning the full '{count, next, previous, results}' dict as it is not equipped for pagination
2 parents 80b51fb + 1736204 commit dd7c4d2

File tree

2 files changed

+90
-11
lines changed

2 files changed

+90
-11
lines changed

server.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -641,14 +641,16 @@ def netbox_search_objects(
641641
# Build results dictionary (error-resilient)
642642
for obj_type in search_types:
643643
try:
644-
results[obj_type] = netbox.get(
644+
response = netbox.get(
645645
NETBOX_OBJECT_TYPES[obj_type],
646646
params={
647647
"q": query,
648648
"limit": limit,
649649
"fields": ",".join(fields) if fields else None,
650650
},
651651
)
652+
# Extract results array from paginated response
653+
results[obj_type] = response.get("results", [])
652654
except Exception:
653655
# Continue searching other types if one fails
654656
# results[obj_type] already has empty list

tests/test_search.py

Lines changed: 87 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77

88
from server import NETBOX_OBJECT_TYPES, netbox_search_objects
99

10-
1110
# ============================================================================
1211
# Parameter Validation Tests
1312
# ============================================================================
@@ -44,7 +43,12 @@ def test_invalid_object_type_raises_error():
4443
@patch("server.netbox")
4544
def test_searches_default_types_when_none_specified(mock_netbox):
4645
"""When object_types=None, should search 8 default common types."""
47-
mock_netbox.get.return_value = []
46+
mock_netbox.get.return_value = {
47+
"count": 0,
48+
"next": None,
49+
"previous": None,
50+
"results": [],
51+
}
4852

4953
result = netbox_search_objects.fn(query="test")
5054

@@ -57,7 +61,12 @@ def test_searches_default_types_when_none_specified(mock_netbox):
5761
@patch("server.netbox")
5862
def test_custom_object_types_limits_search_scope(mock_netbox):
5963
"""When object_types specified, should only search those types."""
60-
mock_netbox.get.return_value = []
64+
mock_netbox.get.return_value = {
65+
"count": 0,
66+
"next": None,
67+
"previous": None,
68+
"results": [],
69+
}
6170

6271
result = netbox_search_objects.fn(query="test", object_types=["devices", "sites"])
6372

@@ -74,7 +83,12 @@ def test_custom_object_types_limits_search_scope(mock_netbox):
7483
@patch("server.netbox")
7584
def test_field_projection_applied_to_queries(mock_netbox):
7685
"""When fields specified, should apply to all queries as comma-separated string."""
77-
mock_netbox.get.return_value = []
86+
mock_netbox.get.return_value = {
87+
"count": 0,
88+
"next": None,
89+
"previous": None,
90+
"results": [],
91+
}
7892

7993
netbox_search_objects.fn(
8094
query="test", object_types=["devices", "sites"], fields=["id", "name"]
@@ -97,8 +111,13 @@ def test_result_structure_with_empty_and_populated_results(mock_netbox):
97111

98112
def mock_get_side_effect(endpoint, params):
99113
if "devices" in endpoint:
100-
return [{"id": 1, "name": "device01"}]
101-
return []
114+
return {
115+
"count": 1,
116+
"next": None,
117+
"previous": None,
118+
"results": [{"id": 1, "name": "device01"}],
119+
}
120+
return {"count": 0, "next": None, "previous": None, "results": []}
102121

103122
mock_netbox.get.side_effect = mock_get_side_effect
104123

@@ -128,8 +147,13 @@ def mock_get_side_effect(endpoint, params):
128147
if "devices" in endpoint:
129148
raise Exception("API error")
130149
elif "sites" in endpoint:
131-
return [{"id": 1, "name": "site01"}]
132-
return []
150+
return {
151+
"count": 1,
152+
"next": None,
153+
"previous": None,
154+
"results": [{"id": 1, "name": "site01"}],
155+
}
156+
return {"count": 0, "next": None, "previous": None, "results": []}
133157

134158
mock_netbox.get.side_effect = mock_get_side_effect
135159

@@ -149,7 +173,12 @@ def mock_get_side_effect(endpoint, params):
149173
@patch("server.netbox")
150174
def test_api_parameters_passed_correctly(mock_netbox):
151175
"""Should pass query, limit, and fields to NetBox API correctly."""
152-
mock_netbox.get.return_value = []
176+
mock_netbox.get.return_value = {
177+
"count": 0,
178+
"next": None,
179+
"previous": None,
180+
"results": [],
181+
}
153182

154183
netbox_search_objects.fn(
155184
query="switch01", object_types=["devices"], fields=["id"], limit=25
@@ -166,10 +195,58 @@ def test_api_parameters_passed_correctly(mock_netbox):
166195
@patch("server.netbox")
167196
def test_uses_correct_api_endpoints(mock_netbox):
168197
"""Should use correct API endpoints from NETBOX_OBJECT_TYPES mapping."""
169-
mock_netbox.get.return_value = []
198+
mock_netbox.get.return_value = {
199+
"count": 0,
200+
"next": None,
201+
"previous": None,
202+
"results": [],
203+
}
170204

171205
netbox_search_objects.fn(query="test", object_types=["devices", "ip-addresses"])
172206

173207
called_endpoints = [call[0][0] for call in mock_netbox.get.call_args_list]
174208
assert NETBOX_OBJECT_TYPES["devices"] in called_endpoints
175209
assert NETBOX_OBJECT_TYPES["ip-addresses"] in called_endpoints
210+
211+
212+
# ============================================================================
213+
# Paginated Response Handling Tests
214+
# ============================================================================
215+
216+
217+
@patch("server.netbox")
218+
def test_extracts_results_from_paginated_response(mock_netbox):
219+
"""Should extract 'results' array from NetBox paginated response structure.
220+
221+
NetBox API returns paginated responses with structure:
222+
{
223+
"count": <total>,
224+
"next": <url or null>,
225+
"previous": <url or null>,
226+
"results": [<objects>]
227+
}
228+
229+
The tool should return just the results arrays, not the full response.
230+
"""
231+
# Mock realistic paginated response from NetBox API
232+
mock_netbox.get.return_value = {
233+
"count": 2,
234+
"next": None,
235+
"previous": None,
236+
"results": [
237+
{"id": 1, "name": "device01"},
238+
{"id": 2, "name": "device02"},
239+
],
240+
}
241+
242+
result = netbox_search_objects.fn(query="test", object_types=["devices"])
243+
244+
# Should return dict with object type as key
245+
assert "devices" in result
246+
# Value should be a list (array), not a dict
247+
assert isinstance(result["devices"], list)
248+
# Should contain just the results, not the paginated response wrapper
249+
assert result["devices"] == [
250+
{"id": 1, "name": "device01"},
251+
{"id": 2, "name": "device02"},
252+
]

0 commit comments

Comments
 (0)