Scert/backend/internal/bootstrap/server_test.go

179 lines
5.8 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 bootstrap
import (
"context"
"crypto/tls"
"crypto/x509"
"encoding/json"
"io"
"net"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"strings"
"testing"
"time"
"git.pcux.cn/xuans/Scert/backend/internal/config"
)
func TestTemporaryHTTPSDrainsCompletionResponse(t *testing.T) {
material, err := NewTemporaryTLSMaterial("127.0.0.1")
if err != nil {
t.Fatal(err)
}
listener, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatal(err)
}
completed := make(chan struct{})
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
result := make(chan error, 1)
body := strings.Repeat("response-data", 10000)
go func() {
ok, err := serveTemporaryHTTPS(ctx, listener, material, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
close(completed)
time.Sleep(30 * time.Millisecond)
_, _ = io.WriteString(w, body)
}), completed)
if !ok && err == nil {
err = context.Canceled
}
result <- err
}()
roots := x509.NewCertPool()
roots.AddCert(material.Leaf)
transport := &http.Transport{TLSClientConfig: &tls.Config{RootCAs: roots, MinVersion: tls.VersionTLS12}}
defer transport.CloseIdleConnections()
client := &http.Client{Transport: transport, Timeout: 5 * time.Second}
response, err := client.Get("https://" + listener.Addr().String() + "/")
if err != nil {
t.Fatal(err)
}
data, err := io.ReadAll(response.Body)
response.Body.Close()
if err != nil || string(data) != body {
t.Fatalf("成功响应被关闭截断:%v,长度 %d", err, len(data))
}
select {
case err := <-result:
if err != nil {
t.Fatal(err)
}
case <-time.After(5 * time.Second):
t.Fatal("临时服务没有关闭")
}
if conn, err := net.DialTimeout("tcp", listener.Addr().String(), 100*time.Millisecond); err == nil {
conn.Close()
t.Fatal("关闭后仍可连接")
}
}
func TestBootstrapHandlerIsolatesBusinessRoutesAndSettings(t *testing.T) {
root := t.TempDir()
if err := os.MkdirAll(filepath.Join(root, "web"), 0o700); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(root, "web", "index.html"), []byte("bootstrap-page"), 0o600); err != nil {
t.Fatal(err)
}
cfg := config.Default(root)
cfg.Paths.Install = root
sessions, token, err := NewSessionManager()
if err != nil {
t.Fatal(err)
}
router := NewRouter(sessions, nil, nil, nil)
handler, closeFiles := bootstrapHandler(router, cfg)
defer closeFiles()
for path, status := range map[string]int{
"/": 200, "/api/v1/bootstrap/status": 200, "/api/v1/bootstrap/settings": 401,
"/api/v1/setup": 404, "/api/v1/domains": 404, "/reset": 404, "/scert.json": 404,
"/assets/../../config.json": 400,
} {
response := httptest.NewRecorder()
handler.ServeHTTP(response, httptest.NewRequest(http.MethodGet, path, nil))
if response.Code != status {
t.Fatalf("%s 状态码 %d,期望 %d", path, response.Code, status)
}
if response.Header().Get("Cache-Control") != "no-store" {
t.Fatal("临时页面允许缓存")
}
}
session, err := sessions.Exchange(token, "127.0.0.1")
if err != nil {
t.Fatal(err)
}
request := httptest.NewRequest(http.MethodGet, "/api/v1/bootstrap/settings", nil)
request.AddCookie(BootstrapSessionCookie(session.Token, session.ExpiresAt))
response := httptest.NewRecorder()
handler.ServeHTTP(response, request)
if response.Code != 200 || !strings.Contains(response.Body.String(), `"mode":"bootstrap"`) {
t.Fatal("认证后无法读取初始化设置")
}
}
func TestBootstrapRejectsInvalidFirewallModeBeforeWritingFiles(t *testing.T) {
root := t.TempDir()
configPath := filepath.Join(root, "conf", "scert.json")
urlPath := filepath.Join(root, "run", "bootstrap-url")
ok, err := Run(context.Background(), ServerOptions{
Config: config.Default(root), ConfigPath: configPath, URLPath: urlPath,
PublicIP: "127.0.0.1", FirewallMode: "invalid",
})
if err == nil || ok {
t.Fatal("错误防火墙模式不应启动初始化")
}
entries, err := os.ReadDir(root)
if err != nil || len(entries) != 0 {
t.Fatal("防火墙模式校验应发生在创建任何配置或敏感信息文件之前")
}
}
func TestBootstrapHealthProbesDoNotRequireSessionOrBusinessResources(t *testing.T) {
root := t.TempDir()
cfg := config.Default(root)
handler, closeFiles := bootstrapHandler(NewRouter(nil, nil, nil, nil), cfg)
defer closeFiles()
for _, scenario := range []struct {
path string
code int
status string
reason string
}{
{"/healthz", http.StatusOK, "ok", ""},
{"/readyz", http.StatusServiceUnavailable, "not_ready", "setup_incomplete"},
} {
t.Run(scenario.path, func(t *testing.T) {
response := httptest.NewRecorder()
handler.ServeHTTP(response, httptest.NewRequest(http.MethodGet, scenario.path, nil))
if response.Code != scenario.code {
t.Fatalf("探针状态错误:得到 %d,期望 %d", response.Code, scenario.code)
}
var payload map[string]string
if err := json.Unmarshal(response.Body.Bytes(), &payload); err != nil {
t.Fatal(err)
}
if payload["service"] != "scert" || payload["status"] != scenario.status || payload["reason"] != scenario.reason {
t.Fatalf("探针响应错误:%v", payload)
}
if response.Header().Get("Cache-Control") != "no-store" || response.Header().Get("Set-Cookie") != "" {
t.Fatal("探针不得缓存或创建初始化会话")
}
for _, method := range []string{http.MethodPost, http.MethodPut, http.MethodDelete} {
rejected := httptest.NewRecorder()
handler.ServeHTTP(rejected, httptest.NewRequest(method, scenario.path, nil))
if rejected.Code != http.StatusMethodNotAllowed || rejected.Header().Get("Allow") != http.MethodGet {
t.Fatalf("探针未拒绝错误方法:%s %d", method, rejected.Code)
}
}
})
}
entries, err := os.ReadDir(root)
if err != nil || len(entries) != 0 {
t.Fatal("健康检查不应提前创建数据库或配置文件")
}
}