test_provider_dnspod.py 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557
  1. # coding=utf-8
  2. """
  3. DNSPod Provider 单元测试
  4. 支持 Python 2.7 和 Python 3
  5. """
  6. from base_test import BaseProviderTestCase, unittest, patch, MagicMock
  7. from ddns.provider.dnspod import DnspodProvider
  8. class TestDnspodProvider(BaseProviderTestCase):
  9. """DNSPod Provider 测试类"""
  10. def setUp(self):
  11. """测试初始化"""
  12. super(TestDnspodProvider, self).setUp()
  13. self.provider = DnspodProvider(self.id, self.token)
  14. def test_init_with_basic_config(self):
  15. """Test DnspodProvider initialization with basic configuration"""
  16. provider = DnspodProvider(self.id, self.token)
  17. self.assertEqual(provider.id, self.id)
  18. self.assertEqual(provider.token, self.token)
  19. self.assertEqual(provider.endpoint, "https://dnsapi.cn")
  20. self.assertEqual(provider.DefaultLine, "默认")
  21. def test_class_constants(self):
  22. """Test DnspodProvider class constants"""
  23. self.assertEqual(DnspodProvider.endpoint, "https://dnsapi.cn")
  24. self.assertEqual(DnspodProvider.DefaultLine, "默认")
  25. # ContentType should be TYPE_FORM
  26. from ddns.provider._base import TYPE_FORM
  27. self.assertEqual(DnspodProvider.content_type, TYPE_FORM)
  28. @patch("ddns.provider.dnspod.DnspodProvider._http")
  29. def test_request_success(self, mock_http):
  30. """Test _request method with successful response"""
  31. mock_response = {"status": {"code": "1", "message": "Success"}, "data": {"test": "value"}}
  32. mock_http.return_value = mock_response
  33. result = self.provider._request("Test.Action", test_param="test_value")
  34. self.assertEqual(result, mock_response)
  35. mock_http.assert_called_once()
  36. # Verify request parameters
  37. call_args = mock_http.call_args
  38. self.assertEqual(call_args[0][0], "POST") # Method
  39. self.assertEqual(call_args[0][1], "/Test.Action") # URL
  40. # Verify body contains login token and format
  41. body = call_args[1]["body"]
  42. self.assertIn("login_token", body)
  43. expected_token = "{0},{1}".format(self.id, self.token)
  44. self.assertEqual(body["login_token"], expected_token)
  45. self.assertEqual(body["format"], "json")
  46. self.assertEqual(body["test_param"], "test_value")
  47. @patch("ddns.provider.dnspod.DnspodProvider._http")
  48. def test_request_failure(self, mock_http):
  49. """Test _request method with failed response"""
  50. mock_response = {"status": {"code": "0", "message": "API Error"}}
  51. mock_http.return_value = mock_response
  52. # Mock logger to capture warning
  53. self.provider.logger = MagicMock()
  54. result = self.provider._request("Test.Action")
  55. self.assertEqual(result, mock_response)
  56. self.provider.logger.warning.assert_called_once()
  57. @patch("ddns.provider.dnspod.DnspodProvider._http")
  58. def test_request_filters_none_params(self, mock_http):
  59. """Test _request method filters out None parameters"""
  60. mock_response = {"status": {"code": "1"}}
  61. mock_http.return_value = mock_response
  62. self.provider._request("Test.Action", param1="value1", param2=None, param3="value3")
  63. body = mock_http.call_args[1]["body"]
  64. self.assertEqual(body["param1"], "value1")
  65. self.assertEqual(body["param3"], "value3")
  66. self.assertNotIn("param2", body)
  67. @patch("ddns.provider.dnspod.DnspodProvider._http")
  68. def test_request_with_extra_params(self, mock_http):
  69. """Test _request method with extra parameters"""
  70. mock_response = {"status": {"code": "1"}}
  71. mock_http.return_value = mock_response
  72. extra = {"extra_param": "extra_value"}
  73. self.provider._request("Test.Action", extra=extra, normal_param="normal_value")
  74. # Verify both extra and normal params are included
  75. body = mock_http.call_args[1]["body"]
  76. self.assertEqual(body["extra_param"], "extra_value")
  77. self.assertEqual(body["normal_param"], "normal_value")
  78. @patch("ddns.provider.dnspod.DnspodProvider._http")
  79. def test_query_zone_id_success(self, mock_http):
  80. """Test _query_zone_id method with successful response"""
  81. mock_http.return_value = {"domain": {"id": "12345", "name": "example.com"}}
  82. zone_id = self.provider._query_zone_id("example.com")
  83. self.assertEqual(zone_id, "12345")
  84. mock_http.assert_called_once()
  85. # Verify the action was correct
  86. call_args = mock_http.call_args
  87. self.assertEqual(call_args[0][1], "/Domain.Info")
  88. @patch("ddns.provider.dnspod.DnspodProvider._http")
  89. def test_query_zone_id_not_found(self, mock_http):
  90. """Test _query_zone_id method when domain is not found"""
  91. mock_http.return_value = {}
  92. zone_id = self.provider._query_zone_id("notfound.com")
  93. self.assertIsNone(zone_id)
  94. @patch("ddns.provider.dnspod.DnspodProvider._request")
  95. def test_query_record_success_single(self, mock_request):
  96. """Test _query_record method with single record found"""
  97. mock_request.return_value = {"records": [{"id": "123", "name": "www", "value": "192.168.1.1", "type": "A"}]}
  98. record = self.provider._query_record("zone123", "www", "example.com", "A", None, {})
  99. self.assertIsNotNone(record)
  100. if record:
  101. self.assertEqual(record["id"], "123")
  102. self.assertEqual(record["name"], "www")
  103. mock_request.assert_called_once_with(
  104. "Record.List", domain_id="zone123", sub_domain="www", record_type="A", line=None
  105. )
  106. @patch("ddns.provider.dnspod.DnspodProvider._request")
  107. def test_query_record_success_multiple(self, mock_request):
  108. """Test _query_record method with multiple records found"""
  109. mock_request.return_value = {
  110. "records": [
  111. {"id": "123", "name": "www", "value": "192.168.1.1", "type": "A"},
  112. {"id": "124", "name": "ftp", "value": "192.168.1.2", "type": "A"},
  113. ]
  114. }
  115. # Mock logger
  116. self.provider.logger = MagicMock()
  117. record = self.provider._query_record("zone123", "www", "example.com", "A", None, {})
  118. self.assertIsNotNone(record)
  119. self.assertEqual(record["name"], "www") # type: ignore[unreachable]
  120. # Should log warning for multiple records
  121. self.provider.logger.warning.assert_called_once()
  122. @patch("ddns.provider.dnspod.DnspodProvider._request")
  123. def test_query_record_not_found(self, mock_request):
  124. """Test _query_record method when no records found"""
  125. mock_request.return_value = {"records": []}
  126. # Mock logger
  127. self.provider.logger = MagicMock()
  128. record = self.provider._query_record("zone123", "notfound", "example.com", "A", None, {})
  129. self.assertIsNone(record)
  130. self.provider.logger.warning.assert_called_once()
  131. @patch("ddns.provider.dnspod.DnspodProvider._request")
  132. def test_create_record_success(self, mock_request):
  133. """Test _create_record method with successful creation"""
  134. mock_request.return_value = {"record": {"id": "12345", "name": "www", "value": "192.168.1.1"}}
  135. # Mock logger
  136. self.provider.logger = MagicMock()
  137. result = self.provider._create_record(
  138. "zone123", "www", "example.com", "192.168.1.1", "A", ttl=600, line="电信", extra={}
  139. )
  140. self.assertTrue(result)
  141. self.provider.logger.info.assert_called_once()
  142. mock_request.assert_called_once_with(
  143. "Record.Create",
  144. extra={},
  145. domain_id="zone123",
  146. sub_domain="www",
  147. value="192.168.1.1",
  148. record_type="A",
  149. record_line="电信",
  150. ttl=600,
  151. )
  152. @patch("ddns.provider.dnspod.DnspodProvider._request")
  153. def test_create_record_with_default_line(self, mock_request):
  154. """Test _create_record method with default line"""
  155. mock_request.return_value = {"record": {"id": "12345", "name": "www", "value": "192.168.1.1"}}
  156. result = self.provider._create_record("zone123", "www", "example.com", "192.168.1.1", "A", None, None, {})
  157. self.assertTrue(result)
  158. # Should use DefaultLine when line is not specified
  159. call_args = mock_request.call_args[1]
  160. self.assertEqual(call_args["record_line"], "默认")
  161. @patch("ddns.provider.dnspod.DnspodProvider._request")
  162. def test_create_record_failure(self, mock_request):
  163. """Test _create_record method with failed creation"""
  164. mock_request.return_value = None
  165. # Mock logger
  166. self.provider.logger = MagicMock()
  167. result = self.provider._create_record("zone123", "www", "example.com", "192.168.1.1", "A", None, None, {})
  168. self.assertFalse(result)
  169. self.provider.logger.error.assert_called_once()
  170. @patch("ddns.provider.dnspod.DnspodProvider._request")
  171. def test_create_record_with_extra_params(self, mock_request):
  172. """Test _create_record method with extra parameters"""
  173. mock_request.return_value = {"record": {"id": "12345", "name": "www", "value": "192.168.1.1"}}
  174. extra = {"weight": 10}
  175. result = self.provider._create_record("zone123", "www", "example.com", "192.168.1.1", "A", None, None, extra)
  176. self.assertTrue(result)
  177. mock_request.assert_called_once_with(
  178. "Record.Create",
  179. extra=extra,
  180. domain_id="zone123",
  181. sub_domain="www",
  182. value="192.168.1.1",
  183. record_type="A",
  184. record_line="默认",
  185. ttl=None,
  186. )
  187. @patch("ddns.provider.dnspod.DnspodProvider._request")
  188. def test_update_record_success(self, mock_request):
  189. """Test _update_record method with successful update"""
  190. mock_request.return_value = {"record": {"id": "12345", "name": "www", "value": "192.168.1.2"}}
  191. old_record = {"id": "12345", "name": "www", "line": "电信"}
  192. # Mock logger
  193. self.provider.logger = MagicMock()
  194. result = self.provider._update_record("zone123", old_record, "192.168.1.2", "A", 300, None, {})
  195. self.assertTrue(result)
  196. self.provider.logger.debug.assert_called_once()
  197. mock_request.assert_called_once_with(
  198. "Record.Modify",
  199. domain_id="zone123",
  200. record_id="12345",
  201. sub_domain="www",
  202. record_type="A",
  203. value="192.168.1.2",
  204. record_line="电信",
  205. extra={},
  206. )
  207. @patch("ddns.provider.dnspod.DnspodProvider._request")
  208. def test_update_record_failure(self, mock_request):
  209. """Test _update_record method with failed update"""
  210. mock_request.return_value = None
  211. old_record = {"id": "12345", "name": "www"}
  212. # Mock logger
  213. self.provider.logger = MagicMock()
  214. result = self.provider._update_record("zone123", old_record, "192.168.1.2", "A", None, None, {})
  215. self.assertFalse(result)
  216. self.provider.logger.error.assert_called_once()
  217. @patch("ddns.provider.dnspod.DnspodProvider._request")
  218. def test_update_record_with_line_conversion(self, mock_request):
  219. """Test _update_record method with line conversion (Default -> default)"""
  220. mock_request.return_value = {"record": {"id": "12345", "name": "www", "value": "192.168.1.2"}}
  221. old_record = {"id": "12345", "name": "www", "line": "Default"}
  222. result = self.provider._update_record("zone123", old_record, "192.168.1.2", "A", None, None, {})
  223. self.assertTrue(result)
  224. # Should convert "Default" to "default"
  225. call_args = mock_request.call_args[1]
  226. self.assertEqual(call_args["record_line"], "default")
  227. @patch("ddns.provider.dnspod.DnspodProvider._request")
  228. def test_update_record_with_fallback_line(self, mock_request):
  229. """Test _update_record method with fallback to default line"""
  230. mock_request.return_value = {"record": {"id": "12345", "name": "www", "value": "192.168.1.2"}}
  231. old_record = {"id": "12345", "name": "www"} # No line specified
  232. result = self.provider._update_record("zone123", old_record, "192.168.1.2", "A", None, None, {})
  233. self.assertTrue(result)
  234. # Should use DefaultLine when old record has no line
  235. call_args = mock_request.call_args[1]
  236. self.assertEqual(call_args["record_line"], "默认")
  237. @patch("ddns.provider.dnspod.DnspodProvider._request")
  238. def test_update_record_with_extra_params(self, mock_request):
  239. """Test _update_record method with extra parameters"""
  240. mock_request.return_value = {"record": {"id": "12345", "name": "www", "value": "192.168.1.2"}}
  241. old_record = {"id": "12345", "name": "www", "line": "电信"}
  242. extra = {"weight": 20}
  243. result = self.provider._update_record("zone123", old_record, "192.168.1.2", "A", None, None, extra)
  244. self.assertTrue(result)
  245. call_args = mock_request.call_args[1]
  246. self.assertEqual(call_args["extra"], extra)
  247. @patch("ddns.provider.dnspod.DnspodProvider._request")
  248. def test_update_record_extra_priority_over_old_record(self, mock_request):
  249. """Test that extra parameters take priority over old_record values"""
  250. mock_request.return_value = {"record": {"id": "12345", "name": "www", "value": "192.168.1.2"}}
  251. old_record = {"id": "12345", "name": "www", "line": "电信", "weight": 10}
  252. # extra should override old_record's weight
  253. extra = {"weight": 20, "mx": 5}
  254. result = self.provider._update_record("zone123", old_record, "192.168.1.2", "A", None, None, extra)
  255. self.assertTrue(result)
  256. call_args = mock_request.call_args[1]
  257. self.assertEqual(call_args["extra"], extra)
  258. # Verify extra contains the new weight value
  259. self.assertEqual(call_args["extra"]["weight"], 20)
  260. self.assertEqual(call_args["extra"]["mx"], 5)
  261. def test_request_with_none_response(self):
  262. """Test _request method when HTTP returns None"""
  263. with patch("ddns.provider.dnspod.DnspodProvider._http") as mock_http:
  264. mock_http.return_value = None
  265. self.provider.logger = MagicMock()
  266. # Should return None and log a warning
  267. result = self.provider._request("Test.Action")
  268. self.assertIsNone(result)
  269. # Verify warning was logged
  270. self.provider.logger.warning.assert_called_once()
  271. def test_create_record_with_no_record_in_response(self):
  272. """Test _create_record method when response has no record field"""
  273. with patch("ddns.provider.dnspod.DnspodProvider._request") as mock_request:
  274. mock_request.return_value = {"status": {"code": "1"}} # No record field
  275. self.provider.logger = MagicMock()
  276. result = self.provider._create_record("zone123", "www", "example.com", "192.168.1.1", "A", None, None, {})
  277. self.assertFalse(result)
  278. self.provider.logger.error.assert_called_once()
  279. def test_update_record_with_no_record_in_response(self):
  280. """Test _update_record method when response has no record field"""
  281. with patch("ddns.provider.dnspod.DnspodProvider._request") as mock_request:
  282. mock_request.return_value = {"status": {"code": "1"}} # No record field
  283. old_record = {"id": "12345", "name": "www"}
  284. self.provider.logger = MagicMock()
  285. result = self.provider._update_record("zone123", old_record, "192.168.1.2", "A", None, None, {})
  286. self.assertFalse(result)
  287. self.provider.logger.error.assert_called_once()
  288. def test_line_configuration_support(self):
  289. """Test that DnspodProvider supports line configuration"""
  290. with patch("ddns.provider.dnspod.DnspodProvider._request") as mock_request:
  291. mock_request.return_value = {"record": {"id": "12345", "name": "www", "value": "192.168.1.1"}}
  292. # Test create record with line parameter
  293. result = self.provider._create_record("zone123", "www", "example.com", "192.168.1.1", "A", 600, "电信", {})
  294. self.assertTrue(result)
  295. mock_request.assert_called_once_with(
  296. "Record.Create",
  297. extra={},
  298. domain_id="zone123",
  299. sub_domain="www",
  300. value="192.168.1.1",
  301. record_type="A",
  302. record_line="电信",
  303. ttl=600,
  304. )
  305. def test_update_record_with_custom_line(self):
  306. """Test _update_record method with custom line parameter"""
  307. with patch("ddns.provider.dnspod.DnspodProvider._request") as mock_request:
  308. mock_request.return_value = {"record": {"id": "12345", "name": "www", "value": "192.168.1.2"}}
  309. old_record = {"id": "12345", "name": "www", "line": "默认"}
  310. # Test with custom line parameter
  311. result = self.provider._update_record("zone123", old_record, "192.168.1.2", "A", 300, "联通", {})
  312. self.assertTrue(result)
  313. call_args = mock_request.call_args[1]
  314. self.assertEqual(call_args["record_line"], "联通")
  315. class TestDnspodProviderIntegration(BaseProviderTestCase):
  316. """DNSPod Provider 集成测试类"""
  317. def setUp(self):
  318. """测试初始化"""
  319. super(TestDnspodProviderIntegration, self).setUp()
  320. self.provider = DnspodProvider(self.id, self.token)
  321. self.provider.logger = MagicMock()
  322. @patch("ddns.provider.dnspod.DnspodProvider._http")
  323. def test_full_workflow_create_record(self, mock_http):
  324. """Test complete workflow for creating a new record"""
  325. # Mock HTTP responses for the workflow
  326. responses = [
  327. # Domain.Info response
  328. {"status": {"code": "1"}, "domain": {"id": "zone123"}},
  329. # Record.List response (no existing records)
  330. {"status": {"code": "1"}, "records": []},
  331. # Record.Create response
  332. {"status": {"code": "1"}, "record": {"id": "rec123", "name": "www", "value": "192.168.1.1"}},
  333. ]
  334. mock_http.side_effect = responses
  335. result = self.provider.set_record("www.example.com", "192.168.1.1")
  336. self.assertTrue(result)
  337. self.assertEqual(mock_http.call_count, 3)
  338. @patch("ddns.provider.dnspod.DnspodProvider._http")
  339. def test_full_workflow_update_record(self, mock_http):
  340. """Test complete workflow for updating an existing record"""
  341. # Mock HTTP responses for the workflow
  342. responses = [
  343. # Domain.Info response
  344. {"status": {"code": "1"}, "domain": {"id": "zone123"}},
  345. # Record.List response (existing record found)
  346. {
  347. "status": {"code": "1"},
  348. "records": [{"id": "rec123", "name": "www", "value": "192.168.1.100", "line": "默认"}],
  349. },
  350. # Record.Modify response
  351. {"status": {"code": "1"}, "record": {"id": "rec123", "name": "www", "value": "192.168.1.1"}},
  352. ]
  353. mock_http.side_effect = responses
  354. result = self.provider.set_record("www.example.com", "192.168.1.1")
  355. self.assertTrue(result)
  356. self.assertEqual(mock_http.call_count, 3)
  357. @patch("ddns.provider.dnspod.DnspodProvider._http")
  358. def test_full_workflow_zone_not_found(self, mock_http):
  359. """Test complete workflow when zone is not found"""
  360. # Domain.Info response - no domain found
  361. mock_http.return_value = {"status": {"code": "0", "message": "Domain not found"}}
  362. # Should return False when zone not found
  363. result = self.provider.set_record("www.notfound.com", "192.168.1.1")
  364. self.assertFalse(result)
  365. @patch("ddns.provider.dnspod.DnspodProvider._http")
  366. def test_full_workflow_create_failure(self, mock_http):
  367. """Test complete workflow when record creation fails"""
  368. responses = [
  369. # Domain.Info response
  370. {"status": {"code": "1"}, "domain": {"id": "zone123"}},
  371. # Record.List response (no existing records)
  372. {"status": {"code": "1"}, "records": []},
  373. # Record.Create response (failure)
  374. {"status": {"code": "0", "message": "Create failed"}},
  375. ]
  376. mock_http.side_effect = responses
  377. result = self.provider.set_record("www.example.com", "192.168.1.1")
  378. self.assertFalse(result)
  379. self.assertEqual(mock_http.call_count, 3)
  380. @patch("ddns.provider.dnspod.DnspodProvider._http")
  381. def test_full_workflow_update_failure(self, mock_http):
  382. """Test complete workflow when record update fails"""
  383. responses = [
  384. # Domain.Info response
  385. {"status": {"code": "1"}, "domain": {"id": "zone123"}},
  386. # Record.List response (existing record found)
  387. {
  388. "status": {"code": "1"},
  389. "records": [{"id": "rec123", "name": "www", "value": "192.168.1.100", "line": "默认"}],
  390. },
  391. # Record.Modify response (failure)
  392. {"status": {"code": "0", "message": "Update failed"}},
  393. ]
  394. mock_http.side_effect = responses
  395. result = self.provider.set_record("www.example.com", "192.168.1.1")
  396. self.assertFalse(result)
  397. self.assertEqual(mock_http.call_count, 3)
  398. @patch("ddns.provider.dnspod.DnspodProvider._http")
  399. def test_full_workflow_with_options(self, mock_http):
  400. """Test complete workflow with additional options like ttl and line"""
  401. responses = [
  402. # Domain.Info response
  403. {"status": {"code": "1"}, "domain": {"id": "zone123"}},
  404. # Record.List response (no existing records)
  405. {"status": {"code": "1"}, "records": []},
  406. # Record.Create response
  407. {"status": {"code": "1"}, "record": {"id": "rec123", "name": "www", "value": "192.168.1.1"}},
  408. ]
  409. mock_http.side_effect = responses
  410. result = self.provider.set_record("www.example.com", "192.168.1.1", record_type="A", ttl=300, line="电信")
  411. self.assertTrue(result)
  412. self.assertEqual(mock_http.call_count, 3)
  413. # Verify the Record.Create call includes the custom options
  414. create_call = mock_http.call_args_list[2]
  415. body = create_call[1]["body"]
  416. self.assertEqual(body["ttl"], 300)
  417. self.assertEqual(body["record_line"], "电信")
  418. class TestDnspodProviderRealRequest(BaseProviderTestCase):
  419. """DNSPod Provider 真实请求测试类"""
  420. def test_auth_failure_real_request(self):
  421. """Test authentication failure with real API request"""
  422. # 使用无效的认证信息创建 provider
  423. invalid_provider = DnspodProvider("invalid_id", "invalid_token")
  424. # 尝试查询域名信息,应该抛出认证失败异常
  425. with self.assertRaises(RuntimeError) as cm:
  426. invalid_provider._query_zone_id("example.com")
  427. # 验证异常信息包含认证失败 - 更新错误消息格式
  428. error_message = str(cm.exception)
  429. self.assertTrue(
  430. "认证失败" in error_message and "401" in error_message,
  431. "Expected authentication error message not found in: {}".format(error_message),
  432. )
  433. if __name__ == "__main__":
  434. unittest.main()