收紧初始化和SSH接口请求边界
This commit is contained in:
parent
5f7930ce35
commit
fb8e6217b9
@ -13,7 +13,6 @@ import (
|
||||
"time"
|
||||
|
||||
"git.pcux.cn/xuans/Scert/backend/internal/certificate"
|
||||
"git.pcux.cn/xuans/Scert/backend/internal/config"
|
||||
"git.pcux.cn/xuans/Scert/backend/internal/deployment"
|
||||
"git.pcux.cn/xuans/Scert/backend/internal/domain"
|
||||
)
|
||||
@ -48,22 +47,22 @@ func TestCertificateReadAPIs(t *testing.T) {
|
||||
versions: []certificate.SavedResource{{Version: "20260827-120000.000000000"}},
|
||||
}
|
||||
repo := &stubDomainRepository{items: []domain.Domain{{ID: "domain-1", Name: "example.com", CertificateType: domain.CertificateSAN}}}
|
||||
server := New(Dependencies{Config: config.Default(t.TempDir()), Repository: repo, CurrentCertificate: reader})
|
||||
server := New(Dependencies{Config: readyTestConfig(t.TempDir()), Repository: repo, CurrentCertificate: reader})
|
||||
|
||||
list := httptest.NewRecorder()
|
||||
server.Handler().ServeHTTP(list, httptest.NewRequest(http.MethodGet, "/api/v1/certificates", nil))
|
||||
serveAdminHTTP(server, list, httptest.NewRequest(http.MethodGet, "/api/v1/certificates", nil))
|
||||
if list.Code != http.StatusOK || !strings.Contains(list.Body.String(), "example.com") {
|
||||
t.Fatalf("证书列表响应错误:%d %s", list.Code, list.Body.String())
|
||||
}
|
||||
|
||||
detail := httptest.NewRecorder()
|
||||
server.Handler().ServeHTTP(detail, httptest.NewRequest(http.MethodGet, "/api/v1/certificates/domain-1", nil))
|
||||
serveAdminHTTP(server, detail, httptest.NewRequest(http.MethodGet, "/api/v1/certificates/domain-1", nil))
|
||||
if detail.Code != http.StatusOK || !strings.Contains(detail.Body.String(), "20260827-120000.000000000") {
|
||||
t.Fatalf("证书详情响应错误:%d %s", detail.Code, detail.Body.String())
|
||||
}
|
||||
|
||||
versions := httptest.NewRecorder()
|
||||
server.Handler().ServeHTTP(versions, httptest.NewRequest(http.MethodGet, "/api/v1/domains/domain-1/certificate/versions", nil))
|
||||
serveAdminHTTP(server, versions, httptest.NewRequest(http.MethodGet, "/api/v1/domains/domain-1/certificate/versions", nil))
|
||||
if versions.Code != http.StatusOK || !strings.Contains(versions.Body.String(), "20260827-120000.000000000") {
|
||||
t.Fatalf("证书历史版本响应错误:%d %s", versions.Code, versions.Body.String())
|
||||
}
|
||||
@ -87,7 +86,7 @@ func TestCertificateImportAPIAcceptsMultipartFiles(t *testing.T) {
|
||||
return certificate.IssueResult{DomainID: request.DomainID}, deployment.Report{Status: "success"}, nil
|
||||
}
|
||||
server := New(Dependencies{
|
||||
Config: config.Default(t.TempDir()),
|
||||
Config: readyTestConfig(t.TempDir()),
|
||||
Repository: &stubDomainRepository{},
|
||||
ImportCertificate: importer,
|
||||
})
|
||||
@ -110,7 +109,7 @@ func TestCertificateImportAPIAcceptsMultipartFiles(t *testing.T) {
|
||||
request := httptest.NewRequest(http.MethodPost, "/api/v1/domains/domain-1/certificate/import", body)
|
||||
request.Header.Set("Content-Type", writer.FormDataContentType())
|
||||
response := httptest.NewRecorder()
|
||||
server.Handler().ServeHTTP(response, request)
|
||||
serveAdminHTTP(server, response, request)
|
||||
if response.Code != http.StatusOK || received.DomainID != "domain-1" {
|
||||
t.Fatalf("证书导入响应错误:%d %s", response.Code, response.Body.String())
|
||||
}
|
||||
@ -121,7 +120,7 @@ func TestCertificateImportAPIAcceptsMultipartFiles(t *testing.T) {
|
||||
|
||||
func TestCertificateImportAPIRejectsMissingFiles(t *testing.T) {
|
||||
server := New(Dependencies{
|
||||
Config: config.Default(t.TempDir()),
|
||||
Config: readyTestConfig(t.TempDir()),
|
||||
Repository: &stubDomainRepository{},
|
||||
ImportCertificate: func(context.Context, certificate.ImportRequest) (certificate.IssueResult, deployment.Report, error) {
|
||||
return certificate.IssueResult{}, deployment.Report{}, nil
|
||||
@ -131,7 +130,7 @@ func TestCertificateImportAPIRejectsMissingFiles(t *testing.T) {
|
||||
request := httptest.NewRequest(http.MethodPost, "/api/v1/domains/domain-1/certificate/import", bytes.NewReader(body))
|
||||
request.Header.Set("Content-Type", "application/json")
|
||||
response := httptest.NewRecorder()
|
||||
server.Handler().ServeHTTP(response, request)
|
||||
serveAdminHTTP(server, response, request)
|
||||
if response.Code != http.StatusBadRequest {
|
||||
t.Fatalf("缺少证书路径应返回参数错误:%d %s", response.Code, response.Body.String())
|
||||
}
|
||||
|
||||
@ -43,7 +43,10 @@ func (s *Server) requireAdmin(c *gin.Context) {
|
||||
setupReady := s.config.SetupReady
|
||||
s.configMu.RUnlock()
|
||||
if !setupReady {
|
||||
c.Next()
|
||||
c.AbortWithStatusJSON(http.StatusConflict, gin.H{
|
||||
"error": "请先完成初始化配置",
|
||||
"request_id": requestIDFromContext(c),
|
||||
})
|
||||
return
|
||||
}
|
||||
session, ok := s.currentAuthSession(c)
|
||||
|
||||
@ -116,3 +116,30 @@ func TestAdminAPIRejectsAuthenticatedRegularUser(t *testing.T) {
|
||||
t.Fatalf("普通用户权限错误提示不明确:%s", response.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestAdminAPIRequiresSetupBeforeAccessingManagementHandlers(t *testing.T) {
|
||||
cfg := config.Default(t.TempDir())
|
||||
cfg.SetupReady = false
|
||||
server := New(Dependencies{Config: cfg, Repository: &stubDomainRepository{}})
|
||||
paths := []string{
|
||||
"/api/v1/domains",
|
||||
"/api/v1/settings",
|
||||
"/api/v1/servers",
|
||||
"/api/v1/tasks",
|
||||
"/api/v1/audit-logs",
|
||||
"/api/v1/certificates",
|
||||
"/api/v1/ssh-keys",
|
||||
}
|
||||
for _, path := range paths {
|
||||
t.Run(path, func(t *testing.T) {
|
||||
response := httptest.NewRecorder()
|
||||
server.Handler().ServeHTTP(response, httptest.NewRequest(http.MethodGet, path, nil))
|
||||
if response.Code != http.StatusConflict {
|
||||
t.Fatalf("初始化前访问 %s 应返回 409,实际为 %d:%s", path, response.Code, response.Body.String())
|
||||
}
|
||||
if !strings.Contains(response.Body.String(), "请先完成初始化配置") {
|
||||
t.Fatalf("初始化前响应缺少中文提示:%s", response.Body.String())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -64,10 +64,10 @@ func (r *stubDomainRepository) Update(_ context.Context, item domain.Domain) err
|
||||
}
|
||||
|
||||
func TestStatusReturnsApplicationAndConfigVersions(t *testing.T) {
|
||||
cfg := config.Default(t.TempDir())
|
||||
cfg := readyTestConfig(t.TempDir())
|
||||
server := New(Dependencies{Config: cfg, Repository: &stubDomainRepository{}})
|
||||
response := httptest.NewRecorder()
|
||||
server.Handler().ServeHTTP(response, httptest.NewRequest(http.MethodGet, "/api/v1/status", nil))
|
||||
serveAdminHTTP(server, response, httptest.NewRequest(http.MethodGet, "/api/v1/status", nil))
|
||||
if response.Code != http.StatusOK {
|
||||
t.Fatalf("状态接口响应错误:%d", response.Code)
|
||||
}
|
||||
@ -84,14 +84,14 @@ func TestStatusReturnsApplicationAndConfigVersions(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestDomainAPIValidatesSANAndHidesInternalErrors(t *testing.T) {
|
||||
cfg := config.Default(t.TempDir())
|
||||
cfg := readyTestConfig(t.TempDir())
|
||||
repo := &stubDomainRepository{}
|
||||
server := New(Dependencies{Config: cfg, Repository: repo})
|
||||
|
||||
invalid := httptest.NewRequest(http.MethodPost, "/api/v1/domains", strings.NewReader(`{"name":"example.com","certificate_type":"san","sans":["bad_domain"]}`))
|
||||
invalid.Header.Set("Content-Type", "application/json")
|
||||
invalidResponse := httptest.NewRecorder()
|
||||
server.Handler().ServeHTTP(invalidResponse, invalid)
|
||||
serveAdminHTTP(server, invalidResponse, invalid)
|
||||
if invalidResponse.Code != http.StatusBadRequest || strings.Contains(invalidResponse.Body.String(), "detail") {
|
||||
t.Fatalf("非法 SAN 响应不正确:%d %s", invalidResponse.Code, invalidResponse.Body.String())
|
||||
}
|
||||
@ -99,14 +99,14 @@ func TestDomainAPIValidatesSANAndHidesInternalErrors(t *testing.T) {
|
||||
valid := httptest.NewRequest(http.MethodPost, "/api/v1/domains", strings.NewReader(`{"name":"example.com","certificate_type":"san","sans":["www.example.com"]}`))
|
||||
valid.Header.Set("Content-Type", "application/json")
|
||||
validResponse := httptest.NewRecorder()
|
||||
server.Handler().ServeHTTP(validResponse, valid)
|
||||
serveAdminHTTP(server, validResponse, valid)
|
||||
if validResponse.Code != http.StatusCreated || len(repo.created.SANs) != 2 || repo.created.SANs[0] != "example.com" {
|
||||
t.Fatalf("合法 SAN 响应不正确:%d %s", validResponse.Code, validResponse.Body.String())
|
||||
}
|
||||
|
||||
repo.listErr = errors.New("internal-dsn-secret")
|
||||
listResponse := httptest.NewRecorder()
|
||||
server.Handler().ServeHTTP(listResponse, httptest.NewRequest(http.MethodGet, "/api/v1/domains", nil))
|
||||
serveAdminHTTP(server, listResponse, httptest.NewRequest(http.MethodGet, "/api/v1/domains", nil))
|
||||
if listResponse.Code != http.StatusInternalServerError {
|
||||
t.Fatalf("列表错误状态码不正确:%d", listResponse.Code)
|
||||
}
|
||||
@ -116,25 +116,25 @@ func TestDomainAPIValidatesSANAndHidesInternalErrors(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestDeleteMissingDomainReturnsNotFound(t *testing.T) {
|
||||
cfg := config.Default(t.TempDir())
|
||||
cfg := readyTestConfig(t.TempDir())
|
||||
repo := &stubDomainRepository{deleteErr: sql.ErrNoRows}
|
||||
server := New(Dependencies{Config: cfg, Repository: repo})
|
||||
response := httptest.NewRecorder()
|
||||
server.Handler().ServeHTTP(response, httptest.NewRequest(http.MethodDelete, "/api/v1/domains/missing", nil))
|
||||
serveAdminHTTP(server, response, httptest.NewRequest(http.MethodDelete, "/api/v1/domains/missing", nil))
|
||||
if response.Code != http.StatusNotFound || strings.Contains(response.Body.String(), "detail") {
|
||||
t.Fatalf("删除不存在域名响应不正确:%d %s", response.Code, response.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateDomainAPI(t *testing.T) {
|
||||
cfg := config.Default(t.TempDir())
|
||||
cfg := readyTestConfig(t.TempDir())
|
||||
loaded := domain.Domain{ID: "domain-1", Name: "example.com", CertificateType: domain.CertificateSingle, SANs: []string{"example.com"}, Status: "active"}
|
||||
repo := &stubDomainRepository{loaded: loaded}
|
||||
server := New(Dependencies{Config: cfg, Repository: repo})
|
||||
request := httptest.NewRequest(http.MethodPut, "/api/v1/domains/domain-1", strings.NewReader(`{"name":"example.com","certificate_type":"san","sans":["www.example.com"],"auto_renew":false,"acme_environment":"production","dns_provider":"cloudflare"}`))
|
||||
request.Header.Set("Content-Type", "application/json")
|
||||
response := httptest.NewRecorder()
|
||||
server.Handler().ServeHTTP(response, request)
|
||||
serveAdminHTTP(server, response, request)
|
||||
if response.Code != http.StatusOK {
|
||||
t.Fatalf("更新域名响应错误:%d %s", response.Code, response.Body.String())
|
||||
}
|
||||
@ -144,33 +144,33 @@ func TestUpdateDomainAPI(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestCreateDomainConflictReturnsConflict(t *testing.T) {
|
||||
cfg := config.Default(t.TempDir())
|
||||
cfg := readyTestConfig(t.TempDir())
|
||||
repo := &stubDomainRepository{createErr: errors.New("UNIQUE constraint failed: domains.name")}
|
||||
server := New(Dependencies{Config: cfg, Repository: repo})
|
||||
request := httptest.NewRequest(http.MethodPost, "/api/v1/domains", strings.NewReader(`{"name":"example.com","certificate_type":"single"}`))
|
||||
request.Header.Set("Content-Type", "application/json")
|
||||
response := httptest.NewRecorder()
|
||||
server.Handler().ServeHTTP(response, request)
|
||||
serveAdminHTTP(server, response, request)
|
||||
if response.Code != http.StatusConflict {
|
||||
t.Fatalf("重复域名响应错误:%d %s", response.Code, response.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateDomainRejectsInvalidEnvironment(t *testing.T) {
|
||||
cfg := config.Default(t.TempDir())
|
||||
cfg := readyTestConfig(t.TempDir())
|
||||
loaded := domain.Domain{ID: "domain-1", Name: "example.com"}
|
||||
server := New(Dependencies{Config: cfg, Repository: &stubDomainRepository{loaded: loaded}})
|
||||
request := httptest.NewRequest(http.MethodPut, "/api/v1/domains/domain-1", strings.NewReader(`{"name":"example.com","certificate_type":"single","acme_environment":"invalid"}`))
|
||||
request.Header.Set("Content-Type", "application/json")
|
||||
response := httptest.NewRecorder()
|
||||
server.Handler().ServeHTTP(response, request)
|
||||
serveAdminHTTP(server, response, request)
|
||||
if response.Code != http.StatusBadRequest {
|
||||
t.Fatalf("非法 ACME 环境响应错误:%d %s", response.Code, response.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestIssueDomainUsesCertificateWorkflow(t *testing.T) {
|
||||
cfg := config.Default(t.TempDir())
|
||||
cfg := readyTestConfig(t.TempDir())
|
||||
calledID := ""
|
||||
server := New(Dependencies{
|
||||
Config: cfg, Repository: &stubDomainRepository{},
|
||||
@ -180,7 +180,7 @@ func TestIssueDomainUsesCertificateWorkflow(t *testing.T) {
|
||||
},
|
||||
})
|
||||
response := httptest.NewRecorder()
|
||||
server.Handler().ServeHTTP(response, httptest.NewRequest(http.MethodPost, "/api/v1/domains/domain-1/issue", nil))
|
||||
serveAdminHTTP(server, response, httptest.NewRequest(http.MethodPost, "/api/v1/domains/domain-1/issue", nil))
|
||||
if response.Code != http.StatusOK || calledID != "domain-1" || !strings.Contains(response.Body.String(), `"status":"success"`) {
|
||||
t.Fatalf("证书申请接口响应错误:%d %s", response.Code, response.Body.String())
|
||||
}
|
||||
|
||||
@ -27,7 +27,7 @@ import (
|
||||
|
||||
func TestHealthAndDomainAPI(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
cfg := config.Default(dir)
|
||||
cfg := readyTestConfig(dir)
|
||||
cfg.Database.DSN = filepath.Join(dir, "data", "test.db")
|
||||
db, err := database.Open(context.Background(), cfg.Database)
|
||||
if err != nil {
|
||||
@ -45,7 +45,7 @@ func TestHealthAndDomainAPI(t *testing.T) {
|
||||
server := New(Dependencies{Config: cfg, Logger: logger, Repository: repository.NewSQLDomainRepository(db.DB)})
|
||||
|
||||
health := httptest.NewRecorder()
|
||||
server.Handler().ServeHTTP(health, httptest.NewRequest(http.MethodGet, "/healthz", nil))
|
||||
serveAdminHTTP(server, health, httptest.NewRequest(http.MethodGet, "/healthz", nil))
|
||||
if health.Code != http.StatusOK || !strings.Contains(health.Body.String(), `"status":"ok"`) {
|
||||
t.Fatalf("健康检查响应错误:%d %s", health.Code, health.Body.String())
|
||||
}
|
||||
@ -53,13 +53,13 @@ func TestHealthAndDomainAPI(t *testing.T) {
|
||||
request := httptest.NewRequest(http.MethodPost, "/api/v1/domains", strings.NewReader(`{"name":"example.com","certificate_type":"single"}`))
|
||||
request.Header.Set("Content-Type", "application/json")
|
||||
created := httptest.NewRecorder()
|
||||
server.Handler().ServeHTTP(created, request)
|
||||
serveAdminHTTP(server, created, request)
|
||||
if created.Code != http.StatusCreated {
|
||||
t.Fatalf("创建域名响应错误:%d %s", created.Code, created.Body.String())
|
||||
}
|
||||
|
||||
list := httptest.NewRecorder()
|
||||
server.Handler().ServeHTTP(list, httptest.NewRequest(http.MethodGet, "/api/v1/domains", nil))
|
||||
serveAdminHTTP(server, list, httptest.NewRequest(http.MethodGet, "/api/v1/domains", nil))
|
||||
if list.Code != http.StatusOK || !strings.Contains(list.Body.String(), "example.com") {
|
||||
t.Fatalf("域名列表响应错误:%d %s", list.Code, list.Body.String())
|
||||
}
|
||||
@ -67,7 +67,7 @@ func TestHealthAndDomainAPI(t *testing.T) {
|
||||
|
||||
func TestResetPathRequiresScertAdminActivation(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
cfg := config.Default(dir)
|
||||
cfg := readyTestConfig(dir)
|
||||
cfg.SetupReady = true
|
||||
cfg.Web.Hostname = "scert.example.test"
|
||||
cfg.Server.Port = 8443
|
||||
@ -75,7 +75,7 @@ func TestResetPathRequiresScertAdminActivation(t *testing.T) {
|
||||
server := New(Dependencies{Config: cfg, Repository: &stubDomainRepository{}, ResetStatePath: statePath})
|
||||
|
||||
inactive := httptest.NewRecorder()
|
||||
server.Handler().ServeHTTP(inactive, httptest.NewRequest(http.MethodGet, "/reset", nil))
|
||||
serveAdminHTTP(server, inactive, httptest.NewRequest(http.MethodGet, "/reset", nil))
|
||||
if inactive.Code != http.StatusNotFound {
|
||||
t.Fatalf("Scert-admin 未启动时 /reset 应返回 404:%d", inactive.Code)
|
||||
}
|
||||
@ -85,7 +85,7 @@ func TestResetPathRequiresScertAdminActivation(t *testing.T) {
|
||||
t.Fatalf("启用测试密码重置入口失败:%v", err)
|
||||
}
|
||||
active := httptest.NewRecorder()
|
||||
server.Handler().ServeHTTP(active, httptest.NewRequest(http.MethodGet, "/reset", nil))
|
||||
serveAdminHTTP(server, active, httptest.NewRequest(http.MethodGet, "/reset", nil))
|
||||
if active.Code != http.StatusOK || !strings.Contains(active.Body.String(), "超级管理员密码重置") {
|
||||
t.Fatalf("Scert-admin 启用后 /reset 页面错误:%d %s", active.Code, active.Body.String())
|
||||
}
|
||||
@ -96,7 +96,7 @@ func TestResetPathRequiresScertAdminActivation(t *testing.T) {
|
||||
t.Fatalf("关闭测试密码重置入口失败:%v", err)
|
||||
}
|
||||
stopped := httptest.NewRecorder()
|
||||
server.Handler().ServeHTTP(stopped, httptest.NewRequest(http.MethodGet, "/reset", nil))
|
||||
serveAdminHTTP(server, stopped, httptest.NewRequest(http.MethodGet, "/reset", nil))
|
||||
if stopped.Code != http.StatusNotFound {
|
||||
t.Fatalf("Scert-admin 停止后 /reset 应恢复 404:%d", stopped.Code)
|
||||
}
|
||||
@ -104,7 +104,7 @@ func TestResetPathRequiresScertAdminActivation(t *testing.T) {
|
||||
|
||||
func TestResetPasswordUpdatesConfigAndConsumesSession(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
cfg := config.Default(dir)
|
||||
cfg := readyTestConfig(dir)
|
||||
cfg.SetupReady = true
|
||||
cfg.Web.Hostname = "scert.example.test"
|
||||
cfg.DNS.VerificationState = "verified"
|
||||
@ -126,7 +126,7 @@ func TestResetPasswordUpdatesConfigAndConsumesSession(t *testing.T) {
|
||||
}
|
||||
server := New(Dependencies{Config: cfg, ConfigPath: configPath, Repository: &stubDomainRepository{}, ResetStatePath: admin.StatePath(cfg.Paths.Data)})
|
||||
get := httptest.NewRecorder()
|
||||
server.Handler().ServeHTTP(get, httptest.NewRequest(http.MethodGet, "http://scert.example.test/reset", nil))
|
||||
serveAdminHTTP(server, get, httptest.NewRequest(http.MethodGet, "http://scert.example.test/reset", nil))
|
||||
if get.Code != http.StatusOK {
|
||||
t.Fatalf("获取密码重置页面失败:%d", get.Code)
|
||||
}
|
||||
@ -142,7 +142,7 @@ func TestResetPasswordUpdatesConfigAndConsumesSession(t *testing.T) {
|
||||
request.Header.Set("Origin", "http://scert.example.test")
|
||||
request.AddCookie(cookie)
|
||||
response := httptest.NewRecorder()
|
||||
server.Handler().ServeHTTP(response, request)
|
||||
serveAdminHTTP(server, response, request)
|
||||
if response.Code != http.StatusOK || !strings.Contains(response.Body.String(), "密码重置成功") {
|
||||
t.Fatalf("密码重置响应错误:%d %s", response.Code, response.Body.String())
|
||||
}
|
||||
@ -164,7 +164,7 @@ func TestResetPasswordUpdatesConfigAndConsumesSession(t *testing.T) {
|
||||
t.Fatal("旧密码不应继续有效")
|
||||
}
|
||||
second := httptest.NewRecorder()
|
||||
server.Handler().ServeHTTP(second, httptest.NewRequest(http.MethodGet, "http://scert.example.test/reset", nil))
|
||||
serveAdminHTTP(server, second, httptest.NewRequest(http.MethodGet, "http://scert.example.test/reset", nil))
|
||||
if second.Code != http.StatusNotFound {
|
||||
t.Fatalf("密码重置成功后入口必须关闭:%d", second.Code)
|
||||
}
|
||||
@ -175,7 +175,7 @@ func TestResetPasswordUpdatesConfigAndConsumesSession(t *testing.T) {
|
||||
|
||||
func TestResetPasswordSynchronizesPrimarySuperAdminUser(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
cfg := config.Default(dir)
|
||||
cfg := readyTestConfig(dir)
|
||||
cfg.SetupReady = true
|
||||
cfg.Web.Hostname = "scert.example.test"
|
||||
cfg.DNS.VerificationState = "verified"
|
||||
@ -215,7 +215,7 @@ func TestResetPasswordSynchronizesPrimarySuperAdminUser(t *testing.T) {
|
||||
ResetStatePath: admin.StatePath(cfg.Paths.Data),
|
||||
})
|
||||
get := httptest.NewRecorder()
|
||||
server.Handler().ServeHTTP(get, httptest.NewRequest(http.MethodGet, "https://scert.example.test/reset", nil))
|
||||
serveAdminHTTP(server, get, httptest.NewRequest(http.MethodGet, "https://scert.example.test/reset", nil))
|
||||
if get.Code != http.StatusOK {
|
||||
t.Fatalf("获取密码重置页面失败:%d", get.Code)
|
||||
}
|
||||
@ -229,7 +229,7 @@ func TestResetPasswordSynchronizesPrimarySuperAdminUser(t *testing.T) {
|
||||
request.Header.Set("Origin", "https://scert.example.test")
|
||||
request.AddCookie(cookies[0])
|
||||
response := httptest.NewRecorder()
|
||||
server.Handler().ServeHTTP(response, request)
|
||||
serveAdminHTTP(server, response, request)
|
||||
if response.Code != http.StatusOK {
|
||||
t.Fatalf("密码重置失败:%d %s", response.Code, response.Body.String())
|
||||
}
|
||||
@ -244,7 +244,7 @@ func TestResetPasswordSynchronizesPrimarySuperAdminUser(t *testing.T) {
|
||||
|
||||
func TestResetRejectsCrossSiteAndLimitsFailures(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
cfg := config.Default(dir)
|
||||
cfg := readyTestConfig(dir)
|
||||
cfg.SetupReady = true
|
||||
cfg.Web.Hostname = "scert.example.test"
|
||||
cfg.DNS.VerificationState = "verified"
|
||||
@ -258,7 +258,7 @@ func TestResetRejectsCrossSiteAndLimitsFailures(t *testing.T) {
|
||||
}
|
||||
server := New(Dependencies{Config: cfg, ConfigPath: filepath.Join(dir, "config", "scert.json"), Repository: &stubDomainRepository{}, ResetStatePath: admin.StatePath(cfg.Paths.Data)})
|
||||
get := httptest.NewRecorder()
|
||||
server.Handler().ServeHTTP(get, httptest.NewRequest(http.MethodGet, "http://scert.example.test/reset", nil))
|
||||
serveAdminHTTP(server, get, httptest.NewRequest(http.MethodGet, "http://scert.example.test/reset", nil))
|
||||
cookie := get.Result().Cookies()[0]
|
||||
base := url.Values{"csrf_token": {cookie.Value}, "reset_code": {session.Code}, "new_password": {"NewPass1234"}, "confirm_password": {"NewPass1234"}}
|
||||
for i := 0; i < 5; i++ {
|
||||
@ -267,7 +267,7 @@ func TestResetRejectsCrossSiteAndLimitsFailures(t *testing.T) {
|
||||
request.Header.Set("Origin", "http://evil.example.test")
|
||||
request.AddCookie(cookie)
|
||||
response := httptest.NewRecorder()
|
||||
server.Handler().ServeHTTP(response, request)
|
||||
serveAdminHTTP(server, response, request)
|
||||
if response.Code != http.StatusForbidden {
|
||||
t.Fatalf("跨站请求应被拒绝:第 %d 次状态码为 %d", i+1, response.Code)
|
||||
}
|
||||
@ -277,7 +277,7 @@ func TestResetRejectsCrossSiteAndLimitsFailures(t *testing.T) {
|
||||
request.Header.Set("Origin", "http://scert.example.test")
|
||||
request.AddCookie(cookie)
|
||||
response := httptest.NewRecorder()
|
||||
server.Handler().ServeHTTP(response, request)
|
||||
serveAdminHTTP(server, response, request)
|
||||
if response.Code != http.StatusTooManyRequests {
|
||||
t.Fatalf("超过失败次数后应限流:%d", response.Code)
|
||||
}
|
||||
@ -285,7 +285,7 @@ func TestResetRejectsCrossSiteAndLimitsFailures(t *testing.T) {
|
||||
|
||||
func TestServerAndDomainAssociationAPI(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
cfg := config.Default(dir)
|
||||
cfg := readyTestConfig(dir)
|
||||
cfg.Database.DSN = filepath.Join(dir, "data", "servers.db")
|
||||
db, err := database.Open(context.Background(), cfg.Database)
|
||||
if err != nil {
|
||||
@ -312,7 +312,7 @@ func TestServerAndDomainAssociationAPI(t *testing.T) {
|
||||
domainRequest := httptest.NewRequest(http.MethodPost, "/api/v1/domains", strings.NewReader(`{"name":"example.com","certificate_type":"single"}`))
|
||||
domainRequest.Header.Set("Content-Type", "application/json")
|
||||
domainResponse := httptest.NewRecorder()
|
||||
server.Handler().ServeHTTP(domainResponse, domainRequest)
|
||||
serveAdminHTTP(server, domainResponse, domainRequest)
|
||||
if domainResponse.Code != http.StatusCreated {
|
||||
t.Fatalf("创建测试域名失败:%d %s", domainResponse.Code, domainResponse.Body.String())
|
||||
}
|
||||
@ -326,7 +326,7 @@ func TestServerAndDomainAssociationAPI(t *testing.T) {
|
||||
serverRequest := httptest.NewRequest(http.MethodPost, "/api/v1/servers", strings.NewReader(`{"name":"测试服务器","address":"192.0.2.50","port":22,"username":"deploy","private_key_ref":"`+key.Reference+`","host_key_fingerprint":"SHA256:test","web_server":"nginx"}`))
|
||||
serverRequest.Header.Set("Content-Type", "application/json")
|
||||
serverResponse := httptest.NewRecorder()
|
||||
server.Handler().ServeHTTP(serverResponse, serverRequest)
|
||||
serveAdminHTTP(server, serverResponse, serverRequest)
|
||||
if serverResponse.Code != http.StatusCreated {
|
||||
t.Fatalf("创建测试服务器失败:%d %s", serverResponse.Code, serverResponse.Body.String())
|
||||
}
|
||||
@ -341,18 +341,18 @@ func TestServerAndDomainAssociationAPI(t *testing.T) {
|
||||
associationRequest := httptest.NewRequest(http.MethodPut, "/api/v1/domains/"+item.ID+"/servers", strings.NewReader(`{"server_ids":["`+created.ID+`"]}`))
|
||||
associationRequest.Header.Set("Content-Type", "application/json")
|
||||
associationResponse := httptest.NewRecorder()
|
||||
server.Handler().ServeHTTP(associationResponse, associationRequest)
|
||||
serveAdminHTTP(server, associationResponse, associationRequest)
|
||||
if associationResponse.Code != http.StatusOK || !strings.Contains(associationResponse.Body.String(), created.ID) {
|
||||
t.Fatalf("保存域名服务器关联失败:%d %s", associationResponse.Code, associationResponse.Body.String())
|
||||
}
|
||||
|
||||
deleteResponse := httptest.NewRecorder()
|
||||
server.Handler().ServeHTTP(deleteResponse, httptest.NewRequest(http.MethodDelete, "/api/v1/servers/"+created.ID, nil))
|
||||
serveAdminHTTP(server, deleteResponse, httptest.NewRequest(http.MethodDelete, "/api/v1/servers/"+created.ID, nil))
|
||||
if deleteResponse.Code != http.StatusNoContent {
|
||||
t.Fatalf("删除测试服务器失败:%d %s", deleteResponse.Code, deleteResponse.Body.String())
|
||||
}
|
||||
listResponse := httptest.NewRecorder()
|
||||
server.Handler().ServeHTTP(listResponse, httptest.NewRequest(http.MethodGet, "/api/v1/domains/"+item.ID+"/servers", nil))
|
||||
serveAdminHTTP(server, listResponse, httptest.NewRequest(http.MethodGet, "/api/v1/domains/"+item.ID+"/servers", nil))
|
||||
if listResponse.Code != http.StatusOK || strings.Contains(listResponse.Body.String(), created.ID) {
|
||||
t.Fatalf("服务器删除后域名关联未清理:%d %s", listResponse.Code, listResponse.Body.String())
|
||||
}
|
||||
@ -497,7 +497,7 @@ type resetTestFixture struct {
|
||||
func newResetTestFixture(t *testing.T) resetTestFixture {
|
||||
t.Helper()
|
||||
dir := t.TempDir()
|
||||
cfg := config.Default(dir)
|
||||
cfg := readyTestConfig(dir)
|
||||
cfg.SetupReady = true
|
||||
cfg.Web.Hostname = "scert.example.test"
|
||||
cfg.DNS.VerificationState = "verified"
|
||||
@ -519,7 +519,7 @@ func newResetTestFixture(t *testing.T) resetTestFixture {
|
||||
}
|
||||
server := New(Dependencies{Config: cfg, ConfigPath: configPath, Repository: &stubDomainRepository{}, ResetStatePath: admin.StatePath(cfg.Paths.Data)})
|
||||
get := httptest.NewRecorder()
|
||||
server.Handler().ServeHTTP(get, httptest.NewRequest(http.MethodGet, "https://scert.example.test/reset", nil))
|
||||
serveAdminHTTP(server, get, httptest.NewRequest(http.MethodGet, "https://scert.example.test/reset", nil))
|
||||
if get.Code != http.StatusOK {
|
||||
t.Fatalf("获取测试重置页面失败:%d", get.Code)
|
||||
}
|
||||
@ -543,7 +543,7 @@ func resetForm(code, csrfToken, password string) url.Values {
|
||||
}
|
||||
|
||||
func TestListenAddressKeepsDirectModeBinding(t *testing.T) {
|
||||
cfg := config.Default(t.TempDir())
|
||||
cfg := readyTestConfig(t.TempDir())
|
||||
cfg.Server.Host = "0.0.0.0"
|
||||
cfg.Server.Port = 5200
|
||||
cfg.Server.HTTPSMode = "direct"
|
||||
@ -557,7 +557,7 @@ func TestListenAddressKeepsDirectModeBinding(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestListenAddressRestrictsProxyModeToLoopback(t *testing.T) {
|
||||
cfg := config.Default(t.TempDir())
|
||||
cfg := readyTestConfig(t.TempDir())
|
||||
cfg.Server.Host = "0.0.0.0"
|
||||
cfg.Server.Port = 5201
|
||||
cfg.Server.HTTPSMode = "proxy"
|
||||
@ -580,7 +580,7 @@ func TestListenAddressRestrictsProxyModeToLoopback(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestListenAddressAllowsLoopbackProxyBinding(t *testing.T) {
|
||||
cfg := config.Default(t.TempDir())
|
||||
cfg := readyTestConfig(t.TempDir())
|
||||
cfg.Server.Host = "[::1]"
|
||||
cfg.Server.Port = 5202
|
||||
cfg.Server.HTTPSMode = "proxy"
|
||||
@ -594,7 +594,7 @@ func TestListenAddressAllowsLoopbackProxyBinding(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestForwardedHTTPSIsTrustedOnlyInProxyMode(t *testing.T) {
|
||||
direct := config.Default(t.TempDir())
|
||||
direct := readyTestConfig(t.TempDir())
|
||||
direct.Server.HTTPSMode = "direct"
|
||||
directServer := New(Dependencies{Config: direct, Repository: &stubDomainRepository{}})
|
||||
directRequest := httptest.NewRequest(http.MethodGet, "http://scert.example.test/healthz", nil)
|
||||
@ -603,7 +603,7 @@ func TestForwardedHTTPSIsTrustedOnlyInProxyMode(t *testing.T) {
|
||||
t.Fatal("直接 HTTPS 模式不应信任外部伪造的 X-Forwarded-Proto")
|
||||
}
|
||||
|
||||
proxy := config.Default(t.TempDir())
|
||||
proxy := readyTestConfig(t.TempDir())
|
||||
proxy.Server.HTTPSMode = "proxy"
|
||||
proxyServer := New(Dependencies{Config: proxy, Repository: &stubDomainRepository{}})
|
||||
proxyRequest := httptest.NewRequest(http.MethodGet, "http://127.0.0.1:5200/healthz", nil)
|
||||
@ -614,7 +614,7 @@ func TestForwardedHTTPSIsTrustedOnlyInProxyMode(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestProxyModeAcceptsHTTPSSameOrigin(t *testing.T) {
|
||||
cfg := config.Default(t.TempDir())
|
||||
cfg := readyTestConfig(t.TempDir())
|
||||
cfg.SetupReady = true
|
||||
cfg.Server.HTTPSMode = "proxy"
|
||||
cfg.Admin.Username = "admin"
|
||||
|
||||
@ -33,9 +33,8 @@ func (s *Server) createSSHKey(c *gin.Context) {
|
||||
s.writeError(c, http.StatusNotImplemented, "SSH 私钥管理接口尚未配置", errors.New("SSH 私钥管理服务未配置"))
|
||||
return
|
||||
}
|
||||
c.Request.Body = http.MaxBytesReader(c.Writer, c.Request.Body, maxSSHKeyRequestBytes)
|
||||
var request sshKeyRequest
|
||||
if err := c.ShouldBindJSON(&request); err != nil {
|
||||
if err := bindJSON(c, &request, maxSSHKeyRequestBytes); err != nil {
|
||||
var tooLarge *http.MaxBytesError
|
||||
if errors.As(err, &tooLarge) {
|
||||
s.writeError(c, http.StatusRequestEntityTooLarge, "SSH 私钥请求内容过大", err)
|
||||
|
||||
@ -13,7 +13,6 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"git.pcux.cn/xuans/Scert/backend/internal/config"
|
||||
"git.pcux.cn/xuans/Scert/backend/internal/database"
|
||||
"git.pcux.cn/xuans/Scert/backend/internal/repository"
|
||||
"git.pcux.cn/xuans/Scert/backend/internal/secretstore"
|
||||
@ -22,7 +21,7 @@ import (
|
||||
|
||||
func TestSSHKeyAPIHidesPrivateKeyAndBlocksDeletingUsedReference(t *testing.T) {
|
||||
directory := t.TempDir()
|
||||
cfg := config.Default(directory)
|
||||
cfg := readyTestConfig(directory)
|
||||
cfg.Database.DSN = filepath.Join(directory, "data", "ssh-keys.db")
|
||||
db, err := database.Open(context.Background(), cfg.Database)
|
||||
if err != nil {
|
||||
@ -50,7 +49,7 @@ func TestSSHKeyAPIHidesPrivateKeyAndBlocksDeletingUsedReference(t *testing.T) {
|
||||
createRequest := httptest.NewRequest(http.MethodPost, "/api/v1/ssh-keys", strings.NewReader(string(createBody)))
|
||||
createRequest.Header.Set("Content-Type", "application/json")
|
||||
createResponse := httptest.NewRecorder()
|
||||
server.Handler().ServeHTTP(createResponse, createRequest)
|
||||
serveAdminHTTP(server, createResponse, createRequest)
|
||||
if createResponse.Code != http.StatusCreated {
|
||||
t.Fatalf("创建 SSH 私钥失败:%d %s", createResponse.Code, createResponse.Body.String())
|
||||
}
|
||||
@ -63,7 +62,7 @@ func TestSSHKeyAPIHidesPrivateKeyAndBlocksDeletingUsedReference(t *testing.T) {
|
||||
}
|
||||
|
||||
listResponse := httptest.NewRecorder()
|
||||
server.Handler().ServeHTTP(listResponse, httptest.NewRequest(http.MethodGet, "/api/v1/ssh-keys", nil))
|
||||
serveAdminHTTP(server, listResponse, httptest.NewRequest(http.MethodGet, "/api/v1/ssh-keys", nil))
|
||||
if listResponse.Code != http.StatusOK || !strings.Contains(listResponse.Body.String(), key.Reference) || strings.Contains(listResponse.Body.String(), "PRIVATE KEY") {
|
||||
t.Fatalf("SSH 私钥列表响应无效或泄露敏感内容:%d %s", listResponse.Code, listResponse.Body.String())
|
||||
}
|
||||
@ -72,7 +71,7 @@ func TestSSHKeyAPIHidesPrivateKeyAndBlocksDeletingUsedReference(t *testing.T) {
|
||||
serverRequest := httptest.NewRequest(http.MethodPost, "/api/v1/servers", strings.NewReader(serverBody))
|
||||
serverRequest.Header.Set("Content-Type", "application/json")
|
||||
serverResponse := httptest.NewRecorder()
|
||||
server.Handler().ServeHTTP(serverResponse, serverRequest)
|
||||
serveAdminHTTP(server, serverResponse, serverRequest)
|
||||
if serverResponse.Code != http.StatusCreated {
|
||||
t.Fatalf("创建使用 SSH 私钥的服务器失败:%d %s", serverResponse.Code, serverResponse.Body.String())
|
||||
}
|
||||
@ -84,17 +83,17 @@ func TestSSHKeyAPIHidesPrivateKeyAndBlocksDeletingUsedReference(t *testing.T) {
|
||||
}
|
||||
|
||||
deleteUsedResponse := httptest.NewRecorder()
|
||||
server.Handler().ServeHTTP(deleteUsedResponse, httptest.NewRequest(http.MethodDelete, "/api/v1/ssh-keys/"+key.ID, nil))
|
||||
serveAdminHTTP(server, deleteUsedResponse, httptest.NewRequest(http.MethodDelete, "/api/v1/ssh-keys/"+key.ID, nil))
|
||||
if deleteUsedResponse.Code != http.StatusConflict {
|
||||
t.Fatalf("仍被使用的 SSH 私钥应禁止删除:%d %s", deleteUsedResponse.Code, deleteUsedResponse.Body.String())
|
||||
}
|
||||
deleteServerResponse := httptest.NewRecorder()
|
||||
server.Handler().ServeHTTP(deleteServerResponse, httptest.NewRequest(http.MethodDelete, "/api/v1/servers/"+createdServer.ID, nil))
|
||||
serveAdminHTTP(server, deleteServerResponse, httptest.NewRequest(http.MethodDelete, "/api/v1/servers/"+createdServer.ID, nil))
|
||||
if deleteServerResponse.Code != http.StatusNoContent {
|
||||
t.Fatalf("删除测试服务器失败:%d %s", deleteServerResponse.Code, deleteServerResponse.Body.String())
|
||||
}
|
||||
deleteKeyResponse := httptest.NewRecorder()
|
||||
server.Handler().ServeHTTP(deleteKeyResponse, httptest.NewRequest(http.MethodDelete, "/api/v1/ssh-keys/"+key.ID, nil))
|
||||
serveAdminHTTP(server, deleteKeyResponse, httptest.NewRequest(http.MethodDelete, "/api/v1/ssh-keys/"+key.ID, nil))
|
||||
if deleteKeyResponse.Code != http.StatusNoContent {
|
||||
t.Fatalf("删除未使用的 SSH 私钥失败:%d %s", deleteKeyResponse.Code, deleteKeyResponse.Body.String())
|
||||
}
|
||||
@ -105,13 +104,13 @@ func TestSSHKeyAPIRejectsInvalidKeyWithoutEchoingIt(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("打开敏感信息存储失败:%v", err)
|
||||
}
|
||||
server := New(Dependencies{Config: config.Default(t.TempDir()), Repository: &stubDomainRepository{}, SSHKeys: sshkey.NewService(store)})
|
||||
server := New(Dependencies{Config: readyTestConfig(t.TempDir()), Repository: &stubDomainRepository{}, SSHKeys: sshkey.NewService(store)})
|
||||
secret := "invalid-private-key-sensitive-content"
|
||||
body, _ := json.Marshal(sshKeyRequest{Name: "错误私钥", PrivateKey: secret})
|
||||
request := httptest.NewRequest(http.MethodPost, "/api/v1/ssh-keys", strings.NewReader(string(body)))
|
||||
request.Header.Set("Content-Type", "application/json")
|
||||
response := httptest.NewRecorder()
|
||||
server.Handler().ServeHTTP(response, request)
|
||||
serveAdminHTTP(server, response, request)
|
||||
if response.Code != http.StatusBadRequest || strings.Contains(response.Body.String(), secret) {
|
||||
t.Fatalf("无效 SSH 私钥响应不安全:%d %s", response.Code, response.Body.String())
|
||||
}
|
||||
@ -122,12 +121,12 @@ func TestSSHKeyAPIRejectsOversizedRequestBody(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("failed to open secret store: %v", err)
|
||||
}
|
||||
server := New(Dependencies{Config: config.Default(t.TempDir()), Repository: &stubDomainRepository{}, SSHKeys: sshkey.NewService(store)})
|
||||
server := New(Dependencies{Config: readyTestConfig(t.TempDir()), Repository: &stubDomainRepository{}, SSHKeys: sshkey.NewService(store)})
|
||||
body := `{"name":"oversized request","private_key":"` + strings.Repeat("a", maxSSHKeyRequestBytes) + `"}`
|
||||
request := httptest.NewRequest(http.MethodPost, "/api/v1/ssh-keys", strings.NewReader(body))
|
||||
request.Header.Set("Content-Type", "application/json")
|
||||
response := httptest.NewRecorder()
|
||||
server.Handler().ServeHTTP(response, request)
|
||||
serveAdminHTTP(server, response, request)
|
||||
if response.Code != http.StatusRequestEntityTooLarge {
|
||||
t.Fatalf("oversized SSH key request should return 413: %d %s", response.Code, response.Body.String())
|
||||
}
|
||||
@ -138,7 +137,7 @@ func TestSSHKeyAPIRejectsOversizedPrivateKeyAndPassphrase(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("failed to open secret store: %v", err)
|
||||
}
|
||||
server := New(Dependencies{Config: config.Default(t.TempDir()), Repository: &stubDomainRepository{}, SSHKeys: sshkey.NewService(store)})
|
||||
server := New(Dependencies{Config: readyTestConfig(t.TempDir()), Repository: &stubDomainRepository{}, SSHKeys: sshkey.NewService(store)})
|
||||
requests := []sshKeyRequest{
|
||||
{Name: "oversized key", PrivateKey: strings.Repeat("a", sshkey.MaxPrivateKeyBytes+1)},
|
||||
{Name: "oversized passphrase", PrivateKey: testSSHPrivateKey(t), Passphrase: strings.Repeat("p", 4097)},
|
||||
@ -151,7 +150,7 @@ func TestSSHKeyAPIRejectsOversizedPrivateKeyAndPassphrase(t *testing.T) {
|
||||
request := httptest.NewRequest(http.MethodPost, "/api/v1/ssh-keys", strings.NewReader(string(body)))
|
||||
request.Header.Set("Content-Type", "application/json")
|
||||
response := httptest.NewRecorder()
|
||||
server.Handler().ServeHTTP(response, request)
|
||||
serveAdminHTTP(server, response, request)
|
||||
leaksPassphrase := input.Passphrase != "" && strings.Contains(response.Body.String(), input.Passphrase)
|
||||
if response.Code != http.StatusBadRequest || strings.Contains(response.Body.String(), input.PrivateKey) || leaksPassphrase {
|
||||
t.Fatalf("oversized input should be rejected safely: %d %s", response.Code, response.Body.String())
|
||||
@ -171,3 +170,48 @@ func testSSHPrivateKey(t *testing.T) string {
|
||||
}
|
||||
return string(pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: encoded}))
|
||||
}
|
||||
|
||||
func TestSSHKeyAPIRejectsUnknownFieldsAndTrailingJSON(t *testing.T) {
|
||||
store, err := secretstore.Open(t.TempDir())
|
||||
if err != nil {
|
||||
t.Fatalf("打开敏感信息存储失败:%v", err)
|
||||
}
|
||||
server := New(Dependencies{Config: readyTestConfig(t.TempDir()), Repository: &stubDomainRepository{}, SSHKeys: sshkey.NewService(store)})
|
||||
privateKey := testSSHPrivateKey(t)
|
||||
tests := []struct {
|
||||
name string
|
||||
body string
|
||||
}{
|
||||
{
|
||||
name: "未知字段",
|
||||
body: `{"name":"测试私钥","private_key":` + mustJSONString(t, privateKey) + `,"unexpected":true}`,
|
||||
},
|
||||
{
|
||||
name: "多段 JSON",
|
||||
body: `{"name":"测试私钥","private_key":` + mustJSONString(t, privateKey) + `}{"name":"第二个私钥","private_key":` + mustJSONString(t, privateKey) + `}`,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
request := httptest.NewRequest(http.MethodPost, "/api/v1/ssh-keys", strings.NewReader(tt.body))
|
||||
request.Header.Set("Content-Type", "application/json")
|
||||
response := httptest.NewRecorder()
|
||||
serveAdminHTTP(server, response, request)
|
||||
if response.Code != http.StatusBadRequest {
|
||||
t.Fatalf("%s 应返回 400,实际为 %d:%s", tt.name, response.Code, response.Body.String())
|
||||
}
|
||||
if strings.Contains(response.Body.String(), privateKey) || strings.Contains(response.Body.String(), "PRIVATE KEY") {
|
||||
t.Fatalf("%s 响应泄露了私钥:%s", tt.name, response.Body.String())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func mustJSONString(t *testing.T, value string) string {
|
||||
t.Helper()
|
||||
encoded, err := json.Marshal(value)
|
||||
if err != nil {
|
||||
t.Fatalf("编码 JSON 字符串失败:%v", err)
|
||||
}
|
||||
return string(encoded)
|
||||
}
|
||||
|
||||
@ -10,7 +10,6 @@ import (
|
||||
"testing"
|
||||
|
||||
"git.pcux.cn/xuans/Scert/backend/internal/certificate"
|
||||
"git.pcux.cn/xuans/Scert/backend/internal/config"
|
||||
"git.pcux.cn/xuans/Scert/backend/internal/database"
|
||||
"git.pcux.cn/xuans/Scert/backend/internal/deployment"
|
||||
"git.pcux.cn/xuans/Scert/backend/internal/domain"
|
||||
@ -19,7 +18,7 @@ import (
|
||||
|
||||
func newTaskAPITestServer(t *testing.T, issue func(context.Context, string) (certificate.IssueResult, deployment.Report, error)) (*Server, *repository.SQLTaskRepository, *repository.SQLAuditRepository, func()) {
|
||||
t.Helper()
|
||||
cfg := config.Default(t.TempDir())
|
||||
cfg := readyTestConfig(t.TempDir())
|
||||
db, err := database.Open(context.Background(), cfg.Database)
|
||||
if err != nil {
|
||||
t.Fatalf("打开测试数据库失败:%v", err)
|
||||
@ -58,7 +57,7 @@ func TestTaskAPIRecordsSuccessfulIssueAndAudit(t *testing.T) {
|
||||
defer closeDB()
|
||||
|
||||
response := httptest.NewRecorder()
|
||||
server.Handler().ServeHTTP(response, httptest.NewRequest(http.MethodPost, "/api/v1/domains/domain-1/issue", nil))
|
||||
serveAdminHTTP(server, response, httptest.NewRequest(http.MethodPost, "/api/v1/domains/domain-1/issue", nil))
|
||||
if response.Code != http.StatusOK {
|
||||
t.Fatalf("成功任务响应错误:%d %s", response.Code, response.Body.String())
|
||||
}
|
||||
@ -76,25 +75,25 @@ func TestTaskAPIRecordsSuccessfulIssueAndAudit(t *testing.T) {
|
||||
}
|
||||
|
||||
list := httptest.NewRecorder()
|
||||
server.Handler().ServeHTTP(list, httptest.NewRequest(http.MethodGet, "/api/v1/tasks?limit=10", nil))
|
||||
serveAdminHTTP(server, list, httptest.NewRequest(http.MethodGet, "/api/v1/tasks?limit=10", nil))
|
||||
if list.Code != http.StatusOK || !strings.Contains(list.Body.String(), items[0].ID) {
|
||||
t.Fatalf("任务列表响应错误:%d %s", list.Code, list.Body.String())
|
||||
}
|
||||
|
||||
detail := httptest.NewRecorder()
|
||||
server.Handler().ServeHTTP(detail, httptest.NewRequest(http.MethodGet, "/api/v1/tasks/"+items[0].ID, nil))
|
||||
serveAdminHTTP(server, detail, httptest.NewRequest(http.MethodGet, "/api/v1/tasks/"+items[0].ID, nil))
|
||||
if detail.Code != http.StatusOK || !strings.Contains(detail.Body.String(), "success") {
|
||||
t.Fatalf("任务详情响应错误:%d %s", detail.Code, detail.Body.String())
|
||||
}
|
||||
|
||||
attemptList := httptest.NewRecorder()
|
||||
server.Handler().ServeHTTP(attemptList, httptest.NewRequest(http.MethodGet, "/api/v1/tasks/"+items[0].ID+"/attempts", nil))
|
||||
serveAdminHTTP(server, attemptList, httptest.NewRequest(http.MethodGet, "/api/v1/tasks/"+items[0].ID+"/attempts", nil))
|
||||
if attemptList.Code != http.StatusOK || !strings.Contains(attemptList.Body.String(), "certificate_issue") {
|
||||
t.Fatalf("任务尝试接口响应错误:%d %s", attemptList.Code, attemptList.Body.String())
|
||||
}
|
||||
|
||||
logList := httptest.NewRecorder()
|
||||
server.Handler().ServeHTTP(logList, httptest.NewRequest(http.MethodGet, "/api/v1/audit-logs?limit=10", nil))
|
||||
serveAdminHTTP(server, logList, httptest.NewRequest(http.MethodGet, "/api/v1/audit-logs?limit=10", nil))
|
||||
if logList.Code != http.StatusOK || !strings.Contains(logList.Body.String(), "certificate_issue") {
|
||||
t.Fatalf("审计日志接口响应错误:%d %s", logList.Code, logList.Body.String())
|
||||
}
|
||||
@ -107,7 +106,7 @@ func TestTaskAPIReturnsFailureAndKeepsTaskTrace(t *testing.T) {
|
||||
defer closeDB()
|
||||
|
||||
response := httptest.NewRecorder()
|
||||
server.Handler().ServeHTTP(response, httptest.NewRequest(http.MethodPost, "/api/v1/domains/domain-1/issue", nil))
|
||||
serveAdminHTTP(server, response, httptest.NewRequest(http.MethodPost, "/api/v1/domains/domain-1/issue", nil))
|
||||
if response.Code != http.StatusBadGateway || !strings.Contains(response.Body.String(), "模拟证书申请失败") {
|
||||
t.Fatalf("失败任务响应错误:%d %s", response.Code, response.Body.String())
|
||||
}
|
||||
@ -145,7 +144,7 @@ func TestTaskAPIRejectsUnknownTask(t *testing.T) {
|
||||
server, _, _, closeDB := newTaskAPITestServer(t, nil)
|
||||
defer closeDB()
|
||||
response := httptest.NewRecorder()
|
||||
server.Handler().ServeHTTP(response, httptest.NewRequest(http.MethodGet, "/api/v1/tasks/unknown", nil))
|
||||
serveAdminHTTP(server, response, httptest.NewRequest(http.MethodGet, "/api/v1/tasks/unknown", nil))
|
||||
if response.Code != http.StatusNotFound {
|
||||
t.Fatalf("未知任务应返回 404:%d %s", response.Code, response.Body.String())
|
||||
}
|
||||
|
||||
20
backend/internal/httpapi/test_helpers_test.go
Normal file
20
backend/internal/httpapi/test_helpers_test.go
Normal file
@ -0,0 +1,20 @@
|
||||
package httpapi
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
|
||||
"git.pcux.cn/xuans/Scert/backend/internal/config"
|
||||
"git.pcux.cn/xuans/Scert/backend/internal/repository"
|
||||
)
|
||||
|
||||
func readyTestConfig(directory string) config.Config {
|
||||
cfg := config.Default(directory)
|
||||
cfg.SetupReady = true
|
||||
return cfg
|
||||
}
|
||||
|
||||
func serveAdminHTTP(server *Server, recorder *httptest.ResponseRecorder, request *http.Request) {
|
||||
request.AddCookie(httpUserSessionCookie(server, "test-admin", repository.UserRoleAdmin))
|
||||
server.Handler().ServeHTTP(recorder, request)
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user