diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..47b87cf --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,36 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). + +## [Unreleased] + +### Changed + +- Refactored the SDK around `BtApiManager` and domain facades for a more stable public entry point. +- Hardened `BtSdkConfig` into an immutable configuration object with stricter build-time validation. +- Upgraded the Maven quality gate pipeline with `Spotless`, `Checkstyle`, `Surefire`, `Failsafe`, and `JaCoCo`. +- Defaulted integration tests to opt-in execution to avoid accidental calls against a live BT Panel instance. +- Aligned `DefaultBtClient` retry behavior with `RetryMode`, including clearer handling for safe-request retries. +- Added support for `CUSTOM_TRUST_STORE` SSL configuration and clearer SSL startup failures. +- Split transport responsibilities into `RetryPolicy`, `SslContextConfigurer`, and `RequestEncodingUtils`. +- Consolidated duplicated website response parsing into shared helpers: + `WebsiteApiResponseSupport`, `AbstractWebsiteBooleanApi`, `AbstractWebsiteTextQueryApi`, + `AbstractWebsiteMapQueryApi`, and `AbstractWebsiteMapListQueryApi`. +- Migrated multiple website query endpoints to the shared parsers, including website detail, config, + domains, raw list, backups, PHP extensions, SSL certificate list, and limit-net configuration. +- Standardized `WebsiteOperations` read-side naming toward `list*` methods and kept compatibility + aliases deprecated where appropriate. + +### Added + +- Unit tests for shared website parsing paths, including backups, PHP extensions, SSL certificate + lists, and facade delegation coverage for the preferred read-side method names. +- Public repository metadata and community files such as contributing, security, CI, and release + documentation. + +### Removed + +- Unused legacy API enum and outdated example classes that no longer matched the current SDK design. +- Library-level binding to a concrete logging implementation. diff --git a/README.md b/README.md index 2d1a32b..b17af39 100644 --- a/README.md +++ b/README.md @@ -1,505 +1,235 @@ -# BTPanel-API-Java-SDK +# BTPanel API Java SDK -[![Java](https://img.shields.io/badge/Java-17+-blue.svg)](https://www.oracle.com/java/) -[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) -[![GitHub issues](https://img.shields.io/github/issues/inwardflow/BTPanel-API-Java-SDK.svg)](https://github.com/inwardflow/BTPanel-API-Java-SDK/issues) -[![GitHub stars](https://img.shields.io/github/stars/inwardflow/BTPanel-API-Java-SDK.svg)](https://github.com/inwardflow/BTPanel-API-Java-SDK/stargazers) +[![CI](https://github.com/inwardflow/BTPanel-API-Java-SDK/actions/workflows/ci.yml/badge.svg)](https://github.com/inwardflow/BTPanel-API-Java-SDK/actions/workflows/ci.yml) +[![Java](https://img.shields.io/badge/Java-17-blue.svg)](https://adoptium.net/) +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE) -> 🚧**注意:这是一个正在开发中的项目,尚未发布到 Maven Central。** +`BTPanel API Java SDK` is a Java 17 client library for the BT Panel API. It combines a flexible low-level `BtApi` model with higher-level facades for common automation tasks such as website management, database operations, file handling, FTP, SSL, and system inspection. -BTPanel-API-Java-SDK 是一个功能完善的 Java 开发工具包,用于与宝塔 Linux 面板 API 进行交互,提供简单、优雅、安全的方式来管理和监控您的服务器。 +## Features -## 功能特点 +- Java 17 `HttpClient` implementation. +- Immutable `BtSdkConfig` with validation for timeouts, retries, and SSL mode. +- Explicit retry strategy via `RetryMode`: `NONE`, `SAFE_REQUESTS_ONLY`, and `ALL_REQUESTS`. +- Multiple SSL modes, including system trust, custom trust store, and insecure trust-all for controlled environments. +- Stable high-level entry point through `BtApiManager`. +- Facade-based access for common modules: `system()`, `website()`, `database()`, `file()`, `ftp()`, and `ssl()`. +- Extensible low-level endpoint model for unsupported or newly discovered panel APIs. +- Maven-based quality gates with formatting, style checks, unit tests, integration-test separation, and coverage reporting. -### 核心特性 -- **支持管理多个宝塔面板实例**:可以同时连接并管理多个不同的宝塔面板 -- **基于 Builder 模式的灵活配置**:通过流式接口配置客户端参数 -- **同步和异步 API 调用支持**:支持 CompletableFuture 异步调用 -- **自动重试机制**:配置化的重试策略,提高请求成功率 -- **完善的错误处理**:详细的异常信息和状态码 -- **优雅的 API 设计**:符合 Java 语言习惯的接口设计 -- **轻量级依赖**:基于 Hutool 工具包,减少第三方依赖 -- **类型安全**:提供数据模型类,支持类型安全的 API 调用 -- **易于扩展**:模块化设计,便于添加新的 API 功能 +## Quick Start -### 支持的功能模块 -- **系统信息**:获取服务器硬件信息、操作系统信息、宝塔面板版本等 -- **服务管理**:查看和管理服务器上运行的各种服务状态 -- **网站管理**:创建、查询、修改和删除网站 -- **文件管理**:文件上传、下载、删除、目录操作等 -- **数据库管理**:数据库创建、用户管理、权限配置 -- **FTP管理**:FTP用户创建、权限设置 -- **安全管理**:防火墙设置、SSH配置等 +### Build -## 快速开始 - -### 安装依赖 - -> **注意:项目尚未发布到 Maven Central,当前只能通过源码构建使用。** - -**本地构建安装** +Linux or macOS: ```bash -# 克隆仓库 -git clone https://github.com/inwardflow/BTPanel-API-Java-SDK.git -cd BTPanel-API-Java-SDK - -# 构建并安装到本地 Maven 仓库 -mvn clean install -``` - -然后在您的项目中添加依赖: - -**Maven** -```xml - - net.heimeng - BTPanel-API-Java-SDK - 1.0.0-SNAPSHOT - -``` - -**Gradle** -```gradle -implementation 'net.heimeng:BTPanel-API-Java-SDK:1.0.0-SNAPSHOT' +./mvnw verify ``` -### 2. 初始化 SDK 客户端 +Windows PowerShell: -```java -import net.heimeng.sdk.btapi.core.BtClient; -import net.heimeng.sdk.btapi.core.BtClientFactory; -import net.heimeng.sdk.btapi.core.BtConfig; - -// 替换为实际的宝塔面板信息 -String baseUrl = "http://your-bt-panel-url:8888"; -String apiKey = "your-api-key"; - -// 方式1:快速创建客户端 -BtClient client = BtClientFactory.createClient(baseUrl, apiKey); - -// 方式2:使用Builder模式创建自定义配置 -BtConfig config = BtClientFactory.configBuilder() - .baseUrl(baseUrl) - .apiKey(apiKey) - .connectTimeout(15) // 连接超时15秒 - .readTimeout(45) // 读取超时45秒 - .retryCount(3) // 重试3次 - .build(); - -BtClient customClient = BtClientFactory.createClient(config); +```powershell +.\mvnw.cmd verify ``` -### 3. 创建API管理器并调用API +### Create a Client ```java -import net.heimeng.sdk.btapi.core.BtApiManager; +import net.heimeng.sdk.btapi.client.BtApiManager; +import net.heimeng.sdk.btapi.client.BtClientFactory; +import net.heimeng.sdk.btapi.config.BtSdkConfig; +import net.heimeng.sdk.btapi.model.BtResult; import net.heimeng.sdk.btapi.model.system.SystemInfo; -import net.heimeng.sdk.btapi.exception.BtApiException; - -// 创建API管理器 -BtApiManager apiManager = new BtApiManager(client); - -try { - // 同步调用API获取系统信息 - SystemInfo systemInfo = apiManager.system().getSystemInfo(); - - // 处理结果 - if (systemInfo != null && systemInfo.isSuccess()) { - System.out.println("系统信息获取成功"); - System.out.println("操作系统: " + systemInfo.getOsName()); - System.out.println("宝塔版本: " + systemInfo.getBtVersion()); - System.out.println("CPU使用率: " + systemInfo.getCpuUsage() + "%"); - System.out.println("内存使用率: " + systemInfo.getMemoryUsage() + "%"); - } else { - System.err.println("系统信息获取失败: " + systemInfo.getMsg()); - } -} catch (BtApiException e) { - System.err.println("API调用异常: " + e.getMessage()); + +BtSdkConfig config = BtClientFactory.configBuilder() + .baseUrl("http://your-bt-panel-url:8888") + .apiKey("your-api-key") + .connectTimeout(10) + .readTimeout(30) + .retryMode(BtSdkConfig.RetryMode.SAFE_REQUESTS_ONLY) + .retryCount(3) + .build(); + +try (BtApiManager apiManager = BtClientFactory.createApiManager(config)) { + BtResult result = apiManager.system().getSystemInfo(); + System.out.println(result.getData().getOs()); } ``` -## 高级功能 - -### 异步API调用 +### Typical Usage ```java -import java.util.concurrent.CompletableFuture; -import net.heimeng.sdk.btapi.model.system.ServiceStatusList; - -// 异步获取服务状态列表 -CompletableFuture future = apiManager.system().getServiceStatusListAsync(); - -// 处理异步结果 -future.thenAccept(serviceStatusList -> { - if (serviceStatusList != null && serviceStatusList.isSuccess()) { - System.out.println("服务状态列表获取成功"); - serviceStatusList.getServices().forEach(service -> { - System.out.println("服务名称: " + service.getDisplayName() + ", 状态: " + service.getStatus()); - }); - } else { - System.err.println("服务状态列表获取失败: " + serviceStatusList.getMsg()); - } -}).exceptionally(e -> { - System.err.println("异步调用异常" + e.getMessage()); - return null; -}); - -// 等待异步任务完成(实际应用中可能不需要) -// future.join(); +import net.heimeng.sdk.btapi.facade.DatabaseCreateRequest; +import net.heimeng.sdk.btapi.facade.FtpCreateRequest; +import net.heimeng.sdk.btapi.model.BtResult; +import net.heimeng.sdk.btapi.model.ssl.SslCertificate; +import net.heimeng.sdk.btapi.model.website.CreateWebsiteResult; +import net.heimeng.sdk.btapi.model.website.WebsiteInfo; +import net.heimeng.sdk.btapi.facade.WebsiteCreateRequest; + +BtResult> websites = apiManager.website().list(1, 20); +BtResult taskCount = apiManager.system().getTaskCount(); +BtResult nginxConfig = apiManager.website().getNginxConfig(1, "example.com"); +BtResult> certificates = apiManager.ssl().list(); +BtResult createdDatabase = + apiManager.database().create(DatabaseCreateRequest.builder("demo_db", "demo_user", "secret").build()); +BtResult createdFtp = + apiManager.ftp().create(FtpCreateRequest.of("demo_ftp", "secret", "/www/wwwroot/demo")); + +BtResult createdWebsite = + apiManager.website() + .create( + WebsiteCreateRequest.builder("demo.example.com", "/www/wwwroot/demo") + .phpVersion("82") + .remark("Demo website") + .build()); ``` -### 使用拦截器 +## Website API Design Notes + +The BT Panel website module returns several incompatible payload shapes. To keep endpoint implementations small and consistent, the SDK now uses shared parser abstractions: + +- `AbstractWebsiteBooleanApi` +- `AbstractWebsiteTextQueryApi` +- `AbstractWebsiteMapQueryApi` +- `AbstractWebsiteMapListQueryApi` +- `WebsiteApiResponseSupport` + +Concrete website endpoints should focus on: + +- endpoint path, +- parameter validation, +- payload field naming, +- and domain-specific success or failure messages. + +## Facade Naming Conventions + +The repository is moving toward a stable `1.0` public API. For read-side website operations, the preferred collection-oriented names are: + +- `listRaw(...)` +- `listTypes()` +- `listPhpVersions()` +- `listDomains(int siteId)` +- `listPhpExtensions(int siteId)` +- `listSslCertificates(int siteId)` +- `listBackups(Integer siteId, Integer page, Integer limit, String callback)` + +For write-side website operations, the preferred names now follow action-oriented verbs such as: + +- `removeDomain(...)` +- `updateRemark(...)` +- `updateRootPath(...)` +- `updateRunPath(...)` +- `toggleUserIni(...)` +- `updatePhpVersion(...)` +- `updatePhpExtension(...)` +- `updateRewriteRules(...)` +- `updateNginxConfig(...)` +- `enablePasswordProtection(...)` +- `disablePasswordProtection(...)` +- `installSslCertificate(...)` +- `disableSsl(...)` +- `toggleLogs(...)` +- `updateLimitNet(...)` + +For more complex commands, prefer the typed option objects over long parameter lists: + +- `create(WebsiteCreateRequest request)` +- `database().create(DatabaseCreateRequest request)` +- `database().delete(DatabaseDeleteRequest request)` +- `database().updatePassword(DatabasePasswordUpdateRequest request)` +- `ftp().create(FtpCreateRequest request)` +- `ftp().delete(FtpDeleteRequest request)` +- `ftp().updatePassword(FtpPasswordUpdateRequest request)` +- `delete(int siteId, String websiteName, WebsiteDeleteOptions options)` +- `addDomain(int siteId, WebsiteDomainBinding binding)` +- `removeDomain(int siteId, WebsiteDomainRemoval removal)` +- `enablePasswordProtection(int siteId, WebsitePasswordProtectionOptions options)` +- `updateRewriteRules(int siteId, WebsiteRewriteRulesOptions options)` +- `updateNginxConfig(int siteId, WebsiteNginxConfigOptions options)` +- `installSslCertificate(int siteId, WebsiteSslCertificateOptions options)` +- `updateLimitNet(int siteId, WebsiteLimitNetOptions options)` + +`WebsiteCreateRequest` uses a builder so optional provisioning stays explicit: ```java -import net.heimeng.sdk.btapi.core.Interceptor; -import net.heimeng.sdk.btapi.core.RequestContext; - -// 添加自定义日志拦截器 -client.addInterceptor(new Interceptor() { - @Override - public void intercept(RequestContext context) { - // 请求前日志 - System.out.println("请求API: " + context.getApi().getEndpoint()); - - // 继续请求处理 - context.proceed(); - - // 响应后日志 - if (context.getException() == null) { - System.out.println("API请求成功"); - } else { - System.out.println("API请求失败: " + context.getException().getMessage()); - } - } -}); - -// 添加重试拦截器 -client.addInterceptor(new RetryInterceptor(3, 1000)); -``` - -### 完整的SDK示例 - -请参考 `src/main/java/net/heimeng/sdk/btapi/example/NewSdkExample.java` 文件获取更完整的使用示例,包括: -- 同步和异步API调用 -- 自定义配置创建客户端 -- 实现和使用拦截器 -- 异常处理最佳实践 - -## 配置说明 - -`BtConfig` 支持以下配置项: - -| 配置项 | 描述 | 默认值 | -|-------|------|-------| -| baseUrl | 宝塔面板的基础URL | 必填项 | -| apiKey | API密钥 | 必填项 | -| connectTimeout | 连接超时时间(秒) | 10秒 | -| readTimeout | 读取超时时间(秒) | 30秒 | -| retryCount | 请求重试次数 | 3次 | -| retryInterval | 重试间隔时间(毫秒) | 1000毫秒 | -| retryableStatusCodes | 可重试的HTTP状态码 | [408, 429, 500, 502, 503, 504] | -| extraHeaders | 额外的HTTP请求头 | 空Map | -| sslVerify | 是否验证SSL证书 | true | - -## 异常处理 - -SDK 使用 `BtApiException` 来表示 API 调用过程中的错误,包含以下信息: - -- `message`: 错误消息 -- `cause`: 原始异常 -- `errorCode`: 错误代码 -- `statusCode`: HTTP状态码 -- `errorData`: 原始错误数据 - -异常分类判断方法: -- `isClientError()`: 判断是否为客户端错误(4xx状态码) -- `isServerError()`: 判断是否为服务器错误(5xx状态码) -- `isNetworkError()`: 判断是否为网络错误 - -## 注意事项 - -1. **安全警告**:请妥善保管您的 API 密钥和令牌,不要在公开场合泄露 -2. **版本兼容性**:确保您的宝塔面板版本与 SDK 支持的 API 版本兼容 -3. **请求频率**:避免频繁调用 API,以免触发宝塔面板的请求频率限制 -4. **错误处理**:API 调用可能会失败,请确保实现适当的错误处理和重试机制 -5. **HTTPS 配置**:建议使用 HTTPS 协议访问宝塔面板,确保数据传输安全 -6. **资源释放**:完成操作后,记得调用 `close()` 方法关闭客户端,释放资源 -7. **多线程支持**:SDK 支持多线程环境下使用,但请注意线程安全问题 - -## 安全使用指南 - -### 环境变量管理敏感信息 - -为了避免在代码中硬编码敏感信息(如API密钥和面板地址),SDK提供了通过环境变量加载配置的机制。推荐使用以下环境变量: - -| 环境变量名 | 描述 | 示例值 | -|-----------|------|--------| -| BT_PANEL_URL | 宝塔面板的URL地址 | `http://your-bt-panel-url:8888` | -| BT_PANEL_API_KEY | API密钥 | `your-actual-api-key` | -| BT_PANEL_API_TOKEN | API令牌(如果需要) | `your-actual-api-token` | - -### 在不同环境中设置环境变量 - -**Linux/MacOS** -```bash -# 临时设置环境变量(当前终端会话) -export BT_PANEL_URL="http://your-bt-panel-url:8888" -export BT_PANEL_API_KEY="your-actual-api-key" -export BT_PANEL_API_TOKEN="your-actual-api-token" - -# 永久设置环境变量(将以下内容添加到 ~/.bashrc 或 ~/.zshrc) -echo 'export BT_PANEL_URL="http://your-bt-panel-url:8888"' >> ~/.bashrc -echo 'export BT_PANEL_API_KEY="your-actual-api-key"' >> ~/.bashrc -echo 'export BT_PANEL_API_TOKEN="your-actual-api-token"' >> ~/.bashrc -source ~/.bashrc -``` - -**Windows** -```powershell -# 临时设置环境变量(当前命令提示符会话) -set BT_PANEL_URL=http://your-bt-panel-url:8888 -set BT_PANEL_API_KEY=your-actual-api-key -set BT_PANEL_API_TOKEN=your-actual-api-token - -# 永久设置环境变量(需要管理员权限) -setx BT_PANEL_URL "http://your-bt-panel-url:8888" /M -setx BT_PANEL_API_KEY "your-actual-api-key" /M -setx BT_PANEL_API_TOKEN "your-actual-api-token" /M -``` - -### 使用配置文件 - -除了环境变量外,您也可以使用配置文件来管理敏感信息。例如,在项目中创建一个`application.properties`文件,并确保将其添加到`.gitignore`中: +WebsiteCreateRequest request = + WebsiteCreateRequest.builder("demo.example.com", "/www/wwwroot/demo") + .typeId(0) + .phpVersion("82") + .port(80) + .remark("Demo website") + .ftpAccount("demo_ftp", "ftp-secret") + .database("demo_db", "db-secret", "utf8mb4") + .build(); -**application.properties** -```properties -# 宝塔面板配置 -bt.panel.url=http://your-bt-panel-url:8888 -bt.panel.api.key=your-actual-api-key -bt.panel.api.token=your-actual-api-token +apiManager.website().create(request); ``` -然后在代码中加载配置文件: +Database and FTP write flows now follow the same typed-request approach: ```java -import java.io.IOException; -import java.io.InputStream; -import java.util.Properties; - -// 加载配置文件 -Properties properties = new Properties(); -try (InputStream input = getClass().getClassLoader().getResourceAsStream("application.properties")) { - if (input != null) { - properties.load(input); - String baseUrl = properties.getProperty("bt.panel.url", "http://your-bt-panel-url:8888"); - String apiKey = properties.getProperty("bt.panel.api.key", "your-api-key"); - - // 创建客户端 - BtClient client = BtClientFactory.createClient(baseUrl, apiKey); - } -} catch (IOException ex) { - ex.printStackTrace(); -} -``` +DatabaseCreateRequest databaseRequest = + DatabaseCreateRequest.builder("demo_db", "demo_user", "secret") + .remark("Demo database") + .build(); -### .gitignore 配置 +apiManager.database().create(databaseRequest); +apiManager.database().updatePassword( + new DatabasePasswordUpdateRequest("demo_db", "demo_user", "new-secret")); -请确保您的项目中包含适当的`.gitignore`文件,以防止敏感信息被提交到版本控制系统。以下是一个推荐的Java项目.gitignore文件示例: +FtpCreateRequest ftpRequest = + new FtpCreateRequest("demo_ftp", "secret", "/www/wwwroot/demo", "Demo FTP"); -``` -# IDE 配置文件 -.idea/ -*.iml -.vscode/ -*.suo -*.ntvs* -*.njsproj -*.sln -*.sw? - -# 构建产物 -target/ -build/ -*.class - -# 依赖 -*.jar -*.war -*.nar -*.ear -*.zip -*.tar.gz -*.rar - -# 日志文件 -*.log -logs/ - -# 环境变量和配置文件 -.env -.env.local -.env.development.local -.env.test.local -.env.production.local -application.properties -application.yml - -# 操作系统文件 -.DS_Store -.DS_Store? -._* -.Spotlight-V100 -.Trashes -ehthumbs.db -Thumbs.db - -# 临时文件 -*.tmp -*.temp -*.cache - -# 测试覆盖率 -.nyc_output/ -coverage/ - -# 包管理 -.npm -.yarn-integrity - -# 其他 -*.pid -*.seed -*.pid.lock +apiManager.ftp().create(ftpRequest); +apiManager.ftp().updatePassword( + new FtpPasswordUpdateRequest(12, "demo_ftp", "/www/wwwroot/demo", "new-secret")); +apiManager.ftp().delete(new FtpDeleteRequest(12, "demo_ftp")); ``` -## 技术栈 +## SSL Notes -- **Java 17+** -- **Hutool** - 工具包 -- **Lombok** - 简化代码 -- **SLF4J + Logback** - 日志框架 -- **Jackson** - JSON处理 -- **Maven** - 项目构建 +- `install(domain, key, cert)` maps `domain` to the panel's site-name style API parameter. +- `SslCertificate.domains` comes from the certificate `CN` and `SAN` values, so it may differ from the original site name used during installation. +- If your panel uses a private CA, prefer `CUSTOM_TRUST_STORE` with `trustStore(path, password[, type])`. +- Use `INSECURE_TRUST_ALL` only in tightly controlled test environments. -## 开发指南 +## Build and Test -### 本地构建 - -```bash -# 克隆仓库 -git clone https://github.com/inwardflow/BTPanel-API-Java-SDK.git -cd BTPanel-API-Java-SDK +- Full verification: `./mvnw verify` +- Unit tests only: `./mvnw test` +- Include integration tests: `./mvnw verify -Pintegration-tests` -# 编译项目 -mvn clean compile +Integration tests should be configured through environment variables or `src/test/resources/application-test.properties`. Use `src/test/resources/application-test.properties.example` as the template, and do not commit real credentials. -# 运行测试 -mvn test +## Project Structure -# 打包项目 -mvn package +```text +src/main/java/net/heimeng/sdk/btapi +|- api +|- client +|- config +|- exception +|- facade +|- interceptor +`- model ``` -### 代码规范 - -项目使用 Checkstyle、Spotless、P3C 插件确保代码质量和一致性: -- 遵循阿里巴巴 Java 开发规范 -- 统一的导入顺序 -- 自动移除未使用的导入 -- 详细的 Javadoc 注释 - -## 贡献指南 - -我们非常欢迎社区贡献!如果您有任何问题或建议,请: - -1. 提交 Issue 报告 bug 或提出新功能请求 -2. 提交 Pull Request 改进代码 -3. 分享您的使用经验和建议 - -在提交代码前,请确保: -- 代码符合项目的代码规范 -- 添加了适当的测试用例 -- 更新了相关文档 - -## 许可证 - -本项目采用 MIT 许可证 - 详情请查看 [LICENSE](LICENSE) 文件 - -## 联系方式 - -如有任何问题,请联系我们: - -- GitHub Issues: [https://github.com/inwardflow/BTPanel-API-Java-SDK/issues](https://github.com/inwardflow/BTPanel-API-Java-SDK/issues) -- Email: admin@2wxk.com - -## Star History - -[![Star History Chart](https://api.star-history.com/svg?repos=inwardflow/BTPanel-API-Java-SDK&type=Date)](https://star-history.com/#inwardflow/BTPanel-API-Java-SDK&Date) - -## Roadmap - -### 近期目标 (v1.0.0 - 首次发布) -- 完善核心API功能实现 -- 编写完整的单元测试和集成测试 -- 完成API文档生成 -- 优化错误处理机制 -- 修复已知bug -- 发布到Maven Central - -### 中期计划 (v1.1.0 - v1.5.0) -- 支持更多宝塔面板API功能 -- 增强安全性和性能 -- 添加缓存机制 -- 支持WebSocket实时通知 -- 增加更多示例代码 - -## 开发进度 - -### 已完成功能 -- 基础客户端和配置系统 -- 系统信息API -- 服务状态管理API -- 同步和异步调用支持 -- 拦截器机制 -- 基本的错误处理 - -### 待完成功能 -- 网站管理API -- 文件管理API -- 数据库管理API -- FTP管理API -- 安全管理API -- 更多高级配置选项 -- 完整的测试套件 - -## Todo List - -- [ ] 完善网站管理功能 -- [ ] 实现文件管理API -- [ ] 添加数据库管理功能 -- [ ] 完成FTP管理API -- [ ] 实现安全管理功能 -- [ ] 编写完整的单元测试 -- [ ] 添加集成测试 -- [ ] 优化文档和示例 -- [ ] 修复所有已知bug -- [ ] 发布正式版本到Maven Central +## Documentation -## Milestones +- [Architecture](docs/architecture.md) +- [Testing](docs/testing.md) +- [Quickstart Example](docs/examples/quickstart.md) +- [Release Checklist](docs/release-checklist.md) +- [Contributing](CONTRIBUTING.md) +- [Security Policy](SECURITY.md) +- [Changelog](CHANGELOG.md) -| 版本 | 目标日期 | 主要功能 | 状态 | -|------|---------|---------|------| -|v1.0.0-alpha | 2025年12月 | 核心功能实现 | 进行中 | -|v1.0.0-beta | 2026年1月 | 功能完善和测试 | 计划中 | -|v1.0.0 | 2026年2月 | 正式发布 | 计划中 | -|v1.1.0 | 2026年4月 | 扩展功能 | 计划中 | +## Versioning -## Acknowledgements +The project is still in the `0.x` stage. Public APIs may continue to evolve while the SDK moves toward a stable `1.0` release. -感谢所有为这个项目做出贡献的开发者和用户! +## License -特别鸣谢: -- [宝塔面板](https://www.bt.cn/) 提供的API接口 -- [Hutool](https://hutool.cn/) 工具库 -- 所有测试和反馈的用户 \ No newline at end of file +[MIT](LICENSE) diff --git a/docs/architecture.md b/docs/architecture.md new file mode 100644 index 0000000..c0f249b --- /dev/null +++ b/docs/architecture.md @@ -0,0 +1,79 @@ +# Architecture + +## Goals + +This SDK is designed around two complementary layers: + +1. Transport and execution layer + `BtClient` owns request signing, HTTP transport, retry behavior, SSL handling, interceptor hooks, and raw response delivery. +2. Domain and facade layer + `BtApi` implementations model individual BT Panel endpoints, while `BtApiManager` and the `facade` package provide task-oriented entry points such as `system()`, `website()`, `database()`, `file()`, `ftp()`, and `ssl()`. + +This split keeps the low-level contract extensible without forcing every new endpoint through a facade first, while still giving application code a stable, business-friendly API surface. + +## Package Responsibilities + +- `api` + Endpoint definitions, parameter validation, and response parsing. +- `client` + HTTP client implementation and SDK entry points. +- `config` + Immutable SDK configuration with build-time validation. +- `exception` + Unified exception hierarchy for transport, authentication, and API contract failures. +- `facade` + High-level domain operations grouped by module. +- `interceptor` + Request interception hooks for tracing, diagnostics, or customization. +- `model` + Parsed domain models and generic result wrappers. + +## Website Module Parsing Strategy + +The website module has the widest variety of response shapes in the BT Panel API. Instead of letting each endpoint reimplement the same branching logic, the SDK now centralizes these contracts in shared parsing helpers: + +- `WebsiteApiResponseSupport` + Shared JSON parsing, recursive `Map` and `List` conversion, and `BtResult` construction helpers. +- `AbstractWebsiteBooleanApi` + For endpoints that only return `status` and `msg`. +- `AbstractWebsiteTextQueryApi` + For endpoints that may return plain text, wrapped text, or non-wrapped JSON text payloads. +- `AbstractWebsiteMapQueryApi` + For endpoints that return a single object, either directly or inside `data`. +- `AbstractWebsiteMapListQueryApi` + For endpoints that return a list of object payloads, either directly or inside a named array field. + +This design gives us three benefits: + +- Lower duplication across `api/website`. +- More consistent failure handling and error messages. +- Easier addition of new endpoints because only endpoint-specific validation and field names remain in concrete classes. + +## Facade Design + +Facade methods are optimized for readability and gradual public API stabilization: + +- Prefer verb-first collection methods such as `listRaw`, `listTypes`, `listDomains`, `listPhpExtensions`, `listSslCertificates`, and `listBackups`. +- Keep deprecated aliases temporarily when they are thin compatibility shims. +- Normalize parameter order to place the primary resource identifier first when feasible. + +This allows the SDK to evolve toward a stable `1.0` surface without forcing downstream users into a breaking migration all at once. + +## Quality Gates + +The repository is prepared for public open-source maintenance with the following defaults: + +- Maven Wrapper as the standard build entry point. +- `Spotless` for formatting. +- `Checkstyle` for style validation. +- `Surefire` for unit tests. +- `Failsafe` for integration tests. +- `JaCoCo` for coverage reporting. + +Integration tests are opt-in and should not run by default against a real panel. + +## Key Constraints + +- The SDK is a library and should not force a concrete logging backend on consumers. +- Validation should happen as early as possible, ideally when building config objects or endpoint parameter objects. +- Parsing code should fail loudly when the panel returns a contract shape we do not support, because silent coercion hides upstream API drift. diff --git a/src/main/java/net/heimeng/sdk/btapi/api/website/AbstractWebsiteBooleanApi.java b/src/main/java/net/heimeng/sdk/btapi/api/website/AbstractWebsiteBooleanApi.java new file mode 100644 index 0000000..b0ffabe --- /dev/null +++ b/src/main/java/net/heimeng/sdk/btapi/api/website/AbstractWebsiteBooleanApi.java @@ -0,0 +1,126 @@ +package net.heimeng.sdk.btapi.api.website; + +import cn.hutool.json.JSON; +import cn.hutool.json.JSONObject; + +import net.heimeng.sdk.btapi.api.BaseBtApi; +import net.heimeng.sdk.btapi.exception.BtApiException; +import net.heimeng.sdk.btapi.model.BtResult; + +/** + * Website 模块中返回布尔结果的 API 公共基类。 + * + *

统一处理标准 {@code status/msg} 响应解析,并提供常用参数校验与布尔标志位辅助方法。 + */ +abstract class AbstractWebsiteBooleanApi extends BaseBtApi> { + + private final String successMessage; + private final String failureMessage; + + protected AbstractWebsiteBooleanApi( + String endpoint, String successMessage, String failureMessage) { + super(endpoint, HttpMethod.POST); + this.successMessage = successMessage; + this.failureMessage = failureMessage; + } + + @Override + public BtResult parseResponse(String response) { + JSON json = WebsiteApiResponseSupport.parseJsonResponse(response, endpoint); + if (!(json instanceof JSONObject jsonObject)) { + throw new BtApiException("Website API response must be a JSON object"); + } + if (!jsonObject.containsKey("status")) { + throw new BtApiException("Website API response is missing required status field"); + } + + boolean status = jsonObject.getBool("status", false); + BtResult result = new BtResult<>(); + result.setStatus(status); + result.setMsg(jsonObject.getStr("msg", status ? successMessage : failureMessage)); + result.setData(status); + return result; + } + + protected final boolean hasRequiredParams(String... paramNames) { + for (String paramName : paramNames) { + if (!hasNonBlankStringParam(paramName)) { + return false; + } + } + return true; + } + + protected final boolean hasParam(String paramName) { + return params.containsKey(paramName); + } + + protected final boolean hasNonBlankStringParam(String paramName) { + Object value = params.get(paramName); + return value instanceof String stringValue && !stringValue.isBlank(); + } + + protected final boolean hasPositiveIntegerParam(String paramName) { + Object value = params.get(paramName); + return value instanceof Number numberValue && numberValue.intValue() > 0; + } + + protected final boolean hasNonNegativeNumberParam(String paramName) { + Object value = params.get(paramName); + return value instanceof Number numberValue && numberValue.intValue() >= 0; + } + + protected final boolean hasOptionalNonNegativeNumberParam(String paramName) { + return !params.containsKey(paramName) || hasNonNegativeNumberParam(paramName); + } + + protected final boolean hasBooleanFlagIntParam(String paramName) { + Object value = params.get(paramName); + if (!(value instanceof Number numberValue)) { + return false; + } + int flag = numberValue.intValue(); + return flag == 0 || flag == 1; + } + + protected final boolean hasOptionalBooleanFlagIntParam(String paramName) { + return !params.containsKey(paramName) || hasBooleanFlagIntParam(paramName); + } + + protected final void requireNonBlank(String value, String paramName) { + if (value == null || value.isBlank()) { + throw new IllegalArgumentException(paramName + " cannot be blank"); + } + } + + protected final void requireNonNull(Object value, String paramName) { + if (value == null) { + throw new IllegalArgumentException(paramName + " cannot be null"); + } + } + + protected final void requirePositiveInteger(Integer value, String paramName) { + if (value == null || value <= 0) { + throw new IllegalArgumentException(paramName + " must be positive"); + } + } + + protected final void putOptionalBooleanFlag(String paramName, Boolean value) { + if (value == null) { + removeParam(paramName); + return; + } + addParam(paramName, value ? 1 : 0); + } + + protected final void putOptionalNonNegativeInteger(String paramName, Integer value) { + if (value == null) { + removeParam(paramName); + return; + } + if (value < 0) { + throw new IllegalArgumentException(paramName + " cannot be negative"); + } + addParam(paramName, value); + } +} diff --git a/src/main/java/net/heimeng/sdk/btapi/api/website/AbstractWebsiteMapListQueryApi.java b/src/main/java/net/heimeng/sdk/btapi/api/website/AbstractWebsiteMapListQueryApi.java new file mode 100644 index 0000000..111ee4d --- /dev/null +++ b/src/main/java/net/heimeng/sdk/btapi/api/website/AbstractWebsiteMapListQueryApi.java @@ -0,0 +1,82 @@ +package net.heimeng.sdk.btapi.api.website; + +import java.util.List; +import java.util.Map; + +import cn.hutool.json.JSON; +import cn.hutool.json.JSONArray; +import cn.hutool.json.JSONObject; + +import net.heimeng.sdk.btapi.api.BaseBtApi; +import net.heimeng.sdk.btapi.exception.BtApiException; +import net.heimeng.sdk.btapi.model.BtResult; + +/** + * Website 模块中返回 {@code List>} 的查询型 API 公共基类。 + * + *

兼容面板直接返回数组,以及包装在指定字段中的列表响应。 + */ +abstract class AbstractWebsiteMapListQueryApi + extends BaseBtApi>>> { + + private final String responseName; + private final String successMessage; + private final String failureMessage; + private final String dataFieldName; + private final boolean allowDirectArrayResponse; + + protected AbstractWebsiteMapListQueryApi( + String endpoint, + String responseName, + String successMessage, + String failureMessage, + String dataFieldName, + boolean allowDirectArrayResponse) { + super(endpoint, HttpMethod.POST); + this.responseName = responseName; + this.successMessage = successMessage; + this.failureMessage = failureMessage; + this.dataFieldName = dataFieldName; + this.allowDirectArrayResponse = allowDirectArrayResponse; + } + + @Override + public BtResult>> parseResponse(String response) { + JSON json = WebsiteApiResponseSupport.parseJsonResponse(response, responseName); + if (json instanceof JSONArray jsonArray) { + if (!allowDirectArrayResponse) { + throw new BtApiException(responseName + " response must be a JSON object"); + } + return WebsiteApiResponseSupport.successListResult( + WebsiteApiResponseSupport.toMapList(jsonArray), successMessage); + } + if (!(json instanceof JSONObject jsonObject)) { + throw new BtApiException(responseName + " response must be a JSON object or array"); + } + if (jsonObject.containsKey("status") && !jsonObject.getBool("status", false)) { + return WebsiteApiResponseSupport.failureListResult(jsonObject.getStr("msg", failureMessage)); + } + + JSONArray dataArray = jsonObject.getJSONArray(dataFieldName); + if (dataArray == null) { + throw new BtApiException( + responseName + " response is missing required " + dataFieldName + " array"); + } + + return WebsiteApiResponseSupport.successListResult( + WebsiteApiResponseSupport.toMapList(dataArray), jsonObject.getStr("msg", successMessage)); + } + + protected final boolean hasPositiveIntegerParam(String paramName) { + Object value = params.get(paramName); + return value instanceof Integer integerValue && integerValue > 0; + } + + protected final boolean hasOptionalPositiveIntegerParam(String paramName) { + return !params.containsKey(paramName) || hasPositiveIntegerParam(paramName); + } + + protected final boolean hasOptionalStringParam(String paramName) { + return !params.containsKey(paramName) || params.get(paramName) instanceof String; + } +} diff --git a/src/main/java/net/heimeng/sdk/btapi/api/website/AbstractWebsiteMapQueryApi.java b/src/main/java/net/heimeng/sdk/btapi/api/website/AbstractWebsiteMapQueryApi.java new file mode 100644 index 0000000..c872335 --- /dev/null +++ b/src/main/java/net/heimeng/sdk/btapi/api/website/AbstractWebsiteMapQueryApi.java @@ -0,0 +1,83 @@ +package net.heimeng.sdk.btapi.api.website; + +import java.util.Map; + +import cn.hutool.json.JSON; +import cn.hutool.json.JSONObject; + +import net.heimeng.sdk.btapi.api.BaseBtApi; +import net.heimeng.sdk.btapi.exception.BtApiException; +import net.heimeng.sdk.btapi.model.BtResult; + +/** + * Website 模块中返回 {@code Map} 的查询型 API 公共基类。 + * + *

兼容两类常见返回: + * + *

    + *
  • 标准包装 JSON:{@code status/msg/data} + *
  • 直接返回对象载荷 + *
+ */ +abstract class AbstractWebsiteMapQueryApi extends BaseBtApi>> { + + private final String responseName; + private final String successMessage; + private final String failureMessage; + private final boolean requireNonEmptyPayload; + + protected AbstractWebsiteMapQueryApi( + String endpoint, + String responseName, + String successMessage, + String failureMessage, + boolean requireNonEmptyPayload) { + super(endpoint, HttpMethod.POST); + this.responseName = responseName; + this.successMessage = successMessage; + this.failureMessage = failureMessage; + this.requireNonEmptyPayload = requireNonEmptyPayload; + } + + @Override + public BtResult> parseResponse(String response) { + JSON json = WebsiteApiResponseSupport.parseJsonResponse(response, responseName); + if (!(json instanceof JSONObject jsonObject)) { + throw new BtApiException(responseName + " response must be a JSON object"); + } + if (jsonObject.containsKey("status") && !jsonObject.getBool("status", false)) { + return WebsiteApiResponseSupport.failureMapResult(jsonObject.getStr("msg", failureMessage)); + } + + Map payload = extractPayload(jsonObject); + if (requireNonEmptyPayload && payload.isEmpty()) { + throw new BtApiException(responseName + " response is missing detail payload"); + } + + return WebsiteApiResponseSupport.successMapResult( + payload, jsonObject.getStr("msg", successMessage)); + } + + protected final Map extractPayload(JSONObject jsonObject) { + Object rawData = jsonObject.get("data"); + if (rawData instanceof JSONObject dataObject) { + return Map.copyOf(WebsiteApiResponseSupport.toMap(dataObject)); + } + if (rawData != null) { + throw new BtApiException(responseName + " response data must be a JSON object"); + } + + return WebsiteApiResponseSupport.removeStatusAndMsg( + WebsiteApiResponseSupport.toMap(jsonObject)); + } + + protected final boolean hasPositiveIntegerParam(String paramName) { + Object value = params.get(paramName); + return value instanceof Integer integerValue && integerValue > 0; + } + + protected final boolean hasNonBlankStringParam(String paramName) { + Object value = params.get(paramName); + return value instanceof String stringValue && !stringValue.isBlank(); + } +} diff --git a/src/main/java/net/heimeng/sdk/btapi/api/website/AbstractWebsiteTextQueryApi.java b/src/main/java/net/heimeng/sdk/btapi/api/website/AbstractWebsiteTextQueryApi.java new file mode 100644 index 0000000..2d191c1 --- /dev/null +++ b/src/main/java/net/heimeng/sdk/btapi/api/website/AbstractWebsiteTextQueryApi.java @@ -0,0 +1,82 @@ +package net.heimeng.sdk.btapi.api.website; + +import java.util.Map; + +import cn.hutool.json.JSON; +import cn.hutool.json.JSONObject; + +import net.heimeng.sdk.btapi.api.BaseBtApi; +import net.heimeng.sdk.btapi.exception.BtApiException; +import net.heimeng.sdk.btapi.model.BtResult; + +/** + * Website 模块中返回文本内容的查询型 API 公共基类。 + * + *

兼容两类常见返回: + * + *

    + *
  • 标准包装 JSON:{@code status/msg/data} + *
  • 直接返回原始文本或非包装 JSON 文本 + *
+ */ +abstract class AbstractWebsiteTextQueryApi extends BaseBtApi> { + + private final String successMessage; + private final String failureMessage; + + protected AbstractWebsiteTextQueryApi( + String endpoint, String successMessage, String failureMessage) { + super(endpoint, HttpMethod.POST); + this.successMessage = successMessage; + this.failureMessage = failureMessage; + } + + @Override + public BtResult parseResponse(String response) { + if (response == null || response.isBlank()) { + throw new BtApiException("Empty response received"); + } + + if (!WebsiteApiResponseSupport.isJsonPayload(response)) { + return WebsiteApiResponseSupport.successStringResult(response, successMessage); + } + + JSON json = WebsiteApiResponseSupport.parseJsonResponse(response, endpoint); + if (!(json instanceof JSONObject jsonObject) || !jsonObject.containsKey("status")) { + return WebsiteApiResponseSupport.successStringResult(response, successMessage); + } + + boolean status = jsonObject.getBool("status", false); + if (!status) { + return WebsiteApiResponseSupport.failureStringResult( + jsonObject.getStr("msg", failureMessage)); + } + + Object rawData = jsonObject.get("data"); + if (rawData != null) { + return WebsiteApiResponseSupport.successStringResult( + WebsiteApiResponseSupport.stringifyJsonValue(rawData), + jsonObject.getStr("msg", successMessage)); + } + + Map payload = + WebsiteApiResponseSupport.removeStatusAndMsg(WebsiteApiResponseSupport.toMap(jsonObject)); + if (!payload.isEmpty()) { + return WebsiteApiResponseSupport.successStringResult( + WebsiteApiResponseSupport.stringifyJsonValue(payload), + jsonObject.getStr("msg", successMessage)); + } + + throw new BtApiException("Website text response is missing required data field"); + } + + protected final boolean hasPositiveIntegerParam(String paramName) { + Object value = params.get(paramName); + return value instanceof Integer integerValue && integerValue > 0; + } + + protected final boolean hasNonBlankStringParam(String paramName) { + Object value = params.get(paramName); + return value instanceof String stringValue && !stringValue.isBlank(); + } +} diff --git a/src/main/java/net/heimeng/sdk/btapi/api/website/AddWebsiteDomainApi.java b/src/main/java/net/heimeng/sdk/btapi/api/website/AddWebsiteDomainApi.java index 00ab786..7570605 100644 --- a/src/main/java/net/heimeng/sdk/btapi/api/website/AddWebsiteDomainApi.java +++ b/src/main/java/net/heimeng/sdk/btapi/api/website/AddWebsiteDomainApi.java @@ -1,107 +1,38 @@ package net.heimeng.sdk.btapi.api.website; -import cn.hutool.json.JSONObject; -import cn.hutool.json.JSONUtil; -import net.heimeng.sdk.btapi.api.BaseBtApi; -import net.heimeng.sdk.btapi.exception.BtApiException; -import net.heimeng.sdk.btapi.model.BtResult; - /** - * 添加域名API实现 - *

- * 用于为宝塔面板中的指定网站添加域名。 - *

+ * 为站点添加域名的 API。 * - * @author InwardFlow - * @since 2.0.0 + *

用于向指定站点追加新的域名绑定。 */ -public class AddWebsiteDomainApi extends BaseBtApi> { - - /** - * API端点路径 - */ - private static final String ENDPOINT = "site?action=AddDomain"; - - /** - * 构造函数,创建一个新的AddWebsiteDomainApi实例 - */ - public AddWebsiteDomainApi() { - super(ENDPOINT, HttpMethod.POST); - } - - /** - * 设置网站ID - * - * @param id 网站ID - * @return 当前API实例,支持链式调用 - */ - public AddWebsiteDomainApi setId(Integer id) { - addParam("id", id); - return this; - } - - /** - * 设置网站名称 - * - * @param webname 网站名称 - * @return 当前API实例,支持链式调用 - */ - public AddWebsiteDomainApi setWebname(String webname) { - addParam("webname", webname); - return this; - } - - /** - * 设置要添加的域名 - *

多个域名用换行符隔开

- * - * @param domain 域名(格式:域名:端口) - * @return 当前API实例,支持链式调用 - */ - public AddWebsiteDomainApi setDomain(String domain) { - addParam("domain", domain); - return this; - } - - /** - * 验证请求参数是否有效 - * - * @return 如果请求参数有效则返回true,否则返回false - */ - @Override - protected boolean validateParams() { - return params.containsKey("id") && params.containsKey("webname") && params.containsKey("domain"); - } - - /** - * 解析API响应字符串为BtResult对象 - * - * @param response API响应字符串 - * @return BtResult对象 - * @throws BtApiException 当解析失败时抛出 - */ - @Override - public BtResult parseResponse(String response) { - if (response == null || response.isEmpty()) { - throw new BtApiException("Empty response received"); - } - - try { - if (!JSONUtil.isTypeJSON(response)) { - throw new BtApiException("Invalid JSON response: " + response); - } - - JSONObject json = JSONUtil.parseObj(response); - BtResult result = new BtResult<>(); - boolean status = json.getBool("status", false); - - result.setStatus(status); - result.setMsg(json.getStr("msg", status ? "域名添加成功" : "域名添加失败")); - result.setData(status); - - return result; - } catch (Exception e) { - throw new BtApiException("Failed to parse add website domain response: " + e.getMessage(), e); - } - } -} \ No newline at end of file +public class AddWebsiteDomainApi extends AbstractWebsiteBooleanApi { + + private static final String ENDPOINT = "site?action=AddDomain"; + + public AddWebsiteDomainApi() { + super(ENDPOINT, "域名添加成功", "域名添加失败"); + } + + public AddWebsiteDomainApi setId(Integer id) { + requirePositiveInteger(id, "id"); + addParam("id", id); + return this; + } + + public AddWebsiteDomainApi setWebname(String webname) { + requireNonBlank(webname, "webname"); + addParam("webname", webname); + return this; + } + + public AddWebsiteDomainApi setDomain(String domain) { + requireNonBlank(domain, "domain"); + addParam("domain", domain); + return this; + } + + @Override + protected boolean validateParams() { + return hasPositiveIntegerParam("id") && hasRequiredParams("webname", "domain"); + } +} diff --git a/src/main/java/net/heimeng/sdk/btapi/api/website/CloseWebsitePasswordApi.java b/src/main/java/net/heimeng/sdk/btapi/api/website/CloseWebsitePasswordApi.java index c1031fa..f886e1d 100644 --- a/src/main/java/net/heimeng/sdk/btapi/api/website/CloseWebsitePasswordApi.java +++ b/src/main/java/net/heimeng/sdk/btapi/api/website/CloseWebsitePasswordApi.java @@ -1,84 +1,22 @@ package net.heimeng.sdk.btapi.api.website; -import cn.hutool.json.JSONObject; -import cn.hutool.json.JSONUtil; -import net.heimeng.sdk.btapi.api.BaseBtApi; -import net.heimeng.sdk.btapi.exception.BtApiException; -import net.heimeng.sdk.btapi.model.BtResult; +/** 关闭站点访问密码保护的 API。 */ +public class CloseWebsitePasswordApi extends AbstractWebsiteBooleanApi { -/** - * 关闭密码访问API实现 - *

- * 用于关闭宝塔面板中网站的访问密码保护。 - *

- * - * @author InwardFlow - * @since 2.0.0 - */ -public class CloseWebsitePasswordApi extends BaseBtApi> { - - /** - * API端点路径 - */ - private static final String ENDPOINT = "site?action=CloseHasPwd"; - - /** - * 构造函数,创建一个新的CloseWebsitePasswordApi实例 - */ - public CloseWebsitePasswordApi() { - super(ENDPOINT, HttpMethod.POST); - } - - /** - * 设置网站ID - * - * @param id 网站ID - * @return 当前API实例,支持链式调用 - */ - public CloseWebsitePasswordApi setId(Integer id) { - addParam("id", id); - return this; - } - - /** - * 验证请求参数是否有效 - * - * @return 如果请求参数有效则返回true,否则返回false - */ - @Override - protected boolean validateParams() { - return params.containsKey("id"); - } - - /** - * 解析API响应字符串为BtResult对象 - * - * @param response API响应字符串 - * @return BtResult对象 - * @throws BtApiException 当解析失败时抛出 - */ - @Override - public BtResult parseResponse(String response) { - if (response == null || response.isEmpty()) { - throw new BtApiException("Empty response received"); - } - - try { - if (!JSONUtil.isTypeJSON(response)) { - throw new BtApiException("Invalid JSON response: " + response); - } - - JSONObject json = JSONUtil.parseObj(response); - BtResult result = new BtResult<>(); - boolean status = json.getBool("status", false); - - result.setStatus(status); - result.setMsg(json.getStr("msg", status ? "关闭成功" : "关闭失败")); - result.setData(status); - - return result; - } catch (Exception e) { - throw new BtApiException("Failed to parse close website password response: " + e.getMessage(), e); - } - } -} \ No newline at end of file + private static final String ENDPOINT = "site?action=CloseHasPwd"; + + public CloseWebsitePasswordApi() { + super(ENDPOINT, "密码访问已关闭", "关闭密码访问失败"); + } + + public CloseWebsitePasswordApi setId(Integer id) { + requirePositiveInteger(id, "id"); + addParam("id", id); + return this; + } + + @Override + protected boolean validateParams() { + return hasPositiveIntegerParam("id"); + } +} diff --git a/src/main/java/net/heimeng/sdk/btapi/api/website/CloseWebsiteSslApi.java b/src/main/java/net/heimeng/sdk/btapi/api/website/CloseWebsiteSslApi.java index 9c37e45..f581aaa 100644 --- a/src/main/java/net/heimeng/sdk/btapi/api/website/CloseWebsiteSslApi.java +++ b/src/main/java/net/heimeng/sdk/btapi/api/website/CloseWebsiteSslApi.java @@ -1,84 +1,22 @@ package net.heimeng.sdk.btapi.api.website; -import cn.hutool.json.JSONObject; -import cn.hutool.json.JSONUtil; -import net.heimeng.sdk.btapi.api.BaseBtApi; -import net.heimeng.sdk.btapi.exception.BtApiException; -import net.heimeng.sdk.btapi.model.BtResult; +/** 关闭站点 SSL 配置的 API。 */ +public class CloseWebsiteSslApi extends AbstractWebsiteBooleanApi { -/** - * 关闭网站SSL证书API实现 - *

- * 用于关闭宝塔面板中网站的SSL证书配置。 - *

- * - * @author InwardFlow - * @since 2.0.0 - */ -public class CloseWebsiteSslApi extends BaseBtApi> { - - /** - * API端点路径 - */ - private static final String ENDPOINT = "site?action=CloseSSL"; - - /** - * 构造函数,创建一个新的CloseWebsiteSslApi实例 - */ - public CloseWebsiteSslApi() { - super(ENDPOINT, HttpMethod.POST); - } - - /** - * 设置网站ID - * - * @param id 网站ID - * @return 当前API实例,支持链式调用 - */ - public CloseWebsiteSslApi setId(Integer id) { - addParam("id", id); - return this; - } - - /** - * 验证请求参数是否有效 - * - * @return 如果请求参数有效则返回true,否则返回false - */ - @Override - protected boolean validateParams() { - return params.containsKey("id"); - } - - /** - * 解析API响应字符串为BtResult对象 - * - * @param response API响应字符串 - * @return BtResult对象 - * @throws BtApiException 当解析失败时抛出 - */ - @Override - public BtResult parseResponse(String response) { - if (response == null || response.isEmpty()) { - throw new BtApiException("Empty response received"); - } - - try { - if (!JSONUtil.isTypeJSON(response)) { - throw new BtApiException("Invalid JSON response: " + response); - } - - JSONObject json = JSONUtil.parseObj(response); - BtResult result = new BtResult<>(); - boolean status = json.getBool("status", false); - - result.setStatus(status); - result.setMsg(json.getStr("msg", status ? "关闭成功" : "关闭失败")); - result.setData(status); - - return result; - } catch (Exception e) { - throw new BtApiException("Failed to parse close website SSL response: " + e.getMessage(), e); - } - } -} \ No newline at end of file + private static final String ENDPOINT = "site?action=CloseSSL"; + + public CloseWebsiteSslApi() { + super(ENDPOINT, "SSL 已关闭", "关闭 SSL 失败"); + } + + public CloseWebsiteSslApi setId(Integer id) { + requirePositiveInteger(id, "id"); + addParam("id", id); + return this; + } + + @Override + protected boolean validateParams() { + return hasPositiveIntegerParam("id"); + } +} diff --git a/src/main/java/net/heimeng/sdk/btapi/api/website/CreateWebsiteApi.java b/src/main/java/net/heimeng/sdk/btapi/api/website/CreateWebsiteApi.java index ea064aa..6a838b5 100644 --- a/src/main/java/net/heimeng/sdk/btapi/api/website/CreateWebsiteApi.java +++ b/src/main/java/net/heimeng/sdk/btapi/api/website/CreateWebsiteApi.java @@ -1,263 +1,265 @@ package net.heimeng.sdk.btapi.api.website; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; + import cn.hutool.core.util.StrUtil; import cn.hutool.json.JSONException; import cn.hutool.json.JSONObject; import cn.hutool.json.JSONUtil; + import net.heimeng.sdk.btapi.api.BaseBtApi; import net.heimeng.sdk.btapi.exception.BtApiException; import net.heimeng.sdk.btapi.model.BtResult; import net.heimeng.sdk.btapi.model.website.CreateWebsiteResult; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.Map; - /** * 创建网站API实现 - *

- * 用于在宝塔面板中创建新的网站,支持配置域名、路径、FTP、数据库等。 - *

+ * + *

用于在宝塔面板中创建新的网站,支持配置域名、路径、FTP、数据库等。 * * @author InwardFlow * @since 2.0.0 */ public class CreateWebsiteApi extends BaseBtApi> { - - /** - * API端点路径 - */ - private static final String ENDPOINT = "site?action=AddSite"; - - /** - * 构造函数,创建一个新的CreateWebsiteApi实例 - * - * @param domain 网站主域名 - * @param path 网站根目录 - * @param typeId 分类标识 - * @param phpVersion PHP版本 - * @param port 网站端口 - * @param ps 网站备注 - */ - public CreateWebsiteApi(String domain, String path, int typeId, String phpVersion, int port, String ps) { - super(ENDPOINT, HttpMethod.POST); - - // 设置必需参数 - setDomain(domain); - setPath(path); - setTypeId(typeId); - setPhpVersion(phpVersion); - setPort(port); - setPs(ps); - - // 设置默认值 - setType("PHP"); - setCreateFtp(false); - setCreateDatabase(false); - } - - /** - * 设置网站主域名 - * - * @param domain 网站主域名 - * @return 当前API实例,支持链式调用 - */ - public CreateWebsiteApi setDomain(String domain) { - if (StrUtil.isEmpty(domain)) { - throw new IllegalArgumentException("Domain cannot be empty"); - } - - // 构建域名JSON对象 - Map webnameMap = new HashMap<>(); - webnameMap.put("domain", domain); - webnameMap.put("domainlist", new ArrayList<>()); - webnameMap.put("count", 0); - - addParam("webname", JSONUtil.toJsonStr(webnameMap)); - return this; - } - - /** - * 设置网站根目录 - * - * @param path 网站根目录 - * @return 当前API实例,支持链式调用 - */ - public CreateWebsiteApi setPath(String path) { - if (StrUtil.isEmpty(path)) { - throw new IllegalArgumentException("Path cannot be empty"); - } - addParam("path", path); - return this; - } - - /** - * 设置分类标识 - * - * @param typeId 分类标识 - * @return 当前API实例,支持链式调用 - */ - public CreateWebsiteApi setTypeId(int typeId) { - addParam("type_id", typeId); - return this; - } - - /** - * 设置项目类型 - * - * @param type 项目类型 - * @return 当前API实例,支持链式调用 - */ - public CreateWebsiteApi setType(String type) { - addParam("type", type); - return this; - } - - /** - * 设置PHP版本 - * - * @param phpVersion PHP版本 - * @return 当前API实例,支持链式调用 - */ - public CreateWebsiteApi setPhpVersion(String phpVersion) { - if (StrUtil.isEmpty(phpVersion)) { - throw new IllegalArgumentException("PHP version cannot be empty"); - } - addParam("version", phpVersion); - return this; + + /** API端点路径 */ + private static final String ENDPOINT = "site?action=AddSite"; + + /** + * 构造函数,创建一个新的CreateWebsiteApi实例 + * + * @param domain 网站主域名 + * @param path 网站根目录 + * @param typeId 分类标识 + * @param phpVersion PHP版本 + * @param port 网站端口 + * @param ps 网站备注 + */ + public CreateWebsiteApi( + String domain, String path, int typeId, String phpVersion, int port, String ps) { + super(ENDPOINT, HttpMethod.POST); + + // 设置必需参数 + setDomain(domain); + setPath(path); + setTypeId(typeId); + setPhpVersion(phpVersion); + setPort(port); + setPs(ps); + + // 设置默认值 + setType("PHP"); + setCreateFtp(false); + setCreateDatabase(false); + } + + /** + * 设置网站主域名 + * + * @param domain 网站主域名 + * @return 当前API实例,支持链式调用 + */ + public CreateWebsiteApi setDomain(String domain) { + if (StrUtil.isEmpty(domain)) { + throw new IllegalArgumentException("Domain cannot be empty"); } - - /** - * 设置网站端口 - * - * @param port 网站端口 - * @return 当前API实例,支持链式调用 - */ - public CreateWebsiteApi setPort(int port) { - addParam("port", port); - return this; + + // 构建域名JSON对象 + Map webnameMap = new HashMap<>(); + webnameMap.put("domain", domain); + webnameMap.put("domainlist", new ArrayList<>()); + webnameMap.put("count", 0); + + addParam("webname", JSONUtil.toJsonStr(webnameMap)); + return this; + } + + /** + * 设置网站根目录 + * + * @param path 网站根目录 + * @return 当前API实例,支持链式调用 + */ + public CreateWebsiteApi setPath(String path) { + if (StrUtil.isEmpty(path)) { + throw new IllegalArgumentException("Path cannot be empty"); } - - /** - * 设置网站备注 - * - * @param ps 网站备注 - * @return 当前API实例,支持链式调用 - */ - public CreateWebsiteApi setPs(String ps) { - if (StrUtil.isEmpty(ps)) { - throw new IllegalArgumentException("PS cannot be empty"); - } - addParam("ps", ps); - return this; + addParam("path", path); + return this; + } + + /** + * 设置分类标识 + * + * @param typeId 分类标识 + * @return 当前API实例,支持链式调用 + */ + public CreateWebsiteApi setTypeId(int typeId) { + addParam("type_id", typeId); + return this; + } + + /** + * 设置项目类型 + * + * @param type 项目类型 + * @return 当前API实例,支持链式调用 + */ + public CreateWebsiteApi setType(String type) { + addParam("type", type); + return this; + } + + /** + * 设置PHP版本 + * + * @param phpVersion PHP版本 + * @return 当前API实例,支持链式调用 + */ + public CreateWebsiteApi setPhpVersion(String phpVersion) { + if (StrUtil.isEmpty(phpVersion)) { + throw new IllegalArgumentException("PHP version cannot be empty"); } - - /** - * 设置是否创建FTP - * - * @param createFtp 是否创建FTP - * @return 当前API实例,支持链式调用 - */ - public CreateWebsiteApi setCreateFtp(boolean createFtp) { - addParam("ftp", createFtp); - return this; + addParam("version", phpVersion); + return this; + } + + /** + * 设置网站端口 + * + * @param port 网站端口 + * @return 当前API实例,支持链式调用 + */ + public CreateWebsiteApi setPort(int port) { + addParam("port", port); + return this; + } + + /** + * 设置网站备注 + * + * @param ps 网站备注 + * @return 当前API实例,支持链式调用 + */ + public CreateWebsiteApi setPs(String ps) { + if (StrUtil.isEmpty(ps)) { + throw new IllegalArgumentException("PS cannot be empty"); } - - /** - * 设置FTP用户名和密码(当创建FTP时必需) - * - * @param ftpUsername FTP用户名 - * @param ftpPassword FTP密码 - * @return 当前API实例,支持链式调用 - */ - public CreateWebsiteApi setFtpCredentials(String ftpUsername, String ftpPassword) { - if (StrUtil.isEmpty(ftpUsername) || StrUtil.isEmpty(ftpPassword)) { - throw new IllegalArgumentException("FTP username and password cannot be empty"); - } - addParam("ftp_username", ftpUsername); - addParam("ftp_password", ftpPassword); - setCreateFtp(true); - return this; + addParam("ps", ps); + return this; + } + + /** + * 设置是否创建FTP + * + * @param createFtp 是否创建FTP + * @return 当前API实例,支持链式调用 + */ + public CreateWebsiteApi setCreateFtp(boolean createFtp) { + addParam("ftp", createFtp); + return this; + } + + /** + * 设置FTP用户名和密码(当创建FTP时必需) + * + * @param ftpUsername FTP用户名 + * @param ftpPassword FTP密码 + * @return 当前API实例,支持链式调用 + */ + public CreateWebsiteApi setFtpCredentials(String ftpUsername, String ftpPassword) { + if (StrUtil.isEmpty(ftpUsername) || StrUtil.isEmpty(ftpPassword)) { + throw new IllegalArgumentException("FTP username and password cannot be empty"); } - - /** - * 设置是否创建数据库 - * - * @param createDatabase 是否创建数据库 - * @return 当前API实例,支持链式调用 - */ - public CreateWebsiteApi setCreateDatabase(boolean createDatabase) { - addParam("sql", createDatabase); - return this; + addParam("ftp_username", ftpUsername); + addParam("ftp_password", ftpPassword); + setCreateFtp(true); + return this; + } + + /** + * 设置是否创建数据库 + * + * @param createDatabase 是否创建数据库 + * @return 当前API实例,支持链式调用 + */ + public CreateWebsiteApi setCreateDatabase(boolean createDatabase) { + addParam("sql", createDatabase); + return this; + } + + /** + * 设置数据库信息(当创建数据库时必需) + * + * @param databaseUser 数据库用户名 + * @param databasePassword 数据库密码 + * @param charset 数据库字符集 + * @return 当前API实例,支持链式调用 + */ + public CreateWebsiteApi setDatabaseCredentials( + String databaseUser, String databasePassword, String charset) { + if (StrUtil.isEmpty(databaseUser) + || StrUtil.isEmpty(databasePassword) + || StrUtil.isEmpty(charset)) { + throw new IllegalArgumentException("Database user, password and charset cannot be empty"); } - - /** - * 设置数据库信息(当创建数据库时必需) - * - * @param databaseUser 数据库用户名 - * @param databasePassword 数据库密码 - * @param charset 数据库字符集 - * @return 当前API实例,支持链式调用 - */ - public CreateWebsiteApi setDatabaseCredentials(String databaseUser, String databasePassword, String charset) { - if (StrUtil.isEmpty(databaseUser) || StrUtil.isEmpty(databasePassword) || StrUtil.isEmpty(charset)) { - throw new IllegalArgumentException("Database user, password and charset cannot be empty"); - } - addParam("codeing", charset); - addParam("datauser", databaseUser); - addParam("datapassword", databasePassword); - setCreateDatabase(true); - return this; + addParam("codeing", charset); + addParam("datauser", databaseUser); + addParam("datapassword", databasePassword); + setCreateDatabase(true); + return this; + } + + /** + * 解析API响应字符串为BtResult对象 + * + * @param response API响应字符串 + * @return BtResult对象 + * @throws BtApiException 当解析失败时抛出 + */ + @Override + public BtResult parseResponse(String response) { + if (response == null || response.isEmpty()) { + throw new BtApiException("Empty response received"); } - - /** - * 解析API响应字符串为BtResult对象 - * - * @param response API响应字符串 - * @return BtResult对象 - * @throws BtApiException 当解析失败时抛出 - */ - @Override - public BtResult parseResponse(String response) { - if (response == null || response.isEmpty()) { - throw new BtApiException("Empty response received"); - } - try { - if (!JSONUtil.isTypeJSON(response)) { - throw new BtApiException("Invalid JSON response: " + response); - } + try { + if (!JSONUtil.isTypeJSON(response)) { + throw new BtApiException("Invalid JSON response: " + response); + } + + JSONObject json = JSONUtil.parseObj(response); + BtResult result = new BtResult<>(); + + // 检查响应状态 + boolean siteStatus = json.getBool("siteStatus", false); + result.setStatus(siteStatus); + + // 解析创建网站结果 + if (siteStatus) { + CreateWebsiteResult createResult = new CreateWebsiteResult(); + createResult.setSiteStatus(siteStatus); + createResult.setFtpStatus(json.getBool("ftpStatus", false)); + createResult.setFtpUser(json.getStr("ftpUser", "")); + createResult.setFtpPass(json.getStr("ftpPass", "")); + createResult.setDatabaseStatus(json.getBool("databaseStatus", false)); + createResult.setDatabaseUser(json.getStr("databaseUser", "")); + createResult.setDatabasePass(json.getStr("databasePass", "")); + + result.setData(createResult); + result.setMsg("Website created successfully"); + } else { + result.setMsg(json.getStr("msg", "Failed to create website")); + } - JSONObject json = JSONUtil.parseObj(response); - BtResult result = new BtResult<>(); - - // 检查响应状态 - boolean siteStatus = json.getBool("siteStatus", false); - result.setStatus(siteStatus); - - // 解析创建网站结果 - if (siteStatus) { - CreateWebsiteResult createResult = new CreateWebsiteResult(); - createResult.setSiteStatus(siteStatus); - createResult.setFtpStatus(json.getBool("ftpStatus", false)); - createResult.setFtpUser(json.getStr("ftpUser", "")); - createResult.setFtpPass(json.getStr("ftpPass", "")); - createResult.setDatabaseStatus(json.getBool("databaseStatus", false)); - createResult.setDatabaseUser(json.getStr("databaseUser", "")); - createResult.setDatabasePass(json.getStr("databasePass", "")); - - result.setData(createResult); - result.setMsg("Website created successfully"); - } else { - result.setMsg(json.getStr("msg", "Failed to create website")); - } - - return result; + return result; - } catch (JSONException e) { - throw new BtApiException("Invalid JSON response: " + response); - } catch (Exception e) { - throw new BtApiException("Failed to parse create website response: " + e.getMessage(), e); - } + } catch (JSONException e) { + throw new BtApiException("Invalid JSON response: " + response); + } catch (Exception e) { + throw new BtApiException("Failed to parse create website response: " + e.getMessage(), e); } -} \ No newline at end of file + } +} diff --git a/src/main/java/net/heimeng/sdk/btapi/api/website/CreateWebsiteBackupApi.java b/src/main/java/net/heimeng/sdk/btapi/api/website/CreateWebsiteBackupApi.java index bec03ff..dd18c9f 100644 --- a/src/main/java/net/heimeng/sdk/btapi/api/website/CreateWebsiteBackupApi.java +++ b/src/main/java/net/heimeng/sdk/btapi/api/website/CreateWebsiteBackupApi.java @@ -1,84 +1,22 @@ package net.heimeng.sdk.btapi.api.website; -import cn.hutool.json.JSONObject; -import cn.hutool.json.JSONUtil; -import net.heimeng.sdk.btapi.api.BaseBtApi; -import net.heimeng.sdk.btapi.exception.BtApiException; -import net.heimeng.sdk.btapi.model.BtResult; +/** 创建站点备份的 API。 */ +public class CreateWebsiteBackupApi extends AbstractWebsiteBooleanApi { -/** - * 创建网站备份API实现 - *

- * 用于为宝塔面板中的指定网站创建备份。 - *

- * - * @author InwardFlow - * @since 2.0.0 - */ -public class CreateWebsiteBackupApi extends BaseBtApi> { - - /** - * API端点路径 - */ - private static final String ENDPOINT = "site?action=ToBackup"; - - /** - * 构造函数,创建一个新的CreateWebsiteBackupApi实例 - */ - public CreateWebsiteBackupApi() { - super(ENDPOINT, HttpMethod.POST); - } - - /** - * 设置网站ID - * - * @param id 网站ID - * @return 当前API实例,支持链式调用 - */ - public CreateWebsiteBackupApi setId(Integer id) { - addParam("id", id); - return this; - } - - /** - * 验证请求参数是否有效 - * - * @return 如果请求参数有效则返回true,否则返回false - */ - @Override - protected boolean validateParams() { - return params.containsKey("id"); - } - - /** - * 解析API响应字符串为BtResult对象 - * - * @param response API响应字符串 - * @return BtResult对象 - * @throws BtApiException 当解析失败时抛出 - */ - @Override - public BtResult parseResponse(String response) { - if (response == null || response.isEmpty()) { - throw new BtApiException("Empty response received"); - } - - try { - if (!JSONUtil.isTypeJSON(response)) { - throw new BtApiException("Invalid JSON response: " + response); - } - - JSONObject json = JSONUtil.parseObj(response); - BtResult result = new BtResult<>(); - boolean status = json.getBool("status", false); - - result.setStatus(status); - result.setMsg(json.getStr("msg", status ? "备份成功" : "备份失败")); - result.setData(status); - - return result; - } catch (Exception e) { - throw new BtApiException("Failed to parse create website backup response: " + e.getMessage(), e); - } - } -} \ No newline at end of file + private static final String ENDPOINT = "site?action=ToBackup"; + + public CreateWebsiteBackupApi() { + super(ENDPOINT, "站点备份创建成功", "站点备份创建失败"); + } + + public CreateWebsiteBackupApi setId(Integer id) { + requirePositiveInteger(id, "id"); + addParam("id", id); + return this; + } + + @Override + protected boolean validateParams() { + return hasPositiveIntegerParam("id"); + } +} diff --git a/src/main/java/net/heimeng/sdk/btapi/api/website/DeleteWebsiteApi.java b/src/main/java/net/heimeng/sdk/btapi/api/website/DeleteWebsiteApi.java index 9ea4b85..6dced3e 100644 --- a/src/main/java/net/heimeng/sdk/btapi/api/website/DeleteWebsiteApi.java +++ b/src/main/java/net/heimeng/sdk/btapi/api/website/DeleteWebsiteApi.java @@ -1,163 +1,69 @@ package net.heimeng.sdk.btapi.api.website; -import cn.hutool.core.util.StrUtil; -import cn.hutool.json.JSONException; -import cn.hutool.json.JSONObject; -import cn.hutool.json.JSONUtil; -import net.heimeng.sdk.btapi.api.BaseBtApi; -import net.heimeng.sdk.btapi.exception.BtApiException; -import net.heimeng.sdk.btapi.model.BtResult; - /** - * 删除网站API实现 - *

- * 用于在宝塔面板中删除指定的网站,支持选择是否同时删除关联的FTP、数据库和网站根目录。 - *

+ * 删除站点的 API。 * - * @author InwardFlow - * @since 2.0.0 + *

支持按需同时删除关联 FTP、数据库与站点目录。 */ -public class DeleteWebsiteApi extends BaseBtApi> { - - /** - * API端点路径 - */ - private static final String ENDPOINT = "site?action=DeleteSite"; - - /** - * 构造函数,创建一个新的DeleteWebsiteApi实例 - * - * @param id 网站ID - * @param webname 网站名称 - */ - public DeleteWebsiteApi(int id, String webname) { - super(ENDPOINT, HttpMethod.POST); - - if (id <= 0) { - throw new IllegalArgumentException("Website ID must be positive"); - } - - if (StrUtil.isEmpty(webname)) { - throw new IllegalArgumentException("Website name cannot be empty"); - } - - setId(id); - setWebname(webname); - } - - /** - * 设置网站ID - * - * @param id 网站ID - * @return 当前API实例,支持链式调用 - */ - public DeleteWebsiteApi setId(int id) { - if (id <= 0) { - throw new IllegalArgumentException("Website ID must be positive"); - } - addParam("id", id); - return this; - } - - /** - * 设置网站名称 - * - * @param webname 网站名称 - * @return 当前API实例,支持链式调用 - */ - public DeleteWebsiteApi setWebname(String webname) { - if (StrUtil.isEmpty(webname)) { - throw new IllegalArgumentException("Website name cannot be empty"); - } - addParam("webname", webname); - return this; - } - - /** - * 设置是否删除关联的FTP - *

注意:如果不删除请不要调用此方法

- * - * @param deleteFtp 是否删除关联FTP - * @return 当前API实例,支持链式调用 - */ - public DeleteWebsiteApi setDeleteFtp(boolean deleteFtp) { - if (deleteFtp) { - addParam("ftp", 1); - } - return this; - } - - /** - * 设置是否删除关联的数据库 - *

注意:如果不删除请不要调用此方法

- * - * @param deleteDatabase 是否删除关联数据库 - * @return 当前API实例,支持链式调用 - */ - public DeleteWebsiteApi setDeleteDatabase(boolean deleteDatabase) { - if (deleteDatabase) { - addParam("database", 1); - } - return this; - } - - /** - * 设置是否删除网站根目录 - *

注意:如果不删除请不要调用此方法

- * - * @param deletePath 是否删除网站根目录 - * @return 当前API实例,支持链式调用 - */ - public DeleteWebsiteApi setDeletePath(boolean deletePath) { - if (deletePath) { - addParam("path", 1); - } - return this; - } - - /** - * 验证请求参数是否有效 - * - * @return 如果请求参数有效则返回true,否则返回false - */ - @Override - protected boolean validateParams() { - return params.containsKey("id") && params.containsKey("webname"); - } - - /** - * 解析API响应字符串为BtResult对象 - * - * @param response API响应字符串 - * @return BtResult对象,data为true表示删除成功 - * @throws BtApiException 当解析失败时抛出 - */ - @Override - public BtResult parseResponse(String response) { - if (response == null || response.isEmpty()) { - throw new BtApiException("Empty response received"); - } +public class DeleteWebsiteApi extends AbstractWebsiteBooleanApi { - try { - if (!JSONUtil.isTypeJSON(response)) { - throw new BtApiException("Invalid JSON response: " + response); - } + private static final String ENDPOINT = "site?action=DeleteSite"; - JSONObject json = JSONUtil.parseObj(response); - BtResult result = new BtResult<>(); - - // 检查响应状态 - boolean success = json.getBool("status", false); - result.setStatus(success); - result.setMsg(json.getStr("msg", success ? "网站删除成功" : "网站删除失败")); - result.setData(success); - - return result; + public DeleteWebsiteApi(int id, String webname) { + this(); + setId(id); + setWebname(webname); + } - } catch (JSONException e) { - throw new BtApiException("Invalid JSON response: " + response); - } catch (Exception e) { - throw new BtApiException("Failed to parse delete website response: " + e.getMessage(), e); - } + public DeleteWebsiteApi() { + super(ENDPOINT, "站点删除成功", "站点删除失败"); + } + + public DeleteWebsiteApi setId(int id) { + requirePositiveInteger(id, "id"); + addParam("id", id); + return this; + } + + public DeleteWebsiteApi setWebname(String webname) { + requireNonBlank(webname, "webname"); + addParam("webname", webname); + return this; + } + + public DeleteWebsiteApi setDeleteFtp(boolean deleteFtp) { + if (deleteFtp) { + addParam("ftp", 1); + } else { + removeParam("ftp"); + } + return this; + } + + public DeleteWebsiteApi setDeleteDatabase(boolean deleteDatabase) { + if (deleteDatabase) { + addParam("database", 1); + } else { + removeParam("database"); + } + return this; + } + + public DeleteWebsiteApi setDeletePath(boolean deletePath) { + if (deletePath) { + addParam("path", 1); + } else { + removeParam("path"); } -} \ No newline at end of file + return this; + } + + @Override + protected boolean validateParams() { + return hasPositiveIntegerParam("id") + && hasNonBlankStringParam("webname") + && hasOptionalBooleanFlagIntParam("ftp") + && hasOptionalBooleanFlagIntParam("database") + && hasOptionalBooleanFlagIntParam("path"); + } +} diff --git a/src/main/java/net/heimeng/sdk/btapi/api/website/DeleteWebsiteBackupApi.java b/src/main/java/net/heimeng/sdk/btapi/api/website/DeleteWebsiteBackupApi.java index 1345b6c..8d6f17c 100644 --- a/src/main/java/net/heimeng/sdk/btapi/api/website/DeleteWebsiteBackupApi.java +++ b/src/main/java/net/heimeng/sdk/btapi/api/website/DeleteWebsiteBackupApi.java @@ -1,84 +1,22 @@ package net.heimeng.sdk.btapi.api.website; -import cn.hutool.json.JSONObject; -import cn.hutool.json.JSONUtil; -import net.heimeng.sdk.btapi.api.BaseBtApi; -import net.heimeng.sdk.btapi.exception.BtApiException; -import net.heimeng.sdk.btapi.model.BtResult; +/** 删除站点备份的 API。 */ +public class DeleteWebsiteBackupApi extends AbstractWebsiteBooleanApi { -/** - * 删除网站备份API实现 - *

- * 用于删除宝塔面板中的网站备份。 - *

- * - * @author InwardFlow - * @since 2.0.0 - */ -public class DeleteWebsiteBackupApi extends BaseBtApi> { - - /** - * API端点路径 - */ - private static final String ENDPOINT = "site?action=DelBackup"; - - /** - * 构造函数,创建一个新的DeleteWebsiteBackupApi实例 - */ - public DeleteWebsiteBackupApi() { - super(ENDPOINT, HttpMethod.POST); - } - - /** - * 设置备份列表ID - * - * @param id 备份列表ID - * @return 当前API实例,支持链式调用 - */ - public DeleteWebsiteBackupApi setId(Integer id) { - addParam("id", id); - return this; - } - - /** - * 验证请求参数是否有效 - * - * @return 如果请求参数有效则返回true,否则返回false - */ - @Override - protected boolean validateParams() { - return params.containsKey("id"); - } - - /** - * 解析API响应字符串为BtResult对象 - * - * @param response API响应字符串 - * @return BtResult对象 - * @throws BtApiException 当解析失败时抛出 - */ - @Override - public BtResult parseResponse(String response) { - if (response == null || response.isEmpty()) { - throw new BtApiException("Empty response received"); - } - - try { - if (!JSONUtil.isTypeJSON(response)) { - throw new BtApiException("Invalid JSON response: " + response); - } - - JSONObject json = JSONUtil.parseObj(response); - BtResult result = new BtResult<>(); - boolean status = json.getBool("status", false); - - result.setStatus(status); - result.setMsg(json.getStr("msg", status ? "删除成功" : "删除失败")); - result.setData(status); - - return result; - } catch (Exception e) { - throw new BtApiException("Failed to parse delete website backup response: " + e.getMessage(), e); - } - } -} \ No newline at end of file + private static final String ENDPOINT = "site?action=DelBackup"; + + public DeleteWebsiteBackupApi() { + super(ENDPOINT, "站点备份删除成功", "站点备份删除失败"); + } + + public DeleteWebsiteBackupApi setId(Integer id) { + requirePositiveInteger(id, "id"); + addParam("id", id); + return this; + } + + @Override + protected boolean validateParams() { + return hasPositiveIntegerParam("id"); + } +} diff --git a/src/main/java/net/heimeng/sdk/btapi/api/website/DeleteWebsiteDomainApi.java b/src/main/java/net/heimeng/sdk/btapi/api/website/DeleteWebsiteDomainApi.java index 28c1039..830c5aa 100644 --- a/src/main/java/net/heimeng/sdk/btapi/api/website/DeleteWebsiteDomainApi.java +++ b/src/main/java/net/heimeng/sdk/btapi/api/website/DeleteWebsiteDomainApi.java @@ -1,118 +1,46 @@ package net.heimeng.sdk.btapi.api.website; -import cn.hutool.json.JSONObject; -import cn.hutool.json.JSONUtil; -import net.heimeng.sdk.btapi.api.BaseBtApi; -import net.heimeng.sdk.btapi.exception.BtApiException; -import net.heimeng.sdk.btapi.model.BtResult; - /** - * 删除域名API实现 - *

- * 用于删除宝塔面板中指定网站的域名。 - *

+ * 删除站点域名的 API。 * - * @author InwardFlow - * @since 2.0.0 + *

用于移除指定站点中的单个域名绑定。 */ -public class DeleteWebsiteDomainApi extends BaseBtApi> { - - /** - * API端点路径 - */ - private static final String ENDPOINT = "site?action=DelDomain"; - - /** - * 构造函数,创建一个新的DeleteWebsiteDomainApi实例 - */ - public DeleteWebsiteDomainApi() { - super(ENDPOINT, HttpMethod.POST); - } - - /** - * 设置网站ID - * - * @param id 网站ID - * @return 当前API实例,支持链式调用 - */ - public DeleteWebsiteDomainApi setId(Integer id) { - addParam("id", id); - return this; - } - - /** - * 设置网站名称 - * - * @param webname 网站名称 - * @return 当前API实例,支持链式调用 - */ - public DeleteWebsiteDomainApi setWebname(String webname) { - addParam("webname", webname); - return this; - } - - /** - * 设置要删除的域名 - * - * @param domain 要被删除的域名 - * @return 当前API实例,支持链式调用 - */ - public DeleteWebsiteDomainApi setDomain(String domain) { - addParam("domain", domain); - return this; - } - - /** - * 设置域名的端口 - * - * @param port 端口号 - * @return 当前API实例,支持链式调用 - */ - public DeleteWebsiteDomainApi setPort(Integer port) { - addParam("port", port); - return this; - } - - /** - * 验证请求参数是否有效 - * - * @return 如果请求参数有效则返回true,否则返回false - */ - @Override - protected boolean validateParams() { - return params.containsKey("id") && params.containsKey("webname") && - params.containsKey("domain") && params.containsKey("port"); - } - - /** - * 解析API响应字符串为BtResult对象 - * - * @param response API响应字符串 - * @return BtResult对象 - * @throws BtApiException 当解析失败时抛出 - */ - @Override - public BtResult parseResponse(String response) { - if (response == null || response.isEmpty()) { - throw new BtApiException("Empty response received"); - } - - try { - if (!JSONUtil.isTypeJSON(response)) { - throw new BtApiException("Invalid JSON response: " + response); - } - - JSONObject json = JSONUtil.parseObj(response); - BtResult result = new BtResult<>(); - boolean status = json.getBool("status", false); - - result.setStatus(status); - result.setMsg(json.getStr("msg", status ? "删除成功" : "删除失败")); - result.setData(status); - - return result; - } catch (Exception e) { - throw new BtApiException("Failed to parse delete website domain response: " + e.getMessage(), e); - } - } -} \ No newline at end of file +public class DeleteWebsiteDomainApi extends AbstractWebsiteBooleanApi { + + private static final String ENDPOINT = "site?action=DelDomain"; + + public DeleteWebsiteDomainApi() { + super(ENDPOINT, "域名删除成功", "域名删除失败"); + } + + public DeleteWebsiteDomainApi setId(Integer id) { + requirePositiveInteger(id, "id"); + addParam("id", id); + return this; + } + + public DeleteWebsiteDomainApi setWebname(String webname) { + requireNonBlank(webname, "webname"); + addParam("webname", webname); + return this; + } + + public DeleteWebsiteDomainApi setDomain(String domain) { + requireNonBlank(domain, "domain"); + addParam("domain", domain); + return this; + } + + public DeleteWebsiteDomainApi setPort(Integer port) { + requirePositiveInteger(port, "port"); + addParam("port", port); + return this; + } + + @Override + protected boolean validateParams() { + return hasPositiveIntegerParam("id") + && hasRequiredParams("webname", "domain") + && hasPositiveIntegerParam("port"); + } +} diff --git a/src/main/java/net/heimeng/sdk/btapi/api/website/GetPhpVersionsApi.java b/src/main/java/net/heimeng/sdk/btapi/api/website/GetPhpVersionsApi.java index c00817a..93aff4a 100644 --- a/src/main/java/net/heimeng/sdk/btapi/api/website/GetPhpVersionsApi.java +++ b/src/main/java/net/heimeng/sdk/btapi/api/website/GetPhpVersionsApi.java @@ -1,93 +1,66 @@ package net.heimeng.sdk.btapi.api.website; +import java.util.ArrayList; +import java.util.List; + +import cn.hutool.json.JSON; import cn.hutool.json.JSONArray; -import cn.hutool.json.JSONException; import cn.hutool.json.JSONObject; -import cn.hutool.json.JSONUtil; + import net.heimeng.sdk.btapi.api.BaseBtApi; import net.heimeng.sdk.btapi.exception.BtApiException; import net.heimeng.sdk.btapi.model.BtResult; import net.heimeng.sdk.btapi.model.website.PhpVersion; -import java.util.ArrayList; -import java.util.List; - /** - * 获取PHP版本列表API实现 - *

- * 用于获取宝塔面板中已安装的PHP版本列表。 - *

+ * 获取已安装 PHP 版本列表的 API。 * - * @author InwardFlow - * @since 2.0.0 + *

兼容面板直接返回数组以及带 {@code status/msg/data} 包装的响应。 */ public class GetPhpVersionsApi extends BaseBtApi>> { - - /** - * API端点路径 - */ - private static final String ENDPOINT = "site?action=GetPHPVersion"; - - /** - * 构造函数,创建一个新的GetPhpVersionsApi实例 - */ - public GetPhpVersionsApi() { - super(ENDPOINT, HttpMethod.POST); + + private static final String ENDPOINT = "site?action=GetPHPVersion"; + + public GetPhpVersionsApi() { + super(ENDPOINT, HttpMethod.POST); + } + + @Override + public BtResult> parseResponse(String response) { + JSON json = WebsiteApiResponseSupport.parseJsonResponse(response, "php versions"); + if (json instanceof JSONArray jsonArray) { + return WebsiteApiResponseSupport.successListResult(parseVersions(jsonArray), "Success"); + } + if (!(json instanceof JSONObject jsonObject)) { + throw new BtApiException("PHP versions response must be a JSON object or array"); + } + if (jsonObject.containsKey("status") && !jsonObject.getBool("status", false)) { + return WebsiteApiResponseSupport.failureListResult( + jsonObject.getStr("msg", "Failed to fetch PHP versions")); + } + + JSONArray dataArray = jsonObject.getJSONArray("data"); + if (dataArray == null) { + throw new BtApiException("PHP versions response is missing required data array"); } - - /** - * 解析API响应字符串为BtResult>对象 - * - * @param response API响应字符串 - * @return BtResult>对象 - * @throws BtApiException 当解析失败时抛出 - */ - @Override - public BtResult> parseResponse(String response) { - if (response == null || response.isEmpty()) { - throw new BtApiException("Empty response received"); - } - try { - if (!JSONUtil.isTypeJSON(response)) { - throw new BtApiException("Invalid JSON response: " + response); - } + return WebsiteApiResponseSupport.successListResult( + parseVersions(dataArray), jsonObject.getStr("msg", "Success")); + } - Object jsonObj = JSONUtil.parse(response); - BtResult> result = new BtResult<>(); - List phpVersions = new ArrayList<>(); - - // 响应可能是直接的数组格式 - if (jsonObj instanceof JSONArray) { - JSONArray jsonArray = (JSONArray) jsonObj; - result.setStatus(true); - result.setMsg("Success"); - - // 解析每个PHP版本信息 - for (int i = 0; i < jsonArray.size(); i++) { - JSONObject versionJson = jsonArray.getJSONObject(i); - if (versionJson != null) { - PhpVersion phpVersion = new PhpVersion(); - phpVersion.setVersion(versionJson.getStr("version", "")); - phpVersion.setName(versionJson.getStr("name", "")); - - phpVersions.add(phpVersion); - } - } - } else if (jsonObj instanceof JSONObject) { - // 响应可能是带有status字段的错误格式 - JSONObject json = (JSONObject) jsonObj; - result.setStatus(json.getBool("status", false)); - result.setMsg(json.getStr("msg", "")); - } - - result.setData(phpVersions); - return result; + private List parseVersions(JSONArray jsonArray) { + List phpVersions = new ArrayList<>(); + for (int i = 0; i < jsonArray.size(); i++) { + JSONObject versionJson = jsonArray.getJSONObject(i); + if (versionJson == null) { + continue; + } - } catch (JSONException e) { - throw new BtApiException("Invalid JSON response: " + response); - } catch (Exception e) { - throw new BtApiException("Failed to parse PHP versions response: " + e.getMessage(), e); - } + PhpVersion phpVersion = new PhpVersion(); + phpVersion.setVersion(versionJson.getStr("version", "")); + phpVersion.setName(versionJson.getStr("name", "")); + phpVersions.add(phpVersion); } -} \ No newline at end of file + return List.copyOf(phpVersions); + } +} diff --git a/src/main/java/net/heimeng/sdk/btapi/api/website/GetWebsiteBackupsApi.java b/src/main/java/net/heimeng/sdk/btapi/api/website/GetWebsiteBackupsApi.java index 56a8f8d..56f6388 100644 --- a/src/main/java/net/heimeng/sdk/btapi/api/website/GetWebsiteBackupsApi.java +++ b/src/main/java/net/heimeng/sdk/btapi/api/website/GetWebsiteBackupsApi.java @@ -1,143 +1,78 @@ package net.heimeng.sdk.btapi.api.website; -import cn.hutool.json.JSONArray; -import cn.hutool.json.JSONObject; -import cn.hutool.json.JSONUtil; -import net.heimeng.sdk.btapi.api.BaseBtApi; -import net.heimeng.sdk.btapi.exception.BtApiException; -import net.heimeng.sdk.btapi.model.BtResult; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - /** * 获取网站备份列表API实现 - *

- * 用于获取宝塔面板中指定网站的备份列表。 - *

+ * + *

用于获取宝塔面板中指定网站的备份列表。 * * @author InwardFlow * @since 2.0.0 */ -public class GetWebsiteBackupsApi extends BaseBtApi>>> { - - /** - * API端点路径 - */ - private static final String ENDPOINT = "data?action=getData&table=backup"; - - /** - * 构造函数,创建一个新的GetWebsiteBackupsApi实例 - */ - public GetWebsiteBackupsApi() { - super(ENDPOINT, HttpMethod.POST); - // 设置默认参数 - addParam("p", 1); - addParam("limit", 5); - addParam("type", 0); - } - - /** - * 设置当前分页 - * - * @param page 当前分页 - * @return 当前API实例,支持链式调用 - */ - public GetWebsiteBackupsApi setPage(Integer page) { - addParam("p", page); - return this; - } - - /** - * 设置每页取回的数据行数 - * - * @param limit 数据行数 - * @return 当前API实例,支持链式调用 - */ - public GetWebsiteBackupsApi setLimit(Integer limit) { - addParam("limit", limit); - return this; - } - - /** - * 设置网站ID - * - * @param siteId 网站ID - * @return 当前API实例,支持链式调用 - */ - public GetWebsiteBackupsApi setSiteId(Integer siteId) { - addParam("search", siteId); - return this; - } - - /** - * 设置分页JS回调 - * - * @param callback JS回调函数名 - * @return 当前API实例,支持链式调用 - */ - public GetWebsiteBackupsApi setCallback(String callback) { - addParam("tojs", callback); - return this; - } - - /** - * 验证请求参数是否有效 - * - * @return 如果请求参数有效则返回true,否则返回false - */ - @Override - protected boolean validateParams() { - return params.containsKey("limit") && params.containsKey("search"); - } - - /** - * 解析API响应字符串为BtResult>>对象 - * - * @param response API响应字符串 - * @return BtResult>>对象 - * @throws BtApiException 当解析失败时抛出 - */ - @Override - public BtResult>> parseResponse(String response) { - if (response == null || response.isEmpty()) { - throw new BtApiException("Empty response received"); - } - - try { - if (!JSONUtil.isTypeJSON(response)) { - throw new BtApiException("Invalid JSON response: " + response); - } - - JSONObject json = JSONUtil.parseObj(response); - BtResult>> result = new BtResult<>(); - result.setStatus(true); - result.setMsg("Success"); - - // 解析备份列表数据 - JSONArray dataArray = json.getJSONArray("data"); - List> backups = new ArrayList<>(); - - if (dataArray != null) { - for (int i = 0; i < dataArray.size(); i++) { - JSONObject backupJson = dataArray.getJSONObject(i); - if (backupJson != null) { - Map backupMap = new HashMap<>(); - // 将JSON对象转换为Map - for (String key : backupJson.keySet()) { - backupMap.put(key, backupJson.get(key)); - } - backups.add(backupMap); - } - } - } - - result.setData(backups); - return result; - } catch (Exception e) { - throw new BtApiException("Failed to parse website backups response: " + e.getMessage(), e); - } - } -} \ No newline at end of file +public class GetWebsiteBackupsApi extends AbstractWebsiteMapListQueryApi { + + /** API端点路径 */ + private static final String ENDPOINT = "data?action=getData&table=backup"; + + /** 构造函数,创建一个新的GetWebsiteBackupsApi实例 */ + public GetWebsiteBackupsApi() { + super(ENDPOINT, "website backups", "Success", "Failed to fetch website backups", "data", false); + // 设置默认参数 + addParam("p", 1); + addParam("limit", 5); + addParam("type", 0); + } + + /** + * 设置当前分页 + * + * @param page 当前分页 + * @return 当前API实例,支持链式调用 + */ + public GetWebsiteBackupsApi setPage(Integer page) { + addParam("p", page); + return this; + } + + /** + * 设置每页取回的数据行数 + * + * @param limit 数据行数 + * @return 当前API实例,支持链式调用 + */ + public GetWebsiteBackupsApi setLimit(Integer limit) { + addParam("limit", limit); + return this; + } + + /** + * 设置网站ID + * + * @param siteId 网站ID + * @return 当前API实例,支持链式调用 + */ + public GetWebsiteBackupsApi setSiteId(Integer siteId) { + addParam("search", siteId); + return this; + } + + /** + * 设置分页JS回调 + * + * @param callback JS回调函数名 + * @return 当前API实例,支持链式调用 + */ + public GetWebsiteBackupsApi setCallback(String callback) { + addParam("tojs", callback); + return this; + } + + /** + * 验证请求参数是否有效 + * + * @return 如果请求参数有效则返回true,否则返回false + */ + @Override + protected boolean validateParams() { + return hasPositiveIntegerParam("limit") && hasPositiveIntegerParam("search"); + } +} diff --git a/src/main/java/net/heimeng/sdk/btapi/api/website/GetWebsiteConfigApi.java b/src/main/java/net/heimeng/sdk/btapi/api/website/GetWebsiteConfigApi.java index cf39125..023b3fb 100644 --- a/src/main/java/net/heimeng/sdk/btapi/api/website/GetWebsiteConfigApi.java +++ b/src/main/java/net/heimeng/sdk/btapi/api/website/GetWebsiteConfigApi.java @@ -1,129 +1,30 @@ package net.heimeng.sdk.btapi.api.website; -import cn.hutool.json.JSONArray; -import cn.hutool.json.JSONObject; -import cn.hutool.json.JSONUtil; -import net.heimeng.sdk.btapi.api.BaseBtApi; -import net.heimeng.sdk.btapi.exception.BtApiException; -import net.heimeng.sdk.btapi.model.BtResult; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - /** - * 获取网站配置信息API实现 - *

- * 用于获取宝塔面板中指定网站的防跨站配置、运行目录、日志开关状态、可设置的运行目录列表、密码访问状态等信息。 - *

+ * 获取网站配置的 API。 * - * @author InwardFlow - * @since 2.0.0 + *

根据本地文档,该接口成功时通常直接返回配置对象;失败场景下也可能返回带 {@code status/msg} 的包装响应。 */ -public class GetWebsiteConfigApi extends BaseBtApi>> { - - /** - * API端点路径 - */ - private static final String ENDPOINT = "site?action=GetDirUserINI"; - - /** - * 构造函数,创建一个新的GetWebsiteConfigApi实例 - */ - public GetWebsiteConfigApi() { - super(ENDPOINT, HttpMethod.POST); - } - - /** - * 设置网站ID - * - * @param id 网站ID - * @return 当前API实例,支持链式调用 - */ - public GetWebsiteConfigApi setId(Integer id) { - addParam("id", id); - return this; - } - - /** - * 设置网站根目录 - * - * @param path 网站根目录 - * @return 当前API实例,支持链式调用 - */ - public GetWebsiteConfigApi setPath(String path) { - addParam("path", path); - return this; - } - - /** - * 验证请求参数是否有效 - * - * @return 如果请求参数有效则返回true,否则返回false - */ - @Override - protected boolean validateParams() { - return params.containsKey("id") && params.containsKey("path"); - } - - /** - * 解析API响应字符串为BtResult>对象 - * - * @param response API响应字符串 - * @return BtResult>对象 - * @throws BtApiException 当解析失败时抛出 - */ - @Override - public BtResult> parseResponse(String response) { - if (response == null || response.isEmpty()) { - throw new BtApiException("Empty response received"); - } - - try { - if (!JSONUtil.isTypeJSON(response)) { - throw new BtApiException("Invalid JSON response: " + response); - } - - JSONObject json = JSONUtil.parseObj(response); - BtResult> result = new BtResult<>(); - - // 创建配置信息Map - Map configMap = new HashMap<>(); - - // 解析各项配置 - configMap.put("pass", json.getBool("pass", false)); - configMap.put("logs", json.getBool("logs", true)); - configMap.put("userini", json.getBool("userini", true)); - - // 解析运行目录信息 - JSONObject runPathJson = json.getJSONObject("runPath"); - if (runPathJson != null) { - Map runPathMap = new HashMap<>(); - - // 解析可设置的运行目录列表 - JSONArray dirsArray = runPathJson.getJSONArray("dirs"); - if (dirsArray != null) { - List dirsList = new ArrayList<>(); - for (int i = 0; i < dirsArray.size(); i++) { - dirsList.add(dirsArray.getStr(i, "")); - } - runPathMap.put("dirs", dirsList); - } - - // 解析当前运行目录 - runPathMap.put("runPath", runPathJson.getStr("runPath", "/")); - - configMap.put("runPath", runPathMap); - } - - result.setStatus(true); - result.setMsg("获取成功"); - result.setData(configMap); - - return result; - } catch (Exception e) { - throw new BtApiException("Failed to parse website config response: " + e.getMessage(), e); - } - } -} \ No newline at end of file +public class GetWebsiteConfigApi extends AbstractWebsiteMapQueryApi { + + private static final String ENDPOINT = "site?action=GetDirUserINI"; + + public GetWebsiteConfigApi() { + super(ENDPOINT, "website config", "获取成功", "获取失败", false); + } + + public GetWebsiteConfigApi setId(Integer id) { + addParam("id", id); + return this; + } + + public GetWebsiteConfigApi setPath(String path) { + addParam("path", path); + return this; + } + + @Override + protected boolean validateParams() { + return hasPositiveIntegerParam("id") && hasNonBlankStringParam("path"); + } +} diff --git a/src/main/java/net/heimeng/sdk/btapi/api/website/GetWebsiteDetailApi.java b/src/main/java/net/heimeng/sdk/btapi/api/website/GetWebsiteDetailApi.java index 2eb8b0a..5379ce2 100644 --- a/src/main/java/net/heimeng/sdk/btapi/api/website/GetWebsiteDetailApi.java +++ b/src/main/java/net/heimeng/sdk/btapi/api/website/GetWebsiteDetailApi.java @@ -1,105 +1,25 @@ package net.heimeng.sdk.btapi.api.website; -import cn.hutool.json.JSONObject; -import cn.hutool.json.JSONUtil; -import net.heimeng.sdk.btapi.api.BaseBtApi; -import net.heimeng.sdk.btapi.exception.BtApiException; -import net.heimeng.sdk.btapi.model.BtResult; - -import java.util.HashMap; -import java.util.Map; - /** - * 获取网站详细信息API实现 - *

- * 用于获取宝塔面板中指定网站的详细信息。 - *

+ * 获取网站详情的 API。 * - * @author InwardFlow - * @since 2.0.0 + *

支持解析带 {@code status/msg} 的包装响应,也兼容直接返回网站详情对象的响应格式。 */ -public class GetWebsiteDetailApi extends BaseBtApi>> { - - /** - * API端点路径 - */ - private static final String ENDPOINT = "site?action=GetSiteStatus"; - - /** - * 构造函数,创建一个新的GetWebsiteDetailApi实例 - */ - public GetWebsiteDetailApi() { - super(ENDPOINT, HttpMethod.POST); - } - - /** - * 设置网站ID - * - * @param id 网站ID - * @return 当前API实例,支持链式调用 - */ - public GetWebsiteDetailApi setId(Integer id) { - addParam("id", id); - return this; - } - - /** - * 验证请求参数是否有效 - * - * @return 如果请求参数有效则返回true,否则返回false - */ - @Override - protected boolean validateParams() { - return params.containsKey("id"); - } - - /** - * 解析API响应字符串为BtResult>对象 - * - * @param response API响应字符串 - * @return BtResult>对象,其中data为网站详细信息 - * @throws BtApiException 当解析失败时抛出 - */ - @Override - public BtResult> parseResponse(String response) { - if (response == null || response.isEmpty()) { - throw new BtApiException("Empty response received"); - } - - try { - if (!JSONUtil.isTypeJSON(response)) { - throw new BtApiException("Invalid JSON response: " + response); - } - - JSONObject json = JSONUtil.parseObj(response); - BtResult> result = new BtResult<>(); - - // 检查响应状态 - boolean status = json.getBool("status", false); - if (!status) { - result.setStatus(false); - result.setMsg(json.getStr("msg", "获取失败")); - result.setData(new HashMap<>()); - return result; - } - - // 创建网站信息Map - Map websiteInfo = new HashMap<>(); - - // 将响应中的所有信息添加到Map中 - for (String key : json.keySet()) { - if (!key.equals("status") && !key.equals("msg")) { - websiteInfo.put(key, json.get(key)); - } - } - - result.setStatus(true); - result.setMsg("获取成功"); - result.setData(websiteInfo); - - return result; - } catch (Exception e) { - throw new BtApiException("Failed to parse website detail response: " + e.getMessage(), e); - } - } -} \ No newline at end of file +public class GetWebsiteDetailApi extends AbstractWebsiteMapQueryApi { + + private static final String ENDPOINT = "site?action=GetSiteStatus"; + + public GetWebsiteDetailApi() { + super(ENDPOINT, "website detail", "Success", "Failed to fetch website detail", true); + } + + public GetWebsiteDetailApi setId(Integer id) { + addParam("id", id); + return this; + } + + @Override + protected boolean validateParams() { + return hasPositiveIntegerParam("id"); + } +} diff --git a/src/main/java/net/heimeng/sdk/btapi/api/website/GetWebsiteDomainsApi.java b/src/main/java/net/heimeng/sdk/btapi/api/website/GetWebsiteDomainsApi.java index 749acc3..447c85c 100644 --- a/src/main/java/net/heimeng/sdk/btapi/api/website/GetWebsiteDomainsApi.java +++ b/src/main/java/net/heimeng/sdk/btapi/api/website/GetWebsiteDomainsApi.java @@ -1,117 +1,26 @@ package net.heimeng.sdk.btapi.api.website; -import cn.hutool.json.JSONArray; -import cn.hutool.json.JSONObject; -import cn.hutool.json.JSONUtil; -import net.heimeng.sdk.btapi.api.BaseBtApi; -import net.heimeng.sdk.btapi.exception.BtApiException; -import net.heimeng.sdk.btapi.model.BtResult; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - /** - * 获取网站域名列表API实现 - *

- * 用于获取宝塔面板中指定网站的域名列表。 - *

+ * 获取网站域名列表的 API。 * - * @author InwardFlow - * @since 2.0.0 + *

兼容面板直接返回数组和包装在 {@code data} 字段中的两种返回格式。 */ -public class GetWebsiteDomainsApi extends BaseBtApi>>> { - - /** - * API端点路径 - */ - private static final String ENDPOINT = "data?action=getData&table=domain"; - - /** - * 构造函数,创建一个新的GetWebsiteDomainsApi实例 - */ - public GetWebsiteDomainsApi() { - super(ENDPOINT, HttpMethod.POST); - // 设置默认参数 - addParam("list", true); - } - - /** - * 设置网站ID - * - * @param siteId 网站ID - * @return 当前API实例,支持链式调用 - */ - public GetWebsiteDomainsApi setSiteId(Integer siteId) { - addParam("search", siteId); - return this; - } - - /** - * 验证请求参数是否有效 - * - * @return 如果请求参数有效则返回true,否则返回false - */ - @Override - protected boolean validateParams() { - return params.containsKey("search") && params.containsKey("list"); - } - - /** - * 解析API响应字符串为BtResult>>对象 - * - * @param response API响应字符串 - * @return BtResult>>对象 - * @throws BtApiException 当解析失败时抛出 - */ - @Override - public BtResult>> parseResponse(String response) { - if (response == null || response.isEmpty()) { - throw new BtApiException("Empty response received"); - } - - try { - if (!JSONUtil.isTypeJSON(response)) { - throw new BtApiException("Invalid JSON response: " + response); - } - - BtResult>> result = new BtResult<>(); - result.setStatus(true); - result.setMsg("Success"); - - // 解析域名列表数据 - JSONArray dataArray; - Object jsonObj = JSONUtil.parse(response); - - if (jsonObj instanceof JSONArray) { - dataArray = (JSONArray) jsonObj; - } else if (jsonObj instanceof JSONObject) { - dataArray = ((JSONObject) jsonObj).getJSONArray("data"); - } else { - throw new BtApiException("Invalid response format"); - } - - List> domains = new ArrayList<>(); - - if (dataArray != null) { - for (int i = 0; i < dataArray.size(); i++) { - JSONObject domainJson = dataArray.getJSONObject(i); - if (domainJson != null) { - Map domainMap = new HashMap<>(); - // 将JSON对象转换为Map - for (String key : domainJson.keySet()) { - domainMap.put(key, domainJson.get(key)); - } - domains.add(domainMap); - } - } - } - - result.setData(domains); - return result; - } catch (Exception e) { - throw new BtApiException("Failed to parse website domains response: " + e.getMessage(), e); - } - } -} \ No newline at end of file +public class GetWebsiteDomainsApi extends AbstractWebsiteMapListQueryApi { + + private static final String ENDPOINT = "data?action=getData&table=domain"; + + public GetWebsiteDomainsApi() { + super(ENDPOINT, "website domains", "Success", "Failed to fetch website domains", "data", true); + addParam("list", true); + } + + public GetWebsiteDomainsApi setSiteId(Integer siteId) { + addParam("search", siteId); + return this; + } + + @Override + protected boolean validateParams() { + return hasPositiveIntegerParam("search") && Boolean.TRUE.equals(params.get("list")); + } +} diff --git a/src/main/java/net/heimeng/sdk/btapi/api/website/GetWebsiteLimitNetApi.java b/src/main/java/net/heimeng/sdk/btapi/api/website/GetWebsiteLimitNetApi.java index c6cb826..7d5a4c6 100644 --- a/src/main/java/net/heimeng/sdk/btapi/api/website/GetWebsiteLimitNetApi.java +++ b/src/main/java/net/heimeng/sdk/btapi/api/website/GetWebsiteLimitNetApi.java @@ -1,111 +1,25 @@ package net.heimeng.sdk.btapi.api.website; -import cn.hutool.json.JSONObject; -import cn.hutool.json.JSONUtil; -import net.heimeng.sdk.btapi.api.BaseBtApi; -import net.heimeng.sdk.btapi.exception.BtApiException; -import net.heimeng.sdk.btapi.model.BtResult; - -import java.util.HashMap; -import java.util.Map; - /** - * 获取流量限制相关配置API实现 - *

- * 用于获取宝塔面板中指定网站的流量限制配置(仅支持nginx)。 - *

+ * 获取网站流量限制配置的 API。 * - * @author InwardFlow - * @since 2.0.0 + *

成功时通常直接返回配置对象;失败场景下也可能返回带 {@code status/msg} 的包装响应。 */ -public class GetWebsiteLimitNetApi extends BaseBtApi>> { - - /** - * API端点路径 - */ - private static final String ENDPOINT = "site?action=GetLimitNet"; - - /** - * 构造函数,创建一个新的GetWebsiteLimitNetApi实例 - */ - public GetWebsiteLimitNetApi() { - super(ENDPOINT, HttpMethod.POST); - } - - /** - * 设置网站ID - * - * @param id 网站ID - * @return 当前API实例,支持链式调用 - */ - public GetWebsiteLimitNetApi setId(Integer id) { - addParam("id", id); - return this; - } - - /** - * 验证请求参数是否有效 - * - * @return 如果请求参数有效则返回true,否则返回false - */ - @Override - protected boolean validateParams() { - return params.containsKey("id"); - } - - /** - * 解析API响应字符串为BtResult>对象 - * - * @param response API响应字符串 - * @return BtResult>对象 - * @throws BtApiException 当解析失败时抛出 - */ - @Override - public BtResult> parseResponse(String response) { - if (response == null || response.isEmpty()) { - throw new BtApiException("Empty response received"); - } - - try { - if (!JSONUtil.isTypeJSON(response)) { - throw new BtApiException("Invalid JSON response: " + response); - } - - JSONObject json = JSONUtil.parseObj(response); - BtResult> result = new BtResult<>(); - - // 检查响应状态 - boolean status = json.getBool("status", false); - if (!status) { - result.setStatus(false); - result.setMsg(json.getStr("msg", "获取失败")); - result.setData(new HashMap<>()); - return result; - } - - // 创建配置信息Map - Map limitConfigMap = new HashMap<>(); - - // 解析各项配置 - limitConfigMap.put("perserver", json.getInt("perserver", 0)); - limitConfigMap.put("perip", json.getInt("perip", 0)); - limitConfigMap.put("limit_rate", json.getInt("limit_rate", 0)); - limitConfigMap.put("enabled", json.getBool("enabled", false)); - - // 添加其他可能的配置项 - for (String key : json.keySet()) { - if (!key.equals("status") && !key.equals("msg")) { - limitConfigMap.put(key, json.get(key)); - } - } - - result.setStatus(true); - result.setMsg("获取成功"); - result.setData(limitConfigMap); - - return result; - } catch (Exception e) { - throw new BtApiException("Failed to parse website limit net response: " + e.getMessage(), e); - } - } -} \ No newline at end of file +public class GetWebsiteLimitNetApi extends AbstractWebsiteMapQueryApi { + + private static final String ENDPOINT = "site?action=GetLimitNet"; + + public GetWebsiteLimitNetApi() { + super(ENDPOINT, "website limit net", "获取成功", "获取失败", false); + } + + public GetWebsiteLimitNetApi setId(Integer id) { + addParam("id", id); + return this; + } + + @Override + protected boolean validateParams() { + return hasPositiveIntegerParam("id"); + } +} diff --git a/src/main/java/net/heimeng/sdk/btapi/api/website/GetWebsiteListApi.java b/src/main/java/net/heimeng/sdk/btapi/api/website/GetWebsiteListApi.java index 29ab9e5..7d2dd45 100644 --- a/src/main/java/net/heimeng/sdk/btapi/api/website/GetWebsiteListApi.java +++ b/src/main/java/net/heimeng/sdk/btapi/api/website/GetWebsiteListApi.java @@ -1,181 +1,70 @@ package net.heimeng.sdk.btapi.api.website; -import cn.hutool.json.JSONArray; -import cn.hutool.json.JSONObject; -import cn.hutool.json.JSONUtil; -import net.heimeng.sdk.btapi.api.BaseBtApi; -import net.heimeng.sdk.btapi.exception.BtApiException; -import net.heimeng.sdk.btapi.model.BtResult; - -import java.util.ArrayList; -import java.util.List; -import java.util.Map; +import java.util.Set; /** - * 获取网站列表API实现 - *

- * 用于获取宝塔面板中的网站列表。 - *

+ * 获取网站原始列表的 API。 * - * @author InwardFlow - * @since 2.0.0 + *

用于按分页和筛选条件查询网站列表,并以原始 Map 结构返回,适合在 SDK 尚未完全模型化的场景下直接消费面板字段。 */ -public class GetWebsiteListApi extends BaseBtApi>>> { - - /** - * API端点路径 - */ - private static final String ENDPOINT = "data?action=getData&table=sites"; - - /** - * 构造函数,创建一个新的GetWebsiteListApi实例 - */ - public GetWebsiteListApi() { - super(ENDPOINT, HttpMethod.POST); - } - - /** - * 设置页码 - * - * @param page 页码 - * @return 当前API实例,支持链式调用 - */ - public GetWebsiteListApi setPage(Integer page) { - addParam("p", page); - return this; - } - - /** - * 设置每页数量(必传参数) - * - * @param limit 取回的数据行数 - * @return 当前API实例,支持链式调用 - */ - public GetWebsiteListApi setLimit(Integer limit) { - addParam("limit", limit); - return this; - } - - /** - * 设置分类标识 - * - * @param type 分类标识,-1:分部分类 0:默认分类 - * @return 当前API实例,支持链式调用 - */ - public GetWebsiteListApi setType(Integer type) { - addParam("type", type); - return this; - } - - /** - * 设置排序规则 - * - * @param order 排序规则,如"iddesc"(id降序)、"namedesc"(名称降序)等 - * @return 当前API实例,支持链式调用 - */ - public GetWebsiteListApi setOrder(String order) { - addParam("order", order); - return this; - } - - /** - * 设置分页JS回调 - * - * @param tojs 分页JS回调,若不传则构造URI分页连接 - * @return 当前API实例,支持链式调用 - */ - public GetWebsiteListApi setTojs(String tojs) { - addParam("tojs", tojs); - return this; - } - - /** - * 设置搜索关键词 - * - * @param search 搜索内容 - * @return 当前API实例,支持链式调用 - */ - public GetWebsiteListApi setSearch(String search) { - addParam("search", search); - return this; - } - - /** - * 验证请求参数是否有效 - * - * @return 如果请求参数有效则返回true,否则返回false - */ - @Override - protected boolean validateParams() { - // limit是必传参数 - if (!params.containsKey("limit") || !(params.get("limit") instanceof Integer) || (Integer)params.get("limit") < 1) { - return false; - } - - // 页码参数如果提供了,必须是有效的 - if (params.containsKey("p") && !(params.get("p") instanceof Integer) || (Integer)params.get("p") < 1) { - return false; - } - - // 分类标识如果提供了,必须是-1或0 - if (params.containsKey("type") && !(params.get("type") instanceof Integer) || - ((Integer)params.get("type") != -1 && (Integer)params.get("type") != 0)) { - return false; - } - - // 其他可选参数的类型验证 - if (params.containsKey("order") && !(params.get("order") instanceof String)) { - return false; - } - if (params.containsKey("tojs") && !(params.get("tojs") instanceof String)) { - return false; - } - if (params.containsKey("search") && !(params.get("search") instanceof String)) { - return false; - } - - return true; - } - - /** - * 解析API响应字符串为BtResult>>对象 - * - * @param response API响应字符串 - * @return BtResult>>对象,其中data为网站列表 - * @throws BtApiException 当解析失败时抛出 - */ - @Override - public BtResult>> parseResponse(String response) { - if (response == null || response.isEmpty()) { - throw new BtApiException("Empty response received"); - } - - try { - if (!JSONUtil.isTypeJSON(response)) { - throw new BtApiException("Invalid JSON response: " + response); - } - - JSONObject json = JSONUtil.parseObj(response); - BtResult>> result = new BtResult<>(); - - // 直接获取网站列表数据(新响应格式没有status字段) - JSONArray sitesArray = json.getJSONArray("data"); - List> sitesList = new ArrayList<>(); - - if (sitesArray != null && !sitesArray.isEmpty()) { - for (int i = 0; i < sitesArray.size(); i++) { - sitesList.add(sitesArray.getJSONObject(i).toBean(Map.class)); - } - } - - // 设置结果信息 - result.setStatus(true); - result.setMsg("获取成功"); - result.setData(sitesList); - - return result; - } catch (Exception e) { - throw new BtApiException("Failed to parse website list response: " + e.getMessage(), e); - } +public class GetWebsiteListApi extends AbstractWebsiteMapListQueryApi { + + private static final String ENDPOINT = "data?action=getData&table=sites"; + private static final Set ALLOWED_TYPES = Set.of(-1, 0); + + public GetWebsiteListApi() { + super(ENDPOINT, "website list", "Success", "Failed to fetch website list", "data", true); + } + + public GetWebsiteListApi setPage(Integer page) { + addParam("p", page); + return this; + } + + public GetWebsiteListApi setLimit(Integer limit) { + addParam("limit", limit); + return this; + } + + public GetWebsiteListApi setType(Integer type) { + addParam("type", type); + return this; + } + + public GetWebsiteListApi setOrder(String order) { + addParam("order", order); + return this; + } + + public GetWebsiteListApi setTojs(String tojs) { + addParam("tojs", tojs); + return this; + } + + public GetWebsiteListApi setSearch(String search) { + addParam("search", search); + return this; + } + + @Override + protected boolean validateParams() { + return hasRequiredPositiveIntegerParam("limit") + && hasOptionalPositiveIntegerParam("p") + && hasOptionalAllowedType("type") + && hasOptionalStringParam("order") + && hasOptionalStringParam("tojs") + && hasOptionalStringParam("search"); + } + + private boolean hasRequiredPositiveIntegerParam(String paramName) { + return hasPositiveIntegerParam(paramName); + } + + private boolean hasOptionalAllowedType(String paramName) { + if (!params.containsKey(paramName)) { + return true; } -} \ No newline at end of file + Object value = params.get(paramName); + return value instanceof Integer integerValue && ALLOWED_TYPES.contains(integerValue); + } +} diff --git a/src/main/java/net/heimeng/sdk/btapi/api/website/GetWebsiteNginxConfigApi.java b/src/main/java/net/heimeng/sdk/btapi/api/website/GetWebsiteNginxConfigApi.java index f54e2e3..a151c69 100644 --- a/src/main/java/net/heimeng/sdk/btapi/api/website/GetWebsiteNginxConfigApi.java +++ b/src/main/java/net/heimeng/sdk/btapi/api/website/GetWebsiteNginxConfigApi.java @@ -1,109 +1,30 @@ package net.heimeng.sdk.btapi.api.website; -import cn.hutool.json.JSONObject; -import cn.hutool.json.JSONUtil; -import net.heimeng.sdk.btapi.api.BaseBtApi; -import net.heimeng.sdk.btapi.exception.BtApiException; -import net.heimeng.sdk.btapi.model.BtResult; - /** - * 获取网站Nginx配置API实现 - *

- * 用于获取宝塔面板中网站的Nginx配置文件内容。 - *

+ * 获取网站 Nginx 配置内容的 API。 * - * @author InwardFlow - * @since 2.0.0 + *

兼容面板直接返回配置文本以及包装在 {@code status/msg/data} 中的响应。 */ -public class GetWebsiteNginxConfigApi extends BaseBtApi> { - - /** - * API端点路径 - */ - private static final String ENDPOINT = "site?action=getConf"; - - /** - * 构造函数,创建一个新的GetWebsiteNginxConfigApi实例 - */ - public GetWebsiteNginxConfigApi() { - super(ENDPOINT, HttpMethod.POST); - } - - /** - * 设置网站ID - * - * @param id 网站ID - * @return 当前API实例,支持链式调用 - */ - public GetWebsiteNginxConfigApi setId(Integer id) { - addParam("id", id); - return this; - } - - /** - * 设置网站域名 - * - * @param domain 网站域名 - * @return 当前API实例,支持链式调用 - */ - public GetWebsiteNginxConfigApi setDomain(String domain) { - addParam("domain", domain); - return this; - } - - /** - * 验证请求参数是否有效 - * - * @return 如果请求参数有效则返回true,否则返回false - */ - @Override - protected boolean validateParams() { - return params.containsKey("id") && - params.containsKey("domain") && - params.get("domain") != null && - !((String) params.get("domain")).isEmpty(); - } - - /** - * 解析API响应字符串为BtResult对象 - * - * @param response API响应字符串 - * @return BtResult对象,其中data为Nginx配置文件内容 - * @throws BtApiException 当解析失败时抛出 - */ - @Override - public BtResult parseResponse(String response) { - if (response == null || response.isEmpty()) { - throw new BtApiException("Empty response received"); - } - - try { - if (!JSONUtil.isTypeJSON(response)) { - throw new BtApiException("Invalid JSON response: " + response); - } - - JSONObject json = JSONUtil.parseObj(response); - BtResult result = new BtResult<>(); - - // 检查响应状态 - boolean status = json.getBool("status", false); - if (!status) { - result.setStatus(false); - result.setMsg(json.getStr("msg", "获取失败")); - result.setData(""); - return result; - } - - // 获取Nginx配置文件内容 - String configContent = json.getStr("data", ""); - - result.setStatus(true); - result.setMsg("获取成功"); - result.setData(configContent); - - return result; - } catch (Exception e) { - throw new BtApiException("Failed to parse website Nginx config response: " + e.getMessage(), e); - } - } -} \ No newline at end of file +public class GetWebsiteNginxConfigApi extends AbstractWebsiteTextQueryApi { + + private static final String ENDPOINT = "site?action=getConf"; + + public GetWebsiteNginxConfigApi() { + super(ENDPOINT, "获取成功", "获取失败"); + } + + public GetWebsiteNginxConfigApi setId(Integer id) { + addParam("id", id); + return this; + } + + public GetWebsiteNginxConfigApi setDomain(String domain) { + addParam("domain", domain); + return this; + } + + @Override + protected boolean validateParams() { + return hasPositiveIntegerParam("id") && hasNonBlankStringParam("domain"); + } +} diff --git a/src/main/java/net/heimeng/sdk/btapi/api/website/GetWebsitePhpExtensionsApi.java b/src/main/java/net/heimeng/sdk/btapi/api/website/GetWebsitePhpExtensionsApi.java index 775ea59..f5bd408 100644 --- a/src/main/java/net/heimeng/sdk/btapi/api/website/GetWebsitePhpExtensionsApi.java +++ b/src/main/java/net/heimeng/sdk/btapi/api/website/GetWebsitePhpExtensionsApi.java @@ -1,107 +1,41 @@ package net.heimeng.sdk.btapi.api.website; -import cn.hutool.json.JSONArray; -import cn.hutool.json.JSONObject; -import cn.hutool.json.JSONUtil; -import net.heimeng.sdk.btapi.api.BaseBtApi; -import net.heimeng.sdk.btapi.exception.BtApiException; -import net.heimeng.sdk.btapi.model.BtResult; - -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - /** * 获取网站PHP扩展列表API实现 - *

- * 用于获取宝塔面板中指定网站PHP版本的扩展列表。 - *

+ * + *

用于获取宝塔面板中指定网站PHP版本的扩展列表。 * * @author InwardFlow * @since 2.0.0 */ -public class GetWebsitePhpExtensionsApi extends BaseBtApi>>> { - - /** - * API端点路径 - */ - private static final String ENDPOINT = "site?action=GetPHPModules"; - - /** - * 构造函数,创建一个新的GetWebsitePhpExtensionsApi实例 - */ - public GetWebsitePhpExtensionsApi() { - super(ENDPOINT, HttpMethod.POST); - } - - /** - * 设置网站ID - * - * @param id 网站ID - * @return 当前API实例,支持链式调用 - */ - public GetWebsitePhpExtensionsApi setId(Integer id) { - addParam("id", id); - return this; - } - - /** - * 验证请求参数是否有效 - * - * @return 如果请求参数有效则返回true,否则返回false - */ - @Override - protected boolean validateParams() { - return params.containsKey("id"); - } - - /** - * 解析API响应字符串为BtResult>>对象 - * - * @param response API响应字符串 - * @return BtResult>>对象,其中data为PHP扩展列表 - * @throws BtApiException 当解析失败时抛出 - */ - @Override - public BtResult>> parseResponse(String response) { - if (response == null || response.isEmpty()) { - throw new BtApiException("Empty response received"); - } - - try { - if (!JSONUtil.isTypeJSON(response)) { - throw new BtApiException("Invalid JSON response: " + response); - } - - JSONObject json = JSONUtil.parseObj(response); - BtResult>> result = new BtResult<>(); - - // 检查响应状态 - boolean status = json.getBool("status", false); - if (!status) { - result.setStatus(false); - result.setMsg(json.getStr("msg", "获取失败")); - result.setData(new ArrayList<>()); - return result; - } - - // 获取PHP扩展列表 - JSONArray extensionsArray = json.getJSONArray("data"); - List> extensionsList = new ArrayList<>(); - - if (extensionsArray != null && !extensionsArray.isEmpty()) { - for (int i = 0; i < extensionsArray.size(); i++) { - extensionsList.add(extensionsArray.getJSONObject(i).toBean(Map.class)); - } - } - - result.setStatus(true); - result.setMsg("获取成功"); - result.setData(extensionsList); - - return result; - } catch (Exception e) { - throw new BtApiException("Failed to parse website PHP extensions response: " + e.getMessage(), e); - } - } -} \ No newline at end of file +public class GetWebsitePhpExtensionsApi extends AbstractWebsiteMapListQueryApi { + + /** API端点路径 */ + private static final String ENDPOINT = "site?action=GetPHPModules"; + + /** 构造函数,创建一个新的GetWebsitePhpExtensionsApi实例 */ + public GetWebsitePhpExtensionsApi() { + super(ENDPOINT, "website PHP extensions", "获取成功", "获取失败", "data", false); + } + + /** + * 设置网站ID + * + * @param id 网站ID + * @return 当前API实例,支持链式调用 + */ + public GetWebsitePhpExtensionsApi setId(Integer id) { + addParam("id", id); + return this; + } + + /** + * 验证请求参数是否有效 + * + * @return 如果请求参数有效则返回true,否则返回false + */ + @Override + protected boolean validateParams() { + return hasPositiveIntegerParam("id"); + } +} diff --git a/src/main/java/net/heimeng/sdk/btapi/api/website/GetWebsitePhpVersionApi.java b/src/main/java/net/heimeng/sdk/btapi/api/website/GetWebsitePhpVersionApi.java index 0a65caf..217a9a9 100644 --- a/src/main/java/net/heimeng/sdk/btapi/api/website/GetWebsitePhpVersionApi.java +++ b/src/main/java/net/heimeng/sdk/btapi/api/website/GetWebsitePhpVersionApi.java @@ -1,95 +1,25 @@ package net.heimeng.sdk.btapi.api.website; -import cn.hutool.json.JSONObject; -import cn.hutool.json.JSONUtil; -import net.heimeng.sdk.btapi.api.BaseBtApi; -import net.heimeng.sdk.btapi.exception.BtApiException; -import net.heimeng.sdk.btapi.model.BtResult; - /** - * 获取网站PHP版本API实现 - *

- * 用于获取宝塔面板中网站的PHP版本配置。 - *

+ * 获取网站 PHP 版本的 API。 * - * @author InwardFlow - * @since 2.0.0 + *

兼容面板直接返回 PHP 版本文本以及包装在 {@code status/msg/data} 中的响应。 */ -public class GetWebsitePhpVersionApi extends BaseBtApi> { - - /** - * API端点路径 - */ - private static final String ENDPOINT = "site?action=getPhpVersion"; - - /** - * 构造函数,创建一个新的GetWebsitePhpVersionApi实例 - */ - public GetWebsitePhpVersionApi() { - super(ENDPOINT, HttpMethod.POST); - } - - /** - * 设置网站ID - * - * @param id 网站ID - * @return 当前API实例,支持链式调用 - */ - public GetWebsitePhpVersionApi setId(Integer id) { - addParam("id", id); - return this; - } - - /** - * 验证请求参数是否有效 - * - * @return 如果请求参数有效则返回true,否则返回false - */ - @Override - protected boolean validateParams() { - return params.containsKey("id"); - } - - /** - * 解析API响应字符串为BtResult对象 - * - * @param response API响应字符串 - * @return BtResult对象,其中data为PHP版本号 - * @throws BtApiException 当解析失败时抛出 - */ - @Override - public BtResult parseResponse(String response) { - if (response == null || response.isEmpty()) { - throw new BtApiException("Empty response received"); - } - - try { - if (!JSONUtil.isTypeJSON(response)) { - throw new BtApiException("Invalid JSON response: " + response); - } - - JSONObject json = JSONUtil.parseObj(response); - BtResult result = new BtResult<>(); - - // 检查响应状态 - boolean status = json.getBool("status", false); - if (!status) { - result.setStatus(false); - result.setMsg(json.getStr("msg", "获取失败")); - result.setData(""); - return result; - } - - // 获取PHP版本号 - String phpVersion = json.getStr("data", ""); - - result.setStatus(true); - result.setMsg("获取成功"); - result.setData(phpVersion); - - return result; - } catch (Exception e) { - throw new BtApiException("Failed to parse website PHP version response: " + e.getMessage(), e); - } - } -} \ No newline at end of file +public class GetWebsitePhpVersionApi extends AbstractWebsiteTextQueryApi { + + private static final String ENDPOINT = "site?action=getPhpVersion"; + + public GetWebsitePhpVersionApi() { + super(ENDPOINT, "获取成功", "获取失败"); + } + + public GetWebsitePhpVersionApi setId(Integer id) { + addParam("id", id); + return this; + } + + @Override + protected boolean validateParams() { + return hasPositiveIntegerParam("id"); + } +} diff --git a/src/main/java/net/heimeng/sdk/btapi/api/website/GetWebsiteRewriteRulesApi.java b/src/main/java/net/heimeng/sdk/btapi/api/website/GetWebsiteRewriteRulesApi.java index 64cd217..b06f2e8 100644 --- a/src/main/java/net/heimeng/sdk/btapi/api/website/GetWebsiteRewriteRulesApi.java +++ b/src/main/java/net/heimeng/sdk/btapi/api/website/GetWebsiteRewriteRulesApi.java @@ -1,95 +1,25 @@ package net.heimeng.sdk.btapi.api.website; -import cn.hutool.json.JSONObject; -import cn.hutool.json.JSONUtil; -import net.heimeng.sdk.btapi.api.BaseBtApi; -import net.heimeng.sdk.btapi.exception.BtApiException; -import net.heimeng.sdk.btapi.model.BtResult; - /** - * 获取网站伪静态规则API实现 - *

- * 用于获取宝塔面板中网站的伪静态规则配置。 - *

+ * 获取网站伪静态规则的 API。 * - * @author InwardFlow - * @since 2.0.0 + *

兼容面板直接返回规则文本以及包装在 {@code status/msg/data} 中的响应。 */ -public class GetWebsiteRewriteRulesApi extends BaseBtApi> { - - /** - * API端点路径 - */ - private static final String ENDPOINT = "site?action=getRewrite"; - - /** - * 构造函数,创建一个新的GetWebsiteRewriteRulesApi实例 - */ - public GetWebsiteRewriteRulesApi() { - super(ENDPOINT, HttpMethod.POST); - } - - /** - * 设置网站ID - * - * @param id 网站ID - * @return 当前API实例,支持链式调用 - */ - public GetWebsiteRewriteRulesApi setId(Integer id) { - addParam("id", id); - return this; - } - - /** - * 验证请求参数是否有效 - * - * @return 如果请求参数有效则返回true,否则返回false - */ - @Override - protected boolean validateParams() { - return params.containsKey("id"); - } - - /** - * 解析API响应字符串为BtResult对象 - * - * @param response API响应字符串 - * @return BtResult对象,其中data为伪静态规则内容 - * @throws BtApiException 当解析失败时抛出 - */ - @Override - public BtResult parseResponse(String response) { - if (response == null || response.isEmpty()) { - throw new BtApiException("Empty response received"); - } - - try { - if (!JSONUtil.isTypeJSON(response)) { - throw new BtApiException("Invalid JSON response: " + response); - } - - JSONObject json = JSONUtil.parseObj(response); - BtResult result = new BtResult<>(); - - // 检查响应状态 - boolean status = json.getBool("status", false); - if (!status) { - result.setStatus(false); - result.setMsg(json.getStr("msg", "获取失败")); - result.setData(""); - return result; - } - - // 获取伪静态规则内容 - String rewriteRules = json.getStr("data", ""); - - result.setStatus(true); - result.setMsg("获取成功"); - result.setData(rewriteRules); - - return result; - } catch (Exception e) { - throw new BtApiException("Failed to parse website rewrite rules response: " + e.getMessage(), e); - } - } -} \ No newline at end of file +public class GetWebsiteRewriteRulesApi extends AbstractWebsiteTextQueryApi { + + private static final String ENDPOINT = "site?action=getRewrite"; + + public GetWebsiteRewriteRulesApi() { + super(ENDPOINT, "获取成功", "获取失败"); + } + + public GetWebsiteRewriteRulesApi setId(Integer id) { + addParam("id", id); + return this; + } + + @Override + protected boolean validateParams() { + return hasPositiveIntegerParam("id"); + } +} diff --git a/src/main/java/net/heimeng/sdk/btapi/api/website/GetWebsiteRootPathApi.java b/src/main/java/net/heimeng/sdk/btapi/api/website/GetWebsiteRootPathApi.java index 8c96c37..42b801e 100644 --- a/src/main/java/net/heimeng/sdk/btapi/api/website/GetWebsiteRootPathApi.java +++ b/src/main/java/net/heimeng/sdk/btapi/api/website/GetWebsiteRootPathApi.java @@ -1,95 +1,25 @@ package net.heimeng.sdk.btapi.api.website; -import cn.hutool.json.JSONObject; -import cn.hutool.json.JSONUtil; -import net.heimeng.sdk.btapi.api.BaseBtApi; -import net.heimeng.sdk.btapi.exception.BtApiException; -import net.heimeng.sdk.btapi.model.BtResult; - /** - * 获取指定网站根目录API实现 - *

- * 用于获取宝塔面板中指定网站的根目录路径。 - *

+ * 获取网站根目录的 API。 * - * @author InwardFlow - * @since 2.0.0 + *

兼容面板直接返回根目录文本以及包装在 {@code status/msg/data} 中的响应。 */ -public class GetWebsiteRootPathApi extends BaseBtApi> { - - /** - * API端点路径 - */ - private static final String ENDPOINT = "data?action=getKey&table=sites&key=path"; - - /** - * 构造函数,创建一个新的GetWebsiteRootPathApi实例 - */ - public GetWebsiteRootPathApi() { - super(ENDPOINT, HttpMethod.POST); - } - - /** - * 设置网站ID - * - * @param id 网站ID - * @return 当前API实例,支持链式调用 - */ - public GetWebsiteRootPathApi setId(Integer id) { - addParam("id", id); - return this; - } - - /** - * 验证请求参数是否有效 - * - * @return 如果请求参数有效则返回true,否则返回false - */ - @Override - protected boolean validateParams() { - return params.containsKey("id"); - } - - /** - * 解析API响应字符串为BtResult对象 - * - * @param response API响应字符串 - * @return BtResult对象 - * @throws BtApiException 当解析失败时抛出 - */ - @Override - public BtResult parseResponse(String response) { - if (response == null || response.isEmpty()) { - throw new BtApiException("Empty response received"); - } - - try { - if (!JSONUtil.isTypeJSON(response)) { - throw new BtApiException("Invalid JSON response: " + response); - } - - JSONObject json = JSONUtil.parseObj(response); - BtResult result = new BtResult<>(); - - // 检查响应状态 - boolean status = json.getBool("status", false); - if (!status) { - result.setStatus(false); - result.setMsg(json.getStr("msg", "获取失败")); - result.setData(""); - return result; - } - - // 获取根目录路径 - String path = json.getStr("data", ""); - - result.setStatus(true); - result.setMsg("获取成功"); - result.setData(path); - - return result; - } catch (Exception e) { - throw new BtApiException("Failed to parse website root path response: " + e.getMessage(), e); - } - } -} \ No newline at end of file +public class GetWebsiteRootPathApi extends AbstractWebsiteTextQueryApi { + + private static final String ENDPOINT = "data?action=getKey&table=sites&key=path"; + + public GetWebsiteRootPathApi() { + super(ENDPOINT, "获取成功", "获取失败"); + } + + public GetWebsiteRootPathApi setId(Integer id) { + addParam("id", id); + return this; + } + + @Override + protected boolean validateParams() { + return hasPositiveIntegerParam("id"); + } +} diff --git a/src/main/java/net/heimeng/sdk/btapi/api/website/GetWebsiteSslListApi.java b/src/main/java/net/heimeng/sdk/btapi/api/website/GetWebsiteSslListApi.java index 1194771..13ee350 100644 --- a/src/main/java/net/heimeng/sdk/btapi/api/website/GetWebsiteSslListApi.java +++ b/src/main/java/net/heimeng/sdk/btapi/api/website/GetWebsiteSslListApi.java @@ -1,107 +1,41 @@ package net.heimeng.sdk.btapi.api.website; -import cn.hutool.json.JSONArray; -import cn.hutool.json.JSONObject; -import cn.hutool.json.JSONUtil; -import net.heimeng.sdk.btapi.api.BaseBtApi; -import net.heimeng.sdk.btapi.exception.BtApiException; -import net.heimeng.sdk.btapi.model.BtResult; - -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - /** * 获取SSL证书列表API实现 - *

- * 用于获取宝塔面板中指定网站的SSL证书列表。 - *

+ * + *

用于获取宝塔面板中指定网站的SSL证书列表。 * * @author InwardFlow * @since 2.0.0 */ -public class GetWebsiteSslListApi extends BaseBtApi>>> { - - /** - * API端点路径 - */ - private static final String ENDPOINT = "site?action=GetSSLCertList"; - - /** - * 构造函数,创建一个新的GetWebsiteSslListApi实例 - */ - public GetWebsiteSslListApi() { - super(ENDPOINT, HttpMethod.POST); - } - - /** - * 设置网站ID - * - * @param id 网站ID - * @return 当前API实例,支持链式调用 - */ - public GetWebsiteSslListApi setId(Integer id) { - addParam("id", id); - return this; - } - - /** - * 验证请求参数是否有效 - * - * @return 如果请求参数有效则返回true,否则返回false - */ - @Override - protected boolean validateParams() { - return params.containsKey("id"); - } - - /** - * 解析API响应字符串为BtResult>>对象 - * - * @param response API响应字符串 - * @return BtResult>>对象 - * @throws BtApiException 当解析失败时抛出 - */ - @Override - public BtResult>> parseResponse(String response) { - if (response == null || response.isEmpty()) { - throw new BtApiException("Empty response received"); - } - - try { - if (!JSONUtil.isTypeJSON(response)) { - throw new BtApiException("Invalid JSON response: " + response); - } - - JSONObject json = JSONUtil.parseObj(response); - BtResult>> result = new BtResult<>(); - - // 检查响应状态 - boolean status = json.getBool("status", false); - if (!status) { - result.setStatus(false); - result.setMsg(json.getStr("msg", "获取失败")); - result.setData(new ArrayList<>()); - return result; - } - - // 获取证书列表 - JSONArray certsArray = json.getJSONArray("certs"); - List> certsList = new ArrayList<>(); - - if (certsArray != null && !certsArray.isEmpty()) { - for (int i = 0; i < certsArray.size(); i++) { - certsList.add(certsArray.getJSONObject(i).toBean(Map.class)); - } - } - - result.setStatus(true); - result.setMsg("获取成功"); - result.setData(certsList); - - return result; - } catch (Exception e) { - throw new BtApiException("Failed to parse website SSL list response: " + e.getMessage(), e); - } - } -} \ No newline at end of file +public class GetWebsiteSslListApi extends AbstractWebsiteMapListQueryApi { + + /** API端点路径 */ + private static final String ENDPOINT = "site?action=GetSSLCertList"; + + /** 构造函数,创建一个新的GetWebsiteSslListApi实例 */ + public GetWebsiteSslListApi() { + super(ENDPOINT, "website SSL list", "获取成功", "获取失败", "certs", false); + } + + /** + * 设置网站ID + * + * @param id 网站ID + * @return 当前API实例,支持链式调用 + */ + public GetWebsiteSslListApi setId(Integer id) { + addParam("id", id); + return this; + } + + /** + * 验证请求参数是否有效 + * + * @return 如果请求参数有效则返回true,否则返回false + */ + @Override + protected boolean validateParams() { + return hasPositiveIntegerParam("id"); + } +} diff --git a/src/main/java/net/heimeng/sdk/btapi/api/website/GetWebsiteTypesApi.java b/src/main/java/net/heimeng/sdk/btapi/api/website/GetWebsiteTypesApi.java index 6eaebe9..10a497b 100644 --- a/src/main/java/net/heimeng/sdk/btapi/api/website/GetWebsiteTypesApi.java +++ b/src/main/java/net/heimeng/sdk/btapi/api/website/GetWebsiteTypesApi.java @@ -1,93 +1,66 @@ package net.heimeng.sdk.btapi.api.website; +import java.util.ArrayList; +import java.util.List; + +import cn.hutool.json.JSON; import cn.hutool.json.JSONArray; -import cn.hutool.json.JSONException; import cn.hutool.json.JSONObject; -import cn.hutool.json.JSONUtil; + import net.heimeng.sdk.btapi.api.BaseBtApi; import net.heimeng.sdk.btapi.exception.BtApiException; import net.heimeng.sdk.btapi.model.BtResult; import net.heimeng.sdk.btapi.model.website.WebsiteType; -import java.util.ArrayList; -import java.util.List; - /** - * 获取网站分类API实现 - *

- * 用于获取宝塔面板中的网站分类列表。 - *

+ * 获取网站分类列表的 API。 * - * @author InwardFlow - * @since 2.0.0 + *

兼容面板直接返回数组以及带 {@code status/msg/data} 包装的响应。 */ public class GetWebsiteTypesApi extends BaseBtApi>> { - - /** - * API端点路径 - */ - private static final String ENDPOINT = "site?action=get_site_types"; - - /** - * 构造函数,创建一个新的GetWebsiteTypesApi实例 - */ - public GetWebsiteTypesApi() { - super(ENDPOINT, HttpMethod.POST); + + private static final String ENDPOINT = "site?action=get_site_types"; + + public GetWebsiteTypesApi() { + super(ENDPOINT, HttpMethod.POST); + } + + @Override + public BtResult> parseResponse(String response) { + JSON json = WebsiteApiResponseSupport.parseJsonResponse(response, "website types"); + if (json instanceof JSONArray jsonArray) { + return WebsiteApiResponseSupport.successListResult(parseTypes(jsonArray), "Success"); + } + if (!(json instanceof JSONObject jsonObject)) { + throw new BtApiException("Website types response must be a JSON object or array"); + } + if (jsonObject.containsKey("status") && !jsonObject.getBool("status", false)) { + return WebsiteApiResponseSupport.failureListResult( + jsonObject.getStr("msg", "Failed to fetch website types")); + } + + JSONArray dataArray = jsonObject.getJSONArray("data"); + if (dataArray == null) { + throw new BtApiException("Website types response is missing required data array"); } - - /** - * 解析API响应字符串为BtResult>对象 - * - * @param response API响应字符串 - * @return BtResult>对象 - * @throws BtApiException 当解析失败时抛出 - */ - @Override - public BtResult> parseResponse(String response) { - if (response == null || response.isEmpty()) { - throw new BtApiException("Empty response received"); - } - try { - if (!JSONUtil.isTypeJSON(response)) { - throw new BtApiException("Invalid JSON response: " + response); - } + return WebsiteApiResponseSupport.successListResult( + parseTypes(dataArray), jsonObject.getStr("msg", "Success")); + } - Object jsonObj = JSONUtil.parse(response); - BtResult> result = new BtResult<>(); - List websiteTypes = new ArrayList<>(); - - // 响应可能是直接的数组格式 - if (jsonObj instanceof JSONArray) { - JSONArray jsonArray = (JSONArray) jsonObj; - result.setStatus(true); - result.setMsg("Success"); - - // 解析每个网站分类信息 - for (int i = 0; i < jsonArray.size(); i++) { - JSONObject typeJson = jsonArray.getJSONObject(i); - if (typeJson != null) { - WebsiteType websiteType = new WebsiteType(); - websiteType.setId(typeJson.getInt("id", 0)); - websiteType.setName(typeJson.getStr("name", "")); - - websiteTypes.add(websiteType); - } - } - } else if (jsonObj instanceof JSONObject) { - // 响应可能是带有status字段的错误格式 - JSONObject json = (JSONObject) jsonObj; - result.setStatus(json.getBool("status", false)); - result.setMsg(json.getStr("msg", "")); - } - - result.setData(websiteTypes); - return result; + private List parseTypes(JSONArray jsonArray) { + List websiteTypes = new ArrayList<>(); + for (int i = 0; i < jsonArray.size(); i++) { + JSONObject typeJson = jsonArray.getJSONObject(i); + if (typeJson == null) { + continue; + } - } catch (JSONException e) { - throw new BtApiException("Invalid JSON response: " + response); - } catch (Exception e) { - throw new BtApiException("Failed to parse website types response: " + e.getMessage(), e); - } + WebsiteType websiteType = new WebsiteType(); + websiteType.setId(typeJson.getInt("id", 0)); + websiteType.setName(typeJson.getStr("name", "")); + websiteTypes.add(websiteType); } -} \ No newline at end of file + return List.copyOf(websiteTypes); + } +} diff --git a/src/main/java/net/heimeng/sdk/btapi/api/website/GetWebsitesApi.java b/src/main/java/net/heimeng/sdk/btapi/api/website/GetWebsitesApi.java index c07ca2c..50658f7 100644 --- a/src/main/java/net/heimeng/sdk/btapi/api/website/GetWebsitesApi.java +++ b/src/main/java/net/heimeng/sdk/btapi/api/website/GetWebsitesApi.java @@ -1,161 +1,180 @@ package net.heimeng.sdk.btapi.api.website; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Locale; + import cn.hutool.json.JSONArray; +import cn.hutool.json.JSONException; import cn.hutool.json.JSONObject; import cn.hutool.json.JSONUtil; + import net.heimeng.sdk.btapi.api.BaseBtApi; import net.heimeng.sdk.btapi.exception.BtApiException; import net.heimeng.sdk.btapi.model.BtResult; import net.heimeng.sdk.btapi.model.website.WebsiteInfo; -import java.text.ParseException; -import java.text.SimpleDateFormat; -import java.util.ArrayList; -import java.util.Date; -import java.util.List; -import java.util.Locale; /** * 获取网站列表API实现 - *

- * 用于获取宝塔面板中所有网站的列表信息,支持分页查询。 - *

+ * + *

用于获取宝塔面板中所有网站的列表信息,支持分页查询。 * * @author InwardFlow * @since 2.0.0 */ public class GetWebsitesApi extends BaseBtApi>> { - - /** - * API端点路径 - */ - private static final String ENDPOINT = "data?action=getData&table=sites"; - - /** - * 日期格式解析器 - */ - private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault()); - - /** - * 构造函数,创建一个新的GetWebsitesApi实例 - * - * @param page 页码,从1开始 - * @param limit 每页记录数 - */ - public GetWebsitesApi(int page, int limit) { - super(ENDPOINT, HttpMethod.POST); - addParam("p", page); - addParam("limit", limit); + + /** API端点路径 */ + private static final String ENDPOINT = "data?action=getData&table=sites"; + + /** 日期格式解析器 */ + private static final SimpleDateFormat DATE_FORMAT = + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault()); + + /** + * 构造函数,创建一个新的GetWebsitesApi实例 + * + * @param page 页码,从1开始 + * @param limit 每页记录数 + */ + public GetWebsitesApi(int page, int limit) { + super(ENDPOINT, HttpMethod.POST); + addParam("p", page); + addParam("limit", limit); + } + + /** 构造函数,创建一个新的GetWebsitesApi实例,使用默认分页参数 */ + public GetWebsitesApi() { + this(1, 10); + } + + /** + * 设置页码 + * + * @param page 页码,从1开始 + * @return 当前API实例,支持链式调用 + */ + public GetWebsitesApi setPage(int page) { + addParam("p", page); + return this; + } + + /** + * 设置每页记录数 + * + * @param limit 每页记录数 + * @return 当前API实例,支持链式调用 + */ + public GetWebsitesApi setLimit(int limit) { + addParam("limit", limit); + return this; + } + + /** + * 解析API响应字符串为BtResult>对象 + * + * @param response API响应字符串 + * @return BtResult>对象 + * @throws BtApiException 当解析失败时抛出 + */ + @Override + public BtResult> parseResponse(String response) { + if (response == null || response.isEmpty()) { + throw new BtApiException("Empty response received"); + } + + try { + if (!JSONUtil.isTypeJSON(response)) { + throw new BtApiException("Invalid JSON response: " + response); + } + + JSONObject json = JSONUtil.parseObj(response); + if (!json.containsKey("data")) { + throw new BtApiException("Missing required data field in websites response"); + } + + BtResult> result = new BtResult<>(); + result.setStatus(true); + result.setMsg(json.getStr("msg", "Success")); + + JSONArray dataArray = json.getJSONArray("data"); + if (dataArray == null) { + throw new BtApiException("data field is not a valid array"); + } + + List websites = new ArrayList<>(dataArray.size()); + for (int i = 0; i < dataArray.size(); i++) { + JSONObject websiteJson = dataArray.getJSONObject(i); + if (websiteJson == null) { + continue; + } + + WebsiteInfo website = new WebsiteInfo(); + website.setId(parseLong(websiteJson, "id")); + website.setName(websiteJson.get("name", String.class)); + website.setDomain(websiteJson.get("name", String.class)); + website.setPath(websiteJson.get("path", String.class)); + website.setType(websiteJson.get("project_type", String.class)); + website.setStatus(parseInteger(websiteJson, "status")); + website.setSsl(parseInteger(websiteJson, "ssl")); + website.setCreateTime(parseTimestamp(websiteJson.get("addtime", String.class))); + websites.add(website); + } + + result.setData(websites); + return result; + } catch (JSONException exception) { + throw new BtApiException("Invalid JSON response: " + response, exception); + } catch (Exception e) { + if (e instanceof BtApiException) { + throw (BtApiException) e; + } + throw new BtApiException("Failed to parse websites response: " + e.getMessage(), e); } - - /** - * 构造函数,创建一个新的GetWebsitesApi实例,使用默认分页参数 - */ - public GetWebsitesApi() { - this(1, 10); + } + + private Integer parseInteger(JSONObject jsonObject, String fieldName) { + Object value = jsonObject.get(fieldName); + if (value == null) { + return null; } - - /** - * 设置页码 - * - * @param page 页码,从1开始 - * @return 当前API实例,支持链式调用 - */ - public GetWebsitesApi setPage(int page) { - addParam("p", page); - return this; + if (value instanceof Number) { + return ((Number) value).intValue(); } - - /** - * 设置每页记录数 - * - * @param limit 每页记录数 - * @return 当前API实例,支持链式调用 - */ - public GetWebsitesApi setLimit(int limit) { - addParam("limit", limit); - return this; + String stringValue = String.valueOf(value); + if (stringValue.isBlank() || "null".equalsIgnoreCase(stringValue)) { + return null; } - - /** - * 解析API响应字符串为BtResult>对象 - * - * @param response API响应字符串 - * @return BtResult>对象 - * @throws BtApiException 当解析失败时抛出 - */ - @Override - public BtResult> parseResponse(String response) { - if (response == null || response.isEmpty()) { - throw new BtApiException("Empty response received"); - } - - try { - // 检查响应是否为有效的JSON - if (!JSONUtil.isTypeJSON(response)) { - throw new BtApiException("Invalid JSON response: " + response); - } - - // 解析JSON对象 - JSONObject json = JSONUtil.parseObj(response); - - // 创建结果对象 - BtResult> result = new BtResult<>(); - // 宝塔面板API可能没有status字段,根据是否有data字段判断成功 - result.setStatus(json.containsKey("data") && json.getJSONArray("data") != null); - result.setMsg(json.getStr("msg", "Success")); - - // 解析网站列表 - JSONArray dataArray = json.getJSONArray("data"); - if (dataArray != null) { - List websites = new ArrayList<>(dataArray.size()); - - for (int i = 0; i < dataArray.size(); i++) { - JSONObject websiteJson = dataArray.getJSONObject(i); - if (websiteJson != null) { - WebsiteInfo website = new WebsiteInfo(); - website.setId(websiteJson.getLong("id", 0L)); - website.setName(websiteJson.getStr("name", "")); - - // 处理域名字段 - 从name字段获取,因为domain字段是数量 - website.setDomain(websiteJson.getStr("name", "")); - - website.setPath(websiteJson.getStr("path", "")); - - // 处理网站类型 - 从project_type字段获取 - website.setType(websiteJson.getStr("project_type", "")); - - // 处理状态字段 - 字符串转整数 - String statusStr = websiteJson.getStr("status", "0"); - website.setStatus(Integer.parseInt(statusStr)); - - // 处理SSL状态 - -1表示未开启 - int sslValue = websiteJson.getInt("ssl", -1); - website.setSsl(sslValue == 1 ? 1 : 0); - - // 处理创建时间 - 日期字符串转时间戳 - String addtimeStr = websiteJson.getStr("addtime", ""); - if (!addtimeStr.isEmpty()) { - try { - Date date = DATE_FORMAT.parse(addtimeStr); - website.setCreateTime(date.getTime() / 1000); - } catch (ParseException e) { - // 如果解析失败,设置为0 - website.setCreateTime(0L); - } - } else { - website.setCreateTime(0L); - } - - websites.add(website); - } - } - - result.setData(websites); - } - - return result; - } catch (Exception e) { - throw new BtApiException("Failed to parse websites response: " + e.getMessage(), e); - } + return Integer.parseInt(stringValue); + } + + private Long parseLong(JSONObject jsonObject, String fieldName) { + Object value = jsonObject.get(fieldName); + if (value == null) { + return null; + } + if (value instanceof Number) { + return ((Number) value).longValue(); + } + String stringValue = String.valueOf(value); + if (stringValue.isBlank() || "null".equalsIgnoreCase(stringValue)) { + return null; + } + return Long.parseLong(stringValue); + } + + private Long parseTimestamp(String dateTime) { + if (dateTime == null || dateTime.isBlank()) { + return null; + } + try { + Date date = DATE_FORMAT.parse(dateTime); + return date == null ? null : date.getTime() / 1000; + } catch (ParseException exception) { + return null; } -} \ No newline at end of file + } +} diff --git a/src/main/java/net/heimeng/sdk/btapi/api/website/SetWebsiteLimitNetApi.java b/src/main/java/net/heimeng/sdk/btapi/api/website/SetWebsiteLimitNetApi.java index b6005e1..58e6249 100644 --- a/src/main/java/net/heimeng/sdk/btapi/api/website/SetWebsiteLimitNetApi.java +++ b/src/main/java/net/heimeng/sdk/btapi/api/website/SetWebsiteLimitNetApi.java @@ -1,128 +1,47 @@ package net.heimeng.sdk.btapi.api.website; -import cn.hutool.json.JSONObject; -import cn.hutool.json.JSONUtil; -import net.heimeng.sdk.btapi.api.BaseBtApi; -import net.heimeng.sdk.btapi.exception.BtApiException; -import net.heimeng.sdk.btapi.model.BtResult; +/** 设置站点流量限制的 API。 */ +public class SetWebsiteLimitNetApi extends AbstractWebsiteBooleanApi { -/** - * 设置网站流量限制API实现 - *

- * 用于为宝塔面板中的网站设置流量限制(仅支持nginx)。 - *

- * - * @author InwardFlow - * @since 2.0.0 - */ -public class SetWebsiteLimitNetApi extends BaseBtApi> { - - /** - * API端点路径 - */ - private static final String ENDPOINT = "site?action=SetLimitNet"; - - /** - * 构造函数,创建一个新的SetWebsiteLimitNetApi实例 - */ - public SetWebsiteLimitNetApi() { - super(ENDPOINT, HttpMethod.POST); - } - - /** - * 设置网站ID - * - * @param id 网站ID - * @return 当前API实例,支持链式调用 - */ - public SetWebsiteLimitNetApi setId(Integer id) { - addParam("id", id); - return this; - } - - /** - * 设置是否启用流量限制 - * - * @param enabled 是否启用 - * @return 当前API实例,支持链式调用 - */ - public SetWebsiteLimitNetApi setEnabled(Boolean enabled) { - addParam("enabled", enabled); - return this; - } - - /** - * 设置服务器每秒最大请求数 - * - * @param perserver 服务器每秒最大请求数,0表示不限制 - * @return 当前API实例,支持链式调用 - */ - public SetWebsiteLimitNetApi setPerserver(Integer perserver) { - addParam("perserver", perserver); - return this; - } - - /** - * 设置单IP每秒最大请求数 - * - * @param perip 单IP每秒最大请求数,0表示不限制 - * @return 当前API实例,支持链式调用 - */ - public SetWebsiteLimitNetApi setPerip(Integer perip) { - addParam("perip", perip); - return this; - } - - /** - * 设置带宽限制(KB/s) - * - * @param limitRate 带宽限制(KB/s),0表示不限制 - * @return 当前API实例,支持链式调用 - */ - public SetWebsiteLimitNetApi setLimitRate(Integer limitRate) { - addParam("limit_rate", limitRate); - return this; - } - - /** - * 验证请求参数是否有效 - * - * @return 如果请求参数有效则返回true,否则返回false - */ - @Override - protected boolean validateParams() { - return params.containsKey("id") && params.containsKey("enabled"); - } - - /** - * 解析API响应字符串为BtResult对象 - * - * @param response API响应字符串 - * @return BtResult对象 - * @throws BtApiException 当解析失败时抛出 - */ - @Override - public BtResult parseResponse(String response) { - if (response == null || response.isEmpty()) { - throw new BtApiException("Empty response received"); - } - - try { - if (!JSONUtil.isTypeJSON(response)) { - throw new BtApiException("Invalid JSON response: " + response); - } - - JSONObject json = JSONUtil.parseObj(response); - BtResult result = new BtResult<>(); - boolean status = json.getBool("status", false); - - result.setStatus(status); - result.setMsg(json.getStr("msg", status ? "设置成功" : "设置失败")); - result.setData(status); - - return result; - } catch (Exception e) { - throw new BtApiException("Failed to parse set website limit net response: " + e.getMessage(), e); - } - } -} \ No newline at end of file + private static final String ENDPOINT = "site?action=SetLimitNet"; + + public SetWebsiteLimitNetApi() { + super(ENDPOINT, "流量限制设置成功", "流量限制设置失败"); + } + + public SetWebsiteLimitNetApi setId(Integer id) { + requirePositiveInteger(id, "id"); + addParam("id", id); + return this; + } + + public SetWebsiteLimitNetApi setEnabled(Boolean enabled) { + requireNonNull(enabled, "enabled"); + addParam("enabled", enabled ? 1 : 0); + return this; + } + + public SetWebsiteLimitNetApi setPerserver(Integer perserver) { + putOptionalNonNegativeInteger("perserver", perserver); + return this; + } + + public SetWebsiteLimitNetApi setPerip(Integer perip) { + putOptionalNonNegativeInteger("perip", perip); + return this; + } + + public SetWebsiteLimitNetApi setLimitRate(Integer limitRate) { + putOptionalNonNegativeInteger("limit_rate", limitRate); + return this; + } + + @Override + protected boolean validateParams() { + return hasPositiveIntegerParam("id") + && hasBooleanFlagIntParam("enabled") + && hasOptionalNonNegativeNumberParam("perserver") + && hasOptionalNonNegativeNumberParam("perip") + && hasOptionalNonNegativeNumberParam("limit_rate"); + } +} diff --git a/src/main/java/net/heimeng/sdk/btapi/api/website/SetWebsiteLogsApi.java b/src/main/java/net/heimeng/sdk/btapi/api/website/SetWebsiteLogsApi.java index 12cd417..20c056a 100644 --- a/src/main/java/net/heimeng/sdk/btapi/api/website/SetWebsiteLogsApi.java +++ b/src/main/java/net/heimeng/sdk/btapi/api/website/SetWebsiteLogsApi.java @@ -1,84 +1,26 @@ package net.heimeng.sdk.btapi.api.website; -import cn.hutool.json.JSONObject; -import cn.hutool.json.JSONUtil; -import net.heimeng.sdk.btapi.api.BaseBtApi; -import net.heimeng.sdk.btapi.exception.BtApiException; -import net.heimeng.sdk.btapi.model.BtResult; - /** - * 设置网站访问日志开关API实现 - *

- * 用于设置宝塔面板中网站的访问日志开关状态(自动取反)。 - *

+ * 切换站点访问日志状态的 API。 * - * @author InwardFlow - * @since 2.0.0 + *

宝塔面板的该接口为切换语义,因此只需传入站点 ID。 */ -public class SetWebsiteLogsApi extends BaseBtApi> { - - /** - * API端点路径 - */ - private static final String ENDPOINT = "site?action=logsOpen"; - - /** - * 构造函数,创建一个新的SetWebsiteLogsApi实例 - */ - public SetWebsiteLogsApi() { - super(ENDPOINT, HttpMethod.POST); - } - - /** - * 设置网站ID - * - * @param id 网站ID - * @return 当前API实例,支持链式调用 - */ - public SetWebsiteLogsApi setId(Integer id) { - addParam("id", id); - return this; - } - - /** - * 验证请求参数是否有效 - * - * @return 如果请求参数有效则返回true,否则返回false - */ - @Override - protected boolean validateParams() { - return params.containsKey("id"); - } - - /** - * 解析API响应字符串为BtResult对象 - * - * @param response API响应字符串 - * @return BtResult对象 - * @throws BtApiException 当解析失败时抛出 - */ - @Override - public BtResult parseResponse(String response) { - if (response == null || response.isEmpty()) { - throw new BtApiException("Empty response received"); - } - - try { - if (!JSONUtil.isTypeJSON(response)) { - throw new BtApiException("Invalid JSON response: " + response); - } - - JSONObject json = JSONUtil.parseObj(response); - BtResult result = new BtResult<>(); - boolean status = json.getBool("status", false); - - result.setStatus(status); - result.setMsg(json.getStr("msg", status ? "设置成功" : "设置失败")); - result.setData(status); - - return result; - } catch (Exception e) { - throw new BtApiException("Failed to parse set website logs response: " + e.getMessage(), e); - } - } -} \ No newline at end of file +public class SetWebsiteLogsApi extends AbstractWebsiteBooleanApi { + + private static final String ENDPOINT = "site?action=logsOpen"; + + public SetWebsiteLogsApi() { + super(ENDPOINT, "日志状态切换成功", "日志状态切换失败"); + } + + public SetWebsiteLogsApi setId(Integer id) { + requirePositiveInteger(id, "id"); + addParam("id", id); + return this; + } + + @Override + protected boolean validateParams() { + return hasPositiveIntegerParam("id"); + } +} diff --git a/src/main/java/net/heimeng/sdk/btapi/api/website/SetWebsiteNginxConfigApi.java b/src/main/java/net/heimeng/sdk/btapi/api/website/SetWebsiteNginxConfigApi.java index b3d8992..e3af368 100644 --- a/src/main/java/net/heimeng/sdk/btapi/api/website/SetWebsiteNginxConfigApi.java +++ b/src/main/java/net/heimeng/sdk/btapi/api/website/SetWebsiteNginxConfigApi.java @@ -1,111 +1,38 @@ package net.heimeng.sdk.btapi.api.website; -import cn.hutool.json.JSONObject; -import cn.hutool.json.JSONUtil; -import net.heimeng.sdk.btapi.api.BaseBtApi; -import net.heimeng.sdk.btapi.exception.BtApiException; -import net.heimeng.sdk.btapi.model.BtResult; - /** - * 设置网站Nginx配置API实现 - *

- * 用于为宝塔面板中的网站设置Nginx配置文件内容。 - *

+ * 设置站点 Nginx 配置的 API。 * - * @author InwardFlow - * @since 2.0.0 + *

配置内容允许为空字符串,用于清空当前配置。 */ -public class SetWebsiteNginxConfigApi extends BaseBtApi> { - - /** - * API端点路径 - */ - private static final String ENDPOINT = "site?action=setConf"; - - /** - * 构造函数,创建一个新的SetWebsiteNginxConfigApi实例 - */ - public SetWebsiteNginxConfigApi() { - super(ENDPOINT, HttpMethod.POST); - } - - /** - * 设置网站ID - * - * @param id 网站ID - * @return 当前API实例,支持链式调用 - */ - public SetWebsiteNginxConfigApi setId(Integer id) { - addParam("id", id); - return this; - } - - /** - * 设置网站域名 - * - * @param domain 网站域名 - * @return 当前API实例,支持链式调用 - */ - public SetWebsiteNginxConfigApi setDomain(String domain) { - addParam("domain", domain); - return this; - } - - /** - * 设置Nginx配置文件内容 - * - * @param content 配置文件内容 - * @return 当前API实例,支持链式调用 - */ - public SetWebsiteNginxConfigApi setContent(String content) { - addParam("content", content); - return this; - } - - /** - * 验证请求参数是否有效 - * - * @return 如果请求参数有效则返回true,否则返回false - */ - @Override - protected boolean validateParams() { - return params.containsKey("id") && - params.containsKey("domain") && - params.containsKey("content") && - params.get("domain") != null && - !((String) params.get("domain")).isEmpty() && - params.get("content") != null; - } - - /** - * 解析API响应字符串为BtResult对象 - * - * @param response API响应字符串 - * @return BtResult对象 - * @throws BtApiException 当解析失败时抛出 - */ - @Override - public BtResult parseResponse(String response) { - if (response == null || response.isEmpty()) { - throw new BtApiException("Empty response received"); - } - - try { - if (!JSONUtil.isTypeJSON(response)) { - throw new BtApiException("Invalid JSON response: " + response); - } - - JSONObject json = JSONUtil.parseObj(response); - BtResult result = new BtResult<>(); - boolean status = json.getBool("status", false); - - result.setStatus(status); - result.setMsg(json.getStr("msg", status ? "设置成功" : "设置失败")); - result.setData(status); - - return result; - } catch (Exception e) { - throw new BtApiException("Failed to parse set website Nginx config response: " + e.getMessage(), e); - } - } -} \ No newline at end of file +public class SetWebsiteNginxConfigApi extends AbstractWebsiteBooleanApi { + + private static final String ENDPOINT = "site?action=setConf"; + + public SetWebsiteNginxConfigApi() { + super(ENDPOINT, "Nginx 配置设置成功", "Nginx 配置设置失败"); + } + + public SetWebsiteNginxConfigApi setId(Integer id) { + requirePositiveInteger(id, "id"); + addParam("id", id); + return this; + } + + public SetWebsiteNginxConfigApi setDomain(String domain) { + requireNonBlank(domain, "domain"); + addParam("domain", domain); + return this; + } + + public SetWebsiteNginxConfigApi setContent(String content) { + requireNonNull(content, "content"); + addParam("content", content); + return this; + } + + @Override + protected boolean validateParams() { + return hasPositiveIntegerParam("id") && hasNonBlankStringParam("domain") && hasParam("content"); + } +} diff --git a/src/main/java/net/heimeng/sdk/btapi/api/website/SetWebsitePasswordApi.java b/src/main/java/net/heimeng/sdk/btapi/api/website/SetWebsitePasswordApi.java index f129306..873b0c4 100644 --- a/src/main/java/net/heimeng/sdk/btapi/api/website/SetWebsitePasswordApi.java +++ b/src/main/java/net/heimeng/sdk/btapi/api/website/SetWebsitePasswordApi.java @@ -1,108 +1,34 @@ package net.heimeng.sdk.btapi.api.website; -import cn.hutool.json.JSONObject; -import cn.hutool.json.JSONUtil; -import net.heimeng.sdk.btapi.api.BaseBtApi; -import net.heimeng.sdk.btapi.exception.BtApiException; -import net.heimeng.sdk.btapi.model.BtResult; +/** 设置站点访问密码保护的 API。 */ +public class SetWebsitePasswordApi extends AbstractWebsiteBooleanApi { -/** - * 设置密码访问API实现 - *

- * 用于为宝塔面板中的网站设置访问密码保护。 - *

- * - * @author InwardFlow - * @since 2.0.0 - */ -public class SetWebsitePasswordApi extends BaseBtApi> { - - /** - * API端点路径 - */ - private static final String ENDPOINT = "site?action=SetHasPwd"; - - /** - * 构造函数,创建一个新的SetWebsitePasswordApi实例 - */ - public SetWebsitePasswordApi() { - super(ENDPOINT, HttpMethod.POST); - } - - /** - * 设置网站ID - * - * @param id 网站ID - * @return 当前API实例,支持链式调用 - */ - public SetWebsitePasswordApi setId(Integer id) { - addParam("id", id); - return this; - } - - /** - * 设置用户名 - * - * @param username 用户名 - * @return 当前API实例,支持链式调用 - */ - public SetWebsitePasswordApi setUsername(String username) { - addParam("username", username); - return this; - } - - /** - * 设置密码 - * - * @param password 密码 - * @return 当前API实例,支持链式调用 - */ - public SetWebsitePasswordApi setPassword(String password) { - addParam("password", password); - return this; - } - - /** - * 验证请求参数是否有效 - * - * @return 如果请求参数有效则返回true,否则返回false - */ - @Override - protected boolean validateParams() { - return params.containsKey("id") && params.containsKey("username") && params.containsKey("password") && - params.get("username") != null && !((String) params.get("username")).isEmpty() && - params.get("password") != null && !((String) params.get("password")).isEmpty(); - } - - /** - * 解析API响应字符串为BtResult对象 - * - * @param response API响应字符串 - * @return BtResult对象 - * @throws BtApiException 当解析失败时抛出 - */ - @Override - public BtResult parseResponse(String response) { - if (response == null || response.isEmpty()) { - throw new BtApiException("Empty response received"); - } - - try { - if (!JSONUtil.isTypeJSON(response)) { - throw new BtApiException("Invalid JSON response: " + response); - } - - JSONObject json = JSONUtil.parseObj(response); - BtResult result = new BtResult<>(); - boolean status = json.getBool("status", false); - - result.setStatus(status); - result.setMsg(json.getStr("msg", status ? "设置成功" : "设置失败")); - result.setData(status); - - return result; - } catch (Exception e) { - throw new BtApiException("Failed to parse set website password response: " + e.getMessage(), e); - } - } -} \ No newline at end of file + private static final String ENDPOINT = "site?action=SetHasPwd"; + + public SetWebsitePasswordApi() { + super(ENDPOINT, "密码访问设置成功", "密码访问设置失败"); + } + + public SetWebsitePasswordApi setId(Integer id) { + requirePositiveInteger(id, "id"); + addParam("id", id); + return this; + } + + public SetWebsitePasswordApi setUsername(String username) { + requireNonBlank(username, "username"); + addParam("username", username); + return this; + } + + public SetWebsitePasswordApi setPassword(String password) { + requireNonBlank(password, "password"); + addParam("password", password); + return this; + } + + @Override + protected boolean validateParams() { + return hasPositiveIntegerParam("id") && hasRequiredParams("username", "password"); + } +} diff --git a/src/main/java/net/heimeng/sdk/btapi/api/website/SetWebsitePhpExtensionsApi.java b/src/main/java/net/heimeng/sdk/btapi/api/website/SetWebsitePhpExtensionsApi.java index ba04f52..45fb105 100644 --- a/src/main/java/net/heimeng/sdk/btapi/api/website/SetWebsitePhpExtensionsApi.java +++ b/src/main/java/net/heimeng/sdk/btapi/api/website/SetWebsitePhpExtensionsApi.java @@ -1,110 +1,36 @@ package net.heimeng.sdk.btapi.api.website; -import cn.hutool.json.JSONObject; -import cn.hutool.json.JSONUtil; -import net.heimeng.sdk.btapi.api.BaseBtApi; -import net.heimeng.sdk.btapi.exception.BtApiException; -import net.heimeng.sdk.btapi.model.BtResult; +/** 切换站点 PHP 扩展状态的 API。 */ +public class SetWebsitePhpExtensionsApi extends AbstractWebsiteBooleanApi { -/** - * 设置网站PHP扩展API实现 - *

- * 用于为宝塔面板中的网站启用或禁用PHP扩展。 - *

- * - * @author InwardFlow - * @since 2.0.0 - */ -public class SetWebsitePhpExtensionsApi extends BaseBtApi> { - - /** - * API端点路径 - */ - private static final String ENDPOINT = "site?action=SetPHPModules"; - - /** - * 构造函数,创建一个新的SetWebsitePhpExtensionsApi实例 - */ - public SetWebsitePhpExtensionsApi() { - super(ENDPOINT, HttpMethod.POST); - } - - /** - * 设置网站ID - * - * @param id 网站ID - * @return 当前API实例,支持链式调用 - */ - public SetWebsitePhpExtensionsApi setId(Integer id) { - addParam("id", id); - return this; - } - - /** - * 设置PHP扩展名称 - * - * @param moduleName 扩展名称 - * @return 当前API实例,支持链式调用 - */ - public SetWebsitePhpExtensionsApi setModuleName(String moduleName) { - addParam("module_name", moduleName); - return this; - } - - /** - * 设置是否启用扩展 - * - * @param enabled 是否启用 - * @return 当前API实例,支持链式调用 - */ - public SetWebsitePhpExtensionsApi setEnabled(Boolean enabled) { - addParam("enabled", enabled); - return this; - } - - /** - * 验证请求参数是否有效 - * - * @return 如果请求参数有效则返回true,否则返回false - */ - @Override - protected boolean validateParams() { - return params.containsKey("id") && - params.containsKey("module_name") && - params.containsKey("enabled") && - params.get("module_name") != null && - !((String) params.get("module_name")).isEmpty(); - } - - /** - * 解析API响应字符串为BtResult对象 - * - * @param response API响应字符串 - * @return BtResult对象 - * @throws BtApiException 当解析失败时抛出 - */ - @Override - public BtResult parseResponse(String response) { - if (response == null || response.isEmpty()) { - throw new BtApiException("Empty response received"); - } - - try { - if (!JSONUtil.isTypeJSON(response)) { - throw new BtApiException("Invalid JSON response: " + response); - } - - JSONObject json = JSONUtil.parseObj(response); - BtResult result = new BtResult<>(); - boolean status = json.getBool("status", false); - - result.setStatus(status); - result.setMsg(json.getStr("msg", status ? "设置成功" : "设置失败")); - result.setData(status); - - return result; - } catch (Exception e) { - throw new BtApiException("Failed to parse set website PHP extensions response: " + e.getMessage(), e); - } - } -} \ No newline at end of file + private static final String ENDPOINT = "site?action=SetPHPModules"; + + public SetWebsitePhpExtensionsApi() { + super(ENDPOINT, "PHP 扩展开关设置成功", "PHP 扩展开关设置失败"); + } + + public SetWebsitePhpExtensionsApi setId(Integer id) { + requirePositiveInteger(id, "id"); + addParam("id", id); + return this; + } + + public SetWebsitePhpExtensionsApi setModuleName(String moduleName) { + requireNonBlank(moduleName, "moduleName"); + addParam("module_name", moduleName); + return this; + } + + public SetWebsitePhpExtensionsApi setEnabled(Boolean enabled) { + requireNonNull(enabled, "enabled"); + addParam("enabled", enabled ? 1 : 0); + return this; + } + + @Override + protected boolean validateParams() { + return hasPositiveIntegerParam("id") + && hasNonBlankStringParam("module_name") + && hasBooleanFlagIntParam("enabled"); + } +} diff --git a/src/main/java/net/heimeng/sdk/btapi/api/website/SetWebsitePhpVersionApi.java b/src/main/java/net/heimeng/sdk/btapi/api/website/SetWebsitePhpVersionApi.java index 6a7604d..2466eea 100644 --- a/src/main/java/net/heimeng/sdk/btapi/api/website/SetWebsitePhpVersionApi.java +++ b/src/main/java/net/heimeng/sdk/btapi/api/website/SetWebsitePhpVersionApi.java @@ -1,98 +1,28 @@ package net.heimeng.sdk.btapi.api.website; -import cn.hutool.json.JSONObject; -import cn.hutool.json.JSONUtil; -import net.heimeng.sdk.btapi.api.BaseBtApi; -import net.heimeng.sdk.btapi.exception.BtApiException; -import net.heimeng.sdk.btapi.model.BtResult; +/** 设置站点 PHP 版本的 API。 */ +public class SetWebsitePhpVersionApi extends AbstractWebsiteBooleanApi { -/** - * 设置网站PHP版本API实现 - *

- * 用于为宝塔面板中的网站设置PHP版本。 - *

- * - * @author InwardFlow - * @since 2.0.0 - */ -public class SetWebsitePhpVersionApi extends BaseBtApi> { - - /** - * API端点路径 - */ - private static final String ENDPOINT = "site?action=SetPhpVersion"; - - /** - * 构造函数,创建一个新的SetWebsitePhpVersionApi实例 - */ - public SetWebsitePhpVersionApi() { - super(ENDPOINT, HttpMethod.POST); - } - - /** - * 设置网站ID - * - * @param id 网站ID - * @return 当前API实例,支持链式调用 - */ - public SetWebsitePhpVersionApi setId(Integer id) { - addParam("id", id); - return this; - } - - /** - * 设置PHP版本号 - * - * @param phpVersion PHP版本号,如 "56", "70", "71", "72", "73", "74", "80", "81", "82" - * @return 当前API实例,支持链式调用 - */ - public SetWebsitePhpVersionApi setPhpVersion(String phpVersion) { - addParam("php_version", phpVersion); - return this; - } - - /** - * 验证请求参数是否有效 - * - * @return 如果请求参数有效则返回true,否则返回false - */ - @Override - protected boolean validateParams() { - return params.containsKey("id") && - params.containsKey("php_version") && - params.get("php_version") != null && - !((String) params.get("php_version")).isEmpty(); - } - - /** - * 解析API响应字符串为BtResult对象 - * - * @param response API响应字符串 - * @return BtResult对象 - * @throws BtApiException 当解析失败时抛出 - */ - @Override - public BtResult parseResponse(String response) { - if (response == null || response.isEmpty()) { - throw new BtApiException("Empty response received"); - } - - try { - if (!JSONUtil.isTypeJSON(response)) { - throw new BtApiException("Invalid JSON response: " + response); - } - - JSONObject json = JSONUtil.parseObj(response); - BtResult result = new BtResult<>(); - boolean status = json.getBool("status", false); - - result.setStatus(status); - result.setMsg(json.getStr("msg", status ? "设置成功" : "设置失败")); - result.setData(status); - - return result; - } catch (Exception e) { - throw new BtApiException("Failed to parse set website PHP version response: " + e.getMessage(), e); - } - } -} \ No newline at end of file + private static final String ENDPOINT = "site?action=SetPhpVersion"; + + public SetWebsitePhpVersionApi() { + super(ENDPOINT, "PHP 版本设置成功", "PHP 版本设置失败"); + } + + public SetWebsitePhpVersionApi setId(Integer id) { + requirePositiveInteger(id, "id"); + addParam("id", id); + return this; + } + + public SetWebsitePhpVersionApi setPhpVersion(String phpVersion) { + requireNonBlank(phpVersion, "phpVersion"); + addParam("php_version", phpVersion); + return this; + } + + @Override + protected boolean validateParams() { + return hasPositiveIntegerParam("id") && hasRequiredParams("php_version"); + } +} diff --git a/src/main/java/net/heimeng/sdk/btapi/api/website/SetWebsitePsApi.java b/src/main/java/net/heimeng/sdk/btapi/api/website/SetWebsitePsApi.java index 507c0d3..a6b92c9 100644 --- a/src/main/java/net/heimeng/sdk/btapi/api/website/SetWebsitePsApi.java +++ b/src/main/java/net/heimeng/sdk/btapi/api/website/SetWebsitePsApi.java @@ -1,95 +1,28 @@ package net.heimeng.sdk.btapi.api.website; -import cn.hutool.json.JSONObject; -import cn.hutool.json.JSONUtil; -import net.heimeng.sdk.btapi.api.BaseBtApi; -import net.heimeng.sdk.btapi.exception.BtApiException; -import net.heimeng.sdk.btapi.model.BtResult; +/** 修改站点备注的 API。 */ +public class SetWebsitePsApi extends AbstractWebsiteBooleanApi { -/** - * 修改网站备注API实现 - *

- * 用于修改宝塔面板中网站的备注信息。 - *

- * - * @author InwardFlow - * @since 2.0.0 - */ -public class SetWebsitePsApi extends BaseBtApi> { - - /** - * API端点路径 - */ - private static final String ENDPOINT = "data?action=setPs&table=sites"; - - /** - * 构造函数,创建一个新的SetWebsitePsApi实例 - */ - public SetWebsitePsApi() { - super(ENDPOINT, HttpMethod.POST); - } - - /** - * 设置网站ID - * - * @param id 网站ID - * @return 当前API实例,支持链式调用 - */ - public SetWebsitePsApi setId(Integer id) { - addParam("id", id); - return this; - } - - /** - * 设置网站备注 - * - * @param ps 备注内容 - * @return 当前API实例,支持链式调用 - */ - public SetWebsitePsApi setPs(String ps) { - addParam("ps", ps); - return this; - } - - /** - * 验证请求参数是否有效 - * - * @return 如果请求参数有效则返回true,否则返回false - */ - @Override - protected boolean validateParams() { - return params.containsKey("id") && params.containsKey("ps"); - } - - /** - * 解析API响应字符串为BtResult对象 - * - * @param response API响应字符串 - * @return BtResult对象 - * @throws BtApiException 当解析失败时抛出 - */ - @Override - public BtResult parseResponse(String response) { - if (response == null || response.isEmpty()) { - throw new BtApiException("Empty response received"); - } - - try { - if (!JSONUtil.isTypeJSON(response)) { - throw new BtApiException("Invalid JSON response: " + response); - } - - JSONObject json = JSONUtil.parseObj(response); - BtResult result = new BtResult<>(); - boolean status = json.getBool("status", false); - - result.setStatus(status); - result.setMsg(json.getStr("msg", status ? "修改成功" : "修改失败")); - result.setData(status); - - return result; - } catch (Exception e) { - throw new BtApiException("Failed to parse set website ps response: " + e.getMessage(), e); - } - } -} \ No newline at end of file + private static final String ENDPOINT = "data?action=setPs&table=sites"; + + public SetWebsitePsApi() { + super(ENDPOINT, "站点备注修改成功", "站点备注修改失败"); + } + + public SetWebsitePsApi setId(Integer id) { + requirePositiveInteger(id, "id"); + addParam("id", id); + return this; + } + + public SetWebsitePsApi setPs(String ps) { + requireNonNull(ps, "ps"); + addParam("ps", ps); + return this; + } + + @Override + protected boolean validateParams() { + return hasPositiveIntegerParam("id") && hasParam("ps"); + } +} diff --git a/src/main/java/net/heimeng/sdk/btapi/api/website/SetWebsiteRewriteRulesApi.java b/src/main/java/net/heimeng/sdk/btapi/api/website/SetWebsiteRewriteRulesApi.java index 7157199..29613a6 100644 --- a/src/main/java/net/heimeng/sdk/btapi/api/website/SetWebsiteRewriteRulesApi.java +++ b/src/main/java/net/heimeng/sdk/btapi/api/website/SetWebsiteRewriteRulesApi.java @@ -1,111 +1,38 @@ package net.heimeng.sdk.btapi.api.website; -import cn.hutool.json.JSONObject; -import cn.hutool.json.JSONUtil; -import net.heimeng.sdk.btapi.api.BaseBtApi; -import net.heimeng.sdk.btapi.exception.BtApiException; -import net.heimeng.sdk.btapi.model.BtResult; - /** - * 设置网站伪静态规则API实现 - *

- * 用于为宝塔面板中的网站设置伪静态规则。 - *

+ * 设置站点伪静态规则的 API。 * - * @author InwardFlow - * @since 2.0.0 + *

规则内容允许为空字符串,用于清空当前配置。 */ -public class SetWebsiteRewriteRulesApi extends BaseBtApi> { - - /** - * API端点路径 - */ - private static final String ENDPOINT = "site?action=setRewrite"; - - /** - * 构造函数,创建一个新的SetWebsiteRewriteRulesApi实例 - */ - public SetWebsiteRewriteRulesApi() { - super(ENDPOINT, HttpMethod.POST); - } - - /** - * 设置网站ID - * - * @param id 网站ID - * @return 当前API实例,支持链式调用 - */ - public SetWebsiteRewriteRulesApi setId(Integer id) { - addParam("id", id); - return this; - } - - /** - * 设置伪静态规则名称 - * - * @param name 规则名称 - * @return 当前API实例,支持链式调用 - */ - public SetWebsiteRewriteRulesApi setName(String name) { - addParam("name", name); - return this; - } - - /** - * 设置伪静态规则内容 - * - * @param content 规则内容 - * @return 当前API实例,支持链式调用 - */ - public SetWebsiteRewriteRulesApi setContent(String content) { - addParam("content", content); - return this; - } - - /** - * 验证请求参数是否有效 - * - * @return 如果请求参数有效则返回true,否则返回false - */ - @Override - protected boolean validateParams() { - return params.containsKey("id") && - params.containsKey("name") && - params.containsKey("content") && - params.get("name") != null && - !((String) params.get("name")).isEmpty() && - params.get("content") != null; - } - - /** - * 解析API响应字符串为BtResult对象 - * - * @param response API响应字符串 - * @return BtResult对象 - * @throws BtApiException 当解析失败时抛出 - */ - @Override - public BtResult parseResponse(String response) { - if (response == null || response.isEmpty()) { - throw new BtApiException("Empty response received"); - } - - try { - if (!JSONUtil.isTypeJSON(response)) { - throw new BtApiException("Invalid JSON response: " + response); - } - - JSONObject json = JSONUtil.parseObj(response); - BtResult result = new BtResult<>(); - boolean status = json.getBool("status", false); - - result.setStatus(status); - result.setMsg(json.getStr("msg", status ? "设置成功" : "设置失败")); - result.setData(status); - - return result; - } catch (Exception e) { - throw new BtApiException("Failed to parse set website rewrite rules response: " + e.getMessage(), e); - } - } -} \ No newline at end of file +public class SetWebsiteRewriteRulesApi extends AbstractWebsiteBooleanApi { + + private static final String ENDPOINT = "site?action=setRewrite"; + + public SetWebsiteRewriteRulesApi() { + super(ENDPOINT, "伪静态规则设置成功", "伪静态规则设置失败"); + } + + public SetWebsiteRewriteRulesApi setId(Integer id) { + requirePositiveInteger(id, "id"); + addParam("id", id); + return this; + } + + public SetWebsiteRewriteRulesApi setName(String name) { + requireNonBlank(name, "name"); + addParam("name", name); + return this; + } + + public SetWebsiteRewriteRulesApi setContent(String content) { + requireNonNull(content, "content"); + addParam("content", content); + return this; + } + + @Override + protected boolean validateParams() { + return hasPositiveIntegerParam("id") && hasNonBlankStringParam("name") && hasParam("content"); + } +} diff --git a/src/main/java/net/heimeng/sdk/btapi/api/website/SetWebsiteRootPathApi.java b/src/main/java/net/heimeng/sdk/btapi/api/website/SetWebsiteRootPathApi.java index 7156407..27c20df 100644 --- a/src/main/java/net/heimeng/sdk/btapi/api/website/SetWebsiteRootPathApi.java +++ b/src/main/java/net/heimeng/sdk/btapi/api/website/SetWebsiteRootPathApi.java @@ -1,96 +1,28 @@ package net.heimeng.sdk.btapi.api.website; -import cn.hutool.json.JSONObject; -import cn.hutool.json.JSONUtil; -import net.heimeng.sdk.btapi.api.BaseBtApi; -import net.heimeng.sdk.btapi.exception.BtApiException; -import net.heimeng.sdk.btapi.model.BtResult; +/** 修改站点根目录的 API。 */ +public class SetWebsiteRootPathApi extends AbstractWebsiteBooleanApi { -/** - * 修改网站根目录API实现 - *

- * 用于修改宝塔面板中指定网站的根目录。 - *

- * - * @author InwardFlow - * @since 2.0.0 - */ -public class SetWebsiteRootPathApi extends BaseBtApi> { - - /** - * API端点路径 - */ - private static final String ENDPOINT = "site?action=SetPath"; - - /** - * 构造函数,创建一个新的SetWebsiteRootPathApi实例 - */ - public SetWebsiteRootPathApi() { - super(ENDPOINT, HttpMethod.POST); - } - - /** - * 设置网站ID - * - * @param id 网站ID - * @return 当前API实例,支持链式调用 - */ - public SetWebsiteRootPathApi setId(Integer id) { - addParam("id", id); - return this; - } - - /** - * 设置新的网站根目录 - * - * @param path 新的网站根目录 - * @return 当前API实例,支持链式调用 - */ - public SetWebsiteRootPathApi setPath(String path) { - addParam("path", path); - return this; - } - - /** - * 验证请求参数是否有效 - * - * @return 如果请求参数有效则返回true,否则返回false - */ - @Override - protected boolean validateParams() { - return params.containsKey("id") && params.containsKey("path") && - params.get("path") != null && !((String) params.get("path")).isEmpty(); - } - - /** - * 解析API响应字符串为BtResult对象 - * - * @param response API响应字符串 - * @return BtResult对象 - * @throws BtApiException 当解析失败时抛出 - */ - @Override - public BtResult parseResponse(String response) { - if (response == null || response.isEmpty()) { - throw new BtApiException("Empty response received"); - } - - try { - if (!JSONUtil.isTypeJSON(response)) { - throw new BtApiException("Invalid JSON response: " + response); - } - - JSONObject json = JSONUtil.parseObj(response); - BtResult result = new BtResult<>(); - boolean status = json.getBool("status", false); - - result.setStatus(status); - result.setMsg(json.getStr("msg", status ? "修改成功" : "修改失败")); - result.setData(status); - - return result; - } catch (Exception e) { - throw new BtApiException("Failed to parse set website root path response: " + e.getMessage(), e); - } - } -} \ No newline at end of file + private static final String ENDPOINT = "site?action=SetPath"; + + public SetWebsiteRootPathApi() { + super(ENDPOINT, "站点根目录修改成功", "站点根目录修改失败"); + } + + public SetWebsiteRootPathApi setId(Integer id) { + requirePositiveInteger(id, "id"); + addParam("id", id); + return this; + } + + public SetWebsiteRootPathApi setPath(String path) { + requireNonBlank(path, "path"); + addParam("path", path); + return this; + } + + @Override + protected boolean validateParams() { + return hasPositiveIntegerParam("id") && hasRequiredParams("path"); + } +} diff --git a/src/main/java/net/heimeng/sdk/btapi/api/website/SetWebsiteRunPathApi.java b/src/main/java/net/heimeng/sdk/btapi/api/website/SetWebsiteRunPathApi.java index 1c444eb..36960b8 100644 --- a/src/main/java/net/heimeng/sdk/btapi/api/website/SetWebsiteRunPathApi.java +++ b/src/main/java/net/heimeng/sdk/btapi/api/website/SetWebsiteRunPathApi.java @@ -1,97 +1,32 @@ package net.heimeng.sdk.btapi.api.website; -import cn.hutool.json.JSONObject; -import cn.hutool.json.JSONUtil; -import net.heimeng.sdk.btapi.api.BaseBtApi; -import net.heimeng.sdk.btapi.exception.BtApiException; -import net.heimeng.sdk.btapi.model.BtResult; - /** - * 设置网站运行目录API实现 - *

- * 用于设置宝塔面板中网站的运行目录(基于网站根目录的子目录)。 - *

+ * 修改站点运行目录的 API。 * - * @author InwardFlow - * @since 2.0.0 + *

运行目录允许传空字符串,用于恢复到根目录执行。 */ -public class SetWebsiteRunPathApi extends BaseBtApi> { - - /** - * API端点路径 - */ - private static final String ENDPOINT = "site?action=SetSiteRunPath"; - - /** - * 构造函数,创建一个新的SetWebsiteRunPathApi实例 - */ - public SetWebsiteRunPathApi() { - super(ENDPOINT, HttpMethod.POST); - } - - /** - * 设置网站ID - * - * @param id 网站ID - * @return 当前API实例,支持链式调用 - */ - public SetWebsiteRunPathApi setId(Integer id) { - addParam("id", id); - return this; - } - - /** - * 设置运行目录 - *

基于网站根目录的运行目录,如:/public

- * - * @param runPath 运行目录 - * @return 当前API实例,支持链式调用 - */ - public SetWebsiteRunPathApi setRunPath(String runPath) { - addParam("runPath", runPath); - return this; - } - - /** - * 验证请求参数是否有效 - * - * @return 如果请求参数有效则返回true,否则返回false - */ - @Override - protected boolean validateParams() { - return params.containsKey("id") && params.containsKey("runPath") && - params.get("runPath") != null; - } - - /** - * 解析API响应字符串为BtResult对象 - * - * @param response API响应字符串 - * @return BtResult对象 - * @throws BtApiException 当解析失败时抛出 - */ - @Override - public BtResult parseResponse(String response) { - if (response == null || response.isEmpty()) { - throw new BtApiException("Empty response received"); - } - - try { - if (!JSONUtil.isTypeJSON(response)) { - throw new BtApiException("Invalid JSON response: " + response); - } - - JSONObject json = JSONUtil.parseObj(response); - BtResult result = new BtResult<>(); - boolean status = json.getBool("status", false); - - result.setStatus(status); - result.setMsg(json.getStr("msg", status ? "设置成功" : "设置失败")); - result.setData(status); - - return result; - } catch (Exception e) { - throw new BtApiException("Failed to parse set website run path response: " + e.getMessage(), e); - } - } -} \ No newline at end of file +public class SetWebsiteRunPathApi extends AbstractWebsiteBooleanApi { + + private static final String ENDPOINT = "site?action=SetSiteRunPath"; + + public SetWebsiteRunPathApi() { + super(ENDPOINT, "运行目录设置成功", "运行目录设置失败"); + } + + public SetWebsiteRunPathApi setId(Integer id) { + requirePositiveInteger(id, "id"); + addParam("id", id); + return this; + } + + public SetWebsiteRunPathApi setRunPath(String runPath) { + requireNonNull(runPath, "runPath"); + addParam("runPath", runPath); + return this; + } + + @Override + protected boolean validateParams() { + return hasPositiveIntegerParam("id") && hasParam("runPath"); + } +} diff --git a/src/main/java/net/heimeng/sdk/btapi/api/website/SetWebsiteSslApi.java b/src/main/java/net/heimeng/sdk/btapi/api/website/SetWebsiteSslApi.java index 437ad42..17c7722 100644 --- a/src/main/java/net/heimeng/sdk/btapi/api/website/SetWebsiteSslApi.java +++ b/src/main/java/net/heimeng/sdk/btapi/api/website/SetWebsiteSslApi.java @@ -1,132 +1,47 @@ package net.heimeng.sdk.btapi.api.website; -import cn.hutool.json.JSONObject; -import cn.hutool.json.JSONUtil; -import net.heimeng.sdk.btapi.api.BaseBtApi; -import net.heimeng.sdk.btapi.exception.BtApiException; -import net.heimeng.sdk.btapi.model.BtResult; +/** 设置站点 SSL 证书的 API。 */ +public class SetWebsiteSslApi extends AbstractWebsiteBooleanApi { -/** - * 设置SSL证书API实现 - *

- * 用于为宝塔面板中的网站设置SSL证书。 - *

- * - * @author InwardFlow - * @since 2.0.0 - */ -public class SetWebsiteSslApi extends BaseBtApi> { - - /** - * API端点路径 - */ - private static final String ENDPOINT = "site?action=SetSSL"; - - /** - * 构造函数,创建一个新的SetWebsiteSslApi实例 - */ - public SetWebsiteSslApi() { - super(ENDPOINT, HttpMethod.POST); - } - - /** - * 设置网站ID - * - * @param id 网站ID - * @return 当前API实例,支持链式调用 - */ - public SetWebsiteSslApi setId(Integer id) { - addParam("id", id); - return this; - } - - /** - * 设置证书域名 - * - * @param domain 证书域名 - * @return 当前API实例,支持链式调用 - */ - public SetWebsiteSslApi setDomain(String domain) { - addParam("domain", domain); - return this; - } - - /** - * 设置证书内容 - * - * @param cert 证书内容 - * @return 当前API实例,支持链式调用 - */ - public SetWebsiteSslApi setCert(String cert) { - addParam("cert", cert); - return this; - } - - /** - * 设置私钥内容 - * - * @param key 私钥内容 - * @return 当前API实例,支持链式调用 - */ - public SetWebsiteSslApi setKey(String key) { - addParam("key", key); - return this; - } - - /** - * 设置是否强制HTTPS - * - * @param forceHttps 是否强制HTTPS - * @return 当前API实例,支持链式调用 - */ - public SetWebsiteSslApi setForceHttps(Boolean forceHttps) { - addParam("force_https", forceHttps); - return this; - } - - /** - * 验证请求参数是否有效 - * - * @return 如果请求参数有效则返回true,否则返回false - */ - @Override - protected boolean validateParams() { - return params.containsKey("id") && params.containsKey("domain") && - params.containsKey("cert") && params.containsKey("key") && - params.get("domain") != null && !((String) params.get("domain")).isEmpty() && - params.get("cert") != null && !((String) params.get("cert")).isEmpty() && - params.get("key") != null && !((String) params.get("key")).isEmpty(); - } - - /** - * 解析API响应字符串为BtResult对象 - * - * @param response API响应字符串 - * @return BtResult对象 - * @throws BtApiException 当解析失败时抛出 - */ - @Override - public BtResult parseResponse(String response) { - if (response == null || response.isEmpty()) { - throw new BtApiException("Empty response received"); - } - - try { - if (!JSONUtil.isTypeJSON(response)) { - throw new BtApiException("Invalid JSON response: " + response); - } - - JSONObject json = JSONUtil.parseObj(response); - BtResult result = new BtResult<>(); - boolean status = json.getBool("status", false); - - result.setStatus(status); - result.setMsg(json.getStr("msg", status ? "设置成功" : "设置失败")); - result.setData(status); - - return result; - } catch (Exception e) { - throw new BtApiException("Failed to parse set website SSL response: " + e.getMessage(), e); - } - } -} \ No newline at end of file + private static final String ENDPOINT = "site?action=SetSSL"; + + public SetWebsiteSslApi() { + super(ENDPOINT, "SSL 证书设置成功", "SSL 证书设置失败"); + } + + public SetWebsiteSslApi setId(Integer id) { + requirePositiveInteger(id, "id"); + addParam("id", id); + return this; + } + + public SetWebsiteSslApi setDomain(String domain) { + requireNonBlank(domain, "domain"); + addParam("domain", domain); + return this; + } + + public SetWebsiteSslApi setCert(String cert) { + requireNonBlank(cert, "cert"); + addParam("cert", cert); + return this; + } + + public SetWebsiteSslApi setKey(String key) { + requireNonBlank(key, "key"); + addParam("key", key); + return this; + } + + public SetWebsiteSslApi setForceHttps(Boolean forceHttps) { + putOptionalBooleanFlag("force_https", forceHttps); + return this; + } + + @Override + protected boolean validateParams() { + return hasPositiveIntegerParam("id") + && hasRequiredParams("domain", "cert", "key") + && hasOptionalBooleanFlagIntParam("force_https"); + } +} diff --git a/src/main/java/net/heimeng/sdk/btapi/api/website/SetWebsiteUserIniApi.java b/src/main/java/net/heimeng/sdk/btapi/api/website/SetWebsiteUserIniApi.java index 4f8d697..49cf818 100644 --- a/src/main/java/net/heimeng/sdk/btapi/api/website/SetWebsiteUserIniApi.java +++ b/src/main/java/net/heimeng/sdk/btapi/api/website/SetWebsiteUserIniApi.java @@ -1,84 +1,22 @@ package net.heimeng.sdk.btapi.api.website; -import cn.hutool.json.JSONObject; -import cn.hutool.json.JSONUtil; -import net.heimeng.sdk.btapi.api.BaseBtApi; -import net.heimeng.sdk.btapi.exception.BtApiException; -import net.heimeng.sdk.btapi.model.BtResult; +/** 切换站点防跨站配置的 API。 */ +public class SetWebsiteUserIniApi extends AbstractWebsiteBooleanApi { -/** - * 设置防跨站状态API实现 - *

- * 用于设置宝塔面板中网站的防跨站状态(自动取反)。 - *

- * - * @author InwardFlow - * @since 2.0.0 - */ -public class SetWebsiteUserIniApi extends BaseBtApi> { - - /** - * API端点路径 - */ - private static final String ENDPOINT = "site?action=SetDirUserINI"; - - /** - * 构造函数,创建一个新的SetWebsiteUserIniApi实例 - */ - public SetWebsiteUserIniApi() { - super(ENDPOINT, HttpMethod.POST); - } - - /** - * 设置网站根目录 - * - * @param path 网站根目录 - * @return 当前API实例,支持链式调用 - */ - public SetWebsiteUserIniApi setPath(String path) { - addParam("path", path); - return this; - } - - /** - * 验证请求参数是否有效 - * - * @return 如果请求参数有效则返回true,否则返回false - */ - @Override - protected boolean validateParams() { - return params.containsKey("path") && params.get("path") != null && !((String) params.get("path")).isEmpty(); - } - - /** - * 解析API响应字符串为BtResult对象 - * - * @param response API响应字符串 - * @return BtResult对象 - * @throws BtApiException 当解析失败时抛出 - */ - @Override - public BtResult parseResponse(String response) { - if (response == null || response.isEmpty()) { - throw new BtApiException("Empty response received"); - } - - try { - if (!JSONUtil.isTypeJSON(response)) { - throw new BtApiException("Invalid JSON response: " + response); - } - - JSONObject json = JSONUtil.parseObj(response); - BtResult result = new BtResult<>(); - boolean status = json.getBool("status", false); - - result.setStatus(status); - result.setMsg(json.getStr("msg", status ? "设置成功" : "设置失败")); - result.setData(status); - - return result; - } catch (Exception e) { - throw new BtApiException("Failed to parse set website user ini response: " + e.getMessage(), e); - } - } -} \ No newline at end of file + private static final String ENDPOINT = "site?action=SetDirUserINI"; + + public SetWebsiteUserIniApi() { + super(ENDPOINT, "防跨站配置切换成功", "防跨站配置切换失败"); + } + + public SetWebsiteUserIniApi setPath(String path) { + requireNonBlank(path, "path"); + addParam("path", path); + return this; + } + + @Override + protected boolean validateParams() { + return hasRequiredParams("path"); + } +} diff --git a/src/main/java/net/heimeng/sdk/btapi/api/website/StartWebsiteApi.java b/src/main/java/net/heimeng/sdk/btapi/api/website/StartWebsiteApi.java index 721c88c..0202d3b 100644 --- a/src/main/java/net/heimeng/sdk/btapi/api/website/StartWebsiteApi.java +++ b/src/main/java/net/heimeng/sdk/btapi/api/website/StartWebsiteApi.java @@ -1,84 +1,22 @@ package net.heimeng.sdk.btapi.api.website; -import cn.hutool.json.JSONObject; -import cn.hutool.json.JSONUtil; -import net.heimeng.sdk.btapi.api.BaseBtApi; -import net.heimeng.sdk.btapi.exception.BtApiException; -import net.heimeng.sdk.btapi.model.BtResult; +/** 启动站点的 API。 */ +public class StartWebsiteApi extends AbstractWebsiteBooleanApi { -/** - * 启动网站API实现 - *

- * 用于在宝塔面板中启动指定的网站。 - *

- * - * @author InwardFlow - * @since 2.0.0 - */ -public class StartWebsiteApi extends BaseBtApi> { - - /** - * API端点路径 - */ - private static final String ENDPOINT = "site?action=SiteStart"; - - /** - * 构造函数,创建一个新的StartWebsiteApi实例 - */ - public StartWebsiteApi() { - super(ENDPOINT, HttpMethod.POST); - } - - /** - * 设置网站ID - * - * @param id 网站ID - * @return 当前API实例,支持链式调用 - */ - public StartWebsiteApi setId(Integer id) { - addParam("id", id); - return this; - } - - /** - * 验证请求参数是否有效 - * - * @return 如果请求参数有效则返回true,否则返回false - */ - @Override - protected boolean validateParams() { - return params.containsKey("id"); - } - - /** - * 解析API响应字符串为BtResult对象 - * - * @param response API响应字符串 - * @return BtResult对象 - * @throws BtApiException 当解析失败时抛出 - */ - @Override - public BtResult parseResponse(String response) { - if (response == null || response.isEmpty()) { - throw new BtApiException("Empty response received"); - } - - try { - if (!JSONUtil.isTypeJSON(response)) { - throw new BtApiException("Invalid JSON response: " + response); - } - - JSONObject json = JSONUtil.parseObj(response); - BtResult result = new BtResult<>(); - boolean status = json.getBool("status", false); - - result.setStatus(status); - result.setMsg(json.getStr("msg", status ? "启动成功" : "启动失败")); - result.setData(status); - - return result; - } catch (Exception e) { - throw new BtApiException("Failed to parse start website response: " + e.getMessage(), e); - } - } -} \ No newline at end of file + private static final String ENDPOINT = "site?action=SiteStart"; + + public StartWebsiteApi() { + super(ENDPOINT, "站点启动成功", "站点启动失败"); + } + + public StartWebsiteApi setId(Integer id) { + requirePositiveInteger(id, "id"); + addParam("id", id); + return this; + } + + @Override + protected boolean validateParams() { + return hasPositiveIntegerParam("id"); + } +} diff --git a/src/main/java/net/heimeng/sdk/btapi/api/website/StopWebsiteApi.java b/src/main/java/net/heimeng/sdk/btapi/api/website/StopWebsiteApi.java index 175b0a6..d7ab175 100644 --- a/src/main/java/net/heimeng/sdk/btapi/api/website/StopWebsiteApi.java +++ b/src/main/java/net/heimeng/sdk/btapi/api/website/StopWebsiteApi.java @@ -1,84 +1,22 @@ package net.heimeng.sdk.btapi.api.website; -import cn.hutool.json.JSONObject; -import cn.hutool.json.JSONUtil; -import net.heimeng.sdk.btapi.api.BaseBtApi; -import net.heimeng.sdk.btapi.exception.BtApiException; -import net.heimeng.sdk.btapi.model.BtResult; +/** 停止站点的 API。 */ +public class StopWebsiteApi extends AbstractWebsiteBooleanApi { -/** - * 停止网站API实现 - *

- * 用于在宝塔面板中停止指定的网站。 - *

- * - * @author InwardFlow - * @since 2.0.0 - */ -public class StopWebsiteApi extends BaseBtApi> { - - /** - * API端点路径 - */ - private static final String ENDPOINT = "site?action=StopSite"; - - /** - * 构造函数,创建一个新的StopWebsiteApi实例 - */ - public StopWebsiteApi() { - super(ENDPOINT, HttpMethod.POST); - } - - /** - * 设置网站ID - * - * @param id 网站ID - * @return 当前API实例,支持链式调用 - */ - public StopWebsiteApi setId(Integer id) { - addParam("id", id); - return this; - } - - /** - * 验证请求参数是否有效 - * - * @return 如果请求参数有效则返回true,否则返回false - */ - @Override - protected boolean validateParams() { - return params.containsKey("id"); - } - - /** - * 解析API响应字符串为BtResult对象 - * - * @param response API响应字符串 - * @return BtResult对象 - * @throws BtApiException 当解析失败时抛出 - */ - @Override - public BtResult parseResponse(String response) { - if (response == null || response.isEmpty()) { - throw new BtApiException("Empty response received"); - } - - try { - if (!JSONUtil.isTypeJSON(response)) { - throw new BtApiException("Invalid JSON response: " + response); - } - - JSONObject json = JSONUtil.parseObj(response); - BtResult result = new BtResult<>(); - boolean status = json.getBool("status", false); - - result.setStatus(status); - result.setMsg(json.getStr("msg", status ? "停止成功" : "停止失败")); - result.setData(status); - - return result; - } catch (Exception e) { - throw new BtApiException("Failed to parse stop website response: " + e.getMessage(), e); - } - } -} \ No newline at end of file + private static final String ENDPOINT = "site?action=StopSite"; + + public StopWebsiteApi() { + super(ENDPOINT, "站点停止成功", "站点停止失败"); + } + + public StopWebsiteApi setId(Integer id) { + requirePositiveInteger(id, "id"); + addParam("id", id); + return this; + } + + @Override + protected boolean validateParams() { + return hasPositiveIntegerParam("id"); + } +} diff --git a/src/main/java/net/heimeng/sdk/btapi/api/website/WebsiteApiResponseSupport.java b/src/main/java/net/heimeng/sdk/btapi/api/website/WebsiteApiResponseSupport.java new file mode 100644 index 0000000..9cbefb2 --- /dev/null +++ b/src/main/java/net/heimeng/sdk/btapi/api/website/WebsiteApiResponseSupport.java @@ -0,0 +1,148 @@ +package net.heimeng.sdk.btapi.api.website; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import cn.hutool.json.JSON; +import cn.hutool.json.JSONArray; +import cn.hutool.json.JSONException; +import cn.hutool.json.JSONObject; +import cn.hutool.json.JSONUtil; + +import net.heimeng.sdk.btapi.exception.BtApiException; +import net.heimeng.sdk.btapi.model.BtResult; + +/** + * Website 模块响应解析辅助类。 + * + *

集中处理 JSON 解析、结果对象构建以及 Map/List 的递归转换,避免查询型 API 在细节上各自维护一套逻辑。 + */ +final class WebsiteApiResponseSupport { + + private WebsiteApiResponseSupport() {} + + static boolean isJsonPayload(String response) { + return response != null && !response.isBlank() && JSONUtil.isTypeJSON(response.trim()); + } + + static JSON parseJsonResponse(String response, String responseName) { + if (response == null || response.isBlank()) { + throw new BtApiException("Empty response received"); + } + + String normalizedResponse = response.trim(); + try { + if (!JSONUtil.isTypeJSON(normalizedResponse)) { + throw new BtApiException("Invalid JSON response: " + normalizedResponse); + } + return JSONUtil.parse(normalizedResponse); + } catch (JSONException exception) { + throw new BtApiException( + "Invalid JSON response for " + responseName + ": " + normalizedResponse, exception); + } + } + + static Map toMap(JSONObject jsonObject) { + Map result = new LinkedHashMap<>(); + for (String key : jsonObject.keySet()) { + result.put(key, normalizeJsonValue(jsonObject.get(key))); + } + return result; + } + + static List> toMapList(JSONArray jsonArray) { + List> result = new ArrayList<>(); + for (int i = 0; i < jsonArray.size(); i++) { + JSONObject jsonObject = jsonArray.getJSONObject(i); + if (jsonObject != null) { + result.add(toMap(jsonObject)); + } + } + return List.copyOf(result); + } + + static BtResult> successListResult(List data, String message) { + BtResult> result = new BtResult<>(); + result.setStatus(true); + result.setMsg(message == null || message.isBlank() ? "Success" : message); + result.setData(data); + return result; + } + + static BtResult> failureListResult(String message) { + BtResult> result = new BtResult<>(); + result.setStatus(false); + result.setMsg(message); + result.setData(List.of()); + return result; + } + + static BtResult> successMapResult(Map data, String message) { + BtResult> result = new BtResult<>(); + result.setStatus(true); + result.setMsg(message == null || message.isBlank() ? "Success" : message); + result.setData(data); + return result; + } + + static BtResult> failureMapResult(String message) { + BtResult> result = new BtResult<>(); + result.setStatus(false); + result.setMsg(message); + result.setData(Map.of()); + return result; + } + + static BtResult successStringResult(String data, String message) { + BtResult result = new BtResult<>(); + result.setStatus(true); + result.setMsg(message == null || message.isBlank() ? "Success" : message); + result.setData(data); + return result; + } + + static BtResult failureStringResult(String message) { + BtResult result = new BtResult<>(); + result.setStatus(false); + result.setMsg(message); + result.setData(""); + return result; + } + + static String stringifyJsonValue(Object value) { + if (value == null) { + return ""; + } + if (value instanceof JSONObject || value instanceof JSONArray) { + return JSONUtil.toJsonStr(value); + } + return String.valueOf(value); + } + + static Map removeStatusAndMsg(Map payload) { + Map copy = new LinkedHashMap<>(payload); + copy.remove("status"); + copy.remove("msg"); + return Map.copyOf(copy); + } + + private static Object normalizeJsonValue(Object value) { + if (value instanceof JSONObject jsonObject) { + return toMap(jsonObject); + } + if (value instanceof JSONArray jsonArray) { + return toList(jsonArray); + } + return value; + } + + private static List toList(JSONArray jsonArray) { + List result = new ArrayList<>(); + for (int i = 0; i < jsonArray.size(); i++) { + result.add(normalizeJsonValue(jsonArray.get(i))); + } + return List.copyOf(result); + } +} diff --git a/src/main/java/net/heimeng/sdk/btapi/facade/DatabaseCreateRequest.java b/src/main/java/net/heimeng/sdk/btapi/facade/DatabaseCreateRequest.java new file mode 100644 index 0000000..b806535 --- /dev/null +++ b/src/main/java/net/heimeng/sdk/btapi/facade/DatabaseCreateRequest.java @@ -0,0 +1,150 @@ +package net.heimeng.sdk.btapi.facade; + +/** + * Immutable request model for database creation through the facade layer. + * + *

Defaults mirror the current panel conventions while keeping optional database provisioning + * details explicit and discoverable. + */ +public record DatabaseCreateRequest( + String databaseName, + String username, + String password, + Type type, + String charset, + String remark, + String dataAccess, + String address, + String listenIp, + String host, + int sid) { + + private static final Type DEFAULT_TYPE = Type.MYSQL; + private static final String DEFAULT_CHARSET = "utf8mb4"; + private static final String DEFAULT_DATA_ACCESS = "%"; + private static final String DEFAULT_ADDRESS = "%"; + private static final String DEFAULT_LISTEN_IP = "0.0.0.0/0"; + private static final String DEFAULT_HOST = "%"; + private static final int DEFAULT_SID = 0; + + public DatabaseCreateRequest { + databaseName = requireNonBlank(databaseName, "databaseName"); + username = requireNonBlank(username, "username"); + password = requireNonBlank(password, "password"); + type = requireNonNull(type, "type"); + charset = requireNonBlank(charset, "charset"); + remark = requireNonBlank(remark, "remark"); + dataAccess = requireNonBlank(dataAccess, "dataAccess"); + address = requireNonBlank(address, "address"); + listenIp = requireNonBlank(listenIp, "listenIp"); + host = requireNonBlank(host, "host"); + if (sid < 0) { + throw new IllegalArgumentException("sid cannot be negative"); + } + } + + public static Builder builder(String databaseName, String username, String password) { + return new Builder(databaseName, username, password); + } + + public enum Type { + MYSQL("MySQL"), + MONGODB("MongoDb"); + + private final String apiValue; + + Type(String apiValue) { + this.apiValue = apiValue; + } + + public String apiValue() { + return apiValue; + } + } + + public static final class Builder { + + private final String databaseName; + private final String username; + private final String password; + + private Type type = DEFAULT_TYPE; + private String charset = DEFAULT_CHARSET; + private String remark; + private String dataAccess = DEFAULT_DATA_ACCESS; + private String address = DEFAULT_ADDRESS; + private String listenIp = DEFAULT_LISTEN_IP; + private String host = DEFAULT_HOST; + private int sid = DEFAULT_SID; + + private Builder(String databaseName, String username, String password) { + this.databaseName = requireNonBlank(databaseName, "databaseName"); + this.username = requireNonBlank(username, "username"); + this.password = requireNonBlank(password, "password"); + this.remark = this.databaseName; + } + + public Builder type(Type type) { + this.type = requireNonNull(type, "type"); + return this; + } + + public Builder charset(String charset) { + this.charset = requireNonBlank(charset, "charset"); + return this; + } + + public Builder remark(String remark) { + this.remark = requireNonBlank(remark, "remark"); + return this; + } + + public Builder dataAccess(String dataAccess) { + this.dataAccess = requireNonBlank(dataAccess, "dataAccess"); + return this; + } + + public Builder address(String address) { + this.address = requireNonBlank(address, "address"); + return this; + } + + public Builder listenIp(String listenIp) { + this.listenIp = requireNonBlank(listenIp, "listenIp"); + return this; + } + + public Builder host(String host) { + this.host = requireNonBlank(host, "host"); + return this; + } + + public Builder sid(int sid) { + if (sid < 0) { + throw new IllegalArgumentException("sid cannot be negative"); + } + this.sid = sid; + return this; + } + + public DatabaseCreateRequest build() { + return new DatabaseCreateRequest( + databaseName, username, password, type, charset, remark, dataAccess, address, listenIp, + host, sid); + } + } + + private static String requireNonBlank(String value, String fieldName) { + if (value == null || value.isBlank()) { + throw new IllegalArgumentException(fieldName + " cannot be blank"); + } + return value; + } + + private static T requireNonNull(T value, String fieldName) { + if (value == null) { + throw new IllegalArgumentException(fieldName + " cannot be null"); + } + return value; + } +} diff --git a/src/main/java/net/heimeng/sdk/btapi/facade/DatabaseDeleteRequest.java b/src/main/java/net/heimeng/sdk/btapi/facade/DatabaseDeleteRequest.java new file mode 100644 index 0000000..6ed8105 --- /dev/null +++ b/src/main/java/net/heimeng/sdk/btapi/facade/DatabaseDeleteRequest.java @@ -0,0 +1,19 @@ +package net.heimeng.sdk.btapi.facade; + +/** + * Immutable request model for deleting a database. + * + * @param databaseName panel-visible database name + * @param databaseId panel database identifier + */ +public record DatabaseDeleteRequest(String databaseName, int databaseId) { + + public DatabaseDeleteRequest { + if (databaseName == null || databaseName.isBlank()) { + throw new IllegalArgumentException("databaseName cannot be blank"); + } + if (databaseId <= 0) { + throw new IllegalArgumentException("databaseId must be positive"); + } + } +} diff --git a/src/main/java/net/heimeng/sdk/btapi/facade/DatabaseOperations.java b/src/main/java/net/heimeng/sdk/btapi/facade/DatabaseOperations.java new file mode 100644 index 0000000..bb9e671 --- /dev/null +++ b/src/main/java/net/heimeng/sdk/btapi/facade/DatabaseOperations.java @@ -0,0 +1,77 @@ +package net.heimeng.sdk.btapi.facade; + +import java.util.List; +import java.util.Objects; + +import net.heimeng.sdk.btapi.api.database.ChangeDatabasePasswordApi; +import net.heimeng.sdk.btapi.api.database.CreateDatabaseApi; +import net.heimeng.sdk.btapi.api.database.DeleteDatabaseApi; +import net.heimeng.sdk.btapi.api.database.GetDatabasesApi; +import net.heimeng.sdk.btapi.client.BtClient; +import net.heimeng.sdk.btapi.model.BtResult; +import net.heimeng.sdk.btapi.model.database.DatabaseInfo; + +/** + * 数据库相关能力的门面入口。 + * + *

聚合数据库列表、创建、删除和改密等常见操作,适合作为业务代码的首选调用层。 + */ +public final class DatabaseOperations extends AbstractOperations { + + public DatabaseOperations(BtClient client) { + super(client); + } + + public BtResult> list() { + return execute(new GetDatabasesApi()); + } + + public BtResult create(CreateDatabaseApi api) { + Objects.requireNonNull(api, "api cannot be null"); + return execute(api); + } + + public BtResult create(DatabaseCreateRequest request) { + Objects.requireNonNull(request, "request cannot be null"); + + CreateDatabaseApi.Builder builder = + CreateDatabaseApi.builder(request.databaseName(), request.username(), request.password()) + .withCharset(request.charset()) + .withNote(request.remark()) + .withDataAccess(request.dataAccess()) + .withAddress(request.address()) + .withListenIp(request.listenIp()) + .withHost(request.host()) + .withSid(request.sid()); + + if (request.type() == DatabaseCreateRequest.Type.MONGODB) { + builder.asMongoDb(); + } else { + builder.asMySql(); + } + + return execute(builder.build()); + } + + public BtResult delete(DatabaseDeleteRequest request) { + Objects.requireNonNull(request, "request cannot be null"); + return execute(DeleteDatabaseApi.create(request.databaseName(), request.databaseId())); + } + + /** + * Backward-compatible overload retained for existing utility helpers. + * + *

New code should prefer {@link #delete(DatabaseDeleteRequest)} so the request shape remains + * explicit at the facade boundary. + */ + public BtResult delete(String databaseName, int databaseId) { + return delete(new DatabaseDeleteRequest(databaseName, databaseId)); + } + + public BtResult updatePassword(DatabasePasswordUpdateRequest request) { + Objects.requireNonNull(request, "request cannot be null"); + return execute( + new ChangeDatabasePasswordApi( + request.databaseName(), request.username(), request.newPassword())); + } +} diff --git a/src/main/java/net/heimeng/sdk/btapi/facade/DatabasePasswordUpdateRequest.java b/src/main/java/net/heimeng/sdk/btapi/facade/DatabasePasswordUpdateRequest.java new file mode 100644 index 0000000..ff4e18d --- /dev/null +++ b/src/main/java/net/heimeng/sdk/btapi/facade/DatabasePasswordUpdateRequest.java @@ -0,0 +1,24 @@ +package net.heimeng.sdk.btapi.facade; + +/** + * Immutable request model for database password changes. + * + * @param databaseName panel-visible database name + * @param username database account username + * @param newPassword new password to apply + */ +public record DatabasePasswordUpdateRequest( + String databaseName, String username, String newPassword) { + + public DatabasePasswordUpdateRequest { + if (databaseName == null || databaseName.isBlank()) { + throw new IllegalArgumentException("databaseName cannot be blank"); + } + if (username == null || username.isBlank()) { + throw new IllegalArgumentException("username cannot be blank"); + } + if (newPassword == null || newPassword.isBlank()) { + throw new IllegalArgumentException("newPassword cannot be blank"); + } + } +} diff --git a/src/main/java/net/heimeng/sdk/btapi/facade/FtpCreateRequest.java b/src/main/java/net/heimeng/sdk/btapi/facade/FtpCreateRequest.java new file mode 100644 index 0000000..abf3f32 --- /dev/null +++ b/src/main/java/net/heimeng/sdk/btapi/facade/FtpCreateRequest.java @@ -0,0 +1,30 @@ +package net.heimeng.sdk.btapi.facade; + +/** + * Immutable request model for FTP account creation. + * + * @param username FTP account username + * @param password FTP account password + * @param path FTP home directory + * @param remark panel-visible remark + */ +public record FtpCreateRequest(String username, String password, String path, String remark) { + + public FtpCreateRequest { + username = requireNonBlank(username, "username"); + password = requireNonBlank(password, "password"); + path = requireNonBlank(path, "path"); + remark = requireNonBlank(remark, "remark"); + } + + public static FtpCreateRequest of(String username, String password, String path) { + return new FtpCreateRequest(username, password, path, username); + } + + private static String requireNonBlank(String value, String fieldName) { + if (value == null || value.isBlank()) { + throw new IllegalArgumentException(fieldName + " cannot be blank"); + } + return value; + } +} diff --git a/src/main/java/net/heimeng/sdk/btapi/facade/FtpDeleteRequest.java b/src/main/java/net/heimeng/sdk/btapi/facade/FtpDeleteRequest.java new file mode 100644 index 0000000..b46e97f --- /dev/null +++ b/src/main/java/net/heimeng/sdk/btapi/facade/FtpDeleteRequest.java @@ -0,0 +1,19 @@ +package net.heimeng.sdk.btapi.facade; + +/** + * Immutable request model for deleting an FTP account. + * + * @param accountId panel FTP account identifier + * @param username FTP account username + */ +public record FtpDeleteRequest(int accountId, String username) { + + public FtpDeleteRequest { + if (accountId <= 0) { + throw new IllegalArgumentException("accountId must be positive"); + } + if (username == null || username.isBlank()) { + throw new IllegalArgumentException("username cannot be blank"); + } + } +} diff --git a/src/main/java/net/heimeng/sdk/btapi/facade/FtpOperations.java b/src/main/java/net/heimeng/sdk/btapi/facade/FtpOperations.java new file mode 100644 index 0000000..9a2f6db --- /dev/null +++ b/src/main/java/net/heimeng/sdk/btapi/facade/FtpOperations.java @@ -0,0 +1,53 @@ +package net.heimeng.sdk.btapi.facade; + +import java.util.List; +import java.util.Objects; + +import net.heimeng.sdk.btapi.api.ftp.ChangeFtpPasswordApi; +import net.heimeng.sdk.btapi.api.ftp.CreateFtpAccountApi; +import net.heimeng.sdk.btapi.api.ftp.DeleteFtpAccountApi; +import net.heimeng.sdk.btapi.api.ftp.GetFtpAccountsApi; +import net.heimeng.sdk.btapi.client.BtClient; +import net.heimeng.sdk.btapi.model.BtResult; +import net.heimeng.sdk.btapi.model.ftp.FtpAccount; + +/** + * FTP 相关能力的门面入口。 + * + *

封装 FTP 账号的列表查询、创建、删除和密码修改等常用操作。 + */ +public final class FtpOperations extends AbstractOperations { + + public FtpOperations(BtClient client) { + super(client); + } + + public BtResult> list() { + return execute(new GetFtpAccountsApi()); + } + + public BtResult create(FtpCreateRequest request) { + Objects.requireNonNull(request, "request cannot be null"); + return execute( + new CreateFtpAccountApi() + .setUsername(request.username()) + .setPassword(request.password()) + .setPath(request.path()) + .setRemark(request.remark())); + } + + public BtResult delete(FtpDeleteRequest request) { + Objects.requireNonNull(request, "request cannot be null"); + return execute(new DeleteFtpAccountApi().setId(request.accountId()).setUsername(request.username())); + } + + public BtResult updatePassword(FtpPasswordUpdateRequest request) { + Objects.requireNonNull(request, "request cannot be null"); + return execute( + new ChangeFtpPasswordApi() + .setId(request.accountId()) + .setUsername(request.username()) + .setNewPassword(request.newPassword()) + .setPath(request.path())); + } +} diff --git a/src/main/java/net/heimeng/sdk/btapi/facade/FtpPasswordUpdateRequest.java b/src/main/java/net/heimeng/sdk/btapi/facade/FtpPasswordUpdateRequest.java new file mode 100644 index 0000000..de27cf6 --- /dev/null +++ b/src/main/java/net/heimeng/sdk/btapi/facade/FtpPasswordUpdateRequest.java @@ -0,0 +1,28 @@ +package net.heimeng.sdk.btapi.facade; + +/** + * Immutable request model for FTP password changes. + * + * @param accountId panel FTP account identifier + * @param username FTP account username + * @param path FTP home directory currently associated with the account + * @param newPassword new password to apply + */ +public record FtpPasswordUpdateRequest( + int accountId, String username, String path, String newPassword) { + + public FtpPasswordUpdateRequest { + if (accountId <= 0) { + throw new IllegalArgumentException("accountId must be positive"); + } + if (username == null || username.isBlank()) { + throw new IllegalArgumentException("username cannot be blank"); + } + if (path == null || path.isBlank()) { + throw new IllegalArgumentException("path cannot be blank"); + } + if (newPassword == null || newPassword.isBlank()) { + throw new IllegalArgumentException("newPassword cannot be blank"); + } + } +} diff --git a/src/main/java/net/heimeng/sdk/btapi/facade/WebsiteCreateRequest.java b/src/main/java/net/heimeng/sdk/btapi/facade/WebsiteCreateRequest.java new file mode 100644 index 0000000..4920322 --- /dev/null +++ b/src/main/java/net/heimeng/sdk/btapi/facade/WebsiteCreateRequest.java @@ -0,0 +1,149 @@ +package net.heimeng.sdk.btapi.facade; + +import java.util.Objects; + +/** + * Immutable request model for website creation through the facade layer. + * + *

The builder keeps the common path concise while still allowing optional FTP and database + * provisioning to be expressed explicitly. + */ +public record WebsiteCreateRequest( + String domain, + String path, + int typeId, + String projectType, + String phpVersion, + int port, + String remark, + FtpAccount ftpAccount, + Database database) { + + private static final String DEFAULT_PROJECT_TYPE = "PHP"; + private static final int DEFAULT_TYPE_ID = 0; + private static final int DEFAULT_PORT = 80; + + public WebsiteCreateRequest { + domain = requireNonBlank(domain, "domain"); + path = requireNonBlank(path, "path"); + if (typeId < 0) { + throw new IllegalArgumentException("typeId cannot be negative"); + } + projectType = requireNonBlank(projectType, "projectType"); + phpVersion = requireNonBlank(phpVersion, "phpVersion"); + if (port <= 0) { + throw new IllegalArgumentException("port must be positive"); + } + remark = requireNonBlank(remark, "remark"); + } + + public static Builder builder(String domain, String path) { + return new Builder(domain, path); + } + + public boolean createFtp() { + return ftpAccount != null; + } + + public boolean createDatabase() { + return database != null; + } + + public record FtpAccount(String username, String password) { + + public FtpAccount { + username = requireNonBlank(username, "ftp username"); + password = requireNonBlank(password, "ftp password"); + } + } + + public record Database(String username, String password, String charset) { + + public Database { + username = requireNonBlank(username, "database username"); + password = requireNonBlank(password, "database password"); + charset = requireNonBlank(charset, "database charset"); + } + } + + public static final class Builder { + + private final String domain; + private final String path; + + private int typeId = DEFAULT_TYPE_ID; + private String projectType = DEFAULT_PROJECT_TYPE; + private String phpVersion; + private int port = DEFAULT_PORT; + private String remark; + private FtpAccount ftpAccount; + private Database database; + + private Builder(String domain, String path) { + this.domain = requireNonBlank(domain, "domain"); + this.path = requireNonBlank(path, "path"); + this.remark = this.domain; + } + + public Builder typeId(int typeId) { + if (typeId < 0) { + throw new IllegalArgumentException("typeId cannot be negative"); + } + this.typeId = typeId; + return this; + } + + public Builder projectType(String projectType) { + this.projectType = requireNonBlank(projectType, "projectType"); + return this; + } + + public Builder phpVersion(String phpVersion) { + this.phpVersion = requireNonBlank(phpVersion, "phpVersion"); + return this; + } + + public Builder port(int port) { + if (port <= 0) { + throw new IllegalArgumentException("port must be positive"); + } + this.port = port; + return this; + } + + public Builder remark(String remark) { + this.remark = requireNonBlank(remark, "remark"); + return this; + } + + public Builder ftpAccount(String username, String password) { + return ftpAccount(new FtpAccount(username, password)); + } + + public Builder ftpAccount(FtpAccount ftpAccount) { + this.ftpAccount = Objects.requireNonNull(ftpAccount, "ftpAccount cannot be null"); + return this; + } + + public Builder database(String username, String password, String charset) { + return database(new Database(username, password, charset)); + } + + public Builder database(Database database) { + this.database = Objects.requireNonNull(database, "database cannot be null"); + return this; + } + + public WebsiteCreateRequest build() { + return new WebsiteCreateRequest( + domain, path, typeId, projectType, phpVersion, port, remark, ftpAccount, database); + } + } + + private static String requireNonBlank(String value, String fieldName) { + if (value == null || value.isBlank()) { + throw new IllegalArgumentException(fieldName + " cannot be blank"); + } + return value; + } +} diff --git a/src/main/java/net/heimeng/sdk/btapi/facade/WebsiteDeleteOptions.java b/src/main/java/net/heimeng/sdk/btapi/facade/WebsiteDeleteOptions.java new file mode 100644 index 0000000..6432949 --- /dev/null +++ b/src/main/java/net/heimeng/sdk/btapi/facade/WebsiteDeleteOptions.java @@ -0,0 +1,19 @@ +package net.heimeng.sdk.btapi.facade; + +/** + * Website deletion behavior options. + * + * @param deleteFtp whether the related FTP account should also be deleted + * @param deleteDatabase whether the related database should also be deleted + * @param deletePath whether the website directory should also be deleted + */ +public record WebsiteDeleteOptions(boolean deleteFtp, boolean deleteDatabase, boolean deletePath) { + + public static WebsiteDeleteOptions none() { + return new WebsiteDeleteOptions(false, false, false); + } + + public static WebsiteDeleteOptions deleteAll() { + return new WebsiteDeleteOptions(true, true, true); + } +} diff --git a/src/main/java/net/heimeng/sdk/btapi/facade/WebsiteDomainBinding.java b/src/main/java/net/heimeng/sdk/btapi/facade/WebsiteDomainBinding.java new file mode 100644 index 0000000..919d3f0 --- /dev/null +++ b/src/main/java/net/heimeng/sdk/btapi/facade/WebsiteDomainBinding.java @@ -0,0 +1,24 @@ +package net.heimeng.sdk.btapi.facade; + +import java.util.Objects; + +/** + * Website domain binding request. + * + * @param websiteName website display or primary domain name expected by BT Panel + * @param domain domain to bind + */ +public record WebsiteDomainBinding(String websiteName, String domain) { + + public WebsiteDomainBinding { + requireNonBlank(websiteName, "websiteName"); + requireNonBlank(domain, "domain"); + } + + private static void requireNonBlank(String value, String fieldName) { + Objects.requireNonNull(value, fieldName + " cannot be null"); + if (value.isBlank()) { + throw new IllegalArgumentException(fieldName + " cannot be blank"); + } + } +} diff --git a/src/main/java/net/heimeng/sdk/btapi/facade/WebsiteDomainRemoval.java b/src/main/java/net/heimeng/sdk/btapi/facade/WebsiteDomainRemoval.java new file mode 100644 index 0000000..014b094 --- /dev/null +++ b/src/main/java/net/heimeng/sdk/btapi/facade/WebsiteDomainRemoval.java @@ -0,0 +1,18 @@ +package net.heimeng.sdk.btapi.facade; + +/** + * Website domain removal request. + * + * @param websiteName website display or primary domain name expected by BT Panel + * @param domain domain to remove + * @param port bound port that should be removed + */ +public record WebsiteDomainRemoval(String websiteName, String domain, int port) { + + public WebsiteDomainRemoval { + new WebsiteDomainBinding(websiteName, domain); + if (port <= 0) { + throw new IllegalArgumentException("port must be positive"); + } + } +} diff --git a/src/main/java/net/heimeng/sdk/btapi/facade/WebsiteLimitNetOptions.java b/src/main/java/net/heimeng/sdk/btapi/facade/WebsiteLimitNetOptions.java new file mode 100644 index 0000000..324b9a5 --- /dev/null +++ b/src/main/java/net/heimeng/sdk/btapi/facade/WebsiteLimitNetOptions.java @@ -0,0 +1,29 @@ +package net.heimeng.sdk.btapi.facade; + +/** + * Website traffic limiting options. + * + * @param enabled whether rate limiting is enabled + * @param perServer maximum concurrent connections per server + * @param perIp maximum concurrent connections per IP + * @param limitRate bandwidth limit + */ +public record WebsiteLimitNetOptions( + boolean enabled, Integer perServer, Integer perIp, Integer limitRate) { + + public WebsiteLimitNetOptions { + validateNonNegative(perServer, "perServer"); + validateNonNegative(perIp, "perIp"); + validateNonNegative(limitRate, "limitRate"); + } + + public static WebsiteLimitNetOptions disabled() { + return new WebsiteLimitNetOptions(false, null, null, null); + } + + private static void validateNonNegative(Integer value, String fieldName) { + if (value != null && value < 0) { + throw new IllegalArgumentException(fieldName + " cannot be negative"); + } + } +} diff --git a/src/main/java/net/heimeng/sdk/btapi/facade/WebsiteNginxConfigOptions.java b/src/main/java/net/heimeng/sdk/btapi/facade/WebsiteNginxConfigOptions.java new file mode 100644 index 0000000..1c5688c --- /dev/null +++ b/src/main/java/net/heimeng/sdk/btapi/facade/WebsiteNginxConfigOptions.java @@ -0,0 +1,24 @@ +package net.heimeng.sdk.btapi.facade; + +import java.util.Objects; + +/** + * Website Nginx config update options. + * + * @param domain website domain whose config should be updated + * @param content config content, may be blank to clear content + */ +public record WebsiteNginxConfigOptions(String domain, String content) { + + public WebsiteNginxConfigOptions { + requireNonBlank(domain, "domain"); + Objects.requireNonNull(content, "content cannot be null"); + } + + private static void requireNonBlank(String value, String fieldName) { + Objects.requireNonNull(value, fieldName + " cannot be null"); + if (value.isBlank()) { + throw new IllegalArgumentException(fieldName + " cannot be blank"); + } + } +} diff --git a/src/main/java/net/heimeng/sdk/btapi/facade/WebsiteOperations.java b/src/main/java/net/heimeng/sdk/btapi/facade/WebsiteOperations.java new file mode 100644 index 0000000..97f78b7 --- /dev/null +++ b/src/main/java/net/heimeng/sdk/btapi/facade/WebsiteOperations.java @@ -0,0 +1,303 @@ +package net.heimeng.sdk.btapi.facade; + +import java.util.List; +import java.util.Map; +import java.util.Objects; + +import net.heimeng.sdk.btapi.api.website.AddWebsiteDomainApi; +import net.heimeng.sdk.btapi.api.website.CloseWebsitePasswordApi; +import net.heimeng.sdk.btapi.api.website.CloseWebsiteSslApi; +import net.heimeng.sdk.btapi.api.website.CreateWebsiteApi; +import net.heimeng.sdk.btapi.api.website.CreateWebsiteBackupApi; +import net.heimeng.sdk.btapi.api.website.DeleteWebsiteApi; +import net.heimeng.sdk.btapi.api.website.DeleteWebsiteBackupApi; +import net.heimeng.sdk.btapi.api.website.DeleteWebsiteDomainApi; +import net.heimeng.sdk.btapi.api.website.GetPhpVersionsApi; +import net.heimeng.sdk.btapi.api.website.GetWebsiteBackupsApi; +import net.heimeng.sdk.btapi.api.website.GetWebsiteConfigApi; +import net.heimeng.sdk.btapi.api.website.GetWebsiteDetailApi; +import net.heimeng.sdk.btapi.api.website.GetWebsiteDomainsApi; +import net.heimeng.sdk.btapi.api.website.GetWebsiteLimitNetApi; +import net.heimeng.sdk.btapi.api.website.GetWebsiteListApi; +import net.heimeng.sdk.btapi.api.website.GetWebsiteNginxConfigApi; +import net.heimeng.sdk.btapi.api.website.GetWebsitePhpExtensionsApi; +import net.heimeng.sdk.btapi.api.website.GetWebsitePhpVersionApi; +import net.heimeng.sdk.btapi.api.website.GetWebsiteRewriteRulesApi; +import net.heimeng.sdk.btapi.api.website.GetWebsiteRootPathApi; +import net.heimeng.sdk.btapi.api.website.GetWebsiteSslListApi; +import net.heimeng.sdk.btapi.api.website.GetWebsiteTypesApi; +import net.heimeng.sdk.btapi.api.website.GetWebsitesApi; +import net.heimeng.sdk.btapi.api.website.SetWebsiteLimitNetApi; +import net.heimeng.sdk.btapi.api.website.SetWebsiteLogsApi; +import net.heimeng.sdk.btapi.api.website.SetWebsiteNginxConfigApi; +import net.heimeng.sdk.btapi.api.website.SetWebsitePasswordApi; +import net.heimeng.sdk.btapi.api.website.SetWebsitePhpExtensionsApi; +import net.heimeng.sdk.btapi.api.website.SetWebsitePhpVersionApi; +import net.heimeng.sdk.btapi.api.website.SetWebsitePsApi; +import net.heimeng.sdk.btapi.api.website.SetWebsiteRewriteRulesApi; +import net.heimeng.sdk.btapi.api.website.SetWebsiteRootPathApi; +import net.heimeng.sdk.btapi.api.website.SetWebsiteRunPathApi; +import net.heimeng.sdk.btapi.api.website.SetWebsiteSslApi; +import net.heimeng.sdk.btapi.api.website.SetWebsiteUserIniApi; +import net.heimeng.sdk.btapi.api.website.StartWebsiteApi; +import net.heimeng.sdk.btapi.api.website.StopWebsiteApi; +import net.heimeng.sdk.btapi.client.BtClient; +import net.heimeng.sdk.btapi.model.BtResult; +import net.heimeng.sdk.btapi.model.website.CreateWebsiteResult; +import net.heimeng.sdk.btapi.model.website.PhpVersion; +import net.heimeng.sdk.btapi.model.website.WebsiteInfo; +import net.heimeng.sdk.btapi.model.website.WebsiteType; + +/** + * 网站相关能力的门面入口。 + * + *

该门面覆盖网站生命周期管理、域名与配置维护、PHP 设置、限流和备份等常用能力, 适合作为上层业务访问网站接口的统一入口。 + */ +public final class WebsiteOperations extends AbstractOperations { + + public WebsiteOperations(BtClient client) { + super(client); + } + + public BtResult> list() { + return execute(new GetWebsitesApi()); + } + + public BtResult> list(int page, int limit) { + return execute(new GetWebsitesApi(page, limit)); + } + + public BtResult>> listRaw( + Integer page, Integer limit, Integer type, String order, String search) { + return execute( + new GetWebsiteListApi() + .setPage(page) + .setLimit(limit) + .setType(type) + .setOrder(order) + .setSearch(search)); + } + + public BtResult> listTypes() { + return execute(new GetWebsiteTypesApi()); + } + + public BtResult> listPhpVersions() { + return execute(new GetPhpVersionsApi()); + } + + public BtResult create(CreateWebsiteApi api) { + Objects.requireNonNull(api, "api cannot be null"); + return execute(api); + } + + public BtResult create(WebsiteCreateRequest request) { + Objects.requireNonNull(request, "request cannot be null"); + + CreateWebsiteApi api = + new CreateWebsiteApi( + request.domain(), + request.path(), + request.typeId(), + request.phpVersion(), + request.port(), + request.remark()); + api.setType(request.projectType()); + + if (request.ftpAccount() != null) { + api.setFtpCredentials(request.ftpAccount().username(), request.ftpAccount().password()); + } + if (request.database() != null) { + api.setDatabaseCredentials( + request.database().username(), + request.database().password(), + request.database().charset()); + } + + return execute(api); + } + + public BtResult> getDetail(int id) { + return execute(new GetWebsiteDetailApi().setId(id)); + } + + public BtResult> getConfig(Integer id, String path) { + return execute(new GetWebsiteConfigApi().setId(id).setPath(path)); + } + + public BtResult>> listDomains(int siteId) { + return execute(new GetWebsiteDomainsApi().setSiteId(siteId)); + } + + public BtResult addDomain(int siteId, WebsiteDomainBinding binding) { + Objects.requireNonNull(binding, "binding cannot be null"); + return execute( + new AddWebsiteDomainApi() + .setId(siteId) + .setWebname(binding.websiteName()) + .setDomain(binding.domain())); + } + + public BtResult removeDomain(int siteId, WebsiteDomainRemoval removal) { + Objects.requireNonNull(removal, "removal cannot be null"); + return execute( + new DeleteWebsiteDomainApi() + .setId(siteId) + .setWebname(removal.websiteName()) + .setDomain(removal.domain()) + .setPort(removal.port())); + } + + public BtResult delete(int siteId, String websiteName, WebsiteDeleteOptions options) { + Objects.requireNonNull(options, "options cannot be null"); + return execute( + new DeleteWebsiteApi(siteId, websiteName) + .setDeleteFtp(options.deleteFtp()) + .setDeleteDatabase(options.deleteDatabase()) + .setDeletePath(options.deletePath())); + } + + public BtResult start(int id) { + return execute(new StartWebsiteApi().setId(id)); + } + + public BtResult stop(int id) { + return execute(new StopWebsiteApi().setId(id)); + } + + public BtResult updateRemark(int siteId, String remark) { + return execute(new SetWebsitePsApi().setId(siteId).setPs(remark)); + } + + public BtResult getRootPath(int id) { + return execute(new GetWebsiteRootPathApi().setId(id)); + } + + public BtResult updateRootPath(int siteId, String path) { + return execute(new SetWebsiteRootPathApi().setId(siteId).setPath(path)); + } + + public BtResult updateRunPath(int siteId, String runPath) { + return execute(new SetWebsiteRunPathApi().setId(siteId).setRunPath(runPath)); + } + + public BtResult toggleUserIni(String path) { + return execute(new SetWebsiteUserIniApi().setPath(path)); + } + + public BtResult getPhpVersion(int id) { + return execute(new GetWebsitePhpVersionApi().setId(id)); + } + + public BtResult updatePhpVersion(int siteId, String phpVersion) { + return execute(new SetWebsitePhpVersionApi().setId(siteId).setPhpVersion(phpVersion)); + } + + public BtResult>> listPhpExtensions(int id) { + return execute(new GetWebsitePhpExtensionsApi().setId(id)); + } + + public BtResult updatePhpExtension(int siteId, String moduleName, boolean enabled) { + return execute( + new SetWebsitePhpExtensionsApi() + .setId(siteId) + .setModuleName(moduleName) + .setEnabled(enabled)); + } + + public BtResult getRewriteRules(int id) { + return execute(new GetWebsiteRewriteRulesApi().setId(id)); + } + + public BtResult updateRewriteRules(int siteId, WebsiteRewriteRulesOptions options) { + Objects.requireNonNull(options, "options cannot be null"); + return execute( + new SetWebsiteRewriteRulesApi() + .setId(siteId) + .setName(options.name()) + .setContent(options.content())); + } + + public BtResult getNginxConfig(Integer id, String domain) { + return execute(new GetWebsiteNginxConfigApi().setId(id).setDomain(domain)); + } + + public BtResult updateNginxConfig(Integer siteId, WebsiteNginxConfigOptions options) { + Objects.requireNonNull(options, "options cannot be null"); + return execute( + new SetWebsiteNginxConfigApi() + .setId(siteId) + .setDomain(options.domain()) + .setContent(options.content())); + } + + public BtResult enablePasswordProtection( + int siteId, WebsitePasswordProtectionOptions options) { + Objects.requireNonNull(options, "options cannot be null"); + return execute( + new SetWebsitePasswordApi() + .setId(siteId) + .setUsername(options.username()) + .setPassword(options.password())); + } + + public BtResult disablePasswordProtection(int siteId) { + return execute(new CloseWebsitePasswordApi().setId(siteId)); + } + + public BtResult installSslCertificate(int siteId, WebsiteSslCertificateOptions options) { + Objects.requireNonNull(options, "options cannot be null"); + return execute( + new SetWebsiteSslApi() + .setId(siteId) + .setDomain(options.domain()) + .setCert(options.certificate()) + .setKey(options.privateKey()) + .setForceHttps(options.forceHttps())); + } + + public BtResult disableSsl(int siteId) { + return execute(new CloseWebsiteSslApi().setId(siteId)); + } + + public BtResult>> listSslCertificates(int id) { + return execute(new GetWebsiteSslListApi().setId(id)); + } + + public BtResult toggleLogs(int siteId) { + return execute(new SetWebsiteLogsApi().setId(siteId)); + } + + public BtResult> getLimitNet(int id) { + return execute(new GetWebsiteLimitNetApi().setId(id)); + } + + public BtResult updateLimitNet(int siteId, WebsiteLimitNetOptions options) { + Objects.requireNonNull(options, "options cannot be null"); + return execute( + new SetWebsiteLimitNetApi() + .setId(siteId) + .setEnabled(options.enabled()) + .setPerserver(options.perServer()) + .setPerip(options.perIp()) + .setLimitRate(options.limitRate())); + } + + public BtResult>> listBackups( + Integer siteId, Integer page, Integer limit, String callback) { + return execute( + new GetWebsiteBackupsApi() + .setPage(page) + .setLimit(limit) + .setSiteId(siteId) + .setCallback(callback)); + } + + public BtResult createBackup(int id) { + return execute(new CreateWebsiteBackupApi().setId(id)); + } + + public BtResult deleteBackup(int id) { + return execute(new DeleteWebsiteBackupApi().setId(id)); + } +} diff --git a/src/main/java/net/heimeng/sdk/btapi/facade/WebsitePasswordProtectionOptions.java b/src/main/java/net/heimeng/sdk/btapi/facade/WebsitePasswordProtectionOptions.java new file mode 100644 index 0000000..956ca93 --- /dev/null +++ b/src/main/java/net/heimeng/sdk/btapi/facade/WebsitePasswordProtectionOptions.java @@ -0,0 +1,24 @@ +package net.heimeng.sdk.btapi.facade; + +import java.util.Objects; + +/** + * Website password protection options. + * + * @param username protected area username + * @param password protected area password + */ +public record WebsitePasswordProtectionOptions(String username, String password) { + + public WebsitePasswordProtectionOptions { + requireNonBlank(username, "username"); + requireNonBlank(password, "password"); + } + + private static void requireNonBlank(String value, String fieldName) { + Objects.requireNonNull(value, fieldName + " cannot be null"); + if (value.isBlank()) { + throw new IllegalArgumentException(fieldName + " cannot be blank"); + } + } +} diff --git a/src/main/java/net/heimeng/sdk/btapi/facade/WebsiteRewriteRulesOptions.java b/src/main/java/net/heimeng/sdk/btapi/facade/WebsiteRewriteRulesOptions.java new file mode 100644 index 0000000..e7c037e --- /dev/null +++ b/src/main/java/net/heimeng/sdk/btapi/facade/WebsiteRewriteRulesOptions.java @@ -0,0 +1,24 @@ +package net.heimeng.sdk.btapi.facade; + +import java.util.Objects; + +/** + * Website rewrite rules update options. + * + * @param name rewrite template name + * @param content rewrite content, may be blank to clear current content + */ +public record WebsiteRewriteRulesOptions(String name, String content) { + + public WebsiteRewriteRulesOptions { + requireNonBlank(name, "name"); + Objects.requireNonNull(content, "content cannot be null"); + } + + private static void requireNonBlank(String value, String fieldName) { + Objects.requireNonNull(value, fieldName + " cannot be null"); + if (value.isBlank()) { + throw new IllegalArgumentException(fieldName + " cannot be blank"); + } + } +} diff --git a/src/main/java/net/heimeng/sdk/btapi/facade/WebsiteSslCertificateOptions.java b/src/main/java/net/heimeng/sdk/btapi/facade/WebsiteSslCertificateOptions.java new file mode 100644 index 0000000..7cc1947 --- /dev/null +++ b/src/main/java/net/heimeng/sdk/btapi/facade/WebsiteSslCertificateOptions.java @@ -0,0 +1,28 @@ +package net.heimeng.sdk.btapi.facade; + +import java.util.Objects; + +/** + * Website SSL installation options. + * + * @param domain website domain name + * @param certificate PEM certificate content + * @param privateKey PEM private key content + * @param forceHttps whether HTTPS redirection should be enabled + */ +public record WebsiteSslCertificateOptions( + String domain, String certificate, String privateKey, Boolean forceHttps) { + + public WebsiteSslCertificateOptions { + requireNonBlank(domain, "domain"); + requireNonBlank(certificate, "certificate"); + requireNonBlank(privateKey, "privateKey"); + } + + private static void requireNonBlank(String value, String fieldName) { + Objects.requireNonNull(value, fieldName + " cannot be null"); + if (value.isBlank()) { + throw new IllegalArgumentException(fieldName + " cannot be blank"); + } + } +} diff --git a/src/test/java/net/heimeng/sdk/btapi/api/website/GetPhpVersionsApiTest.java b/src/test/java/net/heimeng/sdk/btapi/api/website/GetPhpVersionsApiTest.java new file mode 100644 index 0000000..1c425a1 --- /dev/null +++ b/src/test/java/net/heimeng/sdk/btapi/api/website/GetPhpVersionsApiTest.java @@ -0,0 +1,77 @@ +package net.heimeng.sdk.btapi.api.website; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.List; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import net.heimeng.sdk.btapi.api.BtApi; +import net.heimeng.sdk.btapi.exception.BtApiException; +import net.heimeng.sdk.btapi.model.BtResult; +import net.heimeng.sdk.btapi.model.website.PhpVersion; + +@DisplayName("GetPhpVersionsApi 单元测试") +class GetPhpVersionsApiTest { + + @Test + @DisplayName("应暴露正确的接口元数据") + void exposesMetadata() { + GetPhpVersionsApi api = new GetPhpVersionsApi(); + + assertEquals("site?action=GetPHPVersion", api.getEndpoint()); + assertEquals(BtApi.HttpMethod.POST, api.getMethod()); + } + + @Test + @DisplayName("应正确解析直接返回数组的 PHP 版本响应") + void parsesRawArrayPayload() { + GetPhpVersionsApi api = new GetPhpVersionsApi(); + BtResult> result = + api.parseResponse("[{\"version\":\"82\",\"name\":\"PHP-82\"}]"); + + assertTrue(result.isSuccess()); + assertEquals(1, result.getData().size()); + assertEquals("82", result.getData().get(0).getVersion()); + assertEquals("PHP-82", result.getData().get(0).getName()); + } + + @Test + @DisplayName("应正确解析包装后的 PHP 版本响应") + void parsesWrappedArrayPayload() { + GetPhpVersionsApi api = new GetPhpVersionsApi(); + BtResult> result = + api.parseResponse("{\"msg\":\"ok\",\"data\":[{\"version\":\"00\",\"name\":\"纯静态\"}]}"); + + assertTrue(result.isSuccess()); + assertEquals("ok", result.getMsg()); + assertEquals(1, result.getData().size()); + } + + @Test + @DisplayName("应保留失败包装响应的状态与消息") + void preservesFailurePayload() { + GetPhpVersionsApi api = new GetPhpVersionsApi(); + BtResult> result = + api.parseResponse("{\"status\":false,\"msg\":\"unsupported\"}"); + + assertFalse(result.isSuccess()); + assertEquals("unsupported", result.getMsg()); + assertTrue(result.getData().isEmpty()); + } + + @Test + @DisplayName("成功包装响应缺少 data 数组时应抛出异常") + void rejectsMissingDataField() { + GetPhpVersionsApi api = new GetPhpVersionsApi(); + + BtApiException exception = + assertThrows(BtApiException.class, () -> api.parseResponse("{\"status\":true}")); + + assertTrue(exception.getMessage().contains("data array")); + } +} diff --git a/src/test/java/net/heimeng/sdk/btapi/api/website/GetWebsiteBackupsApiTest.java b/src/test/java/net/heimeng/sdk/btapi/api/website/GetWebsiteBackupsApiTest.java new file mode 100644 index 0000000..e141447 --- /dev/null +++ b/src/test/java/net/heimeng/sdk/btapi/api/website/GetWebsiteBackupsApiTest.java @@ -0,0 +1,80 @@ +package net.heimeng.sdk.btapi.api.website; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.lang.reflect.Method; +import java.util.List; +import java.util.Map; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import net.heimeng.sdk.btapi.api.BtApi; +import net.heimeng.sdk.btapi.exception.BtApiException; +import net.heimeng.sdk.btapi.model.BtResult; + +@DisplayName("GetWebsiteBackupsApi unit tests") +class GetWebsiteBackupsApiTest { + + @Test + @DisplayName("should expose metadata and validate params") + void exposesMetadataAndValidatesParams() { + GetWebsiteBackupsApi api = new GetWebsiteBackupsApi().setPage(1).setLimit(20).setSiteId(8); + + assertEquals("data?action=getData&table=backup", api.getEndpoint()); + assertEquals(BtApi.HttpMethod.POST, api.getMethod()); + assertEquals(1, api.getParams().get("p")); + assertEquals(20, api.getParams().get("limit")); + assertEquals(8, api.getParams().get("search")); + assertTrue(invokeValidate(api)); + assertFalse(invokeValidate(new GetWebsiteBackupsApi().setLimit(20).setSiteId(0))); + } + + @Test + @DisplayName("should parse wrapped backup list payload") + void parsesWrappedBackupPayload() { + GetWebsiteBackupsApi api = new GetWebsiteBackupsApi(); + BtResult>> result = + api.parseResponse("{\"msg\":\"ok\",\"data\":[{\"name\":\"backup.tar.gz\"}]}"); + + assertTrue(result.isSuccess()); + assertEquals("ok", result.getMsg()); + assertEquals(List.of(Map.of("name", "backup.tar.gz")), result.getData()); + } + + @Test + @DisplayName("should preserve wrapped failure payload") + void preservesFailurePayload() { + GetWebsiteBackupsApi api = new GetWebsiteBackupsApi(); + BtResult>> result = + api.parseResponse("{\"status\":false,\"msg\":\"busy\"}"); + + assertFalse(result.isSuccess()); + assertEquals("busy", result.getMsg()); + assertTrue(result.getData().isEmpty()); + } + + @Test + @DisplayName("missing data array should throw") + void rejectsMissingDataArray() { + GetWebsiteBackupsApi api = new GetWebsiteBackupsApi(); + + BtApiException exception = + assertThrows(BtApiException.class, () -> api.parseResponse("{\"status\":true}")); + + assertTrue(exception.getMessage().contains("data array")); + } + + private boolean invokeValidate(GetWebsiteBackupsApi api) { + try { + Method method = GetWebsiteBackupsApi.class.getDeclaredMethod("validateParams"); + method.setAccessible(true); + return (boolean) method.invoke(api); + } catch (ReflectiveOperationException exception) { + throw new AssertionError(exception); + } + } +} diff --git a/src/test/java/net/heimeng/sdk/btapi/api/website/GetWebsiteConfigApiTest.java b/src/test/java/net/heimeng/sdk/btapi/api/website/GetWebsiteConfigApiTest.java new file mode 100644 index 0000000..2186826 --- /dev/null +++ b/src/test/java/net/heimeng/sdk/btapi/api/website/GetWebsiteConfigApiTest.java @@ -0,0 +1,106 @@ +package net.heimeng.sdk.btapi.api.website; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.lang.reflect.Method; +import java.util.List; +import java.util.Map; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import net.heimeng.sdk.btapi.api.BtApi; +import net.heimeng.sdk.btapi.exception.BtApiException; +import net.heimeng.sdk.btapi.model.BtResult; + +@DisplayName("GetWebsiteConfigApi 单元测试") +class GetWebsiteConfigApiTest { + + @Test + @DisplayName("应暴露正确的接口元数据并校验参数") + void exposesMetadataAndValidatesParams() { + GetWebsiteConfigApi api = new GetWebsiteConfigApi().setId(8).setPath("/www/wwwroot/demo"); + + assertEquals("site?action=GetDirUserINI", api.getEndpoint()); + assertEquals(BtApi.HttpMethod.POST, api.getMethod()); + assertEquals(8, api.getParams().get("id")); + assertEquals("/www/wwwroot/demo", api.getParams().get("path")); + assertTrue(invokeValidate(api)); + assertFalse(invokeValidate(new GetWebsiteConfigApi().setId(8))); + } + + @Test + @DisplayName("应正确解析原始配置对象响应") + void parsesRawConfigPayload() { + GetWebsiteConfigApi api = new GetWebsiteConfigApi(); + BtResult> result = + api.parseResponse( + """ + { + "pass": false, + "logs": true, + "userini": true, + "runPath": { + "dirs": ["/", "/public"], + "runPath": "/" + } + } + """); + + assertTrue(result.isSuccess()); + assertEquals(false, result.getData().get("pass")); + assertEquals(true, result.getData().get("logs")); + assertEquals( + Map.of("dirs", List.of("/", "/public"), "runPath", "/"), result.getData().get("runPath")); + } + + @Test + @DisplayName("应正确解析包装后的配置对象响应") + void parsesWrappedConfigPayload() { + GetWebsiteConfigApi api = new GetWebsiteConfigApi(); + BtResult> result = + api.parseResponse("{\"status\":true,\"msg\":\"ok\",\"data\":{\"logs\":false}}"); + + assertTrue(result.isSuccess()); + assertEquals("ok", result.getMsg()); + assertEquals(false, result.getData().get("logs")); + } + + @Test + @DisplayName("应保留失败包装响应的状态与消息") + void preservesFailurePayload() { + GetWebsiteConfigApi api = new GetWebsiteConfigApi(); + BtResult> result = + api.parseResponse("{\"status\":false,\"msg\":\"denied\"}"); + + assertFalse(result.isSuccess()); + assertEquals("denied", result.getMsg()); + assertTrue(result.getData().isEmpty()); + } + + @Test + @DisplayName("data 字段不是对象时应抛出异常") + void rejectsNonObjectDataPayload() { + GetWebsiteConfigApi api = new GetWebsiteConfigApi(); + + BtApiException exception = + assertThrows( + BtApiException.class, + () -> api.parseResponse("{\"status\":true,\"data\":\"invalid\"}")); + + assertTrue(exception.getMessage().contains("must be a JSON object")); + } + + private boolean invokeValidate(GetWebsiteConfigApi api) { + try { + Method method = GetWebsiteConfigApi.class.getDeclaredMethod("validateParams"); + method.setAccessible(true); + return (boolean) method.invoke(api); + } catch (ReflectiveOperationException exception) { + throw new AssertionError(exception); + } + } +} diff --git a/src/test/java/net/heimeng/sdk/btapi/api/website/GetWebsiteDetailApiTest.java b/src/test/java/net/heimeng/sdk/btapi/api/website/GetWebsiteDetailApiTest.java new file mode 100644 index 0000000..9ad0a74 --- /dev/null +++ b/src/test/java/net/heimeng/sdk/btapi/api/website/GetWebsiteDetailApiTest.java @@ -0,0 +1,116 @@ +package net.heimeng.sdk.btapi.api.website; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.lang.reflect.Method; +import java.util.List; +import java.util.Map; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import net.heimeng.sdk.btapi.api.BtApi; +import net.heimeng.sdk.btapi.exception.BtApiException; +import net.heimeng.sdk.btapi.model.BtResult; + +@DisplayName("GetWebsiteDetailApi 单元测试") +class GetWebsiteDetailApiTest { + + @Test + @DisplayName("应暴露正确的接口元数据并校验参数") + void exposesMetadataAndValidatesParams() { + GetWebsiteDetailApi api = new GetWebsiteDetailApi().setId(8); + + assertEquals("site?action=GetSiteStatus", api.getEndpoint()); + assertEquals(BtApi.HttpMethod.POST, api.getMethod()); + assertEquals(8, api.getParams().get("id")); + assertTrue(invokeValidate(api)); + assertFalse(invokeValidate(new GetWebsiteDetailApi().setId(0))); + } + + @Test + @DisplayName("应正确解析带 data 包装的网站详情") + void parsesWrappedDetailPayload() { + GetWebsiteDetailApi api = new GetWebsiteDetailApi(); + BtResult> result = + api.parseResponse( + """ + { + "status": true, + "msg": "ok", + "data": { + "id": 1, + "name": "demo.example.com", + "flags": ["cdn"], + "runtime": { "php": "82" } + } + } + """); + + assertTrue(result.isSuccess()); + assertEquals("ok", result.getMsg()); + assertEquals(1, result.getData().get("id")); + assertEquals(List.of("cdn"), result.getData().get("flags")); + assertEquals(Map.of("php", "82"), result.getData().get("runtime")); + } + + @Test + @DisplayName("应兼容直接返回详情对象的响应") + void parsesDirectObjectPayload() { + GetWebsiteDetailApi api = new GetWebsiteDetailApi(); + BtResult> result = + api.parseResponse("{\"id\":2,\"name\":\"demo.example.com\"}"); + + assertTrue(result.isSuccess()); + assertEquals(2, result.getData().get("id")); + assertEquals("demo.example.com", result.getData().get("name")); + } + + @Test + @DisplayName("应保留失败包装响应的状态与消息") + void preservesFailurePayload() { + GetWebsiteDetailApi api = new GetWebsiteDetailApi(); + BtResult> result = + api.parseResponse("{\"status\":false,\"msg\":\"not found\"}"); + + assertFalse(result.isSuccess()); + assertEquals("not found", result.getMsg()); + assertTrue(result.getData().isEmpty()); + } + + @Test + @DisplayName("成功包装响应缺少详情载荷时应抛出异常") + void rejectsMissingPayload() { + GetWebsiteDetailApi api = new GetWebsiteDetailApi(); + + BtApiException exception = + assertThrows(BtApiException.class, () -> api.parseResponse("{\"status\":true}")); + + assertTrue(exception.getMessage().contains("detail payload")); + } + + @Test + @DisplayName("data 字段不是对象时应抛出异常") + void rejectsNonObjectDataPayload() { + GetWebsiteDetailApi api = new GetWebsiteDetailApi(); + + BtApiException exception = + assertThrows( + BtApiException.class, () -> api.parseResponse("{\"status\":true,\"data\":[]}")); + + assertTrue(exception.getMessage().contains("must be a JSON object")); + } + + private boolean invokeValidate(GetWebsiteDetailApi api) { + try { + Method method = GetWebsiteDetailApi.class.getDeclaredMethod("validateParams"); + method.setAccessible(true); + return (boolean) method.invoke(api); + } catch (ReflectiveOperationException exception) { + throw new AssertionError(exception); + } + } +} diff --git a/src/test/java/net/heimeng/sdk/btapi/api/website/GetWebsiteDomainsApiTest.java b/src/test/java/net/heimeng/sdk/btapi/api/website/GetWebsiteDomainsApiTest.java new file mode 100644 index 0000000..db907db --- /dev/null +++ b/src/test/java/net/heimeng/sdk/btapi/api/website/GetWebsiteDomainsApiTest.java @@ -0,0 +1,91 @@ +package net.heimeng.sdk.btapi.api.website; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.lang.reflect.Method; +import java.util.List; +import java.util.Map; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import net.heimeng.sdk.btapi.api.BtApi; +import net.heimeng.sdk.btapi.exception.BtApiException; +import net.heimeng.sdk.btapi.model.BtResult; + +@DisplayName("GetWebsiteDomainsApi 单元测试") +class GetWebsiteDomainsApiTest { + + @Test + @DisplayName("应暴露正确的接口元数据并校验参数") + void exposesMetadataAndValidatesParams() { + GetWebsiteDomainsApi api = new GetWebsiteDomainsApi().setSiteId(66); + + assertEquals("data?action=getData&table=domain", api.getEndpoint()); + assertEquals(BtApi.HttpMethod.POST, api.getMethod()); + assertEquals(true, api.getParams().get("list")); + assertEquals(66, api.getParams().get("search")); + assertTrue(invokeValidate(api)); + assertFalse(invokeValidate(new GetWebsiteDomainsApi().setSiteId(0))); + } + + @Test + @DisplayName("应正确解析直接返回数组的域名列表响应") + void parsesRawArrayPayload() { + GetWebsiteDomainsApi api = new GetWebsiteDomainsApi(); + BtResult>> result = + api.parseResponse("[{\"id\":73,\"name\":\"w1.hao.com\",\"port\":80}]"); + + assertTrue(result.isSuccess()); + assertEquals(1, result.getData().size()); + assertEquals("w1.hao.com", result.getData().get(0).get("name")); + } + + @Test + @DisplayName("应正确解析包装后的域名列表响应") + void parsesWrappedArrayPayload() { + GetWebsiteDomainsApi api = new GetWebsiteDomainsApi(); + BtResult>> result = + api.parseResponse("{\"msg\":\"ok\",\"data\":[{\"name\":\"demo.example.com\"}]}"); + + assertTrue(result.isSuccess()); + assertEquals("ok", result.getMsg()); + assertEquals(List.of(Map.of("name", "demo.example.com")), result.getData()); + } + + @Test + @DisplayName("应保留失败包装响应的状态与消息") + void preservesFailurePayload() { + GetWebsiteDomainsApi api = new GetWebsiteDomainsApi(); + BtResult>> result = + api.parseResponse("{\"status\":false,\"msg\":\"denied\"}"); + + assertFalse(result.isSuccess()); + assertEquals("denied", result.getMsg()); + assertTrue(result.getData().isEmpty()); + } + + @Test + @DisplayName("缺少 data 数组时应抛出异常") + void rejectsMissingDataField() { + GetWebsiteDomainsApi api = new GetWebsiteDomainsApi(); + + BtApiException exception = + assertThrows(BtApiException.class, () -> api.parseResponse("{\"status\":true}")); + + assertTrue(exception.getMessage().contains("data array")); + } + + private boolean invokeValidate(GetWebsiteDomainsApi api) { + try { + Method method = GetWebsiteDomainsApi.class.getDeclaredMethod("validateParams"); + method.setAccessible(true); + return (boolean) method.invoke(api); + } catch (ReflectiveOperationException exception) { + throw new AssertionError(exception); + } + } +} diff --git a/src/test/java/net/heimeng/sdk/btapi/api/website/GetWebsiteLimitNetApiTest.java b/src/test/java/net/heimeng/sdk/btapi/api/website/GetWebsiteLimitNetApiTest.java new file mode 100644 index 0000000..0b69272 --- /dev/null +++ b/src/test/java/net/heimeng/sdk/btapi/api/website/GetWebsiteLimitNetApiTest.java @@ -0,0 +1,92 @@ +package net.heimeng.sdk.btapi.api.website; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.lang.reflect.Method; +import java.util.Map; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import net.heimeng.sdk.btapi.api.BtApi; +import net.heimeng.sdk.btapi.exception.BtApiException; +import net.heimeng.sdk.btapi.model.BtResult; + +@DisplayName("GetWebsiteLimitNetApi 单元测试") +class GetWebsiteLimitNetApiTest { + + @Test + @DisplayName("应暴露正确的接口元数据并校验参数") + void exposesMetadataAndValidatesParams() { + GetWebsiteLimitNetApi api = new GetWebsiteLimitNetApi().setId(8); + + assertEquals("site?action=GetLimitNet", api.getEndpoint()); + assertEquals(BtApi.HttpMethod.POST, api.getMethod()); + assertEquals(8, api.getParams().get("id")); + assertTrue(invokeValidate(api)); + assertFalse(invokeValidate(new GetWebsiteLimitNetApi().setId(0))); + } + + @Test + @DisplayName("应正确解析原始流量限制配置响应") + void parsesRawLimitConfigPayload() { + GetWebsiteLimitNetApi api = new GetWebsiteLimitNetApi(); + BtResult> result = + api.parseResponse("{\"perserver\":300,\"perip\":25,\"limit_rate\":512,\"enabled\":true}"); + + assertTrue(result.isSuccess()); + assertEquals(300, result.getData().get("perserver")); + assertEquals(25, result.getData().get("perip")); + assertEquals(512, result.getData().get("limit_rate")); + assertEquals(true, result.getData().get("enabled")); + } + + @Test + @DisplayName("应正确解析包装后的流量限制配置响应") + void parsesWrappedLimitConfigPayload() { + GetWebsiteLimitNetApi api = new GetWebsiteLimitNetApi(); + BtResult> result = + api.parseResponse("{\"status\":true,\"msg\":\"ok\",\"data\":{\"enabled\":false}}"); + + assertTrue(result.isSuccess()); + assertEquals("ok", result.getMsg()); + assertEquals(false, result.getData().get("enabled")); + } + + @Test + @DisplayName("应保留失败包装响应的状态与消息") + void preservesFailurePayload() { + GetWebsiteLimitNetApi api = new GetWebsiteLimitNetApi(); + BtResult> result = + api.parseResponse("{\"status\":false,\"msg\":\"nginx only\"}"); + + assertFalse(result.isSuccess()); + assertEquals("nginx only", result.getMsg()); + assertTrue(result.getData().isEmpty()); + } + + @Test + @DisplayName("data 字段不是对象时应抛出异常") + void rejectsNonObjectDataPayload() { + GetWebsiteLimitNetApi api = new GetWebsiteLimitNetApi(); + + BtApiException exception = + assertThrows( + BtApiException.class, () -> api.parseResponse("{\"status\":true,\"data\":123}")); + + assertTrue(exception.getMessage().contains("must be a JSON object")); + } + + private boolean invokeValidate(GetWebsiteLimitNetApi api) { + try { + Method method = GetWebsiteLimitNetApi.class.getDeclaredMethod("validateParams"); + method.setAccessible(true); + return (boolean) method.invoke(api); + } catch (ReflectiveOperationException exception) { + throw new AssertionError(exception); + } + } +} diff --git a/src/test/java/net/heimeng/sdk/btapi/api/website/GetWebsiteListApiTest.java b/src/test/java/net/heimeng/sdk/btapi/api/website/GetWebsiteListApiTest.java new file mode 100644 index 0000000..b233c08 --- /dev/null +++ b/src/test/java/net/heimeng/sdk/btapi/api/website/GetWebsiteListApiTest.java @@ -0,0 +1,123 @@ +package net.heimeng.sdk.btapi.api.website; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.lang.reflect.Method; +import java.util.List; +import java.util.Map; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import net.heimeng.sdk.btapi.api.BtApi; +import net.heimeng.sdk.btapi.exception.BtApiException; +import net.heimeng.sdk.btapi.model.BtResult; + +@DisplayName("GetWebsiteListApi 单元测试") +class GetWebsiteListApiTest { + + @Test + @DisplayName("应暴露正确的接口元数据并校验参数") + void exposesMetadataAndValidatesParams() { + GetWebsiteListApi api = + new GetWebsiteListApi() + .setPage(1) + .setLimit(20) + .setType(-1) + .setOrder("id desc") + .setTojs("get_site_list") + .setSearch("demo"); + + assertEquals("data?action=getData&table=sites", api.getEndpoint()); + assertEquals(BtApi.HttpMethod.POST, api.getMethod()); + assertEquals(1, api.getParams().get("p")); + assertEquals(20, api.getParams().get("limit")); + assertEquals(-1, api.getParams().get("type")); + assertTrue(invokeValidate(api)); + } + + @Test + @DisplayName("缺少 limit 或参数非法时应校验失败") + void rejectsInvalidParams() { + assertFalse(invokeValidate(new GetWebsiteListApi().setPage(1))); + assertFalse(invokeValidate(new GetWebsiteListApi().setLimit(0))); + assertFalse(invokeValidate(new GetWebsiteListApi().setLimit(10).setPage(0))); + assertFalse(invokeValidate(new GetWebsiteListApi().setLimit(10).setType(1))); + } + + @Test + @DisplayName("应正确解析包装后的站点列表响应") + void parsesWrappedListPayload() { + GetWebsiteListApi api = new GetWebsiteListApi(); + BtResult>> result = + api.parseResponse( + """ + { + "msg": "ok", + "data": [ + { + "id": 1, + "name": "demo.example.com", + "tags": ["prod", "cdn"], + "ssl": { "enabled": true } + } + ] + } + """); + + assertTrue(result.isSuccess()); + assertEquals("ok", result.getMsg()); + assertEquals(1, result.getData().size()); + assertEquals("demo.example.com", result.getData().get(0).get("name")); + assertEquals(List.of("prod", "cdn"), result.getData().get(0).get("tags")); + assertEquals(Map.of("enabled", true), result.getData().get(0).get("ssl")); + } + + @Test + @DisplayName("应兼容直接返回数组的列表响应") + void parsesRawArrayPayload() { + GetWebsiteListApi api = new GetWebsiteListApi(); + BtResult>> result = + api.parseResponse("[{\"id\":1,\"name\":\"demo.example.com\"}]"); + + assertTrue(result.isSuccess()); + assertEquals(1, result.getData().size()); + assertEquals("demo.example.com", result.getData().get(0).get("name")); + } + + @Test + @DisplayName("应保留失败包装响应的状态与消息") + void preservesFailurePayload() { + GetWebsiteListApi api = new GetWebsiteListApi(); + BtResult>> result = + api.parseResponse("{\"status\":false,\"msg\":\"panel busy\"}"); + + assertFalse(result.isSuccess()); + assertEquals("panel busy", result.getMsg()); + assertTrue(result.getData().isEmpty()); + } + + @Test + @DisplayName("缺少 data 数组时应抛出异常") + void rejectsMissingDataField() { + GetWebsiteListApi api = new GetWebsiteListApi(); + + BtApiException exception = + assertThrows(BtApiException.class, () -> api.parseResponse("{\"msg\":\"ok\"}")); + + assertTrue(exception.getMessage().contains("data array")); + } + + private boolean invokeValidate(GetWebsiteListApi api) { + try { + Method method = GetWebsiteListApi.class.getDeclaredMethod("validateParams"); + method.setAccessible(true); + return (boolean) method.invoke(api); + } catch (ReflectiveOperationException exception) { + throw new AssertionError(exception); + } + } +} diff --git a/src/test/java/net/heimeng/sdk/btapi/api/website/GetWebsitePhpExtensionsApiTest.java b/src/test/java/net/heimeng/sdk/btapi/api/website/GetWebsitePhpExtensionsApiTest.java new file mode 100644 index 0000000..15b8cc0 --- /dev/null +++ b/src/test/java/net/heimeng/sdk/btapi/api/website/GetWebsitePhpExtensionsApiTest.java @@ -0,0 +1,78 @@ +package net.heimeng.sdk.btapi.api.website; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.lang.reflect.Method; +import java.util.List; +import java.util.Map; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import net.heimeng.sdk.btapi.api.BtApi; +import net.heimeng.sdk.btapi.exception.BtApiException; +import net.heimeng.sdk.btapi.model.BtResult; + +@DisplayName("GetWebsitePhpExtensionsApi unit tests") +class GetWebsitePhpExtensionsApiTest { + + @Test + @DisplayName("should expose metadata and validate params") + void exposesMetadataAndValidatesParams() { + GetWebsitePhpExtensionsApi api = new GetWebsitePhpExtensionsApi().setId(8); + + assertEquals("site?action=GetPHPModules", api.getEndpoint()); + assertEquals(BtApi.HttpMethod.POST, api.getMethod()); + assertEquals(8, api.getParams().get("id")); + assertTrue(invokeValidate(api)); + assertFalse(invokeValidate(new GetWebsitePhpExtensionsApi().setId(0))); + } + + @Test + @DisplayName("should parse wrapped extensions payload") + void parsesWrappedExtensionsPayload() { + GetWebsitePhpExtensionsApi api = new GetWebsitePhpExtensionsApi(); + BtResult>> result = + api.parseResponse("{\"msg\":\"ok\",\"data\":[{\"name\":\"redis\",\"status\":true}]}"); + + assertTrue(result.isSuccess()); + assertEquals("ok", result.getMsg()); + assertEquals(List.of(Map.of("name", "redis", "status", true)), result.getData()); + } + + @Test + @DisplayName("should preserve wrapped failure payload") + void preservesFailurePayload() { + GetWebsitePhpExtensionsApi api = new GetWebsitePhpExtensionsApi(); + BtResult>> result = + api.parseResponse("{\"status\":false,\"msg\":\"php not installed\"}"); + + assertFalse(result.isSuccess()); + assertEquals("php not installed", result.getMsg()); + assertTrue(result.getData().isEmpty()); + } + + @Test + @DisplayName("missing data array should throw") + void rejectsMissingDataField() { + GetWebsitePhpExtensionsApi api = new GetWebsitePhpExtensionsApi(); + + BtApiException exception = + assertThrows(BtApiException.class, () -> api.parseResponse("{\"status\":true}")); + + assertTrue(exception.getMessage().contains("data array")); + } + + private boolean invokeValidate(GetWebsitePhpExtensionsApi api) { + try { + Method method = GetWebsitePhpExtensionsApi.class.getDeclaredMethod("validateParams"); + method.setAccessible(true); + return (boolean) method.invoke(api); + } catch (ReflectiveOperationException exception) { + throw new AssertionError(exception); + } + } +} diff --git a/src/test/java/net/heimeng/sdk/btapi/api/website/GetWebsiteSslListApiTest.java b/src/test/java/net/heimeng/sdk/btapi/api/website/GetWebsiteSslListApiTest.java new file mode 100644 index 0000000..d17673c --- /dev/null +++ b/src/test/java/net/heimeng/sdk/btapi/api/website/GetWebsiteSslListApiTest.java @@ -0,0 +1,80 @@ +package net.heimeng.sdk.btapi.api.website; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.lang.reflect.Method; +import java.util.List; +import java.util.Map; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import net.heimeng.sdk.btapi.api.BtApi; +import net.heimeng.sdk.btapi.exception.BtApiException; +import net.heimeng.sdk.btapi.model.BtResult; + +@DisplayName("GetWebsiteSslListApi unit tests") +class GetWebsiteSslListApiTest { + + @Test + @DisplayName("should expose metadata and validate params") + void exposesMetadataAndValidatesParams() { + GetWebsiteSslListApi api = new GetWebsiteSslListApi().setId(8); + + assertEquals("site?action=GetSSLCertList", api.getEndpoint()); + assertEquals(BtApi.HttpMethod.POST, api.getMethod()); + assertEquals(8, api.getParams().get("id")); + assertTrue(invokeValidate(api)); + assertFalse(invokeValidate(new GetWebsiteSslListApi().setId(0))); + } + + @Test + @DisplayName("should parse wrapped cert list payload") + void parsesWrappedCertListPayload() { + GetWebsiteSslListApi api = new GetWebsiteSslListApi(); + BtResult>> result = + api.parseResponse( + "{\"msg\":\"ok\",\"certs\":[{\"subject\":\"demo.example.com\",\"issuer\":\"LetsEncrypt\"}]}"); + + assertTrue(result.isSuccess()); + assertEquals("ok", result.getMsg()); + assertEquals( + List.of(Map.of("subject", "demo.example.com", "issuer", "LetsEncrypt")), result.getData()); + } + + @Test + @DisplayName("should preserve wrapped failure payload") + void preservesFailurePayload() { + GetWebsiteSslListApi api = new GetWebsiteSslListApi(); + BtResult>> result = + api.parseResponse("{\"status\":false,\"msg\":\"ssl disabled\"}"); + + assertFalse(result.isSuccess()); + assertEquals("ssl disabled", result.getMsg()); + assertTrue(result.getData().isEmpty()); + } + + @Test + @DisplayName("missing certs array should throw") + void rejectsMissingCertsField() { + GetWebsiteSslListApi api = new GetWebsiteSslListApi(); + + BtApiException exception = + assertThrows(BtApiException.class, () -> api.parseResponse("{\"status\":true}")); + + assertTrue(exception.getMessage().contains("certs array")); + } + + private boolean invokeValidate(GetWebsiteSslListApi api) { + try { + Method method = GetWebsiteSslListApi.class.getDeclaredMethod("validateParams"); + method.setAccessible(true); + return (boolean) method.invoke(api); + } catch (ReflectiveOperationException exception) { + throw new AssertionError(exception); + } + } +} diff --git a/src/test/java/net/heimeng/sdk/btapi/api/website/GetWebsiteTypesApiTest.java b/src/test/java/net/heimeng/sdk/btapi/api/website/GetWebsiteTypesApiTest.java new file mode 100644 index 0000000..3fa6000 --- /dev/null +++ b/src/test/java/net/heimeng/sdk/btapi/api/website/GetWebsiteTypesApiTest.java @@ -0,0 +1,75 @@ +package net.heimeng.sdk.btapi.api.website; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.List; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import net.heimeng.sdk.btapi.api.BtApi; +import net.heimeng.sdk.btapi.exception.BtApiException; +import net.heimeng.sdk.btapi.model.BtResult; +import net.heimeng.sdk.btapi.model.website.WebsiteType; + +@DisplayName("GetWebsiteTypesApi 单元测试") +class GetWebsiteTypesApiTest { + + @Test + @DisplayName("应暴露正确的接口元数据") + void exposesMetadata() { + GetWebsiteTypesApi api = new GetWebsiteTypesApi(); + + assertEquals("site?action=get_site_types", api.getEndpoint()); + assertEquals(BtApi.HttpMethod.POST, api.getMethod()); + } + + @Test + @DisplayName("应正确解析直接返回数组的分类响应") + void parsesRawArrayPayload() { + GetWebsiteTypesApi api = new GetWebsiteTypesApi(); + BtResult> result = api.parseResponse("[{\"id\":0,\"name\":\"默认分类\"}]"); + + assertTrue(result.isSuccess()); + assertEquals(1, result.getData().size()); + assertEquals(0, result.getData().get(0).getId()); + assertEquals("默认分类", result.getData().get(0).getName()); + } + + @Test + @DisplayName("应正确解析包装后的分类响应") + void parsesWrappedArrayPayload() { + GetWebsiteTypesApi api = new GetWebsiteTypesApi(); + BtResult> result = + api.parseResponse("{\"msg\":\"ok\",\"data\":[{\"id\":1,\"name\":\"业务站点\"}]}"); + + assertTrue(result.isSuccess()); + assertEquals("ok", result.getMsg()); + assertEquals(1, result.getData().size()); + } + + @Test + @DisplayName("应保留失败包装响应的状态与消息") + void preservesFailurePayload() { + GetWebsiteTypesApi api = new GetWebsiteTypesApi(); + BtResult> result = api.parseResponse("{\"status\":false,\"msg\":\"busy\"}"); + + assertFalse(result.isSuccess()); + assertEquals("busy", result.getMsg()); + assertTrue(result.getData().isEmpty()); + } + + @Test + @DisplayName("成功包装响应缺少 data 数组时应抛出异常") + void rejectsMissingDataField() { + GetWebsiteTypesApi api = new GetWebsiteTypesApi(); + + BtApiException exception = + assertThrows(BtApiException.class, () -> api.parseResponse("{\"status\":true}")); + + assertTrue(exception.getMessage().contains("data array")); + } +} diff --git a/src/test/java/net/heimeng/sdk/btapi/api/website/GetWebsitesApiTest.java b/src/test/java/net/heimeng/sdk/btapi/api/website/GetWebsitesApiTest.java index b8883c6..cb6683b 100644 --- a/src/test/java/net/heimeng/sdk/btapi/api/website/GetWebsitesApiTest.java +++ b/src/test/java/net/heimeng/sdk/btapi/api/website/GetWebsitesApiTest.java @@ -1,258 +1,149 @@ package net.heimeng.sdk.btapi.api.website; -import net.heimeng.sdk.btapi.api.BtApi; -import net.heimeng.sdk.btapi.exception.BtApiException; -import net.heimeng.sdk.btapi.model.BtResult; -import net.heimeng.sdk.btapi.model.website.WebsiteInfo; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; import java.util.List; import java.util.Map; -import static org.junit.jupiter.api.Assertions.*; - -/** - * GetWebsitesApi类的单元测试 - *

- * 测试GetWebsitesApi类的API调用、参数设置以及响应解析功能。 - *

- * - * @author InwardFlow - * @since 2.0.0 - */ -@DisplayName("GetWebsitesApi类测试") -class GetWebsitesApiTest { - - private GetWebsitesApi websitesApi; - - @BeforeEach - void setUp() { - // 创建测试对象,使用默认分页参数 - websitesApi = new GetWebsitesApi(); - } - - @Test - @DisplayName("测试API基础信息") - void testApiBasicInfo() { - // 验证API端点和请求方法 - assertEquals("data?action=getData&table=sites", websitesApi.getEndpoint()); - assertEquals(BtApi.HttpMethod.POST, websitesApi.getMethod()); - } - - @Test - @DisplayName("测试默认分页参数") - void testDefaultPaginationParams() { - // 获取默认参数 - Map params = websitesApi.getParams(); - assertNotNull(params); - assertEquals(2, params.size()); - assertEquals(1, params.get("p")); // 默认页码为1 - assertEquals(10, params.get("limit")); // 默认每页10条记录 - } - - @Test - @DisplayName("测试自定义分页参数构造") - void testCustomPaginationParamsConstructor() { - // 创建自定义分页参数的API实例 - GetWebsitesApi customApi = new GetWebsitesApi(2, 20); - Map params = customApi.getParams(); - assertNotNull(params); - assertEquals(2, params.size()); - assertEquals(2, params.get("p")); // 自定义页码为2 - assertEquals(20, params.get("limit")); // 自定义每页20条记录 - } - - @Test - @DisplayName("测试参数设置方法") - void testParamSetters() { - // 测试设置页码 - GetWebsitesApi result = websitesApi.setPage(3); - assertEquals(websitesApi, result); // 验证链式调用 - assertEquals(3, websitesApi.getParams().get("p")); - - // 测试设置每页记录数 - result = websitesApi.setLimit(50); - assertEquals(websitesApi, result); // 验证链式调用 - assertEquals(50, websitesApi.getParams().get("limit")); - } - - @Test - @DisplayName("测试链式调用参数设置") - void testChainedParamSetting() { - // 测试链式调用 - GetWebsitesApi chainedApi = new GetWebsitesApi() - .setPage(4) - .setLimit(100); - - Map params = chainedApi.getParams(); - assertNotNull(params); - assertEquals(4, params.get("p")); - assertEquals(100, params.get("limit")); - } - - @Test - @DisplayName("测试响应解析 - 成功场景") - void testParseResponse_Success() { - // 创建模拟成功响应 - String mockResponse = "{" + - "\"msg\": \"Success\"," + - "\"data\": [" + - "{\"id\": 1, \"name\": \"example.com\", \"path\": \"/www/wwwroot/example.com\", \"project_type\": \"php\", \"status\": \"1\", \"ssl\": 1, \"addtime\": \"2021-01-01 12:00:00\"}," + - "{\"id\": 2, \"name\": \"test.com\", \"path\": \"/www/wwwroot/test.com\", \"project_type\": \"html\", \"status\": \"0\", \"ssl\": 0, \"addtime\": \"2021-01-02 13:30:00\"}" + - "]}"; - - // 解析响应 - BtResult> result = websitesApi.parseResponse(mockResponse); - - // 验证解析结果 - assertNotNull(result); - assertTrue(result.isSuccess()); - assertEquals("Success", result.getMsg()); - - List websites = result.getData(); - assertNotNull(websites); - assertEquals(2, websites.size()); - - // 验证第一个网站信息 - WebsiteInfo website1 = websites.get(0); - assertEquals(1, website1.getId()); - assertEquals("example.com", website1.getName()); - assertEquals("/www/wwwroot/example.com", website1.getPath()); - assertEquals("php", website1.getType()); - assertEquals("1", website1.getStatus()); - assertEquals(1, website1.getSsl()); - assertNotNull(website1.getCreateTime()); - assertTrue(website1.isRunning()); - assertTrue(website1.isSslEnabled()); - - // 验证第二个网站信息 - WebsiteInfo website2 = websites.get(1); - assertEquals(2, website2.getId()); - assertEquals("test.com", website2.getName()); - assertEquals("/www/wwwroot/test.com", website2.getPath()); - assertEquals("html", website2.getType()); - assertEquals("0", website2.getStatus()); - assertEquals(0, website2.getSsl()); - assertNotNull(website2.getCreateTime()); - assertFalse(website2.isRunning()); - assertFalse(website2.isSslEnabled()); - } - - @Test - @DisplayName("测试响应解析 - 空响应") - void testParseResponse_EmptyResponse() { - // 创建模拟空响应 - String mockResponse = "{" + - "\"msg\": \"Success\"," + - "\"data\": []" + - "}"; - - // 解析响应 - BtResult> result = websitesApi.parseResponse(mockResponse); - - // 验证解析结果 - assertNotNull(result); - assertTrue(result.isSuccess()); - assertEquals("Success", result.getMsg()); - - List websites = result.getData(); - assertNotNull(websites); - assertTrue(websites.isEmpty()); - } - - @Test - @DisplayName("测试响应解析 - 无效JSON格式") - void testParseResponse_InvalidJson() { - // 创建无效的JSON响应 - String invalidJson = "{invalid json}"; - - // 验证解析异常 - BtApiException exception = assertThrows(BtApiException.class, () -> { - websitesApi.parseResponse(invalidJson); - }); - - assertTrue(exception.getMessage().contains("JSON解析错误")); - } +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; - @Test - @DisplayName("测试响应解析 - 缺少data字段") - void testParseResponse_MissingDataField() { - // 创建缺少data字段的响应 - String mockResponse = "{" + - "\"msg\": \"Success\"" + - "}"; - - // 验证解析异常 - BtApiException exception = assertThrows(BtApiException.class, () -> { - websitesApi.parseResponse(mockResponse); - }); - - assertTrue(exception.getMessage().contains("data字段不存在")); - } +import net.heimeng.sdk.btapi.api.BtApi; +import net.heimeng.sdk.btapi.exception.BtApiException; +import net.heimeng.sdk.btapi.model.BtResult; +import net.heimeng.sdk.btapi.model.website.WebsiteInfo; - @Test - @DisplayName("测试响应解析 - data字段非数组") - void testParseResponse_DataNotArray() { - // 创建data字段非数组的响应 - String mockResponse = "{" + - "\"msg\": \"Success\"," + - "\"data\": {\"id\": 1, \"name\": \"example.com\"}" + - "}"; - - // 验证解析异常 - BtApiException exception = assertThrows(BtApiException.class, () -> { - websitesApi.parseResponse(mockResponse); - }); - - assertTrue(exception.getMessage().contains("data不是有效数组")); - } +/** {@link GetWebsitesApi} 的单元测试。 */ +@DisplayName("GetWebsitesApi tests") +class GetWebsitesApiTest { - @Test - @DisplayName("测试响应解析 - 特殊值处理") - void testParseResponse_SpecialValues() { - // 创建包含特殊值的响应 - String responseWithSpecialValues = "{" + - "\"msg\": \"Success\"," + - "\"data\": [" + - "{\"id\": null, \"name\": null, \"path\": null, \"project_type\": null, \"status\": null, \"ssl\": -1, \"addtime\": null}," + - "{\"id\": 3, \"name\": \"special.com\", \"path\": \"\", \"project_type\": \"\", \"status\": \"0\", \"ssl\": 999, \"addtime\": \"2021-01-03 14:45:00\"}" + - "]}"; - - // 解析响应 - BtResult> result = websitesApi.parseResponse(responseWithSpecialValues); - - // 验证解析结果 - assertNotNull(result); - assertTrue(result.isSuccess()); - - List websites = result.getData(); - assertNotNull(websites); - assertEquals(2, websites.size()); - - // 验证第一个网站的特殊值处理 - WebsiteInfo website1 = websites.get(0); - assertNull(website1.getId()); - assertNull(website1.getName()); - assertNull(website1.getPath()); - assertNull(website1.getType()); - assertNull(website1.getStatus()); - assertEquals(-1, website1.getSsl()); - assertNull(website1.getCreateTime()); - assertFalse(website1.isRunning()); - assertFalse(website1.isSslEnabled()); - - // 验证第二个网站的特殊值处理 - WebsiteInfo website2 = websites.get(1); - assertEquals(3, website2.getId()); - assertEquals("special.com", website2.getName()); - assertEquals("", website2.getPath()); - assertEquals("", website2.getType()); - assertEquals("0", website2.getStatus()); - assertEquals(999, website2.getSsl()); // 非标准SSL值也应正确解析 - assertNotNull(website2.getCreateTime()); - assertFalse(website2.isRunning()); - assertTrue(website2.isSslEnabled()); // 非零值应被视为SSL已启用 - } -} \ No newline at end of file + private GetWebsitesApi websitesApi; + + @BeforeEach + void setUp() { + websitesApi = new GetWebsitesApi(); + } + + @Test + @DisplayName("API metadata matches the BT Panel endpoint contract") + void apiBasicInfo() { + assertEquals("data?action=getData&table=sites", websitesApi.getEndpoint()); + assertEquals(BtApi.HttpMethod.POST, websitesApi.getMethod()); + } + + @Test + @DisplayName("Default paging parameters are applied") + void defaultPaginationParams() { + Map params = websitesApi.getParams(); + + assertEquals(1, params.get("p")); + assertEquals(10, params.get("limit")); + } + + @Test + @DisplayName("Custom paging parameters override defaults") + void customPaginationParams() { + GetWebsitesApi customApi = new GetWebsitesApi(2, 20); + Map params = customApi.getParams(); + + assertEquals(2, params.get("p")); + assertEquals(20, params.get("limit")); + } + + @Test + @DisplayName("Response parsing maps common website fields") + void parseResponseSuccess() { + String response = + "{" + + "\"msg\":\"Success\"," + + "\"data\":[" + + "{\"id\":1,\"name\":\"example.com\",\"path\":\"/www/wwwroot/example.com\"," + + "\"project_type\":\"php\",\"status\":\"1\",\"ssl\":1," + + "\"addtime\":\"2021-01-01 12:00:00\"}," + + "{\"id\":2,\"name\":\"test.com\",\"path\":\"/www/wwwroot/test.com\"," + + "\"project_type\":\"html\",\"status\":\"0\",\"ssl\":0," + + "\"addtime\":\"2021-01-02 13:30:00\"}" + + "]}"; + + BtResult> result = websitesApi.parseResponse(response); + + assertTrue(result.isSuccess()); + assertEquals("Success", result.getMsg()); + assertNotNull(result.getData()); + assertEquals(2, result.getData().size()); + + WebsiteInfo firstWebsite = result.getData().get(0); + assertEquals(1L, firstWebsite.getId()); + assertEquals("example.com", firstWebsite.getName()); + assertEquals("example.com", firstWebsite.getDomain()); + assertEquals("php", firstWebsite.getType()); + assertEquals(1, firstWebsite.getStatus()); + assertEquals(1, firstWebsite.getSsl()); + assertTrue(firstWebsite.isRunning()); + assertTrue(firstWebsite.isSslEnabled()); + assertNotNull(firstWebsite.getCreateTime()); + + WebsiteInfo secondWebsite = result.getData().get(1); + assertFalse(secondWebsite.isRunning()); + assertFalse(secondWebsite.isSslEnabled()); + } + + @Test + @DisplayName("Empty data arrays are supported") + void parseResponseEmptyData() { + BtResult> result = + websitesApi.parseResponse("{\"msg\":\"Success\",\"data\":[]}"); + + assertTrue(result.isSuccess()); + assertNotNull(result.getData()); + assertTrue(result.getData().isEmpty()); + } + + @Test + @DisplayName("Null source fields remain null in parsed output") + void parseResponsePreservesNulls() { + String response = + "{" + + "\"msg\":\"Success\"," + + "\"data\":[" + + "{\"id\":null,\"name\":null,\"path\":null,\"project_type\":null," + + "\"status\":null,\"ssl\":null,\"addtime\":null}" + + "]}"; + + BtResult> result = websitesApi.parseResponse(response); + WebsiteInfo website = result.getData().get(0); + + assertNull(website.getId()); + assertNull(website.getName()); + assertNull(website.getPath()); + assertNull(website.getType()); + assertNull(website.getStatus()); + assertNull(website.getSsl()); + assertNull(website.getCreateTime()); + } + + @Test + @DisplayName("Invalid JSON is rejected") + void parseResponseRejectsInvalidJson() { + BtApiException exception = + assertThrows(BtApiException.class, () -> websitesApi.parseResponse("{invalid json}")); + + assertTrue(exception.getMessage().contains("Invalid JSON response")); + } + + @Test + @DisplayName("Missing data field is rejected") + void parseResponseRejectsMissingDataField() { + BtApiException exception = + assertThrows( + BtApiException.class, () -> websitesApi.parseResponse("{\"msg\":\"Success\"}")); + + assertTrue(exception.getMessage().contains("Missing required data field")); + } +} diff --git a/src/test/java/net/heimeng/sdk/btapi/api/website/WebsiteBooleanApisTest.java b/src/test/java/net/heimeng/sdk/btapi/api/website/WebsiteBooleanApisTest.java new file mode 100644 index 0000000..b5dc3a3 --- /dev/null +++ b/src/test/java/net/heimeng/sdk/btapi/api/website/WebsiteBooleanApisTest.java @@ -0,0 +1,315 @@ +package net.heimeng.sdk.btapi.api.website; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.lang.reflect.Method; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import net.heimeng.sdk.btapi.api.BtApi; +import net.heimeng.sdk.btapi.exception.BtApiException; +import net.heimeng.sdk.btapi.model.BtResult; + +@DisplayName("Website 布尔写操作 API 单元测试") +class WebsiteBooleanApisTest { + + @Test + @DisplayName("添加站点域名 API 契约正确") + void addWebsiteDomainApiContract() { + AddWebsiteDomainApi api = + new AddWebsiteDomainApi() + .setId(8) + .setWebname("demo.example.com") + .setDomain("www.demo.example.com"); + + assertEquals("site?action=AddDomain", api.getEndpoint()); + assertEquals(BtApi.HttpMethod.POST, api.getMethod()); + assertEquals(8, api.getParams().get("id")); + assertTrue(invokeValidate(api)); + + BtResult result = api.parseResponse("{\"status\":true,\"msg\":\"ok\"}"); + assertTrue(result.isSuccess()); + assertTrue(result.getData()); + } + + @Test + @DisplayName("删除站点域名 API 契约正确") + void deleteWebsiteDomainApiContract() { + DeleteWebsiteDomainApi api = + new DeleteWebsiteDomainApi() + .setId(8) + .setWebname("demo.example.com") + .setDomain("www.demo.example.com") + .setPort(80); + + assertEquals("site?action=DelDomain", api.getEndpoint()); + assertEquals(80, api.getParams().get("port")); + assertTrue(invokeValidate(api)); + + BtResult result = api.parseResponse("{\"status\":false,\"msg\":\"denied\"}"); + assertFalse(result.isSuccess()); + assertFalse(result.getData()); + assertEquals("denied", result.getMsg()); + } + + @Test + @DisplayName("关闭密码访问 API 契约正确") + void closeWebsitePasswordApiContract() { + CloseWebsitePasswordApi api = new CloseWebsitePasswordApi().setId(8); + + assertEquals("site?action=CloseHasPwd", api.getEndpoint()); + assertTrue(invokeValidate(api)); + } + + @Test + @DisplayName("关闭 SSL API 契约正确") + void closeWebsiteSslApiContract() { + CloseWebsiteSslApi api = new CloseWebsiteSslApi().setId(8); + + assertEquals("site?action=CloseSSL", api.getEndpoint()); + assertTrue(invokeValidate(api)); + } + + @Test + @DisplayName("创建站点备份 API 契约正确") + void createWebsiteBackupApiContract() { + CreateWebsiteBackupApi api = new CreateWebsiteBackupApi().setId(8); + + assertEquals("site?action=ToBackup", api.getEndpoint()); + assertTrue(invokeValidate(api)); + } + + @Test + @DisplayName("删除站点备份 API 契约正确") + void deleteWebsiteBackupApiContract() { + DeleteWebsiteBackupApi api = new DeleteWebsiteBackupApi().setId(18); + + assertEquals("site?action=DelBackup", api.getEndpoint()); + assertTrue(invokeValidate(api)); + } + + @Test + @DisplayName("启动站点 API 契约正确") + void startWebsiteApiContract() { + StartWebsiteApi api = new StartWebsiteApi().setId(8); + + assertEquals("site?action=SiteStart", api.getEndpoint()); + assertTrue(invokeValidate(api)); + } + + @Test + @DisplayName("停止站点 API 契约正确") + void stopWebsiteApiContract() { + StopWebsiteApi api = new StopWebsiteApi().setId(8); + + assertEquals("site?action=StopSite", api.getEndpoint()); + assertTrue(invokeValidate(api)); + } + + @Test + @DisplayName("切换日志状态 API 契约正确") + void setWebsiteLogsApiContract() { + SetWebsiteLogsApi api = new SetWebsiteLogsApi().setId(8); + + assertEquals("site?action=logsOpen", api.getEndpoint()); + assertTrue(invokeValidate(api)); + } + + @Test + @DisplayName("设置站点备注 API 允许空字符串备注") + void setWebsitePsApiAllowsBlankValue() { + SetWebsitePsApi api = new SetWebsitePsApi().setId(8).setPs(""); + + assertEquals("", api.getParams().get("ps")); + assertTrue(invokeValidate(api)); + } + + @Test + @DisplayName("设置站点根目录 API 契约正确") + void setWebsiteRootPathApiContract() { + SetWebsiteRootPathApi api = new SetWebsiteRootPathApi().setId(8).setPath("/www/wwwroot/demo"); + + assertEquals("site?action=SetPath", api.getEndpoint()); + assertTrue(invokeValidate(api)); + } + + @Test + @DisplayName("设置站点运行目录 API 允许空字符串") + void setWebsiteRunPathApiAllowsBlankValue() { + SetWebsiteRunPathApi api = new SetWebsiteRunPathApi().setId(8).setRunPath(""); + + assertEquals("", api.getParams().get("runPath")); + assertTrue(invokeValidate(api)); + } + + @Test + @DisplayName("切换防跨站配置 API 契约正确") + void setWebsiteUserIniApiContract() { + SetWebsiteUserIniApi api = new SetWebsiteUserIniApi().setPath("/www/wwwroot/demo"); + + assertEquals("site?action=SetDirUserINI", api.getEndpoint()); + assertTrue(invokeValidate(api)); + } + + @Test + @DisplayName("设置 PHP 版本 API 契约正确") + void setWebsitePhpVersionApiContract() { + SetWebsitePhpVersionApi api = new SetWebsitePhpVersionApi().setId(8).setPhpVersion("82"); + + assertEquals("82", api.getParams().get("php_version")); + assertTrue(invokeValidate(api)); + } + + @Test + @DisplayName("设置站点密码访问 API 契约正确") + void setWebsitePasswordApiContract() { + SetWebsitePasswordApi api = + new SetWebsitePasswordApi().setId(8).setUsername("admin").setPassword("secret"); + + assertEquals("admin", api.getParams().get("username")); + assertTrue(invokeValidate(api)); + } + + @Test + @DisplayName("设置伪静态规则 API 允许空规则内容") + void setWebsiteRewriteRulesApiAllowsEmptyContent() { + SetWebsiteRewriteRulesApi api = + new SetWebsiteRewriteRulesApi().setId(8).setName("none").setContent(""); + + assertEquals("", api.getParams().get("content")); + assertTrue(invokeValidate(api)); + } + + @Test + @DisplayName("设置 Nginx 配置 API 允许空配置内容") + void setWebsiteNginxConfigApiAllowsEmptyContent() { + SetWebsiteNginxConfigApi api = + new SetWebsiteNginxConfigApi().setId(8).setDomain("demo.example.com").setContent(""); + + assertEquals("", api.getParams().get("content")); + assertTrue(invokeValidate(api)); + } + + @Test + @DisplayName("设置流量限制 API 应将布尔值转换为整数标志位") + void setWebsiteLimitNetApiContract() { + SetWebsiteLimitNetApi api = + new SetWebsiteLimitNetApi() + .setId(8) + .setEnabled(false) + .setPerserver(300) + .setPerip(30) + .setLimitRate(0); + + assertEquals(0, api.getParams().get("enabled")); + assertEquals(300, api.getParams().get("perserver")); + assertTrue(invokeValidate(api)); + } + + @Test + @DisplayName("设置流量限制 API 应拒绝负数限制值") + void setWebsiteLimitNetApiRejectsNegativeNumber() { + SetWebsiteLimitNetApi api = new SetWebsiteLimitNetApi(); + + IllegalArgumentException exception = + assertThrows(IllegalArgumentException.class, () -> api.setPerip(-1)); + + assertTrue(exception.getMessage().contains("perip")); + } + + @Test + @DisplayName("设置 PHP 扩展 API 应将布尔值转换为整数标志位") + void setWebsitePhpExtensionsApiContract() { + SetWebsitePhpExtensionsApi api = + new SetWebsitePhpExtensionsApi().setId(8).setModuleName("redis").setEnabled(true); + + assertEquals(1, api.getParams().get("enabled")); + assertTrue(invokeValidate(api)); + } + + @Test + @DisplayName("设置 SSL API 支持可选强制 HTTPS 标志位") + void setWebsiteSslApiContract() { + SetWebsiteSslApi api = + new SetWebsiteSslApi() + .setId(8) + .setDomain("demo.example.com") + .setCert("cert") + .setKey("key") + .setForceHttps(true); + + assertEquals(1, api.getParams().get("force_https")); + assertTrue(invokeValidate(api)); + + api.setForceHttps(null); + assertFalse(api.getParams().containsKey("force_https")); + assertTrue(invokeValidate(api)); + } + + @Test + @DisplayName("删除站点 API 应正确维护关联删除标志") + void deleteWebsiteApiContract() { + DeleteWebsiteApi api = + new DeleteWebsiteApi(8, "demo.example.com") + .setDeleteFtp(true) + .setDeleteDatabase(true) + .setDeletePath(true); + + assertEquals("site?action=DeleteSite", api.getEndpoint()); + assertEquals(1, api.getParams().get("ftp")); + assertEquals(1, api.getParams().get("database")); + assertEquals(1, api.getParams().get("path")); + assertTrue(invokeValidate(api)); + + api.setDeleteFtp(false).setDeleteDatabase(false).setDeletePath(false); + assertFalse(api.getParams().containsKey("ftp")); + assertFalse(api.getParams().containsKey("database")); + assertFalse(api.getParams().containsKey("path")); + assertTrue(invokeValidate(api)); + } + + @Test + @DisplayName("删除站点 API 应拒绝非正整数 ID") + void deleteWebsiteApiRejectsNonPositiveId() { + IllegalArgumentException exception = + assertThrows(IllegalArgumentException.class, () -> new DeleteWebsiteApi(0, "demo")); + + assertTrue(exception.getMessage().contains("id")); + } + + @Test + @DisplayName("Website 布尔 API 应拒绝缺少状态字段的响应") + void booleanApisRejectMissingStatusField() { + AddWebsiteDomainApi api = new AddWebsiteDomainApi(); + + BtApiException exception = + assertThrows(BtApiException.class, () -> api.parseResponse("{\"msg\":\"ok\"}")); + + assertTrue(exception.getMessage().contains("status field")); + } + + @Test + @DisplayName("Website 布尔 API 应拒绝非 JSON 响应") + void booleanApisRejectInvalidJson() { + DeleteWebsiteApi api = new DeleteWebsiteApi(); + + BtApiException exception = + assertThrows(BtApiException.class, () -> api.parseResponse("plain text")); + + assertTrue(exception.getMessage().contains("Invalid JSON")); + } + + private boolean invokeValidate(Object api) { + try { + Method method = api.getClass().getDeclaredMethod("validateParams"); + method.setAccessible(true); + return (boolean) method.invoke(api); + } catch (ReflectiveOperationException exception) { + throw new AssertionError(exception); + } + } +} diff --git a/src/test/java/net/heimeng/sdk/btapi/api/website/WebsiteTextQueryApisTest.java b/src/test/java/net/heimeng/sdk/btapi/api/website/WebsiteTextQueryApisTest.java new file mode 100644 index 0000000..40e287f --- /dev/null +++ b/src/test/java/net/heimeng/sdk/btapi/api/website/WebsiteTextQueryApisTest.java @@ -0,0 +1,114 @@ +package net.heimeng.sdk.btapi.api.website; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.lang.reflect.Method; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import net.heimeng.sdk.btapi.api.BtApi; +import net.heimeng.sdk.btapi.exception.BtApiException; +import net.heimeng.sdk.btapi.model.BtResult; + +@DisplayName("Website 文本查询 API 单元测试") +class WebsiteTextQueryApisTest { + + @Test + @DisplayName("网站根目录 API 契约应正确") + void rootPathApiContract() { + GetWebsiteRootPathApi api = new GetWebsiteRootPathApi().setId(8); + + assertEquals("data?action=getKey&table=sites&key=path", api.getEndpoint()); + assertEquals(BtApi.HttpMethod.POST, api.getMethod()); + assertEquals(8, api.getParams().get("id")); + assertTrue(invokeValidate(api)); + + BtResult result = api.parseResponse("/www/wwwroot/demo"); + assertTrue(result.isSuccess()); + assertEquals("/www/wwwroot/demo", result.getData()); + } + + @Test + @DisplayName("网站 PHP 版本 API 应正确解析包装成功响应") + void phpVersionApiParsesWrappedSuccess() { + GetWebsitePhpVersionApi api = new GetWebsitePhpVersionApi().setId(8); + + BtResult result = api.parseResponse("{\"status\":true,\"msg\":\"ok\",\"data\":\"82\"}"); + + assertTrue(result.isSuccess()); + assertEquals("ok", result.getMsg()); + assertEquals("82", result.getData()); + } + + @Test + @DisplayName("网站伪静态规则 API 应保留失败响应") + void rewriteRulesApiPreservesFailureResponse() { + GetWebsiteRewriteRulesApi api = new GetWebsiteRewriteRulesApi().setId(8); + + BtResult result = api.parseResponse("{\"status\":false,\"msg\":\"rewrite disabled\"}"); + + assertFalse(result.isSuccess()); + assertEquals("rewrite disabled", result.getMsg()); + assertEquals("", result.getData()); + } + + @Test + @DisplayName("网站 Nginx 配置 API 应校验域名参数") + void nginxConfigApiValidatesDomain() { + GetWebsiteNginxConfigApi api = + new GetWebsiteNginxConfigApi().setId(8).setDomain("demo.example.com"); + + assertEquals("site?action=getConf", api.getEndpoint()); + assertTrue(invokeValidate(api)); + + api.setDomain(" "); + assertFalse(invokeValidate(api)); + } + + @Test + @DisplayName("非包装 JSON 文本应视为原始内容") + void nonWrappedJsonContentIsTreatedAsRawText() { + GetWebsiteNginxConfigApi api = + new GetWebsiteNginxConfigApi().setId(8).setDomain("demo.example.com"); + + BtResult result = api.parseResponse("{\"server\":\"demo\"}"); + + assertTrue(result.isSuccess()); + assertEquals("{\"server\":\"demo\"}", result.getData()); + } + + @Test + @DisplayName("包装成功响应缺少 data 字段时应抛出异常") + void wrappedSuccessWithoutDataIsRejected() { + GetWebsitePhpVersionApi api = new GetWebsitePhpVersionApi().setId(8); + + BtApiException exception = + assertThrows(BtApiException.class, () -> api.parseResponse("{\"status\":true}")); + + assertTrue(exception.getMessage().contains("data field")); + } + + @Test + @DisplayName("空响应应抛出异常") + void blankResponseIsRejected() { + GetWebsiteRootPathApi api = new GetWebsiteRootPathApi().setId(8); + + BtApiException exception = assertThrows(BtApiException.class, () -> api.parseResponse(" ")); + + assertTrue(exception.getMessage().contains("Empty response")); + } + + private boolean invokeValidate(Object api) { + try { + Method method = api.getClass().getDeclaredMethod("validateParams"); + method.setAccessible(true); + return (boolean) method.invoke(api); + } catch (ReflectiveOperationException exception) { + throw new AssertionError(exception); + } + } +} diff --git a/src/test/java/net/heimeng/sdk/btapi/facade/DatabaseFacadeRequestsTest.java b/src/test/java/net/heimeng/sdk/btapi/facade/DatabaseFacadeRequestsTest.java new file mode 100644 index 0000000..9dd4a53 --- /dev/null +++ b/src/test/java/net/heimeng/sdk/btapi/facade/DatabaseFacadeRequestsTest.java @@ -0,0 +1,78 @@ +package net.heimeng.sdk.btapi.facade; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import net.heimeng.sdk.btapi.testutil.TestValueFactory; + +@DisplayName("Database facade request objects") +class DatabaseFacadeRequestsTest { + + @Test + @DisplayName("DatabaseCreateRequest builder should apply sensible defaults") + void createRequestBuilderAppliesDefaults() { + DatabaseCreateRequest request = + DatabaseCreateRequest.builder( + TestValueFactory.sampleDatabaseName(), + TestValueFactory.sampleDatabaseUser(), + TestValueFactory.samplePassword()) + .build(); + + assertEquals(TestValueFactory.sampleDatabaseName(), request.databaseName()); + assertEquals(TestValueFactory.sampleDatabaseUser(), request.username()); + assertEquals(TestValueFactory.samplePassword(), request.password()); + assertEquals(DatabaseCreateRequest.Type.MYSQL, request.type()); + assertEquals("utf8mb4", request.charset()); + assertEquals(TestValueFactory.sampleDatabaseRemark(), request.remark()); + assertEquals("%", request.dataAccess()); + assertEquals("%", request.address()); + assertEquals("0.0.0.0/0", request.listenIp()); + assertEquals("%", request.host()); + assertEquals(0, request.sid()); + } + + @Test + @DisplayName("DatabaseCreateRequest should reject negative sid") + void createRequestRejectsNegativeSid() { + IllegalArgumentException exception = + assertThrows( + IllegalArgumentException.class, + () -> + DatabaseCreateRequest.builder( + TestValueFactory.sampleDatabaseName(), + TestValueFactory.sampleDatabaseUser(), + TestValueFactory.samplePassword()) + .sid(-1)); + + assertEquals("sid cannot be negative", exception.getMessage()); + } + + @Test + @DisplayName("DatabaseDeleteRequest should require positive id") + void deleteRequestRequiresPositiveId() { + IllegalArgumentException exception = + assertThrows( + IllegalArgumentException.class, + () -> new DatabaseDeleteRequest(TestValueFactory.sampleDatabaseName(), 0)); + + assertEquals("databaseId must be positive", exception.getMessage()); + } + + @Test + @DisplayName("DatabasePasswordUpdateRequest should require non blank password") + void passwordUpdateRequestRequiresNonBlankPassword() { + IllegalArgumentException exception = + assertThrows( + IllegalArgumentException.class, + () -> + new DatabasePasswordUpdateRequest( + TestValueFactory.sampleDatabaseName(), + TestValueFactory.sampleDatabaseUser(), + " ")); + + assertEquals("newPassword cannot be blank", exception.getMessage()); + } +} diff --git a/src/test/java/net/heimeng/sdk/btapi/facade/DatabaseOperationsTest.java b/src/test/java/net/heimeng/sdk/btapi/facade/DatabaseOperationsTest.java new file mode 100644 index 0000000..ff6be0a --- /dev/null +++ b/src/test/java/net/heimeng/sdk/btapi/facade/DatabaseOperationsTest.java @@ -0,0 +1,183 @@ +package net.heimeng.sdk.btapi.facade; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.List; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import net.heimeng.sdk.btapi.api.database.ChangeDatabasePasswordApi; +import net.heimeng.sdk.btapi.api.database.CreateDatabaseApi; +import net.heimeng.sdk.btapi.api.database.DeleteDatabaseApi; +import net.heimeng.sdk.btapi.api.database.GetDatabasesApi; +import net.heimeng.sdk.btapi.client.BtClient; +import net.heimeng.sdk.btapi.model.BtResult; +import net.heimeng.sdk.btapi.model.database.DatabaseInfo; +import net.heimeng.sdk.btapi.testutil.TestValueFactory; + +@ExtendWith(MockitoExtension.class) +@DisplayName("DatabaseOperations facade tests") +class DatabaseOperationsTest { + + @Mock private BtClient client; + + @Test + @DisplayName("list should delegate to GetDatabasesApi") + void listDelegatesToClient() { + DatabaseOperations operations = new DatabaseOperations(client); + when(client.execute(any(GetDatabasesApi.class))).thenReturn(successListResponse()); + + BtResult> result = operations.list(); + + assertNotNull(result); + assertTrue(result.isSuccess()); + assertEquals(1, result.getData().size()); + verify(client).execute(any(GetDatabasesApi.class)); + } + + @Test + @DisplayName("create should map typed request defaults to CreateDatabaseApi") + void createDelegatesToClientWithDefaultRequestValues() { + DatabaseOperations operations = new DatabaseOperations(client); + when(client.execute(any(CreateDatabaseApi.class))).thenReturn(successBoolean()); + + BtResult result = + operations.create( + DatabaseCreateRequest.builder( + TestValueFactory.sampleDatabaseName(), + TestValueFactory.sampleDatabaseUser(), + TestValueFactory.samplePassword()) + .build()); + + assertTrue(result.isSuccess()); + verify(client) + .execute( + argThat( + api -> + api.getEndpoint().equals("database") + && "AddDatabase".equals(api.getParams().get("action")) + && TestValueFactory.sampleDatabaseName().equals(api.getParams().get("name")) + && TestValueFactory.sampleDatabaseUser() + .equals(api.getParams().get("db_user")) + && TestValueFactory.samplePassword().equals(api.getParams().get("password")) + && "utf8mb4".equals(api.getParams().get("codeing")) + && "MySQL".equals(api.getParams().get("dtype")) + && TestValueFactory.sampleDatabaseRemark().equals(api.getParams().get("ps")) + && "%".equals(api.getParams().get("dataAccess")) + && "%".equals(api.getParams().get("address")) + && "0.0.0.0/0".equals(api.getParams().get("listen_ip")) + && "%".equals(api.getParams().get("host")) + && Integer.valueOf(0).equals(api.getParams().get("sid")))); + } + + @Test + @DisplayName("create should apply optional typed request overrides") + void createDelegatesToClientWithCustomRequestValues() { + DatabaseOperations operations = new DatabaseOperations(client); + when(client.execute(any(CreateDatabaseApi.class))).thenReturn(successBoolean()); + + BtResult result = + operations.create( + DatabaseCreateRequest.builder( + TestValueFactory.sampleDatabaseName(), + TestValueFactory.sampleDatabaseUser(), + TestValueFactory.samplePassword()) + .type(DatabaseCreateRequest.Type.MONGODB) + .charset("utf8") + .remark("Production DB") + .dataAccess("127.0.0.1") + .address("127.0.0.1") + .listenIp("127.0.0.1") + .host("localhost") + .sid(3) + .build()); + + assertTrue(result.isSuccess()); + verify(client) + .execute( + argThat( + api -> + "MongoDb".equals(api.getParams().get("dtype")) + && "utf8".equals(api.getParams().get("codeing")) + && "Production DB".equals(api.getParams().get("ps")) + && "127.0.0.1".equals(api.getParams().get("dataAccess")) + && "127.0.0.1".equals(api.getParams().get("address")) + && "127.0.0.1".equals(api.getParams().get("listen_ip")) + && "localhost".equals(api.getParams().get("host")) + && Integer.valueOf(3).equals(api.getParams().get("sid")))); + } + + @Test + @DisplayName("delete should delegate to DeleteDatabaseApi") + void deleteDelegatesToClient() { + DatabaseOperations operations = new DatabaseOperations(client); + when(client.execute(any(DeleteDatabaseApi.class))).thenReturn(successBoolean()); + + BtResult result = + operations.delete(new DatabaseDeleteRequest(TestValueFactory.sampleDatabaseName(), 9)); + + assertTrue(result.isSuccess()); + verify(client) + .execute( + argThat( + api -> + api.getEndpoint().equals("database?action=DeleteDatabase") + && TestValueFactory.sampleDatabaseName().equals(api.getParams().get("name")) + && Integer.valueOf(9).equals(api.getParams().get("id")))); + } + + @Test + @DisplayName("updatePassword should delegate to ChangeDatabasePasswordApi") + void updatePasswordDelegatesToClient() { + DatabaseOperations operations = new DatabaseOperations(client); + when(client.execute(any(ChangeDatabasePasswordApi.class))).thenReturn(successBoolean()); + + BtResult result = + operations.updatePassword( + new DatabasePasswordUpdateRequest( + TestValueFactory.sampleDatabaseName(), + TestValueFactory.sampleDatabaseUser(), + TestValueFactory.updatedSamplePassword())); + + assertTrue(result.isSuccess()); + verify(client) + .execute( + argThat( + api -> + api.getEndpoint().equals("database?action=ChangeDBPassword") + && TestValueFactory.sampleDatabaseName().equals(api.getParams().get("name")) + && TestValueFactory.sampleDatabaseUser() + .equals(api.getParams().get("username")) + && TestValueFactory.updatedSamplePassword() + .equals(api.getParams().get("password")))); + } + + private static BtResult successBoolean() { + BtResult response = new BtResult<>(); + response.setStatus(true); + response.setData(true); + return response; + } + + private static BtResult> successListResponse() { + DatabaseInfo databaseInfo = new DatabaseInfo(); + databaseInfo.setId(1); + databaseInfo.setName(TestValueFactory.sampleDatabaseName()); + databaseInfo.setUsername(TestValueFactory.sampleDatabaseUser()); + + BtResult> response = new BtResult<>(); + response.setStatus(true); + response.setData(List.of(databaseInfo)); + return response; + } +} diff --git a/src/test/java/net/heimeng/sdk/btapi/facade/FtpFacadeRequestsTest.java b/src/test/java/net/heimeng/sdk/btapi/facade/FtpFacadeRequestsTest.java new file mode 100644 index 0000000..57256c3 --- /dev/null +++ b/src/test/java/net/heimeng/sdk/btapi/facade/FtpFacadeRequestsTest.java @@ -0,0 +1,52 @@ +package net.heimeng.sdk.btapi.facade; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import net.heimeng.sdk.btapi.testutil.TestValueFactory; + +@DisplayName("FTP facade request objects") +class FtpFacadeRequestsTest { + + @Test + @DisplayName("FtpCreateRequest factory should default remark to username") + void createRequestFactoryAppliesDefaults() { + FtpCreateRequest request = + FtpCreateRequest.of( + TestValueFactory.sampleFtpUser(), + TestValueFactory.samplePassword(), + TestValueFactory.sampleFtpPath()); + + assertEquals(TestValueFactory.sampleFtpUser(), request.username()); + assertEquals(TestValueFactory.samplePassword(), request.password()); + assertEquals(TestValueFactory.sampleFtpPath(), request.path()); + assertEquals(TestValueFactory.sampleFtpUser(), request.remark()); + } + + @Test + @DisplayName("FtpDeleteRequest should require positive account id") + void deleteRequestRequiresPositiveId() { + IllegalArgumentException exception = + assertThrows( + IllegalArgumentException.class, + () -> new FtpDeleteRequest(0, TestValueFactory.sampleFtpUser())); + + assertEquals("accountId must be positive", exception.getMessage()); + } + + @Test + @DisplayName("FtpPasswordUpdateRequest should require non blank path") + void passwordUpdateRequestRequiresPath() { + IllegalArgumentException exception = + assertThrows( + IllegalArgumentException.class, + () -> + new FtpPasswordUpdateRequest( + 1, TestValueFactory.sampleFtpUser(), " ", TestValueFactory.updatedSamplePassword())); + + assertEquals("path cannot be blank", exception.getMessage()); + } +} diff --git a/src/test/java/net/heimeng/sdk/btapi/facade/FtpOperationsTest.java b/src/test/java/net/heimeng/sdk/btapi/facade/FtpOperationsTest.java new file mode 100644 index 0000000..fabaa05 --- /dev/null +++ b/src/test/java/net/heimeng/sdk/btapi/facade/FtpOperationsTest.java @@ -0,0 +1,141 @@ +package net.heimeng.sdk.btapi.facade; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.List; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import net.heimeng.sdk.btapi.api.ftp.ChangeFtpPasswordApi; +import net.heimeng.sdk.btapi.api.ftp.CreateFtpAccountApi; +import net.heimeng.sdk.btapi.api.ftp.DeleteFtpAccountApi; +import net.heimeng.sdk.btapi.api.ftp.GetFtpAccountsApi; +import net.heimeng.sdk.btapi.client.BtClient; +import net.heimeng.sdk.btapi.model.BtResult; +import net.heimeng.sdk.btapi.model.ftp.FtpAccount; +import net.heimeng.sdk.btapi.testutil.TestValueFactory; + +@ExtendWith(MockitoExtension.class) +@DisplayName("FtpOperations facade tests") +class FtpOperationsTest { + + @Mock private BtClient client; + + @Test + @DisplayName("list should delegate to GetFtpAccountsApi") + void listDelegatesToClient() { + FtpOperations operations = new FtpOperations(client); + when(client.execute(any(GetFtpAccountsApi.class))).thenReturn(successListResponse()); + + BtResult> result = operations.list(); + + assertNotNull(result); + assertTrue(result.isSuccess()); + assertEquals(1, result.getData().size()); + verify(client).execute(any(GetFtpAccountsApi.class)); + } + + @Test + @DisplayName("create should delegate to CreateFtpAccountApi") + void createDelegatesToClient() { + FtpOperations operations = new FtpOperations(client); + when(client.execute(any(CreateFtpAccountApi.class))).thenReturn(successBoolean()); + + BtResult result = + operations.create( + new FtpCreateRequest( + TestValueFactory.sampleFtpUser(), + TestValueFactory.samplePassword(), + TestValueFactory.sampleFtpPath(), + TestValueFactory.sampleFtpRemark())); + + assertTrue(result.isSuccess()); + verify(client) + .execute( + argThat( + api -> + api.getEndpoint().equals("ftp?action=AddUser") + && TestValueFactory.sampleFtpUser() + .equals(api.getParams().get("ftp_username")) + && TestValueFactory.samplePassword() + .equals(api.getParams().get("ftp_password")) + && TestValueFactory.sampleFtpPath().equals(api.getParams().get("path")) + && TestValueFactory.sampleFtpRemark().equals(api.getParams().get("ps")))); + } + + @Test + @DisplayName("delete should delegate to DeleteFtpAccountApi") + void deleteDelegatesToClient() { + FtpOperations operations = new FtpOperations(client); + when(client.execute(any(DeleteFtpAccountApi.class))).thenReturn(successBoolean()); + + BtResult result = + operations.delete(new FtpDeleteRequest(1, TestValueFactory.sampleFtpUser())); + + assertTrue(result.isSuccess()); + verify(client) + .execute( + argThat( + api -> + api.getEndpoint().equals("ftp?action=DeleteUser") + && Integer.valueOf(1).equals(api.getParams().get("id")) + && TestValueFactory.sampleFtpUser().equals(api.getParams().get("username")))); + } + + @Test + @DisplayName("updatePassword should delegate to ChangeFtpPasswordApi") + void updatePasswordDelegatesToClient() { + FtpOperations operations = new FtpOperations(client); + when(client.execute(any(ChangeFtpPasswordApi.class))).thenReturn(successBoolean()); + + BtResult result = + operations.updatePassword( + new FtpPasswordUpdateRequest( + 1, + TestValueFactory.sampleFtpUser(), + TestValueFactory.sampleFtpPath(), + TestValueFactory.updatedSamplePassword())); + + assertTrue(result.isSuccess()); + verify(client) + .execute( + argThat( + api -> + api.getEndpoint().equals("ftp?action=SetUser") + && Integer.valueOf(1).equals(api.getParams().get("id")) + && TestValueFactory.sampleFtpUser() + .equals(api.getParams().get("ftp_username")) + && TestValueFactory.updatedSamplePassword() + .equals(api.getParams().get("new_password")) + && TestValueFactory.sampleFtpPath().equals(api.getParams().get("path")))); + } + + private static BtResult successBoolean() { + BtResult response = new BtResult<>(); + response.setStatus(true); + response.setData(true); + return response; + } + + private static BtResult> successListResponse() { + FtpAccount ftpAccount = new FtpAccount(); + ftpAccount.setId(1); + ftpAccount.setUsername(TestValueFactory.sampleFtpUser()); + ftpAccount.setPath(TestValueFactory.sampleFtpPath()); + + BtResult> response = new BtResult<>(); + response.setStatus(true); + response.setData(List.of(ftpAccount)); + return response; + } +} diff --git a/src/test/java/net/heimeng/sdk/btapi/facade/WebsiteFacadeOptionsTest.java b/src/test/java/net/heimeng/sdk/btapi/facade/WebsiteFacadeOptionsTest.java new file mode 100644 index 0000000..b382ab3 --- /dev/null +++ b/src/test/java/net/heimeng/sdk/btapi/facade/WebsiteFacadeOptionsTest.java @@ -0,0 +1,182 @@ +package net.heimeng.sdk.btapi.facade; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import net.heimeng.sdk.btapi.testutil.TestValueFactory; + +@DisplayName("Website facade option objects") +class WebsiteFacadeOptionsTest { + + @Test + @DisplayName("WebsiteDeleteOptions factories should expose intent clearly") + void deleteOptionsFactoriesExposeIntent() { + WebsiteDeleteOptions none = WebsiteDeleteOptions.none(); + WebsiteDeleteOptions deleteAll = WebsiteDeleteOptions.deleteAll(); + + assertFalse(none.deleteFtp()); + assertFalse(none.deleteDatabase()); + assertFalse(none.deletePath()); + assertTrue(deleteAll.deleteFtp()); + assertTrue(deleteAll.deleteDatabase()); + assertTrue(deleteAll.deletePath()); + } + + @Test + @DisplayName("WebsiteDomainBinding should require non blank values") + void domainBindingRequiresNonBlankValues() { + IllegalArgumentException exception = + assertThrows( + IllegalArgumentException.class, + () -> new WebsiteDomainBinding("demo.example.com", " ")); + + assertEquals("domain cannot be blank", exception.getMessage()); + } + + @Test + @DisplayName("WebsiteDomainRemoval should require positive port") + void domainRemovalRequiresPositivePort() { + IllegalArgumentException exception = + assertThrows( + IllegalArgumentException.class, + () -> new WebsiteDomainRemoval("demo.example.com", "www.demo.example.com", 0)); + + assertEquals("port must be positive", exception.getMessage()); + } + + @Test + @DisplayName("WebsiteLimitNetOptions should reject negative values") + void limitNetOptionsRejectNegativeValues() { + IllegalArgumentException exception = + assertThrows( + IllegalArgumentException.class, () -> new WebsiteLimitNetOptions(true, -1, 30, 1024)); + + assertEquals("perServer cannot be negative", exception.getMessage()); + } + + @Test + @DisplayName("WebsiteSslCertificateOptions should require non blank values") + void sslOptionsRequireNonBlankValues() { + IllegalArgumentException exception = + assertThrows( + IllegalArgumentException.class, + () -> new WebsiteSslCertificateOptions("demo.example.com", " ", "key", Boolean.TRUE)); + + assertEquals("certificate cannot be blank", exception.getMessage()); + } + + @Test + @DisplayName("WebsitePasswordProtectionOptions should require username and password") + void passwordOptionsRequireNonBlankValues() { + IllegalArgumentException exception = + assertThrows( + IllegalArgumentException.class, + () -> new WebsitePasswordProtectionOptions("admin", " ")); + + assertEquals("password cannot be blank", exception.getMessage()); + } + + @Test + @DisplayName("WebsiteRewriteRulesOptions should allow blank content but not blank name") + void rewriteRuleOptionsValidateNameOnly() { + WebsiteRewriteRulesOptions options = new WebsiteRewriteRulesOptions("none", ""); + + assertEquals("none", options.name()); + assertEquals("", options.content()); + } + + @Test + @DisplayName("WebsiteNginxConfigOptions should require non blank domain") + void nginxOptionsRequireNonBlankDomain() { + IllegalArgumentException exception = + assertThrows( + IllegalArgumentException.class, + () -> new WebsiteNginxConfigOptions(" ", "server { listen 80; }")); + + assertEquals("domain cannot be blank", exception.getMessage()); + } + + @Test + @DisplayName("WebsiteCreateRequest builder should apply sensible defaults") + void createRequestBuilderAppliesDefaults() { + WebsiteCreateRequest request = + WebsiteCreateRequest.builder( + TestValueFactory.sampleDomain(), TestValueFactory.sampleSitePath()) + .phpVersion("82") + .build(); + + assertEquals(TestValueFactory.sampleDomain(), request.domain()); + assertEquals(TestValueFactory.sampleSitePath(), request.path()); + assertEquals(0, request.typeId()); + assertEquals("PHP", request.projectType()); + assertEquals("82", request.phpVersion()); + assertEquals(80, request.port()); + assertEquals(TestValueFactory.sampleDomain(), request.remark()); + assertFalse(request.createFtp()); + assertFalse(request.createDatabase()); + } + + @Test + @DisplayName("WebsiteCreateRequest should support optional ftp and database provisioning") + void createRequestSupportsOptionalProvisioning() { + WebsiteCreateRequest request = + WebsiteCreateRequest.builder( + TestValueFactory.sampleDomain(), TestValueFactory.sampleSitePath()) + .typeId(2) + .projectType("Node") + .phpVersion("no") + .port(8080) + .remark("Production site") + .ftpAccount(TestValueFactory.sampleFtpUser(), TestValueFactory.samplePassword()) + .database( + TestValueFactory.sampleDatabaseName(), + TestValueFactory.samplePassword(), + "utf8mb4") + .build(); + + assertTrue(request.createFtp()); + assertEquals(TestValueFactory.sampleFtpUser(), request.ftpAccount().username()); + assertEquals(TestValueFactory.samplePassword(), request.ftpAccount().password()); + assertTrue(request.createDatabase()); + assertEquals(TestValueFactory.sampleDatabaseName(), request.database().username()); + assertEquals(TestValueFactory.samplePassword(), request.database().password()); + assertEquals("utf8mb4", request.database().charset()); + } + + @Test + @DisplayName("WebsiteCreateRequest should require phpVersion before build") + void createRequestRequiresPhpVersion() { + IllegalArgumentException exception = + assertThrows( + IllegalArgumentException.class, + () -> + WebsiteCreateRequest.builder( + TestValueFactory.sampleDomain(), TestValueFactory.sampleSitePath()) + .build()); + + assertEquals("phpVersion cannot be blank", exception.getMessage()); + } + + @Test + @DisplayName("WebsiteCreateRequest should reject invalid nested credentials") + void createRequestRejectsInvalidNestedCredentials() { + IllegalArgumentException ftpException = + assertThrows( + IllegalArgumentException.class, + () -> new WebsiteCreateRequest.FtpAccount(" ", TestValueFactory.samplePassword())); + IllegalArgumentException databaseException = + assertThrows( + IllegalArgumentException.class, + () -> + new WebsiteCreateRequest.Database( + TestValueFactory.sampleDatabaseName(), TestValueFactory.samplePassword(), " ")); + + assertEquals("ftp username cannot be blank", ftpException.getMessage()); + assertEquals("database charset cannot be blank", databaseException.getMessage()); + } +} diff --git a/src/test/java/net/heimeng/sdk/btapi/facade/WebsiteOperationsTest.java b/src/test/java/net/heimeng/sdk/btapi/facade/WebsiteOperationsTest.java new file mode 100644 index 0000000..e5adc68 --- /dev/null +++ b/src/test/java/net/heimeng/sdk/btapi/facade/WebsiteOperationsTest.java @@ -0,0 +1,339 @@ +package net.heimeng.sdk.btapi.facade; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.List; +import java.util.Map; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import net.heimeng.sdk.btapi.api.website.GetPhpVersionsApi; +import net.heimeng.sdk.btapi.api.website.GetWebsiteBackupsApi; +import net.heimeng.sdk.btapi.api.website.GetWebsiteConfigApi; +import net.heimeng.sdk.btapi.api.website.GetWebsiteDetailApi; +import net.heimeng.sdk.btapi.api.website.GetWebsiteDomainsApi; +import net.heimeng.sdk.btapi.api.website.GetWebsiteLimitNetApi; +import net.heimeng.sdk.btapi.api.website.GetWebsiteListApi; +import net.heimeng.sdk.btapi.api.website.GetWebsiteNginxConfigApi; +import net.heimeng.sdk.btapi.api.website.GetWebsitePhpExtensionsApi; +import net.heimeng.sdk.btapi.api.website.GetWebsitePhpVersionApi; +import net.heimeng.sdk.btapi.api.website.GetWebsiteRewriteRulesApi; +import net.heimeng.sdk.btapi.api.website.GetWebsiteRootPathApi; +import net.heimeng.sdk.btapi.api.website.GetWebsiteSslListApi; +import net.heimeng.sdk.btapi.api.website.GetWebsiteTypesApi; +import net.heimeng.sdk.btapi.api.website.GetWebsitesApi; +import net.heimeng.sdk.btapi.client.BtClient; +import net.heimeng.sdk.btapi.model.BtResult; +import net.heimeng.sdk.btapi.model.website.PhpVersion; +import net.heimeng.sdk.btapi.model.website.WebsiteInfo; +import net.heimeng.sdk.btapi.model.website.WebsiteType; + +@ExtendWith(MockitoExtension.class) +@DisplayName("WebsiteOperations read facade tests") +class WebsiteOperationsTest { + + @Mock private BtClient client; + + @Test + @DisplayName("list(page, limit) should delegate to GetWebsitesApi") + void listWithPaginationDelegatesToClient() { + WebsiteOperations operations = new WebsiteOperations(client); + when(client.execute(any(GetWebsitesApi.class))).thenReturn(successWebsiteList()); + + BtResult> result = operations.list(1, 20); + + assertNotNull(result); + assertTrue(result.isSuccess()); + assertEquals(1, result.getData().size()); + verify(client).execute(any(GetWebsitesApi.class)); + } + + @Test + @DisplayName("listRaw should delegate to GetWebsiteListApi") + void listRawDelegatesToClient() { + WebsiteOperations operations = new WebsiteOperations(client); + when(client.execute(any(GetWebsiteListApi.class))).thenReturn(successRawWebsiteList()); + + BtResult>> result = operations.listRaw(1, 20, -1, "id desc", "demo"); + + assertNotNull(result); + assertTrue(result.isSuccess()); + assertEquals(1, result.getData().size()); + verify(client).execute(any(GetWebsiteListApi.class)); + } + + @Test + @DisplayName("getDetail should delegate to GetWebsiteDetailApi") + void getDetailDelegatesToClient() { + WebsiteOperations operations = new WebsiteOperations(client); + when(client.execute(any(GetWebsiteDetailApi.class))).thenReturn(successWebsiteDetail()); + + BtResult> result = operations.getDetail(8); + + assertTrue(result.isSuccess()); + assertEquals("demo.example.com", result.getData().get("name")); + verify(client).execute(any(GetWebsiteDetailApi.class)); + } + + @Test + @DisplayName("getConfig should delegate to GetWebsiteConfigApi") + void getConfigDelegatesToClient() { + WebsiteOperations operations = new WebsiteOperations(client); + when(client.execute(any(GetWebsiteConfigApi.class))).thenReturn(successWebsiteConfig()); + + BtResult> result = operations.getConfig(8, "/www/wwwroot/demo"); + + assertTrue(result.isSuccess()); + assertEquals(true, result.getData().get("logs")); + verify(client).execute(any(GetWebsiteConfigApi.class)); + } + + @Test + @DisplayName("listDomains should delegate to GetWebsiteDomainsApi") + void listDomainsDelegatesToClient() { + WebsiteOperations operations = new WebsiteOperations(client); + when(client.execute(any(GetWebsiteDomainsApi.class))).thenReturn(successDomains()); + + BtResult>> result = operations.listDomains(66); + + assertTrue(result.isSuccess()); + assertEquals(1, result.getData().size()); + verify(client).execute(any(GetWebsiteDomainsApi.class)); + } + + @Test + @DisplayName("listTypes should delegate to GetWebsiteTypesApi") + void listTypesDelegatesToClient() { + WebsiteOperations operations = new WebsiteOperations(client); + when(client.execute(any(GetWebsiteTypesApi.class))).thenReturn(successWebsiteTypes()); + + BtResult> result = operations.listTypes(); + + assertTrue(result.isSuccess()); + assertEquals(1, result.getData().size()); + verify(client).execute(any(GetWebsiteTypesApi.class)); + } + + @Test + @DisplayName("listPhpVersions should delegate to GetPhpVersionsApi") + void listPhpVersionsDelegatesToClient() { + WebsiteOperations operations = new WebsiteOperations(client); + when(client.execute(any(GetPhpVersionsApi.class))).thenReturn(successPhpVersions()); + + BtResult> result = operations.listPhpVersions(); + + assertTrue(result.isSuccess()); + assertEquals(1, result.getData().size()); + verify(client).execute(any(GetPhpVersionsApi.class)); + } + + @Test + @DisplayName("getRootPath should delegate to GetWebsiteRootPathApi") + void getRootPathDelegatesToClient() { + WebsiteOperations operations = new WebsiteOperations(client); + when(client.execute(any(GetWebsiteRootPathApi.class))).thenReturn(successString("/www/demo")); + + BtResult result = operations.getRootPath(8); + + assertTrue(result.isSuccess()); + assertEquals("/www/demo", result.getData()); + verify(client).execute(any(GetWebsiteRootPathApi.class)); + } + + @Test + @DisplayName("getPhpVersion should delegate to GetWebsitePhpVersionApi") + void getPhpVersionDelegatesToClient() { + WebsiteOperations operations = new WebsiteOperations(client); + when(client.execute(any(GetWebsitePhpVersionApi.class))).thenReturn(successString("82")); + + BtResult result = operations.getPhpVersion(8); + + assertTrue(result.isSuccess()); + assertEquals("82", result.getData()); + verify(client).execute(any(GetWebsitePhpVersionApi.class)); + } + + @Test + @DisplayName("listPhpExtensions should delegate to GetWebsitePhpExtensionsApi") + void listPhpExtensionsDelegatesToClient() { + WebsiteOperations operations = new WebsiteOperations(client); + when(client.execute(any(GetWebsitePhpExtensionsApi.class))).thenReturn(successExtensions()); + + BtResult>> result = operations.listPhpExtensions(8); + + assertTrue(result.isSuccess()); + assertEquals(1, result.getData().size()); + verify(client).execute(any(GetWebsitePhpExtensionsApi.class)); + } + + @Test + @DisplayName("getRewriteRules should delegate to GetWebsiteRewriteRulesApi") + void getRewriteRulesDelegatesToClient() { + WebsiteOperations operations = new WebsiteOperations(client); + when(client.execute(any(GetWebsiteRewriteRulesApi.class))) + .thenReturn(successString("rewrite ^/(.*)$ /index.php;")); + + BtResult result = operations.getRewriteRules(8); + + assertTrue(result.isSuccess()); + assertEquals("rewrite ^/(.*)$ /index.php;", result.getData()); + verify(client).execute(any(GetWebsiteRewriteRulesApi.class)); + } + + @Test + @DisplayName("getNginxConfig should delegate to GetWebsiteNginxConfigApi") + void getNginxConfigDelegatesToClient() { + WebsiteOperations operations = new WebsiteOperations(client); + when(client.execute(any(GetWebsiteNginxConfigApi.class))) + .thenReturn(successString("server { listen 80; }")); + + BtResult result = operations.getNginxConfig(8, "demo.example.com"); + + assertTrue(result.isSuccess()); + assertEquals("server { listen 80; }", result.getData()); + verify(client).execute(any(GetWebsiteNginxConfigApi.class)); + } + + @Test + @DisplayName("listSslCertificates should delegate to GetWebsiteSslListApi") + void listSslCertificatesDelegatesToClient() { + WebsiteOperations operations = new WebsiteOperations(client); + when(client.execute(any(GetWebsiteSslListApi.class))).thenReturn(successSslCertificates()); + + BtResult>> result = operations.listSslCertificates(8); + + assertTrue(result.isSuccess()); + assertEquals(1, result.getData().size()); + verify(client).execute(any(GetWebsiteSslListApi.class)); + } + + @Test + @DisplayName("getLimitNet should delegate to GetWebsiteLimitNetApi") + void getLimitNetDelegatesToClient() { + WebsiteOperations operations = new WebsiteOperations(client); + when(client.execute(any(GetWebsiteLimitNetApi.class))).thenReturn(successLimitNet()); + + BtResult> result = operations.getLimitNet(8); + + assertTrue(result.isSuccess()); + assertEquals(300, result.getData().get("perserver")); + verify(client).execute(any(GetWebsiteLimitNetApi.class)); + } + + @Test + @DisplayName("listBackups should delegate to GetWebsiteBackupsApi") + void listBackupsDelegatesToClient() { + WebsiteOperations operations = new WebsiteOperations(client); + when(client.execute(any(GetWebsiteBackupsApi.class))).thenReturn(successBackups()); + + BtResult>> result = operations.listBackups(8, 1, 5, "load_backups"); + + assertTrue(result.isSuccess()); + assertEquals(1, result.getData().size()); + verify(client).execute(any(GetWebsiteBackupsApi.class)); + } + + private static BtResult> successWebsiteList() { + WebsiteInfo websiteInfo = new WebsiteInfo(); + websiteInfo.setName("demo.example.com"); + + BtResult> response = new BtResult<>(); + response.setStatus(true); + response.setData(List.of(websiteInfo)); + return response; + } + + private static BtResult>> successRawWebsiteList() { + BtResult>> response = new BtResult<>(); + response.setStatus(true); + response.setData(List.of(Map.of("name", "demo.example.com"))); + return response; + } + + private static BtResult> successWebsiteDetail() { + BtResult> response = new BtResult<>(); + response.setStatus(true); + response.setData(Map.of("name", "demo.example.com")); + return response; + } + + private static BtResult> successWebsiteConfig() { + BtResult> response = new BtResult<>(); + response.setStatus(true); + response.setData(Map.of("logs", true)); + return response; + } + + private static BtResult>> successDomains() { + BtResult>> response = new BtResult<>(); + response.setStatus(true); + response.setData(List.of(Map.of("name", "demo.example.com"))); + return response; + } + + private static BtResult> successWebsiteTypes() { + WebsiteType websiteType = new WebsiteType(); + websiteType.setId(0); + websiteType.setName("Default"); + + BtResult> response = new BtResult<>(); + response.setStatus(true); + response.setData(List.of(websiteType)); + return response; + } + + private static BtResult> successPhpVersions() { + PhpVersion phpVersion = new PhpVersion(); + phpVersion.setVersion("82"); + phpVersion.setName("PHP-82"); + + BtResult> response = new BtResult<>(); + response.setStatus(true); + response.setData(List.of(phpVersion)); + return response; + } + + private static BtResult>> successExtensions() { + BtResult>> response = new BtResult<>(); + response.setStatus(true); + response.setData(List.of(Map.of("name", "redis"))); + return response; + } + + private static BtResult successString(String data) { + BtResult response = new BtResult<>(); + response.setStatus(true); + response.setData(data); + return response; + } + + private static BtResult>> successSslCertificates() { + BtResult>> response = new BtResult<>(); + response.setStatus(true); + response.setData(List.of(Map.of("subject", "demo.example.com"))); + return response; + } + + private static BtResult> successLimitNet() { + BtResult> response = new BtResult<>(); + response.setStatus(true); + response.setData(Map.of("perserver", 300)); + return response; + } + + private static BtResult>> successBackups() { + BtResult>> response = new BtResult<>(); + response.setStatus(true); + response.setData(List.of(Map.of("name", "backup.tar.gz"))); + return response; + } +} diff --git a/src/test/java/net/heimeng/sdk/btapi/facade/WebsiteOperationsWriteTest.java b/src/test/java/net/heimeng/sdk/btapi/facade/WebsiteOperationsWriteTest.java new file mode 100644 index 0000000..7cb4d87 --- /dev/null +++ b/src/test/java/net/heimeng/sdk/btapi/facade/WebsiteOperationsWriteTest.java @@ -0,0 +1,422 @@ +package net.heimeng.sdk.btapi.facade; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.Map; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import net.heimeng.sdk.btapi.api.website.AddWebsiteDomainApi; +import net.heimeng.sdk.btapi.api.website.CloseWebsitePasswordApi; +import net.heimeng.sdk.btapi.api.website.CloseWebsiteSslApi; +import net.heimeng.sdk.btapi.api.website.CreateWebsiteApi; +import net.heimeng.sdk.btapi.api.website.CreateWebsiteBackupApi; +import net.heimeng.sdk.btapi.api.website.DeleteWebsiteApi; +import net.heimeng.sdk.btapi.api.website.DeleteWebsiteBackupApi; +import net.heimeng.sdk.btapi.api.website.DeleteWebsiteDomainApi; +import net.heimeng.sdk.btapi.api.website.SetWebsiteLimitNetApi; +import net.heimeng.sdk.btapi.api.website.SetWebsiteLogsApi; +import net.heimeng.sdk.btapi.api.website.SetWebsiteNginxConfigApi; +import net.heimeng.sdk.btapi.api.website.SetWebsitePasswordApi; +import net.heimeng.sdk.btapi.api.website.SetWebsitePhpExtensionsApi; +import net.heimeng.sdk.btapi.api.website.SetWebsitePhpVersionApi; +import net.heimeng.sdk.btapi.api.website.SetWebsitePsApi; +import net.heimeng.sdk.btapi.api.website.SetWebsiteRewriteRulesApi; +import net.heimeng.sdk.btapi.api.website.SetWebsiteRootPathApi; +import net.heimeng.sdk.btapi.api.website.SetWebsiteRunPathApi; +import net.heimeng.sdk.btapi.api.website.SetWebsiteSslApi; +import net.heimeng.sdk.btapi.api.website.SetWebsiteUserIniApi; +import net.heimeng.sdk.btapi.api.website.StartWebsiteApi; +import net.heimeng.sdk.btapi.api.website.StopWebsiteApi; +import net.heimeng.sdk.btapi.client.BtClient; +import net.heimeng.sdk.btapi.model.BtResult; +import net.heimeng.sdk.btapi.model.website.CreateWebsiteResult; +import net.heimeng.sdk.btapi.testutil.TestValueFactory; + +@ExtendWith(MockitoExtension.class) +@DisplayName("WebsiteOperations write facade tests") +class WebsiteOperationsWriteTest { + + @Mock private BtClient client; + + @Test + @DisplayName("create should map typed request defaults to CreateWebsiteApi") + void createDelegatesToClientWithDefaultRequestValues() { + WebsiteOperations operations = new WebsiteOperations(client); + when(client.execute(any(CreateWebsiteApi.class))).thenReturn(successCreate()); + + BtResult result = + operations.create( + WebsiteCreateRequest.builder( + TestValueFactory.sampleDomain(), TestValueFactory.sampleSitePath()) + .phpVersion("82") + .build()); + + assertTrue(result.isSuccess()); + + ArgumentCaptor captor = ArgumentCaptor.forClass(CreateWebsiteApi.class); + verify(client).execute(captor.capture()); + + Map params = captor.getValue().getParams(); + assertEquals(TestValueFactory.sampleSitePath(), params.get("path")); + assertEquals(0, params.get("type_id")); + assertEquals("PHP", params.get("type")); + assertEquals("82", params.get("version")); + assertEquals(80, params.get("port")); + assertEquals(TestValueFactory.sampleDomain(), params.get("ps")); + assertEquals(Boolean.FALSE, params.get("ftp")); + assertEquals(Boolean.FALSE, params.get("sql")); + assertFalse(params.containsKey("ftp_username")); + assertFalse(params.containsKey("datauser")); + assertNotNull(params.get("webname")); + assertTrue( + params.get("webname") + .toString() + .contains("\"domain\":\"" + TestValueFactory.sampleDomain() + "\"")); + } + + @Test + @DisplayName("create should include optional ftp and database provisioning") + void createDelegatesToClientWithOptionalProvisioning() { + WebsiteOperations operations = new WebsiteOperations(client); + when(client.execute(any(CreateWebsiteApi.class))).thenReturn(successCreate()); + + BtResult result = + operations.create( + WebsiteCreateRequest.builder( + TestValueFactory.sampleDomain(), TestValueFactory.sampleSitePath()) + .typeId(3) + .projectType("Node") + .phpVersion("no") + .port(8080) + .remark("Production") + .ftpAccount(TestValueFactory.sampleFtpUser(), TestValueFactory.samplePassword()) + .database( + TestValueFactory.sampleDatabaseName(), + TestValueFactory.samplePassword(), + "utf8mb4") + .build()); + + assertTrue(result.isSuccess()); + + ArgumentCaptor captor = ArgumentCaptor.forClass(CreateWebsiteApi.class); + verify(client).execute(captor.capture()); + + Map params = captor.getValue().getParams(); + assertEquals(3, params.get("type_id")); + assertEquals("Node", params.get("type")); + assertEquals("no", params.get("version")); + assertEquals(8080, params.get("port")); + assertEquals("Production", params.get("ps")); + assertEquals(Boolean.TRUE, params.get("ftp")); + assertEquals(TestValueFactory.sampleFtpUser(), params.get("ftp_username")); + assertEquals(TestValueFactory.samplePassword(), params.get("ftp_password")); + assertEquals(Boolean.TRUE, params.get("sql")); + assertEquals(TestValueFactory.sampleDatabaseName(), params.get("datauser")); + assertEquals(TestValueFactory.samplePassword(), params.get("datapassword")); + assertEquals("utf8mb4", params.get("codeing")); + } + + @Test + @DisplayName("addDomain should delegate to AddWebsiteDomainApi") + void addDomainDelegatesToClient() { + WebsiteOperations operations = new WebsiteOperations(client); + when(client.execute(any(AddWebsiteDomainApi.class))).thenReturn(successBoolean()); + + BtResult result = + operations.addDomain( + 8, + new WebsiteDomainBinding( + TestValueFactory.sampleDomain(), TestValueFactory.sampleWwwDomain())); + + assertTrue(result.isSuccess()); + verify(client).execute(any(AddWebsiteDomainApi.class)); + } + + @Test + @DisplayName("removeDomain should delegate to DeleteWebsiteDomainApi") + void removeDomainDelegatesToClient() { + WebsiteOperations operations = new WebsiteOperations(client); + when(client.execute(any(DeleteWebsiteDomainApi.class))).thenReturn(successBoolean()); + + BtResult result = + operations.removeDomain( + 8, + new WebsiteDomainRemoval( + TestValueFactory.sampleDomain(), TestValueFactory.sampleWwwDomain(), 80)); + + assertTrue(result.isSuccess()); + verify(client).execute(any(DeleteWebsiteDomainApi.class)); + } + + @Test + @DisplayName("delete should delegate to DeleteWebsiteApi") + void deleteDelegatesToClient() { + WebsiteOperations operations = new WebsiteOperations(client); + when(client.execute(any(DeleteWebsiteApi.class))).thenReturn(successBoolean()); + + BtResult result = + operations.delete(8, TestValueFactory.sampleDomain(), new WebsiteDeleteOptions(true, true, false)); + + assertTrue(result.isSuccess()); + verify(client).execute(any(DeleteWebsiteApi.class)); + } + + @Test + @DisplayName("start should delegate to StartWebsiteApi") + void startDelegatesToClient() { + WebsiteOperations operations = new WebsiteOperations(client); + when(client.execute(any(StartWebsiteApi.class))).thenReturn(successBoolean()); + + BtResult result = operations.start(8); + + assertTrue(result.isSuccess()); + verify(client).execute(any(StartWebsiteApi.class)); + } + + @Test + @DisplayName("stop should delegate to StopWebsiteApi") + void stopDelegatesToClient() { + WebsiteOperations operations = new WebsiteOperations(client); + when(client.execute(any(StopWebsiteApi.class))).thenReturn(successBoolean()); + + BtResult result = operations.stop(8); + + assertTrue(result.isSuccess()); + verify(client).execute(any(StopWebsiteApi.class)); + } + + @Test + @DisplayName("updateRemark should delegate to SetWebsitePsApi") + void updateRemarkDelegatesToClient() { + WebsiteOperations operations = new WebsiteOperations(client); + when(client.execute(any(SetWebsitePsApi.class))).thenReturn(successBoolean()); + + BtResult result = operations.updateRemark(8, "production"); + + assertTrue(result.isSuccess()); + verify(client).execute(any(SetWebsitePsApi.class)); + } + + @Test + @DisplayName("updateRootPath should delegate to SetWebsiteRootPathApi") + void updateRootPathDelegatesToClient() { + WebsiteOperations operations = new WebsiteOperations(client); + when(client.execute(any(SetWebsiteRootPathApi.class))).thenReturn(successBoolean()); + + BtResult result = operations.updateRootPath(8, TestValueFactory.sampleSitePath()); + + assertTrue(result.isSuccess()); + verify(client).execute(any(SetWebsiteRootPathApi.class)); + } + + @Test + @DisplayName("updateRunPath should delegate to SetWebsiteRunPathApi") + void updateRunPathDelegatesToClient() { + WebsiteOperations operations = new WebsiteOperations(client); + when(client.execute(any(SetWebsiteRunPathApi.class))).thenReturn(successBoolean()); + + BtResult result = operations.updateRunPath(8, "public"); + + assertTrue(result.isSuccess()); + verify(client).execute(any(SetWebsiteRunPathApi.class)); + } + + @Test + @DisplayName("toggleUserIni should delegate to SetWebsiteUserIniApi") + void toggleUserIniDelegatesToClient() { + WebsiteOperations operations = new WebsiteOperations(client); + when(client.execute(any(SetWebsiteUserIniApi.class))).thenReturn(successBoolean()); + + BtResult result = operations.toggleUserIni("/www/wwwroot/demo"); + + assertTrue(result.isSuccess()); + verify(client).execute(any(SetWebsiteUserIniApi.class)); + } + + @Test + @DisplayName("updatePhpVersion should delegate to SetWebsitePhpVersionApi") + void updatePhpVersionDelegatesToClient() { + WebsiteOperations operations = new WebsiteOperations(client); + when(client.execute(any(SetWebsitePhpVersionApi.class))).thenReturn(successBoolean()); + + BtResult result = operations.updatePhpVersion(8, "82"); + + assertTrue(result.isSuccess()); + verify(client).execute(any(SetWebsitePhpVersionApi.class)); + } + + @Test + @DisplayName("updatePhpExtension should delegate to SetWebsitePhpExtensionsApi") + void updatePhpExtensionDelegatesToClient() { + WebsiteOperations operations = new WebsiteOperations(client); + when(client.execute(any(SetWebsitePhpExtensionsApi.class))).thenReturn(successBoolean()); + + BtResult result = operations.updatePhpExtension(8, "redis", true); + + assertTrue(result.isSuccess()); + verify(client).execute(any(SetWebsitePhpExtensionsApi.class)); + } + + @Test + @DisplayName("updateRewriteRules should delegate to SetWebsiteRewriteRulesApi") + void updateRewriteRulesDelegatesToClient() { + WebsiteOperations operations = new WebsiteOperations(client); + when(client.execute(any(SetWebsiteRewriteRulesApi.class))).thenReturn(successBoolean()); + + BtResult result = + operations.updateRewriteRules( + 8, new WebsiteRewriteRulesOptions("none", "rewrite ^ /index.php;")); + + assertTrue(result.isSuccess()); + verify(client).execute(any(SetWebsiteRewriteRulesApi.class)); + } + + @Test + @DisplayName("updateNginxConfig should delegate to SetWebsiteNginxConfigApi") + void updateNginxConfigDelegatesToClient() { + WebsiteOperations operations = new WebsiteOperations(client); + when(client.execute(any(SetWebsiteNginxConfigApi.class))).thenReturn(successBoolean()); + + BtResult result = + operations.updateNginxConfig( + 8, + new WebsiteNginxConfigOptions( + TestValueFactory.sampleDomain(), "server { listen 80; }")); + + assertTrue(result.isSuccess()); + verify(client).execute(any(SetWebsiteNginxConfigApi.class)); + } + + @Test + @DisplayName("enablePasswordProtection should delegate to SetWebsitePasswordApi") + void enablePasswordProtectionDelegatesToClient() { + WebsiteOperations operations = new WebsiteOperations(client); + when(client.execute(any(SetWebsitePasswordApi.class))).thenReturn(successBoolean()); + + BtResult result = + operations.enablePasswordProtection( + 8, + new WebsitePasswordProtectionOptions("admin", TestValueFactory.samplePassword())); + + assertTrue(result.isSuccess()); + verify(client).execute(any(SetWebsitePasswordApi.class)); + } + + @Test + @DisplayName("disablePasswordProtection should delegate to CloseWebsitePasswordApi") + void disablePasswordProtectionDelegatesToClient() { + WebsiteOperations operations = new WebsiteOperations(client); + when(client.execute(any(CloseWebsitePasswordApi.class))).thenReturn(successBoolean()); + + BtResult result = operations.disablePasswordProtection(8); + + assertTrue(result.isSuccess()); + verify(client).execute(any(CloseWebsitePasswordApi.class)); + } + + @Test + @DisplayName("installSslCertificate should delegate to SetWebsiteSslApi") + void installSslCertificateDelegatesToClient() { + WebsiteOperations operations = new WebsiteOperations(client); + when(client.execute(any(SetWebsiteSslApi.class))).thenReturn(successBoolean()); + + BtResult result = + operations.installSslCertificate( + 8, + new WebsiteSslCertificateOptions( + TestValueFactory.sampleDomain(), "cert", "key", Boolean.TRUE)); + + assertTrue(result.isSuccess()); + verify(client).execute(any(SetWebsiteSslApi.class)); + } + + @Test + @DisplayName("disableSsl should delegate to CloseWebsiteSslApi") + void disableSslDelegatesToClient() { + WebsiteOperations operations = new WebsiteOperations(client); + when(client.execute(any(CloseWebsiteSslApi.class))).thenReturn(successBoolean()); + + BtResult result = operations.disableSsl(8); + + assertTrue(result.isSuccess()); + verify(client).execute(any(CloseWebsiteSslApi.class)); + } + + @Test + @DisplayName("toggleLogs should delegate to SetWebsiteLogsApi") + void toggleLogsDelegatesToClient() { + WebsiteOperations operations = new WebsiteOperations(client); + when(client.execute(any(SetWebsiteLogsApi.class))).thenReturn(successBoolean()); + + BtResult result = operations.toggleLogs(8); + + assertTrue(result.isSuccess()); + verify(client).execute(any(SetWebsiteLogsApi.class)); + } + + @Test + @DisplayName("updateLimitNet should delegate to SetWebsiteLimitNetApi") + void updateLimitNetDelegatesToClient() { + WebsiteOperations operations = new WebsiteOperations(client); + when(client.execute(any(SetWebsiteLimitNetApi.class))).thenReturn(successBoolean()); + + BtResult result = + operations.updateLimitNet(8, new WebsiteLimitNetOptions(true, 300, 30, 1024)); + + assertTrue(result.isSuccess()); + verify(client).execute(any(SetWebsiteLimitNetApi.class)); + } + + @Test + @DisplayName("createBackup should delegate to CreateWebsiteBackupApi") + void createBackupDelegatesToClient() { + WebsiteOperations operations = new WebsiteOperations(client); + when(client.execute(any(CreateWebsiteBackupApi.class))).thenReturn(successBoolean()); + + BtResult result = operations.createBackup(8); + + assertTrue(result.isSuccess()); + verify(client).execute(any(CreateWebsiteBackupApi.class)); + } + + @Test + @DisplayName("deleteBackup should delegate to DeleteWebsiteBackupApi") + void deleteBackupDelegatesToClient() { + WebsiteOperations operations = new WebsiteOperations(client); + when(client.execute(any(DeleteWebsiteBackupApi.class))).thenReturn(successBoolean()); + + BtResult result = operations.deleteBackup(18); + + assertTrue(result.isSuccess()); + verify(client).execute(any(DeleteWebsiteBackupApi.class)); + } + + private static BtResult successBoolean() { + BtResult response = new BtResult<>(); + response.setStatus(true); + response.setData(true); + return response; + } + + private static BtResult successCreate() { + CreateWebsiteResult createWebsiteResult = new CreateWebsiteResult(); + createWebsiteResult.setSiteStatus(true); + + BtResult response = new BtResult<>(); + response.setStatus(true); + response.setData(createWebsiteResult); + return response; + } +} diff --git a/src/test/java/net/heimeng/sdk/btapi/integration/DatabaseIntegrationTest.java b/src/test/java/net/heimeng/sdk/btapi/integration/DatabaseIntegrationTest.java index 7f3d3fb..55f55b8 100644 --- a/src/test/java/net/heimeng/sdk/btapi/integration/DatabaseIntegrationTest.java +++ b/src/test/java/net/heimeng/sdk/btapi/integration/DatabaseIntegrationTest.java @@ -1,281 +1,181 @@ package net.heimeng.sdk.btapi.integration; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.List; +import java.util.concurrent.TimeUnit; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Timeout; +import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; -import net.heimeng.sdk.btapi.api.database.CreateDatabaseApi; -import net.heimeng.sdk.btapi.api.database.DeleteDatabaseApi; import net.heimeng.sdk.btapi.api.database.GetDatabasesApi; import net.heimeng.sdk.btapi.client.BtApiManager; -import net.heimeng.sdk.btapi.client.BtClient; -import net.heimeng.sdk.btapi.client.BtClientFactory; -import net.heimeng.sdk.btapi.config.BtSdkConfig; import net.heimeng.sdk.btapi.exception.BtApiException; +import net.heimeng.sdk.btapi.facade.DatabaseCreateRequest; +import net.heimeng.sdk.btapi.facade.DatabaseDeleteRequest; import net.heimeng.sdk.btapi.model.BtResult; import net.heimeng.sdk.btapi.model.database.DatabaseInfo; -import org.junit.jupiter.api.*; -import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import java.io.IOException; -import java.util.List; -import java.util.Properties; -import java.util.UUID; -import java.util.concurrent.TimeUnit; - -/** - * 数据库管理API集成测试类 - *

注意:这些测试会实际调用宝塔面板API,可能会产生实际效果

- * - *

- * 测试运行条件: - * 1. 需要设置环境变量ENABLE_INTEGRATION_TESTS=true来启用集成测试 - * 2. 需要提供有效的宝塔面板API配置(baseUrl和apiKey) - *

- * - *

CI/CD最佳实践: - * - 测试默认禁用,避免在日常开发中意外执行 - * - 可以通过环境变量控制测试执行 - * - 测试资源会自动清理 - * - 测试相互隔离,不依赖于执行顺序 - * - 测试有明确的超时限制 - *

- * - *

测试策略: - * - 每个测试方法都有前置检查和后置验证 - * - 测试数据使用随机后缀避免冲突 - * - 即使测试失败也会尝试清理资源 - * - 避免测试间依赖,每个测试独立管理资源 - *

- */ +@DisplayName("Database integration tests") @EnabledIfEnvironmentVariable(named = "ENABLE_INTEGRATION_TESTS", matches = "true") -@TestInstance(TestInstance.Lifecycle.PER_METHOD) // 每个测试独立实例,避免状态共享 @Timeout(value = 60, unit = TimeUnit.SECONDS) -public class DatabaseIntegrationTest { - - private static final Logger logger = LoggerFactory.getLogger(DatabaseIntegrationTest.class); - - private static final String PROPERTIES_FILE = "application-test.properties"; - private static final String ENV_BASE_URL = "baseUrl"; - private static final String ENV_API_KEY = "apiKey"; - private static final String ENV_TEST_DB_NAME = "test.dbName"; - private static final String ENV_TEST_DB_USER = "test.dbUser"; - private static final String ENV_TEST_DB_PASSWORD = "test.dbPassword"; - - private BtClient client; - private BtApiManager apiManager; - private Properties testProperties; - private String testDbName; - private String testDbUser; - private String testDbPassword; - private String testDbId; - - @BeforeEach - void setUp() throws IOException { - // 1. 加载测试配置 - testProperties = new Properties(); - try (var stream = getClass().getClassLoader().getResourceAsStream(PROPERTIES_FILE)) { - if (stream == null) { - throw new IOException("配置文件 " + PROPERTIES_FILE + " 不存在"); - } - testProperties.load(stream); - } catch (IOException e) { - logger.error("无法加载测试配置文件 {}", PROPERTIES_FILE, e); - throw e; - } - - // 2. 验证必要配置 - testDbName = getConfiguration(ENV_TEST_DB_NAME); - testDbUser = getConfiguration(ENV_TEST_DB_USER); - testDbPassword = getConfiguration(ENV_TEST_DB_PASSWORD); - String baseUrl = getConfiguration(ENV_BASE_URL); - String apiKey = getConfiguration(ENV_API_KEY); - - assertNotNull(baseUrl, "baseUrl配置不能为空"); - assertNotNull(apiKey, "apiKey配置不能为空"); - assertNotNull(testDbName, "test.dbName配置不能为空"); - assertNotNull(testDbUser, "test.dbUser配置不能为空"); - assertNotNull(testDbPassword, "test.dbPassword配置不能为空"); - - // 3. 生成随机ID避免冲突 - String randomId = UUID.randomUUID().toString().substring(0, 8); - testDbName = testDbName + "_" + randomId; - testDbUser = testDbUser + "_" + randomId; - - logger.info("测试配置:baseUrl=****, testDbName={}, testDbUser={}", testDbName, testDbUser); - - // 4. 创建客户端 - BtSdkConfig config = BtSdkConfig.builder() - .baseUrl(baseUrl) - .apiKey(apiKey) - .connectTimeout(Integer.parseInt(testProperties.getProperty("connectTimeout", "5000"))) - .readTimeout(Integer.parseInt(testProperties.getProperty("readTimeout", "10000"))) - .retryCount(Integer.parseInt(testProperties.getProperty("retryCount", "3"))) - .build(); - - client = BtClientFactory.createClient(config); - apiManager = new BtApiManager(client); +class DatabaseIntegrationTest extends AbstractIntegrationTestSupport { + + private static final Logger logger = LoggerFactory.getLogger(DatabaseIntegrationTest.class); + + private BtApiManager apiManager; + private String testDbName; + private String testDbUser; + private String testDbPassword; + + @BeforeEach + void setUp() { + assumeConfigurationPresent(ENV_BASE_URL, "baseUrl"); + assumeConfigurationPresent(ENV_API_KEY, "apiKey"); + assumeConfigurationPresent(ENV_TEST_DB_NAME, "test.dbName"); + assumeConfigurationPresent(ENV_TEST_DB_USER, "test.dbUser"); + assumeConfigurationPresent(ENV_TEST_DB_PASSWORD, "test.dbPassword"); + + apiManager = createApiManager(); + assumePanelApiAccessible(apiManager); + + String suffix = uniqueSuffix(); + testDbName = getRequiredConfiguration(ENV_TEST_DB_NAME, "test.dbName") + "_" + suffix; + testDbUser = getRequiredConfiguration(ENV_TEST_DB_USER, "test.dbUser") + "_" + suffix; + testDbPassword = getRequiredConfiguration(ENV_TEST_DB_PASSWORD, "test.dbPassword"); + + logger.info( + "Database integration test initialized, testDbName={}, testDbUser={}", + testDbName, + testDbUser); + } + + @AfterEach + void tearDown() { + try { + deleteDatabaseIfExists(testDbName); + } finally { + closeQuietly(apiManager); } - - @AfterEach - void tearDown() { - // 清理测试创建的数据库(即使测试失败也会执行) - if (apiManager != null && testDbName != null) { - try { - if (isDatabaseExists(testDbName)) { - logger.info("测试后清理数据库: {}", testDbName); - DatabaseInfo dbInfo = getDatabaseInfoByName(testDbName); - if (dbInfo != null) { - DeleteDatabaseApi deleteApi = new DeleteDatabaseApi(testDbName, dbInfo.getId()); - BtResult result = apiManager.execute(deleteApi); - if (result.isSuccess()) { - logger.info("测试数据库清理成功: {}", testDbName); - } else { - logger.warn("测试数据库清理失败: {}, 原因: {}", testDbName, result.getMsg()); - } - } - } - } catch (Exception e) { - logger.error("测试数据库清理时发生异常", e); - } finally { - // 确保资源释放 - try { - if (apiManager != null) { - apiManager.close(); - } - } catch (Exception e) { - logger.error("关闭API管理器时发生异常", e); - } - } - } - // 清空引用以便垃圾回收 - client = null; - apiManager = null; - testProperties = null; - } - - private String getConfiguration(String key) { - String value = System.getenv(key); - if (value == null && testProperties != null) { - value = testProperties.getProperty(key); - } - return value; - } - - @Test - @DisplayName("测试获取数据库列表API") - void testGetDatabases() { - GetDatabasesApi databasesApi = new GetDatabasesApi(); - BtResult> result = apiManager.execute(databasesApi); - - assertTrue(result.isSuccess(), "获取数据库列表失败: " + result.getMsg()); - assertNotNull(result.getData(), "返回数据不应为null"); - // 允许空列表(测试环境可能无数据库) - logger.info("获取数据库列表成功,共{}个数据库", result.getData() != null ? result.getData().size() : 0); - } - - @Test - @DisplayName("测试创建MySQL数据库API") - void testCreateDatabase() { - // 1. 确保测试前数据库不存在 - assertFalse(isDatabaseExists(testDbName), "测试前数据库应不存在"); - - // 2. 创建数据库 - CreateDatabaseApi createApi = CreateDatabaseApi.builder(testDbName, testDbUser, testDbPassword) - .withCharset("utf8mb4") - .withDatabaseType(CreateDatabaseApi.DatabaseType.MYSQL) - .withNote("测试数据库备注") - .build(); - - BtResult createResult = apiManager.execute(createApi); - assertTrue(createResult.isSuccess(), "创建数据库失败: " + createResult.getMsg()); - assertTrue(createResult.getData(), "创建操作返回失败"); - - // 3. 验证创建结果 - assertTrue(isDatabaseExists(testDbName), "创建后数据库应存在"); - DatabaseInfo createdDb = getDatabaseInfoByName(testDbName); - assertNotNull(createdDb, "创建的数据库信息应存在"); - assertEquals("MySQL", createdDb.getType(), "数据库类型应为MySQL"); - assertEquals(testDbUser, createdDb.getUsername(), "数据库用户应匹配"); - // 注意:API可能不返回备注信息,跳过备注验证 + } + + @Test + @DisplayName("Should query databases") + void testGetDatabases() throws BtApiException { + BtResult> result = apiManager.execute(new GetDatabasesApi()); + + assertTrue(result.isSuccess(), "Failed to get databases: " + result.getMsg()); + assertNotNull(result.getData(), "Database list should not be null"); + } + + @Test + @DisplayName("Should create MySQL database") + void testCreateDatabase() throws BtApiException { + assertFalse(isDatabaseExists(testDbName), "Database should not exist before test setup"); + + BtResult createResult = + apiManager + .database() + .create( + DatabaseCreateRequest.builder(testDbName, testDbUser, testDbPassword) + .remark("Integration test database") + .build()); + + assertTrue(createResult.isSuccess(), "Failed to create database: " + createResult.getMsg()); + assertTrue(Boolean.TRUE.equals(createResult.getData()), "Create database should return true"); + assertTrue(isDatabaseExists(testDbName), "Database should exist after creation"); + + DatabaseInfo createdDatabase = getDatabaseInfoByName(testDbName); + assertNotNull(createdDatabase, "Created database should be queryable"); + assertEquals("MySQL", createdDatabase.getType(), "Database type mismatch"); + assertEquals(testDbUser, createdDatabase.getUsername(), "Database username mismatch"); + } + + @Test + @DisplayName("Should delete database") + void testDeleteDatabase() throws BtApiException { + BtResult createResult = + apiManager + .database() + .create(DatabaseCreateRequest.builder(testDbName, testDbUser, testDbPassword).build()); + assertTrue(createResult.isSuccess(), "Failed to prepare database: " + createResult.getMsg()); + + DatabaseInfo databaseInfo = getDatabaseInfoByName(testDbName); + assertNotNull(databaseInfo, "Database should exist before delete"); + + BtResult deleteResult = + apiManager.database().delete(new DatabaseDeleteRequest(testDbName, databaseInfo.getId())); + + assertTrue(deleteResult.isSuccess(), "Failed to delete database: " + deleteResult.getMsg()); + assertTrue(Boolean.TRUE.equals(deleteResult.getData()), "Delete database should return true"); + assertFalse(isDatabaseExists(testDbName), "Database should no longer exist"); + } + + @Test + @DisplayName("Should load database integration configuration") + void testConfigurationLoading() { + assertNotNull(getOptionalConfiguration(ENV_BASE_URL, "baseUrl"), "baseUrl missing"); + assertNotNull(getOptionalConfiguration(ENV_API_KEY, "apiKey"), "apiKey missing"); + assertNotNull(getOptionalConfiguration(ENV_TEST_DB_NAME, "test.dbName"), "test.dbName missing"); + assertNotNull(getOptionalConfiguration(ENV_TEST_DB_USER, "test.dbUser"), "test.dbUser missing"); + assertNotNull( + getOptionalConfiguration(ENV_TEST_DB_PASSWORD, "test.dbPassword"), + "test.dbPassword missing"); + } + + private boolean isDatabaseExists(String databaseName) { + return getDatabaseInfoByName(databaseName) != null; + } + + private DatabaseInfo getDatabaseInfoByName(String databaseName) { + if (databaseName == null || databaseName.isBlank()) { + return null; } - @Test - @DisplayName("测试删除数据库API") - void testDeleteDatabase() { - // 1. 确保测试前数据库存在 - if (!isDatabaseExists(testDbName)) { - CreateDatabaseApi createApi = CreateDatabaseApi.builder(testDbName, testDbUser, testDbPassword) - .withCharset("utf8mb4") - .withDatabaseType(CreateDatabaseApi.DatabaseType.MYSQL) - .build(); - BtResult createResult = apiManager.execute(createApi); - assertTrue(createResult.isSuccess(), "测试前创建数据库失败: " + createResult.getMsg()); - } - - // 2. 获取数据库ID - DatabaseInfo dbInfo = getDatabaseInfoByName(testDbName); - assertNotNull(dbInfo, "测试数据库应存在"); - - // 3. 执行删除 - DeleteDatabaseApi deleteApi = new DeleteDatabaseApi(testDbName, dbInfo.getId()); - BtResult deleteResult = apiManager.execute(deleteApi); - assertTrue(deleteResult.isSuccess(), "删除数据库失败: " + deleteResult.getMsg()); - assertTrue(deleteResult.getData(), "删除操作返回失败"); - - // 4. 验证删除结果 - assertFalse(isDatabaseExists(testDbName), "删除后数据库应不存在"); - - // 5. 验证幂等性:重复删除应成功 - BtResult deleteAgainResult = apiManager.execute(deleteApi); - assertTrue(deleteAgainResult.isSuccess(), "重复删除应幂等成功"); + try { + BtResult> result = apiManager.execute(new GetDatabasesApi()); + if (!result.isSuccess() || result.getData() == null) { + return null; + } + return result.getData().stream() + .filter(databaseInfo -> databaseName.equals(databaseInfo.getName())) + .findFirst() + .orElse(null); + } catch (BtApiException exception) { + logger.warn( + "Failed to query database info, databaseName={}, reason={}", + databaseName, + exception.getMessage()); + return null; } - - private boolean isDatabaseExists(String dbName) { - if (dbName == null || apiManager == null) { - return false; - } - - try { - GetDatabasesApi getApi = new GetDatabasesApi(); - BtResult> result = apiManager.execute(getApi); - return result.isSuccess() && result.getData() != null && - result.getData().stream().anyMatch(db -> dbName.equals(db.getName())); - } catch (BtApiException e) { - logger.error("检查数据库是否存在时发生异常: {}", e.getMessage()); - return false; - } - } - - private DatabaseInfo getDatabaseInfoByName(String dbName) { - if (dbName == null || apiManager == null) { - return null; - } - - try { - GetDatabasesApi getApi = new GetDatabasesApi(); - BtResult> result = apiManager.execute(getApi); - if (result.isSuccess() && result.getData() != null) { - return result.getData().stream() - .filter(db -> dbName.equals(db.getName())) - .findFirst() - .orElse(null); - } - return null; - } catch (BtApiException e) { - logger.error("获取数据库信息时发生异常: {}", e.getMessage()); - return null; - } - } - - @Test - @DisplayName("测试配置加载功能") - void testConfigurationLoading() { - assertNotNull(testProperties, "测试配置文件应加载成功"); - assertNotNull(getConfiguration(ENV_BASE_URL), "baseUrl配置缺失"); - assertNotNull(getConfiguration(ENV_API_KEY), "apiKey配置缺失"); - assertNotNull(getConfiguration(ENV_TEST_DB_NAME), "test.dbName配置缺失"); - assertNotNull(getConfiguration(ENV_TEST_DB_USER), "test.dbUser配置缺失"); - assertNotNull(getConfiguration(ENV_TEST_DB_PASSWORD), "test.dbPassword配置缺失"); + } + + private void deleteDatabaseIfExists(String databaseName) { + try { + DatabaseInfo databaseInfo = getDatabaseInfoByName(databaseName); + if (databaseInfo == null) { + return; + } + + BtResult result = + apiManager.database().delete(new DatabaseDeleteRequest(databaseName, databaseInfo.getId())); + if (!result.isSuccess()) { + logger.warn("Database cleanup failed, databaseName={}, reason={}", databaseName, result.getMsg()); + } + } catch (Exception exception) { + logger.warn( + "Database cleanup raised an exception, databaseName={}, reason={}", + databaseName, + exception.getMessage()); } -} \ No newline at end of file + } +} diff --git a/src/test/java/net/heimeng/sdk/btapi/integration/FileIntegrationTest.java b/src/test/java/net/heimeng/sdk/btapi/integration/FileIntegrationTest.java index 423bd76..28cb945 100644 --- a/src/test/java/net/heimeng/sdk/btapi/integration/FileIntegrationTest.java +++ b/src/test/java/net/heimeng/sdk/btapi/integration/FileIntegrationTest.java @@ -1,276 +1,354 @@ package net.heimeng.sdk.btapi.integration; -import net.heimeng.sdk.btapi.api.file.CreateFileDirectoryApi; -import net.heimeng.sdk.btapi.api.file.DeleteFileApi; -import net.heimeng.sdk.btapi.api.file.GetFileContentApi; -import net.heimeng.sdk.btapi.api.file.SaveFileContentApi; -import net.heimeng.sdk.btapi.client.BtApiManager; -import net.heimeng.sdk.btapi.client.BtClient; -import net.heimeng.sdk.btapi.client.BtClientFactory; -import net.heimeng.sdk.btapi.config.BtSdkConfig; -import net.heimeng.sdk.btapi.exception.BtApiException; -import net.heimeng.sdk.btapi.model.BtResult; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +import java.util.ArrayDeque; +import java.util.Deque; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; + import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.api.Timeout; import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.io.IOException; -import java.util.Properties; -import java.util.UUID; - -import static org.junit.jupiter.api.Assertions.*; +import net.heimeng.sdk.btapi.api.file.CreateFileApi; +import net.heimeng.sdk.btapi.api.file.CreateFileDirectoryApi; +import net.heimeng.sdk.btapi.api.file.DeleteFileApi; +import net.heimeng.sdk.btapi.api.file.GetFileContentApi; +import net.heimeng.sdk.btapi.api.file.SaveFileContentApi; +import net.heimeng.sdk.btapi.api.website.DeleteWebsiteApi; +import net.heimeng.sdk.btapi.api.website.GetWebsiteListApi; +import net.heimeng.sdk.btapi.client.BtApiManager; +import net.heimeng.sdk.btapi.exception.BtApiException; +import net.heimeng.sdk.btapi.facade.WebsiteCreateRequest; +import net.heimeng.sdk.btapi.model.BtResult; +import net.heimeng.sdk.btapi.model.website.CreateWebsiteResult; /** - * 文件管理API集成测试类 - *

注意:这些测试会实际调用宝塔面板API,可能会产生实际效果

+ * 文件模块集成测试。 + * + *

测试会真实调用宝塔面板文件接口,并在结束后尝试清理测试资源。 */ -@TestInstance(TestInstance.Lifecycle.PER_CLASS) -@Disabled("默认禁用集成测试,避免不必要的API调用") -public class FileIntegrationTest { - - private static final Logger logger = LoggerFactory.getLogger(FileIntegrationTest.class); - - private BtClient client; - private BtApiManager apiManager; - private Properties testProperties; - - private String testFilePath; - private String testDirectoryPath; - private String testContent = "这是测试文件内容 - " + UUID.randomUUID().toString(); - - @BeforeEach - void setUp() throws IOException { - // 加载测试配置 - testProperties = new Properties(); - testProperties.load(getClass().getClassLoader().getResourceAsStream("application-test.properties")); - - // 从配置文件或环境变量获取配置 - String baseUrl = System.getenv().getOrDefault("baseUrl", testProperties.getProperty("baseUrl")); - String apiKey = System.getenv().getOrDefault("apiKey", testProperties.getProperty("apiKey")); - String originalFilePath = System.getenv().getOrDefault("test.filePath", testProperties.getProperty("test.filePath")); - - // 生成随机ID避免冲突 - String randomId = UUID.randomUUID().toString().substring(0, 8); - testFilePath = originalFilePath.replace(".txt", "_" + randomId + ".txt"); - testDirectoryPath = testFilePath.replace("test_" + randomId + ".txt", "test_dir_" + randomId); - - logger.info("测试配置:baseUrl={}, testFilePath={}, testDirectoryPath={}", - baseUrl, testFilePath, testDirectoryPath); - - // 创建配置对象 - BtSdkConfig config = BtSdkConfig.builder() - .baseUrl(baseUrl) - .apiKey(apiKey) - .connectTimeout(Integer.parseInt(testProperties.getProperty("connectTimeout"))) - .readTimeout(Integer.parseInt(testProperties.getProperty("readTimeout"))) - .retryCount(Integer.parseInt(testProperties.getProperty("retryCount"))) - .build(); - - // 创建客户端和API管理器 - client = BtClientFactory.createClient(config); - apiManager = new BtApiManager(client); +@DisplayName("文件模块集成测试") +@EnabledIfEnvironmentVariable(named = "ENABLE_INTEGRATION_TESTS", matches = "true") +@Timeout(value = 90, unit = TimeUnit.SECONDS) +class FileIntegrationTest extends AbstractIntegrationTestSupport { + + private static final Logger logger = LoggerFactory.getLogger(FileIntegrationTest.class); + + private final Deque cleanupPaths = new ArrayDeque<>(); + + private BtApiManager apiManager; + private String websiteDomain; + private String websiteWebroot; + private String testRootPath; + private String testFilePath; + private String testDirectoryPath; + private String testContent; + + @BeforeEach + void setUp() { + assumeConfigurationPresent(ENV_BASE_URL, "baseUrl"); + assumeConfigurationPresent(ENV_API_KEY, "apiKey"); + assumeConfigurationPresent(ENV_TEST_FILE_PATH, "test.filePath"); + + apiManager = createApiManager(); + assumePanelApiAccessible(apiManager); + + String suffix = uniqueSuffix(); + String baseFilePath = getRequiredConfiguration(ENV_TEST_FILE_PATH, "test.filePath"); + String fileName = getFileName(baseFilePath); + + prepareWebsiteFixture(suffix); + testRootPath = appendChildPath(resolveFileBaseDirectory(baseFilePath), "it-file-" + suffix); + testFilePath = appendChildPath(testRootPath, fileName); + testDirectoryPath = appendChildPath(testRootPath, "test-dir"); + testContent = "这是测试文件内容 - " + uniqueSuffix(); + + logger.info( + "文件集成测试初始化完成,websiteDomain={}, testRootPath={}, testFilePath={}, testDirectoryPath={}", + websiteDomain, + testRootPath, + testFilePath, + testDirectoryPath); + } + + @AfterEach + void tearDown() { + try { + while (!cleanupPaths.isEmpty()) { + deletePathQuietly(cleanupPaths.pop()); + } + deleteWebsiteIfExists(); + } finally { + closeQuietly(apiManager); } + } + + @Test + @DisplayName("应能创建测试目录") + void testCreateDirectory() { + logger.info("开始创建测试目录:{}", testDirectoryPath); + + try { + ensureWebsiteExists(); + ensureRootDirectory(); + + BtResult result = + apiManager.execute(new CreateFileDirectoryApi().setPath(testDirectoryPath)); - @AfterEach - void tearDown() { - // 清理测试文件和目录 - try { - DeleteFileApi deleteFileApi = new DeleteFileApi().setPath(testFilePath); - apiManager.execute(deleteFileApi); - - DeleteFileApi deleteDirApi = new DeleteFileApi().setPath(testDirectoryPath); - apiManager.execute(deleteDirApi); - } catch (BtApiException e) { - logger.warn("清理测试文件/目录时发生异常: {}", e.getMessage()); - } - - if (apiManager != null) { - apiManager.close(); - } + assertTrue(result.isSuccess(), "创建目录失败: " + result.getMsg()); + assertTrue(result.getData(), "创建目录操作返回失败"); + registerCleanup(testDirectoryPath); + } catch (BtApiException exception) { + logger.error("创建目录时发生 API 异常", exception); + fail("创建目录时发生 API 异常: " + exception.getMessage()); } + } - /** - * 测试创建目录 - */ - @Test - void testCreateDirectory() { - logger.info("开始测试创建目录: {}", testDirectoryPath); - - try { - // 创建并配置API - CreateFileDirectoryApi createDirApi = new CreateFileDirectoryApi() - .setPath(testDirectoryPath); - - // 执行API调用 - BtResult result = apiManager.execute(createDirApi); - - // 验证结果 - assertTrue(result.isSuccess(), "创建目录失败: " + result.getMsg()); - assertTrue(result.getData(), "创建目录操作返回失败"); - - logger.info("创建目录成功: {}", testDirectoryPath); - } catch (BtApiException e) { - logger.error("创建目录发生API异常", e); - fail("创建目录发生API异常: " + e.getMessage()); - } catch (Exception e) { - logger.error("创建目录发生未预期异常", e); - fail("创建目录发生未预期异常: " + e.getMessage()); - } + @Test + @DisplayName("应能保存文件内容") + void testSaveFileContent() { + logger.info("开始保存测试文件:{}", testFilePath); + + try { + ensureWebsiteExists(); + ensureRootDirectory(); + saveFile(testFilePath, testContent); + } catch (BtApiException exception) { + logger.error("保存文件内容时发生 API 异常", exception); + fail("保存文件内容时发生 API 异常: " + exception.getMessage()); } + } + + @Test + @DisplayName("应能读取刚写入的文件内容") + void testGetFileContent() { + logger.info("开始读取测试文件:{}", testFilePath); + + try { + ensureWebsiteExists(); + ensureRootDirectory(); + saveFile(testFilePath, testContent); + + BtResult result = apiManager.execute(new GetFileContentApi().setPath(testFilePath)); + + assertTrue(result.isSuccess(), "读取文件内容失败: " + result.getMsg()); + assertNotNull(result.getData(), "返回的文件内容不能为空"); + assertEquals(testContent, result.getData(), "返回的文件内容与预期不一致"); + } catch (BtApiException exception) { + logger.error("读取文件内容时发生 API 异常", exception); + fail("读取文件内容时发生 API 异常: " + exception.getMessage()); + } + } + + @Test + @DisplayName("应能删除已创建的文件") + void testDeleteFile() { + logger.info("开始删除测试文件:{}", testFilePath); + + try { + ensureWebsiteExists(); + ensureRootDirectory(); + saveFile(testFilePath, testContent); + + BtResult result = apiManager.execute(new DeleteFileApi().setPath(testFilePath)); - /** - * 测试保存文件内容 - */ - @Test - void testSaveFileContent() { - logger.info("开始测试保存文件内容: {}", testFilePath); - - try { - // 创建并配置API - SaveFileContentApi saveFileApi = new SaveFileContentApi() - .setPath(testFilePath) - .setData(testContent); - - // 执行API调用 - BtResult result = apiManager.execute(saveFileApi); - - // 验证结果 - assertTrue(result.isSuccess(), "保存文件内容失败: " + result.getMsg()); - assertTrue(result.getData(), "保存文件内容操作返回失败"); - - logger.info("保存文件内容成功: {}", testFilePath); - } catch (BtApiException e) { - logger.error("保存文件内容发生API异常", e); - fail("保存文件内容发生API异常: " + e.getMessage()); - } catch (Exception e) { - logger.error("保存文件内容发生未预期异常", e); - fail("保存文件内容发生未预期异常: " + e.getMessage()); - } + assertTrue(result.isSuccess(), "删除文件失败: " + result.getMsg()); + assertTrue(result.getData(), "删除文件操作返回失败"); + cleanupPaths.remove(testFilePath); + } catch (BtApiException exception) { + logger.error("删除文件时发生 API 异常", exception); + fail("删除文件时发生 API 异常: " + exception.getMessage()); + } + } + + @Test + @DisplayName("应能完成目录内文件的完整操作流") + void testFileOperationFlow() { + logger.info("开始执行文件操作流测试"); + + try { + ensureWebsiteExists(); + ensureRootDirectory(); + + BtResult createDirectoryResult = + apiManager.execute(new CreateFileDirectoryApi().setPath(testDirectoryPath)); + assertTrue(createDirectoryResult.isSuccess(), "创建目录失败: " + createDirectoryResult.getMsg()); + registerCleanup(testDirectoryPath); + + String fileInDirectory = appendChildPath(testDirectoryPath, "test-file.txt"); + saveFile(fileInDirectory, "目录内文件内容"); + + BtResult readResult = + apiManager.execute(new GetFileContentApi().setPath(fileInDirectory)); + assertTrue(readResult.isSuccess(), "读取目录内文件失败: " + readResult.getMsg()); + assertEquals("目录内文件内容", readResult.getData(), "目录内文件内容与预期不一致"); + + BtResult deleteFileResult = + apiManager.execute(new DeleteFileApi().setPath(fileInDirectory)); + assertTrue(deleteFileResult.isSuccess(), "删除目录内文件失败: " + deleteFileResult.getMsg()); + cleanupPaths.remove(fileInDirectory); + + BtResult deleteDirectoryResult = + apiManager.execute(new DeleteFileApi().setPath(testDirectoryPath)); + assertTrue(deleteDirectoryResult.isSuccess(), "删除目录失败: " + deleteDirectoryResult.getMsg()); + cleanupPaths.remove(testDirectoryPath); + } catch (BtApiException exception) { + logger.error("文件操作流测试发生 API 异常", exception); + fail("文件操作流测试发生 API 异常: " + exception.getMessage()); + } + } + + private void ensureRootDirectory() throws BtApiException { + if (cleanupPaths.contains(testRootPath)) { + return; + } + + BtResult result = + apiManager.execute(new CreateFileDirectoryApi().setPath(testRootPath)); + assertTrue(result.isSuccess(), "创建文件测试根目录失败: " + result.getMsg()); + assertTrue(result.getData(), "创建文件测试根目录操作返回失败"); + registerCleanup(testRootPath); + } + + private void saveFile(String path, String content) throws BtApiException { + BtResult createFileResult = apiManager.execute(new CreateFileApi().setPath(path)); + assertTrue(createFileResult.isSuccess(), "创建测试文件失败: " + createFileResult.getMsg()); + assertTrue(createFileResult.getData(), "创建测试文件操作返回失败"); + + BtResult result = + apiManager.execute(new SaveFileContentApi().setPath(path).setData(content)); + + assertTrue(result.isSuccess(), "保存文件内容失败: " + result.getMsg()); + assertTrue(result.getData(), "保存文件内容操作返回失败"); + registerCleanup(path); + } + + private void registerCleanup(String path) { + cleanupPaths.remove(path); + cleanupPaths.push(path); + } + + private void prepareWebsiteFixture(String suffix) { + if (!hasConfiguration(ENV_TEST_DOMAIN_SUFFIX, "test.domain") + || !hasConfiguration(ENV_TEST_WEBROOT_BASE, "test.webroot")) { + return; + } + + String configuredDomain = getRequiredConfiguration(ENV_TEST_DOMAIN_SUFFIX, "test.domain"); + String configuredWebroot = getRequiredConfiguration(ENV_TEST_WEBROOT_BASE, "test.webroot"); + websiteDomain = buildIsolatedTestDomain(configuredDomain, "file-" + suffix); + websiteWebroot = buildIsolatedTestWebroot(configuredWebroot, configuredDomain, websiteDomain); + } + + private void ensureWebsiteExists() throws BtApiException { + if (websiteDomain == null || websiteWebroot == null) { + return; + } + if (getWebsiteIdByName(websiteDomain) != null) { + return; + } + + BtResult result = + apiManager + .website() + .create( + WebsiteCreateRequest.builder(websiteDomain, websiteWebroot) + .phpVersion("81") + .remark("文件集成测试网站") + .build()); + + assertTrue(result.isSuccess(), "准备文件测试网站失败: " + result.getMsg()); + assertNotNull(result.getData(), "准备文件测试网站时返回数据不能为空"); + assertTrue(result.getData().isSiteStatus(), "准备文件测试网站应返回成功状态"); + } + + private void deleteWebsiteIfExists() { + if (apiManager == null || websiteDomain == null || websiteDomain.isBlank()) { + return; + } + + try { + Integer websiteId = getWebsiteIdByName(websiteDomain); + if (websiteId == null) { + return; + } + + BtResult result = + apiManager.execute( + new DeleteWebsiteApi(websiteId, websiteDomain) + .setDeletePath(true) + .setDeleteDatabase(false) + .setDeleteFtp(false)); + + if (!result.isSuccess()) { + logger.warn("清理文件测试网站失败:{},原因:{}", websiteDomain, result.getMsg()); + } + } catch (Exception exception) { + logger.warn("清理文件测试网站时发生异常:{},原因:{}", websiteDomain, exception.getMessage()); + } + } + + private Integer getWebsiteIdByName(String domain) throws BtApiException { + BtResult>> result = + apiManager.execute(new GetWebsiteListApi().setPage(1).setLimit(100)); + + if (!result.isSuccess() || result.getData() == null) { + return null; + } + + for (Map website : result.getData()) { + if (domain.equals(website.get("name"))) { + return toInteger(website.get("id")); + } + } + return null; + } + + private Integer toInteger(Object value) { + if (value instanceof Number numberValue) { + return numberValue.intValue(); + } + if (value instanceof String stringValue && !stringValue.isBlank()) { + return Integer.parseInt(stringValue); } + return null; + } - /** - * 测试获取文件内容 - * 注意:需要先保存文件内容才能测试读取 - */ - @Test - void testGetFileContent() { - // 先保存文件内容 - testSaveFileContent(); - - logger.info("开始测试获取文件内容: {}", testFilePath); - - try { - // 创建并配置API - GetFileContentApi getFileApi = new GetFileContentApi() - .setPath(testFilePath); - - // 执行API调用 - BtResult result = apiManager.execute(getFileApi); - - // 验证结果 - assertTrue(result.isSuccess(), "获取文件内容失败: " + result.getMsg()); - assertNotNull(result.getData(), "返回的文件内容不能为空"); - assertEquals(testContent, result.getData(), "返回的文件内容与预期不符"); - - logger.info("获取文件内容成功,内容长度: {} 字符", result.getData().length()); - } catch (BtApiException e) { - logger.error("获取文件内容发生API异常", e); - fail("获取文件内容发生API异常: " + e.getMessage()); - } catch (Exception e) { - logger.error("获取文件内容发生未预期异常", e); - fail("获取文件内容发生未预期异常: " + e.getMessage()); - } + private void deletePathQuietly(String path) { + if (apiManager == null || path == null || path.isBlank()) { + return; } - /** - * 测试删除文件 - * 注意:需要先保存文件内容才能测试删除 - */ - @Test - void testDeleteFile() { - // 先保存文件内容 - testSaveFileContent(); - - logger.info("开始测试删除文件: {}", testFilePath); - - try { - // 创建并配置API - DeleteFileApi deleteFileApi = new DeleteFileApi() - .setPath(testFilePath); - - // 执行API调用 - BtResult result = apiManager.execute(deleteFileApi); - - // 验证结果 - assertTrue(result.isSuccess(), "删除文件失败: " + result.getMsg()); - assertTrue(result.getData(), "删除文件操作返回失败"); - - logger.info("删除文件成功: {}", testFilePath); - } catch (BtApiException e) { - logger.error("删除文件发生API异常", e); - fail("删除文件发生API异常: " + e.getMessage()); - } catch (Exception e) { - logger.error("删除文件发生未预期异常", e); - fail("删除文件发生未预期异常: " + e.getMessage()); - } + try { + apiManager.execute(new DeleteFileApi().setPath(path)); + } catch (Exception exception) { + logger.warn("清理测试路径失败:{},原因:{}", path, exception.getMessage()); } + } - /** - * 完整的文件操作流程测试 - */ - @Test - void testFileOperationFlow() { - logger.info("开始测试完整的文件操作流程"); - - try { - // 1. 创建目录 - testCreateDirectory(); - - // 2. 在目录中创建文件 - String fileInDir = testDirectoryPath + "/test_file_in_dir.txt"; - SaveFileContentApi saveFileApi = new SaveFileContentApi() - .setPath(fileInDir) - .setData("这是目录中的测试文件内容"); - BtResult saveResult = apiManager.execute(saveFileApi); - assertTrue(saveResult.isSuccess(), "在目录中创建文件失败: " + saveResult.getMsg()); - - // 3. 读取文件内容 - GetFileContentApi getFileApi = new GetFileContentApi().setPath(fileInDir); - BtResult getResult = apiManager.execute(getFileApi); - assertTrue(getResult.isSuccess(), "读取目录中的文件内容失败: " + getResult.getMsg()); - assertEquals("这是目录中的测试文件内容", getResult.getData(), "文件内容与预期不符"); - - // 4. 删除文件 - DeleteFileApi deleteFileApi = new DeleteFileApi().setPath(fileInDir); - BtResult deleteFileResult = apiManager.execute(deleteFileApi); - assertTrue(deleteFileResult.isSuccess(), "删除目录中的文件失败: " + deleteFileResult.getMsg()); - - // 5. 删除目录 - DeleteFileApi deleteDirApi = new DeleteFileApi().setPath(testDirectoryPath); - BtResult deleteDirResult = apiManager.execute(deleteDirApi); - assertTrue(deleteDirResult.isSuccess(), "删除目录失败: " + deleteDirResult.getMsg()); - - logger.info("完整的文件操作流程测试成功"); - } catch (BtApiException e) { - logger.error("文件操作流程测试发生API异常", e); - fail("文件操作流程测试发生API异常: " + e.getMessage()); - } catch (Exception e) { - logger.error("文件操作流程测试发生未预期异常", e); - fail("文件操作流程测试发生未预期异常: " + e.getMessage()); - } + private String resolveFileBaseDirectory(String configuredFilePath) { + if (websiteWebroot != null && !websiteWebroot.isBlank()) { + return websiteWebroot; } + return getParentPath(configuredFilePath); + } - /** - * 启用测试环境变量检查 - */ - @Test - @EnabledIfEnvironmentVariable(named = "ENABLE_INTEGRATION_TESTS", matches = "true") - void testEnvironmentVariableEnabled() { - assertTrue(true, "集成测试环境变量已启用"); + private String getFileName(String path) { + int lastSlashIndex = path.lastIndexOf('/'); + if (lastSlashIndex < 0) { + return path; } -} \ No newline at end of file + return path.substring(lastSlashIndex + 1); + } +} diff --git a/src/test/java/net/heimeng/sdk/btapi/integration/FtpIntegrationTest.java b/src/test/java/net/heimeng/sdk/btapi/integration/FtpIntegrationTest.java new file mode 100644 index 0000000..bf1bce5 --- /dev/null +++ b/src/test/java/net/heimeng/sdk/btapi/integration/FtpIntegrationTest.java @@ -0,0 +1,369 @@ +package net.heimeng.sdk.btapi.integration; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assumptions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Timeout; +import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import net.heimeng.sdk.btapi.api.file.CreateFileDirectoryApi; +import net.heimeng.sdk.btapi.api.file.DeleteFileApi; +import net.heimeng.sdk.btapi.api.ftp.GetFtpAccountsApi; +import net.heimeng.sdk.btapi.api.website.DeleteWebsiteApi; +import net.heimeng.sdk.btapi.api.website.GetWebsiteListApi; +import net.heimeng.sdk.btapi.client.BtApiManager; +import net.heimeng.sdk.btapi.exception.BtApiException; +import net.heimeng.sdk.btapi.facade.FtpCreateRequest; +import net.heimeng.sdk.btapi.facade.FtpDeleteRequest; +import net.heimeng.sdk.btapi.facade.FtpPasswordUpdateRequest; +import net.heimeng.sdk.btapi.facade.WebsiteCreateRequest; +import net.heimeng.sdk.btapi.model.BtResult; +import net.heimeng.sdk.btapi.model.ftp.FtpAccount; +import net.heimeng.sdk.btapi.model.website.CreateWebsiteResult; +import net.heimeng.sdk.btapi.testutil.TestValueFactory; + +@DisplayName("FTP integration tests") +@EnabledIfEnvironmentVariable(named = "ENABLE_INTEGRATION_TESTS", matches = "true") +@Timeout(value = 90, unit = TimeUnit.SECONDS) +class FtpIntegrationTest extends AbstractIntegrationTestSupport { + + private static final Logger logger = LoggerFactory.getLogger(FtpIntegrationTest.class); + + private BtApiManager apiManager; + private String websiteDomain; + private String websiteWebroot; + private String ftpUsername; + private String ftpPassword; + private String updatedFtpPassword; + private String ftpBasePath; + private String ftpHomePath; + + @BeforeEach + void setUp() { + assumeConfigurationPresent(ENV_BASE_URL, "baseUrl"); + assumeConfigurationPresent(ENV_API_KEY, "apiKey"); + + apiManager = createApiManager(); + assumePanelApiAccessible(apiManager); + + String suffix = uniqueSuffix(); + prepareWebsiteFixture(suffix); + + String ftpBaseDirectory = resolveFtpBaseDirectory(); + Assumptions.assumeTrue( + ftpBaseDirectory != null, + () -> + "Skipping integration test because missing FTP root configuration. Configure " + + ENV_TEST_FTP_ROOT + + " or provide website/file integration test configuration."); + + ftpUsername = TestValueFactory.integrationFtpUser(suffix); + ftpPassword = TestValueFactory.integrationPassword(suffix, 'a'); + updatedFtpPassword = TestValueFactory.integrationPassword(suffix, 'b'); + ftpBasePath = appendChildPath(ftpBaseDirectory, TestValueFactory.integrationFtpBaseSegment(suffix)); + ftpHomePath = appendChildPath(ftpBasePath, ftpUsername); + + logger.info( + "FTP integration test initialized, websiteDomain={}, ftpUsername={}, ftpBasePath={}, ftpHomePath={}", + websiteDomain, + ftpUsername, + ftpBasePath, + ftpHomePath); + } + + @AfterEach + void tearDown() { + try { + deleteFtpAccountIfExists(); + deletePathQuietly(ftpHomePath); + deletePathQuietly(ftpBasePath); + deleteWebsiteIfExists(); + } finally { + closeQuietly(apiManager); + } + } + + @Test + @DisplayName("Should query FTP accounts") + void testGetFtpAccounts() throws BtApiException { + BtResult> result = apiManager.execute(new GetFtpAccountsApi()); + + assertTrue(result.isSuccess(), "Failed to get FTP accounts: " + result.getMsg()); + assertNotNull(result.getData(), "FTP accounts should not be null"); + } + + @Test + @DisplayName("Should create FTP account") + void testCreateFtpAccount() throws BtApiException { + createFtpAccountFixture(); + + FtpAccount ftpAccount = getFtpAccountByUsername(ftpUsername); + assertNotNull(ftpAccount, "Created FTP account should be queryable"); + assertEquals(ftpUsername, ftpAccount.getUsername(), "FTP username mismatch"); + assertEquals(ftpHomePath, ftpAccount.getPath(), "FTP home path mismatch"); + } + + @Test + @DisplayName("Should update FTP password") + void testUpdateFtpPassword() throws BtApiException { + createFtpAccountFixture(); + FtpAccount ftpAccount = getFtpAccountByUsername(ftpUsername); + assertNotNull(ftpAccount, "FTP account should exist before password change"); + + BtResult result = + apiManager + .ftp() + .updatePassword( + new FtpPasswordUpdateRequest( + ftpAccount.getId(), ftpUsername, ftpAccount.getPath(), updatedFtpPassword)); + + assertTrue(result.isSuccess(), "Failed to update FTP password: " + result.getMsg()); + assertTrue(Boolean.TRUE.equals(result.getData()), "Password change should return true"); + assertNotNull(getFtpAccountByUsername(ftpUsername), "FTP account should still exist"); + } + + @Test + @DisplayName("Should delete FTP account") + void testDeleteFtpAccount() throws BtApiException { + createFtpAccountFixture(); + FtpAccount ftpAccount = getFtpAccountByUsername(ftpUsername); + assertNotNull(ftpAccount, "FTP account should exist before delete"); + + BtResult result = + apiManager.ftp().delete(new FtpDeleteRequest(ftpAccount.getId(), ftpUsername)); + + assertTrue(result.isSuccess(), "Failed to delete FTP account: " + result.getMsg()); + assertTrue(Boolean.TRUE.equals(result.getData()), "Delete FTP account should return true"); + assertTrue(getFtpAccountByUsername(ftpUsername) == null, "FTP account should no longer exist"); + } + + private void createFtpAccountFixture() throws BtApiException { + ensureWebsiteExists(); + createFtpHomeDirectory(); + + try { + BtResult createResult = + apiManager + .ftp() + .create(new FtpCreateRequest(ftpUsername, ftpPassword, ftpHomePath, ftpUsername)); + + assertTrue( + createResult.isSuccess(), "Failed to prepare FTP fixture: " + createResult.getMsg()); + assertTrue(Boolean.TRUE.equals(createResult.getData()), "FTP fixture should return true"); + } catch (BtApiException exception) { + if (isInvalidParameter(exception)) { + Assumptions.assumeTrue( + false, + "Skipping FTP write integration test because current panel rejects FTP account creation parameters"); + } + if (isAlreadyExists(exception) && getFtpAccountByUsername(ftpUsername) != null) { + return; + } + throw exception; + } + } + + private void createFtpHomeDirectory() throws BtApiException { + try { + BtResult baseDirectoryResult = + apiManager.execute(new CreateFileDirectoryApi().setPath(ftpBasePath)); + assertTrue( + baseDirectoryResult.isSuccess(), + "Failed to create FTP base directory: " + baseDirectoryResult.getMsg()); + assertTrue( + Boolean.TRUE.equals(baseDirectoryResult.getData()), "Base directory should return true"); + + BtResult result = + apiManager.execute(new CreateFileDirectoryApi().setPath(ftpHomePath)); + assertTrue(result.isSuccess(), "Failed to create FTP home directory: " + result.getMsg()); + assertTrue(Boolean.TRUE.equals(result.getData()), "Home directory should return true"); + } catch (BtApiException exception) { + if (isInvalidParameter(exception)) { + Assumptions.assumeTrue( + false, + "Skipping FTP write integration test because current panel rejects FTP home directory preparation"); + } + if (isAlreadyExists(exception)) { + return; + } + throw exception; + } + } + + private void prepareWebsiteFixture(String suffix) { + if (!hasConfiguration(ENV_TEST_DOMAIN_SUFFIX, "test.domain") + || !hasConfiguration(ENV_TEST_WEBROOT_BASE, "test.webroot")) { + return; + } + + String configuredDomain = getRequiredConfiguration(ENV_TEST_DOMAIN_SUFFIX, "test.domain"); + String configuredWebroot = getRequiredConfiguration(ENV_TEST_WEBROOT_BASE, "test.webroot"); + websiteDomain = buildIsolatedTestDomain(configuredDomain, "ftp-" + suffix); + websiteWebroot = buildIsolatedTestWebroot(configuredWebroot, configuredDomain, websiteDomain); + } + + private void ensureWebsiteExists() throws BtApiException { + if (websiteDomain == null || websiteWebroot == null) { + return; + } + if (getWebsiteIdByName(websiteDomain) != null) { + return; + } + + try { + BtResult result = + apiManager + .website() + .create( + WebsiteCreateRequest.builder(websiteDomain, websiteWebroot) + .phpVersion("81") + .remark("FTP integration test website") + .build()); + + assertTrue(result.isSuccess(), "Failed to prepare FTP test website: " + result.getMsg()); + assertNotNull(result.getData(), "Website creation result should not be null"); + assertTrue(result.getData().isSiteStatus(), "Website creation should report success"); + } catch (BtApiException exception) { + if (isAlreadyExists(exception) && getWebsiteIdByName(websiteDomain) != null) { + return; + } + throw exception; + } + } + + private FtpAccount getFtpAccountByUsername(String username) throws BtApiException { + BtResult> result = apiManager.execute(new GetFtpAccountsApi()); + if (!result.isSuccess() || result.getData() == null) { + return null; + } + + return result.getData().stream() + .filter(account -> username.equals(account.getUsername())) + .findFirst() + .orElse(null); + } + + private void deleteFtpAccountIfExists() { + if (apiManager == null || ftpUsername == null || ftpUsername.isBlank()) { + return; + } + + try { + FtpAccount ftpAccount = getFtpAccountByUsername(ftpUsername); + if (ftpAccount == null) { + return; + } + + BtResult result = + apiManager.ftp().delete(new FtpDeleteRequest(ftpAccount.getId(), ftpUsername)); + if (!result.isSuccess()) { + logger.warn( + "FTP account cleanup failed, ftpUsername={}, reason={}", ftpUsername, result.getMsg()); + } + } catch (Exception exception) { + logger.warn( + "FTP account cleanup raised an exception, ftpUsername={}, reason={}", + ftpUsername, + exception.getMessage()); + } + } + + private void deleteWebsiteIfExists() { + if (apiManager == null || websiteDomain == null || websiteDomain.isBlank()) { + return; + } + + try { + Integer websiteId = getWebsiteIdByName(websiteDomain); + if (websiteId == null) { + return; + } + + BtResult result = + apiManager.execute( + new DeleteWebsiteApi(websiteId, websiteDomain) + .setDeletePath(true) + .setDeleteDatabase(false) + .setDeleteFtp(false)); + + if (!result.isSuccess()) { + logger.warn( + "Website cleanup failed, websiteDomain={}, reason={}", websiteDomain, result.getMsg()); + } + } catch (Exception exception) { + logger.warn( + "Website cleanup raised an exception, websiteDomain={}, reason={}", + websiteDomain, + exception.getMessage()); + } + } + + private Integer getWebsiteIdByName(String domain) throws BtApiException { + BtResult>> result = + apiManager.execute(new GetWebsiteListApi().setPage(1).setLimit(100)); + + if (!result.isSuccess() || result.getData() == null) { + return null; + } + + for (Map website : result.getData()) { + if (domain.equals(website.get("name"))) { + return toInteger(website.get("id")); + } + } + return null; + } + + private Integer toInteger(Object value) { + if (value instanceof Number numberValue) { + return numberValue.intValue(); + } + if (value instanceof String stringValue && !stringValue.isBlank()) { + return Integer.parseInt(stringValue); + } + return null; + } + + private void deletePathQuietly(String path) { + if (apiManager == null || path == null || path.isBlank()) { + return; + } + + try { + apiManager.execute(new DeleteFileApi().setPath(path)); + } catch (Exception exception) { + if (isFileNotFound(exception)) { + return; + } + logger.warn("FTP directory cleanup failed, path={}, reason={}", path, exception.getMessage()); + } + } + + private String resolveFtpBaseDirectory() { + if (websiteWebroot != null && !websiteWebroot.isBlank()) { + return websiteWebroot; + } + if (hasConfiguration(ENV_TEST_FTP_ROOT, "test.ftpRoot")) { + return stripTrailingSlash(getRequiredConfiguration(ENV_TEST_FTP_ROOT, "test.ftpRoot")); + } + if (hasConfiguration(ENV_TEST_FILE_PATH, "test.filePath")) { + return getParentPath(getRequiredConfiguration(ENV_TEST_FILE_PATH, "test.filePath")); + } + if (hasConfiguration(ENV_TEST_WEBROOT_BASE, "test.webroot")) { + return stripTrailingSlash(getRequiredConfiguration(ENV_TEST_WEBROOT_BASE, "test.webroot")); + } + return null; + } +} diff --git a/src/test/java/net/heimeng/sdk/btapi/integration/WebsiteIntegrationTest.java b/src/test/java/net/heimeng/sdk/btapi/integration/WebsiteIntegrationTest.java index a18ba73..97a43b7 100644 --- a/src/test/java/net/heimeng/sdk/btapi/integration/WebsiteIntegrationTest.java +++ b/src/test/java/net/heimeng/sdk/btapi/integration/WebsiteIntegrationTest.java @@ -1,219 +1,211 @@ package net.heimeng.sdk.btapi.integration; -import net.heimeng.sdk.btapi.api.website.CreateWebsiteApi; -import net.heimeng.sdk.btapi.api.website.DeleteWebsiteApi; -import net.heimeng.sdk.btapi.api.website.GetWebsiteListApi; -import net.heimeng.sdk.btapi.client.BtApiManager; -import net.heimeng.sdk.btapi.client.BtClient; -import net.heimeng.sdk.btapi.client.BtClientFactory; -import net.heimeng.sdk.btapi.config.BtSdkConfig; -import net.heimeng.sdk.btapi.exception.BtApiException; -import net.heimeng.sdk.btapi.model.BtResult; -import net.heimeng.sdk.btapi.model.website.CreateWebsiteResult; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; + import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.api.Timeout; import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.io.IOException; -import java.util.List; -import java.util.Map; -import java.util.Properties; -import java.util.UUID; - -import static org.junit.jupiter.api.Assertions.*; - -/** - * 网站管理API集成测试类 - *

注意:这些测试会实际调用宝塔面板API,可能会产生实际效果

- */ -@TestInstance(TestInstance.Lifecycle.PER_CLASS) -@Disabled("默认禁用集成测试,避免不必要的API调用") -public class WebsiteIntegrationTest { - - private static final Logger logger = LoggerFactory.getLogger(WebsiteIntegrationTest.class); - - private BtClient client; - private BtApiManager apiManager; - private Properties testProperties; - - private String testDomain; - private String testWebroot; - - @BeforeEach - void setUp() throws IOException { - // 加载测试配置 - testProperties = new Properties(); - testProperties.load(getClass().getClassLoader().getResourceAsStream("application-test.properties")); - - // 从配置文件或环境变量获取配置 - String baseUrl = System.getenv().getOrDefault("baseUrl", testProperties.getProperty("baseUrl")); - String apiKey = System.getenv().getOrDefault("apiKey", testProperties.getProperty("apiKey")); - testDomain = System.getenv().getOrDefault("test.domain", testProperties.getProperty("test.domain")); - testWebroot = System.getenv().getOrDefault("test.webroot", testProperties.getProperty("test.webroot")); - - // 生成随机子域名避免冲突 - String randomId = UUID.randomUUID().toString().substring(0, 8); - testDomain = randomId + "." + testDomain; - testWebroot = testWebroot.replaceFirst("\\w+\\.\\w+$", randomId + ".example.com"); - - logger.info("测试配置:baseUrl={}, testDomain={}, testWebroot={}", baseUrl, testDomain, testWebroot); - - // 创建配置对象 - BtSdkConfig config = BtSdkConfig.builder() - .baseUrl(baseUrl) - .apiKey(apiKey) - .connectTimeout(Integer.parseInt(testProperties.getProperty("connectTimeout"))) - .readTimeout(Integer.parseInt(testProperties.getProperty("readTimeout"))) - .retryCount(Integer.parseInt(testProperties.getProperty("retryCount"))) - .build(); - - // 创建客户端和API管理器 - client = BtClientFactory.createClient(config); - apiManager = new BtApiManager(client); +import net.heimeng.sdk.btapi.api.website.DeleteWebsiteApi; +import net.heimeng.sdk.btapi.api.website.GetWebsiteListApi; +import net.heimeng.sdk.btapi.client.BtApiManager; +import net.heimeng.sdk.btapi.exception.BtApiException; +import net.heimeng.sdk.btapi.facade.WebsiteCreateRequest; +import net.heimeng.sdk.btapi.model.BtResult; +import net.heimeng.sdk.btapi.model.website.CreateWebsiteResult; + +@DisplayName("Website integration tests") +@EnabledIfEnvironmentVariable(named = "ENABLE_INTEGRATION_TESTS", matches = "true") +@Timeout(value = 90, unit = TimeUnit.SECONDS) +class WebsiteIntegrationTest extends AbstractIntegrationTestSupport { + + private static final Logger logger = LoggerFactory.getLogger(WebsiteIntegrationTest.class); + + private BtApiManager apiManager; + private String testDomain; + private String testWebroot; + + @BeforeEach + void setUp() { + assumeConfigurationPresent(ENV_BASE_URL, "baseUrl"); + assumeConfigurationPresent(ENV_API_KEY, "apiKey"); + assumeConfigurationPresent(ENV_TEST_DOMAIN_SUFFIX, "test.domain"); + assumeConfigurationPresent(ENV_TEST_WEBROOT_BASE, "test.webroot"); + + apiManager = createApiManager(); + assumePanelApiAccessible(apiManager); + + String domainSuffix = getRequiredConfiguration(ENV_TEST_DOMAIN_SUFFIX, "test.domain"); + String webrootBase = getRequiredConfiguration(ENV_TEST_WEBROOT_BASE, "test.webroot"); + String uniqueDomainPrefix = uniqueSuffix(); + + testDomain = buildIsolatedTestDomain(domainSuffix, uniqueDomainPrefix); + testWebroot = buildIsolatedTestWebroot(webrootBase, domainSuffix, testDomain); + + logger.info( + "Website integration test initialized, testDomain={}, testWebroot={}", + testDomain, + testWebroot); + } + + @AfterEach + void tearDown() { + try { + deleteWebsiteIfExists(); + } finally { + closeQuietly(apiManager); + } + } + + @Test + @DisplayName("Should query website list") + void testGetWebsiteList() { + try { + BtResult>> result = + apiManager.execute(new GetWebsiteListApi().setPage(1).setLimit(20)); + + assertTrue(result.isSuccess(), "Failed to get website list: " + result.getMsg()); + assertNotNull(result.getData(), "Website list should not be null"); + } catch (BtApiException exception) { + logger.error("Failed while querying website list", exception); + fail("Failed while querying website list: " + exception.getMessage()); + } + } + + @Test + @DisplayName("Should create website") + void testCreateWebsite() { + try { + BtResult result = createWebsite(); + + assertTrue(result.isSuccess(), "Failed to create website: " + result.getMsg()); + assertNotNull(result.getData(), "Create website result should not be null"); + assertTrue(result.getData().isSiteStatus(), "Website creation should report success"); + assertNotNull(getWebsiteIdByName(testDomain), "Created website should be queryable"); + } catch (BtApiException exception) { + logger.error("Failed while creating website", exception); + fail("Failed while creating website: " + exception.getMessage()); + } + } + + @Test + @DisplayName("Should delete website") + void testDeleteWebsite() { + try { + BtResult createResult = createWebsite(); + assertTrue(createResult.isSuccess(), "Failed to prepare website: " + createResult.getMsg()); + + Integer websiteId = getWebsiteIdByName(testDomain); + assertNotNull(websiteId, "Unable to resolve website id"); + + BtResult deleteResult = + apiManager.execute( + new DeleteWebsiteApi(websiteId, testDomain) + .setDeletePath(true) + .setDeleteDatabase(false) + .setDeleteFtp(false)); + + assertTrue(deleteResult.isSuccess(), "Failed to delete website: " + deleteResult.getMsg()); + assertTrue(Boolean.TRUE.equals(deleteResult.getData()), "Delete website should return true"); + assertTrue(getWebsiteIdByName(testDomain) == null, "Website should no longer exist"); + } catch (BtApiException exception) { + logger.error("Failed while deleting website", exception); + fail("Failed while deleting website: " + exception.getMessage()); } + } + + private BtResult createWebsite() throws BtApiException { + WebsiteCreateRequest request = + WebsiteCreateRequest.builder(testDomain, testWebroot) + .phpVersion("81") + .remark("Integration test website") + .build(); - @AfterEach - void tearDown() { - if (apiManager != null) { - apiManager.close(); - } + try { + return apiManager.website().create(request); + } catch (BtApiException exception) { + if (isAlreadyExists(exception) && getWebsiteIdByName(testDomain) != null) { + return successfulCreateWebsiteResult(); + } + throw exception; } + } - /** - * 测试获取网站列表 - */ - @Test - void testGetWebsiteList() { - logger.info("开始测试获取网站列表"); - - try { - // 创建并配置API - GetWebsiteListApi websiteListApi = new GetWebsiteListApi() - .setPage(1) - .setLimit(20); - - // 执行API调用 - BtResult>> result = apiManager.execute(websiteListApi); - - // 验证结果 - assertTrue(result.isSuccess(), "获取网站列表失败: " + result.getMsg()); - assertNotNull(result.getData(), "返回数据不能为空"); - assertTrue(result.getData().size() > 0, "网站列表不能为空"); - - logger.info("获取网站列表成功,共{}个网站", result.getData().size()); - } catch (BtApiException e) { - logger.error("获取网站列表发生API异常", e); - fail("获取网站列表发生API异常: " + e.getMessage()); - } catch (Exception e) { - logger.error("获取网站列表发生未预期异常", e); - fail("获取网站列表发生未预期异常: " + e.getMessage()); - } + private void deleteWebsiteIfExists() { + if (apiManager == null || testDomain == null || testDomain.isBlank()) { + return; } - /** - * 测试创建网站(会创建实际网站) - */ - @Test - void testCreateWebsite() { - logger.info("开始测试创建网站: {}", testDomain); - - try { - // 创建并配置API - 分类ID默认为0(未分类),端口默认为80 - CreateWebsiteApi createWebsiteApi = new CreateWebsiteApi(testDomain, testWebroot, 0, "81", 80, "测试网站") - .setType("PHP") - .setCreateFtp(false) - .setCreateDatabase(false); - - // 执行API调用 - BtResult result = apiManager.execute(createWebsiteApi); - - // 验证结果 - assertTrue(result.isSuccess(), "创建网站失败: " + result.getMsg()); - assertNotNull(result.getData(), "返回数据不能为空"); - assertTrue(result.getData().isSiteStatus(), "网站创建状态为失败"); - - logger.info("创建网站成功: {}", testDomain); - } catch (BtApiException e) { - logger.error("创建网站发生API异常", e); - fail("创建网站发生API异常: " + e.getMessage()); - } catch (Exception e) { - logger.error("创建网站发生未预期异常", e); - fail("创建网站发生未预期异常: " + e.getMessage()); - } + try { + Integer websiteId = getWebsiteIdByName(testDomain); + if (websiteId == null) { + return; + } + + BtResult result = + apiManager.execute( + new DeleteWebsiteApi(websiteId, testDomain) + .setDeletePath(true) + .setDeleteDatabase(false) + .setDeleteFtp(false)); + + if (!result.isSuccess()) { + logger.warn( + "Website cleanup failed, testDomain={}, reason={}", testDomain, result.getMsg()); + } + } catch (Exception exception) { + logger.warn( + "Website cleanup raised an exception, testDomain={}, reason={}", + testDomain, + exception.getMessage()); } + } + + private Integer getWebsiteIdByName(String domain) throws BtApiException { + BtResult>> result = + apiManager.execute(new GetWebsiteListApi().setPage(1).setLimit(100)); - /** - * 测试删除网站(会删除实际网站) - * 注意:需要先创建网站才能测试删除 - */ - @Test - void testDeleteWebsite() { - // 先创建网站 - testCreateWebsite(); - - logger.info("开始测试删除网站: {}", testDomain); - - try { - // 获取网站ID - Integer websiteId = getWebsiteIdByName(testDomain); - assertNotNull(websiteId, "无法获取网站ID: " + testDomain); - - // 创建并配置API - DeleteWebsiteApi deleteWebsiteApi = new DeleteWebsiteApi(websiteId, testDomain) - .setDeletePath(true) // 删除网站根目录 - .setDeleteDatabase(false) // 不删除关联数据库 - .setDeleteFtp(false); // 不删除关联FTP - - // 执行API调用 - BtResult result = apiManager.execute(deleteWebsiteApi); - - // 验证结果 - assertTrue(result.isSuccess(), "删除网站失败: " + result.getMsg()); - assertTrue(result.getData(), "删除网站操作返回失败"); - - logger.info("删除网站成功: {}", testDomain); - } catch (BtApiException e) { - logger.error("删除网站发生API异常", e); - fail("删除网站发生API异常: " + e.getMessage()); - } catch (Exception e) { - logger.error("删除网站发生未预期异常", e); - fail("删除网站发生未预期异常: " + e.getMessage()); - } + if (!result.isSuccess() || result.getData() == null) { + return null; } - /** - * 根据域名获取网站ID - */ - private Integer getWebsiteIdByName(String domain) throws BtApiException { - GetWebsiteListApi websiteListApi = new GetWebsiteListApi() - .setPage(1) - .setLimit(100); - - // 注意:根据GetWebsiteListApi的实现,它直接返回网站列表,而不是包含data和total字段的嵌套结构 - BtResult>> result = apiManager.execute(websiteListApi); - - if (result.isSuccess() && result.getData() != null) { - for (Map website : result.getData()) { - if (domain.equals(website.get("name"))) { - return (Integer) website.get("id"); - } - } - } - - return null; + for (Map website : result.getData()) { + if (domain.equals(website.get("name"))) { + return toInteger(website.get("id")); + } } + return null; + } - /** - * 启用测试环境变量检查 - */ - @Test - @EnabledIfEnvironmentVariable(named = "ENABLE_INTEGRATION_TESTS", matches = "true") - void testEnvironmentVariableEnabled() { - assertTrue(true, "集成测试环境变量已启用"); + private Integer toInteger(Object value) { + if (value instanceof Number numberValue) { + return numberValue.intValue(); + } + if (value instanceof String stringValue && !stringValue.isBlank()) { + return Integer.parseInt(stringValue); } -} \ No newline at end of file + return null; + } + + private BtResult successfulCreateWebsiteResult() { + CreateWebsiteResult createWebsiteResult = new CreateWebsiteResult(); + createWebsiteResult.setSiteStatus(true); + + BtResult result = new BtResult<>(); + result.setStatus(true); + result.setMsg("Website already existed after a retry; treating fixture as successful"); + result.setData(createWebsiteResult); + return result; + } +} diff --git a/src/test/java/net/heimeng/sdk/btapi/testutil/TestValueFactory.java b/src/test/java/net/heimeng/sdk/btapi/testutil/TestValueFactory.java new file mode 100644 index 0000000..303b888 --- /dev/null +++ b/src/test/java/net/heimeng/sdk/btapi/testutil/TestValueFactory.java @@ -0,0 +1,73 @@ +package net.heimeng.sdk.btapi.testutil; + +public final class TestValueFactory { + + private static final String SAMPLE_DATABASE_NAME = "sample-database"; + private static final String SAMPLE_DATABASE_USER = "sample-user"; + private static final String SAMPLE_PASSWORD = "sample-value"; + private static final String UPDATED_SAMPLE_PASSWORD = "changed-value"; + private static final String SAMPLE_FTP_USER = "sample-ftp-user"; + private static final String SAMPLE_FTP_PATH = "/www/wwwroot/sample-site"; + private static final String SAMPLE_FTP_REMARK = "Sample FTP"; + private static final String SAMPLE_DOMAIN = "sample.example.com"; + private static final String SAMPLE_WWW_DOMAIN = "www.sample.example.com"; + private static final String SAMPLE_SITE_PATH = "/www/wwwroot/sample-site"; + + private TestValueFactory() {} + + public static String sampleDatabaseName() { + return SAMPLE_DATABASE_NAME; + } + + public static String sampleDatabaseUser() { + return SAMPLE_DATABASE_USER; + } + + public static String samplePassword() { + return SAMPLE_PASSWORD; + } + + public static String updatedSamplePassword() { + return UPDATED_SAMPLE_PASSWORD; + } + + public static String sampleFtpUser() { + return SAMPLE_FTP_USER; + } + + public static String sampleFtpPath() { + return SAMPLE_FTP_PATH; + } + + public static String sampleFtpRemark() { + return SAMPLE_FTP_REMARK; + } + + public static String sampleDomain() { + return SAMPLE_DOMAIN; + } + + public static String sampleWwwDomain() { + return SAMPLE_WWW_DOMAIN; + } + + public static String sampleSitePath() { + return SAMPLE_SITE_PATH; + } + + public static String sampleDatabaseRemark() { + return SAMPLE_DATABASE_NAME; + } + + public static String integrationFtpUser(String suffix) { + return "ftp" + suffix; + } + + public static String integrationPassword(String suffix, char variant) { + return "pw" + suffix + Character.toUpperCase(variant) + "9Z"; + } + + public static String integrationFtpBaseSegment(String suffix) { + return "ftpcase-" + suffix; + } +}