新建 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 测试覆盖。
1123 lines
29 KiB
Go
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")
|
|
}
|