Files
DDNS/tests/test_provider_dnspod.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

559 lines
23 KiB
Python

# coding=utf-8
"""
DNSPod Provider 单元测试
支持 Python 2.7 和 Python 3
"""
from base_test import BaseProviderTestCase, unittest, patch, MagicMock
from ddns.provider.dnspod import DnspodProvider
class TestDnspodProvider(BaseProviderTestCase):
"""DNSPod Provider 测试类"""
def setUp(self):
"""测试初始化"""
super(TestDnspodProvider, self).setUp()
self.provider = DnspodProvider(self.id, self.token)
def test_init_with_basic_config(self):
"""Test DnspodProvider initialization with basic configuration"""
provider = DnspodProvider(self.id, self.token)
self.assertEqual(provider.id, self.id)
self.assertEqual(provider.token, self.token)
self.assertEqual(provider.endpoint, "https://dnsapi.cn")
self.assertEqual(provider.DefaultLine, "默认")
def test_class_constants(self):
"""Test DnspodProvider class constants"""
self.assertEqual(DnspodProvider.endpoint, "https://dnsapi.cn")
self.assertEqual(DnspodProvider.DefaultLine, "默认")
# ContentType should be TYPE_FORM
from ddns.provider._base import TYPE_FORM
self.assertEqual(DnspodProvider.content_type, TYPE_FORM)
@patch("ddns.provider.dnspod.DnspodProvider._http")
def test_request_success(self, mock_http):
"""Test _request method with successful response"""
mock_response = {"status": {"code": "1", "message": "Success"}, "data": {"test": "value"}}
mock_http.return_value = mock_response
result = self.provider._request("Test.Action", test_param="test_value")
self.assertEqual(result, mock_response)
mock_http.assert_called_once()
# Verify request parameters
call_args = mock_http.call_args
self.assertEqual(call_args[0][0], "POST") # Method
self.assertEqual(call_args[0][1], "/Test.Action") # URL
# Verify body contains login token and format
body = call_args[1]["body"]
self.assertIn("login_token", body)
expected_token = "{0},{1}".format(self.id, self.token)
self.assertEqual(body["login_token"], expected_token)
self.assertEqual(body["format"], "json")
self.assertEqual(body["test_param"], "test_value")
@patch("ddns.provider.dnspod.DnspodProvider._http")
def test_request_failure(self, mock_http):
"""Test _request method with failed response"""
mock_response = {"status": {"code": "0", "message": "API Error"}}
mock_http.return_value = mock_response
# Mock logger to capture warning
self.provider.logger = MagicMock()
result = self.provider._request("Test.Action")
self.assertEqual(result, mock_response)
self.provider.logger.warning.assert_called_once()
@patch("ddns.provider.dnspod.DnspodProvider._http")
def test_request_filters_none_params(self, mock_http):
"""Test _request method filters out None parameters"""
mock_response = {"status": {"code": "1"}}
mock_http.return_value = mock_response
self.provider._request("Test.Action", param1="value1", param2=None, param3="value3")
body = mock_http.call_args[1]["body"]
self.assertEqual(body["param1"], "value1")
self.assertEqual(body["param3"], "value3")
self.assertNotIn("param2", body)
@patch("ddns.provider.dnspod.DnspodProvider._http")
def test_request_with_extra_params(self, mock_http):
"""Test _request method with extra parameters"""
mock_response = {"status": {"code": "1"}}
mock_http.return_value = mock_response
extra = {"extra_param": "extra_value"}
self.provider._request("Test.Action", extra=extra, normal_param="normal_value")
# Verify both extra and normal params are included
body = mock_http.call_args[1]["body"]
self.assertEqual(body["extra_param"], "extra_value")
self.assertEqual(body["normal_param"], "normal_value")
@patch("ddns.provider.dnspod.DnspodProvider._http")
def test_query_zone_id_success(self, mock_http):
"""Test _query_zone_id method with successful response"""
mock_http.return_value = {"domain": {"id": "12345", "name": "example.com"}}
zone_id = self.provider._query_zone_id("example.com")
self.assertEqual(zone_id, "12345")
mock_http.assert_called_once()
# Verify the action was correct
call_args = mock_http.call_args
self.assertEqual(call_args[0][1], "/Domain.Info")
@patch("ddns.provider.dnspod.DnspodProvider._http")
def test_query_zone_id_not_found(self, mock_http):
"""Test _query_zone_id method when domain is not found"""
mock_http.return_value = {}
zone_id = self.provider._query_zone_id("notfound.com")
self.assertIsNone(zone_id)
@patch("ddns.provider.dnspod.DnspodProvider._request")
def test_query_record_success_single(self, mock_request):
"""Test _query_record method with single record found"""
mock_request.return_value = {"records": [{"id": "123", "name": "www", "value": "192.168.1.1", "type": "A"}]}
record = self.provider._query_record("zone123", "www", "example.com", "A", None, {})
self.assertIsNotNone(record)
if record:
self.assertEqual(record["id"], "123")
self.assertEqual(record["name"], "www")
mock_request.assert_called_once_with(
"Record.List", domain_id="zone123", sub_domain="www", record_type="A", line=None
)
@patch("ddns.provider.dnspod.DnspodProvider._request")
def test_query_record_success_multiple(self, mock_request):
"""Test _query_record method with multiple records found"""
mock_request.return_value = {
"records": [
{"id": "123", "name": "www", "value": "192.168.1.1", "type": "A"},
{"id": "124", "name": "ftp", "value": "192.168.1.2", "type": "A"},
]
}
# Mock logger
self.provider.logger = MagicMock()
record = self.provider._query_record("zone123", "www", "example.com", "A", None, {})
self.assertIsNotNone(record)
self.assertEqual(record["name"], "www") # type: ignore[unreachable]
# Should log warning for multiple records
self.provider.logger.warning.assert_called_once()
@patch("ddns.provider.dnspod.DnspodProvider._request")
def test_query_record_not_found(self, mock_request):
"""Test _query_record method when no records found"""
mock_request.return_value = {"records": []}
# Mock logger
self.provider.logger = MagicMock()
record = self.provider._query_record("zone123", "notfound", "example.com", "A", None, {})
self.assertIsNone(record)
self.provider.logger.warning.assert_called_once()
@patch("ddns.provider.dnspod.DnspodProvider._request")
def test_create_record_success(self, mock_request):
"""Test _create_record method with successful creation"""
mock_request.return_value = {"record": {"id": "12345", "name": "www", "value": "192.168.1.1"}}
# Mock logger
self.provider.logger = MagicMock()
result = self.provider._create_record(
"zone123", "www", "example.com", "192.168.1.1", "A", ttl=600, line="电信", extra={}
)
self.assertTrue(result)
self.provider.logger.info.assert_called_once()
mock_request.assert_called_once_with(
"Record.Create",
extra={},
domain_id="zone123",
sub_domain="www",
value="192.168.1.1",
record_type="A",
record_line="电信",
ttl=600,
)
@patch("ddns.provider.dnspod.DnspodProvider._request")
def test_create_record_with_default_line(self, mock_request):
"""Test _create_record method with default line"""
mock_request.return_value = {"record": {"id": "12345", "name": "www", "value": "192.168.1.1"}}
result = self.provider._create_record("zone123", "www", "example.com", "192.168.1.1", "A", None, None, {})
self.assertTrue(result)
# Should use DefaultLine when line is not specified
call_args = mock_request.call_args[1]
self.assertEqual(call_args["record_line"], "默认")
@patch("ddns.provider.dnspod.DnspodProvider._request")
def test_create_record_failure(self, mock_request):
"""Test _create_record method with failed creation"""
mock_request.return_value = None
# Mock logger
self.provider.logger = MagicMock()
result = self.provider._create_record("zone123", "www", "example.com", "192.168.1.1", "A", None, None, {})
self.assertFalse(result)
self.provider.logger.error.assert_called_once()
@patch("ddns.provider.dnspod.DnspodProvider._request")
def test_create_record_with_extra_params(self, mock_request):
"""Test _create_record method with extra parameters"""
mock_request.return_value = {"record": {"id": "12345", "name": "www", "value": "192.168.1.1"}}
extra = {"weight": 10}
result = self.provider._create_record("zone123", "www", "example.com", "192.168.1.1", "A", None, None, extra)
self.assertTrue(result)
mock_request.assert_called_once_with(
"Record.Create",
extra=extra,
domain_id="zone123",
sub_domain="www",
value="192.168.1.1",
record_type="A",
record_line="默认",
ttl=None,
)
@patch("ddns.provider.dnspod.DnspodProvider._request")
def test_update_record_success(self, mock_request):
"""Test _update_record method with successful update"""
mock_request.return_value = {"record": {"id": "12345", "name": "www", "value": "192.168.1.2"}}
old_record = {"id": "12345", "name": "www", "line": "电信"}
# Mock logger
self.provider.logger = MagicMock()
result = self.provider._update_record("zone123", old_record, "192.168.1.2", "A", 300, None, {})
self.assertTrue(result)
self.provider.logger.debug.assert_called_once()
mock_request.assert_called_once_with(
"Record.Modify",
domain_id="zone123",
record_id="12345",
sub_domain="www",
record_type="A",
value="192.168.1.2",
record_line="电信",
extra={},
)
@patch("ddns.provider.dnspod.DnspodProvider._request")
def test_update_record_failure(self, mock_request):
"""Test _update_record method with failed update"""
mock_request.return_value = None
old_record = {"id": "12345", "name": "www"}
# Mock logger
self.provider.logger = MagicMock()
result = self.provider._update_record("zone123", old_record, "192.168.1.2", "A", None, None, {})
self.assertFalse(result)
self.provider.logger.error.assert_called_once()
@patch("ddns.provider.dnspod.DnspodProvider._request")
def test_update_record_with_line_conversion(self, mock_request):
"""Test _update_record method with line conversion (Default -> default)"""
mock_request.return_value = {"record": {"id": "12345", "name": "www", "value": "192.168.1.2"}}
old_record = {"id": "12345", "name": "www", "line": "Default"}
result = self.provider._update_record("zone123", old_record, "192.168.1.2", "A", None, None, {})
self.assertTrue(result)
# Should convert "Default" to "default"
call_args = mock_request.call_args[1]
self.assertEqual(call_args["record_line"], "default")
@patch("ddns.provider.dnspod.DnspodProvider._request")
def test_update_record_with_fallback_line(self, mock_request):
"""Test _update_record method with fallback to default line"""
mock_request.return_value = {"record": {"id": "12345", "name": "www", "value": "192.168.1.2"}}
old_record = {"id": "12345", "name": "www"} # No line specified
result = self.provider._update_record("zone123", old_record, "192.168.1.2", "A", None, None, {})
self.assertTrue(result)
# Should use DefaultLine when old record has no line
call_args = mock_request.call_args[1]
self.assertEqual(call_args["record_line"], "默认")
@patch("ddns.provider.dnspod.DnspodProvider._request")
def test_update_record_with_extra_params(self, mock_request):
"""Test _update_record method with extra parameters"""
mock_request.return_value = {"record": {"id": "12345", "name": "www", "value": "192.168.1.2"}}
old_record = {"id": "12345", "name": "www", "line": "电信"}
extra = {"weight": 20}
result = self.provider._update_record("zone123", old_record, "192.168.1.2", "A", None, None, extra)
self.assertTrue(result)
call_args = mock_request.call_args[1]
self.assertEqual(call_args["extra"], extra)
@patch("ddns.provider.dnspod.DnspodProvider._request")
def test_update_record_extra_priority_over_old_record(self, mock_request):
"""Test that extra parameters take priority over old_record values"""
mock_request.return_value = {"record": {"id": "12345", "name": "www", "value": "192.168.1.2"}}
old_record = {"id": "12345", "name": "www", "line": "电信", "weight": 10}
# extra should override old_record's weight
extra = {"weight": 20, "mx": 5}
result = self.provider._update_record("zone123", old_record, "192.168.1.2", "A", None, None, extra)
self.assertTrue(result)
call_args = mock_request.call_args[1]
self.assertEqual(call_args["extra"], extra)
# Verify extra contains the new weight value
self.assertEqual(call_args["extra"]["weight"], 20)
self.assertEqual(call_args["extra"]["mx"], 5)
def test_request_with_none_response(self):
"""Test _request method when HTTP returns None"""
with patch("ddns.provider.dnspod.DnspodProvider._http") as mock_http:
mock_http.return_value = None
self.provider.logger = MagicMock()
# Should return None and log a warning
result = self.provider._request("Test.Action")
self.assertIsNone(result)
# Verify warning was logged
self.provider.logger.warning.assert_called_once()
def test_create_record_with_no_record_in_response(self):
"""Test _create_record method when response has no record field"""
with patch("ddns.provider.dnspod.DnspodProvider._request") as mock_request:
mock_request.return_value = {"status": {"code": "1"}} # No record field
self.provider.logger = MagicMock()
result = self.provider._create_record("zone123", "www", "example.com", "192.168.1.1", "A", None, None, {})
self.assertFalse(result)
self.provider.logger.error.assert_called_once()
def test_update_record_with_no_record_in_response(self):
"""Test _update_record method when response has no record field"""
with patch("ddns.provider.dnspod.DnspodProvider._request") as mock_request:
mock_request.return_value = {"status": {"code": "1"}} # No record field
old_record = {"id": "12345", "name": "www"}
self.provider.logger = MagicMock()
result = self.provider._update_record("zone123", old_record, "192.168.1.2", "A", None, None, {})
self.assertFalse(result)
self.provider.logger.error.assert_called_once()
def test_line_configuration_support(self):
"""Test that DnspodProvider supports line configuration"""
with patch("ddns.provider.dnspod.DnspodProvider._request") as mock_request:
mock_request.return_value = {"record": {"id": "12345", "name": "www", "value": "192.168.1.1"}}
# Test create record with line parameter
result = self.provider._create_record("zone123", "www", "example.com", "192.168.1.1", "A", 600, "电信", {})
self.assertTrue(result)
mock_request.assert_called_once_with(
"Record.Create",
extra={},
domain_id="zone123",
sub_domain="www",
value="192.168.1.1",
record_type="A",
record_line="电信",
ttl=600,
)
def test_update_record_with_custom_line(self):
"""Test _update_record method with custom line parameter"""
with patch("ddns.provider.dnspod.DnspodProvider._request") as mock_request:
mock_request.return_value = {"record": {"id": "12345", "name": "www", "value": "192.168.1.2"}}
old_record = {"id": "12345", "name": "www", "line": "默认"}
# Test with custom line parameter
result = self.provider._update_record("zone123", old_record, "192.168.1.2", "A", 300, "联通", {})
self.assertTrue(result)
call_args = mock_request.call_args[1]
self.assertEqual(call_args["record_line"], "联通")
class TestDnspodProviderIntegration(BaseProviderTestCase):
"""DNSPod Provider 集成测试类"""
def setUp(self):
"""测试初始化"""
super(TestDnspodProviderIntegration, self).setUp()
self.provider = DnspodProvider(self.id, self.token)
self.provider.logger = MagicMock()
@patch("ddns.provider.dnspod.DnspodProvider._http")
def test_full_workflow_create_record(self, mock_http):
"""Test complete workflow for creating a new record"""
# Mock HTTP responses for the workflow
responses = [
# Domain.Info response
{"status": {"code": "1"}, "domain": {"id": "zone123"}},
# Record.List response (no existing records)
{"status": {"code": "1"}, "records": []},
# Record.Create response
{"status": {"code": "1"}, "record": {"id": "rec123", "name": "www", "value": "192.168.1.1"}},
]
mock_http.side_effect = responses
result = self.provider.set_record("www.example.com", "192.168.1.1")
self.assertTrue(result)
self.assertEqual(mock_http.call_count, 3)
@patch("ddns.provider.dnspod.DnspodProvider._http")
def test_full_workflow_update_record(self, mock_http):
"""Test complete workflow for updating an existing record"""
# Mock HTTP responses for the workflow
responses = [
# Domain.Info response
{"status": {"code": "1"}, "domain": {"id": "zone123"}},
# Record.List response (existing record found)
{
"status": {"code": "1"},
"records": [{"id": "rec123", "name": "www", "value": "192.168.1.100", "line": "默认"}],
},
# Record.Modify response
{"status": {"code": "1"}, "record": {"id": "rec123", "name": "www", "value": "192.168.1.1"}},
]
mock_http.side_effect = responses
result = self.provider.set_record("www.example.com", "192.168.1.1")
self.assertTrue(result)
self.assertEqual(mock_http.call_count, 3)
@patch("ddns.provider.dnspod.DnspodProvider._http")
def test_full_workflow_zone_not_found(self, mock_http):
"""Test complete workflow when zone is not found"""
# Domain.Info response - no domain found
mock_http.return_value = {"status": {"code": "0", "message": "Domain not found"}}
# Should return False when zone not found
result = self.provider.set_record("www.notfound.com", "192.168.1.1")
self.assertFalse(result)
@patch("ddns.provider.dnspod.DnspodProvider._http")
def test_full_workflow_create_failure(self, mock_http):
"""Test complete workflow when record creation fails"""
responses = [
# Domain.Info response
{"status": {"code": "1"}, "domain": {"id": "zone123"}},
# Record.List response (no existing records)
{"status": {"code": "1"}, "records": []},
# Record.Create response (failure)
{"status": {"code": "0", "message": "Create failed"}},
]
mock_http.side_effect = responses
result = self.provider.set_record("www.example.com", "192.168.1.1")
self.assertFalse(result)
self.assertEqual(mock_http.call_count, 3)
@patch("ddns.provider.dnspod.DnspodProvider._http")
def test_full_workflow_update_failure(self, mock_http):
"""Test complete workflow when record update fails"""
responses = [
# Domain.Info response
{"status": {"code": "1"}, "domain": {"id": "zone123"}},
# Record.List response (existing record found)
{
"status": {"code": "1"},
"records": [{"id": "rec123", "name": "www", "value": "192.168.1.100", "line": "默认"}],
},
# Record.Modify response (failure)
{"status": {"code": "0", "message": "Update failed"}},
]
mock_http.side_effect = responses
result = self.provider.set_record("www.example.com", "192.168.1.1")
self.assertFalse(result)
self.assertEqual(mock_http.call_count, 3)
@patch("ddns.provider.dnspod.DnspodProvider._http")
def test_full_workflow_with_options(self, mock_http):
"""Test complete workflow with additional options like ttl and line"""
responses = [
# Domain.Info response
{"status": {"code": "1"}, "domain": {"id": "zone123"}},
# Record.List response (no existing records)
{"status": {"code": "1"}, "records": []},
# Record.Create response
{"status": {"code": "1"}, "record": {"id": "rec123", "name": "www", "value": "192.168.1.1"}},
]
mock_http.side_effect = responses
result = self.provider.set_record("www.example.com", "192.168.1.1", record_type="A", ttl=300, line="电信")
self.assertTrue(result)
self.assertEqual(mock_http.call_count, 3)
# Verify the Record.Create call includes the custom options
create_call = mock_http.call_args_list[2]
body = create_call[1]["body"]
self.assertEqual(body["ttl"], 300)
self.assertEqual(body["record_line"], "电信")
class TestDnspodProviderRealRequest(BaseProviderTestCase):
"""DNSPod Provider 真实请求测试类"""
def test_auth_failure_real_request(self):
"""Test authentication failure with real API request"""
# 使用无效的认证信息创建 provider
invalid_provider = DnspodProvider("invalid_id", "invalid_token")
self.configure_test_http(invalid_provider)
# 尝试查询域名信息,应该抛出认证失败异常
with self.assertRaises(RuntimeError) as cm:
invalid_provider._query_zone_id("example.com")
# 验证异常信息包含认证失败 - 更新错误消息格式
error_message = str(cm.exception)
self.assertTrue(
"认证失败" in error_message and "401" in error_message,
"Expected authentication error message not found in: {}".format(error_message),
)
if __name__ == "__main__":
unittest.main()