test_provider_alidns.py 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543
  1. # coding=utf-8
  2. """
  3. Unit tests for AlidnsProvider
  4. @author: Github Copilot
  5. """
  6. from base_test import BaseProviderTestCase, unittest, patch
  7. from ddns.provider.alidns import AlidnsProvider
  8. class TestAlidnsProvider(BaseProviderTestCase):
  9. """Test cases for AlidnsProvider"""
  10. def setUp(self):
  11. """Set up test fixtures"""
  12. super(TestAlidnsProvider, self).setUp()
  13. self.id = "test_access_key_id"
  14. self.token = "test_access_key_secret"
  15. def test_class_constants(self):
  16. """Test AlidnsProvider class constants"""
  17. self.assertEqual(AlidnsProvider.endpoint, "https://alidns.aliyuncs.com")
  18. self.assertEqual(AlidnsProvider.content_type, "application/x-www-form-urlencoded")
  19. self.assertTrue(AlidnsProvider.decode_response)
  20. def test_init_with_basic_config(self):
  21. """Test AlidnsProvider initialization with basic configuration"""
  22. provider = AlidnsProvider(self.id, self.token)
  23. self.assertEqual(provider.id, self.id)
  24. self.assertEqual(provider.token, self.token)
  25. self.assertEqual(provider.endpoint, "https://alidns.aliyuncs.com")
  26. def test_request_basic(self):
  27. """Test _request method with basic parameters"""
  28. provider = AlidnsProvider(self.id, self.token)
  29. # Only mock the HTTP call to avoid actual network requests
  30. with patch.object(provider, "_http") as mock_http:
  31. mock_http.return_value = {"Success": True}
  32. result = provider._request("TestAction", DomainName="example.com")
  33. # Verify HTTP call was made with correct method and path
  34. mock_http.assert_called_once()
  35. call_args = mock_http.call_args
  36. self.assertEqual(call_args[0], ("POST", "/"))
  37. # Verify body and headers are present
  38. self.assertIn("body", call_args[1])
  39. self.assertIn("headers", call_args[1])
  40. # Verify headers contain required v3 signature fields
  41. headers = call_args[1]["headers"]
  42. self.assertIn("Authorization", headers)
  43. self.assertIn("x-acs-action", headers)
  44. self.assertIn("x-acs-date", headers)
  45. self.assertIn("x-acs-version", headers)
  46. self.assertEqual(headers["x-acs-action"], "TestAction")
  47. # Verify body is form-encoded
  48. body = call_args[1]["body"]
  49. self.assertIn("DomainName=example.com", body)
  50. self.assertEqual(result, {"Success": True})
  51. def test_request_filters_none_params(self):
  52. """Test _request method filters out None parameters"""
  53. provider = AlidnsProvider(self.id, self.token)
  54. # Only mock the HTTP call
  55. with patch.object(provider, "_http") as mock_http:
  56. mock_http.return_value = {}
  57. provider._request("TestAction", DomainName="example.com", TTL=None, Line=None)
  58. # Verify HTTP call was made
  59. mock_http.assert_called_once()
  60. # Body should not contain None parameters
  61. call_args = mock_http.call_args[1]
  62. body = call_args.get("body", "")
  63. self.assertNotIn("TTL=None", body)
  64. self.assertNotIn("Line=None", body)
  65. self.assertIn("DomainName=example.com", body)
  66. def test_split_zone_and_sub_success(self):
  67. """Test _split_zone_and_sub method with successful response"""
  68. provider = AlidnsProvider(self.id, self.token)
  69. with patch.object(provider, "_http") as mock_http:
  70. mock_http.return_value = {"DomainName": "example.com", "RR": "sub"}
  71. main, sub, zone_id = provider._split_zone_and_sub("sub.example.com")
  72. mock_http.assert_called_once()
  73. # Verify GetMainDomainName API was called via headers
  74. call_headers = mock_http.call_args[1]["headers"]
  75. self.assertEqual(call_headers["x-acs-action"], "GetMainDomainName")
  76. # Verify the input parameter in body
  77. call_body = mock_http.call_args[1]["body"]
  78. self.assertIn("InputString=sub.example.com", call_body)
  79. self.assertEqual(main, "example.com")
  80. self.assertEqual(sub, "sub")
  81. self.assertEqual(zone_id, "example.com")
  82. def test_split_zone_and_sub_not_found(self):
  83. """Test _split_zone_and_sub method when domain is not found"""
  84. provider = AlidnsProvider(self.id, self.token)
  85. with patch.object(provider, "_http") as mock_http:
  86. mock_http.return_value = {}
  87. main, sub, zone_id = provider._split_zone_and_sub("notfound.com")
  88. mock_http.assert_called_once()
  89. self.assertIsNone(main)
  90. self.assertIsNone(sub)
  91. self.assertEqual(zone_id, "notfound.com")
  92. def test_query_record_success_single(self):
  93. """Test _query_record method with single record found"""
  94. provider = AlidnsProvider(self.id, self.token)
  95. with patch.object(provider, "_http") as mock_http:
  96. mock_http.return_value = {
  97. "DomainRecords": {"Record": [{"RR": "www", "RecordId": "123", "Value": "1.2.3.4", "Type": "A"}]}
  98. }
  99. result = provider._query_record("example.com", "www", "example.com", "A", None, {})
  100. mock_http.assert_called_once()
  101. # Verify DescribeSubDomainRecords API was called via headers
  102. call_headers = mock_http.call_args[1]["headers"]
  103. self.assertEqual(call_headers["x-acs-action"], "DescribeSubDomainRecords")
  104. # Verify parameters in body
  105. call_body = mock_http.call_args[1]["body"]
  106. self.assertIn("SubDomain=www.example.com", call_body)
  107. self.assertIn("DomainName=example.com", call_body)
  108. self.assertIn("Type=A", call_body)
  109. self.assertIsNotNone(result)
  110. if result: # Type narrowing for mypy
  111. self.assertEqual(result["RecordId"], "123")
  112. self.assertEqual(result["RR"], "www")
  113. def test_query_record_not_found(self):
  114. """Test _query_record method when no matching record is found"""
  115. provider = AlidnsProvider(self.id, self.token)
  116. with patch.object(provider, "_http") as mock_http:
  117. mock_http.return_value = {"DomainRecords": {"Record": []}}
  118. result = provider._query_record("example.com", "www", "example.com", "A", None, {})
  119. self.assertIsNone(result)
  120. def test_query_record_empty_response(self):
  121. """Test _query_record method with empty records response"""
  122. provider = AlidnsProvider(self.id, self.token)
  123. with patch.object(provider, "_http") as mock_http:
  124. mock_http.return_value = {"DomainRecords": {"Record": []}}
  125. result = provider._query_record("example.com", "www", "example.com", "A", None, {})
  126. self.assertIsNone(result)
  127. def test_query_record_with_extra_params(self):
  128. """Test _query_record method with extra parameters"""
  129. provider = AlidnsProvider(self.id, self.token)
  130. with patch.object(provider, "_http") as mock_http:
  131. mock_http.return_value = {"DomainRecords": {"Record": []}}
  132. extra = {"Lang": "en", "Status": "Enable"}
  133. provider._query_record("example.com", "www", "example.com", "A", "default", extra)
  134. mock_http.assert_called_once()
  135. # Verify extra parameters are included in the request
  136. call_body = mock_http.call_args[1]["body"]
  137. self.assertIn("Lang=en", call_body)
  138. self.assertIn("Status=Enable", call_body)
  139. self.assertIn("Line=default", call_body)
  140. def test_create_record_success(self):
  141. """Test _create_record method with successful creation"""
  142. provider = AlidnsProvider(self.id, self.token)
  143. with patch.object(provider, "_request") as mock_request:
  144. mock_request.return_value = {"RecordId": "123456"}
  145. result = provider._create_record("example.com", "www", "example.com", "1.2.3.4", "A", 300, "default", {})
  146. mock_request.assert_called_once_with(
  147. "AddDomainRecord",
  148. DomainName="example.com",
  149. RR="www",
  150. Value="1.2.3.4",
  151. Type="A",
  152. TTL=300,
  153. Line="default",
  154. )
  155. self.assertTrue(result)
  156. def test_create_record_failure(self):
  157. """Test _create_record method with failed creation"""
  158. provider = AlidnsProvider(self.id, self.token)
  159. with patch.object(provider, "_request") as mock_request:
  160. mock_request.return_value = {"Error": "Invalid domain"}
  161. result = provider._create_record("example.com", "www", "example.com", "1.2.3.4", "A", None, None, {})
  162. self.assertFalse(result)
  163. def test_create_record_with_extra_params(self):
  164. """Test _create_record method with extra parameters"""
  165. provider = AlidnsProvider(self.id, self.token)
  166. with patch.object(provider, "_request") as mock_request:
  167. mock_request.return_value = {"RecordId": "123456"}
  168. extra = {"Priority": 10, "Remark": "Test record"}
  169. result = provider._create_record("t.com", "www", "t.com", "1.2.3.4", "A", 300, "default", extra)
  170. mock_request.assert_called_once_with(
  171. "AddDomainRecord",
  172. DomainName="t.com",
  173. RR="www",
  174. Value="1.2.3.4",
  175. Type="A",
  176. TTL=300,
  177. Line="default",
  178. Priority=10,
  179. Remark="Test record",
  180. )
  181. self.assertTrue(result)
  182. def test_update_record_success(self):
  183. """Test _update_record method with successful update"""
  184. provider = AlidnsProvider(self.id, self.token)
  185. old_record = {"RecordId": "123456", "RR": "www", "Line": "default"}
  186. with patch.object(provider, "_request") as mock_request:
  187. mock_request.return_value = {"RecordId": "123456"}
  188. result = provider._update_record("example.com", old_record, "5.6.7.8", "A", 600, "unicom", {})
  189. mock_request.assert_called_once_with(
  190. "UpdateDomainRecord", RecordId="123456", Value="5.6.7.8", RR="www", Type="A", TTL=600, Line="unicom"
  191. )
  192. self.assertTrue(result)
  193. def test_update_record_with_fallback_line(self):
  194. """Test _update_record method uses old record's line when line is None"""
  195. provider = AlidnsProvider(self.id, self.token)
  196. old_record = {"RecordId": "123456", "RR": "www", "Line": "default"}
  197. with patch.object(provider, "_request") as mock_request:
  198. mock_request.return_value = {"RecordId": "123456"}
  199. result = provider._update_record("example.com", old_record, "5.6.7.8", "A", None, None, {})
  200. mock_request.assert_called_once_with(
  201. "UpdateDomainRecord",
  202. RecordId="123456",
  203. Value="5.6.7.8",
  204. RR="www",
  205. Type="A",
  206. TTL=None,
  207. Line="default", # Should use old record's line
  208. )
  209. self.assertTrue(result)
  210. def test_update_record_failure(self):
  211. """Test _update_record method with failed update"""
  212. provider = AlidnsProvider(self.id, self.token)
  213. old_record = {"RecordId": "123456", "RR": "www", "Line": "default"}
  214. with patch.object(provider, "_request") as mock_request:
  215. mock_request.return_value = {"Error": "Record not found"}
  216. result = provider._update_record("example.com", old_record, "5.6.7.8", "A", None, None, {})
  217. self.assertFalse(result)
  218. def test_update_record_no_changes(self):
  219. """Test _update_record method when no changes are detected"""
  220. provider = AlidnsProvider(self.id, self.token)
  221. old_record = {"RecordId": "123456", "RR": "www", "Value": "1.2.3.4", "Type": "A", "TTL": 300, "Line": "default"}
  222. with patch.object(provider, "_request") as mock_request:
  223. # Same value, type, and TTL should skip update
  224. result = provider._update_record("example.com", old_record, "1.2.3.4", "A", 300, "default", {})
  225. # Should return True without making any API calls
  226. self.assertTrue(result)
  227. mock_request.assert_not_called()
  228. def test_update_record_with_extra_params(self):
  229. """Test _update_record method with extra parameters"""
  230. provider = AlidnsProvider(self.id, self.token)
  231. old_record = {"RecordId": "123456", "RR": "www", "Line": "default"}
  232. with patch.object(provider, "_request") as mock_request:
  233. mock_request.return_value = {"RecordId": "123456"}
  234. extra = {"Priority": 20, "Remark": "Updated record"}
  235. result = provider._update_record("example.com", old_record, "5.6.7.8", "A", 600, "unicom", extra)
  236. mock_request.assert_called_once_with(
  237. "UpdateDomainRecord",
  238. RecordId="123456",
  239. Value="5.6.7.8",
  240. RR="www",
  241. Type="A",
  242. TTL=600,
  243. Line="unicom",
  244. Priority=20,
  245. Remark="Updated record",
  246. )
  247. self.assertTrue(result)
  248. def test_update_record_extra_priority_over_old_record(self):
  249. """Test that extra parameters take priority over old_record values"""
  250. provider = AlidnsProvider(self.id, self.token)
  251. old_record = {"RecordId": "123456", "RR": "www", "Line": "default", "Priority": 10, "Remark": "Old remark"}
  252. with patch.object(provider, "_request") as mock_request:
  253. mock_request.return_value = {"RecordId": "123456"}
  254. # extra should override old_record's Priority and Remark
  255. extra = {"Priority": 20, "Remark": "New remark from extra"}
  256. result = provider._update_record("example.com", old_record, "5.6.7.8", "A", 600, "unicom", extra)
  257. mock_request.assert_called_once_with(
  258. "UpdateDomainRecord",
  259. RecordId="123456",
  260. Value="5.6.7.8",
  261. RR="www",
  262. Type="A",
  263. TTL=600,
  264. Line="unicom",
  265. Priority=20, # Should use extra, not old_record's 10
  266. Remark="New remark from extra", # Should use extra, not old_record's "Old remark"
  267. )
  268. self.assertTrue(result)
  269. def test_line_configuration_support(self):
  270. """Test that AlidnsProvider supports line configuration"""
  271. provider = AlidnsProvider(self.id, self.token)
  272. old_record = {"RecordId": "123456", "RR": "www", "Line": "default"}
  273. with patch.object(provider, "_request") as mock_request:
  274. mock_request.return_value = {"RecordId": "123456"}
  275. # Test with custom line parameter
  276. result = provider._update_record("example.com", old_record, "5.6.7.8", "A", 600, "telecom", {})
  277. mock_request.assert_called_once_with(
  278. "UpdateDomainRecord",
  279. RecordId="123456",
  280. Value="5.6.7.8",
  281. RR="www",
  282. Type="A",
  283. TTL=600,
  284. Line="telecom", # Should use the provided line parameter
  285. )
  286. self.assertTrue(result)
  287. def test_create_record_with_line(self):
  288. """Test _create_record method with line parameter"""
  289. provider = AlidnsProvider(self.id, self.token)
  290. with patch.object(provider, "_request") as mock_request:
  291. mock_request.return_value = {"RecordId": "123456"}
  292. result = provider._create_record("example.com", "www", "example.com", "1.2.3.4", "A", 300, "unicom", {})
  293. mock_request.assert_called_once_with(
  294. "AddDomainRecord", DomainName="example.com", RR="www", Value="1.2.3.4", Type="A", TTL=300, Line="unicom"
  295. )
  296. self.assertTrue(result)
  297. class TestAlidnsProviderIntegration(BaseProviderTestCase):
  298. """Integration test cases for AlidnsProvider - testing with minimal mocking"""
  299. def setUp(self):
  300. """Set up test fixtures"""
  301. super(TestAlidnsProviderIntegration, self).setUp()
  302. self.id = "test_access_key_id"
  303. self.token = "test_access_key_secret"
  304. def test_full_workflow_create_new_record(self):
  305. """Test complete workflow for creating a new record"""
  306. provider = AlidnsProvider(self.id, self.token)
  307. # Mock only the HTTP layer to simulate API responses
  308. with patch.object(provider, "_http") as mock_http:
  309. # Simulate API responses in order: GetMainDomainName, DescribeSubDomainRecords, AddDomainRecord
  310. mock_http.side_effect = [
  311. {"DomainName": "example.com", "RR": "www"}, # _split_zone_and_sub response
  312. {"DomainRecords": {"Record": []}}, # _query_record response (no existing record)
  313. {"RecordId": "123456"}, # _create_record response
  314. ]
  315. result = provider.set_record("www.example.com", "1.2.3.4", "A", 300, "default")
  316. self.assertTrue(result)
  317. # Verify the actual HTTP calls made - should be 3 calls
  318. self.assertEqual(mock_http.call_count, 3)
  319. # Check that proper API actions were called by examining request headers
  320. call_actions = [call[1]["headers"]["x-acs-action"] for call in mock_http.call_args_list]
  321. self.assertIn("GetMainDomainName", call_actions)
  322. self.assertIn("DescribeSubDomainRecords", call_actions)
  323. self.assertIn("AddDomainRecord", call_actions)
  324. def test_full_workflow_update_existing_record(self):
  325. """Test complete workflow for updating an existing record"""
  326. provider = AlidnsProvider(self.id, self.token)
  327. with patch.object(provider, "_http") as mock_http:
  328. # Simulate API responses: GetMainDomainName, DescribeSubDomainRecords, UpdateDomainRecord
  329. mock_http.side_effect = [
  330. {"DomainName": "example.com", "RR": "www"}, # _split_zone_and_sub response
  331. { # _query_record response (existing record found)
  332. "DomainRecords": {
  333. "Record": [
  334. {"RecordId": "123456", "RR": "www", "Value": "5.6.7.8", "Type": "A", "Line": "default"}
  335. ]
  336. }
  337. },
  338. {"RecordId": "123456"}, # _update_record response
  339. ]
  340. result = provider.set_record("www.example.com", "1.2.3.4", "A", 300, "default")
  341. self.assertTrue(result)
  342. # Verify 3 HTTP calls were made
  343. self.assertEqual(mock_http.call_count, 3)
  344. # Check that UpdateDomainRecord was called
  345. call_actions = [call[1]["headers"]["x-acs-action"] for call in mock_http.call_args_list]
  346. self.assertIn("UpdateDomainRecord", call_actions)
  347. def test_full_workflow_zone_not_found(self):
  348. """Test complete workflow when zone is not found"""
  349. provider = AlidnsProvider(self.id, self.token)
  350. with patch.object(provider, "_http") as mock_http:
  351. # Simulate API returning empty response for zone query
  352. mock_http.return_value = {}
  353. # Should return False when zone not found
  354. result = provider.set_record("www.nonexistent.com", "1.2.3.4", "A")
  355. self.assertFalse(result)
  356. def test_full_workflow_create_failure(self):
  357. """Test complete workflow when record creation fails"""
  358. provider = AlidnsProvider(self.id, self.token)
  359. with patch.object(provider, "_http") as mock_http:
  360. # Simulate responses: zone found, no existing record, creation fails
  361. mock_http.side_effect = [
  362. {"DomainName": "example.com", "RR": "www"}, # _split_zone_and_sub response
  363. {"DomainRecords": {"Record": []}}, # _query_record response (no existing record)
  364. {"Error": "API error", "Code": "InvalidParameter"}, # _create_record fails
  365. ]
  366. result = provider.set_record("www.example.com", "1.2.3.4", "A")
  367. self.assertFalse(result)
  368. def test_full_workflow_update_failure(self):
  369. """Test complete workflow when record update fails"""
  370. provider = AlidnsProvider(self.id, self.token)
  371. with patch.object(provider, "_http") as mock_http:
  372. # Simulate responses: zone found, existing record found, update fails
  373. mock_http.side_effect = [
  374. {"DomainName": "example.com", "RR": "www"}, # _split_zone_and_sub response
  375. { # _query_record response (existing record found)
  376. "DomainRecords": {
  377. "Record": [
  378. {"RecordId": "123456", "RR": "www", "Value": "5.6.7.8", "Type": "A", "Line": "default"}
  379. ]
  380. }
  381. },
  382. {"Error": "API error", "Code": "InvalidParameter"}, # _update_record fails
  383. ]
  384. result = provider.set_record("www.example.com", "1.2.3.4", "A")
  385. self.assertFalse(result)
  386. def test_full_workflow_with_options(self):
  387. """Test complete workflow with additional options like ttl and line"""
  388. provider = AlidnsProvider(self.id, self.token)
  389. with patch.object(provider, "_http") as mock_http:
  390. # Simulate successful creation with custom options
  391. mock_http.side_effect = [
  392. {"DomainName": "example.com", "RR": "www"}, # _split_zone_and_sub response
  393. {"DomainRecords": {"Record": []}}, # _query_record response (no existing record)
  394. {"RecordId": "123456"}, # _create_record response
  395. ]
  396. result = provider.set_record("www.example.com", "1.2.3.4", "A", 600, "unicom")
  397. self.assertTrue(result)
  398. # Verify that extra parameters are passed through correctly
  399. self.assertEqual(mock_http.call_count, 3)
  400. # Check that the create call contains the correct parameters
  401. # Find the AddDomainRecord call (should be the last one)
  402. add_record_call = None
  403. for call in mock_http.call_args_list:
  404. if call[1]["headers"]["x-acs-action"] == "AddDomainRecord":
  405. add_record_call = call
  406. break
  407. self.assertIsNotNone(add_record_call)
  408. if add_record_call:
  409. create_body = add_record_call[1]["body"]
  410. self.assertIn("TTL=600", create_body)
  411. self.assertIn("Line=unicom", create_body)
  412. if __name__ == "__main__":
  413. unittest.main()