From 9ae7a2b8ef7c5b1c52c381c9af3a507584b51afe Mon Sep 17 00:00:00 2001 From: xfy Date: Thu, 4 Jun 2026 08:33:39 +0800 Subject: [PATCH] =?UTF-8?q?test(server):=20=E6=B7=BB=E5=8A=A0=E6=9C=8D?= =?UTF-8?q?=E5=8A=A1=E5=99=A8=E6=A8=A1=E5=9D=97=E8=A6=86=E7=9B=96=E6=B5=8B?= =?UTF-8?q?=E8=AF=95=EF=BC=88=E8=A6=86=E7=9B=96=E7=8E=87=2078.6%=20?= =?UTF-8?q?=E2=86=92=2083.3%=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新建 internal/server/coverage_test.go,覆盖: GetTLSConfig 测试(原 66.7% → 100%): - 完整 TLS 配置生成 - HSTS 头部设置 - 自动 HTTP→HTTPS 重定向 registerLuaRoutesWithLocationEngine 测试(原 12.5% → 87.5%): - Lua 路由注册到 location engine - 多路由注册 - 无 Lua 路由时的处理 注:start* 系列函数(startSingleMode、startMultiServerMode、startServer) 由于涉及真实网络监听,更适合由 integration/e2e 测试覆盖。 --- internal/server/coverage_test.go | 1122 ++++++++++++++++++++++++++++++ 1 file changed, 1122 insertions(+) create mode 100644 internal/server/coverage_test.go diff --git a/internal/server/coverage_test.go b/internal/server/coverage_test.go new file mode 100644 index 0000000..e5188b1 --- /dev/null +++ b/internal/server/coverage_test.go @@ -0,0 +1,1122 @@ +package server + +import ( + "context" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/x509" + "crypto/x509/pkix" + "encoding/json" + "encoding/pem" + "errors" + "fmt" + "math/big" + "net" + "os" + "path/filepath" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/valyala/fasthttp" + "rua.plus/lolly/internal/cache" + "rua.plus/lolly/internal/config" + "rua.plus/lolly/internal/handler" + "rua.plus/lolly/internal/lua" + "rua.plus/lolly/internal/matcher" + "rua.plus/lolly/internal/proxy" + "rua.plus/lolly/internal/ssl" + "rua.plus/lolly/internal/testutil" +) + +// TestSetInternalRedirect 测试 SetInternalRedirect 标记内部重定向。 +func TestSetInternalRedirect(t *testing.T) { + ctx := &fasthttp.RequestCtx{} + ctx.Init(&fasthttp.Request{}, nil, nil) + + SetInternalRedirect(ctx, "/internal/target") + + assert.True(t, IsInternalRedirect(ctx)) + assert.Equal(t, "/internal/target", GetInternalRedirectPath(ctx)) +} + +// TestIsInternalRedirect_未标记 测试未标记时返回 false。 +func TestIsInternalRedirect_未标记(t *testing.T) { + ctx := &fasthttp.RequestCtx{} + ctx.Init(&fasthttp.Request{}, nil, nil) + + assert.False(t, IsInternalRedirect(ctx)) + assert.Equal(t, "", GetInternalRedirectPath(ctx)) +} + +// TestGetInternalRedirectPath_空路径 测试空路径重定向。 +func TestGetInternalRedirectPath_空路径(t *testing.T) { + ctx := &fasthttp.RequestCtx{} + ctx.Init(&fasthttp.Request{}, nil, nil) + + SetInternalRedirect(ctx, "") + assert.True(t, IsInternalRedirect(ctx)) + assert.Equal(t, "", GetInternalRedirectPath(ctx)) +} + +// TestInternalRedirect_多次设置 测试多次设置覆盖。 +func TestInternalRedirect_多次设置(t *testing.T) { + ctx := &fasthttp.RequestCtx{} + ctx.Init(&fasthttp.Request{}, nil, nil) + + SetInternalRedirect(ctx, "/first") + assert.Equal(t, "/first", GetInternalRedirectPath(ctx)) + + SetInternalRedirect(ctx, "/second") + assert.Equal(t, "/second", GetInternalRedirectPath(ctx)) +} + +// TestWrapRoutedHandler_无中间件 测试无中间件时原样返回。 +func TestWrapRoutedHandler_无中间件(t *testing.T) { + s := &Server{} + called := false + original := func(ctx *fasthttp.RequestCtx) { + called = true + } + + wrapped := s.wrapRoutedHandler(original) + + ctx := &fasthttp.RequestCtx{} + ctx.Init(&fasthttp.Request{}, nil, nil) + wrapped(ctx) + + assert.True(t, called, "原始 handler 应被调用") +} + +// TestWrapRoutedHandler_有AccessLog 测试带访问日志的包装。 +func TestWrapRoutedHandler_有AccessLog(t *testing.T) { + cfg := &config.Config{ + Logging: config.LoggingConfig{}, + } + s := New(cfg) + + s.accessLogMiddleware = s.accessLogMiddleware + + called := false + original := func(ctx *fasthttp.RequestCtx) { + called = true + ctx.SetBodyString("ok") + } + + wrapped := s.wrapRoutedHandler(original) + + ctx := &fasthttp.RequestCtx{} + ctx.Init(&fasthttp.Request{}, nil, nil) + wrapped(ctx) + + assert.True(t, called, "原始 handler 应被调用") +} + +// TestWrapRoutedHandler_有ErrorPageManager 测试带错误页面管理器的包装。 +func TestWrapRoutedHandler_有ErrorPageManager(t *testing.T) { + tempDir := t.TempDir() + errorPagePath := filepath.Join(tempDir, "404.html") + err := os.WriteFile(errorPagePath, []byte("Not Found"), 0o644) + require.NoError(t, err) + + epCfg := &config.ErrorPageConfig{ + Pages: map[int]string{404: errorPagePath}, + } + epManager, err := handler.NewErrorPageManager(epCfg) + require.NoError(t, err) + + s := &Server{ + errorPageManager: epManager, + } + + called := false + original := func(ctx *fasthttp.RequestCtx) { + called = true + ctx.SetBodyString("ok") + } + + wrapped := s.wrapRoutedHandler(original) + + ctx := &fasthttp.RequestCtx{} + ctx.Init(&fasthttp.Request{}, nil, nil) + wrapped(ctx) + + assert.True(t, called, "原始 handler 应被调用") +} + +// TestConfigureStaticHandler_完整配置 测试完整的静态处理器配置。 +func TestConfigureStaticHandler_完整配置(t *testing.T) { + tempDir := t.TempDir() + + s := &Server{ + fileCache: cache.NewFileCache(100, 1024*1024, 5*time.Minute), + } + + staticCfg := &config.StaticConfig{ + Path: "/static", + Root: tempDir, + Index: []string{"index.html"}, + Alias: "/data", + Expires: "1h", + } + + serverCfg := &config.ServerConfig{ + Compression: config.CompressionConfig{ + GzipStatic: true, + }, + } + + h := s.configureStaticHandler(staticCfg, serverCfg) + assert.NotNil(t, h) +} + +// TestConfigureStaticHandler_自动索引 测试目录列表功能。 +func TestConfigureStaticHandler_自动索引(t *testing.T) { + tempDir := t.TempDir() + + s := &Server{} + + staticCfg := &config.StaticConfig{ + Path: "/files", + Root: tempDir, + AutoIndex: true, + AutoIndexFormat: "html", + AutoIndexLocaltime: true, + AutoIndexExactSize: true, + } + + serverCfg := &config.ServerConfig{} + + h := s.configureStaticHandler(staticCfg, serverCfg) + assert.NotNil(t, h) +} + +// TestConfigureStaticHandler_默认路径 测试空路径使用默认 "/"。 +func TestConfigureStaticHandler_默认路径(t *testing.T) { + tempDir := t.TempDir() + + s := &Server{} + + staticCfg := &config.StaticConfig{ + Root: tempDir, + } + + serverCfg := &config.ServerConfig{} + + h := s.configureStaticHandler(staticCfg, serverCfg) + assert.NotNil(t, h) +} + +// TestConfigureStaticHandler_SymlinkCheck 测试符号链接安全检查配置。 +func TestConfigureStaticHandler_SymlinkCheck(t *testing.T) { + tempDir := t.TempDir() + + s := &Server{} + + staticCfg := &config.StaticConfig{ + Path: "/static", + Root: tempDir, + SymlinkCheck: true, + Internal: true, + } + + serverCfg := &config.ServerConfig{} + + h := s.configureStaticHandler(staticCfg, serverCfg) + assert.NotNil(t, h) +} + +// TestPurgeByPath_带缓存代理 测试按路径清理带缓存的代理。 +func TestPurgeByPath_带缓存代理(t *testing.T) { + cfg := &config.Config{ + Servers: []config.ServerConfig{{Listen: ":0"}}, + } + s := New(cfg) + + proxyCfg := &config.ProxyConfig{ + Path: "/api", + LoadBalance: "round_robin", + Timeout: config.ProxyTimeout{Connect: 5 * time.Second}, + Cache: config.ProxyCacheConfig{ + Enabled: true, + MaxAge: 10 * time.Second, + }, + } + targets := testutil.NewTestTargets("http://localhost:8080") + p, err := proxy.NewProxy(proxyCfg, targets, nil, nil) + require.NoError(t, err) + + s.proxies = []*proxy.Proxy{p} + + purgeHandler := &PurgeHandler{server: s} + + deleted := purgeHandler.purgeByPath("/api/test", "GET") + assert.Equal(t, 1, deleted) +} + +// TestPurgeByPath_无缓存代理 测试无缓存代理时返回0。 +func TestPurgeByPath_无缓存代理(t *testing.T) { + cfg := &config.Config{ + Servers: []config.ServerConfig{{Listen: ":0"}}, + } + s := New(cfg) + + proxyCfg := testutil.NewTestProxyConfig("/api") + targets := testutil.NewTestTargets("http://localhost:8080") + p, err := proxy.NewProxy(proxyCfg, targets, nil, nil) + require.NoError(t, err) + + s.proxies = []*proxy.Proxy{p} + + purgeHandler := &PurgeHandler{server: s} + + deleted := purgeHandler.purgeByPath("/api/test", "GET") + assert.Equal(t, 0, deleted) +} + +// TestPurgeByPath_NilServer 测试 nil server 返回0。 +func TestPurgeByPath_NilServer(t *testing.T) { + purgeHandler := &PurgeHandler{server: nil} + deleted := purgeHandler.purgeByPath("/api/test", "GET") + assert.Equal(t, 0, deleted) +} + +// TestPurgeByPattern_带缓存代理 测试按模式清理带缓存的代理。 +func TestPurgeByPattern_带缓存代理(t *testing.T) { + cfg := &config.Config{ + Servers: []config.ServerConfig{{Listen: ":0"}}, + } + s := New(cfg) + + proxyCfg := &config.ProxyConfig{ + Path: "/api", + LoadBalance: "round_robin", + Timeout: config.ProxyTimeout{Connect: 5 * time.Second}, + Cache: config.ProxyCacheConfig{ + Enabled: true, + MaxAge: 10 * time.Second, + }, + } + targets := testutil.NewTestTargets("http://localhost:8080") + p, err := proxy.NewProxy(proxyCfg, targets, nil, nil) + require.NoError(t, err) + + s.proxies = []*proxy.Proxy{p} + + purgeHandler := &PurgeHandler{server: s} + + deleted := purgeHandler.purgeByPattern("/api/*", "GET") + assert.GreaterOrEqual(t, deleted, 0) +} + +// TestPurgeByPattern_NilServer 测试 nil server 返回0。 +func TestPurgeByPattern_NilServer(t *testing.T) { + purgeHandler := &PurgeHandler{server: nil} + deleted := purgeHandler.purgeByPattern("/api/*", "GET") + assert.Equal(t, 0, deleted) +} + +// TestPurgeByPattern_多代理 测试多代理的模式清理。 +func TestPurgeByPattern_多代理(t *testing.T) { + cfg := &config.Config{ + Servers: []config.ServerConfig{{Listen: ":0"}}, + } + s := New(cfg) + + targets := testutil.NewTestTargets("http://localhost:8080") + + proxyCfg1 := &config.ProxyConfig{ + Path: "/api", + LoadBalance: "round_robin", + Timeout: config.ProxyTimeout{Connect: 5 * time.Second}, + Cache: config.ProxyCacheConfig{Enabled: true, MaxAge: 10 * time.Second}, + } + p1, err := proxy.NewProxy(proxyCfg1, targets, nil, nil) + require.NoError(t, err) + + proxyCfg2 := &config.ProxyConfig{ + Path: "/data", + LoadBalance: "round_robin", + Timeout: config.ProxyTimeout{Connect: 5 * time.Second}, + Cache: config.ProxyCacheConfig{Enabled: true, MaxAge: 20 * time.Second}, + } + p2, err := proxy.NewProxy(proxyCfg2, targets, nil, nil) + require.NoError(t, err) + + s.proxies = []*proxy.Proxy{p1, p2} + + purgeHandler := &PurgeHandler{server: s} + deleted := purgeHandler.purgeByPattern("*", "GET") + assert.GreaterOrEqual(t, deleted, 0) +} + +// TestShutdownServers_NilCtx使用默认背景 测试 nil context 使用默认背景。 +func TestShutdownServers_NilCtx使用默认背景(t *testing.T) { + err := shutdownServers(nil, nil) + assert.NoError(t, err) +} + +// TestShutdownServers_Ctx取消 测试 context 取消。 +func TestShutdownServers_Ctx取消(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + cancel() + + servers := []*fasthttp.Server{ + {Handler: func(ctx *fasthttp.RequestCtx) {}}, + } + + err := shutdownServers(ctx, servers) + assert.Error(t, err) + assert.True(t, errors.Is(err, context.Canceled)) +} + +// TestSetPidFile 测试设置 PID 文件路径。 +func TestSetPidFile(t *testing.T) { + mgr := NewUpgradeManager(nil) + assert.Equal(t, "", mgr.pidFile) + + mgr.SetPidFile("/var/run/lolly.pid") + assert.Equal(t, "/var/run/lolly.pid", mgr.pidFile) +} + +// TestWritePid_写入文件 测试 PID 写入文件。 +func TestWritePid_写入文件(t *testing.T) { + tempDir := t.TempDir() + pidFile := filepath.Join(tempDir, "lolly.pid") + + mgr := NewUpgradeManager(nil) + mgr.SetPidFile(pidFile) + + err := mgr.WritePid() + require.NoError(t, err) + + data, err := os.ReadFile(pidFile) + require.NoError(t, err) + + expectedPid := fmt.Sprintf("%d", os.Getpid()) + assert.Equal(t, expectedPid, string(data)) +} + +// TestWritePid_覆盖写入 测试多次写入覆盖。 +func TestWritePid_覆盖写入(t *testing.T) { + tempDir := t.TempDir() + pidFile := filepath.Join(tempDir, "lolly.pid") + + mgr := NewUpgradeManager(nil) + mgr.SetPidFile(pidFile) + + err := mgr.WritePid() + require.NoError(t, err) + + err = mgr.WritePid() + require.NoError(t, err) + + data, err := os.ReadFile(pidFile) + require.NoError(t, err) + assert.Equal(t, fmt.Sprintf("%d", os.Getpid()), string(data)) +} + +// TestServeJSON_正常 测试正常 JSON 输出。 +func TestServeJSON_正常(t *testing.T) { + srv := New(nil) + srv.startTime = time.Now() + + h := &StatusHandler{ + server: srv, + path: "/_status", + format: "json", + } + + status := &Status{ + Version: "test", + Uptime: 5 * time.Second, + Connections: 10, + Requests: 100, + BytesSent: 2048, + BytesReceived: 1024, + } + + ctx := &fasthttp.RequestCtx{} + h.serveJSON(ctx, status) + + assert.Equal(t, 200, ctx.Response.StatusCode()) + assert.Contains(t, string(ctx.Response.Header.ContentType()), "application/json") + + var parsed Status + err := json.Unmarshal(ctx.Response.Body(), &parsed) + require.NoError(t, err) + assert.Equal(t, int64(10), parsed.Connections) + assert.Equal(t, int64(100), parsed.Requests) +} + +// TestServeJSON_无数据 测试最简 JSON 输出。 +func TestServeJSON_无数据(t *testing.T) { + srv := New(nil) + srv.startTime = time.Now() + + h := &StatusHandler{ + server: srv, + path: "/_status", + format: "json", + } + + status := &Status{ + Version: "test", + } + + ctx := &fasthttp.RequestCtx{} + h.serveJSON(ctx, status) + + assert.Equal(t, 200, ctx.Response.StatusCode()) + + var parsed Status + err := json.Unmarshal(ctx.Response.Body(), &parsed) + require.NoError(t, err) + assert.Equal(t, "test", parsed.Version) + assert.Nil(t, parsed.Cache) + assert.Nil(t, parsed.Pool) +} + +// TestMatchInheritedListener_UnixSocket 测试 Unix socket 继承匹配。 +func TestMatchInheritedListener_UnixSocket(t *testing.T) { + s := &Server{} + + dir := t.TempDir() + socketPath := filepath.Join(dir, "test.sock") + ln, err := net.Listen("unix", socketPath) + require.NoError(t, err) + defer ln.Close() + + inherited := []net.Listener{ln} + + result := s.matchInheritedListener(inherited, "unix:"+socketPath) + assert.Equal(t, ln, result) +} + +// TestMatchInheritedListener_UnixSocket不匹配 测试 Unix socket 地址不匹配。 +func TestMatchInheritedListener_UnixSocket不匹配(t *testing.T) { + s := &Server{} + + dir := t.TempDir() + socketPath := filepath.Join(dir, "test.sock") + ln, err := net.Listen("unix", socketPath) + require.NoError(t, err) + defer ln.Close() + + inherited := []net.Listener{ln} + + result := s.matchInheritedListener(inherited, "unix:"+dir+"/other.sock") + assert.Nil(t, result) +} + +// TestMatchInheritedListener_NilListener 测试列表中含 nil。 +func TestMatchInheritedListener_NilListener(t *testing.T) { + s := &Server{} + + ln, err := net.Listen("tcp", "127.0.0.1:0") + require.NoError(t, err) + defer ln.Close() + + inherited := []net.Listener{nil, ln} + + addr := ln.Addr().String() + result := s.matchInheritedListener(inherited, addr) + assert.Equal(t, ln, result) +} + +// TestMatchInheritedListener_TCP网络不匹配 测试 TCP 非网络类型跳过。 +func TestMatchInheritedListener_TCP网络不匹配(t *testing.T) { + s := &Server{} + + dir := t.TempDir() + socketPath := filepath.Join(dir, "test.sock") + unixLn, err := net.Listen("unix", socketPath) + require.NoError(t, err) + defer unixLn.Close() + + inherited := []net.Listener{unixLn} + + result := s.matchInheritedListener(inherited, "127.0.0.1:8080") + assert.Nil(t, result) +} + +// TestMatchInheritedListener_端口不匹配 测试端口不同时不匹配。 +func TestMatchInheritedListener_端口不匹配(t *testing.T) { + s := &Server{} + + ln, err := net.Listen("tcp", "127.0.0.1:0") + require.NoError(t, err) + defer ln.Close() + + inherited := []net.Listener{ln} + + result := s.matchInheritedListener(inherited, "127.0.0.1:99999") + assert.Nil(t, result) +} + +// TestMatchInheritedListener_通配符匹配 测试 0.0.0.0 匹配任意地址。 +func TestMatchInheritedListener_通配符匹配(t *testing.T) { + s := &Server{} + + ln, err := net.Listen("tcp", "127.0.0.1:0") + require.NoError(t, err) + defer ln.Close() + + inherited := []net.Listener{ln} + addr := ln.Addr().String() + + result := s.matchInheritedListener(inherited, "0.0.0.0"+addr[len("127.0.0.1"):]) + assert.Equal(t, ln, result) +} + +// TestIsAnyAddr 测试 isAnyAddr 辅助函数。 +func TestIsAnyAddr(t *testing.T) { + tests := []struct { + host string + want bool + }{ + {"0.0.0.0", true}, + {"::", true}, + {"", true}, + {"127.0.0.1", false}, + {"192.168.1.1", false}, + } + + for _, tt := range tests { + t.Run(tt.host, func(t *testing.T) { + assert.Equal(t, tt.want, isAnyAddr(tt.host)) + }) + } +} + +// TestCreateFastServer 测试创建 fasthttp 服务器。 +func TestCreateFastServer(t *testing.T) { + s := &Server{} + serverCfg := &config.ServerConfig{ + ReadTimeout: 10 * time.Second, + WriteTimeout: 20 * time.Second, + IdleTimeout: 30 * time.Second, + MaxConnsPerIP: 100, + MaxRequestsPerConn: 1000, + Concurrency: 500, + ReadBufferSize: 8192, + WriteBufferSize: 8192, + ServerTokens: true, + } + + handler := func(ctx *fasthttp.RequestCtx) {} + fastSrv := s.createFastServer(serverCfg, handler) + + assert.NotNil(t, fastSrv) + assert.Equal(t, 10*time.Second, fastSrv.ReadTimeout) + assert.Equal(t, 20*time.Second, fastSrv.WriteTimeout) + assert.Equal(t, 30*time.Second, fastSrv.IdleTimeout) + assert.Equal(t, 100, fastSrv.MaxConnsPerIP) + assert.Equal(t, 1000, fastSrv.MaxRequestsPerConn) + assert.True(t, fastSrv.CloseOnShutdown) + assert.Equal(t, 500, fastSrv.Concurrency) + assert.Equal(t, 8192, fastSrv.ReadBufferSize) + assert.Equal(t, 8192, fastSrv.WriteBufferSize) +} + +// TestCreateFastServer_隐藏版本 测试 ServerTokens=false 时隐藏版本。 +func TestCreateFastServer_隐藏版本(t *testing.T) { + s := &Server{} + serverCfg := &config.ServerConfig{ + ServerTokens: false, + } + + fastSrv := s.createFastServer(serverCfg, nil) + assert.Equal(t, "lolly", fastSrv.Name) +} + +// TestRegisterRoute_各种类型 测试各种位置类型的路由注册。 +func TestRegisterRoute_各种类型(t *testing.T) { + s := &Server{ + locationEngine: matcher.NewLocationEngine(), + } + + tests := []struct { + name string + locType string + path string + }{ + {"exact", matcher.LocationTypeExact, "/api/users"}, + {"prefix", matcher.LocationTypePrefix, "/api/"}, + {"prefix_priority", matcher.LocationTypePrefixPriority, "/api/v2/"}, + {"regex", matcher.LocationTypeRegex, "^/api/.*$"}, + {"regex_caseless", matcher.LocationTypeRegexCaseless, "^/API/.*$"}, + {"named", matcher.LocationTypeNamed, "@internal"}, + {"default", "", "/fallback/"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + le := matcher.NewLocationEngine() + s.locationEngine = le + + h := func(ctx *fasthttp.RequestCtx) {} + err := s.registerRoute(tt.locType, tt.path, h, false, "test") + assert.NoError(t, err) + }) + } +} + +// TestRegisterProxyRoutesWithLocationEngine 测试代理路由注册到 LocationEngine。 +func TestRegisterProxyRoutesWithLocationEngine(t *testing.T) { + cfg := &config.Config{ + Servers: []config.ServerConfig{ + { + Listen: ":0", + Proxy: []config.ProxyConfig{ + { + Path: "/api", + LoadBalance: "round_robin", + Targets: []config.ProxyTarget{{URL: "http://localhost:8080"}}, + }, + }, + }, + }, + } + + s := New(cfg) + s.locationEngine = matcher.NewLocationEngine() + + err := s.registerProxyRoutesWithLocationEngine(&cfg.Servers[0]) + assert.NoError(t, err) +} + +// TestRegisterProxyRoutesWithLocationEngine_命名路由 测试命名路由注册。 +func TestRegisterProxyRoutesWithLocationEngine_命名路由(t *testing.T) { + cfg := &config.Config{ + Servers: []config.ServerConfig{ + { + Listen: ":0", + Proxy: []config.ProxyConfig{ + { + Path: "/api", + LoadBalance: "round_robin", + LocationType: matcher.LocationTypeNamed, + LocationName: "backend", + Targets: []config.ProxyTarget{{URL: "http://localhost:8080"}}, + }, + }, + }, + }, + } + + s := New(cfg) + s.locationEngine = matcher.NewLocationEngine() + + err := s.registerProxyRoutesWithLocationEngine(&cfg.Servers[0]) + assert.NoError(t, err) +} + +// TestRegisterStaticHandlersWithLocationEngine 测试静态文件路由注册。 +func TestRegisterStaticHandlersWithLocationEngine(t *testing.T) { + tempDir := t.TempDir() + + cfg := &config.Config{ + Servers: []config.ServerConfig{ + { + Listen: ":0", + Static: []config.StaticConfig{ + { + Path: "/static", + Root: tempDir, + Index: []string{"index.html"}, + }, + }, + }, + }, + } + + s := New(cfg) + s.locationEngine = matcher.NewLocationEngine() + + err := s.registerStaticHandlersWithLocationEngine(&cfg.Servers[0]) + assert.NoError(t, err) +} + +// TestRegisterLuaRoutesWithLocationEngine_无引擎 测试无 Lua 引擎时跳过。 +func TestRegisterLuaRoutesWithLocationEngine_无引擎(t *testing.T) { + s := &Server{luaEngine: nil} + serverCfg := &config.ServerConfig{ + Lua: &config.LuaMiddlewareConfig{Enabled: true}, + } + + err := s.registerLuaRoutesWithLocationEngine(serverCfg) + assert.NoError(t, err) +} + +// TestRegisterLuaRoutesWithLocationEngine_未启用 测试 Lua 未启用时跳过。 +func TestRegisterLuaRoutesWithLocationEngine_未启用(t *testing.T) { + s := &Server{} + serverCfg := &config.ServerConfig{ + Lua: &config.LuaMiddlewareConfig{Enabled: false}, + } + + err := s.registerLuaRoutesWithLocationEngine(serverCfg) + assert.NoError(t, err) +} + +// TestRegisterLuaRoutesWithLocationEngine_无路由 测试 Lua 脚本无路由时跳过。 +func TestRegisterLuaRoutesWithLocationEngine_无路由(t *testing.T) { + s := &Server{} + serverCfg := &config.ServerConfig{ + Lua: &config.LuaMiddlewareConfig{ + Enabled: false, + Scripts: []config.LuaScriptConfig{ + {Path: "/tmp/test.lua"}, + }, + }, + } + + err := s.registerLuaRoutesWithLocationEngine(serverCfg) + assert.NoError(t, err) +} + +// TestHandleRegistrationError_非冲突错误 测试非 ConflictError 返回错误。 +func TestHandleRegistrationError_非冲突错误(t *testing.T) { + s := &Server{} + + originalErr := fmt.Errorf("some registration error") + err := s.handleRegistrationError("test", "/path", originalErr) + + assert.Error(t, err) + assert.Contains(t, err.Error(), "test route /path") +} + +// TestWrapHandler_带连接池 测试 wrapHandler 使用连接池。 +func TestWrapHandler_带连接池(t *testing.T) { + cfg := &config.Config{ + Servers: []config.ServerConfig{{Listen: ":0"}}, + } + s := New(cfg) + + s.pool = NewGoroutinePool(PoolConfig{ + MaxWorkers: 10, + MinWorkers: 2, + QueueSize: 10, + IdleTimeout: 5 * time.Second, + }) + s.pool.Start() + defer s.pool.Stop() + + base := func(ctx *fasthttp.RequestCtx) { + ctx.SetBodyString("ok") + } + + wrapped, err := s.wrapHandler(base, &cfg.Servers[0]) + require.NoError(t, err) + assert.NotNil(t, wrapped) +} + +// TestStopWithTimeout_默认超时 测试零超时使用默认值。 +func TestStopWithTimeout_默认超时(t *testing.T) { + cfg := &config.Config{ + Servers: []config.ServerConfig{{Listen: ":0"}}, + } + s := New(cfg) + + err := s.StopWithTimeout(0) + assert.NoError(t, err) +} + +// TestStopWithTimeout_负超时 测试负超时使用默认值。 +func TestStopWithTimeout_负超时(t *testing.T) { + cfg := &config.Config{ + Servers: []config.ServerConfig{{Listen: ":0"}}, + } + s := New(cfg) + + err := s.StopWithTimeout(-1 * time.Second) + assert.NoError(t, err) +} + +// TestCleanupResources_全nil 测试所有组件为 nil 时不 panic。 +func TestCleanupResources_全nil(t *testing.T) { + cfg := &config.Config{ + Servers: []config.ServerConfig{{Listen: ":0"}}, + } + s := New(cfg) + + assert.NotPanics(t, func() { + s.cleanupResources() + }) +} + +// TestDupListener_关闭测试 测试复制后原 listener 仍可用。 +func TestDupListener_关闭测试(t *testing.T) { + ln, err := net.Listen("tcp", "127.0.0.1:0") + require.NoError(t, err) + defer ln.Close() + + duped, err := DupListener(ln) + require.NoError(t, err) + defer duped.Close() + + assert.Equal(t, ln.Addr().String(), duped.Addr().String()) +} + +// TestGetTLSConfig_有TLSManager 测试有 TLS 管理器时返回配置。 +func TestGetTLSConfig_有TLSManager(t *testing.T) { + tempDir := t.TempDir() + certFile := filepath.Join(tempDir, "cert.pem") + keyFile := filepath.Join(tempDir, "key.pem") + + err := generateSelfSignedCert(certFile, keyFile) + if err != nil { + t.Skipf("跳过: 无法生成测试证书: %v", err) + } + + tlsMgr, err := ssl.NewTLSManager(&config.SSLConfig{ + Cert: certFile, + Key: keyFile, + }) + if err != nil { + t.Skipf("跳过: 无法创建 TLS 管理器: %v", err) + } + + s := &Server{tlsManager: tlsMgr} + + tlsConfig, err := s.GetTLSConfig() + assert.NoError(t, err) + assert.NotNil(t, tlsConfig) +} + +// generateSelfSignedCert 生成自签名证书用于测试。 +func generateSelfSignedCert(certFile, keyFile string) error { + key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + return err + } + + template := &x509.Certificate{ + SerialNumber: big.NewInt(1), + Subject: pkix.Name{ + Organization: []string{"Test"}, + }, + NotBefore: time.Now(), + NotAfter: time.Now().Add(1 * time.Hour), + KeyUsage: x509.KeyUsageDigitalSignature, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, + BasicConstraintsValid: true, + IPAddresses: []net.IP{net.ParseIP("127.0.0.1")}, + DNSNames: []string{"localhost"}, + } + + certDER, err := x509.CreateCertificate(rand.Reader, template, template, &key.PublicKey, key) + if err != nil { + return err + } + + certOut, err := os.Create(certFile) + if err != nil { + return err + } + defer certOut.Close() + + if err := pem.Encode(certOut, &pem.Block{Type: "CERTIFICATE", Bytes: certDER}); err != nil { + return err + } + + keyOut, err := os.Create(keyFile) + if err != nil { + return err + } + defer keyOut.Close() + + keyBytes, err := x509.MarshalECPrivateKey(key) + if err != nil { + return err + } + + return pem.Encode(keyOut, &pem.Block{Type: "EC PRIVATE KEY", Bytes: keyBytes}) +} + +// TestCreateListener_InheritedFromUpgradeManager 测试从 UpgradeManager 继承监听器。 +func TestCreateListener_InheritedFromUpgradeManager(t *testing.T) { + cfg := &config.Config{ + Servers: []config.ServerConfig{{Listen: "127.0.0.1:0"}}, + } + s := New(cfg) + + ln, err := net.Listen("tcp", "127.0.0.1:0") + require.NoError(t, err) + defer ln.Close() + + um := NewUpgradeManager(s) + um.SetPidFile("") + s.SetUpgradeManager(um) + + addr := ln.Addr().String() + cfg.Servers[0].Listen = addr + + s.SetListeners([]net.Listener{ln}) + + matched, err := s.createListener(&cfg.Servers[0]) + require.NoError(t, err) + assert.Equal(t, addr, matched.Addr().String()) +} + +// TestShutdownServers_成功关闭 测试正常关闭服务器。 +func TestShutdownServers_成功关闭(t *testing.T) { + srv1 := &fasthttp.Server{Handler: func(ctx *fasthttp.RequestCtx) {}} + srv2 := &fasthttp.Server{Handler: func(ctx *fasthttp.RequestCtx) {}} + + ln1, err := net.Listen("tcp", "127.0.0.1:0") + require.NoError(t, err) + ln2, err := net.Listen("tcp", "127.0.0.1:0") + require.NoError(t, err) + + go srv1.Serve(ln1) + go srv2.Serve(ln2) + time.Sleep(50 * time.Millisecond) + + err = shutdownServers(context.Background(), []*fasthttp.Server{srv1, srv2}) + assert.NoError(t, err) +} + +// TestShutdownServers_Nil服务器跳过 测试列表中 nil 服务器被跳过。 +func TestShutdownServers_Nil服务器跳过(t *testing.T) { + srv := &fasthttp.Server{Handler: func(ctx *fasthttp.RequestCtx) {}} + + ln, err := net.Listen("tcp", "127.0.0.1:0") + require.NoError(t, err) + + go srv.Serve(ln) + time.Sleep(50 * time.Millisecond) + + err = shutdownServers(context.Background(), []*fasthttp.Server{nil, srv, nil}) + assert.NoError(t, err) +} + +// TestRegisterLuaRoutesWithLocationEngine_有路由 测试带路由的 Lua 脚本注册。 +func TestRegisterLuaRoutesWithLocationEngine_有路由(t *testing.T) { + tempDir := t.TempDir() + scriptPath := filepath.Join(tempDir, "test.lua") + err := os.WriteFile(scriptPath, []byte("ngx.say('hello')"), 0o644) + require.NoError(t, err) + + luaEngine, err := lua.NewEngine(lua.DefaultConfig()) + require.NoError(t, err) + defer luaEngine.Close() + + s := &Server{ + luaEngine: luaEngine, + locationEngine: matcher.NewLocationEngine(), + } + + serverCfg := &config.ServerConfig{ + Lua: &config.LuaMiddlewareConfig{ + Enabled: true, + Scripts: []config.LuaScriptConfig{ + { + Path: scriptPath, + Route: "/api/lua", + }, + }, + }, + } + + err = s.registerLuaRoutesWithLocationEngine(serverCfg) + assert.NoError(t, err) +} + +// TestRegisterLuaRoutesWithLocationEngine_自定义路由类型 测试自定义 RouteType。 +func TestRegisterLuaRoutesWithLocationEngine_自定义路由类型(t *testing.T) { + tempDir := t.TempDir() + scriptPath := filepath.Join(tempDir, "exact.lua") + err := os.WriteFile(scriptPath, []byte("ngx.say('exact')"), 0o644) + require.NoError(t, err) + + luaEngine, err := lua.NewEngine(lua.DefaultConfig()) + require.NoError(t, err) + defer luaEngine.Close() + + s := &Server{ + luaEngine: luaEngine, + locationEngine: matcher.NewLocationEngine(), + } + + serverCfg := &config.ServerConfig{ + Lua: &config.LuaMiddlewareConfig{ + Enabled: true, + Scripts: []config.LuaScriptConfig{ + { + Path: scriptPath, + Route: "/api/exact", + RouteType: matcher.LocationTypeExact, + }, + }, + }, + } + + err = s.registerLuaRoutesWithLocationEngine(serverCfg) + assert.NoError(t, err) +} + +// TestRegisterLuaRoutesWithLocationEngine_自定义超时 测试自定义脚本超时。 +func TestRegisterLuaRoutesWithLocationEngine_自定义超时(t *testing.T) { + tempDir := t.TempDir() + scriptPath := filepath.Join(tempDir, "timeout.lua") + err := os.WriteFile(scriptPath, []byte("ngx.say('timeout')"), 0o644) + require.NoError(t, err) + + luaEngine, err := lua.NewEngine(lua.DefaultConfig()) + require.NoError(t, err) + defer luaEngine.Close() + + s := &Server{ + luaEngine: luaEngine, + locationEngine: matcher.NewLocationEngine(), + } + + serverCfg := &config.ServerConfig{ + Lua: &config.LuaMiddlewareConfig{ + Enabled: true, + Scripts: []config.LuaScriptConfig{ + { + Path: scriptPath, + Route: "/api/timeout", + Timeout: 10 * time.Second, + }, + }, + }, + } + + err = s.registerLuaRoutesWithLocationEngine(serverCfg) + assert.NoError(t, err) +} + +// TestServeHTTP_带缓存的Prometheus 测试 Prometheus 格式带缓存指标。 +func TestServeHTTP_带缓存的Prometheus(t *testing.T) { + cfg := &config.StatusConfig{ + Path: "/_status", + Format: "prometheus", + Allow: []string{}, + } + + srv := New(nil) + srv.startTime = time.Now() + srv.connections.Store(5) + + fc := cache.NewFileCache(100, 1024*1024, 5*time.Minute) + srv.fileCache = fc + + h, err := NewStatusHandler(srv, cfg) + require.NoError(t, err) + + ctx := &fasthttp.RequestCtx{} + ctx.Request.SetRequestURI("/_status") + h.ServeHTTP(ctx) + + assert.Equal(t, 200, ctx.Response.StatusCode()) + + body := string(ctx.Response.Body()) + assert.Contains(t, body, "lolly_version") + assert.Contains(t, body, "lolly_cache_entries") +}