167 lines
5.2 KiB
Go
167 lines
5.2 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"git.pcux.cn/xuans/Scert/backend/internal/setup"
|
|
)
|
|
|
|
func prepareDesktopTransition(t *testing.T) (*Runtime, http.Handler, string, func()) {
|
|
t.Helper()
|
|
root := t.TempDir()
|
|
path := filepath.Join(root, "config", "scert.json")
|
|
runtime, err := Open(path, root)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Cleanup(func() {
|
|
if err := runtime.Close(); err != nil {
|
|
t.Error(err)
|
|
}
|
|
})
|
|
cfg := defaultConfig(root)
|
|
if err := setup.Apply(&cfg, setup.Input{
|
|
AdminUsername: "test-admin", AdminEmail: "admin@example.test", AdminPassword: "Test1234!",
|
|
ServerPort: 8080, WebHostname: "scert.example.test", WebEnabled: true,
|
|
DNSProvider: "aliyun", DNSCredentialRef: "secret://dns/test", DNSVerified: true,
|
|
CertificateRoot: cfg.Paths.Certificate, HTTPSMode: "direct",
|
|
}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
cfg.SetupReady = true
|
|
cfg.Web.InitReady = true
|
|
return runtime, runtime.Handler(), path, func() {
|
|
t.Helper()
|
|
// 只模拟旧向导已经完成的配置提交,不连接真实 DNS 或证书签发服务。
|
|
if err := cfg.Save(path); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestDesktopSameHandlerActivatesSavedSetupAndAllowsLogin(t *testing.T) {
|
|
runtime, handler, _, save := prepareDesktopTransition(t)
|
|
if runtime.core != nil {
|
|
t.Fatal("未保存配置前不应启动正式服务")
|
|
}
|
|
save()
|
|
request := httptest.NewRequest(http.MethodPost, "http://localhost/api/v1/auth/login", strings.NewReader(`{"identifier":"test-admin","password":"Test1234!"}`))
|
|
request.Header.Set("Content-Type", "application/json")
|
|
request.RemoteAddr = "127.0.0.1:12345"
|
|
response := httptest.NewRecorder()
|
|
handler.ServeHTTP(response, request)
|
|
if response.Code != http.StatusOK || !strings.Contains(response.Body.String(), `"authenticated":true`) {
|
|
t.Fatalf("保存后原处理器不能登录:%d", response.Code)
|
|
}
|
|
if runtime.core == nil || runtime.initial != nil || runtime.initialDNS != nil {
|
|
t.Fatal("初始化运行时没有切换为正式服务")
|
|
}
|
|
if len(response.Result().Cookies()) == 0 {
|
|
t.Fatal("登录没有返回会话")
|
|
}
|
|
cookie := response.Result().Cookies()[0]
|
|
request = httptest.NewRequest(http.MethodGet, "/api/v1/auth/session", nil)
|
|
request.AddCookie(cookie)
|
|
response = httptest.NewRecorder()
|
|
handler.ServeHTTP(response, request)
|
|
if response.Code != http.StatusOK {
|
|
t.Fatal("正式登录会话不可用")
|
|
}
|
|
response = httptest.NewRecorder()
|
|
handler.ServeHTTP(response, httptest.NewRequest(http.MethodPost, "/api/v1/setup", nil))
|
|
if response.Code != http.StatusNotFound {
|
|
t.Fatal("切换后旧初始化写接口仍可访问")
|
|
}
|
|
}
|
|
|
|
func TestDesktopConcurrentTransitionAndIdempotentClose(t *testing.T) {
|
|
runtime, handler, _, save := prepareDesktopTransition(t)
|
|
save()
|
|
var pending sync.WaitGroup
|
|
for index := 0; index < 16; index++ {
|
|
pending.Go(func() {
|
|
response := httptest.NewRecorder()
|
|
handler.ServeHTTP(response, httptest.NewRequest(http.MethodGet, "/api/v1/auth/session", nil))
|
|
if response.Code != http.StatusUnauthorized {
|
|
t.Errorf("并发切换后的路由状态错误:%d", response.Code)
|
|
}
|
|
})
|
|
}
|
|
pending.Wait()
|
|
for index := 0; index < 8; index++ {
|
|
pending.Go(func() {
|
|
if err := runtime.Close(); err != nil {
|
|
t.Error(err)
|
|
}
|
|
})
|
|
}
|
|
pending.Wait()
|
|
response := httptest.NewRecorder()
|
|
handler.ServeHTTP(response, httptest.NewRequest(http.MethodGet, "/api/v1/setup/status", nil))
|
|
if response.Code != http.StatusServiceUnavailable {
|
|
t.Fatal("关闭后不应重新创建正式服务")
|
|
}
|
|
}
|
|
|
|
func TestDesktopTransitionRejectsCorruptedSavedConfig(t *testing.T) {
|
|
runtime, handler, path, _ := prepareDesktopTransition(t)
|
|
if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(path, []byte(`{invalid`), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
response := httptest.NewRecorder()
|
|
handler.ServeHTTP(response, httptest.NewRequest(http.MethodGet, "/api/v1/setup/status", nil))
|
|
if response.Code != http.StatusServiceUnavailable || runtime.core != nil {
|
|
t.Fatal("损坏配置不得回退到新的初始化或启动正式服务")
|
|
}
|
|
}
|
|
|
|
func TestDesktopCloseCancelsInFlightInitialization(t *testing.T) {
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
started := make(chan struct{})
|
|
finished := make(chan struct{})
|
|
runtime := &Runtime{
|
|
ctx: ctx, cancel: cancel, configPath: filepath.Join(t.TempDir(), "scert.json"),
|
|
initial: http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
|
|
close(started)
|
|
<-request.Context().Done()
|
|
writer.WriteHeader(http.StatusServiceUnavailable)
|
|
}),
|
|
}
|
|
go func() {
|
|
defer close(finished)
|
|
runtime.Handler().ServeHTTP(httptest.NewRecorder(), httptest.NewRequest(http.MethodPost, "/api/v1/setup", nil))
|
|
}()
|
|
select {
|
|
case <-started:
|
|
case <-time.After(5 * time.Second):
|
|
t.Fatal("初始化处理器没有启动")
|
|
}
|
|
closed := make(chan error, 1)
|
|
go func() { closed <- runtime.Close() }()
|
|
select {
|
|
case err := <-closed:
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
case <-time.After(5 * time.Second):
|
|
t.Fatal("关闭运行时没有取消在途初始化请求")
|
|
}
|
|
select {
|
|
case <-finished:
|
|
case <-time.After(5 * time.Second):
|
|
t.Fatal("关闭后仍有初始化处理器运行")
|
|
}
|
|
}
|