| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544 |
- # coding=utf-8
- """
- Unit tests for TencentCloudProvider
- 腾讯云 DNSPod 提供商单元测试
- @author: NewFuture
- """
- from base_test import BaseProviderTestCase, unittest, patch, MagicMock
- from ddns.provider.tencentcloud import TencentCloudProvider
- class TestTencentCloudProvider(BaseProviderTestCase):
- """Test TencentCloudProvider functionality"""
- def setUp(self):
- """Set up test fixtures"""
- super(TestTencentCloudProvider, self).setUp()
- self.provider = TencentCloudProvider(self.auth_id, self.auth_token)
- self.logger = self.mock_logger(self.provider)
- def test_init(self):
- """Test provider initialization"""
- self.assertProviderInitialized(self.provider)
- self.assertEqual(self.provider.service, "dnspod")
- self.assertEqual(self.provider.version_date, "2021-03-23")
- self.assertEqual(self.provider.API, "https://dnspod.tencentcloudapi.com")
- self.assertEqual(self.provider.content_type, "application/json")
- def test_validate_success(self):
- """Test successful validation"""
- # Should not raise any exception
- self.provider._validate()
- def test_validate_missing_auth_id(self):
- """Test validation with missing auth_id"""
- with self.assertRaises(ValueError) as context:
- TencentCloudProvider("", self.auth_token, self.logger)
- self.assertIn("id", str(context.exception))
- def test_validate_missing_auth_token(self):
- """Test validation with missing auth_token"""
- with self.assertRaises(ValueError) as context:
- TencentCloudProvider(self.auth_id, "", self.logger)
- self.assertIn("token", str(context.exception))
- @patch("ddns.provider.tencentcloud.strftime")
- @patch("ddns.provider.tencentcloud.time")
- @patch.object(TencentCloudProvider, "_http")
- def test_sign_tc3(self, mock_http, mock_time, mock_strftime):
- """Test TC3 signature generation"""
- mock_time.return_value = 1609459200 # 2021-01-01
- mock_strftime.return_value = "2021-01-01"
- self.provider._request("DescribeDomains")
- self.assertTrue(mock_http.called)
- call_args = mock_http.call_args[1]
- headers = call_args.get("headers", {})
- authorization = headers.get("authorization")
- self.assertIn("TC3-HMAC-SHA256", authorization)
- self.assertIn("Credential=test_id/2021-01-01/dnspod/tc3_request", authorization)
- self.assertIn("SignedHeaders=", authorization)
- self.assertIn("content-type", authorization)
- self.assertIn("host", authorization)
- self.assertIn("Signature=", authorization)
- self.assertIn(self.auth_id, authorization)
- @patch.object(TencentCloudProvider, "_http")
- def test_query_zone_id_success(self, mock_http):
- """Test successful zone ID query"""
- domain = "example.com"
- expected_domain_id = 12345678
- mock_http.return_value = {
- "Response": {"DomainInfo": {"Domain": domain, "DomainId": expected_domain_id, "Status": "enable"}}
- }
- zone_id = self.provider._query_zone_id(domain)
- self.assertEqual(zone_id, str(expected_domain_id))
- @patch.object(TencentCloudProvider, "_http")
- def test_query_zone_id_not_found(self, mock_http):
- """Test zone ID query when domain not found"""
- domain = "nonexistent.com"
- mock_http.return_value = {
- "Response": {
- "Error": {
- "Code": "InvalidParameterValue.DomainNotExists",
- "Message": "当前域名有误,请返回重新操作。",
- }
- }
- }
- zone_id = self.provider._query_zone_id(domain)
- self.assertIsNone(zone_id)
- @patch.object(TencentCloudProvider, "_http")
- def test_query_zone_id_invalid_response(self, mock_http):
- """Test zone ID query with invalid response format"""
- domain = "example.com"
- mock_http.return_value = {"Response": {}}
- zone_id = self.provider._query_zone_id(domain)
- self.assertIsNone(zone_id)
- @patch.object(TencentCloudProvider, "_http")
- def test_query_record_found(self, mock_http):
- """Test successful record query"""
- mock_http.return_value = {
- "Response": {
- "RecordList": [
- {"RecordId": 123456, "Name": "www", "Type": "A", "Value": "1.2.3.4", "Line": "默认", "TTL": 600}
- ]
- }
- }
- record = self.provider._query_record("12345678", "www", "example.com", "A", None, {})
- self.assertIsNotNone(record)
- if record: # Type narrowing for mypy
- self.assertEqual(record["RecordId"], 123456)
- self.assertEqual(record["Name"], "www")
- self.assertEqual(record["Type"], "A")
- # Verify HTTP call was made correctly
- mock_http.assert_called_once()
- call_args = mock_http.call_args
- self.assertEqual(call_args[0][0], "POST") # method
- self.assertEqual(call_args[0][1], "/") # path
- @patch.object(TencentCloudProvider, "_http")
- def test_query_record_not_found(self, mock_http):
- """Test record query when record not found"""
- mock_http.return_value = {"Response": {"RecordList": []}}
- record = self.provider._query_record(
- "12345678", "www", "example.com", "A", None, {}
- ) # type: dict # type: ignore
- self.assertIsNone(record)
- @patch.object(TencentCloudProvider, "_http")
- def test_query_record_root_domain(self, mock_http):
- """Test record query for root domain (@)"""
- mock_http.return_value = {
- "Response": {"RecordList": [{"RecordId": 123456, "Name": "@", "Type": "A", "Value": "1.2.3.4"}]}
- }
- record = self.provider._query_record(
- "12345678", "@", "example.com", "A", None, {}
- ) # type: dict # type: ignore
- self.assertIsNotNone(record)
- self.assertEqual(record["Name"], "@")
- @patch.object(TencentCloudProvider, "_http")
- def test_create_record_success(self, mock_http):
- """Test successful record creation"""
- mock_http.return_value = {"Response": {"RecordId": 789012}}
- result = self.provider._create_record("12345678", "www", "example.com", "1.2.3.4", "A", 600, None, {})
- self.assertTrue(result)
- # Verify HTTP call was made
- mock_http.assert_called_once()
- @patch.object(TencentCloudProvider, "_http")
- def test_create_record_root_domain(self, mock_http):
- """Test record creation for root domain"""
- mock_http.return_value = {"Response": {"RecordId": 789012}}
- result = self.provider._create_record("12345678", "@", "example.com", "1.2.3.4", "A", None, None, {})
- self.assertTrue(result)
- # Verify HTTP call was made
- mock_http.assert_called_once()
- @patch.object(TencentCloudProvider, "_http")
- def test_create_record_with_mx(self, mock_http):
- """Test record creation with MX priority"""
- mock_http.return_value = {"Response": {"RecordId": 789012}}
- result = self.provider._create_record(
- "12345678", "mail", "example.com", "mail.example.com", "MX", None, None, {"MX": 10}
- )
- self.assertTrue(result)
- # Verify HTTP call was made
- mock_http.assert_called_once()
- @patch.object(TencentCloudProvider, "_http")
- def test_create_record_failure(self, mock_http):
- """Test record creation failure"""
- mock_http.return_value = {"Response": {}} # No RecordId in response
- result = self.provider._create_record("12345678", "www", "example.com", "1.2.3.4", "A", None, None, {})
- self.assertFalse(result)
- @patch.object(TencentCloudProvider, "_http")
- def test_update_record_success(self, mock_http):
- """Test successful record update"""
- mock_http.return_value = {"Response": {"RecordId": 123456}}
- old_record = {"RecordId": 123456, "Name": "www", "Type": "A", "Value": "1.2.3.4", "Line": "默认", "TTL": 300}
- result = self.provider._update_record("12345678", old_record, "5.6.7.8", "A", 600, None, {})
- self.assertTrue(result)
- # Verify HTTP call was made
- mock_http.assert_called_once()
- @patch.object(TencentCloudProvider, "_http")
- def test_update_record_preserve_old_values(self, mock_http):
- """Test record update preserves old values when not specified"""
- mock_http.return_value = {"Response": {"RecordId": 123456}}
- old_record = {
- "RecordId": 123456,
- "Name": "www",
- "Type": "A",
- "Value": "1.2.3.4",
- "Line": "电信",
- "TTL": 300,
- "MX": 10,
- "Weight": 5,
- "Remark": "Old remark",
- }
- result = self.provider._update_record("12345678", old_record, "5.6.7.8", "A", None, None, {})
- self.assertTrue(result)
- # Verify HTTP call was made
- mock_http.assert_called_once()
- @patch.object(TencentCloudProvider, "_http")
- def test_update_record_missing_record_id(self, mock_http):
- """Test record update with missing RecordId"""
- mock_http.return_value = {"Response": {}} # No RecordId in response
- old_record = {"Name": "www", "Type": "A"}
- result = self.provider._update_record("12345678", old_record, "5.6.7.8", "A", None, None, {})
- self.assertFalse(result) # Returns False because response doesn't contain RecordId
- mock_http.assert_called_once() # Request is still made
- @patch.object(TencentCloudProvider, "_http")
- def test_update_record_failure(self, mock_http):
- """Test record update failure"""
- mock_http.return_value = {"Response": {}} # No RecordId in response
- old_record = {"RecordId": 123456}
- result = self.provider._update_record("12345678", old_record, "5.6.7.8", "A", None, None, {})
- self.assertFalse(result)
- @patch("ddns.provider.tencentcloud.strftime")
- @patch("ddns.provider.tencentcloud.time")
- @patch.object(TencentCloudProvider, "_http")
- def test_request_success(self, mock_http, mock_time, mock_strftime):
- """Test successful API request"""
- # Mock time functions to get consistent results
- mock_time.return_value = 1609459200
- mock_strftime.return_value = "20210101"
- mock_http.return_value = {"Response": {"RecordId": 123456, "RequestId": "test-request-id"}}
- result = self.provider._request("DescribeRecordList", Domain="example.com")
- self.assertIsNotNone(result)
- if result: # Type narrowing for mypy
- self.assertEqual(result["RecordId"], 123456)
- mock_http.assert_called_once()
- @patch("ddns.provider.tencentcloud.strftime")
- @patch("ddns.provider.tencentcloud.time")
- @patch.object(TencentCloudProvider, "_http")
- def test_request_api_error(self, mock_http, mock_time, mock_strftime):
- """Test API request with error response"""
- mock_time.return_value = 1609459200
- mock_strftime.return_value = "20210101"
- mock_http.return_value = {
- "Response": {"Error": {"Code": "InvalidParameter", "Message": "Invalid domain name"}}
- }
- result = self.provider._request("DescribeRecordList", Domain="invalid")
- self.assertIsNone(result)
- @patch("ddns.provider.tencentcloud.strftime")
- @patch("ddns.provider.tencentcloud.time")
- @patch.object(TencentCloudProvider, "_http")
- def test_request_unexpected_response(self, mock_http, mock_time, mock_strftime):
- """Test API request with unexpected response format"""
- mock_time.return_value = 1609459200
- mock_strftime.return_value = "20210101"
- mock_http.return_value = {"UnexpectedField": "value"}
- result = self.provider._request("DescribeRecordList", Domain="example.com")
- self.assertIsNone(result)
- @patch("ddns.provider.tencentcloud.strftime")
- @patch("ddns.provider.tencentcloud.time")
- @patch.object(TencentCloudProvider, "_http")
- def test_request_exception(self, mock_http, mock_time, mock_strftime):
- """Test API request with exception"""
- mock_time.return_value = 1609459200
- mock_strftime.return_value = "20210101"
- mock_http.side_effect = Exception("Network error")
- # The implementation doesn't catch exceptions, so it will propagate
- with self.assertRaises(Exception) as cm:
- self.provider._request("DescribeRecordList", Domain="example.com")
- self.assertEqual(str(cm.exception), "Network error")
- @patch.object(TencentCloudProvider, "_http")
- def test_set_record_create_new(self, mock_http):
- """Test set_record creating a new record"""
- # Mock HTTP responses for the workflow
- responses = [
- # DescribeDomain response (get domain ID)
- {"Response": {"DomainInfo": {"Domain": "example.com", "DomainId": 12345678}}},
- # DescribeRecordList response (no existing records)
- {"Response": {"RecordList": []}},
- # CreateRecord response (record created successfully)
- {"Response": {"RecordId": 123456}},
- ]
- mock_http.side_effect = responses
- result = self.provider.set_record("www.example.com", "1.2.3.4", "A")
- self.assertTrue(result)
- self.assertEqual(mock_http.call_count, 3)
- @patch.object(TencentCloudProvider, "_http")
- def test_set_record_update_existing(self, mock_http):
- """Test set_record updating an existing record"""
- # Mock HTTP responses for the workflow
- responses = [
- # DescribeDomain response (get domain ID)
- {"Response": {"DomainInfo": {"Domain": "example.com", "DomainId": 12345678}}},
- # DescribeRecordList response (existing record found)
- {
- "Response": {
- "RecordList": [
- {
- "RecordId": 123456,
- "Name": "www",
- "Type": "A",
- "Value": "1.2.3.4",
- "DomainId": 12345678,
- "Line": "默认",
- }
- ]
- }
- },
- # ModifyRecord response (record updated successfully)
- {"Response": {"RecordId": 123456}},
- ]
- mock_http.side_effect = responses
- result = self.provider.set_record("www.example.com", "5.6.7.8", "A")
- self.assertTrue(result)
- self.assertEqual(mock_http.call_count, 3)
- @patch("ddns.provider.tencentcloud.strftime")
- def test_sign_tc3_date_format(self, mock_strftime):
- """Test that the TC3 signature uses the current date in credential scope"""
- mock_strftime.return_value = "20210323" # Mock strftime to return a specific date
- method = "POST"
- uri = "/"
- query = ""
- headers = {"content-type": "application/json", "host": "dnspod.tencentcloudapi.com"}
- payload = "{}"
- timestamp = 1609459200 # 2021-01-01
- authorization = self.provider._sign_tc3(method, uri, query, headers, payload, timestamp)
- # Check that the mocked date is used in the credential scope
- self.assertIn("20210323/dnspod/tc3_request", authorization)
- class TestTencentCloudProviderIntegration(BaseProviderTestCase):
- """Integration tests for TencentCloudProvider"""
- def setUp(self):
- """Set up test fixtures"""
- super(TestTencentCloudProviderIntegration, self).setUp()
- self.provider = TencentCloudProvider(self.auth_id, self.auth_token)
- self.logger = self.mock_logger(self.provider)
- @patch.object(TencentCloudProvider, "_http")
- def test_full_domain_resolution_flow(self, mock_http):
- """Test complete domain resolution flow"""
- # Mock HTTP responses for the workflow
- responses = [
- # DescribeDomain response (get domain ID)
- {"Response": {"DomainInfo": {"Domain": "example.com", "DomainId": 12345678}}},
- # DescribeRecordList response (no existing records)
- {"Response": {"RecordList": []}},
- # CreateRecord response (record created successfully)
- {"Response": {"RecordId": 123456}},
- ]
- mock_http.side_effect = responses
- result = self.provider.set_record("test.example.com", "1.2.3.4", "A", ttl=600)
- self.assertTrue(result)
- self.assertEqual(mock_http.call_count, 3)
- # Verify the CreateRecord call parameters
- create_call = mock_http.call_args_list[2]
- call_body = create_call[1]["body"]
- self.assertIn("DomainId", call_body)
- self.assertIn("CreateRecord", create_call[1]["headers"]["X-TC-Action"])
- @patch.object(TencentCloudProvider, "_http")
- def test_custom_domain_format(self, mock_http):
- """Test custom domain format with ~ separator"""
- # Mock HTTP responses
- responses = [
- # DescribeDomain response (get domain ID)
- {"Response": {"DomainInfo": {"Domain": "example.com", "DomainId": 12345678}}},
- # DescribeRecordList response (no existing records)
- {"Response": {"RecordList": []}},
- # CreateRecord response (record created successfully)
- {"Response": {"RecordId": 123456}},
- ]
- mock_http.side_effect = responses
- result = self.provider.set_record("test~example.com", "1.2.3.4", "A")
- self.assertTrue(result)
- # Verify the CreateRecord action was called
- create_call = mock_http.call_args_list[2]
- headers = create_call[1]["headers"]
- self.assertEqual(headers["X-TC-Action"], "CreateRecord")
- # Verify the body contains the right domain data
- call_body = create_call[1]["body"]
- self.assertIn("12345678", call_body) # DomainId instead of domain name
- self.assertIn("test", call_body)
- @patch.object(TencentCloudProvider, "_http")
- def test_update_existing_record(self, mock_http):
- """Test updating an existing record"""
- # Mock HTTP responses for the workflow
- responses = [
- # DescribeDomain response (get domain ID)
- {"Response": {"DomainInfo": {"Domain": "example.com", "DomainId": 12345678}}},
- # DescribeRecordList response (existing record found)
- {
- "Response": {
- "RecordList": [
- {
- "RecordId": 12345,
- "Name": "test",
- "Type": "A",
- "Value": "1.2.3.4",
- "DomainId": 12345678,
- "Line": "默认",
- }
- ]
- }
- },
- # ModifyRecord response (record updated successfully)
- {"Response": {"RecordId": 12345}},
- ]
- mock_http.side_effect = responses
- result = self.provider.set_record("test.example.com", "5.6.7.8", "A", ttl=300)
- self.assertTrue(result)
- self.assertEqual(mock_http.call_count, 3)
- # Verify the ModifyRecord call
- modify_call = mock_http.call_args_list[2]
- self.assertIn("ModifyRecord", modify_call[1]["headers"]["X-TC-Action"])
- @patch.object(TencentCloudProvider, "_http")
- def test_api_error_handling(self, mock_http):
- """Test API error handling"""
- # Mock API error response for DescribeDomain
- mock_http.return_value = {
- "Response": {"Error": {"Code": "InvalidParameter", "Message": "Invalid domain name"}}
- }
- # This should return False because zone_id cannot be resolved
- result = self.provider.set_record("test.example.com", "1.2.3.4", "A")
- self.assertFalse(result)
- # Two calls are made: split domain name first, then DescribeDomain for main domain
- self.assertGreater(mock_http.call_count, 0)
- class TestTencentCloudProviderRealRequest(BaseProviderTestCase):
- """TencentCloud Provider 真实请求测试类"""
- def setUp(self):
- """Set up test fixtures"""
- super(TestTencentCloudProviderRealRequest, self).setUp()
- def test_auth_failure_real_request(self):
- """Test authentication failure with real API request"""
- # 使用无效的认证信息创建 provider
- invalid_provider = TencentCloudProvider("invalid_id", "invalid_token")
- # Mock logger to capture error logs
- invalid_provider.logger = MagicMock()
- # 尝试查询域名信息,应该返回认证失败
- result = invalid_provider._query_zone_id("example.com")
- # 认证失败时应该返回 None (因为 API 会返回错误)
- self.assertIsNone(result)
- # 验证错误日志被记录
- # 应该有错误日志调用,因为 API 返回认证错误
- self.assertGreaterEqual(invalid_provider.logger.error.call_count, 1)
- # 检查日志内容包含认证相关的错误信息
- error_calls = invalid_provider.logger.error.call_args_list
- logged_messages = [str(call) for call in error_calls]
- # 至少有一个日志应该包含腾讯云 API 错误信息
- has_auth_error = any(
- "tencentcloud api error" in msg.lower() or "authfailure" in msg.lower() or "unauthorized" in msg.lower()
- for msg in logged_messages
- )
- self.assertTrue(
- has_auth_error, "Expected TencentCloud authentication error in logs: {0}".format(logged_messages)
- )
- if __name__ == "__main__":
- unittest.main()
|