Scert/backend/internal/httpapi/dns_api_test.go
xuans b8fd536dbd 权限整改:完成 DNS 账户项目归属与凭据使用隔离
为 DNS 配置和账户补充项目归属,接入 DNS 凭据使用与项目管理权限校验,并补充配置和接口回归测试。
2026-09-29 19:21:38 +08:00

144 lines
6.1 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"
"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 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 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")
serveAdminHTTP(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")
serveAdminHTTP(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")
serveAdminHTTP(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())
}
}