Files
DDNS/tests/test_provider_edgeone.py
6e3d3560c7 fix(http): bound external requests in CI tests (#726)
* 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>
2026-08-15 22:23:27 +08:00

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()