Skip to content

Commit fac77ed

Browse files
committed
cover
1 parent 2080a4e commit fac77ed

File tree

1 file changed

+144
-0
lines changed

1 file changed

+144
-0
lines changed

tests/test_server_disconnected_retry.py

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,14 @@
55
import asyncio
66

77
from collections.abc import AsyncGenerator
8+
from unittest.mock import AsyncMock, Mock, patch
89
import pytest
910
import pytest_asyncio
11+
import aiohttp
1012
from aiohttp import web, ClientSession
1113
from lxml import etree
1214
from onvif.client import AsyncTransportProtocolErrorHandler
15+
from onvif.zeep_aiohttp import AIOHTTPTransport
1316

1417

1518
class DisconnectingHTTPProtocol(asyncio.Protocol):
@@ -282,3 +285,144 @@ async def test_no_retry_with_proper_connection_close(
282285

283286
# Should be exactly 3 requests (no retries)
284287
assert server.request_count == 3
288+
289+
290+
@pytest.mark.asyncio
291+
async def test_post_xml_without_retry_decorator_fails() -> None:
292+
"""Test that without the retry decorator on post_xml, ServerDisconnectedError propagates."""
293+
294+
# Create a mock session
295+
mock_session: Mock = Mock(spec=ClientSession)
296+
mock_session.timeout = Mock(total=30, sock_read=10)
297+
298+
# Create the base transport (without retry decorator)
299+
transport = AIOHTTPTransport(session=mock_session, verify_ssl=False)
300+
301+
# Mock envelope
302+
mock_envelope = Mock()
303+
mock_envelope.tag = "TestEnvelope"
304+
305+
# Mock etree_to_string
306+
with patch("onvif.zeep_aiohttp.etree_to_string", return_value=b"<test/>"):
307+
# Make session.post raise ServerDisconnectedError
308+
mock_session.post = AsyncMock(
309+
side_effect=aiohttp.ServerDisconnectedError("Server disconnected")
310+
)
311+
312+
# This should raise ServerDisconnectedError without retry
313+
with pytest.raises(aiohttp.ServerDisconnectedError):
314+
await transport.post_xml("http://example.com/onvif", mock_envelope, {})
315+
316+
# Should only be called once (no retry)
317+
assert mock_session.post.call_count == 1
318+
319+
320+
@pytest.mark.asyncio
321+
async def test_post_xml_with_retry_decorator_succeeds():
322+
"""Test that with the retry decorator on post_xml, ServerDisconnectedError is retried."""
323+
324+
# Create a mock session
325+
mock_session = Mock(spec=ClientSession)
326+
mock_session.timeout = Mock(total=30, sock_read=10)
327+
328+
# Create the transport with retry decorator
329+
transport = AsyncTransportProtocolErrorHandler(
330+
session=mock_session, verify_ssl=False
331+
)
332+
333+
# Mock envelope
334+
mock_envelope = Mock()
335+
mock_envelope.tag = "TestEnvelope"
336+
337+
# Mock etree_to_string
338+
with patch("onvif.zeep_aiohttp.etree_to_string", return_value=b"<test/>"):
339+
# First call fails, second succeeds
340+
mock_response = Mock()
341+
mock_response.status = 200
342+
mock_response.headers = {}
343+
mock_response.cookies = {}
344+
mock_response.charset = "utf-8"
345+
mock_response.read = AsyncMock(return_value=b"<response/>")
346+
347+
mock_session.post = AsyncMock(
348+
side_effect=[
349+
aiohttp.ServerDisconnectedError("Server disconnected"),
350+
mock_response,
351+
]
352+
)
353+
354+
# This should succeed after retry
355+
result = await transport.post_xml("http://example.com/onvif", mock_envelope, {})
356+
357+
# Should be called twice (initial + retry)
358+
assert mock_session.post.call_count == 2
359+
assert result.status_code == 200
360+
361+
362+
@pytest.mark.asyncio
363+
async def test_post_xml_decorator_is_applied():
364+
"""Verify that the post_xml method has the retry decorator applied."""
365+
366+
# Check that AsyncTransportProtocolErrorHandler.post_xml has the decorator
367+
import inspect
368+
369+
# The decorated function will have been wrapped
370+
# Check if the function has the expected decorator behavior
371+
mock_session = Mock(spec=ClientSession)
372+
mock_session.timeout = Mock(total=30, sock_read=10)
373+
374+
transport = AsyncTransportProtocolErrorHandler(
375+
session=mock_session, verify_ssl=False
376+
)
377+
378+
# Get the actual method
379+
post_xml_method = transport.post_xml
380+
381+
# Check if it's wrapped (the wrapper will have different attributes than the original)
382+
# The retry decorator wrapper should be a coroutine
383+
assert inspect.iscoroutinefunction(post_xml_method)
384+
385+
# Verify it actually retries by testing with ServerDisconnectedError
386+
mock_envelope = Mock()
387+
mock_envelope.tag = "TestEnvelope"
388+
389+
with patch("onvif.zeep_aiohttp.etree_to_string", return_value=b"<test/>"):
390+
# Set up to fail twice (max retries)
391+
mock_session.post = AsyncMock(
392+
side_effect=aiohttp.ServerDisconnectedError("Server disconnected")
393+
)
394+
395+
# Should raise after 2 attempts
396+
with pytest.raises(aiohttp.ServerDisconnectedError):
397+
await transport.post_xml("http://example.com/onvif", mock_envelope, {})
398+
399+
# Verify it was called exactly twice (2 attempts as configured)
400+
assert mock_session.post.call_count == 2
401+
402+
403+
@pytest.mark.asyncio
404+
async def test_retry_only_for_server_disconnected():
405+
"""Test that retry only happens for ServerDisconnectedError, not other exceptions."""
406+
407+
mock_session = Mock(spec=ClientSession)
408+
mock_session.timeout = Mock(total=30, sock_read=10)
409+
410+
transport = AsyncTransportProtocolErrorHandler(
411+
session=mock_session, verify_ssl=False
412+
)
413+
414+
mock_envelope = Mock()
415+
mock_envelope.tag = "TestEnvelope"
416+
417+
with patch("onvif.zeep_aiohttp.etree_to_string", return_value=b"<test/>"):
418+
# Different error type should not retry
419+
mock_session.post = AsyncMock(
420+
side_effect=aiohttp.ClientError("Different error")
421+
)
422+
423+
with pytest.raises(aiohttp.ClientError) as exc_info:
424+
await transport.post_xml("http://example.com/onvif", mock_envelope, {})
425+
426+
assert str(exc_info.value) == "Different error"
427+
# Should only be called once (no retry for other errors)
428+
assert mock_session.post.call_count == 1

0 commit comments

Comments
 (0)