test_provider_edgeone.py 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507
  1. # coding=utf-8
  2. """
  3. Unit tests for EdgeOneProvider
  4. 腾讯云 EdgeOne 提供商单元测试
  5. @author: NewFuture
  6. """
  7. from base_test import BaseProviderTestCase, unittest, patch, MagicMock
  8. from ddns.provider.edgeone import EdgeOneProvider
  9. class TestEdgeOneProvider(BaseProviderTestCase):
  10. """Test EdgeOneProvider functionality"""
  11. def setUp(self):
  12. """Set up test fixtures"""
  13. super(TestEdgeOneProvider, self).setUp()
  14. self.provider = EdgeOneProvider(self.id, self.token)
  15. self.logger = self.mock_logger(self.provider)
  16. def test_init(self):
  17. """Test provider initialization"""
  18. self.assertProviderInitialized(self.provider)
  19. self.assertEqual(self.provider.service, "teo")
  20. self.assertEqual(self.provider.version_date, "2022-09-01")
  21. self.assertEqual(self.provider.endpoint, "https://teo.tencentcloudapi.com")
  22. self.assertEqual(self.provider.content_type, "application/json")
  23. def test_validate_success(self):
  24. """Test successful validation"""
  25. # Should not raise any exception
  26. self.provider._validate()
  27. def test_validate_missing_id(self):
  28. """Test validation with missing id"""
  29. with self.assertRaises(ValueError) as context:
  30. EdgeOneProvider("", self.token, self.logger)
  31. self.assertIn("id", str(context.exception))
  32. def test_validate_missing_token(self):
  33. """Test validation with missing token"""
  34. with self.assertRaises(ValueError) as context:
  35. EdgeOneProvider(self.id, "", self.logger)
  36. self.assertIn("token", str(context.exception))
  37. @patch.object(EdgeOneProvider, "_request")
  38. def test_query_zone_id_success(self, mock_request):
  39. """Test successful zone ID query"""
  40. domain = "example.com"
  41. expected_zone_id = "zone-123456789"
  42. mock_request.return_value = {
  43. "Zones": [{"ZoneId": expected_zone_id, "ZoneName": domain, "ActiveStatus": "active", "Status": "active"}]
  44. }
  45. zone_id = self.provider._query_zone_id(domain)
  46. self.assertEqual(zone_id, expected_zone_id)
  47. @patch.object(EdgeOneProvider, "_request")
  48. def test_query_zone_id_not_found(self, mock_request):
  49. """Test zone ID query when domain not found"""
  50. mock_request.return_value = {"Zones": []}
  51. zone_id = self.provider._query_zone_id("nonexistent.com")
  52. self.assertIsNone(zone_id)
  53. @patch.object(EdgeOneProvider, "_request")
  54. def test_query_zone_id_empty_zones(self, mock_request):
  55. """Test zone ID query with empty zones list"""
  56. mock_request.return_value = {"Zones": []}
  57. zone_id = self.provider._query_zone_id("example.com")
  58. self.assertIsNone(zone_id)
  59. @patch.object(EdgeOneProvider, "_request")
  60. def test_query_zone_id_invalid_response(self, mock_request):
  61. """Test zone ID query with invalid response format"""
  62. mock_request.return_value = None
  63. zone_id = self.provider._query_zone_id("example.com")
  64. self.assertIsNone(zone_id)
  65. @patch.object(EdgeOneProvider, "_request")
  66. def test_query_record_found(self, mock_request):
  67. """Test successful acceleration domain query"""
  68. mock_request.return_value = {
  69. "AccelerationDomains": [
  70. {
  71. "ZoneId": "zone-123456789",
  72. "DomainName": "www.example.com",
  73. "DomainStatus": "online",
  74. "OriginDetail": {"OriginType": "ip_domain", "Origin": "1.2.3.4", "BackupOrigin": ""},
  75. }
  76. ]
  77. }
  78. record = self.provider._query_record("zone-123456789", "www", "example.com", "A", None, {})
  79. self.assertIsNotNone(record)
  80. if record: # Type narrowing for mypy
  81. self.assertEqual(record["ZoneId"], "zone-123456789")
  82. self.assertEqual(record["DomainName"], "www.example.com")
  83. self.assertEqual(record["OriginDetail"]["Origin"], "1.2.3.4")
  84. # Verify request call was made correctly
  85. mock_request.assert_called_once()
  86. @patch.object(EdgeOneProvider, "_request")
  87. def test_query_record_not_found(self, mock_request):
  88. """Test acceleration domain query when domain not found"""
  89. mock_request.return_value = {"AccelerationDomains": []}
  90. record = self.provider._query_record("zone-123456789", "www", "example.com", "A", None, {}) # type: dict # type: ignore
  91. self.assertIsNone(record)
  92. @patch.object(EdgeOneProvider, "_request")
  93. def test_query_record_root_domain(self, mock_request):
  94. """Test acceleration domain query for root domain (@)"""
  95. mock_request.return_value = {
  96. "AccelerationDomains": [
  97. {
  98. "ZoneId": "zone-123456789",
  99. "DomainName": "example.com",
  100. "DomainStatus": "online",
  101. "OriginDetail": {"OriginType": "ip_domain", "Origin": "1.2.3.4", "BackupOrigin": ""},
  102. }
  103. ]
  104. }
  105. record = self.provider._query_record("zone-123456789", "@", "example.com", "A", None, {}) # type: dict # type: ignore
  106. self.assertIsNotNone(record)
  107. self.assertEqual(record["DomainName"], "example.com")
  108. @patch.object(EdgeOneProvider, "_request")
  109. def test_create_record_success(self, mock_request):
  110. """Test successful acceleration domain creation"""
  111. mock_request.return_value = {"Response": {"RequestId": "req-123456789"}}
  112. result = self.provider._create_record("zone-123456789", "www", "example.com", "1.2.3.4", "A", 600, None, {})
  113. self.assertTrue(result)
  114. # Verify request call was made correctly
  115. mock_request.assert_called_once()
  116. args, kwargs = mock_request.call_args
  117. self.assertEqual(args[0], "CreateAccelerationDomain")
  118. self.assertEqual(kwargs["ZoneId"], "zone-123456789")
  119. self.assertEqual(kwargs["DomainName"], "www.example.com")
  120. @patch.object(EdgeOneProvider, "_request")
  121. def test_create_record_root_domain(self, mock_request):
  122. """Test acceleration domain creation for root domain"""
  123. mock_request.return_value = {"Response": {"RequestId": "req-123456789"}}
  124. result = self.provider._create_record("zone-123456789", "@", "example.com", "1.2.3.4", "A", 300, None, {})
  125. self.assertTrue(result)
  126. # Verify domain name is correct for root
  127. args, kwargs = mock_request.call_args
  128. self.assertEqual(kwargs["DomainName"], "example.com")
  129. @patch.object(EdgeOneProvider, "_request")
  130. def test_create_record_with_extra_params(self, mock_request):
  131. """Test acceleration domain creation with extra parameters"""
  132. mock_request.return_value = {"Response": {"RequestId": "req-123456789"}}
  133. result = self.provider._create_record(
  134. "zone-123456789", "mail", "example.com", "mail.example.com", "MX", 300, None, {"Priority": 10}
  135. )
  136. self.assertTrue(result)
  137. # Verify extra parameters are passed through
  138. args, kwargs = mock_request.call_args
  139. self.assertEqual(kwargs["Priority"], 10)
  140. @patch.object(EdgeOneProvider, "_request")
  141. def test_create_record_failure(self, mock_request):
  142. """Test acceleration domain creation failure"""
  143. mock_request.return_value = None
  144. result = self.provider._create_record("zone-123456789", "www", "example.com", "1.2.3.4", "A", 300, None, {})
  145. self.assertFalse(result)
  146. @patch.object(EdgeOneProvider, "_request")
  147. def test_update_record_success(self, mock_request):
  148. """Test successful acceleration domain origin update"""
  149. mock_request.return_value = {"Response": {"RequestId": "req-123456789"}}
  150. old_record = {
  151. "ZoneId": "zone-123456789",
  152. "DomainName": "www.example.com",
  153. "DomainStatus": "online",
  154. "OriginDetail": {"OriginType": "ip_domain", "Origin": "1.2.3.4", "BackupOrigin": ""},
  155. }
  156. result = self.provider._update_record("zone-123456789", old_record, "5.6.7.8", "A", 600, None, {})
  157. self.assertTrue(result)
  158. # Verify request call was made
  159. mock_request.assert_called_once()
  160. @patch.object(EdgeOneProvider, "_request")
  161. def test_update_record_preserve_backup_origin(self, mock_request):
  162. """Test acceleration domain update preserves backup origin"""
  163. mock_request.return_value = {"Response": {"RequestId": "req-123456789"}}
  164. old_record = {
  165. "ZoneId": "zone-123456789",
  166. "DomainName": "www.example.com",
  167. "DomainStatus": "online",
  168. "OriginDetail": {"OriginType": "ip_domain", "Origin": "1.2.3.4", "BackupOrigin": "backup.example.com"},
  169. }
  170. result = self.provider._update_record("zone-123456789", old_record, "5.6.7.8", "A", None, None, {})
  171. self.assertTrue(result)
  172. # Verify request call was made
  173. mock_request.assert_called_once()
  174. @patch.object(EdgeOneProvider, "_request")
  175. def test_update_record_missing_domain_name(self, mock_request):
  176. """Test acceleration domain update with missing domain name"""
  177. mock_request.return_value = None # Simulate API failure due to missing domain name
  178. old_record = {
  179. "ZoneId": "zone-123456789",
  180. # Missing DomainName
  181. "OriginDetail": {"OriginType": "ip_domain", "Origin": "1.2.3.4"},
  182. }
  183. result = self.provider._update_record("zone-123456789", old_record, "5.6.7.8", "A", None, None, {})
  184. # Should fail because domain name is None and API call will fail
  185. self.assertFalse(result)
  186. @patch.object(EdgeOneProvider, "_request")
  187. def test_update_record_failure(self, mock_request):
  188. """Test acceleration domain update failure"""
  189. mock_request.return_value = None # API call failed
  190. old_record = {
  191. "ZoneId": "zone-123456789",
  192. "DomainName": "www.example.com",
  193. "OriginDetail": {"OriginType": "ip_domain", "Origin": "1.2.3.4"},
  194. }
  195. result = self.provider._update_record("zone-123456789", old_record, "5.6.7.8", "A", None, None, {})
  196. self.assertFalse(result)
  197. @patch("ddns.provider.tencentcloud.strftime")
  198. @patch("ddns.provider.tencentcloud.time")
  199. @patch.object(EdgeOneProvider, "_http")
  200. def test_request_success(self, mock_http, mock_time, mock_strftime):
  201. """Test successful API request"""
  202. # Mock time functions to get consistent results
  203. mock_time.return_value = 1609459200
  204. mock_strftime.return_value = "20210101"
  205. mock_http.return_value = {"Response": {"ZoneId": "zone-123456", "RequestId": "test-request-id"}}
  206. result = self.provider._request(
  207. "DescribeZones",
  208. Filters=[{"Name": "zone-name", "Values": ["example.com"]}], # type: ignore[arg-type]
  209. )
  210. self.assertIsNotNone(result)
  211. if result: # Type narrowing for mypy
  212. self.assertEqual(result["ZoneId"], "zone-123456")
  213. mock_http.assert_called_once()
  214. @patch("ddns.provider.tencentcloud.strftime")
  215. @patch("ddns.provider.tencentcloud.time")
  216. @patch.object(EdgeOneProvider, "_http")
  217. def test_request_api_error(self, mock_http, mock_time, mock_strftime):
  218. """Test API request with error response"""
  219. mock_time.return_value = 1609459200
  220. mock_strftime.return_value = "20210101"
  221. mock_http.return_value = {"Response": {"Error": {"Code": "InvalidParameter", "Message": "Invalid zone name"}}}
  222. result = self.provider._request(
  223. "DescribeZones",
  224. Filters=[{"Name": "zone-name", "Values": ["invalid"]}], # type: ignore[arg-type]
  225. )
  226. self.assertIsNone(result)
  227. @patch("ddns.provider.tencentcloud.strftime")
  228. @patch("ddns.provider.tencentcloud.time")
  229. @patch.object(EdgeOneProvider, "_http")
  230. def test_request_unexpected_response(self, mock_http, mock_time, mock_strftime):
  231. """Test API request with unexpected response format"""
  232. mock_time.return_value = 1609459200
  233. mock_strftime.return_value = "20210101"
  234. mock_http.return_value = {"UnexpectedField": "value"}
  235. result = self.provider._request(
  236. "DescribeZones",
  237. Filters=[{"Name": "zone-name", "Values": ["example.com"]}], # type: ignore[arg-type]
  238. )
  239. self.assertIsNone(result)
  240. @patch("ddns.provider.tencentcloud.strftime")
  241. @patch("ddns.provider.tencentcloud.time")
  242. @patch.object(EdgeOneProvider, "_http")
  243. def test_request_exception(self, mock_http, mock_time, mock_strftime):
  244. """Test API request with exception"""
  245. mock_time.return_value = 1609459200
  246. mock_strftime.return_value = "20210101"
  247. mock_http.side_effect = Exception("Network error")
  248. # The implementation doesn't catch exceptions, so it will propagate
  249. with self.assertRaises(Exception) as cm:
  250. self.provider._request(
  251. "DescribeZones",
  252. Filters=[{"Name": "zone-name", "Values": ["example.com"]}], # type: ignore[arg-type]
  253. )
  254. self.assertEqual(str(cm.exception), "Network error")
  255. @patch.object(EdgeOneProvider, "_request")
  256. def test_set_record_create_new(self, mock_request):
  257. """Test set_record creating a new acceleration domain"""
  258. # Mock HTTP responses for the workflow
  259. responses = [
  260. # DescribeZones response (get zone ID for main domain)
  261. {"Zones": [{"ZoneId": "zone-123456789", "ZoneName": "example.com"}]},
  262. # DescribeAccelerationDomains response (no existing acceleration domain for subdomain)
  263. {"AccelerationDomains": []},
  264. # CreateAccelerationDomain response (acceleration domain created successfully)
  265. {"Response": {"RequestId": "req-123456789"}},
  266. ]
  267. mock_request.side_effect = responses
  268. result = self.provider.set_record("www.example.com", "1.2.3.4", "A")
  269. # Should succeed because EdgeOne supports creating new acceleration domains
  270. self.assertTrue(result)
  271. self.assertEqual(mock_request.call_count, 3) # Zone lookup, record query, and create calls
  272. @patch.object(EdgeOneProvider, "_request")
  273. def test_set_record_update_existing(self, mock_request):
  274. """Test set_record updating an existing acceleration domain"""
  275. # Mock HTTP responses for the workflow
  276. responses = [
  277. # DescribeZones response (get zone ID for main domain)
  278. {"Zones": [{"ZoneId": "zone-123456789", "ZoneName": "example.com"}]},
  279. # DescribeAccelerationDomains response (existing acceleration domain found)
  280. {
  281. "AccelerationDomains": [
  282. {
  283. "ZoneId": "zone-123456789",
  284. "DomainName": "www.example.com",
  285. "DomainStatus": "online",
  286. "OriginDetail": {"OriginType": "ip_domain", "Origin": "1.2.3.4", "BackupOrigin": ""},
  287. }
  288. ]
  289. },
  290. # ModifyAccelerationDomain response (acceleration domain updated successfully)
  291. {"Response": {"RequestId": "req-123456789"}},
  292. ]
  293. mock_request.side_effect = responses
  294. result = self.provider.set_record("www.example.com", "5.6.7.8", "A")
  295. self.assertTrue(result)
  296. self.assertEqual(mock_request.call_count, 3)
  297. class TestEdgeOneProviderIntegration(BaseProviderTestCase):
  298. """Integration tests for EdgeOneProvider"""
  299. def setUp(self):
  300. """Set up test fixtures"""
  301. super(TestEdgeOneProviderIntegration, self).setUp()
  302. self.provider = EdgeOneProvider(self.id, self.token)
  303. self.logger = self.mock_logger(self.provider)
  304. @patch.object(EdgeOneProvider, "_request")
  305. def test_full_domain_resolution_flow(self, mock_request):
  306. """Test complete domain resolution flow for creating new domains"""
  307. # Mock request responses for the workflow
  308. responses = [
  309. # DescribeZones response (get zone ID for main domain)
  310. {"Zones": [{"ZoneId": "zone-123456789", "ZoneName": "example.com"}]},
  311. # DescribeAccelerationDomains response (no existing acceleration domain for subdomain)
  312. {"AccelerationDomains": []},
  313. # CreateAccelerationDomain response (acceleration domain created successfully)
  314. {"Response": {"RequestId": "req-123456789"}},
  315. ]
  316. mock_request.side_effect = responses
  317. result = self.provider.set_record("test.example.com", "1.2.3.4", "A", ttl=600)
  318. # Should succeed because EdgeOne supports creating new acceleration domains
  319. self.assertTrue(result)
  320. self.assertEqual(mock_request.call_count, 3) # Zone lookup, record query, and create calls
  321. @patch.object(EdgeOneProvider, "_request")
  322. def test_custom_domain_format(self, mock_request):
  323. """Test custom domain format with ~ separator (create new domain)"""
  324. # Mock request responses
  325. responses = [
  326. # DescribeZones response (get zone ID for main domain)
  327. {"Zones": [{"ZoneId": "zone-123456789", "ZoneName": "example.com"}]},
  328. # DescribeAccelerationDomains response (no existing acceleration domain for subdomain)
  329. {"AccelerationDomains": []},
  330. # CreateAccelerationDomain response (acceleration domain created successfully)
  331. {"Response": {"RequestId": "req-123456789"}},
  332. ]
  333. mock_request.side_effect = responses
  334. result = self.provider.set_record("test~example.com", "1.2.3.4", "A")
  335. # Should succeed because EdgeOne supports creating new acceleration domains
  336. self.assertTrue(result)
  337. # Zone lookup, record query, and create calls should be made
  338. self.assertEqual(mock_request.call_count, 3)
  339. @patch.object(EdgeOneProvider, "_request")
  340. def test_update_existing_record(self, mock_request):
  341. """Test updating an existing acceleration domain"""
  342. # Mock request responses for the workflow
  343. responses = [
  344. # DescribeZones response (get zone ID for main domain)
  345. {"Zones": [{"ZoneId": "zone-123456789", "ZoneName": "example.com"}]},
  346. # DescribeAccelerationDomains response (existing acceleration domain found)
  347. {
  348. "AccelerationDomains": [
  349. {
  350. "ZoneId": "zone-123456789",
  351. "DomainName": "test.example.com",
  352. "DomainStatus": "online",
  353. "OriginDetail": {"OriginType": "ip_domain", "Origin": "1.2.3.4", "BackupOrigin": ""},
  354. }
  355. ]
  356. },
  357. # ModifyAccelerationDomain response (acceleration domain updated successfully)
  358. {"Response": {"RequestId": "req-123456789"}},
  359. ]
  360. mock_request.side_effect = responses
  361. result = self.provider.set_record("test.example.com", "5.6.7.8", "A", ttl=300)
  362. self.assertTrue(result)
  363. self.assertEqual(mock_request.call_count, 3)
  364. # Verify the ModifyAccelerationDomain call
  365. modify_call = mock_request.call_args_list[2]
  366. self.assertEqual(modify_call[0][0], "ModifyAccelerationDomain")
  367. @patch.object(EdgeOneProvider, "_request")
  368. def test_api_error_handling(self, mock_request):
  369. """Test API error handling"""
  370. # Mock API error response - the _request method returns None on error
  371. mock_request.return_value = None
  372. # This should return False because zone_id cannot be resolved
  373. result = self.provider.set_record("test.example.com", "1.2.3.4", "A")
  374. self.assertFalse(result)
  375. # At least one call should be made to try to resolve zone ID
  376. self.assertGreater(mock_request.call_count, 0)
  377. class TestEdgeOneProviderRealRequest(BaseProviderTestCase):
  378. """EdgeOne Provider 真实请求测试类"""
  379. def setUp(self):
  380. """Set up test fixtures"""
  381. super(TestEdgeOneProviderRealRequest, self).setUp()
  382. def test_auth_failure_real_request(self):
  383. """Test authentication failure with real API request"""
  384. # 使用无效的认证信息创建 provider
  385. invalid_provider = EdgeOneProvider("invalid_id", "invalid_token")
  386. # Mock logger to capture error logs
  387. invalid_provider.logger = MagicMock()
  388. # 尝试查询域名信息,应该返回认证失败
  389. result = invalid_provider._query_zone_id("example.com")
  390. # 认证失败时应该返回 None (因为 API 会返回错误)
  391. self.assertIsNone(result)
  392. # 验证错误日志被记录
  393. # 应该有错误日志调用,因为 API 返回认证错误
  394. self.assertGreaterEqual(invalid_provider.logger.error.call_count, 1)
  395. # 检查日志内容包含认证相关的错误信息
  396. error_calls = invalid_provider.logger.error.call_args_list
  397. logged_messages = [str(call) for call in error_calls]
  398. # 至少有一个日志应该包含EdgeOne API 错误信息
  399. has_auth_error = any(
  400. "edgeone api error" in msg.lower() or "authfailure" in msg.lower() or "unauthorized" in msg.lower()
  401. for msg in logged_messages
  402. )
  403. self.assertTrue(has_auth_error, "Expected EdgeOne authentication error in logs: {0}".format(logged_messages))
  404. if __name__ == "__main__":
  405. unittest.main()