202 lines
6.6 KiB
Go
202 lines
6.6 KiB
Go
package httpapi
|
||
|
||
import (
|
||
"context"
|
||
"errors"
|
||
"net/http"
|
||
"net/http/httptest"
|
||
"os"
|
||
"path/filepath"
|
||
"strings"
|
||
"testing"
|
||
|
||
"git.pcux.cn/xuans/Scert/backend/internal/config"
|
||
"git.pcux.cn/xuans/Scert/backend/internal/secretstore"
|
||
)
|
||
|
||
const readinessDNSReference = "secret://dns/aliyun/readiness-test"
|
||
|
||
func TestReadinessRejectsIncompleteSetup(t *testing.T) {
|
||
cfg := config.Default(t.TempDir())
|
||
response := requestReadiness(New(Dependencies{Config: cfg}))
|
||
assertReadinessResponse(t, response, http.StatusServiceUnavailable, "setup_incomplete")
|
||
}
|
||
|
||
func TestReadinessRejectsMissingDNSCredentialReference(t *testing.T) {
|
||
cfg := readinessConfig(t)
|
||
cfg.DNS.CredentialRef = ""
|
||
response := requestReadiness(New(Dependencies{Config: cfg}))
|
||
assertReadinessResponse(t, response, http.StatusServiceUnavailable, "dns_credential_missing")
|
||
}
|
||
|
||
func TestReadinessRejectsUnavailableCredentialWithoutLeakingReference(t *testing.T) {
|
||
cfg := readinessConfig(t)
|
||
store, err := secretstore.Open(filepath.Join(t.TempDir(), "secrets"))
|
||
if err != nil {
|
||
t.Fatalf("打开测试敏感信息存储失败:%v", err)
|
||
}
|
||
response := requestReadiness(New(Dependencies{Config: cfg, Secrets: store}))
|
||
assertReadinessResponse(t, response, http.StatusServiceUnavailable, "credential_unavailable")
|
||
if strings.Contains(response.Body.String(), readinessDNSReference) || strings.Contains(response.Body.String(), "secret://") {
|
||
t.Fatalf("生产就绪响应泄漏敏感信息引用:%s", response.Body.String())
|
||
}
|
||
}
|
||
|
||
func TestReadinessChecksConfiguredNotificationCredentials(t *testing.T) {
|
||
tests := []struct {
|
||
name string
|
||
configure func(*config.Config)
|
||
}{
|
||
{
|
||
name: "HTTP notification",
|
||
configure: func(cfg *config.Config) {
|
||
cfg.Notification.HTTPCredentialRef = "secret://notification/http/readiness-test"
|
||
},
|
||
},
|
||
{
|
||
name: "SMTP notification",
|
||
configure: func(cfg *config.Config) {
|
||
cfg.Notification.SMTPCredentialRef = "secret://notification/smtp/readiness-test"
|
||
},
|
||
},
|
||
}
|
||
for _, test := range tests {
|
||
t.Run(test.name, func(t *testing.T) {
|
||
cfg := readinessConfig(t)
|
||
test.configure(&cfg)
|
||
store := readinessStore(t)
|
||
response := requestReadiness(New(Dependencies{Config: cfg, Secrets: store}))
|
||
assertReadinessResponse(t, response, http.StatusServiceUnavailable, "credential_unavailable")
|
||
})
|
||
}
|
||
}
|
||
|
||
func TestReadinessRejectsInvalidTLSMaterial(t *testing.T) {
|
||
tests := []struct {
|
||
name string
|
||
configure func(*testing.T, *config.Config)
|
||
}{
|
||
{
|
||
name: "missing file",
|
||
configure: func(t *testing.T, cfg *config.Config) {
|
||
cfg.Server.TLSCert = filepath.Join(t.TempDir(), "missing.pem")
|
||
},
|
||
},
|
||
{
|
||
name: "directory",
|
||
configure: func(t *testing.T, cfg *config.Config) {
|
||
cfg.Server.TLSKey = t.TempDir()
|
||
},
|
||
},
|
||
{
|
||
name: "symbolic link",
|
||
configure: func(t *testing.T, cfg *config.Config) {
|
||
target := filepath.Join(t.TempDir(), "target.pem")
|
||
if err := os.WriteFile(target, []byte("test"), 0o600); err != nil {
|
||
t.Fatalf("写入符号链接目标失败:%v", err)
|
||
}
|
||
link := filepath.Join(t.TempDir(), "linked.pem")
|
||
if err := os.Symlink(target, link); err != nil {
|
||
t.Skipf("当前环境无法创建符号链接:%v", err)
|
||
}
|
||
cfg.Server.TLSCert = link
|
||
},
|
||
},
|
||
}
|
||
for _, test := range tests {
|
||
t.Run(test.name, func(t *testing.T) {
|
||
cfg := readinessConfig(t)
|
||
test.configure(t, &cfg)
|
||
response := requestReadiness(New(Dependencies{Config: cfg, Secrets: readinessStore(t)}))
|
||
assertReadinessResponse(t, response, http.StatusServiceUnavailable, "tls_material_unavailable")
|
||
})
|
||
}
|
||
}
|
||
|
||
func TestReadinessRejectsUnavailableDatabase(t *testing.T) {
|
||
cfg := readinessConfig(t)
|
||
response := requestReadiness(New(Dependencies{
|
||
Config: cfg,
|
||
Secrets: readinessStore(t),
|
||
ReadinessCheck: func(_ context.Context) error {
|
||
return errors.New("database unavailable")
|
||
},
|
||
}))
|
||
assertReadinessResponse(t, response, http.StatusServiceUnavailable, "database_unavailable")
|
||
}
|
||
|
||
func TestReadinessSucceedsWhenDependenciesAreAvailable(t *testing.T) {
|
||
cfg := readinessConfig(t)
|
||
response := requestReadiness(New(Dependencies{
|
||
Config: cfg,
|
||
Secrets: readinessStore(t),
|
||
ReadinessCheck: func(_ context.Context) error {
|
||
return nil
|
||
},
|
||
}))
|
||
assertReadinessResponse(t, response, http.StatusOK, "ready")
|
||
}
|
||
|
||
func TestReadinessIsNotHandledByFrontendFallback(t *testing.T) {
|
||
staticDir := t.TempDir()
|
||
if err := os.WriteFile(filepath.Join(staticDir, "index.html"), []byte("frontend fallback"), 0o644); err != nil {
|
||
t.Fatalf("写入前端入口失败:%v", err)
|
||
}
|
||
cfg := config.Default(t.TempDir())
|
||
response := requestReadiness(New(Dependencies{Config: cfg, FrontendDir: staticDir}))
|
||
assertReadinessResponse(t, response, http.StatusServiceUnavailable, "setup_incomplete")
|
||
if strings.Contains(response.Body.String(), "frontend fallback") {
|
||
t.Fatalf("生产就绪路径被前端回退接管:%s", response.Body.String())
|
||
}
|
||
}
|
||
|
||
func readinessConfig(t *testing.T) config.Config {
|
||
t.Helper()
|
||
directory := t.TempDir()
|
||
cfg := config.Default(directory)
|
||
cfg.SetupReady = true
|
||
cfg.DNS.Provider = "aliyun"
|
||
cfg.DNS.CredentialRef = readinessDNSReference
|
||
cfg.Server.TLSCert = filepath.Join(directory, "server.crt")
|
||
cfg.Server.TLSKey = filepath.Join(directory, "server.key")
|
||
if err := os.WriteFile(cfg.Server.TLSCert, []byte("certificate"), 0o644); err != nil {
|
||
t.Fatalf("写入测试证书失败:%v", err)
|
||
}
|
||
if err := os.WriteFile(cfg.Server.TLSKey, []byte("private key"), 0o600); err != nil {
|
||
t.Fatalf("写入测试私钥失败:%v", err)
|
||
}
|
||
return cfg
|
||
}
|
||
|
||
func readinessStore(t *testing.T) *secretstore.Store {
|
||
t.Helper()
|
||
store, err := secretstore.Open(filepath.Join(t.TempDir(), "secrets"))
|
||
if err != nil {
|
||
t.Fatalf("打开测试敏感信息存储失败:%v", err)
|
||
}
|
||
if err := store.Put(t.Context(), readinessDNSReference, map[string]string{
|
||
"access_key_id": "readiness-test-id",
|
||
"access_key_secret": "readiness-test-secret",
|
||
}); err != nil {
|
||
t.Fatalf("写入测试 DNS 凭据失败:%v", err)
|
||
}
|
||
return store
|
||
}
|
||
|
||
func requestReadiness(server *Server) *httptest.ResponseRecorder {
|
||
response := httptest.NewRecorder()
|
||
server.Handler().ServeHTTP(response, httptest.NewRequest(http.MethodGet, "/readyz", nil))
|
||
return response
|
||
}
|
||
|
||
func assertReadinessResponse(t *testing.T, response *httptest.ResponseRecorder, wantStatus int, wantState string) {
|
||
t.Helper()
|
||
if response.Code != wantStatus {
|
||
t.Fatalf("生产就绪状态码 = %d,期望 %d,响应:%s", response.Code, wantStatus, response.Body.String())
|
||
}
|
||
if !strings.Contains(response.Body.String(), `"status":"`+wantState+`"`) &&
|
||
!strings.Contains(response.Body.String(), `"reason":"`+wantState+`"`) {
|
||
t.Fatalf("生产就绪响应缺少状态 %q:%s", wantState, response.Body.String())
|
||
}
|
||
}
|