test_provider_huaweidns.py 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482
  1. # coding=utf-8
  2. """
  3. Unit tests for HuaweiDNSProvider
  4. @author: GitHub Copilot
  5. """
  6. from base_test import BaseProviderTestCase, unittest, patch
  7. from ddns.provider.huaweidns import HuaweiDNSProvider
  8. class TestHuaweiDNSProvider(BaseProviderTestCase):
  9. """Test cases for HuaweiDNSProvider"""
  10. def setUp(self):
  11. """Set up test fixtures"""
  12. super(TestHuaweiDNSProvider, self).setUp()
  13. self.auth_id = "test_access_key"
  14. self.auth_token = "test_secret_key"
  15. self.provider = HuaweiDNSProvider(self.auth_id, self.auth_token)
  16. # Mock strftime for all tests
  17. self.strftime_patcher = patch("ddns.provider.huaweidns.strftime")
  18. self.mock_strftime = self.strftime_patcher.start()
  19. self.mock_strftime.return_value = "20230101T120000Z"
  20. def tearDown(self):
  21. """Clean up test fixtures"""
  22. self.strftime_patcher.stop()
  23. super(TestHuaweiDNSProvider, self).tearDown()
  24. def test_class_constants(self):
  25. """Test HuaweiDNSProvider class constants"""
  26. self.assertEqual(HuaweiDNSProvider.API, "https://dns.myhuaweicloud.com")
  27. self.assertEqual(HuaweiDNSProvider.content_type, "application/json")
  28. self.assertTrue(HuaweiDNSProvider.decode_response)
  29. self.assertEqual(HuaweiDNSProvider.algorithm, "SDK-HMAC-SHA256")
  30. def test_init_with_basic_config(self):
  31. """Test HuaweiDNSProvider initialization with basic configuration"""
  32. self.assertEqual(self.provider.auth_id, self.auth_id)
  33. self.assertEqual(self.provider.auth_token, self.auth_token)
  34. self.assertEqual(self.provider.API, "https://dns.myhuaweicloud.com")
  35. def test_hex_encode_sha256(self):
  36. """Test _hex_encode_sha256 method"""
  37. test_data = b"test data"
  38. result = self.provider._hex_encode_sha256(test_data)
  39. # Should return a 64-character hex string (SHA256)
  40. self.assertEqual(len(result), 64)
  41. self.assertIsInstance(result, str)
  42. # SHA256 of "test data"
  43. expected_hash = "916f0027a575074ce72a331777c3478d6513f786a591bd892da1a577bf2335f9"
  44. self.assertEqual(result, expected_hash)
  45. def test_sign_headers(self):
  46. """Test _sign_headers method"""
  47. headers = {
  48. "Content-Type": "application/json",
  49. "Host": "dns.myhuaweicloud.com",
  50. "X-Sdk-Date": "20230101T000000Z",
  51. }
  52. signed_headers = ["content-type", "host", "x-sdk-date"]
  53. result = self.provider._sign_headers(headers, signed_headers)
  54. expected = "content-type:application/json\nhost:dns.myhuaweicloud.com\nx-sdk-date:20230101T000000Z\n"
  55. self.assertEqual(result, expected)
  56. def test_request_get_method(self):
  57. """Test _request method with GET method"""
  58. with patch.object(self.provider, "_http") as mock_http:
  59. mock_http.return_value = {"zones": []}
  60. result = self.provider._request("GET", "/v2/zones", name="example.com", limit=500)
  61. mock_http.assert_called_once()
  62. self.assertEqual(result, {"zones": []})
  63. def test_request_post_method(self):
  64. """Test _request method with POST method"""
  65. with patch.object(self.provider, "_http") as mock_http:
  66. mock_http.return_value = {"id": "record123"}
  67. result = self.provider._request(
  68. "POST", "/v2.1/zones/zone123/recordsets", name="www.example.com", type="A", records=["1.2.3.4"]
  69. )
  70. mock_http.assert_called_once()
  71. self.assertEqual(result, {"id": "record123"})
  72. def test_request_filters_none_params(self):
  73. """Test _request method filters out None parameters"""
  74. with patch.object(self.provider, "_http") as mock_http:
  75. mock_http.return_value = {"zones": []}
  76. self.provider._request("GET", "/v2/zones", name="example.com", limit=None, type=None)
  77. # Verify that _http was called (None params should be filtered)
  78. mock_http.assert_called_once()
  79. def test_query_zone_id_success(self):
  80. """Test _query_zone_id method with successful response"""
  81. with patch.object(self.provider, "_request") as mock_request:
  82. mock_request.return_value = {
  83. "zones": [{"id": "zone123", "name": "example.com."}, {"id": "zone456", "name": "another.com."}]
  84. }
  85. result = self.provider._query_zone_id("example.com")
  86. mock_request.assert_called_once_with(
  87. "GET", "/v2/zones", search_mode="equal", limit=500, name="example.com."
  88. )
  89. self.assertEqual(result, "zone123")
  90. def test_query_zone_id_with_trailing_dot(self):
  91. """Test _query_zone_id method with domain already having trailing dot"""
  92. with patch.object(self.provider, "_request") as mock_request:
  93. mock_request.return_value = {"zones": [{"id": "zone123", "name": "example.com."}]}
  94. result = self.provider._query_zone_id("example.com.")
  95. mock_request.assert_called_once_with(
  96. "GET", "/v2/zones", search_mode="equal", limit=500, name="example.com."
  97. )
  98. self.assertEqual(result, "zone123")
  99. def test_query_zone_id_not_found(self):
  100. """Test _query_zone_id method when domain is not found"""
  101. with patch.object(self.provider, "_request") as mock_request:
  102. mock_request.return_value = {"zones": []}
  103. result = self.provider._query_zone_id("notfound.com")
  104. self.assertIsNone(result)
  105. def test_query_record_success(self):
  106. """Test _query_record method with successful response"""
  107. with patch.object(self.provider, "_request") as mock_request:
  108. mock_request.return_value = {
  109. "recordsets": [
  110. {"id": "rec123", "name": "www.example.com.", "type": "A", "records": ["1.2.3.4"]},
  111. {"id": "rec456", "name": "mail.example.com.", "type": "A", "records": ["5.6.7.8"]},
  112. ]
  113. }
  114. result = self.provider._query_record("zone123", "www", "example.com", "A", None, {})
  115. mock_request.assert_called_once_with(
  116. "GET",
  117. "/v2.1/zones/zone123/recordsets",
  118. limit=500,
  119. name="www.example.com.",
  120. type="A",
  121. line_id=None,
  122. search_mode="equal",
  123. )
  124. self.assertIsNotNone(result)
  125. if result: # Type narrowing
  126. self.assertEqual(result["id"], "rec123")
  127. self.assertEqual(result["name"], "www.example.com.")
  128. def test_query_record_with_line(self):
  129. """Test _query_record method with line parameter"""
  130. with patch.object(self.provider, "_request") as mock_request:
  131. mock_request.return_value = {"recordsets": []}
  132. self.provider._query_record("zone123", "www", "example.com", "A", "line1", {})
  133. mock_request.assert_called_once_with(
  134. "GET",
  135. "/v2.1/zones/zone123/recordsets",
  136. limit=500,
  137. name="www.example.com.",
  138. type="A",
  139. line_id="line1",
  140. search_mode="equal",
  141. )
  142. def test_query_record_not_found(self):
  143. """Test _query_record method when no matching record is found"""
  144. with patch.object(self.provider, "_request") as mock_request:
  145. mock_request.return_value = {
  146. "recordsets": [{"id": "rec456", "name": "mail.example.com.", "type": "A", "records": ["5.6.7.8"]}]
  147. }
  148. result = self.provider._query_record("zone123", "www", "example.com", "A", None, {})
  149. self.assertIsNone(result)
  150. def test_create_record_success(self):
  151. """Test _create_record method with successful creation"""
  152. with patch.object(self.provider, "_request") as mock_request:
  153. mock_request.return_value = {"id": "rec123456"}
  154. result = self.provider._create_record("zone123", "www", "example.com", "1.2.3.4", "A", 300, "line1", {})
  155. mock_request.assert_called_once_with(
  156. "POST",
  157. "/v2.1/zones/zone123/recordsets",
  158. name="www.example.com.",
  159. type="A",
  160. records=["1.2.3.4"],
  161. ttl=300,
  162. line="line1",
  163. description=self.provider.remark,
  164. )
  165. self.assertTrue(result)
  166. def test_create_record_with_extra_params(self):
  167. """Test _create_record method with extra parameters"""
  168. with patch.object(self.provider, "_request") as mock_request:
  169. mock_request.return_value = {"id": "rec123456"}
  170. extra = {"description": "Custom description", "tags": ["tag1", "tag2"]}
  171. result = self.provider._create_record("zone123", "www", "example.com", "1.2.3.4", "A", 300, None, extra)
  172. mock_request.assert_called_once_with(
  173. "POST",
  174. "/v2.1/zones/zone123/recordsets",
  175. name="www.example.com.",
  176. type="A",
  177. records=["1.2.3.4"],
  178. ttl=300,
  179. line=None,
  180. description="Custom description",
  181. tags=["tag1", "tag2"],
  182. )
  183. self.assertTrue(result)
  184. def test_create_record_failure(self):
  185. """Test _create_record method with failed creation"""
  186. provider = HuaweiDNSProvider(self.auth_id, self.auth_token)
  187. with patch.object(provider, "_request") as mock_request:
  188. mock_request.return_value = {"error": "Zone not found"}
  189. result = provider._create_record("zone123", "www", "example.com", "1.2.3.4", "A", None, None, {})
  190. self.assertFalse(result)
  191. def test_update_record_success(self):
  192. """Test _update_record method with successful update"""
  193. provider = HuaweiDNSProvider(self.auth_id, self.auth_token)
  194. old_record = {"id": "rec123", "name": "www.example.com.", "ttl": 300}
  195. with patch.object(provider, "_request") as mock_request:
  196. mock_request.return_value = {"id": "rec123"}
  197. result = provider._update_record("zone123", old_record, "5.6.7.8", "A", 600, None, {})
  198. mock_request.assert_called_once_with(
  199. "PUT",
  200. "/v2.1/zones/zone123/recordsets/rec123",
  201. name="www.example.com.",
  202. type="A",
  203. records=["5.6.7.8"],
  204. ttl=600,
  205. description=provider.remark,
  206. )
  207. self.assertTrue(result)
  208. def test_update_record_with_fallback_ttl(self):
  209. """Test _update_record method uses old record's TTL when ttl is None"""
  210. provider = HuaweiDNSProvider(self.auth_id, self.auth_token)
  211. old_record = {"id": "rec123", "name": "www.example.com.", "ttl": 300}
  212. with patch.object(provider, "_request") as mock_request:
  213. mock_request.return_value = {"id": "rec123"}
  214. result = provider._update_record("zone123", old_record, "5.6.7.8", "A", None, None, {})
  215. mock_request.assert_called_once_with(
  216. "PUT",
  217. "/v2.1/zones/zone123/recordsets/rec123",
  218. name="www.example.com.",
  219. type="A",
  220. records=["5.6.7.8"],
  221. ttl=300,
  222. description=provider.remark,
  223. )
  224. self.assertTrue(result)
  225. def test_update_record_with_extra_params(self):
  226. """Test _update_record method with extra parameters"""
  227. provider = HuaweiDNSProvider(self.auth_id, self.auth_token)
  228. old_record = {"id": "rec123", "name": "www.example.com.", "ttl": 300}
  229. with patch.object(provider, "_request") as mock_request:
  230. mock_request.return_value = {"id": "rec123"}
  231. extra = {"description": "Updated description", "tags": ["newtag"]}
  232. result = provider._update_record("zone123", old_record, "5.6.7.8", "A", 600, "line2", extra)
  233. mock_request.assert_called_once_with(
  234. "PUT",
  235. "/v2.1/zones/zone123/recordsets/rec123",
  236. name="www.example.com.",
  237. type="A",
  238. records=["5.6.7.8"],
  239. ttl=600,
  240. description="Updated description",
  241. tags=["newtag"],
  242. )
  243. self.assertTrue(result)
  244. def test_update_record_failure(self):
  245. """Test _update_record method with failed update"""
  246. provider = HuaweiDNSProvider(self.auth_id, self.auth_token)
  247. old_record = {"id": "rec123", "name": "www.example.com."}
  248. with patch.object(provider, "_request") as mock_request:
  249. mock_request.return_value = {"error": "Record not found"}
  250. result = provider._update_record("zone123", old_record, "5.6.7.8", "A", None, None, {})
  251. self.assertFalse(result)
  252. class TestHuaweiDNSProviderIntegration(BaseProviderTestCase):
  253. """Integration test cases for HuaweiDNSProvider - testing with minimal mocking"""
  254. def setUp(self):
  255. """Set up test fixtures"""
  256. super(TestHuaweiDNSProviderIntegration, self).setUp()
  257. self.auth_id = "test_access_key"
  258. self.auth_token = "test_secret_key"
  259. def test_full_workflow_create_new_record(self):
  260. """Test complete workflow for creating a new record"""
  261. provider = HuaweiDNSProvider(self.auth_id, self.auth_token)
  262. # Mock only the HTTP layer to simulate API responses
  263. with patch.object(provider, "_request") as mock_request:
  264. # Simulate API responses in order: zone query, record query, record creation
  265. mock_request.side_effect = [
  266. {"zones": [{"id": "zone123", "name": "example.com."}]}, # _query_zone_id response
  267. {"recordsets": []}, # _query_record response (no existing record)
  268. {"id": "rec123456"}, # _create_record response
  269. ]
  270. result = provider.set_record("www.example.com", "1.2.3.4", "A", 300, "line1")
  271. self.assertTrue(result)
  272. # Verify the actual API calls made
  273. self.assertEqual(mock_request.call_count, 3)
  274. mock_request.assert_any_call("GET", "/v2/zones", search_mode="equal", limit=500, name="example.com.")
  275. mock_request.assert_any_call(
  276. "GET",
  277. "/v2.1/zones/zone123/recordsets",
  278. limit=500,
  279. name="www.example.com.",
  280. type="A",
  281. line_id="line1",
  282. search_mode="equal",
  283. )
  284. mock_request.assert_any_call(
  285. "POST",
  286. "/v2.1/zones/zone123/recordsets",
  287. name="www.example.com.",
  288. type="A",
  289. records=["1.2.3.4"],
  290. ttl=300,
  291. line="line1",
  292. description="Managed by [DDNS v0.0.0](https://ddns.newfuture.cc)",
  293. )
  294. def test_full_workflow_update_existing_record(self):
  295. """Test complete workflow for updating an existing record"""
  296. provider = HuaweiDNSProvider(self.auth_id, self.auth_token)
  297. with patch.object(provider, "_request") as mock_request:
  298. # Simulate API responses
  299. mock_request.side_effect = [
  300. {"zones": [{"id": "zone123", "name": "example.com."}]}, # _query_zone_id response
  301. { # _query_record response (existing record found)
  302. "recordsets": [
  303. {"id": "rec123", "name": "www.example.com.", "type": "A", "records": ["5.6.7.8"], "ttl": 300}
  304. ]
  305. },
  306. {"id": "rec123"}, # _update_record response
  307. ]
  308. result = provider.set_record("www.example.com", "1.2.3.4", "A", 300, "line1")
  309. self.assertTrue(result)
  310. # Verify the update call was made
  311. mock_request.assert_any_call(
  312. "PUT",
  313. "/v2.1/zones/zone123/recordsets/rec123",
  314. name="www.example.com.",
  315. type="A",
  316. records=["1.2.3.4"],
  317. ttl=300,
  318. description="Managed by [DDNS v0.0.0](https://ddns.newfuture.cc)",
  319. )
  320. def test_full_workflow_zone_not_found(self):
  321. """Test complete workflow when zone is not found"""
  322. provider = HuaweiDNSProvider(self.auth_id, self.auth_token)
  323. with patch.object(provider, "_request") as mock_request:
  324. # Simulate API returning empty zones array
  325. mock_request.return_value = {"zones": []}
  326. result = provider.set_record("www.nonexistent.com", "1.2.3.4", "A")
  327. self.assertFalse(result)
  328. def test_full_workflow_create_failure(self):
  329. """Test complete workflow when record creation fails"""
  330. provider = HuaweiDNSProvider(self.auth_id, self.auth_token)
  331. with patch.object(provider, "_request") as mock_request:
  332. # Simulate responses: zone found, no existing record, creation fails
  333. mock_request.side_effect = [
  334. {"zones": [{"id": "zone123", "name": "example.com."}]}, # _query_zone_id response
  335. {"recordsets": []}, # _query_record response (no existing record)
  336. {"error": "Zone not found"}, # _create_record fails
  337. ]
  338. result = provider.set_record("www.example.com", "1.2.3.4", "A")
  339. self.assertFalse(result)
  340. def test_full_workflow_update_failure(self):
  341. """Test complete workflow when record update fails"""
  342. provider = HuaweiDNSProvider(self.auth_id, self.auth_token)
  343. with patch.object(provider, "_request") as mock_request:
  344. # Simulate responses: zone found, existing record found, update fails
  345. mock_request.side_effect = [
  346. {"zones": [{"id": "zone123", "name": "example.com."}]}, # _query_zone_id response
  347. { # _query_record response (existing record found)
  348. "recordsets": [
  349. {"id": "rec123", "name": "www.example.com.", "type": "A", "records": ["5.6.7.8"], "ttl": 300}
  350. ]
  351. },
  352. {"error": "Update failed"}, # _update_record fails
  353. ]
  354. result = provider.set_record("www.example.com", "1.2.3.4", "A")
  355. self.assertFalse(result)
  356. def test_full_workflow_with_extra_options(self):
  357. """Test complete workflow with additional options"""
  358. provider = HuaweiDNSProvider(self.auth_id, self.auth_token)
  359. with patch.object(provider, "_request") as mock_request:
  360. # Simulate successful creation with custom options
  361. mock_request.side_effect = [
  362. {"zones": [{"id": "zone123", "name": "example.com."}]}, # _query_zone_id response
  363. {"recordsets": []}, # _query_record response (no existing record)
  364. {"id": "rec123456"}, # _create_record response
  365. ]
  366. result = provider.set_record(
  367. "www.example.com", "1.2.3.4", "A", 600, "line2", description="Custom record", tags=["production"]
  368. )
  369. self.assertTrue(result)
  370. # Verify that extra parameters are passed through correctly
  371. mock_request.assert_any_call(
  372. "POST",
  373. "/v2.1/zones/zone123/recordsets",
  374. name="www.example.com.",
  375. type="A",
  376. records=["1.2.3.4"],
  377. ttl=600,
  378. line="line2",
  379. description="Custom record",
  380. tags=["production"],
  381. )
  382. if __name__ == "__main__":
  383. unittest.main()