mirror of
https://github.com/NewFuture/DDNS.git
synced 2026-10-19 16:15:45 +00:00
* fix(tests): handle endpoint timeouts in fallback Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * fix(http): add configurable request timeout Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: NewFuture <6290356+NewFuture@users.noreply.github.com> Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
509 lines
21 KiB
Python
509 lines
21 KiB
Python
# coding=utf-8
|
|
"""
|
|
Unit tests for EdgeOneProvider
|
|
腾讯云 EdgeOne 提供商单元测试
|
|
|
|
@author: NewFuture
|
|
"""
|
|
|
|
from base_test import BaseProviderTestCase, unittest, patch, MagicMock
|
|
from ddns.provider.edgeone import EdgeOneProvider
|
|
|
|
|
|
class TestEdgeOneProvider(BaseProviderTestCase):
|
|
"""Test EdgeOneProvider functionality"""
|
|
|
|
def setUp(self):
|
|
"""Set up test fixtures"""
|
|
super(TestEdgeOneProvider, self).setUp()
|
|
self.provider = EdgeOneProvider(self.id, self.token)
|
|
self.logger = self.mock_logger(self.provider)
|
|
|
|
def test_init(self):
|
|
"""Test provider initialization"""
|
|
self.assertProviderInitialized(self.provider)
|
|
self.assertEqual(self.provider.service, "teo")
|
|
self.assertEqual(self.provider.version_date, "2022-09-01")
|
|
self.assertEqual(self.provider.endpoint, "https://teo.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_id(self):
|
|
"""Test validation with missing id"""
|
|
with self.assertRaises(ValueError) as context:
|
|
EdgeOneProvider("", self.token, self.logger)
|
|
self.assertIn("id", str(context.exception))
|
|
|
|
def test_validate_missing_token(self):
|
|
"""Test validation with missing token"""
|
|
with self.assertRaises(ValueError) as context:
|
|
EdgeOneProvider(self.id, "", self.logger)
|
|
self.assertIn("token", str(context.exception))
|
|
|
|
@patch.object(EdgeOneProvider, "_request")
|
|
def test_query_zone_id_success(self, mock_request):
|
|
"""Test successful zone ID query"""
|
|
domain = "example.com"
|
|
expected_zone_id = "zone-123456789"
|
|
|
|
mock_request.return_value = {
|
|
"Zones": [{"ZoneId": expected_zone_id, "ZoneName": domain, "ActiveStatus": "active", "Status": "active"}]
|
|
}
|
|
|
|
zone_id = self.provider._query_zone_id(domain)
|
|
self.assertEqual(zone_id, expected_zone_id)
|
|
|
|
@patch.object(EdgeOneProvider, "_request")
|
|
def test_query_zone_id_not_found(self, mock_request):
|
|
"""Test zone ID query when domain not found"""
|
|
mock_request.return_value = {"Zones": []}
|
|
|
|
zone_id = self.provider._query_zone_id("nonexistent.com")
|
|
self.assertIsNone(zone_id)
|
|
|
|
@patch.object(EdgeOneProvider, "_request")
|
|
def test_query_zone_id_empty_zones(self, mock_request):
|
|
"""Test zone ID query with empty zones list"""
|
|
mock_request.return_value = {"Zones": []}
|
|
|
|
zone_id = self.provider._query_zone_id("example.com")
|
|
self.assertIsNone(zone_id)
|
|
|
|
@patch.object(EdgeOneProvider, "_request")
|
|
def test_query_zone_id_invalid_response(self, mock_request):
|
|
"""Test zone ID query with invalid response format"""
|
|
mock_request.return_value = None
|
|
|
|
zone_id = self.provider._query_zone_id("example.com")
|
|
self.assertIsNone(zone_id)
|
|
|
|
@patch.object(EdgeOneProvider, "_request")
|
|
def test_query_record_found(self, mock_request):
|
|
"""Test successful acceleration domain query"""
|
|
mock_request.return_value = {
|
|
"AccelerationDomains": [
|
|
{
|
|
"ZoneId": "zone-123456789",
|
|
"DomainName": "www.example.com",
|
|
"DomainStatus": "online",
|
|
"OriginDetail": {"OriginType": "ip_domain", "Origin": "1.2.3.4", "BackupOrigin": ""},
|
|
}
|
|
]
|
|
}
|
|
|
|
record = self.provider._query_record("zone-123456789", "www", "example.com", "A", None, {})
|
|
|
|
self.assertIsNotNone(record)
|
|
if record: # Type narrowing for mypy
|
|
self.assertEqual(record["ZoneId"], "zone-123456789")
|
|
self.assertEqual(record["DomainName"], "www.example.com")
|
|
self.assertEqual(record["OriginDetail"]["Origin"], "1.2.3.4")
|
|
|
|
# Verify request call was made correctly
|
|
mock_request.assert_called_once()
|
|
|
|
@patch.object(EdgeOneProvider, "_request")
|
|
def test_query_record_not_found(self, mock_request):
|
|
"""Test acceleration domain query when domain not found"""
|
|
mock_request.return_value = {"AccelerationDomains": []}
|
|
|
|
record = self.provider._query_record("zone-123456789", "www", "example.com", "A", None, {}) # type: dict # type: ignore
|
|
|
|
self.assertIsNone(record)
|
|
|
|
@patch.object(EdgeOneProvider, "_request")
|
|
def test_query_record_root_domain(self, mock_request):
|
|
"""Test acceleration domain query for root domain (@)"""
|
|
mock_request.return_value = {
|
|
"AccelerationDomains": [
|
|
{
|
|
"ZoneId": "zone-123456789",
|
|
"DomainName": "example.com",
|
|
"DomainStatus": "online",
|
|
"OriginDetail": {"OriginType": "ip_domain", "Origin": "1.2.3.4", "BackupOrigin": ""},
|
|
}
|
|
]
|
|
}
|
|
|
|
record = self.provider._query_record("zone-123456789", "@", "example.com", "A", None, {}) # type: dict # type: ignore
|
|
|
|
self.assertIsNotNone(record)
|
|
self.assertEqual(record["DomainName"], "example.com")
|
|
|
|
@patch.object(EdgeOneProvider, "_request")
|
|
def test_create_record_success(self, mock_request):
|
|
"""Test successful acceleration domain creation"""
|
|
mock_request.return_value = {"Response": {"RequestId": "req-123456789"}}
|
|
|
|
result = self.provider._create_record("zone-123456789", "www", "example.com", "1.2.3.4", "A", 600, None, {})
|
|
|
|
self.assertTrue(result)
|
|
# Verify request call was made correctly
|
|
mock_request.assert_called_once()
|
|
args, kwargs = mock_request.call_args
|
|
self.assertEqual(args[0], "CreateAccelerationDomain")
|
|
self.assertEqual(kwargs["ZoneId"], "zone-123456789")
|
|
self.assertEqual(kwargs["DomainName"], "www.example.com")
|
|
|
|
@patch.object(EdgeOneProvider, "_request")
|
|
def test_create_record_root_domain(self, mock_request):
|
|
"""Test acceleration domain creation for root domain"""
|
|
mock_request.return_value = {"Response": {"RequestId": "req-123456789"}}
|
|
|
|
result = self.provider._create_record("zone-123456789", "@", "example.com", "1.2.3.4", "A", 300, None, {})
|
|
|
|
self.assertTrue(result)
|
|
# Verify domain name is correct for root
|
|
args, kwargs = mock_request.call_args
|
|
self.assertEqual(kwargs["DomainName"], "example.com")
|
|
|
|
@patch.object(EdgeOneProvider, "_request")
|
|
def test_create_record_with_extra_params(self, mock_request):
|
|
"""Test acceleration domain creation with extra parameters"""
|
|
mock_request.return_value = {"Response": {"RequestId": "req-123456789"}}
|
|
|
|
result = self.provider._create_record(
|
|
"zone-123456789", "mail", "example.com", "mail.example.com", "MX", 300, None, {"Priority": 10}
|
|
)
|
|
|
|
self.assertTrue(result)
|
|
# Verify extra parameters are passed through
|
|
args, kwargs = mock_request.call_args
|
|
self.assertEqual(kwargs["Priority"], 10)
|
|
|
|
@patch.object(EdgeOneProvider, "_request")
|
|
def test_create_record_failure(self, mock_request):
|
|
"""Test acceleration domain creation failure"""
|
|
mock_request.return_value = None
|
|
|
|
result = self.provider._create_record("zone-123456789", "www", "example.com", "1.2.3.4", "A", 300, None, {})
|
|
|
|
self.assertFalse(result)
|
|
|
|
@patch.object(EdgeOneProvider, "_request")
|
|
def test_update_record_success(self, mock_request):
|
|
"""Test successful acceleration domain origin update"""
|
|
mock_request.return_value = {"Response": {"RequestId": "req-123456789"}}
|
|
|
|
old_record = {
|
|
"ZoneId": "zone-123456789",
|
|
"DomainName": "www.example.com",
|
|
"DomainStatus": "online",
|
|
"OriginDetail": {"OriginType": "ip_domain", "Origin": "1.2.3.4", "BackupOrigin": ""},
|
|
}
|
|
|
|
result = self.provider._update_record("zone-123456789", old_record, "5.6.7.8", "A", 600, None, {})
|
|
|
|
self.assertTrue(result)
|
|
# Verify request call was made
|
|
mock_request.assert_called_once()
|
|
|
|
@patch.object(EdgeOneProvider, "_request")
|
|
def test_update_record_preserve_backup_origin(self, mock_request):
|
|
"""Test acceleration domain update preserves backup origin"""
|
|
mock_request.return_value = {"Response": {"RequestId": "req-123456789"}}
|
|
|
|
old_record = {
|
|
"ZoneId": "zone-123456789",
|
|
"DomainName": "www.example.com",
|
|
"DomainStatus": "online",
|
|
"OriginDetail": {"OriginType": "ip_domain", "Origin": "1.2.3.4", "BackupOrigin": "backup.example.com"},
|
|
}
|
|
|
|
result = self.provider._update_record("zone-123456789", old_record, "5.6.7.8", "A", None, None, {})
|
|
|
|
self.assertTrue(result)
|
|
# Verify request call was made
|
|
mock_request.assert_called_once()
|
|
|
|
@patch.object(EdgeOneProvider, "_request")
|
|
def test_update_record_missing_domain_name(self, mock_request):
|
|
"""Test acceleration domain update with missing domain name"""
|
|
mock_request.return_value = None # Simulate API failure due to missing domain name
|
|
|
|
old_record = {
|
|
"ZoneId": "zone-123456789",
|
|
# Missing DomainName
|
|
"OriginDetail": {"OriginType": "ip_domain", "Origin": "1.2.3.4"},
|
|
}
|
|
|
|
result = self.provider._update_record("zone-123456789", old_record, "5.6.7.8", "A", None, None, {})
|
|
|
|
# Should fail because domain name is None and API call will fail
|
|
self.assertFalse(result)
|
|
|
|
@patch.object(EdgeOneProvider, "_request")
|
|
def test_update_record_failure(self, mock_request):
|
|
"""Test acceleration domain update failure"""
|
|
mock_request.return_value = None # API call failed
|
|
|
|
old_record = {
|
|
"ZoneId": "zone-123456789",
|
|
"DomainName": "www.example.com",
|
|
"OriginDetail": {"OriginType": "ip_domain", "Origin": "1.2.3.4"},
|
|
}
|
|
|
|
result = self.provider._update_record("zone-123456789", old_record, "5.6.7.8", "A", None, None, {})
|
|
|
|
self.assertFalse(result)
|
|
|
|
@patch("ddns.provider.tencentcloud.strftime")
|
|
@patch("ddns.provider.tencentcloud.time")
|
|
@patch.object(EdgeOneProvider, "_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": {"ZoneId": "zone-123456", "RequestId": "test-request-id"}}
|
|
|
|
result = self.provider._request(
|
|
"DescribeZones",
|
|
Filters=[{"Name": "zone-name", "Values": ["example.com"]}], # type: ignore[arg-type]
|
|
)
|
|
|
|
self.assertIsNotNone(result)
|
|
if result: # Type narrowing for mypy
|
|
self.assertEqual(result["ZoneId"], "zone-123456")
|
|
mock_http.assert_called_once()
|
|
|
|
@patch("ddns.provider.tencentcloud.strftime")
|
|
@patch("ddns.provider.tencentcloud.time")
|
|
@patch.object(EdgeOneProvider, "_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 zone name"}}}
|
|
|
|
result = self.provider._request(
|
|
"DescribeZones",
|
|
Filters=[{"Name": "zone-name", "Values": ["invalid"]}], # type: ignore[arg-type]
|
|
)
|
|
|
|
self.assertIsNone(result)
|
|
|
|
@patch("ddns.provider.tencentcloud.strftime")
|
|
@patch("ddns.provider.tencentcloud.time")
|
|
@patch.object(EdgeOneProvider, "_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(
|
|
"DescribeZones",
|
|
Filters=[{"Name": "zone-name", "Values": ["example.com"]}], # type: ignore[arg-type]
|
|
)
|
|
|
|
self.assertIsNone(result)
|
|
|
|
@patch("ddns.provider.tencentcloud.strftime")
|
|
@patch("ddns.provider.tencentcloud.time")
|
|
@patch.object(EdgeOneProvider, "_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(
|
|
"DescribeZones",
|
|
Filters=[{"Name": "zone-name", "Values": ["example.com"]}], # type: ignore[arg-type]
|
|
)
|
|
|
|
self.assertEqual(str(cm.exception), "Network error")
|
|
|
|
@patch.object(EdgeOneProvider, "_request")
|
|
def test_set_record_create_new(self, mock_request):
|
|
"""Test set_record creating a new acceleration domain"""
|
|
# Mock HTTP responses for the workflow
|
|
responses = [
|
|
# DescribeZones response (get zone ID for main domain)
|
|
{"Zones": [{"ZoneId": "zone-123456789", "ZoneName": "example.com"}]},
|
|
# DescribeAccelerationDomains response (no existing acceleration domain for subdomain)
|
|
{"AccelerationDomains": []},
|
|
# CreateAccelerationDomain response (acceleration domain created successfully)
|
|
{"Response": {"RequestId": "req-123456789"}},
|
|
]
|
|
mock_request.side_effect = responses
|
|
|
|
result = self.provider.set_record("www.example.com", "1.2.3.4", "A")
|
|
|
|
# Should succeed because EdgeOne supports creating new acceleration domains
|
|
self.assertTrue(result)
|
|
self.assertEqual(mock_request.call_count, 3) # Zone lookup, record query, and create calls
|
|
|
|
@patch.object(EdgeOneProvider, "_request")
|
|
def test_set_record_update_existing(self, mock_request):
|
|
"""Test set_record updating an existing acceleration domain"""
|
|
# Mock HTTP responses for the workflow
|
|
responses = [
|
|
# DescribeZones response (get zone ID for main domain)
|
|
{"Zones": [{"ZoneId": "zone-123456789", "ZoneName": "example.com"}]},
|
|
# DescribeAccelerationDomains response (existing acceleration domain found)
|
|
{
|
|
"AccelerationDomains": [
|
|
{
|
|
"ZoneId": "zone-123456789",
|
|
"DomainName": "www.example.com",
|
|
"DomainStatus": "online",
|
|
"OriginDetail": {"OriginType": "ip_domain", "Origin": "1.2.3.4", "BackupOrigin": ""},
|
|
}
|
|
]
|
|
},
|
|
# ModifyAccelerationDomain response (acceleration domain updated successfully)
|
|
{"Response": {"RequestId": "req-123456789"}},
|
|
]
|
|
mock_request.side_effect = responses
|
|
|
|
result = self.provider.set_record("www.example.com", "5.6.7.8", "A")
|
|
|
|
self.assertTrue(result)
|
|
self.assertEqual(mock_request.call_count, 3)
|
|
|
|
|
|
class TestEdgeOneProviderIntegration(BaseProviderTestCase):
|
|
"""Integration tests for EdgeOneProvider"""
|
|
|
|
def setUp(self):
|
|
"""Set up test fixtures"""
|
|
super(TestEdgeOneProviderIntegration, self).setUp()
|
|
self.provider = EdgeOneProvider(self.id, self.token)
|
|
self.logger = self.mock_logger(self.provider)
|
|
|
|
@patch.object(EdgeOneProvider, "_request")
|
|
def test_full_domain_resolution_flow(self, mock_request):
|
|
"""Test complete domain resolution flow for creating new domains"""
|
|
# Mock request responses for the workflow
|
|
responses = [
|
|
# DescribeZones response (get zone ID for main domain)
|
|
{"Zones": [{"ZoneId": "zone-123456789", "ZoneName": "example.com"}]},
|
|
# DescribeAccelerationDomains response (no existing acceleration domain for subdomain)
|
|
{"AccelerationDomains": []},
|
|
# CreateAccelerationDomain response (acceleration domain created successfully)
|
|
{"Response": {"RequestId": "req-123456789"}},
|
|
]
|
|
mock_request.side_effect = responses
|
|
|
|
result = self.provider.set_record("test.example.com", "1.2.3.4", "A", ttl=600)
|
|
|
|
# Should succeed because EdgeOne supports creating new acceleration domains
|
|
self.assertTrue(result)
|
|
self.assertEqual(mock_request.call_count, 3) # Zone lookup, record query, and create calls
|
|
|
|
@patch.object(EdgeOneProvider, "_request")
|
|
def test_custom_domain_format(self, mock_request):
|
|
"""Test custom domain format with ~ separator (create new domain)"""
|
|
# Mock request responses
|
|
responses = [
|
|
# DescribeZones response (get zone ID for main domain)
|
|
{"Zones": [{"ZoneId": "zone-123456789", "ZoneName": "example.com"}]},
|
|
# DescribeAccelerationDomains response (no existing acceleration domain for subdomain)
|
|
{"AccelerationDomains": []},
|
|
# CreateAccelerationDomain response (acceleration domain created successfully)
|
|
{"Response": {"RequestId": "req-123456789"}},
|
|
]
|
|
mock_request.side_effect = responses
|
|
|
|
result = self.provider.set_record("test~example.com", "1.2.3.4", "A")
|
|
|
|
# Should succeed because EdgeOne supports creating new acceleration domains
|
|
self.assertTrue(result)
|
|
|
|
# Zone lookup, record query, and create calls should be made
|
|
self.assertEqual(mock_request.call_count, 3)
|
|
|
|
@patch.object(EdgeOneProvider, "_request")
|
|
def test_update_existing_record(self, mock_request):
|
|
"""Test updating an existing acceleration domain"""
|
|
# Mock request responses for the workflow
|
|
responses = [
|
|
# DescribeZones response (get zone ID for main domain)
|
|
{"Zones": [{"ZoneId": "zone-123456789", "ZoneName": "example.com"}]},
|
|
# DescribeAccelerationDomains response (existing acceleration domain found)
|
|
{
|
|
"AccelerationDomains": [
|
|
{
|
|
"ZoneId": "zone-123456789",
|
|
"DomainName": "test.example.com",
|
|
"DomainStatus": "online",
|
|
"OriginDetail": {"OriginType": "ip_domain", "Origin": "1.2.3.4", "BackupOrigin": ""},
|
|
}
|
|
]
|
|
},
|
|
# ModifyAccelerationDomain response (acceleration domain updated successfully)
|
|
{"Response": {"RequestId": "req-123456789"}},
|
|
]
|
|
mock_request.side_effect = responses
|
|
|
|
result = self.provider.set_record("test.example.com", "5.6.7.8", "A", ttl=300)
|
|
|
|
self.assertTrue(result)
|
|
self.assertEqual(mock_request.call_count, 3)
|
|
|
|
# Verify the ModifyAccelerationDomain call
|
|
modify_call = mock_request.call_args_list[2]
|
|
self.assertEqual(modify_call[0][0], "ModifyAccelerationDomain")
|
|
|
|
@patch.object(EdgeOneProvider, "_request")
|
|
def test_api_error_handling(self, mock_request):
|
|
"""Test API error handling"""
|
|
# Mock API error response - the _request method returns None on error
|
|
mock_request.return_value = None
|
|
|
|
# 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)
|
|
# At least one call should be made to try to resolve zone ID
|
|
self.assertGreater(mock_request.call_count, 0)
|
|
|
|
|
|
class TestEdgeOneProviderRealRequest(BaseProviderTestCase):
|
|
"""EdgeOne Provider 真实请求测试类"""
|
|
|
|
def setUp(self):
|
|
"""Set up test fixtures"""
|
|
super(TestEdgeOneProviderRealRequest, self).setUp()
|
|
|
|
def test_auth_failure_real_request(self):
|
|
"""Test authentication failure with real API request"""
|
|
# 使用无效的认证信息创建 provider
|
|
invalid_provider = EdgeOneProvider("invalid_id", "invalid_token")
|
|
self.configure_test_http(invalid_provider)
|
|
|
|
# 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]
|
|
|
|
# 至少有一个日志应该包含EdgeOne API 错误信息
|
|
has_auth_error = any(
|
|
"edgeone 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 EdgeOne authentication error in logs: {0}".format(logged_messages))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|