Scert/backend/internal/httpapi/readiness_test.go

202 lines
6.6 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 (
"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())
}
}