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("关闭后仍有初始化处理器运行") } }