ddns 4.1.0b3__tar.gz → 4.1.0b4__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of ddns might be problematic. Click here for more details.
- {ddns-4.1.0b3 → ddns-4.1.0b4}/PKG-INFO +2 -2
- {ddns-4.1.0b3 → ddns-4.1.0b4}/README.md +1 -1
- {ddns-4.1.0b3 → ddns-4.1.0b4}/ddns/__init__.py +2 -2
- {ddns-4.1.0b3 → ddns-4.1.0b4}/ddns/__main__.py +12 -7
- {ddns-4.1.0b3 → ddns-4.1.0b4}/ddns/ip.py +50 -4
- {ddns-4.1.0b3 → ddns-4.1.0b4}/ddns/util/fileio.py +2 -2
- ddns-4.1.0b4/ddns/util/try_run.py +37 -0
- {ddns-4.1.0b3 → ddns-4.1.0b4}/ddns.egg-info/PKG-INFO +2 -2
- {ddns-4.1.0b3 → ddns-4.1.0b4}/ddns.egg-info/SOURCES.txt +2 -0
- ddns-4.1.0b4/tests/test_ip.py +239 -0
- {ddns-4.1.0b3 → ddns-4.1.0b4}/tests/test_scheduler_base.py +37 -9
- {ddns-4.1.0b3 → ddns-4.1.0b4}/tests/test_scheduler_cron.py +119 -35
- {ddns-4.1.0b3 → ddns-4.1.0b4}/tests/test_scheduler_init.py +16 -11
- {ddns-4.1.0b3 → ddns-4.1.0b4}/tests/test_scheduler_launchd.py +32 -26
- {ddns-4.1.0b3 → ddns-4.1.0b4}/tests/test_scheduler_schtasks.py +33 -34
- {ddns-4.1.0b3 → ddns-4.1.0b4}/tests/test_scheduler_systemd.py +34 -21
- {ddns-4.1.0b3 → ddns-4.1.0b4}/LICENSE +0 -0
- {ddns-4.1.0b3 → ddns-4.1.0b4}/ddns/__builtins__.pyi +0 -0
- {ddns-4.1.0b3 → ddns-4.1.0b4}/ddns/cache.py +0 -0
- {ddns-4.1.0b3 → ddns-4.1.0b4}/ddns/provider/__init__.py +0 -0
- {ddns-4.1.0b3 → ddns-4.1.0b4}/ddns/provider/_base.py +0 -0
- {ddns-4.1.0b3 → ddns-4.1.0b4}/ddns/provider/_signature.py +0 -0
- {ddns-4.1.0b3 → ddns-4.1.0b4}/ddns/provider/alidns.py +0 -0
- {ddns-4.1.0b3 → ddns-4.1.0b4}/ddns/provider/aliesa.py +0 -0
- {ddns-4.1.0b3 → ddns-4.1.0b4}/ddns/provider/callback.py +0 -0
- {ddns-4.1.0b3 → ddns-4.1.0b4}/ddns/provider/cloudflare.py +0 -0
- {ddns-4.1.0b3 → ddns-4.1.0b4}/ddns/provider/debug.py +0 -0
- {ddns-4.1.0b3 → ddns-4.1.0b4}/ddns/provider/dnscom.py +0 -0
- {ddns-4.1.0b3 → ddns-4.1.0b4}/ddns/provider/dnspod.py +0 -0
- {ddns-4.1.0b3 → ddns-4.1.0b4}/ddns/provider/dnspod_com.py +0 -0
- {ddns-4.1.0b3 → ddns-4.1.0b4}/ddns/provider/edgeone.py +0 -0
- {ddns-4.1.0b3 → ddns-4.1.0b4}/ddns/provider/he.py +0 -0
- {ddns-4.1.0b3 → ddns-4.1.0b4}/ddns/provider/huaweidns.py +0 -0
- {ddns-4.1.0b3 → ddns-4.1.0b4}/ddns/provider/namesilo.py +0 -0
- {ddns-4.1.0b3 → ddns-4.1.0b4}/ddns/provider/noip.py +0 -0
- {ddns-4.1.0b3 → ddns-4.1.0b4}/ddns/provider/tencentcloud.py +0 -0
- {ddns-4.1.0b3 → ddns-4.1.0b4}/ddns/util/__init__.py +0 -0
- {ddns-4.1.0b3 → ddns-4.1.0b4}/ddns/util/comment.py +0 -0
- {ddns-4.1.0b3 → ddns-4.1.0b4}/ddns/util/http.py +0 -0
- {ddns-4.1.0b3 → ddns-4.1.0b4}/ddns.egg-info/dependency_links.txt +0 -0
- {ddns-4.1.0b3 → ddns-4.1.0b4}/ddns.egg-info/entry_points.txt +0 -0
- {ddns-4.1.0b3 → ddns-4.1.0b4}/ddns.egg-info/requires.txt +0 -0
- {ddns-4.1.0b3 → ddns-4.1.0b4}/ddns.egg-info/top_level.txt +0 -0
- {ddns-4.1.0b3 → ddns-4.1.0b4}/pyproject.toml +0 -0
- {ddns-4.1.0b3 → ddns-4.1.0b4}/setup.cfg +0 -0
- {ddns-4.1.0b3 → ddns-4.1.0b4}/tests/test_cache.py +0 -0
- {ddns-4.1.0b3 → ddns-4.1.0b4}/tests/test_config_cli.py +0 -0
- {ddns-4.1.0b3 → ddns-4.1.0b4}/tests/test_config_cli_task.py +0 -0
- {ddns-4.1.0b3 → ddns-4.1.0b4}/tests/test_config_config.py +0 -0
- {ddns-4.1.0b3 → ddns-4.1.0b4}/tests/test_config_env.py +0 -0
- {ddns-4.1.0b3 → ddns-4.1.0b4}/tests/test_config_file.py +0 -0
- {ddns-4.1.0b3 → ddns-4.1.0b4}/tests/test_config_file_remote.py +0 -0
- {ddns-4.1.0b3 → ddns-4.1.0b4}/tests/test_config_init.py +0 -0
- {ddns-4.1.0b3 → ddns-4.1.0b4}/tests/test_config_init_multi.py +0 -0
- {ddns-4.1.0b3 → ddns-4.1.0b4}/tests/test_config_schema_v4_1.py +0 -0
- {ddns-4.1.0b3 → ddns-4.1.0b4}/tests/test_provider__signature.py +0 -0
- {ddns-4.1.0b3 → ddns-4.1.0b4}/tests/test_provider_alidns.py +0 -0
- {ddns-4.1.0b3 → ddns-4.1.0b4}/tests/test_provider_aliesa.py +0 -0
- {ddns-4.1.0b3 → ddns-4.1.0b4}/tests/test_provider_base.py +0 -0
- {ddns-4.1.0b3 → ddns-4.1.0b4}/tests/test_provider_base_simple.py +0 -0
- {ddns-4.1.0b3 → ddns-4.1.0b4}/tests/test_provider_callback.py +0 -0
- {ddns-4.1.0b3 → ddns-4.1.0b4}/tests/test_provider_cloudflare.py +0 -0
- {ddns-4.1.0b3 → ddns-4.1.0b4}/tests/test_provider_debug.py +0 -0
- {ddns-4.1.0b3 → ddns-4.1.0b4}/tests/test_provider_dnscom.py +0 -0
- {ddns-4.1.0b3 → ddns-4.1.0b4}/tests/test_provider_dnspod.py +0 -0
- {ddns-4.1.0b3 → ddns-4.1.0b4}/tests/test_provider_dnspod_com.py +0 -0
- {ddns-4.1.0b3 → ddns-4.1.0b4}/tests/test_provider_edgeone.py +0 -0
- {ddns-4.1.0b3 → ddns-4.1.0b4}/tests/test_provider_he.py +0 -0
- {ddns-4.1.0b3 → ddns-4.1.0b4}/tests/test_provider_huaweidns.py +0 -0
- {ddns-4.1.0b3 → ddns-4.1.0b4}/tests/test_provider_namesilo.py +0 -0
- {ddns-4.1.0b3 → ddns-4.1.0b4}/tests/test_provider_noip.py +0 -0
- {ddns-4.1.0b3 → ddns-4.1.0b4}/tests/test_provider_proxy_list.py +0 -0
- {ddns-4.1.0b3 → ddns-4.1.0b4}/tests/test_provider_tencentcloud.py +0 -0
- {ddns-4.1.0b3 → ddns-4.1.0b4}/tests/test_util_comment.py +0 -0
- {ddns-4.1.0b3 → ddns-4.1.0b4}/tests/test_util_fileio.py +0 -0
- {ddns-4.1.0b3 → ddns-4.1.0b4}/tests/test_util_http.py +0 -0
- {ddns-4.1.0b3 → ddns-4.1.0b4}/tests/test_util_http_proxy_list.py +0 -0
- {ddns-4.1.0b3 → ddns-4.1.0b4}/tests/test_util_http_retry.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ddns
|
|
3
|
-
Version: 4.1.
|
|
3
|
+
Version: 4.1.0b4
|
|
4
4
|
Summary: Dynamic DNS client for multiple providers, supporting IPv4 and IPv6.
|
|
5
5
|
Author-email: NewFuture <python@newfuture.cc>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -138,7 +138,7 @@ Dynamic: license-file
|
|
|
138
138
|
也可使用一键安装脚本自动下载并安装对应平台的二进制:
|
|
139
139
|
|
|
140
140
|
```bash
|
|
141
|
-
curl
|
|
141
|
+
curl -#fSL https://ddns.newfuture.cc/install.sh | sh
|
|
142
142
|
```
|
|
143
143
|
提示:安装到系统目录(如 /usr/local/bin)可能需要 root 或 sudo 权限;若权限不足,可改为 `sudo sh` 运行。
|
|
144
144
|
|
|
@@ -4,16 +4,16 @@ DDNS
|
|
|
4
4
|
@author: NewFuture, rufengsuixing
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
+
import sys
|
|
7
8
|
from io import TextIOWrapper
|
|
8
|
-
from subprocess import check_output
|
|
9
9
|
from logging import getLogger
|
|
10
|
-
import
|
|
10
|
+
from subprocess import check_output
|
|
11
11
|
|
|
12
|
-
from .__init__ import __version__, __description__, build_date
|
|
13
|
-
from .config import load_configs, Config # noqa: F401
|
|
14
|
-
from .provider import get_provider_class, SimpleProvider # noqa: F401
|
|
15
12
|
from . import ip
|
|
13
|
+
from .__init__ import __description__, __version__, build_date
|
|
16
14
|
from .cache import Cache
|
|
15
|
+
from .config import Config, load_configs # noqa: F401
|
|
16
|
+
from .provider import SimpleProvider, get_provider_class # noqa: F401
|
|
17
17
|
|
|
18
18
|
logger = getLogger()
|
|
19
19
|
|
|
@@ -102,11 +102,16 @@ def run(config):
|
|
|
102
102
|
|
|
103
103
|
|
|
104
104
|
def main():
|
|
105
|
-
|
|
106
|
-
if
|
|
105
|
+
stdout = sys.stdout # pythonw 模式无 stdout
|
|
106
|
+
if stdout and stdout.encoding and stdout.encoding.lower() != "utf-8" and hasattr(stdout, "buffer"):
|
|
107
107
|
# 兼容windows 和部分ASCII编码的老旧系统
|
|
108
108
|
sys.stdout = TextIOWrapper(sys.stdout.buffer, encoding="utf-8")
|
|
109
109
|
sys.stderr = TextIOWrapper(sys.stderr.buffer, encoding="utf-8")
|
|
110
|
+
|
|
111
|
+
# Windows 下输出一个空行
|
|
112
|
+
if stdout and sys.platform.startswith("win"):
|
|
113
|
+
stdout.write("\r\n")
|
|
114
|
+
|
|
110
115
|
logger.name = "ddns"
|
|
111
116
|
|
|
112
117
|
# 使用多配置加载器,它会自动处理单个和多个配置
|
|
@@ -16,6 +16,24 @@ IPV4_REG = r"((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1
|
|
|
16
16
|
# https://community.helpsystems.com/forums/intermapper/miscellaneous-topics/5acc4fcf-fa83-e511-80cf-0050568460e4
|
|
17
17
|
IPV6_REG = r"((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:)))" # noqa: E501
|
|
18
18
|
|
|
19
|
+
# 公网IPv4 API列表,按优先级排序
|
|
20
|
+
PUBLIC_IPV4_APIS = [
|
|
21
|
+
"https://api.ipify.cn",
|
|
22
|
+
"https://api.ipify.org",
|
|
23
|
+
"https://4.ipw.cn/",
|
|
24
|
+
"https://ipinfo.io/ip",
|
|
25
|
+
"https://api-ipv4.ip.sb/ip",
|
|
26
|
+
"http://checkip.amazonaws.com",
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
# 公网IPv6 API列表,按优先级排序
|
|
30
|
+
PUBLIC_IPV6_APIS = [
|
|
31
|
+
"https://api6.ipify.org/",
|
|
32
|
+
"https://6.ipw.cn/",
|
|
33
|
+
"https://api-ipv6.ip.sb/ip",
|
|
34
|
+
"http://ipv6.icanhazip.com",
|
|
35
|
+
]
|
|
36
|
+
|
|
19
37
|
|
|
20
38
|
def default_v4(): # 默认连接外网的ipv4
|
|
21
39
|
s = socket(AF_INET, SOCK_DGRAM)
|
|
@@ -60,12 +78,40 @@ def _open(url, reg):
|
|
|
60
78
|
error(e)
|
|
61
79
|
|
|
62
80
|
|
|
63
|
-
def
|
|
64
|
-
|
|
81
|
+
def _try_multiple_apis(api_list, reg, ip_type):
|
|
82
|
+
"""
|
|
83
|
+
Try multiple API endpoints until one succeeds
|
|
84
|
+
"""
|
|
85
|
+
for url in api_list:
|
|
86
|
+
try:
|
|
87
|
+
debug("Trying %s API: %s", ip_type, url)
|
|
88
|
+
result = _open(url, reg)
|
|
89
|
+
if result:
|
|
90
|
+
debug("Successfully got %s from %s: %s", ip_type, url, result)
|
|
91
|
+
return result
|
|
92
|
+
else:
|
|
93
|
+
debug("No valid IP found from %s", url)
|
|
94
|
+
except Exception as e:
|
|
95
|
+
debug("Failed to get %s from %s: %s", ip_type, url, e)
|
|
96
|
+
return None
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def public_v4(url=None, reg=IPV4_REG): # 公网IPV4地址
|
|
100
|
+
if url:
|
|
101
|
+
# 使用指定URL
|
|
102
|
+
return _open(url, reg)
|
|
103
|
+
else:
|
|
104
|
+
# 使用多个API自动重试
|
|
105
|
+
return _try_multiple_apis(PUBLIC_IPV4_APIS, reg, "IPv4")
|
|
65
106
|
|
|
66
107
|
|
|
67
|
-
def public_v6(url=
|
|
68
|
-
|
|
108
|
+
def public_v6(url=None, reg=IPV6_REG): # 公网IPV6地址
|
|
109
|
+
if url:
|
|
110
|
+
# 使用指定URL
|
|
111
|
+
return _open(url, reg)
|
|
112
|
+
else:
|
|
113
|
+
# 使用多个API自动重试
|
|
114
|
+
return _try_multiple_apis(PUBLIC_IPV6_APIS, reg, "IPv6")
|
|
69
115
|
|
|
70
116
|
|
|
71
117
|
def _ip_regex_match(parrent_regex, match_regex):
|
|
@@ -23,7 +23,7 @@ def _ensure_directory_exists(file_path): # type: (str) -> None
|
|
|
23
23
|
os.makedirs(directory)
|
|
24
24
|
|
|
25
25
|
|
|
26
|
-
def read_file_safely(file_path, encoding="utf-8", default=None): # type: (str, str, str|None) -> str
|
|
26
|
+
def read_file_safely(file_path, encoding="utf-8", default=None): # type: (str, str, str|None) -> str
|
|
27
27
|
"""
|
|
28
28
|
Safely read file content with UTF-8 encoding, return None if file doesn't exist or can't be read
|
|
29
29
|
|
|
@@ -37,7 +37,7 @@ def read_file_safely(file_path, encoding="utf-8", default=None): # type: (str,
|
|
|
37
37
|
try:
|
|
38
38
|
return read_file(file_path, encoding)
|
|
39
39
|
except Exception:
|
|
40
|
-
return default
|
|
40
|
+
return default # type: ignore
|
|
41
41
|
|
|
42
42
|
|
|
43
43
|
def write_file_safely(file_path, content, encoding="utf-8"): # type: (str, str, str) -> bool
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# -*- coding:utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
Utility: Safe command execution wrapper used across the project.
|
|
4
|
+
Provides a single try_run function with consistent behavior.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import subprocess
|
|
8
|
+
import sys
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def try_run(command, logger=None, **kwargs):
|
|
12
|
+
# type: (list, object, **object) -> str | None
|
|
13
|
+
"""Safely run a subprocess command and return decoded output or None on failure.
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
command (list): Command array to execute
|
|
17
|
+
logger (object, optional): Logger instance for debug output
|
|
18
|
+
**kwargs: Additional arguments passed to subprocess.check_output
|
|
19
|
+
|
|
20
|
+
Returns:
|
|
21
|
+
str or None: Command output as string, or None if command failed
|
|
22
|
+
|
|
23
|
+
- Adds a default timeout=60s on Python 3 to avoid hangs
|
|
24
|
+
- Decodes output as text via universal_newlines=True
|
|
25
|
+
- Logs at debug level when logger is provided
|
|
26
|
+
"""
|
|
27
|
+
try:
|
|
28
|
+
if sys.version_info[0] >= 3 and "timeout" not in kwargs:
|
|
29
|
+
kwargs["timeout"] = 60
|
|
30
|
+
return subprocess.check_output(command, universal_newlines=True, **kwargs) # type: ignore
|
|
31
|
+
except Exception as e: # noqa: BLE001 - broad for subprocess safety
|
|
32
|
+
if logger is not None:
|
|
33
|
+
try:
|
|
34
|
+
logger.debug("Command failed: %s", e) # type: ignore
|
|
35
|
+
except Exception:
|
|
36
|
+
pass
|
|
37
|
+
return None
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ddns
|
|
3
|
-
Version: 4.1.
|
|
3
|
+
Version: 4.1.0b4
|
|
4
4
|
Summary: Dynamic DNS client for multiple providers, supporting IPv4 and IPv6.
|
|
5
5
|
Author-email: NewFuture <python@newfuture.cc>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -138,7 +138,7 @@ Dynamic: license-file
|
|
|
138
138
|
也可使用一键安装脚本自动下载并安装对应平台的二进制:
|
|
139
139
|
|
|
140
140
|
```bash
|
|
141
|
-
curl
|
|
141
|
+
curl -#fSL https://ddns.newfuture.cc/install.sh | sh
|
|
142
142
|
```
|
|
143
143
|
提示:安装到系统目录(如 /usr/local/bin)可能需要 root 或 sudo 权限;若权限不足,可改为 `sudo sh` 运行。
|
|
144
144
|
|
|
@@ -34,6 +34,7 @@ ddns/util/__init__.py
|
|
|
34
34
|
ddns/util/comment.py
|
|
35
35
|
ddns/util/fileio.py
|
|
36
36
|
ddns/util/http.py
|
|
37
|
+
ddns/util/try_run.py
|
|
37
38
|
tests/test_cache.py
|
|
38
39
|
tests/test_config_cli.py
|
|
39
40
|
tests/test_config_cli_task.py
|
|
@@ -44,6 +45,7 @@ tests/test_config_file_remote.py
|
|
|
44
45
|
tests/test_config_init.py
|
|
45
46
|
tests/test_config_init_multi.py
|
|
46
47
|
tests/test_config_schema_v4_1.py
|
|
48
|
+
tests/test_ip.py
|
|
47
49
|
tests/test_provider__signature.py
|
|
48
50
|
tests/test_provider_alidns.py
|
|
49
51
|
tests/test_provider_aliesa.py
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
# -*- coding:utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
Tests for ddns.ip module including integration tests
|
|
4
|
+
"""
|
|
5
|
+
from __init__ import unittest, patch, MagicMock
|
|
6
|
+
from ddns import ip
|
|
7
|
+
from ddns.__main__ import get_ip
|
|
8
|
+
from ddns.util.http import HttpResponse
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class TestIpModule(unittest.TestCase):
|
|
12
|
+
"""测试IP获取模块"""
|
|
13
|
+
|
|
14
|
+
def setUp(self):
|
|
15
|
+
"""设置测试环境"""
|
|
16
|
+
self.original_ssl_verify = ip.ssl_verify
|
|
17
|
+
|
|
18
|
+
def tearDown(self):
|
|
19
|
+
"""清理测试环境"""
|
|
20
|
+
ip.ssl_verify = self.original_ssl_verify
|
|
21
|
+
|
|
22
|
+
@patch('ddns.ip.request')
|
|
23
|
+
def test_url_v4_success(self, mock_request):
|
|
24
|
+
"""测试自定义URL获取IPv4 - 成功"""
|
|
25
|
+
# 模拟成功响应
|
|
26
|
+
mock_response = MagicMock()
|
|
27
|
+
mock_response.body = "1.2.3.4"
|
|
28
|
+
mock_request.return_value = mock_response
|
|
29
|
+
|
|
30
|
+
result = ip.public_v4("https://test.example.com/ip")
|
|
31
|
+
|
|
32
|
+
self.assertEqual(result, "1.2.3.4")
|
|
33
|
+
mock_request.assert_called_once_with("GET", "https://test.example.com/ip", verify=ip.ssl_verify, retries=2)
|
|
34
|
+
|
|
35
|
+
@patch('ddns.ip.request')
|
|
36
|
+
def test_url_v6_success(self, mock_request):
|
|
37
|
+
"""测试自定义URL获取IPv6 - 成功"""
|
|
38
|
+
# 模拟成功响应
|
|
39
|
+
mock_response = MagicMock()
|
|
40
|
+
mock_response.body = "2001:db8::1"
|
|
41
|
+
mock_request.return_value = mock_response
|
|
42
|
+
|
|
43
|
+
result = ip.public_v6("https://test.example.com/ipv6")
|
|
44
|
+
|
|
45
|
+
self.assertEqual(result, "2001:db8::1")
|
|
46
|
+
mock_request.assert_called_once_with("GET", "https://test.example.com/ipv6", verify=ip.ssl_verify, retries=2)
|
|
47
|
+
|
|
48
|
+
@patch('ddns.ip.request')
|
|
49
|
+
def test_url_v4_request_failure(self, mock_request):
|
|
50
|
+
"""测试自定义URL获取IPv4 - 请求失败"""
|
|
51
|
+
# 模拟请求异常
|
|
52
|
+
mock_request.side_effect = Exception("Network error")
|
|
53
|
+
|
|
54
|
+
result = ip.public_v4("https://test.example.com/ip")
|
|
55
|
+
|
|
56
|
+
self.assertIsNone(result)
|
|
57
|
+
mock_request.assert_called_once_with("GET", "https://test.example.com/ip", verify=ip.ssl_verify, retries=2)
|
|
58
|
+
|
|
59
|
+
@patch('ddns.ip.request')
|
|
60
|
+
def test_url_v4_invalid_response(self, mock_request):
|
|
61
|
+
"""测试自定义URL获取IPv4 - 无效响应"""
|
|
62
|
+
# 模拟无效响应
|
|
63
|
+
mock_response = MagicMock()
|
|
64
|
+
mock_response.body = "invalid response"
|
|
65
|
+
mock_request.return_value = mock_response
|
|
66
|
+
|
|
67
|
+
result = ip.public_v4("https://test.example.com/ip")
|
|
68
|
+
|
|
69
|
+
self.assertIsNone(result)
|
|
70
|
+
mock_request.assert_called_once_with("GET", "https://test.example.com/ip", verify=ip.ssl_verify, retries=2)
|
|
71
|
+
|
|
72
|
+
@patch('ddns.ip.request')
|
|
73
|
+
def test_public_v4_multiple_apis_first_success(self, mock_request):
|
|
74
|
+
"""测试公网IPv4获取 - 多个API第一个成功"""
|
|
75
|
+
# 模拟第一个API成功
|
|
76
|
+
mock_response = MagicMock()
|
|
77
|
+
mock_response.body = "1.2.3.4"
|
|
78
|
+
mock_request.return_value = mock_response
|
|
79
|
+
|
|
80
|
+
result = ip.public_v4()
|
|
81
|
+
|
|
82
|
+
self.assertEqual(result, "1.2.3.4")
|
|
83
|
+
# 应该只调用第一个API
|
|
84
|
+
mock_request.assert_called_once_with("GET", ip.PUBLIC_IPV4_APIS[0], verify=ip.ssl_verify, retries=2)
|
|
85
|
+
|
|
86
|
+
@patch('ddns.ip.request')
|
|
87
|
+
def test_public_v4_multiple_apis_fallback_success(self, mock_request):
|
|
88
|
+
"""测试公网IPv4获取 - 多个API第一个失败第二个成功"""
|
|
89
|
+
def mock_request_side_effect(method, url, **kwargs):
|
|
90
|
+
if url == ip.PUBLIC_IPV4_APIS[0]:
|
|
91
|
+
raise Exception("First API failed")
|
|
92
|
+
else:
|
|
93
|
+
mock_response = MagicMock()
|
|
94
|
+
mock_response.body = "1.2.3.4"
|
|
95
|
+
return mock_response
|
|
96
|
+
|
|
97
|
+
mock_request.side_effect = mock_request_side_effect
|
|
98
|
+
|
|
99
|
+
result = ip.public_v4()
|
|
100
|
+
|
|
101
|
+
self.assertEqual(result, "1.2.3.4")
|
|
102
|
+
# 应该调用前两个API
|
|
103
|
+
self.assertEqual(mock_request.call_count, 2)
|
|
104
|
+
mock_request.assert_any_call("GET", ip.PUBLIC_IPV4_APIS[0], verify=ip.ssl_verify, retries=2)
|
|
105
|
+
mock_request.assert_any_call("GET", ip.PUBLIC_IPV4_APIS[1], verify=ip.ssl_verify, retries=2)
|
|
106
|
+
|
|
107
|
+
@patch('ddns.ip.request')
|
|
108
|
+
def test_public_v4_multiple_apis_all_fail(self, mock_request):
|
|
109
|
+
"""测试公网IPv4获取 - 多个API全部失败"""
|
|
110
|
+
# 模拟所有API都失败
|
|
111
|
+
mock_request.side_effect = Exception("All APIs failed")
|
|
112
|
+
|
|
113
|
+
result = ip.public_v4()
|
|
114
|
+
|
|
115
|
+
self.assertIsNone(result)
|
|
116
|
+
# 应该调用所有API
|
|
117
|
+
self.assertEqual(mock_request.call_count, len(ip.PUBLIC_IPV4_APIS))
|
|
118
|
+
|
|
119
|
+
@patch('ddns.ip.request')
|
|
120
|
+
def test_public_v6_multiple_apis_first_success(self, mock_request):
|
|
121
|
+
"""测试公网IPv6获取 - 多个API第一个成功"""
|
|
122
|
+
# 模拟第一个API成功
|
|
123
|
+
mock_response = MagicMock()
|
|
124
|
+
mock_response.body = "2001:db8::1"
|
|
125
|
+
mock_request.return_value = mock_response
|
|
126
|
+
|
|
127
|
+
result = ip.public_v6()
|
|
128
|
+
|
|
129
|
+
self.assertEqual(result, "2001:db8::1")
|
|
130
|
+
# 应该只调用第一个API
|
|
131
|
+
mock_request.assert_called_once_with("GET", ip.PUBLIC_IPV6_APIS[0], verify=ip.ssl_verify, retries=2)
|
|
132
|
+
|
|
133
|
+
@patch('ddns.ip.request')
|
|
134
|
+
def test_public_v6_multiple_apis_fallback_success(self, mock_request):
|
|
135
|
+
"""测试公网IPv6获取 - 多个API第一个失败第二个成功"""
|
|
136
|
+
def mock_request_side_effect(method, url, **kwargs):
|
|
137
|
+
if url == ip.PUBLIC_IPV6_APIS[0]:
|
|
138
|
+
raise Exception("First API failed")
|
|
139
|
+
else:
|
|
140
|
+
mock_response = MagicMock()
|
|
141
|
+
mock_response.body = "2001:db8::1"
|
|
142
|
+
return mock_response
|
|
143
|
+
|
|
144
|
+
mock_request.side_effect = mock_request_side_effect
|
|
145
|
+
|
|
146
|
+
result = ip.public_v6()
|
|
147
|
+
|
|
148
|
+
self.assertEqual(result, "2001:db8::1")
|
|
149
|
+
# 应该调用前两个API
|
|
150
|
+
self.assertEqual(mock_request.call_count, 2)
|
|
151
|
+
mock_request.assert_any_call("GET", ip.PUBLIC_IPV6_APIS[0], verify=ip.ssl_verify, retries=2)
|
|
152
|
+
mock_request.assert_any_call("GET", ip.PUBLIC_IPV6_APIS[1], verify=ip.ssl_verify, retries=2)
|
|
153
|
+
|
|
154
|
+
def test_public_ipv4_apis_list_exists(self):
|
|
155
|
+
"""测试IPv4 API列表存在并包含所需的API"""
|
|
156
|
+
expected_apis = [
|
|
157
|
+
"https://api.ipify.cn",
|
|
158
|
+
"https://api.ipify.org",
|
|
159
|
+
"https://4.ipw.cn/",
|
|
160
|
+
"https://ipinfo.io/ip",
|
|
161
|
+
"https://api-ipv4.ip.sb/ip",
|
|
162
|
+
"http://checkip.amazonaws.com",
|
|
163
|
+
]
|
|
164
|
+
self.assertEqual(ip.PUBLIC_IPV4_APIS, expected_apis)
|
|
165
|
+
|
|
166
|
+
def test_public_ipv6_apis_list_exists(self):
|
|
167
|
+
"""测试IPv6 API列表存在并包含所需的API"""
|
|
168
|
+
expected_apis = [
|
|
169
|
+
"https://api6.ipify.org/",
|
|
170
|
+
"https://6.ipw.cn/",
|
|
171
|
+
"https://api-ipv6.ip.sb/ip",
|
|
172
|
+
"http://ipv6.icanhazip.com",
|
|
173
|
+
]
|
|
174
|
+
self.assertEqual(ip.PUBLIC_IPV6_APIS, expected_apis)
|
|
175
|
+
|
|
176
|
+
@patch('ddns.ip.request')
|
|
177
|
+
def test_get_ip_public_mode_fallback(self, mock_request):
|
|
178
|
+
"""测试通过get_ip使用public模式的自动fallback功能"""
|
|
179
|
+
# 模拟第一个API失败,第二个成功
|
|
180
|
+
def mock_request_side_effect(method, url, **kwargs):
|
|
181
|
+
if "api.ipify.cn" in url:
|
|
182
|
+
raise Exception("First API failed")
|
|
183
|
+
elif "api.ipify.org" in url:
|
|
184
|
+
mock_response = MagicMock()
|
|
185
|
+
mock_response.body = "1.2.3.4"
|
|
186
|
+
return mock_response
|
|
187
|
+
else:
|
|
188
|
+
raise Exception("Unexpected URL")
|
|
189
|
+
|
|
190
|
+
mock_request.side_effect = mock_request_side_effect
|
|
191
|
+
|
|
192
|
+
# 使用"public"规则获取IPv4地址
|
|
193
|
+
result = get_ip("4", ["public"])
|
|
194
|
+
|
|
195
|
+
self.assertEqual(result, "1.2.3.4")
|
|
196
|
+
# 应该调用了前两个API
|
|
197
|
+
self.assertEqual(mock_request.call_count, 2)
|
|
198
|
+
|
|
199
|
+
@patch('ddns.ip.request')
|
|
200
|
+
def test_get_ip_url_mode_backward_compatibility(self, mock_request):
|
|
201
|
+
"""测试通过get_ip使用url:模式的向后兼容性"""
|
|
202
|
+
# 模拟成功响应
|
|
203
|
+
mock_response = MagicMock()
|
|
204
|
+
mock_response.body = "1.2.3.4"
|
|
205
|
+
mock_request.return_value = mock_response
|
|
206
|
+
|
|
207
|
+
# 使用"url:"规则获取IPv4地址
|
|
208
|
+
result = get_ip("4", ["url:https://custom.api.com/ip"])
|
|
209
|
+
|
|
210
|
+
self.assertEqual(result, "1.2.3.4")
|
|
211
|
+
# 应该只调用指定的API
|
|
212
|
+
mock_request.assert_called_once()
|
|
213
|
+
args, kwargs = mock_request.call_args
|
|
214
|
+
self.assertEqual(args[1], "https://custom.api.com/ip")
|
|
215
|
+
|
|
216
|
+
@patch('ddns.ip.request')
|
|
217
|
+
def test_get_ip_multiple_rules_limitation(self, mock_request):
|
|
218
|
+
"""测试get_ip的多规则限制 - 当前实现的限制"""
|
|
219
|
+
# 注意:当前get_ip实现有限制 - 如果一个规则返回None,它不会尝试下一个规则
|
|
220
|
+
# 只有当规则抛出异常时才会尝试下一个规则
|
|
221
|
+
# 这是一个已知限制,不在本次功能实现范围内
|
|
222
|
+
|
|
223
|
+
# 模拟所有public API都失败(返回无效响应)
|
|
224
|
+
mock_response = MagicMock()
|
|
225
|
+
mock_response.body = "invalid response"
|
|
226
|
+
mock_request.return_value = mock_response
|
|
227
|
+
|
|
228
|
+
# 使用多个规则:先尝试public,失败后应该尝试url:指定的API
|
|
229
|
+
# 但由于当前实现限制,public返回None后不会尝试下一个规则
|
|
230
|
+
result = get_ip("4", ["public", "url:https://backup.api.com/ip"])
|
|
231
|
+
|
|
232
|
+
# 由于当前实现限制,返回None而不是继续尝试backup API
|
|
233
|
+
self.assertIsNone(result)
|
|
234
|
+
# 只调用了public APIs,没有调用backup API
|
|
235
|
+
self.assertEqual(mock_request.call_count, len(ip.PUBLIC_IPV4_APIS))
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
if __name__ == '__main__':
|
|
239
|
+
unittest.main()
|
|
@@ -4,7 +4,8 @@ Unit tests for ddns.scheduler._base module
|
|
|
4
4
|
@author: NewFuture
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
-
from __init__ import
|
|
7
|
+
from __init__ import patch, unittest
|
|
8
|
+
|
|
8
9
|
from ddns.scheduler._base import BaseScheduler
|
|
9
10
|
|
|
10
11
|
|
|
@@ -58,8 +59,10 @@ class TestBaseScheduler(unittest.TestCase):
|
|
|
58
59
|
|
|
59
60
|
command = self.scheduler._build_ddns_command(ddns_args)
|
|
60
61
|
|
|
61
|
-
self.assertIsInstance(command,
|
|
62
|
-
|
|
62
|
+
self.assertIsInstance(command, list)
|
|
63
|
+
command_str = " ".join(command)
|
|
64
|
+
self.assertIn("python", command_str.lower())
|
|
65
|
+
self.assertIn("-m", command)
|
|
63
66
|
self.assertIn("ddns", command)
|
|
64
67
|
self.assertIn("--dns", command)
|
|
65
68
|
self.assertIn("debug", command)
|
|
@@ -98,8 +101,9 @@ class TestBaseScheduler(unittest.TestCase):
|
|
|
98
101
|
|
|
99
102
|
command = self.scheduler._build_ddns_command(ddns_args)
|
|
100
103
|
|
|
101
|
-
self.assertIn("--debug
|
|
102
|
-
self.assertIn("
|
|
104
|
+
self.assertIn("--debug", command)
|
|
105
|
+
self.assertIn("true", command)
|
|
106
|
+
self.assertIn("--cache", command)
|
|
103
107
|
|
|
104
108
|
def test_build_ddns_command_filters_debug_false(self):
|
|
105
109
|
"""Test _build_ddns_command filters out debug=False"""
|
|
@@ -107,8 +111,10 @@ class TestBaseScheduler(unittest.TestCase):
|
|
|
107
111
|
|
|
108
112
|
command = self.scheduler._build_ddns_command(ddns_args)
|
|
109
113
|
|
|
110
|
-
|
|
111
|
-
self.
|
|
114
|
+
command_str = " ".join(command)
|
|
115
|
+
self.assertNotIn("--debug false", command_str)
|
|
116
|
+
self.assertIn("--cache", command)
|
|
117
|
+
self.assertIn("true", command)
|
|
112
118
|
|
|
113
119
|
def test_build_ddns_command_with_single_values(self):
|
|
114
120
|
"""Test _build_ddns_command with single value arguments"""
|
|
@@ -153,7 +159,8 @@ class TestBaseScheduler(unittest.TestCase):
|
|
|
153
159
|
command = self.scheduler._build_ddns_command(ddns_args)
|
|
154
160
|
|
|
155
161
|
# Should use sys.executable for Python path
|
|
156
|
-
|
|
162
|
+
command_str = " ".join(command)
|
|
163
|
+
self.assertIn("python", command_str.lower())
|
|
157
164
|
|
|
158
165
|
def test_build_ddns_command_with_special_characters(self):
|
|
159
166
|
"""Test _build_ddns_command handles special characters"""
|
|
@@ -161,7 +168,7 @@ class TestBaseScheduler(unittest.TestCase):
|
|
|
161
168
|
|
|
162
169
|
command = self.scheduler._build_ddns_command(ddns_args)
|
|
163
170
|
|
|
164
|
-
self.assertIsInstance(command,
|
|
171
|
+
self.assertIsInstance(command, list)
|
|
165
172
|
self.assertIn("test-domain.example.com", command)
|
|
166
173
|
self.assertIn("test_token_with_special_chars!@#", command)
|
|
167
174
|
|
|
@@ -192,6 +199,27 @@ class TestBaseScheduler(unittest.TestCase):
|
|
|
192
199
|
result = self.scheduler.disable()
|
|
193
200
|
self.assertIsInstance(result, bool)
|
|
194
201
|
|
|
202
|
+
def test_quote_command_array(self):
|
|
203
|
+
"""Test _quote_command_array method"""
|
|
204
|
+
# Test basic functionality
|
|
205
|
+
cmd_array = ["python", "script.py"]
|
|
206
|
+
result = self.scheduler._quote_command_array(cmd_array)
|
|
207
|
+
self.assertEqual(result, "python script.py")
|
|
208
|
+
|
|
209
|
+
# Test with spaces
|
|
210
|
+
cmd_array = ["python", "script with spaces.py", "normal_arg"]
|
|
211
|
+
result = self.scheduler._quote_command_array(cmd_array)
|
|
212
|
+
self.assertEqual(result, 'python "script with spaces.py" normal_arg')
|
|
213
|
+
|
|
214
|
+
# Test with multiple spaced arguments
|
|
215
|
+
cmd_array = ["python", "-m", "ddns", "--config", "config file.json"]
|
|
216
|
+
result = self.scheduler._quote_command_array(cmd_array)
|
|
217
|
+
self.assertEqual(result, 'python -m ddns --config "config file.json"')
|
|
218
|
+
|
|
219
|
+
# Test empty array
|
|
220
|
+
result = self.scheduler._quote_command_array([])
|
|
221
|
+
self.assertEqual(result, "")
|
|
222
|
+
|
|
195
223
|
|
|
196
224
|
if __name__ == "__main__":
|
|
197
225
|
unittest.main()
|