Scert/backend/internal/httpapi/dns_api_test.go
xuans b188c779ab 修复已配置DNS账户延迟导入和重复展示
将已配置的旧 DNS 账户按项目权限幂等导入为可编辑账户,避免重复展示只读兼容视图;补充回归测试和整改任务清单。
2026-10-16 01:42:34 +08:00

245 lines
10 KiB
Go
Raw Permalink 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, ProjectDomains: projectDomainRepositoryStub{}})
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 TestDNSAccountsLazilyImportConfiguredAccountForEditing(t *testing.T) {
f := newProjectMemberAPIFixture(t)
f.server.config.DNS.ProjectID = "project-a"
f.server.config.DNS.Provider = "cloudflare"
f.server.config.DNS.CredentialRef = "secret://dns/cloudflare/project-a"
f.server.secrets = &fakeSecretReaderStore{data: map[string]map[string]string{
f.server.config.DNS.CredentialRef: {"api_token": "test-token"},
}}
response := serveUserRequest(f.server, f.cookies["manager"], http.MethodGet, "/api/v1/dns/accounts", "")
if response.Code != http.StatusOK {
t.Fatalf("读取已配置 DNS 账户失败:%d %s", response.Code, response.Body.String())
}
if !strings.Contains(response.Body.String(), `"id":"legacy-system-dns"`) ||
!strings.Contains(response.Body.String(), `"managed":true`) {
t.Fatalf("已配置 DNS 账户未迁移为可编辑账户:%s", response.Body.String())
}
if strings.Contains(response.Body.String(), `"id":"system-dns"`) {
t.Fatalf("已迁移账户仍重复显示旧系统只读视图:%s", response.Body.String())
}
updated := serveUserRequest(f.server, f.cookies["manager"], http.MethodPut,
"/api/v1/dns/accounts/legacy-system-dns", `{"revision":1,"name":"修改后的账户"}`)
if updated.Code != http.StatusOK {
t.Fatalf("已配置 DNS 账户不能修改:%d %s", updated.Code, updated.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())
}
}