test_socket_bugs.py 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160
  1. # -*- coding: utf-8 -*-
  2. import errno
  3. import socket
  4. import unittest
  5. from unittest.mock import Mock, patch
  6. from websocket._socket import recv
  7. from websocket._ssl_compat import SSLWantReadError
  8. from websocket._exceptions import (
  9. WebSocketTimeoutException,
  10. WebSocketConnectionClosedException,
  11. )
  12. """
  13. test_socket_bugs.py
  14. websocket - WebSocket client library for Python
  15. Copyright 2025 engn33r
  16. Licensed under the Apache License, Version 2.0 (the "License");
  17. you may not use this file except in compliance with the License.
  18. You may obtain a copy of the License at
  19. http://www.apache.org/licenses/LICENSE-2.0
  20. Unless required by applicable law or agreed to in writing, software
  21. distributed under the License is distributed on an "AS IS" BASIS,
  22. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  23. See the License for the specific language governing permissions and
  24. limitations under the License.
  25. """
  26. class SocketBugsTest(unittest.TestCase):
  27. """Test bugs found in socket handling logic"""
  28. def test_bug_implicit_none_return_from_ssl_want_read_fixed(self):
  29. """
  30. BUG #5 FIX VERIFICATION: Test SSLWantReadError timeout now raises correct exception
  31. Bug was in _socket.py:100-101 - SSLWantReadError except block returned None implicitly
  32. Fixed: Now properly handles timeout with WebSocketTimeoutException
  33. """
  34. mock_sock = Mock()
  35. mock_sock.recv.side_effect = SSLWantReadError()
  36. mock_sock.gettimeout.return_value = 1.0
  37. with patch("selectors.DefaultSelector") as mock_selector_class:
  38. mock_selector = Mock()
  39. mock_selector_class.return_value = mock_selector
  40. mock_selector.select.return_value = [] # Timeout - no data ready
  41. with self.assertRaises(WebSocketTimeoutException) as cm:
  42. recv(mock_sock, 100)
  43. # Verify correct timeout exception and message
  44. self.assertIn("Connection timed out waiting for data", str(cm.exception))
  45. def test_bug_implicit_none_return_from_socket_error_fixed(self):
  46. """
  47. BUG #5 FIX VERIFICATION: Test that socket.error with EAGAIN now handles timeout correctly
  48. Bug was in _socket.py:102-105 - socket.error except block returned None implicitly
  49. Fixed: Now properly handles timeout with WebSocketTimeoutException
  50. """
  51. mock_sock = Mock()
  52. # Create socket error with EAGAIN (should be retried)
  53. eagain_error = OSError(errno.EAGAIN, "Resource temporarily unavailable")
  54. # First call raises EAGAIN, selector times out on retry
  55. mock_sock.recv.side_effect = eagain_error
  56. mock_sock.gettimeout.return_value = 1.0
  57. with patch("selectors.DefaultSelector") as mock_selector_class:
  58. mock_selector = Mock()
  59. mock_selector_class.return_value = mock_selector
  60. mock_selector.select.return_value = [] # Timeout - no data ready
  61. with self.assertRaises(WebSocketTimeoutException) as cm:
  62. recv(mock_sock, 100)
  63. # Verify correct timeout exception and message
  64. self.assertIn("Connection timed out waiting for data", str(cm.exception))
  65. def test_bug_wrong_exception_for_selector_timeout_fixed(self):
  66. """
  67. BUG #6 FIX VERIFICATION: Test that selector timeout now raises correct exception type
  68. Bug was in _socket.py:115 returning None for timeout, treated as connection error
  69. Fixed: Now raises WebSocketTimeoutException directly
  70. """
  71. mock_sock = Mock()
  72. mock_sock.recv.side_effect = SSLWantReadError() # Trigger retry path
  73. mock_sock.gettimeout.return_value = 1.0
  74. with patch("selectors.DefaultSelector") as mock_selector_class:
  75. mock_selector = Mock()
  76. mock_selector_class.return_value = mock_selector
  77. mock_selector.select.return_value = [] # TIMEOUT - this is key!
  78. with self.assertRaises(WebSocketTimeoutException) as cm:
  79. recv(mock_sock, 100)
  80. # Verify it's the correct timeout exception with proper message
  81. self.assertIn("Connection timed out waiting for data", str(cm.exception))
  82. # This proves the fix works:
  83. # 1. selector.select() returns [] (timeout)
  84. # 2. _recv() now raises WebSocketTimeoutException directly
  85. # 3. No more misclassification as connection closed error!
  86. def test_socket_timeout_exception_handling(self):
  87. """
  88. Test that socket.timeout exceptions are properly handled
  89. """
  90. mock_sock = Mock()
  91. mock_sock.gettimeout.return_value = 1.0
  92. # Simulate a real socket.timeout scenario
  93. mock_sock.recv.side_effect = socket.timeout("Operation timed out")
  94. # This works correctly - socket.timeout raises WebSocketTimeoutException
  95. with self.assertRaises(WebSocketTimeoutException) as cm:
  96. recv(mock_sock, 100)
  97. # In Python 3.10+, socket.timeout is a subclass of TimeoutError
  98. # so it's caught by the TimeoutError handler with hardcoded message
  99. # In Python 3.9, socket.timeout is caught by socket.timeout handler
  100. # which preserves the original message
  101. import sys
  102. if sys.version_info >= (3, 10):
  103. self.assertIn("Connection timed out", str(cm.exception))
  104. else:
  105. self.assertIn("Operation timed out", str(cm.exception))
  106. def test_correct_ssl_want_read_retry_behavior(self):
  107. """Test the correct behavior when SSLWantReadError is properly handled"""
  108. mock_sock = Mock()
  109. # First call raises SSLWantReadError, second call succeeds
  110. mock_sock.recv.side_effect = [SSLWantReadError(), b"data after retry"]
  111. mock_sock.gettimeout.return_value = 1.0
  112. with patch("selectors.DefaultSelector") as mock_selector_class:
  113. mock_selector = Mock()
  114. mock_selector_class.return_value = mock_selector
  115. mock_selector.select.return_value = [True] # Data ready after wait
  116. # This should work correctly
  117. result = recv(mock_sock, 100)
  118. self.assertEqual(result, b"data after retry")
  119. # Selector should be used for retry
  120. mock_selector.register.assert_called()
  121. mock_selector.select.assert_called()
  122. if __name__ == "__main__":
  123. unittest.main()