1
0

test_util_http_retry.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461
  1. # coding=utf-8
  2. """
  3. 测试 HTTP RetryHandler 重试功能
  4. Test HTTP RetryHandler retry functionality
  5. """
  6. from __future__ import unicode_literals
  7. import socket
  8. import ssl
  9. from __init__ import unittest, patch, MagicMock
  10. import logging
  11. # Python 2/3 compatibility
  12. try:
  13. from io import StringIO
  14. from urllib.error import URLError
  15. except ImportError:
  16. from StringIO import StringIO # type: ignore[no-redef]
  17. from urllib2 import URLError # type: ignore[no-redef]
  18. from ddns.util.http import RetryHandler, request
  19. class TestRetryHandler(unittest.TestCase):
  20. """测试 RetryHandler 类"""
  21. def setUp(self):
  22. """设置测试,创建retries=2的RetryHandler(会重试2次,总共最多3次请求)"""
  23. self.retry_handler = RetryHandler(retries=2)
  24. def test_init_default(self):
  25. """测试默认初始化"""
  26. handler = RetryHandler()
  27. self.assertEqual(handler.retries, 3)
  28. self.assertEqual(handler.RETRY_CODES, (408, 429, 500, 502, 503, 504))
  29. def test_init_custom(self):
  30. """测试自定义初始化"""
  31. handler = RetryHandler(retries=5)
  32. self.assertEqual(handler.retries, 5)
  33. self.assertEqual(handler.RETRY_CODES, (408, 429, 500, 502, 503, 504))
  34. def test_retry_codes_default(self):
  35. """测试默认重试状态码"""
  36. expected_codes = (408, 429, 500, 502, 503, 504)
  37. self.assertEqual(RetryHandler.RETRY_CODES, expected_codes)
  38. @patch("time.sleep")
  39. def test_network_error_retry(self, mock_sleep):
  40. """测试网络错误重试"""
  41. # 设置父级opener
  42. mock_parent = MagicMock()
  43. self.retry_handler.parent = mock_parent
  44. # 模拟request对象
  45. mock_req = MagicMock()
  46. mock_req.timeout = 30
  47. # 模拟响应
  48. mock_response = MagicMock()
  49. mock_response.getcode.return_value = 200
  50. mock_response.read.return_value = b"success"
  51. # 第一次失败,第二次成功 (retries=2,允许重试2次,总共最多3次请求)
  52. mock_parent.open.side_effect = [socket.timeout, mock_response]
  53. # 执行测试
  54. result = self.retry_handler._open(mock_req)
  55. # 验证重试次数
  56. self.assertEqual(mock_parent.open.call_count, 2)
  57. self.assertEqual(result, mock_response)
  58. # 验证sleep调用次数 (第一次失败后会sleep)
  59. self.assertEqual(mock_sleep.call_count, 1)
  60. @patch("time.sleep")
  61. def test_http_error_retry(self, mock_sleep):
  62. """测试HTTP错误重试"""
  63. # 设置父级opener
  64. mock_parent = MagicMock()
  65. self.retry_handler.parent = mock_parent
  66. # 模拟request对象
  67. mock_req = MagicMock()
  68. mock_req.timeout = 30
  69. # 模拟HTTP 500错误响应
  70. mock_error_response = MagicMock()
  71. mock_error_response.getcode.return_value = 500
  72. # 模拟成功响应
  73. mock_success_response = MagicMock()
  74. mock_success_response.getcode.return_value = 200
  75. # 第一次返回500错误,第二次成功 (retries=2,允许重试2次,总共最多3次请求)
  76. mock_parent.open.side_effect = [mock_error_response, mock_success_response]
  77. # 执行测试
  78. result = self.retry_handler._open(mock_req)
  79. # 验证重试次数
  80. self.assertEqual(mock_parent.open.call_count, 2)
  81. self.assertEqual(result, mock_success_response)
  82. # 验证sleep调用 (第一次失败后会sleep)
  83. mock_sleep.assert_called_once()
  84. def test_non_retryable_error_immediate_failure(self):
  85. """测试非重试错误立即失败"""
  86. # 设置父级opener
  87. mock_parent = MagicMock()
  88. self.retry_handler.parent = mock_parent
  89. # 模拟request对象
  90. mock_req = MagicMock()
  91. mock_req.timeout = 30
  92. # 模拟不可重试的异常
  93. mock_parent.open.side_effect = ValueError("Non-retryable error")
  94. # 执行测试,期望异常被抛出
  95. with self.assertRaises(ValueError):
  96. self.retry_handler._open(mock_req)
  97. # 验证只调用一次,没有重试
  98. self.assertEqual(mock_parent.open.call_count, 1)
  99. @patch("time.sleep")
  100. def test_max_retries_exceeded(self, mock_sleep):
  101. """测试超过最大重试次数"""
  102. # 设置父级opener
  103. mock_parent = MagicMock()
  104. self.retry_handler.parent = mock_parent
  105. # 模拟request对象
  106. mock_req = MagicMock()
  107. mock_req.timeout = 30
  108. # 所有请求都失败 (retries=2,会重试2次,总共3次请求: attempts 1, 2, 3)
  109. # 修复后的RetryHandler会正确抛出最后的异常
  110. mock_parent.open.side_effect = [socket.gaierror, socket.timeout, socket.timeout]
  111. # 执行测试,期望最后的异常被抛出
  112. with self.assertRaises(socket.timeout):
  113. self.retry_handler._open(mock_req)
  114. # 验证重试次数 (retries=2,总共3次请求)
  115. self.assertEqual(mock_parent.open.call_count, 3)
  116. # 验证sleep调用次数 (前两次失败后会sleep)
  117. self.assertEqual(mock_sleep.call_count, 2)
  118. def test_zero_retries_init(self):
  119. """测试0次重试初始化"""
  120. handler = RetryHandler(retries=0)
  121. self.assertEqual(handler.retries, 0)
  122. self.assertFalse(hasattr(handler, "default_open"))
  123. @patch("time.sleep")
  124. def test_zero_retries_behavior(self, mock_sleep):
  125. """测试0次重试时的各种情况:网络错误、HTTP错误、成功请求"""
  126. # 测试1: 网络错误立即失败
  127. handler = RetryHandler(retries=0)
  128. mock_parent = MagicMock()
  129. handler.parent = mock_parent
  130. mock_req = MagicMock()
  131. mock_parent.open.side_effect = socket.timeout("Connection timeout")
  132. with self.assertRaises(socket.timeout):
  133. handler._open(mock_req)
  134. self.assertEqual(mock_parent.open.call_count, 1)
  135. mock_sleep.assert_not_called()
  136. # 测试2: HTTP错误立即返回 (新handler实例)
  137. handler2 = RetryHandler(retries=0)
  138. mock_parent2 = MagicMock()
  139. handler2.parent = mock_parent2
  140. mock_req2 = MagicMock()
  141. mock_error_response = MagicMock()
  142. mock_error_response.getcode.return_value = 500
  143. mock_parent2.open.return_value = mock_error_response
  144. result = handler2._open(mock_req2)
  145. self.assertEqual(mock_parent2.open.call_count, 1)
  146. self.assertEqual(result, mock_error_response)
  147. # 测试3: 成功请求 (新handler实例)
  148. handler3 = RetryHandler(retries=0)
  149. mock_parent3 = MagicMock()
  150. handler3.parent = mock_parent3
  151. mock_req3 = MagicMock()
  152. mock_success_response = MagicMock()
  153. mock_success_response.getcode.return_value = 200
  154. mock_parent3.open.return_value = mock_success_response
  155. result = handler3._open(mock_req3)
  156. self.assertEqual(mock_parent3.open.call_count, 1)
  157. self.assertEqual(result, mock_success_response)
  158. class TestRequestFunction(unittest.TestCase):
  159. """测试新的 request 函数"""
  160. @patch("ddns.util.http.build_opener")
  161. def test_request_with_retry(self, mock_build_opener):
  162. """测试带重试功能的request函数"""
  163. # Mock response
  164. mock_response = MagicMock()
  165. mock_response.getcode.return_value = 200
  166. mock_response.info.return_value = {}
  167. mock_response.read.return_value = b'{"success": true}'
  168. mock_response.msg = "OK"
  169. # Mock opener
  170. mock_opener = MagicMock()
  171. mock_opener.open.return_value = mock_response
  172. mock_build_opener.return_value = mock_opener
  173. # 测试调用
  174. result = request("GET", "http://example.com", retries=2)
  175. # 验证返回结果
  176. self.assertEqual(result.status, 200)
  177. self.assertEqual(result.body, '{"success": true}')
  178. # 验证build_opener被调用,并且包含RetryHandler
  179. mock_build_opener.assert_called_once()
  180. args = mock_build_opener.call_args[0]
  181. # 在Python 2中,检查实际的类名需要使用__class__.__name__
  182. handler_types = [getattr(handler, "__class__", type(handler)).__name__ for handler in args]
  183. self.assertIn("RetryHandler", handler_types)
  184. @patch("ddns.util.http.build_opener")
  185. def test_request_with_proxy(self, mock_build_opener):
  186. """测试request函数的代理功能"""
  187. mock_response = MagicMock()
  188. mock_response.getcode.return_value = 200
  189. mock_response.info.return_value = {}
  190. mock_response.read.return_value = b"test"
  191. mock_response.msg = "OK"
  192. mock_opener = MagicMock()
  193. mock_opener.open.return_value = mock_response
  194. mock_build_opener.return_value = mock_opener
  195. # 测试代理字符串
  196. proxy_string = "http://proxy:8080"
  197. result = request("GET", "http://example.com", proxies=[proxy_string])
  198. self.assertEqual(result.status, 200)
  199. mock_build_opener.assert_called_once()
  200. @patch("time.sleep")
  201. def test_retry_handler_backoff_delays(self, mock_sleep):
  202. """测试 RetryHandler 的指数退避延迟"""
  203. # 直接测试 RetryHandler 而不是通过 request() 函数
  204. retry_handler = RetryHandler(retries=3)
  205. # 设置父级opener
  206. mock_parent = MagicMock()
  207. retry_handler.parent = mock_parent
  208. # 模拟request对象
  209. mock_req = MagicMock()
  210. mock_req.timeout = 30
  211. # 创建模拟的响应对象
  212. mock_response_1 = MagicMock()
  213. mock_response_1.getcode.return_value = 500
  214. mock_response_2 = MagicMock()
  215. mock_response_2.getcode.return_value = 500
  216. mock_response_3 = MagicMock()
  217. mock_response_3.getcode.return_value = 200
  218. # 设置parent.open的返回值序列:前两次500错误,第三次200成功
  219. mock_parent.open.side_effect = [mock_response_1, mock_response_2, mock_response_3]
  220. # 执行测试
  221. result = retry_handler._open(mock_req)
  222. # 验证返回成功响应
  223. self.assertEqual(result, mock_response_3)
  224. # 验证 time.sleep 被调用的次数和参数
  225. # 基于RetryHandler实现:attempt从1开始,延迟是2^attempt
  226. # 第一次失败(attempt=1)后sleep(2^1=2),第二次失败(attempt=2)后sleep(2^2=4)
  227. expected_delays = [2, 4] # 对应2^1, 2^2
  228. actual_delays = [call[0][0] for call in mock_sleep.call_args_list]
  229. self.assertEqual(actual_delays, expected_delays)
  230. @patch("ddns.util.http.build_opener")
  231. def test_default_retry_counts(self, mock_build_opener):
  232. """测试默认重试次数"""
  233. mock_response = MagicMock()
  234. mock_response.getcode.return_value = 200
  235. mock_response.info.return_value = {}
  236. mock_response.read.return_value = b"test"
  237. mock_response.msg = "OK"
  238. mock_opener = MagicMock()
  239. mock_opener.open.return_value = mock_response
  240. mock_build_opener.return_value = mock_opener
  241. # 测试默认重试次数
  242. request("GET", "http://example.com")
  243. # 验证RetryHandler被创建
  244. mock_build_opener.assert_called_once()
  245. args = mock_build_opener.call_args[0]
  246. handler_types = [getattr(handler, "__class__", type(handler)).__name__ for handler in args]
  247. self.assertIn("RetryHandler", handler_types)
  248. # 测试自定义重试次数
  249. mock_build_opener.reset_mock()
  250. request("GET", "http://example.com", retries=5)
  251. # 验证RetryHandler被创建
  252. mock_build_opener.assert_called_once()
  253. args = mock_build_opener.call_args[0]
  254. handler_types = [getattr(handler, "__class__", type(handler)).__name__ for handler in args]
  255. self.assertIn("RetryHandler", handler_types)
  256. @patch("ddns.util.http.build_opener")
  257. def test_request_with_zero_retries(self, mock_build_opener):
  258. """测试request函数设置0次重试"""
  259. mock_response = MagicMock()
  260. mock_response.getcode.return_value = 200
  261. mock_response.info.return_value = {}
  262. mock_response.read.return_value = b"test"
  263. mock_response.msg = "OK"
  264. mock_opener = MagicMock()
  265. mock_opener.open.return_value = mock_response
  266. mock_build_opener.return_value = mock_opener
  267. # 测试0次重试
  268. result = request("GET", "http://example.com", retries=0)
  269. # 验证请求成功
  270. self.assertEqual(result.status, 200)
  271. self.assertEqual(result.body, "test")
  272. # 验证RetryHandler被创建(即使是0次重试也会创建,但不会有default_open)
  273. mock_build_opener.assert_called_once()
  274. args = mock_build_opener.call_args[0]
  275. handler_types = [getattr(handler, "__class__", type(handler)).__name__ for handler in args]
  276. self.assertIn("RetryHandler", handler_types)
  277. class TestHttpRetryRealNetwork(unittest.TestCase):
  278. """测试HTTP重试功能 - 真实网络请求"""
  279. def test_http_502_retry_auto(self):
  280. """测试HTTP 502状态码的重试机制 - 使用真实请求检查日志"""
  281. # 创建日志捕获器
  282. log_capture = StringIO()
  283. handler = logging.StreamHandler(log_capture)
  284. handler.setLevel(logging.WARNING)
  285. # 获取根logger并设置 - 这样可以捕获所有子logger的日志
  286. root_logger = logging.getLogger()
  287. original_level = root_logger.level
  288. original_handlers = root_logger.handlers[:]
  289. try:
  290. # 设置日志级别和处理器
  291. root_logger.setLevel(logging.WARNING)
  292. root_logger.handlers = [handler]
  293. # 确保logger会传播到我们的handler
  294. root_logger.propagate = True
  295. # 使用httpbin.org的502错误端点测试重试
  296. response = request("GET", "http://postman-echo.com/status/502", retries=1)
  297. # 验证最终返回502错误
  298. self.assertEqual(response.status, 502)
  299. # 检查日志输出
  300. log_output = log_capture.getvalue()
  301. # 验证日志中包含重试信息(匹配实际的日志格式)
  302. # 在Python 2中,日志捕获可能有所不同,使用更宽松的检查
  303. self.assertIn(" retrying in 2 seconds", log_output) # 日志中应该包含重试信息
  304. retry_count = log_output.count(" error, retrying in ")
  305. self.assertEqual(retry_count, 1, "应该有一次重试日志")
  306. finally:
  307. # 恢复原始日志设置
  308. root_logger.setLevel(original_level)
  309. root_logger.handlers = original_handlers
  310. def test_ssl_certificate_error_no_retry_real_case(self):
  311. """测试SSL证书错误不触发重试 - 使用真实证书错误案例"""
  312. # 创建日志捕获器
  313. log_capture = StringIO()
  314. handler = logging.StreamHandler(log_capture)
  315. handler.setLevel(logging.DEBUG) # 使用DEBUG级别捕获更多信息
  316. # 获取根logger并设置 - 这样可以捕获所有子logger的日志
  317. root_logger = logging.getLogger()
  318. original_level = root_logger.level
  319. original_handlers = root_logger.handlers[:]
  320. try:
  321. # 设置日志级别和处理器
  322. root_logger.setLevel(logging.DEBUG)
  323. root_logger.handlers = [handler]
  324. # 使用expired.badssl.com测试过期证书错误
  325. try:
  326. # 使用过期证书的网站,强制验证证书,重试一次即可
  327. request("GET", "https://expired.badssl.com/", retries=1, verify=True)
  328. raise AssertionError("Expected SSL certificate error, but request succeeded unexpectedly.")
  329. except ssl.SSLError as e:
  330. # 这是我们期望的SSL错误
  331. # 检查日志输出
  332. log_output = log_capture.getvalue()
  333. # 验证日志中没有重试信息
  334. self.assertNotIn("retrying", log_output.lower())
  335. self.assertNotIn("retry", log_output.lower())
  336. # 验证确实是SSL证书错误
  337. self.assertIn("CERTIFICATE_VERIFY_FAILED", str(e))
  338. except (OSError, URLError) as e:
  339. # 检查是否是SSL相关错误
  340. error_msg = str(e).lower()
  341. ssl_keywords = ["ssl", "certificate", "verify", "handshake", "tls"]
  342. network_keywords = ["timeout", "connection", "resolution", "unreachable", "network"]
  343. if any(keyword in error_msg for keyword in ssl_keywords):
  344. # 这是SSL错误,检查日志输出
  345. log_output = log_capture.getvalue()
  346. # 验证日志中没有重试信息
  347. self.assertNotIn("retrying", log_output.lower())
  348. self.assertNotIn("retry", log_output.lower())
  349. # 验证确实是SSL证书错误
  350. self.assertIn("certificate", error_msg)
  351. elif any(keyword in error_msg for keyword in network_keywords):
  352. # 网络问题时跳过测试
  353. self.skipTest("Network unavailable for SSL certificate test: {}".format(str(e)))
  354. else:
  355. # 其他异常重新抛出
  356. raise
  357. finally:
  358. # 恢复原始日志设置
  359. root_logger.setLevel(original_level)
  360. root_logger.handlers = original_handlers
  361. if __name__ == "__main__":
  362. unittest.main()