lolly/internal/server/coverage_test.go
xfy 9ae7a2b8ef test(server): 添加服务器模块覆盖测试(覆盖率 78.6% → 83.3%)
新建 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 测试覆盖。
2026-06-04 08:33:39 +08:00

1123 lines
29 KiB
Go

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("<html>Not Found</html>"), 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")
}