test_provider_cloudflare.py 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550
  1. # coding=utf-8
  2. """
  3. Unit tests for CloudflareProvider
  4. @author: GitHub Copilot
  5. """
  6. from base_test import BaseProviderTestCase, unittest, patch
  7. from ddns.provider.cloudflare import CloudflareProvider
  8. class TestCloudflareProvider(BaseProviderTestCase):
  9. """Test cases for CloudflareProvider"""
  10. def setUp(self):
  11. """Set up test fixtures"""
  12. super(TestCloudflareProvider, self).setUp()
  13. self.auth_id = "[email protected]"
  14. self.auth_token = "test_api_key_or_token"
  15. def test_class_constants(self):
  16. """Test CloudflareProvider class constants"""
  17. self.assertEqual(CloudflareProvider.API, "https://api.cloudflare.com")
  18. self.assertEqual(CloudflareProvider.content_type, "application/json")
  19. self.assertTrue(CloudflareProvider.decode_response)
  20. def test_init_with_basic_config(self):
  21. """Test CloudflareProvider initialization with basic configuration"""
  22. provider = CloudflareProvider(self.auth_id, self.auth_token)
  23. self.assertEqual(provider.auth_id, self.auth_id)
  24. self.assertEqual(provider.auth_token, self.auth_token)
  25. self.assertEqual(provider.API, "https://api.cloudflare.com")
  26. def test_init_with_token_only(self):
  27. """Test CloudflareProvider initialization with token only (Bearer auth)"""
  28. provider = CloudflareProvider("", self.auth_token)
  29. self.assertEqual(provider.auth_id, "")
  30. self.assertEqual(provider.auth_token, self.auth_token)
  31. def test_validate_success_with_email(self):
  32. """Test _validate method with valid email"""
  33. provider = CloudflareProvider(self.auth_id, self.auth_token)
  34. # Should not raise any exception
  35. provider._validate()
  36. def test_validate_success_with_token_only(self):
  37. """Test _validate method with token only (no email)"""
  38. provider = CloudflareProvider("", self.auth_token)
  39. # Should not raise any exception
  40. provider._validate()
  41. def test_validate_failure_no_token(self):
  42. """Test _validate method with missing token"""
  43. with self.assertRaises(ValueError) as cm:
  44. CloudflareProvider(self.auth_id, "")
  45. self.assertIn("token must be configured", str(cm.exception))
  46. def test_validate_failure_invalid_email(self):
  47. """Test _validate method with invalid email format"""
  48. with self.assertRaises(ValueError) as cm:
  49. CloudflareProvider("invalid_email", self.auth_token)
  50. self.assertIn("ID must be a valid email or Empty", str(cm.exception))
  51. def test_request_with_email_auth(self):
  52. """Test _request method using email + API key authentication"""
  53. provider = CloudflareProvider(self.auth_id, self.auth_token)
  54. with patch.object(provider, "_http") as mock_http:
  55. mock_http.return_value = {"success": True, "result": {"id": "zone123"}}
  56. result = provider._request("GET", "/test", param1="value1")
  57. mock_http.assert_called_once_with(
  58. "GET",
  59. "/client/v4/zones/test",
  60. headers={"X-Auth-Email": self.auth_id, "X-Auth-Key": self.auth_token},
  61. params={"param1": "value1"},
  62. )
  63. self.assertEqual(result, {"id": "zone123"})
  64. def test_request_with_bearer_auth(self):
  65. """Test _request method using Bearer token authentication"""
  66. provider = CloudflareProvider("", self.auth_token)
  67. with patch.object(provider, "_http") as mock_http:
  68. mock_http.return_value = {"success": True, "result": {"id": "zone123"}}
  69. result = provider._request("GET", "/test", param1="value1")
  70. mock_http.assert_called_once_with(
  71. "GET",
  72. "/client/v4/zones/test",
  73. headers={"Authorization": "Bearer " + self.auth_token},
  74. params={"param1": "value1"},
  75. )
  76. self.assertEqual(result, {"id": "zone123"})
  77. def test_request_failure(self):
  78. """Test _request method with failed response"""
  79. provider = CloudflareProvider(self.auth_id, self.auth_token)
  80. with patch.object(provider, "_http") as mock_http:
  81. mock_http.return_value = {"success": False, "errors": ["Invalid API key"]}
  82. result = provider._request("GET", "/test")
  83. self.assertEqual(result, {"success": False, "errors": ["Invalid API key"]})
  84. def test_request_filters_none_params(self):
  85. """Test _request method filters out None parameters"""
  86. provider = CloudflareProvider(self.auth_id, self.auth_token)
  87. with patch.object(provider, "_http") as mock_http:
  88. mock_http.return_value = {"success": True, "result": {}}
  89. provider._request("GET", "/test", param1="value1", param2=None, param3="value3")
  90. # Verify None parameters were filtered out
  91. call_args = mock_http.call_args
  92. params = call_args[1]["params"]
  93. self.assertEqual(params, {"param1": "value1", "param3": "value3"})
  94. def test_query_zone_id_success(self):
  95. """Test _query_zone_id method with successful response"""
  96. provider = CloudflareProvider(self.auth_id, self.auth_token)
  97. with patch.object(provider, "_request") as mock_request:
  98. mock_request.return_value = [
  99. {"id": "zone123", "name": "example.com"},
  100. {"id": "zone456", "name": "other.com"},
  101. ]
  102. result = provider._query_zone_id("example.com")
  103. mock_request.assert_called_once_with("GET", "", **{"name.exact": "example.com", "per_page": 50})
  104. self.assertEqual(result, "zone123")
  105. def test_query_zone_id_not_found(self):
  106. """Test _query_zone_id method when domain is not found"""
  107. provider = CloudflareProvider(self.auth_id, self.auth_token)
  108. with patch.object(provider, "_request") as mock_request:
  109. mock_request.return_value = [{"id": "zone456", "name": "other.com"}]
  110. result = provider._query_zone_id("notfound.com")
  111. self.assertIsNone(result)
  112. def test_query_zone_id_empty_response(self):
  113. """Test _query_zone_id method with empty response"""
  114. provider = CloudflareProvider(self.auth_id, self.auth_token)
  115. with patch.object(provider, "_request") as mock_request:
  116. mock_request.return_value = []
  117. result = provider._query_zone_id("example.com")
  118. self.assertIsNone(result)
  119. def test_query_record_success(self):
  120. """Test _query_record method with successful response"""
  121. provider = CloudflareProvider(self.auth_id, self.auth_token)
  122. with patch.object(provider, "_join_domain") as mock_join, patch.object(provider, "_request") as mock_request:
  123. mock_join.return_value = "www.example.com"
  124. mock_request.return_value = [
  125. {"id": "rec123", "name": "www.example.com", "type": "A", "content": "1.2.3.4"},
  126. {"id": "rec456", "name": "mail.example.com", "type": "A", "content": "5.6.7.8"},
  127. ]
  128. result = provider._query_record(
  129. "zone123", "www", "example.com", "A", None, {}
  130. ) # type: dict # type: ignore
  131. mock_join.assert_called_once_with("www", "example.com")
  132. params = {"name.exact": "www.example.com"}
  133. mock_request.assert_called_once_with("GET", "/zone123/dns_records", type="A", per_page=10000, **params)
  134. self.assertEqual(result["id"], "rec123")
  135. self.assertEqual(result["name"], "www.example.com")
  136. def test_query_record_not_found(self):
  137. """Test _query_record method when no matching record is found"""
  138. provider = CloudflareProvider(self.auth_id, self.auth_token)
  139. with patch.object(provider, "_join_domain") as mock_join, patch.object(provider, "_request") as mock_request:
  140. mock_join.return_value = "www.example.com"
  141. mock_request.return_value = [
  142. {"id": "rec456", "name": "mail.example.com", "type": "A", "content": "5.6.7.8"}
  143. ]
  144. result = provider._query_record("zone123", "www", "example.com", "A", None, {})
  145. self.assertIsNone(result)
  146. def test_query_record_with_proxy_option(self):
  147. """Test _query_record method with proxy option in extra parameters"""
  148. provider = CloudflareProvider(self.auth_id, self.auth_token)
  149. with patch.object(provider, "_join_domain") as mock_join, patch.object(provider, "_request") as mock_request:
  150. mock_join.return_value = "www.example.com"
  151. mock_request.return_value = []
  152. extra = {"proxied": True}
  153. provider._query_record("zone123", "www", "example.com", "A", None, extra)
  154. mock_request.assert_called_once_with(
  155. "GET",
  156. "/zone123/dns_records",
  157. type="A",
  158. per_page=10000,
  159. **{"name.exact": "www.example.com", "proxied": True}
  160. )
  161. def test_create_record_success(self):
  162. """Test _create_record method with successful creation"""
  163. provider = CloudflareProvider(self.auth_id, self.auth_token)
  164. with patch.object(provider, "_join_domain") as mock_join, patch.object(provider, "_request") as mock_request:
  165. mock_join.return_value = "www.example.com"
  166. mock_request.return_value = {"id": "rec123", "name": "www.example.com"}
  167. result = provider._create_record(
  168. "zone123", "www", "example.com", "1.2.3.4", "A", 300, None, {}
  169. ) # type: dict # type: ignore
  170. mock_join.assert_called_once_with("www", "example.com")
  171. mock_request.assert_called_once_with(
  172. "POST",
  173. "/zone123/dns_records",
  174. name="www.example.com",
  175. type="A",
  176. content="1.2.3.4",
  177. ttl=300,
  178. comment=provider.remark,
  179. )
  180. self.assertTrue(result)
  181. def test_create_record_failure(self):
  182. """Test _create_record method with failed creation"""
  183. provider = CloudflareProvider(self.auth_id, self.auth_token)
  184. with patch.object(provider, "_join_domain") as mock_join, patch.object(provider, "_request") as mock_request:
  185. mock_join.return_value = "www.example.com"
  186. mock_request.return_value = None # API request failed
  187. result = provider._create_record("zone123", "www", "example.com", "1.2.3.4", "A", None, None, {})
  188. self.assertFalse(result)
  189. def test_create_record_with_extra_params(self):
  190. """Test _create_record method with extra parameters"""
  191. provider = CloudflareProvider(self.auth_id, self.auth_token)
  192. with patch.object(provider, "_join_domain") as mock_join, patch.object(provider, "_request") as mock_request:
  193. mock_join.return_value = "www.example.com"
  194. mock_request.return_value = {"id": "rec123"}
  195. extra = {"proxied": True, "comment": "Custom comment", "priority": 10}
  196. result = provider._create_record("zone123", "www", "example.com", "1.2.3.4", "A", 300, None, extra)
  197. mock_request.assert_called_once_with(
  198. "POST",
  199. "/zone123/dns_records",
  200. name="www.example.com",
  201. type="A",
  202. content="1.2.3.4",
  203. ttl=300,
  204. proxied=True,
  205. comment="Custom comment",
  206. priority=10,
  207. )
  208. self.assertTrue(result)
  209. def test_update_record_success(self):
  210. """Test _update_record method with successful update"""
  211. provider = CloudflareProvider(self.auth_id, self.auth_token)
  212. old_record = {
  213. "id": "rec123",
  214. "name": "www.example.com",
  215. "comment": "Old comment",
  216. "proxied": False,
  217. "tags": ["tag1"],
  218. "settings": {"ttl": 300},
  219. }
  220. with patch.object(provider, "_request") as mock_request:
  221. mock_request.return_value = {"id": "rec123", "content": "5.6.7.8"}
  222. result = provider._update_record("zone123", old_record, "5.6.7.8", "A", 600, None, {})
  223. mock_request.assert_called_once_with(
  224. "PUT",
  225. "/zone123/dns_records/rec123",
  226. type="A",
  227. name="www.example.com",
  228. content="5.6.7.8",
  229. ttl=600,
  230. comment="Managed by [DDNS v0.0.0](https://ddns.newfuture.cc)", # Default Remark since extra is empty
  231. proxied=False,
  232. tags=["tag1"],
  233. settings={"ttl": 300},
  234. )
  235. self.assertTrue(result)
  236. def test_update_record_failure(self):
  237. """Test _update_record method with failed update"""
  238. provider = CloudflareProvider(self.auth_id, self.auth_token)
  239. old_record = {"id": "rec123", "name": "www.example.com"}
  240. with patch.object(provider, "_request") as mock_request:
  241. mock_request.return_value = None # API request failed
  242. result = provider._update_record("zone123", old_record, "5.6.7.8", "A", None, None, {})
  243. self.assertFalse(result)
  244. def test_update_record_with_extra_params(self):
  245. """Test _update_record method with extra parameters overriding defaults"""
  246. provider = CloudflareProvider(self.auth_id, self.auth_token)
  247. old_record = {"id": "rec123", "name": "www.example.com", "comment": "Old comment", "proxied": False}
  248. with patch.object(provider, "_request") as mock_request:
  249. mock_request.return_value = {"id": "rec123"}
  250. extra = {"comment": "New comment", "proxied": True, "priority": 20}
  251. result = provider._update_record("zone123", old_record, "5.6.7.8", "A", 600, None, extra)
  252. mock_request.assert_called_once_with(
  253. "PUT",
  254. "/zone123/dns_records/rec123",
  255. type="A",
  256. name="www.example.com",
  257. content="5.6.7.8",
  258. ttl=600,
  259. comment="New comment", # extra.get("comment", self.remark)
  260. proxied=False, # old_record.get("proxied", extra.get("proxied"))
  261. priority=20, # From extra
  262. tags=None,
  263. settings=None,
  264. )
  265. self.assertTrue(result)
  266. def test_update_record_preserves_old_values(self):
  267. """Test _update_record method preserves proxied/tags/settings from old record, uses default comment"""
  268. provider = CloudflareProvider(self.auth_id, self.auth_token)
  269. old_record = {
  270. "id": "rec123",
  271. "name": "www.example.com",
  272. "comment": "Preserve this",
  273. "proxied": True,
  274. "tags": ["important"],
  275. "settings": {"ttl": 300},
  276. }
  277. with patch.object(provider, "_request") as mock_request:
  278. mock_request.return_value = {"id": "rec123"}
  279. # No extra parameters provided
  280. result = provider._update_record("zone123", old_record, "5.6.7.8", "A", 600, None, {})
  281. mock_request.assert_called_once_with(
  282. "PUT",
  283. "/zone123/dns_records/rec123",
  284. type="A",
  285. name="www.example.com",
  286. content="5.6.7.8",
  287. ttl=600,
  288. comment="Managed by [DDNS v0.0.0](https://ddns.newfuture.cc)", # Default Remark
  289. proxied=True, # Preserved from old record
  290. tags=["important"], # Preserved from old record
  291. settings={"ttl": 300}, # Preserved from old record
  292. )
  293. self.assertTrue(result)
  294. class TestCloudflareProviderIntegration(BaseProviderTestCase):
  295. """Integration test cases for CloudflareProvider - testing with minimal mocking"""
  296. def setUp(self):
  297. """Set up test fixtures"""
  298. super(TestCloudflareProviderIntegration, self).setUp()
  299. self.auth_id = "[email protected]"
  300. self.auth_token = "test_api_key"
  301. def test_full_workflow_create_new_record(self):
  302. """Test complete workflow for creating a new record"""
  303. provider = CloudflareProvider(self.auth_id, self.auth_token)
  304. # Mock only the HTTP layer to simulate API responses
  305. with patch.object(provider, "_request") as mock_request:
  306. # Simulate API responses in order: zone query, record query, record creation
  307. mock_request.side_effect = [
  308. [{"id": "zone123", "name": "example.com"}], # _query_zone_id response
  309. [], # _query_record response (no existing record)
  310. {"id": "rec123", "name": "www.example.com"}, # _create_record response
  311. ]
  312. result = provider.set_record("www.example.com", "1.2.3.4", "A", 300)
  313. self.assertTrue(result)
  314. # Verify the actual API calls made
  315. self.assertEqual(mock_request.call_count, 3)
  316. mock_request.assert_any_call("GET", "", **{"name.exact": "example.com", "per_page": 50})
  317. mock_request.assert_any_call(
  318. "GET", "/zone123/dns_records", type="A", per_page=10000, **{"name.exact": "www.example.com"}
  319. )
  320. mock_request.assert_any_call(
  321. "POST",
  322. "/zone123/dns_records",
  323. name="www.example.com",
  324. type="A",
  325. content="1.2.3.4",
  326. ttl=300,
  327. comment="Managed by [DDNS v0.0.0](https://ddns.newfuture.cc)",
  328. )
  329. def test_full_workflow_update_existing_record(self):
  330. """Test complete workflow for updating an existing record"""
  331. provider = CloudflareProvider(self.auth_id, self.auth_token)
  332. with patch.object(provider, "_request") as mock_request:
  333. # Simulate API responses
  334. mock_request.side_effect = [
  335. [{"id": "zone123", "name": "example.com"}], # _query_zone_id response
  336. [ # _query_record response (existing record found)
  337. {"id": "rec123", "name": "www.example.com", "type": "A", "content": "5.6.7.8", "proxied": False}
  338. ],
  339. {"id": "rec123", "content": "1.2.3.4"}, # _update_record response
  340. ]
  341. result = provider.set_record("www.example.com", "1.2.3.4", "A", 300)
  342. self.assertTrue(result)
  343. # Verify the update call was made
  344. mock_request.assert_any_call(
  345. "PUT",
  346. "/zone123/dns_records/rec123",
  347. type="A",
  348. name="www.example.com",
  349. content="1.2.3.4",
  350. ttl=300,
  351. comment="Managed by [DDNS v0.0.0](https://ddns.newfuture.cc)",
  352. proxied=False,
  353. tags=None,
  354. settings=None,
  355. )
  356. def test_full_workflow_zone_not_found(self):
  357. """Test complete workflow when zone is not found"""
  358. provider = CloudflareProvider(self.auth_id, self.auth_token)
  359. with patch.object(provider, "_request") as mock_request:
  360. # Simulate API returning empty array for zone query
  361. mock_request.return_value = []
  362. result = provider.set_record("www.nonexistent.com", "1.2.3.4", "A")
  363. self.assertFalse(result)
  364. def test_full_workflow_create_failure(self):
  365. """Test complete workflow when record creation fails"""
  366. provider = CloudflareProvider(self.auth_id, self.auth_token)
  367. with patch.object(provider, "_request") as mock_request:
  368. # Simulate responses: zone found, no existing record, creation fails
  369. mock_request.side_effect = [
  370. [{"id": "zone123", "name": "example.com"}], # _query_zone_id response
  371. [], # _query_record response (no existing record)
  372. None, # _create_record fails (API returns None)
  373. ]
  374. result = provider.set_record("www.example.com", "1.2.3.4", "A")
  375. self.assertFalse(result)
  376. def test_full_workflow_update_failure(self):
  377. """Test complete workflow when record update fails"""
  378. provider = CloudflareProvider(self.auth_id, self.auth_token)
  379. with patch.object(provider, "_request") as mock_request:
  380. # Simulate responses: zone found, existing record found, update fails
  381. mock_request.side_effect = [
  382. [{"id": "zone123", "name": "example.com"}], # _query_zone_id response
  383. [ # _query_record response (existing record found)
  384. {"id": "rec123", "name": "www.example.com", "type": "A", "content": "5.6.7.8"}
  385. ],
  386. None, # _update_record fails (API returns None)
  387. ]
  388. result = provider.set_record("www.example.com", "1.2.3.4", "A")
  389. self.assertFalse(result)
  390. def test_full_workflow_with_proxy_options(self):
  391. """Test complete workflow with proxy and other Cloudflare-specific options"""
  392. provider = CloudflareProvider(self.auth_id, self.auth_token)
  393. with patch.object(provider, "_request") as mock_request:
  394. # Simulate successful creation with custom options
  395. mock_request.side_effect = [
  396. [{"id": "zone123", "name": "example.com"}], # _query_zone_id response
  397. [], # _query_record response (no existing record)
  398. {"id": "rec123", "name": "www.example.com"}, # _create_record response
  399. ]
  400. result = provider.set_record("www.example.com", "1.2.3.4", "A", 300, None, proxied=True, priority=10)
  401. self.assertTrue(result)
  402. # Verify that extra parameters are passed through correctly
  403. mock_request.assert_any_call(
  404. "POST",
  405. "/zone123/dns_records",
  406. name="www.example.com",
  407. type="A",
  408. content="1.2.3.4",
  409. ttl=300,
  410. comment="Managed by [DDNS v0.0.0](https://ddns.newfuture.cc)",
  411. proxied=True,
  412. priority=10,
  413. )
  414. def test_full_workflow_bearer_token_auth(self):
  415. """Test complete workflow using Bearer token authentication"""
  416. provider = CloudflareProvider("", self.auth_token) # No email, Bearer token only
  417. with patch.object(provider, "_request") as mock_request:
  418. # Simulate successful workflow
  419. mock_request.side_effect = [
  420. [{"id": "zone123", "name": "example.com"}], # _query_zone_id response
  421. [], # _query_record response (no existing record)
  422. {"id": "rec123", "name": "www.example.com"}, # _create_record response
  423. ]
  424. result = provider.set_record("www.example.com", "1.2.3.4", "A")
  425. self.assertTrue(result)
  426. # The workflow should work the same regardless of auth method
  427. if __name__ == "__main__":
  428. unittest.main()