Scert/backend/internal/httpapi/dns_api_test.go
xuans ba6bd80382 收紧全局设置与DNS权限并更新进度清单
限制系统设置修改、DNS在线测试和续期操作的角色权限,补充前后端回归测试与开发进度记录。
2026-09-29 22:33:32 +08:00

217 lines
8.8 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package httpapi
import (
"encoding/json"
"net/http"
"net/http/httptest"
"path/filepath"
"strings"
"testing"
"github.com/gin-gonic/gin"
"git.pcux.cn/xuans/Scert/backend/internal/config"
"git.pcux.cn/xuans/Scert/backend/internal/repository"
"git.pcux.cn/xuans/Scert/backend/internal/setup"
)
func TestAdminDNSProvidersRequiresAuthentication(t *testing.T) {
cfg := config.Default(t.TempDir())
cfg.SetupReady = true
server := New(Dependencies{Config: cfg, SetupDNS: &fakeSetupDNSVerifier{}})
response := httptest.NewRecorder()
request := httptest.NewRequest(http.MethodGet, "/api/v1/dns/providers", nil)
server.Handler().ServeHTTP(response, request)
if response.Code != http.StatusUnauthorized {
t.Fatalf("未登录访问 DNS 服务商接口应返回 401,实际为 %d:%s", response.Code, response.Body.String())
}
}
func TestAdminDNSProvidersReturnsCatalog(t *testing.T) {
cfg := config.Default(t.TempDir())
cfg.SetupReady = true
server := New(Dependencies{Config: cfg, SetupDNS: &fakeSetupDNSVerifier{}})
response := httptest.NewRecorder()
request := httptest.NewRequest(http.MethodGet, "/api/v1/dns/providers", nil)
serveAdminHTTP(server, response, request)
if response.Code != http.StatusOK {
t.Fatalf("读取 DNS 服务商目录失败:%d %s", response.Code, response.Body.String())
}
if !strings.Contains(response.Body.String(), "Cloudflare") || !strings.Contains(response.Body.String(), "credential_fields") {
t.Fatalf("DNS 服务商目录内容不完整:%s", response.Body.String())
}
}
func TestAdminDNSMutationRequiresSuperAdmin(t *testing.T) {
cfg := config.Default(t.TempDir())
cfg.SetupReady = true
server := New(Dependencies{Config: cfg, SetupDNS: &fakeSetupDNSVerifier{}})
cookie := httpUserSessionCookie(server, "dns-admin", repository.UserRoleAdmin)
cases := []struct {
name string
method string
path string
body string
}{
{
name: "在线测试",
method: http.MethodPost,
path: "/api/v1/dns/test",
body: `{"provider":"cloudflare","test_domain":"example.com","credentials":{"api_token":"test-token"}}`,
},
{
name: "阿里云权限检查",
method: http.MethodPost,
path: "/api/v1/dns/aliyun/check",
body: `{"domain":"example.com"}`,
},
}
for _, item := range cases {
t.Run(item.name, func(t *testing.T) {
request := httptest.NewRequest(item.method, item.path, strings.NewReader(item.body))
request.Header.Set("Content-Type", "application/json")
request.AddCookie(cookie)
response := httptest.NewRecorder()
server.Handler().ServeHTTP(response, request)
if response.Code != http.StatusForbidden {
t.Fatalf("普通管理员执行 DNS 变更操作应返回 403,实际为 %d:%s", response.Code, response.Body.String())
}
if !strings.Contains(response.Body.String(), "只有超级管理员可以管理系统设置") {
t.Fatalf("DNS 权限错误提示不明确:%s", response.Body.String())
}
})
}
}
func TestAdminDNSAccountsExposeProjectScope(t *testing.T) {
cfg := config.Default(t.TempDir())
cfg.SetupReady = true
cfg.DNS.ProjectID = "project-a"
cfg.DNS.CredentialRef = "secret://dns/cloudflare/project-a"
secrets := &fakeSecretReaderStore{data: map[string]map[string]string{
cfg.DNS.CredentialRef: {"api_token": "test-token"},
}}
server := New(Dependencies{Config: cfg, Secrets: secrets})
response := httptest.NewRecorder()
request := httptest.NewRequest(http.MethodGet, "/api/v1/dns/accounts", nil)
serveAdminHTTP(server, response, request)
if response.Code != http.StatusOK {
t.Fatalf("读取 DNS 账户失败:%d %s", response.Code, response.Body.String())
}
if !strings.Contains(response.Body.String(), `"project_id":"project-a"`) {
t.Fatalf("DNS 账户响应未包含项目归属:%s", response.Body.String())
}
}
func TestDNSCredentialUseRejectsUnauthorizedProject(t *testing.T) {
server := New(Dependencies{
ProjectDomains: projectDomainRepositoryStub{authorizeErr: repository.ErrProjectAccessDenied},
})
gin.SetMode(gin.TestMode)
recorder := httptest.NewRecorder()
context, _ := gin.CreateTestContext(recorder)
context.Request = httptest.NewRequest(http.MethodGet, "/", nil)
context.Set(authenticatedActorKey, "user-a")
if server.authorizeDNSCredentialUse(context, "project-a") {
t.Fatal("无 DNS 凭据使用权限的项目不应通过授权")
}
if recorder.Code != http.StatusForbidden {
t.Fatalf("无 DNS 凭据使用权限应返回 403,实际为 %d", recorder.Code)
}
}
func TestSettingsDNSProjectAssignmentRequiresDNSManagePermission(t *testing.T) {
dir := t.TempDir()
cfg := config.Default(dir)
cfg.SetupReady = true
cfg.DNS.VerificationState = "verified"
cfg.Web.Hostname = "scert.example.test"
cfg.Admin.Username = "admin"
cfg.Admin.Email = "admin@example.test"
cfg.Admin.PasswordHash = mustTestPasswordHash(t, "Password123")
configPath := filepath.Join(dir, "config", "scert.json")
if err := cfg.Save(configPath); err != nil {
t.Fatalf("保存测试配置失败:%v", err)
}
server := New(Dependencies{
Config: cfg, ConfigPath: configPath,
ProjectDomains: projectDomainRepositoryStub{authorizeErr: repository.ErrProjectAccessDenied},
})
cookie := loginForSettingsTest(t, server)
request := httptest.NewRequest(http.MethodPut, "/api/v1/settings", strings.NewReader(`{"dns":{"project_id":"project-a"}}`))
request.Header.Set("Content-Type", "application/json")
request.AddCookie(cookie)
response := httptest.NewRecorder()
server.Handler().ServeHTTP(response, request)
if response.Code != http.StatusForbidden {
t.Fatalf("无 DNS 管理权限修改项目归属应返回 403,实际为 %d:%s", response.Code, response.Body.String())
}
if server.config.DNS.ProjectID != "default" {
t.Fatalf("拒绝修改后 DNS 项目归属不应变化:%q", server.config.DNS.ProjectID)
}
}
func TestAdminDNSTestReturnsVerificationResult(t *testing.T) {
cfg := config.Default(t.TempDir())
cfg.SetupReady = true
verifier := &fakeSetupDNSVerifier{}
server := New(Dependencies{Config: cfg, SetupDNS: verifier})
response := httptest.NewRecorder()
request := httptest.NewRequest(http.MethodPost, "/api/v1/dns/test", strings.NewReader(`{
"provider":"cloudflare",
"test_domain":"example.com",
"credentials":{"api_token":"test-token"}
}`))
request.Header.Set("Content-Type", "application/json")
serveSuperAdminHTTP(server, response, request)
if response.Code != http.StatusOK {
t.Fatalf("DNS 在线测试失败:%d %s", response.Code, response.Body.String())
}
var payload struct {
Verified bool `json:"verified"`
CredentialRef string `json:"credential_ref"`
VerificationToken string `json:"verification_token"`
TestReport setup.DNSVerificationReport `json:"test_report"`
VerificationTimeoutSecs int `json:"verification_timeout_secs"`
}
if err := json.Unmarshal(response.Body.Bytes(), &payload); err != nil {
t.Fatalf("解析 DNS 在线测试响应失败:%v", err)
}
if !payload.Verified || payload.CredentialRef == "" || payload.VerificationToken == "" || payload.TestReport.RecordName == "" || payload.VerificationTimeoutSecs <= 0 {
t.Fatalf("DNS 在线测试响应内容不正确:%+v", payload)
}
}
func TestAdminAliyunDNSTestReportsFastVerificationTimeout(t *testing.T) {
cfg := config.Default(t.TempDir())
cfg.SetupReady = true
server := New(Dependencies{Config: cfg, SetupDNS: &fakeSetupDNSVerifier{}})
response := httptest.NewRecorder()
request := httptest.NewRequest(http.MethodPost, "/api/v1/dns/test", strings.NewReader(`{
"provider":"aliyun",
"test_domain":"example.com",
"credentials":{"access_key_id":"test-id","access_key_secret":"test-secret","managed_zones":"example.com","allowed_domains":"example.com"}
}`))
request.Header.Set("Content-Type", "application/json")
serveSuperAdminHTTP(server, response, request)
if response.Code != http.StatusOK || !strings.Contains(response.Body.String(), `"verification_timeout_secs":6`) {
t.Fatalf("管理端阿里云 DNS 在线测试未返回 6 秒等待上限:%d %s", response.Code, response.Body.String())
}
}
func TestAdminDNSTestRejectsUnavailableService(t *testing.T) {
cfg := config.Default(t.TempDir())
cfg.SetupReady = true
server := New(Dependencies{Config: cfg})
response := httptest.NewRecorder()
request := httptest.NewRequest(http.MethodPost, "/api/v1/dns/test", strings.NewReader(`{"provider":"cloudflare","test_domain":"example.com","credentials":{"api_token":"test-token"}}`))
request.Header.Set("Content-Type", "application/json")
serveSuperAdminHTTP(server, response, request)
if response.Code != http.StatusServiceUnavailable {
t.Fatalf("DNS 服务未初始化时应返回 503,实际为 %d:%s", response.Code, response.Body.String())
}
if !strings.Contains(response.Body.String(), `"verification_timeout_secs":60`) {
t.Fatalf("DNS 服务未初始化响应应包含当前最大等待时间:%s", response.Body.String())
}
}