Skip to content

Commit af5e774

Browse files
authored
Merge pull request #1089 from NASA-IMPACT/1088-api-tests-for-token-verification-request-accuracy-response-parsing-and-error-handling
API Tests for Token Verification, Request Accuracy, Response Parsing, and Error Handling
2 parents 9ffb37c + 86b7dad commit af5e774

File tree

1 file changed

+159
-21
lines changed

1 file changed

+159
-21
lines changed

sde_collections/tests/test_sinequa_api.py

Lines changed: 159 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
# docker-compose -f local.yml run --rm django pytest sde_collections/tests/api_tests.py
2+
import json
23
from unittest.mock import MagicMock, patch
34

45
import pytest
@@ -12,6 +13,12 @@
1213

1314
@pytest.mark.django_db
1415
class TestApiClass:
16+
"""
17+
Test suite for the Sinequa API integration.
18+
Tests cover authentication, query construction, response processing,
19+
and error handling across different server configurations.
20+
"""
21+
1522
@pytest.fixture
1623
def collection(self):
1724
"""Fixture to create a collection object for testing."""
@@ -25,7 +32,10 @@ def collection(self):
2532

2633
@pytest.fixture
2734
def api_instance(self):
28-
"""Fixture to create an Api instance with mocked server configs."""
35+
"""
36+
Fixture to create an Api instance with mocked server configs.
37+
Provides a consistent test environment with predefined credentials.
38+
"""
2939
with patch(
3040
"sde_collections.sinequa_api.server_configs",
3141
{
@@ -41,35 +51,71 @@ def api_instance(self):
4151

4252
@patch("requests.post")
4353
def test_process_response_success(self, mock_post, api_instance):
44-
"""Test that process_response handles successful responses."""
54+
"""
55+
Test that process_response successfully handles and parses API responses.
56+
Verifies:
57+
1. Correct HTTP request processing
58+
2. JSON response parsing
59+
3. Return value structure
60+
"""
4561
mock_response = MagicMock()
4662
mock_response.status_code = 200
4763
mock_response.json.return_value = {"key": "value"}
4864
mock_post.return_value = mock_response
4965

5066
response = api_instance.process_response("http://example.com", payload={"test": "data"})
5167
assert response == {"key": "value"}
68+
mock_post.assert_called_once()
5269

5370
@patch("requests.post")
5471
def test_process_response_failure(self, mock_post, api_instance):
55-
"""Test that process_response raises an exception on failure."""
72+
"""
73+
Test that process_response properly handles failed API requests.
74+
Verifies appropriate exception raising and error messaging.
75+
"""
5676
mock_response = MagicMock()
5777
mock_response.status_code = 500
5878
mock_post.return_value = mock_response
59-
mock_response.raise_for_status.side_effect = Exception("Internal Server Error")
79+
mock_response.raise_for_status.side_effect = requests.RequestException("Internal Server Error")
6080

61-
with pytest.raises(Exception, match="Internal Server Error"):
81+
with pytest.raises(requests.RequestException, match="Internal Server Error"):
6282
api_instance.process_response("http://example.com", payload={"test": "data"})
6383

84+
def test_missing_token_for_sql_query(self, api_instance):
85+
"""
86+
Test that attempting SQL queries without a token raises an appropriate error.
87+
Verifies token validation before query execution.
88+
"""
89+
api_instance._provided_token = None
90+
with pytest.raises(ValueError, match="Token is required"):
91+
api_instance._execute_sql_query("SELECT * FROM test")
92+
6493
@patch("sde_collections.sinequa_api.Api.process_response")
6594
def test_query(self, mock_process_response, api_instance):
66-
"""Test that query sends correct payload and processes response."""
95+
"""
96+
Test that query method:
97+
1. Constructs the correct URL and payload based on input parameters
98+
2. Processes API response correctly
99+
3. Returns expected data structure
100+
"""
67101
mock_process_response.return_value = {"result": "success"}
68102
response = api_instance.query(page=1, collection_config_folder="folder")
69103
assert response == {"result": "success"}
70104

105+
# Verify payload construction
106+
mock_process_response.assert_called_once()
107+
call_args = mock_process_response.call_args
108+
assert "folder" in str(call_args) # Verify collection folder is included
109+
assert "page" in str(call_args) # Verify pagination parameters
110+
71111
def test_process_rows_to_records(self, api_instance):
72-
"""Test processing row data into record dictionaries."""
112+
"""
113+
Test processing of raw SQL row data into structured record dictionaries.
114+
Verifies:
115+
1. Correct parsing of valid input data
116+
2. Error handling for malformed rows
117+
3. Output format consistency
118+
"""
73119
# Test valid input
74120
valid_rows = [["http://example.com/1", "Text 1", "Title 1"], ["http://example.com/2", "Text 2", "Title 2"]]
75121
expected_output = [
@@ -85,7 +131,13 @@ def test_process_rows_to_records(self, api_instance):
85131

86132
@patch("sde_collections.sinequa_api.Api.process_response")
87133
def test_execute_sql_query(self, mock_process_response, api_instance):
88-
"""Test SQL query execution."""
134+
"""
135+
Test SQL query execution with token-based authentication.
136+
Verifies:
137+
1. Query construction
138+
2. Token validation
139+
3. Response processing
140+
"""
89141
mock_process_response.return_value = {"Rows": [], "TotalRowCount": 0}
90142

91143
# Test successful query
@@ -99,7 +151,13 @@ def test_execute_sql_query(self, mock_process_response, api_instance):
99151

100152
@patch("sde_collections.sinequa_api.Api._execute_sql_query")
101153
def test_get_full_texts_pagination(self, mock_execute_sql, api_instance):
102-
"""Test that get_full_texts correctly handles pagination."""
154+
"""
155+
Test pagination handling in get_full_texts method.
156+
Verifies:
157+
1. Correct batch processing
158+
2. Accurate record counting
159+
3. Proper iteration termination
160+
"""
103161
# Mock responses for two pages of results
104162
mock_execute_sql.side_effect = [
105163
{
@@ -117,17 +175,18 @@ def test_get_full_texts_pagination(self, mock_execute_sql, api_instance):
117175
assert len(batches[0]) == 2 # First batch has 2 records
118176
assert len(batches[1]) == 1 # Second batch has 1 record
119177

120-
# Verify content of first batch
178+
# Verify content of batches
121179
assert batches[0] == [
122180
{"url": "http://example.com/1", "full_text": "Text 1", "title": "Title 1"},
123181
{"url": "http://example.com/2", "full_text": "Text 2", "title": "Title 2"},
124182
]
125-
126-
# Verify content of second batch
127183
assert batches[1] == [{"url": "http://example.com/3", "full_text": "Text 3", "title": "Title 3"}]
128184

129185
def test_get_full_texts_missing_index(self, api_instance):
130-
"""Test that get_full_texts raises error when index is missing from config."""
186+
"""
187+
Test error handling when index configuration is missing.
188+
Verifies appropriate error message and exception type.
189+
"""
131190
api_instance.config.pop("index", None)
132191
with pytest.raises(ValueError, match="Index not defined for server"):
133192
next(api_instance.get_full_texts("test_folder"))
@@ -141,7 +200,13 @@ def test_get_full_texts_missing_index(self, api_instance):
141200
)
142201
@patch("requests.post")
143202
def test_query_authentication(self, mock_post, server_name, expect_auth, api_instance):
144-
"""Test authentication handling for different server types."""
203+
"""
204+
Test authentication handling for different server types.
205+
Verifies:
206+
1. Dev servers require authentication
207+
2. Production servers skip authentication
208+
3. Correct credential handling
209+
"""
145210
api_instance.server_name = server_name
146211
mock_post.return_value = MagicMock(status_code=200, json=lambda: {"result": "success"})
147212

@@ -154,7 +219,10 @@ def test_query_authentication(self, mock_post, server_name, expect_auth, api_ins
154219

155220
@patch("requests.post")
156221
def test_query_dev_server_missing_credentials(self, mock_post, api_instance):
157-
"""Test that dev servers raise error when credentials are missing."""
222+
"""
223+
Test error handling for dev servers with missing credentials.
224+
Verifies appropriate error messages and authentication requirements.
225+
"""
158226
api_instance.server_name = "xli"
159227
api_instance._provided_user = None
160228
api_instance._provided_password = None
@@ -164,7 +232,13 @@ def test_query_dev_server_missing_credentials(self, mock_post, api_instance):
164232

165233
@patch("sde_collections.sinequa_api.Api._execute_sql_query")
166234
def test_get_full_texts_batch_size_reduction(self, mock_execute_sql, api_instance):
167-
"""Test that batch size reduces appropriately on failure and continues processing."""
235+
"""
236+
Test batch size reduction logic when queries fail.
237+
Verifies:
238+
1. Progressive batch size reduction
239+
2. Retry mechanism
240+
3. Successful recovery
241+
"""
168242
# Mock first query to fail, then succeed with smaller batch
169243
mock_execute_sql.side_effect = [
170244
requests.RequestException("Query too large"), # First attempt fails
@@ -181,7 +255,7 @@ def test_get_full_texts_batch_size_reduction(self, mock_execute_sql, api_instanc
181255
assert len(batches[0]) == 1
182256
assert batches[0][0]["url"] == "http://example.com/1"
183257

184-
# Verify the calls made - first with original size, then with reduced size
258+
# Verify batch size reduction logic
185259
assert mock_execute_sql.call_count == 2
186260
first_call = mock_execute_sql.call_args_list[0][0][0]
187261
second_call = mock_execute_sql.call_args_list[1][0][0]
@@ -190,24 +264,88 @@ def test_get_full_texts_batch_size_reduction(self, mock_execute_sql, api_instanc
190264

191265
@patch("sde_collections.sinequa_api.Api._execute_sql_query")
192266
def test_get_full_texts_minimum_batch_size(self, mock_execute_sql, api_instance):
193-
"""Test behavior when reaching minimum batch size."""
267+
"""
268+
Test behavior when reaching minimum batch size.
269+
Verifies error handling at minimum batch size threshold.
270+
"""
194271
mock_execute_sql.side_effect = requests.RequestException("Query failed")
195272

196273
# Start with batch_size=4, min_batch_size=1
197-
# Should try: 4 -> 2 -> 1 -> raise error
198274
with pytest.raises(ValueError, match="Failed to process batch even at minimum size 1"):
199275
list(api_instance.get_full_texts("test_folder", batch_size=4, min_batch_size=1))
200276

201-
# Should have tried 3 times before giving up
277+
# Verify retry attempts
202278
assert mock_execute_sql.call_count == 3
203279
calls = mock_execute_sql.call_args_list
204280
assert "COUNT 4" in calls[0][0][0] # First try with 4
205281
assert "COUNT 2" in calls[1][0][0] # Second try with 2
206282
assert "COUNT 1" in calls[2][0][0] # Final try with 1
207283

284+
@patch("requests.post")
285+
def test_sql_query_construction(self, mock_post, api_instance):
286+
"""
287+
Test direct SQL query execution with specific URL and payload validation.
288+
Verifies:
289+
1. Correct URL construction
290+
2. Proper payload formatting
291+
3. Token-based authentication
292+
"""
293+
mock_response = MagicMock()
294+
mock_response.status_code = 200
295+
mock_response.json.return_value = {"Rows": [["http://example.com", "sample text", "sample title"]]}
296+
mock_post.return_value = mock_response
297+
298+
sql = "SELECT url1, text, title FROM test_index WHERE collection = '/SDE/sample_folder/'"
299+
api_instance._execute_sql_query(sql)
300+
301+
# Verify URL and payload construction
302+
mock_post.assert_called_once()
303+
call_args = mock_post.call_args
304+
305+
# Get the actual payload from the call arguments
306+
_, kwargs = call_args
307+
payload = json.loads(kwargs.get("data", "{}"))
308+
309+
# Verify each component separately
310+
assert "engine.sql" in call_args[0][0] # Verify endpoint
311+
assert kwargs["headers"]["Authorization"] == "Bearer test_token" # Verify token usage
312+
assert payload["sql"] == sql # Verify SQL query inclusion
313+
314+
def test_process_full_text_response(self, api_instance):
315+
"""
316+
Test static method for processing full text response data.
317+
Verifies:
318+
1. Correct parsing of raw response data
319+
2. Proper dictionary structure creation
320+
3. Error handling for invalid response format
321+
"""
322+
# Test valid response processing
323+
raw_response = {
324+
"Rows": [
325+
["http://example.com/article1", "Full text 1", "Title 1"],
326+
["http://example.com/article2", "Full text 2", "Title 2"],
327+
]
328+
}
329+
expected = [
330+
{"url": "http://example.com/article1", "full_text": "Full text 1", "title": "Title 1"},
331+
{"url": "http://example.com/article2", "full_text": "Full text 2", "title": "Title 2"},
332+
]
333+
processed = Api._process_full_text_response(raw_response)
334+
assert processed == expected
335+
336+
# Test invalid response format
337+
with pytest.raises(ValueError, match="Invalid response format"):
338+
Api._process_full_text_response({"wrong_key": []})
339+
208340
@patch("sde_collections.sinequa_api.Api._execute_sql_query")
209341
def test_get_full_texts_batch_size_progression(self, mock_execute_sql, api_instance):
210-
"""Test multiple batch size reductions followed by successful query."""
342+
"""
343+
Test multiple batch size reductions followed by successful query.
344+
Verifies:
345+
1. Progressive batch size reduction steps
346+
2. Recovery after multiple failures
347+
3. Final successful query execution
348+
"""
211349
mock_execute_sql.side_effect = [
212350
requests.RequestException("First failure"),
213351
requests.RequestException("Second failure"),

0 commit comments

Comments
 (0)