Browse Source

feat(provider:NameSilo):添加NameSilo支持(待验证) (#514)

* Implement NameSilo DNS provider with comprehensive test coverage

Co-authored-by: NewFuture <[email protected]>

* Refactor NameSilo provider based on code review feedback

Co-authored-by: NewFuture <[email protected]>

* Add NameSilo to CLI, schema, and documentation

Co-authored-by: NewFuture <[email protected]>

* Fix NameSilo provider based on code review feedback and resolve flake8 linting issues

Co-authored-by: NewFuture <[email protected]>

* Add warning if ID is configured for NameSilo provider

NameSilo only requires API key (token) authentication and does not need the ID field. Added warning message to inform users when ID is unnecessarily configured.

Co-authored-by: NewFuture <[email protected]>

* Fix NameSilo provider based on code review and add to README index

Co-authored-by: NewFuture <[email protected]>

* Fix NameSilo provider based on code review: update domain parameter usage and clarify API parameter meanings

Co-authored-by: NewFuture <[email protected]>

* Remove dict handling logic and single record test, fix rrhost parameter in NameSilo provider

Co-authored-by: NewFuture <[email protected]>

---------

Co-authored-by: copilot-swe-agent[bot] <[email protected]>
Co-authored-by: NewFuture <[email protected]>
Co-authored-by: New Future <[email protected]>
Copilot 4 months ago
parent
commit
22c9cf8d54

+ 2 - 0
README.en.md

@@ -45,6 +45,7 @@
   - [CloudFlare](https://www.cloudflare.com/) ([Configuration Guide](doc/providers/cloudflare.en.md)) (@tongyifan)
   - [HE.net](https://dns.he.net/) ([Configuration Guide](doc/providers/he.en.md)) (@NN708) (Does not support auto-record creation)
   - [Huawei Cloud](https://huaweicloud.com/) ([Configuration Guide](doc/providers/huaweidns.en.md)) (@cybmp3) ⚡
+  - [NameSilo](https://www.namesilo.com/) ([Configuration Guide](doc/providers/namesilo.en.md))
   - [Tencent Cloud](https://cloud.tencent.com/) ([Configuration Guide](doc/providers/tencentcloud.en.md)) ⚡
   - [No-IP](https://www.noip.com/) ([Configuration Guide](doc/providers/noip.en.md))
   - Custom Callback API ([Configuration Guide](doc/providers/callback.en.md))
@@ -124,6 +125,7 @@ Docker version is recommended for best compatibility, small size, and optimized
    - **CloudFlare**: [API Key](https://support.cloudflare.com/hc/en-us/articles/200167836-Where-do-I-find-my-Cloudflare-API-key-) (Besides `email + API KEY`, you can also use `Token`, **requires list Zone permission**) | [Detailed Configuration](doc/providers/cloudflare.en.md)
    - **HE.net**: [DDNS Documentation](https://dns.he.net/docs.html) (Only fill the set password in the `token` field, `id` field can be left empty) | [Detailed Configuration](doc/providers/he.en.md)
    - **Huawei Cloud DNS**: [APIKEY Application](https://console.huaweicloud.com/iam/) (Click Access Keys on the left, then click Create Access Key) | [Detailed Configuration](doc/providers/huaweidns.en.md)
+   - **NameSilo**: [API Key](https://www.namesilo.com/account/api-manager) (Get API Key from API Manager) | [Detailed Configuration](doc/providers/namesilo.en.md)
    - **Tencent Cloud DNS**: [Detailed Configuration](doc/providers/tencentcloud.en.md)
    - **No-IP**: [Username and Password](https://www.noip.com/) (Use No-IP account username and password) | [Detailed Configuration](doc/providers/noip.en.md)
    - **Custom Callback**: For parameter configuration, please refer to the custom callback configuration instructions below

+ 4 - 2
README.md

@@ -45,6 +45,7 @@
   - [CloudFlare](https://www.cloudflare.com/) ([配置指南](doc/providers/cloudflare.md)) (@tongyifan)
   - [HE.net](https://dns.he.net/) ([配置指南](doc/providers/he.md)) (@NN708) (不支持自动创建记录)
   - [华为云](https://huaweicloud.com/) ([配置指南](doc/providers/huaweidns.md)) (@cybmp3) ⚡
+  - [NameSilo](https://www.namesilo.com/) ([配置指南](doc/providers/namesilo.md))
   - [腾讯云](https://cloud.tencent.com/) ([配置指南](doc/providers/tencentcloud.md)) ⚡
   - [No-IP](https://www.noip.com/) ([配置指南](doc/providers/noip.md))
   - 自定义回调 API ([配置指南](doc/providers/callback.md))
@@ -124,6 +125,7 @@
    - **CloudFlare**: [API Key](https://support.cloudflare.com/hc/en-us/articles/200167836-Where-do-I-find-my-Cloudflare-API-key-)(除了 `email + API KEY`,也可使用 `Token`,**需要list Zone 权限**) | [详细配置文档](doc/providers/cloudflare.md)
    - **HE.net**: [DDNS 文档](https://dns.he.net/docs.html)(仅需将设置的密码填入 `token` 字段,`id` 字段可留空) | [详细配置文档](doc/providers/he.md)
    - **华为云 DNS**: [APIKEY 申请](https://console.huaweicloud.com/iam/)(点左边访问密钥,然后点新增访问密钥) | [详细配置文档](doc/providers/huaweidns.md)
+   - **NameSilo**: [API Key](https://www.namesilo.com/account/api-manager)(API Manager 中获取 API Key) | [详细配置文档](doc/providers/namesilo.md)
    - **腾讯云 DNS**: [详细配置文档](doc/providers/tencentcloud.md)
    - **No-IP**: [用户名和密码](https://www.noip.com/)(使用 No-IP 账户的用户名和密码) | [详细配置文档](doc/providers/noip.md)
    - **自定义回调**: 参数填写方式请查看下方的自定义回调配置说明
@@ -176,7 +178,7 @@ python -m ddns -c /path/to/config.json
 | :----: | :----------------: | :------: | :---------: | :----------------: | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
 |   id   |       string       |    √     |     无      |    api 访问 ID     | Cloudflare 为邮箱(使用 Token 时留空)<br>HE.net 可留空<br>华为云为 Access Key ID (AK)                                                                                                   |
 | token  |       string       |    √     |     无      |   api 授权 token   | 部分平台叫 secret key,**反馈粘贴时删除**                                                                                                                                                |
-|  dns   |       string       |    No    | `"dnspod"`  |     dns 服务商     | 阿里 DNS 为 `alidns`,阿里ESA为 `aliesa`,Cloudflare 为 `cloudflare`,dns.com 为 `dnscom`,DNSPOD 国内为 `dnspod`,DNSPOD 国际为 `dnspod_com`,HE.net 为 `he`,华为云为 `huaweidns`,腾讯云为 `tencentcloud`,No-IP 为 `noip`,自定义回调为 `callback`。部分服务商有[详细配置文档](doc/providers/) |
+|  dns   |       string       |    No    | `"dnspod"`  |     dns 服务商     | 阿里 DNS 为 `alidns`,阿里ESA为 `aliesa`,Cloudflare 为 `cloudflare`,dns.com 为 `dnscom`,DNSPOD 国内为 `dnspod`,DNSPOD 国际为 `dnspod_com`,HE.net 为 `he`,华为云为 `huaweidns`,NameSilo 为 `namesilo`,腾讯云为 `tencentcloud`,No-IP 为 `noip`,自定义回调为 `callback`。部分服务商有[详细配置文档](doc/providers/) |
 |  ipv4  |       array        |    No    |    `[]`     |   ipv4 域名列表    | 为 `[]` 时,不会获取和更新 IPv4 地址                                                                                                                                                     |
 |  ipv6  |       array        |    No    |    `[]`     |   ipv6 域名列表    | 为 `[]` 时,不会获取和更新 IPv6 地址                                                                                                                                                     |
 | index4 | string\|int\|array |    No    | `"default"` |   ipv4 获取方式    | 可设置 `网卡`、`内网`、`公网`、`正则` 等方式                                                                                                                                             |
@@ -225,7 +227,7 @@ python -m ddns -c /path/to/config.json
   "$schema": "https://ddns.newfuture.cc/schema/v4.0.json",
   "id": "12345",
   "token": "mytokenkey",
-  "dns": "dnspod 或 dnspod_com 或 alidns 或 aliesa 或 dnscom 或 cloudflare 或 he 或 huaweidns 或 tencentcloud 或 noip 或 callback",
+  "dns": "dnspod 或 dnspod_com 或 alidns 或 aliesa 或 dnscom 或 cloudflare 或 he 或 huaweidns 或 namesilo 或 tencentcloud 或 noip 或 callback",
   "ipv4": ["ddns.newfuture.cc", "ipv4.ddns.newfuture.cc"],
   "ipv6": ["ddns.newfuture.cc", "ipv6.ddns.newfuture.cc"],
   "index4": 0,

+ 1 - 0
ddns/config/cli.py

@@ -145,6 +145,7 @@ def load_config(description, doc, version, date):
             "dnspod",
             "he",
             "huaweidns",
+            "namesilo",
             "noip",
             "tencentcloud",
         ],

+ 4 - 0
ddns/provider/__init__.py

@@ -10,6 +10,7 @@ from .dnspod import DnspodProvider
 from .dnspod_com import DnspodComProvider
 from .he import HeProvider
 from .huaweidns import HuaweiDNSProvider
+from .namesilo import NamesiloProvider
 from .noip import NoipProvider
 from .tencentcloud import TencentCloudProvider
 
@@ -55,6 +56,9 @@ def get_provider_class(provider_name):
         "huaweidns": HuaweiDNSProvider,
         "huawei": HuaweiDNSProvider,  # 兼容huawei
         "huaweicloud": HuaweiDNSProvider,
+        # namesilo
+        "namesilo": NamesiloProvider,
+        "namesilo_com": NamesiloProvider,  # 兼容namesilo.com
         # no-ip
         "noip": NoipProvider,
         "no-ip": NoipProvider,  # 兼容no-ip

+ 159 - 0
ddns/provider/namesilo.py

@@ -0,0 +1,159 @@
+# coding=utf-8
+"""
+NameSilo API Provider
+DNS provider implementation for NameSilo domain registrar
+@doc: https://www.namesilo.com/api-reference
+@author: NewFuture & Copilot
+"""
+
+from ._base import BaseProvider, TYPE_JSON
+
+
+class NamesiloProvider(BaseProvider):
+    """
+    NameSilo DNS API Provider
+
+    Supports DNS record management through NameSilo's API including:
+    - Domain information retrieval
+    - DNS record listing
+    - DNS record creation
+    - DNS record updating
+    """
+
+    endpoint = "https://www.namesilo.com"
+    content_type = TYPE_JSON
+
+    def _validate(self):
+        """Validate authentication credentials"""
+        # NameSilo only requires API key (token), not ID
+        if not self.token:
+            raise ValueError("API key (token) must be configured for NameSilo")
+        if not self.endpoint:
+            raise ValueError("API endpoint must be defined in {}".format(self.__class__.__name__))
+
+        # Warn if ID is configured since NameSilo doesn't need it
+        if self.id:
+            self.logger.warning("NameSilo does not require 'id' configuration - only API key (token) is needed")
+
+        # Show pending verification warning
+        self.logger.warning("NameSilo provider implementation is pending verification - please test thoroughly")
+
+    def _request(self, operation, **params):
+        # type: (str, **(str | int | bytes | bool | None)) -> dict
+        """
+        Send request to NameSilo API
+
+        Args:
+            operation (str): API operation name
+            params: API parameters
+
+        Returns:
+            dict: API response data
+        """
+        # Filter out None parameters
+        params = {k: v for k, v in params.items() if v is not None}
+
+        # Add required authentication and format parameters
+        params.update({"version": "1", "type": "json", "key": self.token})
+
+        # Make API request
+        response = self._http("GET", "/api/" + operation, queries=params)
+
+        # Parse response
+        if response and isinstance(response, dict):
+            reply = response.get("reply", {})
+
+            # Check for successful response
+            if reply.get("code") == "300":  # NameSilo success code
+                return reply
+            else:
+                # Log error details
+                error_msg = reply.get("detail", "Unknown error")
+                self.logger.warning("NameSilo API error [%s]: %s", reply.get("code", "unknown"), error_msg)
+
+        return None
+
+    def _query_zone_id(self, domain):
+        # type: (str) -> str | None
+        """
+        Query domain information to get domain as zone identifier
+        @doc: https://www.namesilo.com/api-reference#domains/get-domain-info
+        """
+        response = self._request("getDomainInfo", domain=domain)
+
+        if response:
+            # Domain exists, return the domain name as zone_id
+            domain_info = response.get("domain", {})
+            if domain_info:
+                self.logger.debug("Domain found: %s", domain)
+                return domain
+
+        self.logger.warning("Domain not found or not accessible: %s", domain)
+        return None
+
+    def _query_record(self, zone_id, subdomain, main_domain, record_type, line, extra):
+        # type: (str, str, str, str, str | None, dict) -> dict | None
+        """
+        Query existing DNS record
+        @doc: https://www.namesilo.com/api-reference#dns/list-dns-records
+        """
+        response = self._request("dnsListRecords", domain=main_domain)
+
+        if response:
+            records = response.get("resource_record", [])
+
+            # Find matching record
+            for record in records:
+                if record.get("host") == subdomain and record.get("type") == record_type:
+                    self.logger.debug("Found existing record: %s", record)
+                    return record
+
+        self.logger.debug("No matching record found for %s.%s (%s)", subdomain, main_domain, record_type)
+        return None
+
+    def _create_record(self, zone_id, subdomain, main_domain, value, record_type, ttl, line, extra):
+        # type: (str, str, str, str, str, int | str | None, str | None, dict) -> bool
+        """
+        Create new DNS record
+        @doc: https://www.namesilo.com/api-reference#dns/add-dns-record
+        """
+        response = self._request(
+            "dnsAddRecord", domain=main_domain, rrtype=record_type, rrhost=subdomain, rrvalue=value, rrttl=ttl
+        )
+
+        if response:
+            record_id = response.get("record_id")
+            self.logger.info("DNS record created successfully: %s", record_id)
+            return True
+        else:
+            self.logger.error("Failed to create DNS record")
+            return False
+
+    def _update_record(self, zone_id, old_record, value, record_type, ttl, line, extra):
+        # type: (str, dict, str, str, int | str | None, str | None, dict) -> bool
+        """
+        Update existing DNS record
+        @doc: https://www.namesilo.com/api-reference#dns/update-dns-record
+        """
+        record_id = old_record.get("record_id")
+        if not record_id:
+            self.logger.error("No record_id found in old_record: %s", old_record)
+            return False
+
+        # In NameSilo, zone_id is the main domain name
+        response = self._request(
+            "dnsUpdateRecord",
+            rrid=record_id,
+            domain=zone_id,  # zone_id is main_domain in NameSilo
+            rrhost=old_record.get("host"),  # host field contains subdomain
+            rrvalue=value,
+            rrtype=record_type,
+            rrttl=ttl or old_record.get("ttl"),
+        )
+
+        if response:
+            self.logger.info("DNS record updated successfully: %s", record_id)
+            return True
+        else:
+            self.logger.error("Failed to update DNS record")
+            return False

+ 1 - 0
doc/providers/README.md

@@ -16,6 +16,7 @@
 | `dnspod` | [DNSPod 中国版](https://www.dnspod.cn/) | [dnspod 中文文档](dnspod.md) | [dnspod English Doc](dnspod.en.md) | 国内最大DNS服务商 |
 | `he` | [HE.net](https://dns.he.net/) | [he 中文文档](he.md) | [he English Doc](he.en.md) | ⚠️ 等待验证,不支持自动创建记录 |
 | `huaweidns` | [华为云 DNS](https://www.huaweicloud.com/product/dns.html) | [huaweidns 中文文档](huaweidns.md) | [huaweidns English Doc](huaweidns.en.md) | ⚠️ 等待验证 |
+| `namesilo` | [NameSilo](https://www.namesilo.com/) | [namesilo 中文文档](namesilo.md) | [namesilo English Doc](namesilo.en.md) | ⚠️ 等待验证 |
 | `noip` | [No-IP](https://www.noip.com/) | [noip 中文文档](noip.md) | [noip English Doc](noip.en.md) | 不支持自动创建记录 |
 | `tencentcloud` | [腾讯云 DNSPod](https://cloud.tencent.com/product/dns) | [tencentcloud 中文文档](tencentcloud.md) | [tencentcloud English Doc](tencentcloud.en.md) | 腾讯云DNSPod服务 |
 

+ 138 - 0
doc/providers/namesilo.en.md

@@ -0,0 +1,138 @@
+# NameSilo DNS Configuration Guide
+
+> **Important Note**: The NameSilo provider implementation is pending verification. Please test thoroughly before using in production environments.
+
+## Overview
+
+NameSilo is a US-based domain registrar and DNS service provider that offers reliable domain management and DNS resolution services. This DDNS project supports automatic DNS record management through the NameSilo API.
+
+## Authentication Method
+
+### API Key Authentication
+
+NameSilo uses API Key for authentication, which is the only available authentication method.
+
+```json
+{
+    "dns": "namesilo",
+    "token": "your_api_key_here"
+}
+```
+
+## Getting Authentication Information
+
+### API Key
+
+1. Log in to [NameSilo Console](https://www.namesilo.com/account_home.php)
+2. Go to "Account Options" → "API Manager" or visit <https://www.namesilo.com/account/api-manager>
+3. Generate a new API Key
+4. Record the generated API Key and keep it secure
+
+> **Note**: The API Key has full account permissions. Please keep it secure and do not share it with others.
+
+## Permission Requirements
+
+NameSilo API Key has the following permissions:
+- **Domain Management**: View and manage all domains in your account
+- **DNS Record Management**: Create, read, update, and delete DNS records
+- **Domain Information Query**: Get domain registration information and status
+
+## Configuration Examples
+
+### Basic Configuration
+
+```json
+{
+    "dns": "namesilo",
+    "token": "c40031261ee449dda629d2df14e9cb63",
+    "ipv4": ["ddns.example.com", "www.example.com"],
+    "index4": ["default"]
+}
+```
+
+### Complete Configuration Example
+
+```json
+{
+    "dns": "namesilo",
+    "token": "c40031261ee449dda629d2df14e9cb63",
+    "index4": ["default"],
+    "index6": ["default"],
+    "ipv4": ["ddns.example.com", "www.example.com"],
+    "ipv6": ["ddns.example.com"],
+    "ttl": 3600
+}
+```
+
+## Optional Parameters
+
+| Parameter | Description | Type | Default |
+|-----------|-------------|------|---------|
+| `ttl` | TTL value for DNS records (seconds) | int | 7207 |
+
+> **Note**: NameSilo TTL minimum value is 300 seconds (5 minutes), maximum value is 2592000 seconds (30 days).
+
+### Custom API Endpoint
+
+In special circumstances, you may need to customize the endpoint:
+
+```json
+{
+    "endpoint": "https://www.namesilo.com"
+}
+```
+
+> **Note**: The official NameSilo API endpoint is `https://www.namesilo.com`. It's not recommended to modify this unless using a proxy service.
+
+## Troubleshooting
+
+### Common Errors
+
+#### "Invalid API key"
+- Check if the API Key is correct
+- Ensure the API Key is not disabled
+- Verify account status is normal
+
+#### "Domain not found"
+- Confirm the domain is added to your NameSilo account
+- Check if the domain spelling is correct
+- Verify the domain status is Active
+
+#### "Record creation failed"
+- Check if the subdomain format is correct
+- Ensure TTL value is within allowed range (300-2592000 seconds)
+- Verify there are no conflicting records
+
+#### "API request limit exceeded"
+- NameSilo has API call rate limits
+- Increase update intervals appropriately
+- Avoid concurrent API calls
+
+### Debug Mode
+
+Enable debug logging to see detailed information:
+
+```sh
+ddns -c config.json --debug
+```
+
+### API Response Codes
+
+- **300**: Success
+- **110**: Domain does not exist
+- **280**: Invalid domain format
+- **200**: Invalid API Key
+
+## API Limitations
+
+- **Request Rate**: Recommended maximum 60 requests per minute
+- **Domain Count**: Limited based on account type
+- **Record Count**: Maximum 100 DNS records per domain
+
+## Related Links
+
+- [NameSilo API Documentation](https://www.namesilo.com/api-reference)
+- [NameSilo Console](https://www.namesilo.com/account_home.php)
+- [NameSilo API Manager](https://www.namesilo.com/account/api-manager)
+
+> **Security Tip**: It's recommended to regularly rotate API Keys and monitor account activity logs to ensure secure API usage.

+ 138 - 0
doc/providers/namesilo.md

@@ -0,0 +1,138 @@
+# NameSilo DNS 配置指南
+
+> **重要说明**:NameSilo 提供商实现正在等待验证,请在生产环境使用前进行充分测试。
+
+## 概述
+
+NameSilo 是一家美国域名注册商和DNS服务提供商,提供可靠的域名管理和DNS解析服务。本 DDNS 项目支持通过 NameSilo API 进行 DNS 记录的自动管理。
+
+## 认证方式
+
+### API Key 认证
+
+NameSilo 使用 API Key 进行身份验证,这是唯一的认证方式。
+
+```json
+{
+    "dns": "namesilo",
+    "token": "your_api_key_here"
+}
+```
+
+## 获取认证信息
+
+### API Key
+
+1. 登录 [NameSilo 控制台](https://www.namesilo.com/account_home.php)
+2. 进入「Account Options」→「API Manager」或访问 <https://www.namesilo.com/account/api-manager>
+3. 生成新的 API Key
+4. 记录下生成的 API Key,请妥善保存
+
+> **注意**:API Key 具有账户的完整权限,请确保妥善保管,不要泄露给他人。
+
+## 权限要求
+
+NameSilo API Key 具有以下权限:
+- **域名管理**:查看和管理您账户下的所有域名
+- **DNS记录管理**:创建、读取、更新和删除DNS记录
+- **域名信息查询**:获取域名注册信息和状态
+
+## 配置示例
+
+### 基本配置
+
+```json
+{
+    "dns": "namesilo",
+    "token": "c40031261ee449dda629d2df14e9cb63",
+    "ipv4": ["ddns.example.com", "www.example.com"],
+    "index4": ["default"]
+}
+```
+
+### 完整配置示例
+
+```json
+{
+    "dns": "namesilo",
+    "token": "c40031261ee449dda629d2df14e9cb63",
+    "index4": ["default"],
+    "index6": ["default"],
+    "ipv4": ["ddns.example.com", "www.example.com"],
+    "ipv6": ["ddns.example.com"],
+    "ttl": 3600
+}
+```
+
+## 可选参数
+
+| 参数 | 说明 | 类型 | 默认值 |
+|------|------|------|-------|
+| `ttl` | DNS记录的TTL值(秒) | int | 7207 |
+
+> **注意**:NameSilo TTL 最小值为 300 秒(5分钟),最大值为 2592000 秒(30天)。
+
+### 自定义API端点
+
+在特殊情况下可能需要自定义端点:
+
+```json
+{
+    "endpoint": "https://www.namesilo.com"
+}
+```
+
+> **注意**:NameSilo 官方 API 端点为 `https://www.namesilo.com`,除非使用代理服务,否则不建议修改。
+
+## 故障排除
+
+### 常见错误
+
+#### "Invalid API key"
+- 检查 API Key 是否正确
+- 确认 API Key 没有被禁用
+- 验证账户状态是否正常
+
+#### "Domain not found" 
+- 确认域名已添加到 NameSilo 账户
+- 检查域名拼写是否正确
+- 验证域名状态是否为 Active
+
+#### "Record creation failed"
+- 检查子域名格式是否正确
+- 确认 TTL 值在允许范围内(300-2592000秒)
+- 验证是否存在冲突的记录
+
+#### "API request limit exceeded"
+- NameSilo 有 API 调用频率限制
+- 适当增加更新间隔
+- 避免并发调用 API
+
+### 调试模式
+
+启用调试日志查看详细信息:
+
+```sh
+ddns -c config.json --debug
+```
+
+### API 响应代码
+
+- **300**:成功
+- **110**:域名不存在
+- **280**:无效的域名格式
+- **200**:无效的 API Key
+
+## API 限制
+
+- **请求频率**:建议每分钟不超过 60 次请求
+- **域名数量**:根据账户类型不同而限制
+- **记录数量**:每个域名最多 100 条 DNS 记录
+
+## 相关链接
+
+- [NameSilo API 文档](https://www.namesilo.com/api-reference)
+- [NameSilo 控制台](https://www.namesilo.com/account_home.php)
+- [NameSilo API Manager](https://www.namesilo.com/account/api-manager)
+
+> **安全提示**:建议定期轮换 API Key,并监控账户活动日志,确保 API 使用安全。

+ 2 - 1
schema/v4.0.json

@@ -48,7 +48,7 @@
       "$id": "/properties/dns",
       "type": "string",
       "title": "DNS Provider",
-      "description": "dns服务商:阿里为alidns,阿里ESA为aliesa,DNS.COM为dnscom,DNSPOD国际版为(dnspod_com),cloudflare,HE.net为he,华为DNS为huaweidns,自定义回调为callback",
+      "description": "dns服务商:阿里为alidns,阿里ESA为aliesa,DNS.COM为dnscom,DNSPOD国际版为(dnspod_com),cloudflare,HE.net为he,华为DNS为huaweidns,NameSilo为namesilo,自定义回调为callback",
       "default": "dnspod",
       "examples": [
         "dnspod",
@@ -66,6 +66,7 @@
         "dnspod",
         "he",
         "huaweidns",
+        "namesilo",
         "noip",
         "tencentcloud"
       ]

+ 317 - 0
tests/test_provider_namesilo.py

@@ -0,0 +1,317 @@
+# coding=utf-8
+"""
+Unit tests for NameSilo DNS provider
+@author: NewFuture & Copilot
+"""
+
+from base_test import BaseProviderTestCase, unittest, patch
+from ddns.provider.namesilo import NamesiloProvider
+
+
+class TestNamesiloProvider(BaseProviderTestCase):
+    """Test cases for NameSilo DNS provider"""
+
+    def setUp(self):
+        """Set up test fixtures"""
+        super(TestNamesiloProvider, self).setUp()
+        self.provider = NamesiloProvider(self.authid, self.token)
+
+    def test_init_with_basic_config(self):
+        """Test basic provider initialization"""
+        self.assertProviderInitialized(self.provider)
+        self.assertEqual(self.provider.endpoint, "https://www.namesilo.com")
+        self.assertEqual(self.provider.content_type, "application/json")
+
+    def test_init_with_custom_endpoint(self):
+        """Test provider initialization with custom endpoint"""
+        custom_endpoint = "https://api.custom.namesilo.com"
+        provider = NamesiloProvider(self.authid, self.token, endpoint=custom_endpoint)
+        self.assertEqual(provider.endpoint, custom_endpoint)
+
+    def test_validate_success(self):
+        """Test successful credential validation"""
+        # Should not raise exception
+        try:
+            self.provider._validate()
+        except Exception as e:
+            self.fail("_validate() raised unexpected exception: {}".format(e))
+
+    def test_validate_missing_token(self):
+        """Test validation with missing token"""
+        with self.assertRaises(ValueError) as context:
+            NamesiloProvider(self.authid, "")
+        self.assertIn("API key", str(context.exception))
+
+    def test_validate_missing_id_allowed(self):
+        """Test validation with missing ID (should be allowed)"""
+        # Should not raise exception - ID is not strictly required for NameSilo
+        try:
+            provider = NamesiloProvider("", self.token)
+            self.assertEqual(provider.token, self.token)
+        except Exception as e:
+            self.fail("_validate() raised unexpected exception with missing ID: {}".format(e))
+
+    @patch.object(NamesiloProvider, "_http")
+    def test_request_success(self, mock_http):
+        """Test successful API request"""
+        mock_response = {"reply": {"code": "300", "detail": "success", "data": {"test": "value"}}}
+        mock_http.return_value = mock_response
+
+        result = self.provider._request("testOperation", domain="example.com")
+
+        # Verify request parameters
+        mock_http.assert_called_once()
+        args, kwargs = mock_http.call_args
+        self.assertEqual(args[0], "GET")
+        self.assertEqual(args[1], "/api/testOperation")
+
+        expected_queries = {"domain": "example.com", "version": "1", "type": "json", "key": self.token}
+        self.assertEqual(kwargs["queries"], expected_queries)
+
+        # Verify response
+        self.assertEqual(result["code"], "300")
+
+    @patch.object(NamesiloProvider, "_http")
+    def test_request_api_error(self, mock_http):
+        """Test API request with error response"""
+        mock_response = {"reply": {"code": "400", "detail": "Invalid domain"}}
+        mock_http.return_value = mock_response
+
+        # Mock logger to capture warning
+        mock_logger = self.mock_logger(self.provider)
+
+        result = self.provider._request("testOperation", domain="invalid.com")
+
+        # Verify warning was logged
+        mock_logger.warning.assert_called_once()
+        warning_call = mock_logger.warning.call_args[0]
+        self.assertIn("400", warning_call[1])
+        self.assertIn("Invalid domain", warning_call[2])
+
+        # Should return None on error
+        self.assertIsNone(result)
+
+    @patch.object(NamesiloProvider, "_request")
+    def test_query_zone_id_success(self, mock_request):
+        """Test successful zone ID query"""
+        mock_request.return_value = {"code": "300", "domain": {"domain": "example.com"}}
+
+        result = self.provider._query_zone_id("example.com")
+
+        mock_request.assert_called_once_with("getDomainInfo", domain="example.com")
+        self.assertEqual(result, "example.com")
+
+    @patch.object(NamesiloProvider, "_request")
+    def test_query_zone_id_not_found(self, mock_request):
+        """Test zone ID query for non-existent domain"""
+        mock_request.return_value = {"code": "400", "detail": "Domain not found"}
+
+        result = self.provider._query_zone_id("nonexistent.com")
+
+        self.assertIsNone(result)
+
+    @patch.object(NamesiloProvider, "_request")
+    def test_query_record_success_multiple_records(self, mock_request):
+        """Test successful record query with multiple records"""
+        mock_request.return_value = {
+            "code": "300",
+            "resource_record": [
+                {"record_id": "12345", "host": "test", "type": "A", "value": "1.2.3.4", "ttl": "3600"},
+                {"record_id": "67890", "host": "other", "type": "A", "value": "5.6.7.8", "ttl": "3600"},
+            ],
+        }
+
+        result = self.provider._query_record("example.com", "test", "example.com", "A", None, {})
+
+        self.assertIsNotNone(result)
+        self.assertEqual(result["record_id"], "12345")
+        self.assertEqual(result["host"], "test")
+
+    @patch.object(NamesiloProvider, "_request")
+    def test_query_record_not_found(self, mock_request):
+        """Test record query when no matching record is found"""
+        mock_request.return_value = {
+            "code": "300",
+            "resource_record": [
+                {"record_id": "67890", "host": "other", "type": "A", "value": "5.6.7.8", "ttl": "3600"}
+            ],
+        }
+
+        result = self.provider._query_record("example.com", "test", "example.com", "A", None, {})
+
+        self.assertIsNone(result)
+
+    @patch.object(NamesiloProvider, "_request")
+    def test_create_record_success(self, mock_request):
+        """Test successful record creation"""
+        mock_request.return_value = {"code": "300", "record_id": "12345"}
+
+        result = self.provider._create_record("example.com", "test", "example.com", "1.2.3.4", "A", 3600, None, {})
+
+        expected_params = {
+            "domain": "example.com",
+            "rrtype": "A",
+            "rrhost": "test",
+            "rrvalue": "1.2.3.4",
+            "rrttl": 3600,
+        }
+        mock_request.assert_called_once_with("dnsAddRecord", **expected_params)
+        self.assertTrue(result)
+
+    @patch.object(NamesiloProvider, "_request")
+    def test_create_record_without_ttl(self, mock_request):
+        """Test record creation without TTL"""
+        mock_request.return_value = {"code": "300", "record_id": "12345"}
+
+        result = self.provider._create_record("example.com", "test", "example.com", "1.2.3.4", "A", None, None, {})
+
+        expected_params = {
+            "domain": "example.com",
+            "rrtype": "A",
+            "rrhost": "test",
+            "rrvalue": "1.2.3.4",
+            "rrttl": None,
+        }
+        mock_request.assert_called_once_with("dnsAddRecord", **expected_params)
+        self.assertTrue(result)
+
+    @patch.object(NamesiloProvider, "_request")
+    def test_create_record_failure(self, mock_request):
+        """Test failed record creation"""
+        mock_request.return_value = None
+
+        result = self.provider._create_record("example.com", "test", "example.com", "invalid", "A", 3600, None, {})
+
+        self.assertFalse(result)
+
+    @patch.object(NamesiloProvider, "_request")
+    def test_update_record_success(self, mock_request):
+        """Test successful record update"""
+        mock_request.return_value = {"code": "300"}
+
+        old_record = {"record_id": "12345", "host": "test", "type": "A", "value": "1.2.3.4", "ttl": "3600"}
+
+        result = self.provider._update_record("example.com", old_record, "5.6.7.8", "A", 7200, None, {})
+
+        expected_params = {
+            "domain": "example.com",
+            "rrid": "12345",
+            "rrhost": "test",
+            "rrvalue": "5.6.7.8",
+            "rrtype": "A",
+            "rrttl": 7200,
+        }
+        mock_request.assert_called_once_with("dnsUpdateRecord", **expected_params)
+        self.assertTrue(result)
+
+    @patch.object(NamesiloProvider, "_request")
+    def test_update_record_keep_existing_ttl(self, mock_request):
+        """Test record update keeping existing TTL"""
+        mock_request.return_value = {"code": "300"}
+
+        old_record = {"record_id": "12345", "host": "test", "type": "A", "value": "1.2.3.4", "ttl": "3600"}
+
+        result = self.provider._update_record("example.com", old_record, "5.6.7.8", "A", None, None, {})
+
+        expected_params = {
+            "domain": "example.com",
+            "rrid": "12345",
+            "rrhost": "test",
+            "rrvalue": "5.6.7.8",
+            "rrtype": "A",
+            "rrttl": "3600",  # Should keep existing TTL
+        }
+        mock_request.assert_called_once_with("dnsUpdateRecord", **expected_params)
+        self.assertTrue(result)
+
+    @patch.object(NamesiloProvider, "_request")
+    def test_update_record_missing_record_id(self, mock_request):
+        """Test record update with missing record_id"""
+        old_record = {"host": "test", "type": "A", "value": "1.2.3.4"}
+
+        result = self.provider._update_record("example.com", old_record, "5.6.7.8", "A", 7200, None, {})
+
+        # Should not make API call and return False
+        mock_request.assert_not_called()
+        self.assertFalse(result)
+
+    @patch.object(NamesiloProvider, "_request")
+    def test_update_record_failure(self, mock_request):
+        """Test failed record update"""
+        mock_request.return_value = None
+
+        old_record = {"record_id": "12345", "host": "test", "type": "A", "value": "1.2.3.4", "ttl": "3600"}
+
+        result = self.provider._update_record("example.com", old_record, "5.6.7.8", "A", 7200, None, {})
+
+        self.assertFalse(result)
+
+    @patch.object(NamesiloProvider, "_http")
+    def test_integration_set_record_update_flow(self, mock_http):
+        """Integration test for complete set_record flow with update"""
+        # Mock the sequence of API calls for an update scenario
+
+        # 1. Domain info check (zone_id query)
+        # 2. Record listing
+        # 3. Record update
+        mock_responses = [
+            # getDomainInfo response
+            {"reply": {"code": "300", "domain": {"domain": "example.com"}}},
+            # dnsListRecords response
+            {
+                "reply": {
+                    "code": "300",
+                    "resource_record": [
+                        {
+                            "record_id": "12345",
+                            "host": "test",
+                            "type": "A",
+                            "value": "1.2.3.4",
+                            "ttl": "3600",
+                        }
+                    ],
+                }
+            },
+            # dnsUpdateRecord response
+            {"reply": {"code": "300"}},
+        ]
+
+        mock_http.side_effect = mock_responses
+
+        # Execute set_record
+        result = self.provider.set_record("test.example.com", "5.6.7.8", "A", 7200)
+
+        # Verify result
+        self.assertTrue(result)
+
+        # Verify all expected API calls were made
+        self.assertEqual(mock_http.call_count, 3)
+
+    @patch.object(NamesiloProvider, "_http")
+    def test_integration_set_record_create_flow(self, mock_http):
+        """Integration test for complete set_record flow with create"""
+        # Mock the sequence of API calls for a create scenario
+
+        mock_responses = [
+            # getDomainInfo response
+            {"reply": {"code": "300", "domain": {"domain": "example.com"}}},
+            # dnsListRecords response (no matching record)
+            {"reply": {"code": "300", "resource_record": []}},
+            # dnsAddRecord response
+            {"reply": {"code": "300", "record_id": "67890"}},
+        ]
+
+        mock_http.side_effect = mock_responses
+
+        # Execute set_record
+        result = self.provider.set_record("newtest.example.com", "9.8.7.6", "A", 3600)
+
+        # Verify result
+        self.assertTrue(result)
+
+        # Verify all expected API calls were made
+        self.assertEqual(mock_http.call_count, 3)
+
+
+if __name__ == "__main__":
+    unittest.main()