test_util_http_proxy_list.py 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228
  1. # coding=utf-8
  2. """
  3. 测试 ddns.util.http 模块的代理列表功能
  4. Test ddns.util.http module proxy list functionality
  5. """
  6. from __init__ import unittest, patch, MagicMock
  7. from ddns.util.http import request, quote
  8. class TestRequestProxyList(unittest.TestCase):
  9. """测试 request 函数的代理列表功能"""
  10. @patch("ddns.util.http.build_opener")
  11. @patch("ddns.util.http.Request")
  12. def test_request_with_single_proxy_in_list(self, mock_request, mock_build_opener):
  13. """测试单个代理的列表形式"""
  14. # 模拟响应
  15. mock_response = MagicMock()
  16. mock_response.getcode.return_value = 200
  17. mock_response.info.return_value = {}
  18. mock_response.read.return_value = b'{"success": true}'
  19. mock_response.msg = "OK"
  20. mock_opener = MagicMock()
  21. mock_opener.open.return_value = mock_response
  22. mock_build_opener.return_value = mock_opener
  23. # 测试单个代理以列表形式传入
  24. result = request("GET", "http://example.com", proxies=["http://proxy:8080"])
  25. self.assertEqual(result.status, 200)
  26. self.assertEqual(result.body, '{"success": true}')
  27. mock_build_opener.assert_called_once()
  28. @patch("ddns.util.http.build_opener")
  29. @patch("ddns.util.http.Request")
  30. def test_request_with_proxy_list(self, mock_request, mock_build_opener):
  31. """测试代理列表功能"""
  32. # 模拟响应
  33. mock_response = MagicMock()
  34. mock_response.getcode.return_value = 200
  35. mock_response.info.return_value = {}
  36. mock_response.read.return_value = b'{"success": true}'
  37. mock_response.msg = "OK"
  38. mock_opener = MagicMock()
  39. mock_opener.open.return_value = mock_response
  40. mock_build_opener.return_value = mock_opener
  41. # 测试代理列表
  42. proxy_list = ["http://proxy1:8080", "http://proxy2:8080", None]
  43. result = request("GET", "http://example.com", proxies=proxy_list)
  44. self.assertEqual(result.status, 200)
  45. self.assertEqual(result.body, '{"success": true}')
  46. mock_build_opener.assert_called_once()
  47. @patch("ddns.util.http.build_opener")
  48. @patch("ddns.util.http.Request")
  49. def test_request_with_direct_connection_only(self, mock_request, mock_build_opener):
  50. """测试仅直连(proxies=[None])"""
  51. # 模拟响应
  52. mock_response = MagicMock()
  53. mock_response.getcode.return_value = 200
  54. mock_response.info.return_value = {}
  55. mock_response.read.return_value = b'{"success": true}'
  56. mock_response.msg = "OK"
  57. mock_opener = MagicMock()
  58. mock_opener.open.return_value = mock_response
  59. mock_build_opener.return_value = mock_opener
  60. # 测试直连
  61. result = request("GET", "http://example.com", proxies=None)
  62. self.assertEqual(result.status, 200)
  63. self.assertEqual(result.body, '{"success": true}')
  64. mock_build_opener.assert_called_once()
  65. @patch("ddns.util.http.build_opener")
  66. @patch("ddns.util.http.Request")
  67. def test_request_no_proxy_parameters(self, mock_request, mock_build_opener):
  68. """测试没有代理参数时默认使用直连"""
  69. # 模拟响应
  70. mock_response = MagicMock()
  71. mock_response.getcode.return_value = 200
  72. mock_response.info.return_value = {}
  73. mock_response.read.return_value = b'{"success": true}'
  74. mock_response.msg = "OK"
  75. mock_opener = MagicMock()
  76. mock_opener.open.return_value = mock_response
  77. mock_build_opener.return_value = mock_opener
  78. # 测试没有代理参数
  79. result = request("GET", "http://example.com")
  80. self.assertEqual(result.status, 200)
  81. self.assertEqual(result.body, '{"success": true}')
  82. mock_build_opener.assert_called_once()
  83. @patch("ddns.util.http.build_opener")
  84. @patch("ddns.util.http.Request")
  85. def test_request_with_empty_proxy_list(self, mock_request, mock_build_opener):
  86. """测试空代理列表时默认使用直连"""
  87. # 模拟响应
  88. mock_response = MagicMock()
  89. mock_response.getcode.return_value = 200
  90. mock_response.info.return_value = {}
  91. mock_response.read.return_value = b'{"success": true}'
  92. mock_response.msg = "OK"
  93. mock_opener = MagicMock()
  94. mock_opener.open.return_value = mock_response
  95. mock_build_opener.return_value = mock_opener
  96. # 测试空代理列表
  97. result = request("GET", "http://example.com", proxies=[])
  98. self.assertEqual(result.status, 200)
  99. self.assertEqual(result.body, '{"success": true}')
  100. mock_build_opener.assert_called_once()
  101. def test_real_network_proxy_fallback(self):
  102. """测试真实网络环境下的代理失败回退(如果网络可用)"""
  103. # 尝试多个测试端点以提高可靠性,优先使用httpbin
  104. test_endpoints = ["http://httpbin.org/get", "http://httpbingo.org/get"]
  105. last_exception = None
  106. for url in test_endpoints:
  107. try:
  108. # 使用无效代理和直连的组合
  109. proxy_list = ["http://invalid-proxy:9999", None]
  110. # 这应该在第一个代理失败后回退到直连
  111. response = request("GET", url, proxies=proxy_list, retries=3)
  112. # 如果成功,应该是通过直连完成的
  113. if response.status == 200:
  114. expected_content = "httpbin.org" if "httpbin.org" in url else "httpbingo.org"
  115. self.assertIn(expected_content, response.body)
  116. return # 成功则退出
  117. elif response.status >= 500:
  118. # 5xx错误,尝试下一个端点
  119. continue
  120. except Exception as e:
  121. last_exception = e
  122. # 网络问题时继续尝试下一个端点
  123. error_msg = str(e).lower()
  124. network_keywords = ["timeout", "connection", "resolution", "unreachable", "network"]
  125. if any(keyword in error_msg for keyword in network_keywords):
  126. continue # 尝试下一个端点
  127. else:
  128. # 其他异常重新抛出
  129. raise
  130. # 如果所有端点都失败,跳过测试
  131. error_info = " - Last error: {}".format(str(last_exception)) if last_exception else ""
  132. self.skipTest("All network endpoints unavailable for proxy fallback test{}".format(error_info))
  133. def test_basic_auth_with_embedded_url(self):
  134. """测试URL嵌入式基本认证"""
  135. from ddns.util.http import request
  136. # 使用URL编码的用户名和密码
  137. special_username = "[email protected]"
  138. special_password = "passwo.rd"
  139. username_encoded = quote(special_username, safe="")
  140. password_encoded = quote(special_password, safe="")
  141. # 验证URL编码的特殊字符
  142. self.assertEqual(username_encoded, "user%40test.com")
  143. self.assertEqual(password_encoded, "passwo.rd")
  144. # 尝试多个测试端点以提高可靠性,优先使用httpbingo
  145. test_hosts = ["httpbingo.org", "httpbin.org"]
  146. last_exception = None
  147. for host in test_hosts:
  148. try:
  149. # 构建认证URL
  150. auth_url = "https://{0}:{1}@{2}/basic-auth/{3}/{4}".format(
  151. username_encoded, password_encoded, host, username_encoded, password_encoded
  152. )
  153. response_auth = request("GET", auth_url, verify=False, retries=2)
  154. if response_auth.status == 200:
  155. self.assertIn('"auth', response_auth.body)
  156. self.assertIn("user", response_auth.body)
  157. self.assertIn(special_username, response_auth.body)
  158. return # 成功则退出
  159. elif response_auth.status >= 500:
  160. # 5xx错误,尝试下一个端点
  161. continue
  162. else:
  163. # 其他状态码也尝试下一个端点
  164. continue
  165. except Exception as e:
  166. last_exception = e
  167. # 网络问题时继续尝试下一个端点
  168. error_msg = str(e).lower()
  169. network_keywords = [
  170. "timeout",
  171. "connection",
  172. "resolution",
  173. "unreachable",
  174. "network",
  175. "ssl",
  176. "certificate",
  177. ]
  178. if any(keyword in error_msg for keyword in network_keywords):
  179. continue # 尝试下一个端点
  180. else:
  181. # 其他异常重新抛出
  182. raise
  183. # 如果所有端点都失败,跳过测试
  184. error_info = " - Last error: {}".format(str(last_exception)) if last_exception else ""
  185. self.skipTest("All network endpoints unavailable for basic auth test{}".format(error_info))
  186. if __name__ == "__main__":
  187. unittest.main()