Scert/backend/internal/httpapi/dns_api_test.go
xuans 07f86f1413 权限整改:完善 DNS 项目归属展示与进度清单
补充 DNS 账户管理页面的项目归属展示和权限拒绝回归测试,更新四份中文整改进度清单,保持总体验收位置为 P0-03 完成待复审。
2026-09-29 19:27:24 +08:00

176 lines
7.4 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 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")
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())
}
}