245 lines
10 KiB
Go
245 lines
10 KiB
Go
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())
|
||
}
|
||
}
|