179 lines
5.8 KiB
Go
179 lines
5.8 KiB
Go
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("健康检查不应提前创建数据库或配置文件")
|
||
}
|
||
}
|