Skip to content

Commit 2aa4332

Browse files
committed
increasing code test coverage
1 parent e3580e4 commit 2aa4332

File tree

1 file changed

+213
-0
lines changed

1 file changed

+213
-0
lines changed

tests/unit/pipeline/extractors/stores/test_splunk_extractor.py

Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
import pytest
55
from hamcrest import assert_that, equal_to, has_entries, has_length
6+
from httpx import HTTPStatusError
67

78
from nodestream.pipeline.extractors.stores.splunk_extractor import SplunkExtractor
89

@@ -109,6 +110,16 @@ def test_splunk_extractor_auth_property_with_basic_auth(splunk_extractor_basic_a
109110
assert_that(auth is not None, equal_to(True))
110111

111112

113+
def test_splunk_extractor_auth_property_returns_none_when_no_credentials():
114+
"""Test _auth property returns None when no credentials are provided."""
115+
extractor = SplunkExtractor.from_file_data(
116+
base_url="https://splunk.example.com:8089",
117+
query="index=main",
118+
# No auth_token, username, or password provided
119+
)
120+
assert_that(extractor._auth, equal_to(None))
121+
122+
112123
def test_splunk_extractor_headers_with_token(splunk_extractor):
113124
headers = splunk_extractor._headers
114125
assert_that(
@@ -212,6 +223,20 @@ async def test_splunk_extractor_create_search_job_malformed_xml(
212223
await splunk_extractor._create_search_job(mock_client)
213224

214225

226+
@pytest.mark.asyncio
227+
async def test_splunk_extractor_create_search_job_http_error(splunk_extractor, mocker):
228+
"""Test job creation when Splunk returns non-201 status code."""
229+
mock_response = mocker.MagicMock()
230+
mock_response.status_code = 400
231+
mock_response.request = mocker.MagicMock()
232+
233+
mock_client = mocker.MagicMock()
234+
mock_client.post = AsyncMock(return_value=mock_response)
235+
236+
with pytest.raises(HTTPStatusError, match="Failed to create search job: 400"):
237+
await splunk_extractor._create_search_job(mock_client)
238+
239+
215240
@pytest.mark.asyncio
216241
async def test_splunk_extractor_wait_for_job_completion_json_response(
217242
splunk_extractor, mocker
@@ -269,10 +294,75 @@ async def test_splunk_extractor_wait_for_job_completion_xml_fallback(
269294
)
270295

271296

297+
@pytest.mark.asyncio
298+
async def test_splunk_extractor_wait_for_job_completion_http_error(
299+
splunk_extractor, mocker
300+
):
301+
"""Test job status checking when Splunk returns non-200 status code."""
302+
mock_response = mocker.MagicMock()
303+
mock_response.status_code = 403
304+
mock_response.request = mocker.MagicMock()
305+
306+
mock_client = mocker.MagicMock()
307+
mock_client.get = AsyncMock(return_value=mock_response)
308+
309+
with pytest.raises(HTTPStatusError, match="Failed to check job status: 403"):
310+
await splunk_extractor._wait_for_job_completion(mock_client, "test123")
311+
312+
313+
@pytest.mark.asyncio
314+
async def test_splunk_extractor_wait_for_job_completion_parse_error_warning(
315+
splunk_extractor, mocker
316+
):
317+
"""Test job status checking when both JSON and XML parsing fail."""
318+
mock_response = mocker.MagicMock()
319+
mock_response.status_code = 200
320+
mock_response.text = "Invalid response format"
321+
mock_response.json.side_effect = json.JSONDecodeError("Not JSON", "", 0)
322+
323+
mock_client = mocker.MagicMock()
324+
mock_client.get = AsyncMock(return_value=mock_response)
325+
326+
# Mock the logger to verify warning is logged
327+
with patch.object(splunk_extractor, "logger") as mock_logger:
328+
with pytest.raises(RuntimeError, match="Search job timed out"):
329+
await splunk_extractor._wait_for_job_completion(
330+
mock_client, "test123", max_wait_seconds=1
331+
)
332+
333+
# Verify warning was logged for parse failure
334+
mock_logger.warning.assert_called()
335+
warning_call = mock_logger.warning.call_args
336+
assert "Failed to parse job status" in warning_call[0][0]
337+
338+
339+
@pytest.mark.asyncio
340+
async def test_splunk_extractor_wait_for_job_completion_timeout(
341+
splunk_extractor, mocker
342+
):
343+
"""Test job status checking timeout."""
344+
mock_response = mocker.MagicMock()
345+
mock_response.status_code = 200
346+
mock_response.json.return_value = {
347+
"entry": [{"content": {"dispatchState": "RUNNING"}}]
348+
}
349+
350+
mock_client = mocker.MagicMock()
351+
mock_client.get = AsyncMock(return_value=mock_response)
352+
353+
with pytest.raises(
354+
RuntimeError, match="Search job timed out after 1 seconds: test123"
355+
):
356+
await splunk_extractor._wait_for_job_completion(
357+
mock_client, "test123", max_wait_seconds=1
358+
)
359+
360+
272361
@pytest.mark.asyncio
273362
async def test_splunk_extractor_wait_for_job_completion_failure(
274363
splunk_extractor, mocker
275364
):
365+
"""Test job status checking when job fails."""
276366
mock_response = mocker.MagicMock()
277367
mock_response.status_code = 200
278368
mock_response.json.return_value = {
@@ -372,6 +462,21 @@ async def test_splunk_extractor_get_job_results_pagination(splunk_extractor, moc
372462
assert_that(splunk_extractor.is_done, equal_to(True))
373463

374464

465+
@pytest.mark.asyncio
466+
async def test_splunk_extractor_get_job_results_http_error(splunk_extractor, mocker):
467+
"""Test results retrieval when Splunk returns non-200 status code."""
468+
mock_response = mocker.MagicMock()
469+
mock_response.status_code = 404
470+
mock_response.request = mocker.MagicMock()
471+
472+
mock_client = mocker.MagicMock()
473+
mock_client.get = AsyncMock(return_value=mock_response)
474+
475+
with pytest.raises(HTTPStatusError, match="Failed to get job results: 404"):
476+
async for _ in splunk_extractor._get_job_results(mock_client, "test123"):
477+
pass
478+
479+
375480
# Full extraction test
376481
@pytest.mark.asyncio
377482
async def test_splunk_extractor_extract_records_full_flow(splunk_extractor, mocker):
@@ -442,3 +547,111 @@ async def test_splunk_extractor_resume_from_checkpoint(splunk_extractor):
442547
assert_that(splunk_extractor.search_id, equal_to("restored123"))
443548
assert_that(splunk_extractor.offset, equal_to(50))
444549
assert_that(splunk_extractor.is_done, equal_to(False))
550+
551+
552+
@pytest.mark.asyncio
553+
async def test_splunk_extractor_extract_records_http_error_handling(
554+
splunk_extractor, mocker
555+
):
556+
"""Test extract_records handles HTTPStatusError and logs properly."""
557+
with patch(
558+
"nodestream.pipeline.extractors.stores.splunk_extractor.AsyncClient"
559+
) as mock_client_class:
560+
mock_client = mocker.MagicMock()
561+
mock_client_class.return_value.__aenter__.return_value = mock_client
562+
mock_client_class.return_value.__aexit__.return_value = None
563+
564+
# Mock HTTPStatusError during job creation
565+
mock_response = mocker.MagicMock()
566+
mock_response.status_code = 401
567+
mock_response.text = "Unauthorized"
568+
mock_response.request = mocker.MagicMock()
569+
mock_response.request.url = "https://splunk.example.com/jobs"
570+
571+
http_error = HTTPStatusError(
572+
"Unauthorized", request=mock_response.request, response=mock_response
573+
)
574+
mock_client.post = AsyncMock(side_effect=http_error)
575+
576+
# Mock the logger to verify error logging
577+
with patch.object(splunk_extractor, "logger") as mock_logger:
578+
with pytest.raises(HTTPStatusError):
579+
async for _ in splunk_extractor.extract_records():
580+
pass
581+
582+
# Verify HTTPStatusError was logged with proper details
583+
mock_logger.error.assert_called()
584+
error_call = mock_logger.error.call_args
585+
assert "Splunk request failed" in error_call[0][0]
586+
assert error_call[1]["extra"]["status_code"] == 401
587+
assert error_call[1]["extra"]["content"] == "Unauthorized"
588+
589+
590+
@pytest.mark.asyncio
591+
async def test_splunk_extractor_extract_records_generic_exception_handling(
592+
splunk_extractor, mocker
593+
):
594+
"""Test extract_records handles generic exceptions and logs properly."""
595+
with patch(
596+
"nodestream.pipeline.extractors.stores.splunk_extractor.AsyncClient"
597+
) as mock_client_class:
598+
mock_client = mocker.MagicMock()
599+
mock_client_class.return_value.__aenter__.return_value = mock_client
600+
mock_client_class.return_value.__aexit__.return_value = None
601+
602+
# Mock a generic exception during job creation
603+
generic_error = ValueError("Something went wrong")
604+
mock_client.post = AsyncMock(side_effect=generic_error)
605+
606+
# Mock the logger to verify error logging
607+
with patch.object(splunk_extractor, "logger") as mock_logger:
608+
with pytest.raises(ValueError):
609+
async for _ in splunk_extractor.extract_records():
610+
pass
611+
612+
# Verify generic exception was logged with proper details
613+
mock_logger.error.assert_called()
614+
error_call = mock_logger.error.call_args
615+
assert "Unexpected error during extraction" in error_call[0][0]
616+
assert error_call[1]["extra"]["error"] == "Something went wrong"
617+
assert error_call[1]["extra"]["error_type"] == "ValueError"
618+
619+
620+
@pytest.mark.asyncio
621+
async def test_splunk_extractor_extract_records_http_error_without_text_attribute(
622+
splunk_extractor, mocker
623+
):
624+
"""Test HTTPStatusError handling when response doesn't have text attribute."""
625+
with patch(
626+
"nodestream.pipeline.extractors.stores.splunk_extractor.AsyncClient"
627+
) as mock_client_class:
628+
mock_client = mocker.MagicMock()
629+
mock_client_class.return_value.__aenter__.return_value = mock_client
630+
mock_client_class.return_value.__aexit__.return_value = None
631+
632+
# Mock HTTPStatusError with response that doesn't have text attribute
633+
mock_response = mocker.MagicMock()
634+
mock_response.status_code = 500
635+
# Remove text attribute to test fallback
636+
del mock_response.text
637+
mock_response.request = mocker.MagicMock()
638+
mock_response.request.url = "https://splunk.example.com/jobs"
639+
640+
http_error = HTTPStatusError(
641+
"Server Error", request=mock_response.request, response=mock_response
642+
)
643+
mock_client.post = AsyncMock(side_effect=http_error)
644+
645+
# Mock the logger to verify error logging
646+
with patch.object(splunk_extractor, "logger") as mock_logger:
647+
with pytest.raises(HTTPStatusError):
648+
async for _ in splunk_extractor.extract_records():
649+
pass
650+
651+
# Verify HTTPStatusError was logged with str(response) fallback
652+
mock_logger.error.assert_called()
653+
error_call = mock_logger.error.call_args
654+
assert "Splunk request failed" in error_call[0][0]
655+
assert error_call[1]["extra"]["status_code"] == 500
656+
# Should use str(response) when text attribute is missing
657+
assert str(mock_response) in error_call[1]["extra"]["content"]

0 commit comments

Comments
 (0)