Scert/backend/internal/cli/application_services_test.go
xuans 0b74886c45 修复Linux启动就绪通知并隔离命令行测试目录
通过本机Unix套接字报告HTTPS监听状态,同步服务配置与安装模板,保持初始化业务就绪隔离。补充生命周期回归并修复旧CLI测试误用正式目录的问题。更新Linux原生测试证据,明确新候选尚未部署、生产验收未放行。
2026-10-02 12:20:47 +08:00

188 lines
5.5 KiB
Go

package cli
import (
"context"
"errors"
"net/http"
"sync/atomic"
"testing"
"time"
"git.pcux.cn/xuans/Scert/backend/internal/config"
"git.pcux.cn/xuans/Scert/backend/internal/localreset"
)
type applicationTestHTTP struct {
ready chan struct{}
exit chan error
started atomic.Int32
}
func (h *applicationTestHTTP) Listening() <-chan struct{} { return h.ready }
func (h *applicationTestHTTP) ListenAndServe() error {
h.started.Add(1)
return <-h.exit
}
type applicationTestControl struct {
serve func(context.Context, localreset.Factory) error
closed atomic.Int32
}
func (c *applicationTestControl) Serve(ctx context.Context, factory localreset.Factory) error {
return c.serve(ctx, factory)
}
func (c *applicationTestControl) Close() error {
c.closed.Add(1)
return nil
}
func applicationTestFactory(context.Context, localreset.Peer) (localreset.Session, error) {
return nil, localreset.ErrUnavailable
}
func TestApplicationControlFailurePreventsHTTPStartup(t *testing.T) {
web := &applicationTestHTTP{ready: make(chan struct{}), exit: make(chan error, 1)}
expected := errors.New("test-control-bind-failure")
_, err := runApplicationServices(t.Context(), web, nil, nil, applicationTestFactory, func() (applicationControl, error) {
return nil, expected
})
if !errors.Is(err, expected) || web.started.Load() != 0 {
t.Fatal("本机通道创建失败后仍启动了网络服务")
}
}
func TestApplicationCancellationBeforeStartupDoesNotOpenControl(t *testing.T) {
ctx, cancel := context.WithCancel(t.Context())
cancel()
opened := false
_, err := runApplicationServices(ctx, nil, nil, nil, nil, func() (applicationControl, error) {
opened = true
return nil, nil
})
if err != nil || opened {
t.Fatal("取消后仍然启动本机通道")
}
}
func TestApplicationBootstrapCannotOpenControl(t *testing.T) {
cfg := config.Default(t.TempDir())
cfg.SetupReady = false
if _, err := runApplication(t.Context(), cfg, ""); !errors.Is(err, localreset.ErrUnavailable) {
t.Fatal("初始化阶段没有拒绝正式本机通道")
}
}
func TestApplicationWaitsForControlWorkBeforeReturning(t *testing.T) {
for _, scenario := range []string{"cancel-starting", "cancel-running", "http-startup-error", "http-runtime-error", "http-closed"} {
t.Run(scenario, func(t *testing.T) {
ctx, cancel := context.WithCancel(t.Context())
defer cancel()
web := &applicationTestHTTP{ready: make(chan struct{}), exit: make(chan error, 1)}
t.Cleanup(func() { web.exit <- http.ErrServerClosed })
entered := make(chan struct{})
cancelled := make(chan struct{})
release := make(chan struct{})
finished := make(chan struct{})
control := &applicationTestControl{serve: func(ctx context.Context, factory localreset.Factory) error {
if factory == nil {
t.Error("主服务未提供重置工厂")
}
close(entered)
<-ctx.Done()
close(cancelled)
<-release
close(finished)
return nil
}}
renewal := make(chan struct{})
done := make(chan error, 1)
go func() {
_, err := runApplicationServices(ctx, web, func(context.Context) { close(renewal) }, nil, applicationTestFactory, func() (applicationControl, error) { return control, nil })
done <- err
}()
<-entered
if scenario == "cancel-running" || scenario == "http-runtime-error" || scenario == "http-closed" {
close(web.ready)
<-renewal
}
expected := errors.New("test-http-failure")
switch scenario {
case "cancel-starting", "cancel-running":
cancel()
case "http-closed":
web.exit <- http.ErrServerClosed
default:
web.exit <- expected
}
select {
case <-cancelled:
case <-time.After(3 * time.Second):
t.Fatal("没有取消本机重置处理")
}
select {
case <-done:
t.Fatal("业务处理尚未结束就返回,可能提前关闭数据库")
default:
}
close(release)
select {
case err := <-done:
if scenario == "http-startup-error" || scenario == "http-runtime-error" {
if !errors.Is(err, expected) {
t.Fatal("丢失 HTTP 启动或运行错误")
}
} else if err != nil {
t.Fatal(err)
}
case <-time.After(3 * time.Second):
t.Fatal("本机通道退出后主服务仍未返回")
}
<-finished
if control.closed.Load() != 1 {
t.Fatal("没有关闭本机通道")
}
})
}
}
func TestApplicationControlUnexpectedExitFailsClosed(t *testing.T) {
for _, ready := range []bool{false, true} {
name := "starting"
if ready {
name = "running"
}
t.Run(name, func(t *testing.T) {
web := &applicationTestHTTP{ready: make(chan struct{}), exit: make(chan error, 1)}
t.Cleanup(func() { web.exit <- http.ErrServerClosed })
stop := make(chan struct{})
control := &applicationTestControl{serve: func(context.Context, localreset.Factory) error {
<-stop
return nil
}}
renewal := make(chan struct{})
done := make(chan error, 1)
go func() {
_, err := runApplicationServices(t.Context(), web, func(context.Context) { close(renewal) }, nil, applicationTestFactory, func() (applicationControl, error) { return control, nil })
done <- err
}()
if ready {
close(web.ready)
<-renewal
}
close(stop)
select {
case err := <-done:
if !errors.Is(err, localreset.ErrUnavailable) {
t.Fatal("本机安全通道异常退出被当作正常状态")
}
case <-time.After(3 * time.Second):
t.Fatal("本机通道退出后 HTTP 生命周期未结束")
}
if control.closed.Load() != 1 {
t.Fatal("本机通道没有关闭")
}
})
}
}