Files
DDNS/tests/test_provider_base.py
e66dcf0240 feat(config): 支持远程配置文件 add remote HTTP(S) config loading support (#539)
* Initial plan

* feat(config): add remote HTTP(S) configuration loading support

- Add URL detection in load_config() function to support remote configuration files
- Leverage existing ddns.util.http module for HTTP requests with proxy, SSL, and auth support
- Support embedded authentication in URLs (https://user:pass@host/config.json)
- Maintain full backward compatibility with local file loading
- Skip file existence check for URLs in config path validation
- Add comprehensive test suite with 16 test cases covering various scenarios
- Support all existing configuration formats including v4.1 providers format
- Pass proxy and SSL settings from CLI/environment to HTTP requests

Co-authored-by: NewFuture <6290356+NewFuture@users.noreply.github.com>

* fix(config): address code review feedback for remote config loading

Co-authored-by: NewFuture <6290356+NewFuture@users.noreply.github.com>

* docs(config): add remote HTTP(S) configuration examples and update README

Co-authored-by: NewFuture <6290356+NewFuture@users.noreply.github.com>

* fix(config): Python 2 compatibility and code structure improvements

- Fix Python 2.7 compatibility in test_config_file_remote.py:
  - Remove encoding parameter from open() function
  - Add proper try/except import for URLError (Python 2/3 compatibility)
- Extract proxy and SSL parameter extraction outside function call in config/__init__.py
- Move remote config example from main README list to CLI usage examples section

Co-authored-by: NewFuture <6290356+NewFuture@users.noreply.github.com>

* fix(config,docs): address PR feedback and lint issues

- Remove remote config from README feature list, add as example code instead
- Fix proxy_settings fallback chain in config/__init__.py
- Add HTTP response body logging in config/file.py and fix line length
- Add real integration test for https://ddns.newfuture.cc/tests/config/debug.json
- Fix all flake8 lint issues: remove unused imports, clean whitespace

Co-authored-by: NewFuture <6290356+NewFuture@users.noreply.github.com>

* Update ddns/config/file.py

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* fix(docs,test,build): update README.en.md, add real remote tests, and improve test skipping logic

Co-authored-by: NewFuture <6290356+NewFuture@users.noreply.github.com>

* feat(http): add default user-agent and fix lint issues

Co-authored-by: NewFuture <6290356+NewFuture@users.noreply.github.com>

* feat(http): implement default user-agent handling and update related tests

* feat(http): add default User-Agent handling in request function and enhance related tests

* fix(tests): update proxy handling in multi-config tests and improve user agent type check

* fix(http): add retry logic to remote config loading and improve error messages

* chore(tests): update test structure for multi-config functionality

* fix(tests): remove unnecessary CLI argument mock in multi-config test

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: NewFuture <6290356+NewFuture@users.noreply.github.com>
Co-authored-by: New Future <NewFuture@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-07-22 01:46:01 +00:00

227 lines
8.9 KiB
Python

#!/usr/bin/env python
# coding=utf-8
"""
BaseProvider 单元测试
支持 Python 2.7 和 Python 3
"""
from base_test import BaseProviderTestCase, unittest
from ddns.provider._base import BaseProvider, encode_params
class _TestProvider(BaseProvider):
"""测试用的具体Provider实现"""
endpoint = "https://api.example.com"
def __init__(self, id="test_id", token="test_token_123456789", **options):
super(_TestProvider, self).__init__(id, token, **options)
self._test_zone_data = {"example.com": "zone123", "test.com": "zone456"}
self._test_records = {}
def _query_zone_id(self, domain):
return self._test_zone_data.get(domain)
def _query_record(self, zone_id, subdomain, main_domain, record_type, line=None, extra=None):
key = "{}-{}-{}".format(zone_id, subdomain, record_type)
return self._test_records.get(key)
def _create_record(self, zone_id, subdomain, main_domain, value, record_type, ttl=None, line=None, extra=None):
key = "{}-{}-{}".format(zone_id, subdomain, record_type)
self._test_records[key] = {"id": "rec123", "name": subdomain, "value": value, "type": record_type}
return True
def _update_record(self, zone_id, old_record, value, record_type, ttl=None, line=None, extra=None):
old_record["value"] = value
return True
class TestBaseProvider(BaseProviderTestCase):
"""BaseProvider 测试类"""
def setUp(self):
"""测试初始化"""
super(TestBaseProvider, self).setUp()
self.provider = _TestProvider()
def test_init_success(self):
"""测试正常初始化"""
provider = _TestProvider("test_id", "test_token")
self.assertEqual(provider.id, "test_id")
self.assertEqual(provider.token, "test_token")
self.assertIsNotNone(provider.logger)
self.assertEqual(provider._proxy, None) # proxy 初始化为 None
self.assertEqual(provider._zone_map, {})
def test_validate_missing_id(self):
"""测试缺少id的验证"""
with self.assertRaises(ValueError) as cm:
_TestProvider("", "token")
self.assertIn("id must be configured", str(cm.exception))
def test_validate_missing_token(self):
"""测试缺少token的验证"""
with self.assertRaises(ValueError) as cm:
_TestProvider("id", "")
self.assertIn("token must be configured", str(cm.exception))
def test_init_with_endpoint_override(self):
"""测试使用endpoint参数覆盖默认API"""
custom_endpoint = "https://custom.api.com"
provider = _TestProvider("test_id", "test_token", endpoint=custom_endpoint)
self.assertEqual(provider.endpoint, custom_endpoint)
self.assertEqual(provider.id, "test_id")
self.assertEqual(provider.token, "test_token")
def test_init_without_endpoint_uses_default(self):
"""测试不提供endpoint时使用默认API"""
provider = _TestProvider("test_id", "test_token")
self.assertEqual(provider.endpoint, "https://api.example.com") # 使用类级别的默认值
self.assertEqual(provider.id, "test_id")
self.assertEqual(provider.token, "test_token")
def test_init_with_empty_endpoint_ignored(self):
"""测试空endpoint参数被忽略"""
provider = _TestProvider("test_id", "test_token", endpoint="")
self.assertEqual(provider.endpoint, "https://api.example.com") # 使用类级别的默认值
provider = _TestProvider("test_id", "test_token", endpoint=None)
self.assertEqual(provider.endpoint, "https://api.example.com") # 使用类级别的默认值
def test_remark_exists_and_format(self):
"""测试remark存在且格式正确"""
provider = _TestProvider("test_id", "test_token")
self.assertTrue(hasattr(provider, "remark"))
self.assertIsInstance(provider.remark, str)
self.assertGreater(len(provider.remark), 0)
# 检查是否包含基本的说明信息
self.assertIn("DDNS", provider.remark)
def test_endpoint_priority_over_class_api(self):
"""测试endpoint参数优先级高于类级别API"""
# 创建一个有不同默认API的测试类
class _CustomAPIProvider(_TestProvider):
endpoint = "https://different.api.com"
# 不使用endpoint - 应该使用类级别的API
provider1 = _CustomAPIProvider("id", "token")
self.assertEqual(provider1.endpoint, "https://different.api.com")
# 使用endpoint - 应该覆盖类级别的API
custom_endpoint = "https://override.api.com"
provider2 = _CustomAPIProvider("id", "token", endpoint=custom_endpoint)
self.assertEqual(provider2.endpoint, custom_endpoint)
def test_get_zone_id_from_cache(self):
"""测试从缓存获取zone_id"""
self.provider._zone_map["cached.com"] = "cached_zone"
zone_id = self.provider.get_zone_id("cached.com")
self.assertEqual(zone_id, "cached_zone")
def test_get_zone_id_query_and_cache(self):
"""测试查询并缓存zone_id"""
zone_id = self.provider.get_zone_id("example.com")
self.assertEqual(zone_id, "zone123")
self.assertEqual(self.provider._zone_map["example.com"], "zone123")
def test_split_custom_domain_with_tilde(self):
"""测试用~分隔的自定义域名"""
from ddns.provider._base import _split_custom_domain
sub, main = _split_custom_domain("www~example.com")
self.assertEqual(sub, "www")
self.assertEqual(main, "example.com")
def test_split_custom_domain_with_plus(self):
"""测试用+分隔的自定义域名"""
from ddns.provider._base import _split_custom_domain
sub, main = _split_custom_domain("api+test.com")
self.assertEqual(sub, "api")
self.assertEqual(main, "test.com")
def test_split_custom_domain_no_separator(self):
"""测试没有分隔符的域名"""
from ddns.provider._base import _split_custom_domain
sub, main = _split_custom_domain("example.com")
self.assertIsNone(sub)
self.assertEqual(main, "example.com")
def test_join_domain_normal(self):
"""测试正常合并域名"""
from ddns.provider._base import join_domain
domain = join_domain("www", "example.com")
self.assertEqual(domain, "www.example.com")
def test_join_domain_empty_sub(self):
"""测试空子域名合并"""
from ddns.provider._base import join_domain
domain = join_domain("", "example.com")
self.assertEqual(domain, "example.com")
domain = join_domain("@", "example.com")
self.assertEqual(domain, "example.com")
def test_encode_dict(self):
"""测试编码字典参数"""
params = {"key1": "value1", "key2": "value2"}
result = encode_params(params)
# 由于字典顺序可能不同,我们检查包含关系
self.assertIn("key1=value1", result)
self.assertIn("key2=value2", result)
def test_encode_none(self):
"""测试编码None参数"""
result = encode_params(None)
self.assertEqual(result, "")
def test_mask_sensitive_data_empty(self):
"""测试空数据打码"""
result = self.provider._mask_sensitive_data("")
self.assertEqual(result, "")
result = self.provider._mask_sensitive_data(None)
self.assertEqual(result, None)
def test_mask_sensitive_data_long_token(self):
"""测试长token的打码"""
data = "token=test_token_123456789&other=value"
result = self.provider._mask_sensitive_data(data)
expected = "token=te***89&other=value"
self.assertEqual(result, expected)
def test_set_record_create(self):
"""测试创建记录"""
result = self.provider.set_record("www~example.com", "1.2.3.4", "A")
self.assertTrue(result)
# 验证记录是否被创建
record = self.provider._query_record("zone123", "www", "example.com", "A", None, {})
self.assertIsNotNone(record)
if record: # Type narrowing for mypy
self.assertEqual(record["value"], "1.2.3.4")
def test_set_record_update_existing(self):
"""测试更新现有记录"""
# 先创建一个记录
self.provider.set_record("www~example.com", "1.2.3.4", "A")
# 再更新它
result = self.provider.set_record("www~example.com", "9.8.7.6", "A")
self.assertTrue(result)
record = self.provider._query_record("zone123", "www", "example.com", "A", None, {})
if record: # Type narrowing for mypy
self.assertEqual(record["value"], "9.8.7.6")
def test_set_record_invalid_domain(self):
"""测试无效域名"""
result = self.provider.set_record("invalid.notfound", "1.2.3.4", "A")
self.assertFalse(result)
if __name__ == "__main__":
# 运行测试
unittest.main(verbosity=2)