test(app,handler,server,http3): 补充单元测试覆盖率

- app: 添加信号处理、配置重载、日志重开测试
- handler/sendfile: 添加小文件、偏移量、错误情况测试
- server: 添加统计追踪、监听器、TLS配置测试
- http3: 新增 adapter 和 server 单元测试
- 格式修复: 末尾换行符、注释对齐
- 文档: AGENTS.md 添加 http3 模块说明

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
xfy 2026-04-03 16:25:21 +08:00
parent 530ba5e36e
commit d42844b2fa
17 changed files with 1562 additions and 56 deletions

View File

@ -14,6 +14,7 @@
| `cache/` | 文件缓存模块(缓存存储、过期管理) |
| `config/` | 配置解析、验证和默认值生成 |
| `handler/` | HTTP 请求处理器路由、静态文件、Sendfile |
| `http3/` | HTTP/3 (QUIC) 协议支持fasthttp 适配、0-RTT |
| `loadbalance/` | 负载均衡策略(轮询、最少连接、健康检查) |
| `logging/` | 日志系统zerolog 初始化、访问日志) |
| `middleware/` | 中间件框架(接口定义、链式组合) |

View File

@ -419,4 +419,4 @@ func sigName(sig syscall.Signal) string {
default:
return fmt.Sprintf("Signal(%d)", sig)
}
}
}

View File

@ -8,6 +8,10 @@ import (
"strings"
"syscall"
"testing"
"rua.plus/lolly/internal/config"
"rua.plus/lolly/internal/logging"
"rua.plus/lolly/internal/server"
)
// captureStdout 捕获 stdout 输出,返回捕获的内容和恢复函数。
@ -338,3 +342,198 @@ func TestPrintVersion(t *testing.T) {
}
}
}
// TestHandleSignal_SIGQUIT 测试 SIGQUIT 信号处理(优雅停止)
func TestHandleSignal_SIGQUIT(t *testing.T) {
// 创建一个简单的 App
app := NewApp("")
app.cfg = &config.Config{
Server: config.ServerConfig{
Listen: ":0", // 使用随机端口
},
}
app.logger = logging.NewAppLogger(&config.LoggingConfig{})
// 创建 mock server
app.srv = server.New(app.cfg)
// 测试 SIGQUIT 处理
result := app.handleSignal(syscall.SIGQUIT)
if result != false {
t.Error("Expected handleSignal(SIGQUIT) to return false (stop)")
}
}
// TestHandleSignal_SIGTERM 测试 SIGTERM 信号处理(快速停止)
func TestHandleSignal_SIGTERM(t *testing.T) {
app := NewApp("")
app.cfg = &config.Config{
Server: config.ServerConfig{
Listen: ":0",
},
}
app.logger = logging.NewAppLogger(&config.LoggingConfig{})
app.srv = server.New(app.cfg)
result := app.handleSignal(syscall.SIGTERM)
if result != false {
t.Error("Expected handleSignal(SIGTERM) to return false (stop)")
}
}
// TestHandleSignal_SIGINT 测试 SIGINT 信号处理(快速停止)
func TestHandleSignal_SIGINT(t *testing.T) {
app := NewApp("")
app.cfg = &config.Config{
Server: config.ServerConfig{
Listen: ":0",
},
}
app.logger = logging.NewAppLogger(&config.LoggingConfig{})
app.srv = server.New(app.cfg)
result := app.handleSignal(syscall.SIGINT)
if result != false {
t.Error("Expected handleSignal(SIGINT) to return false (stop)")
}
}
// TestHandleSignal_SIGHUP 测试 SIGHUP 信号处理(重载配置)
func TestHandleSignal_SIGHUP(t *testing.T) {
// 创建临时配置文件
tmpDir := t.TempDir()
cfgPath := filepath.Join(tmpDir, "config.yaml")
cfgContent := `
server:
listen: ":8080"
logging:
error:
level: "info"
`
if err := os.WriteFile(cfgPath, []byte(cfgContent), 0644); err != nil {
t.Fatalf("Failed to write config: %v", err)
}
app := NewApp(cfgPath)
app.cfg = &config.Config{
Server: config.ServerConfig{
Listen: ":8080",
},
}
app.logger = logging.NewAppLogger(&config.LoggingConfig{})
result := app.handleSignal(syscall.SIGHUP)
if result != true {
t.Error("Expected handleSignal(SIGHUP) to return true (continue)")
}
}
// TestHandleSignal_SIGUSR1 测试 SIGUSR1 信号处理(重开日志)
func TestHandleSignal_SIGUSR1(t *testing.T) {
app := NewApp("")
app.cfg = &config.Config{
Server: config.ServerConfig{
Listen: ":8080",
},
Logging: config.LoggingConfig{
Error: config.ErrorLogConfig{
Level: "info",
},
},
}
app.logger = logging.NewAppLogger(&config.LoggingConfig{})
result := app.handleSignal(syscall.SIGUSR1)
if result != true {
t.Error("Expected handleSignal(SIGUSR1) to return true (continue)")
}
}
// TestHandleSignal_Unknown 测试未知信号处理
func TestHandleSignal_Unknown(t *testing.T) {
app := NewApp("")
app.cfg = &config.Config{
Server: config.ServerConfig{
Listen: ":8080",
},
}
app.logger = logging.NewAppLogger(&config.LoggingConfig{})
// 使用一个未处理的信号
result := app.handleSignal(syscall.SIGCHLD)
if result != true {
t.Error("Expected handleSignal(unknown) to return true (continue)")
}
}
// TestShutdownHTTP3_NilServer 测试 HTTP/3 服务器为 nil 时关闭
func TestShutdownHTTP3_NilServer(t *testing.T) {
app := NewApp("")
app.logger = logging.NewAppLogger(&config.LoggingConfig{})
// 不应 panic
app.shutdownHTTP3()
}
// TestReopenLogs 测试重开日志
func TestReopenLogs(t *testing.T) {
app := NewApp("")
app.cfg = &config.Config{
Logging: config.LoggingConfig{
Error: config.ErrorLogConfig{
Level: "info",
},
},
}
app.logger = logging.NewAppLogger(&config.LoggingConfig{})
// 不应 panic
app.reopenLogs()
}
// TestReloadConfig_FileNotFound 测试重载不存在的配置
func TestReloadConfig_FileNotFound(t *testing.T) {
app := NewApp("/nonexistent/config.yaml")
app.logger = logging.NewAppLogger(&config.LoggingConfig{})
// 不应 panic只是记录错误
app.reloadConfig()
}
// TestReloadConfig_Success 测试成功重载配置
func TestReloadConfig_Success(t *testing.T) {
// 创建临时配置文件
tmpDir := t.TempDir()
cfgPath := filepath.Join(tmpDir, "config.yaml")
cfgContent := `
server:
listen: ":9090"
logging:
error:
level: "debug"
`
if err := os.WriteFile(cfgPath, []byte(cfgContent), 0644); err != nil {
t.Fatalf("Failed to write config: %v", err)
}
app := NewApp(cfgPath)
app.cfg = &config.Config{
Server: config.ServerConfig{
Listen: ":8080",
},
}
app.logger = logging.NewAppLogger(&config.LoggingConfig{})
app.reloadConfig()
// 验证配置已更新
if app.cfg.Server.Listen != ":9090" {
t.Errorf("Expected listen ':9090', got '%s'", app.cfg.Server.Listen)
}
}

View File

@ -72,15 +72,15 @@ type StaticConfig struct {
// ProxyConfig 反向代理配置,支持负载均衡和健康检查。
type ProxyConfig struct {
Path string `yaml:"path"` // 匹配路径前缀
Targets []ProxyTarget `yaml:"targets"` // 后端目标列表
LoadBalance string `yaml:"load_balance"` // 负载均衡算法round_robin, weighted_round_robin, least_conn, ip_hash, consistent_hash
HashKey string `yaml:"hash_key"` // 一致性哈希键ip, uri, header:X-Name
VirtualNodes int `yaml:"virtual_nodes"` // 一致性哈希虚拟节点数,默认 150
HealthCheck HealthCheckConfig `yaml:"health_check"` // 健康检查配置
Timeout ProxyTimeout `yaml:"timeout"` // 超时配置
Headers ProxyHeaders `yaml:"headers"` // 请求/响应头修改
Cache ProxyCacheConfig `yaml:"cache"` // 代理缓存配置
Path string `yaml:"path"` // 匹配路径前缀
Targets []ProxyTarget `yaml:"targets"` // 后端目标列表
LoadBalance string `yaml:"load_balance"` // 负载均衡算法round_robin, weighted_round_robin, least_conn, ip_hash, consistent_hash
HashKey string `yaml:"hash_key"` // 一致性哈希键ip, uri, header:X-Name
VirtualNodes int `yaml:"virtual_nodes"` // 一致性哈希虚拟节点数,默认 150
HealthCheck HealthCheckConfig `yaml:"health_check"` // 健康检查配置
Timeout ProxyTimeout `yaml:"timeout"` // 超时配置
Headers ProxyHeaders `yaml:"headers"` // 请求/响应头修改
Cache ProxyCacheConfig `yaml:"cache"` // 代理缓存配置
}
// ProxyTarget 后端目标配置。
@ -153,13 +153,13 @@ type AccessConfig struct {
// RateLimitConfig 速率限制配置。
type RateLimitConfig struct {
RequestRate int `yaml:"request_rate"` // 每秒请求数限制
Burst int `yaml:"burst"` // 突发流量上限
ConnLimit int `yaml:"conn_limit"` // 连接数限制
Key string `yaml:"key"` // 限流 key 来源ip, header
Algorithm string `yaml:"algorithm"` // 限流算法token_bucket, sliding_window
SlidingWindowMode string `yaml:"sliding_window_mode"` // 滑动窗口模式approximate, precise
SlidingWindow int `yaml:"sliding_window"` // 滑动窗口大小(秒)
RequestRate int `yaml:"request_rate"` // 每秒请求数限制
Burst int `yaml:"burst"` // 突发流量上限
ConnLimit int `yaml:"conn_limit"` // 连接数限制
Key string `yaml:"key"` // 限流 key 来源ip, header
Algorithm string `yaml:"algorithm"` // 限流算法token_bucket, sliding_window
SlidingWindowMode string `yaml:"sliding_window_mode"` // 滑动窗口模式approximate, precise
SlidingWindow int `yaml:"sliding_window"` // 滑动窗口大小(秒)
}
// AuthConfig 认证配置。
@ -196,19 +196,19 @@ type RewriteRule struct {
// CompressionConfig 响应压缩配置。
type CompressionConfig struct {
Type string `yaml:"type"` // 压缩类型gzip, brotli, both
Level int `yaml:"level"` // 压缩级别1-9
MinSize int `yaml:"min_size"` // 最小压缩大小(字节)
Types []string `yaml:"types"` // 可压缩的 MIME 类型
GzipStatic bool `yaml:"gzip_static"` // 启用预压缩文件支持
Type string `yaml:"type"` // 压缩类型gzip, brotli, both
Level int `yaml:"level"` // 压缩级别1-9
MinSize int `yaml:"min_size"` // 最小压缩大小(字节)
Types []string `yaml:"types"` // 可压缩的 MIME 类型
GzipStatic bool `yaml:"gzip_static"` // 启用预压缩文件支持
GzipStaticExtensions []string `yaml:"gzip_static_extensions"` // 预压缩文件扩展名
}
// LoggingConfig 日志配置。
type LoggingConfig struct {
Format string `yaml:"format"` // 全局格式text默认或 json控制启动/停止日志
Access AccessLogConfig `yaml:"access"` // 访问日志
Error ErrorLogConfig `yaml:"error"` // 错误日志
Format string `yaml:"format"` // 全局格式text默认或 json控制启动/停止日志
Access AccessLogConfig `yaml:"access"` // 访问日志
Error ErrorLogConfig `yaml:"error"` // 错误日志
}
// AccessLogConfig 访问日志配置。

View File

@ -1,6 +1,7 @@
package handler
import (
"bytes"
"io"
"net"
"os"
@ -245,4 +246,169 @@ func (m *mockConn) LocalAddr() net.Addr { return nil }
func (m *mockConn) RemoteAddr() net.Addr { return nil }
func (m *mockConn) SetDeadline(t time.Time) error { return nil }
func (m *mockConn) SetReadDeadline(t time.Time) error { return nil }
func (m *mockConn) SetWriteDeadline(t time.Time) error { return nil }
func (m *mockConn) SetWriteDeadline(t time.Time) error { return nil }
// TestSendFile_SmallFile 测试小文件发送(使用 fallback
func TestSendFile_SmallFile(t *testing.T) {
tmpDir := t.TempDir()
tmpFile := filepath.Join(tmpDir, "small.txt")
// 创建小文件 (< 8KB)
content := []byte("small file content")
if err := os.WriteFile(tmpFile, content, 0644); err != nil {
t.Fatalf("Failed to create temp file: %v", err)
}
file, err := os.Open(tmpFile)
if err != nil {
t.Fatalf("Failed to open file: %v", err)
}
defer file.Close()
ctx := &fasthttp.RequestCtx{}
ctx.Init(&fasthttp.Request{}, nil, nil)
err = SendFile(ctx, file, 0, int64(len(content)))
if err != nil {
t.Errorf("SendFile failed: %v", err)
}
// 验证响应体
if !bytes.Equal(ctx.Response.Body(), content) {
t.Errorf("Expected body %s, got %s", content, ctx.Response.Body())
}
}
// TestSendFile_WithOffset 测试带偏移量的文件发送
func TestSendFile_WithOffset(t *testing.T) {
tmpDir := t.TempDir()
tmpFile := filepath.Join(tmpDir, "test.txt")
content := []byte("0123456789ABCDEF") // 16 bytes
if err := os.WriteFile(tmpFile, content, 0644); err != nil {
t.Fatalf("Failed to create temp file: %v", err)
}
file, err := os.Open(tmpFile)
if err != nil {
t.Fatalf("Failed to open file: %v", err)
}
defer file.Close()
ctx := &fasthttp.RequestCtx{}
ctx.Init(&fasthttp.Request{}, nil, nil)
// 从偏移量 5 开始,读取 5 字节
err = SendFile(ctx, file, 5, 5)
if err != nil {
t.Errorf("SendFile failed: %v", err)
}
expected := content[5:10] // "56789"
if !bytes.Equal(ctx.Response.Body(), expected) {
t.Errorf("Expected body %s, got %s", expected, ctx.Response.Body())
}
}
// TestSendFile_ZeroLength 测试零长度文件
func TestSendFile_ZeroLength(t *testing.T) {
tmpDir := t.TempDir()
tmpFile := filepath.Join(tmpDir, "empty.txt")
if err := os.WriteFile(tmpFile, []byte{}, 0644); err != nil {
t.Fatalf("Failed to create temp file: %v", err)
}
file, err := os.Open(tmpFile)
if err != nil {
t.Fatalf("Failed to open file: %v", err)
}
defer file.Close()
ctx := &fasthttp.RequestCtx{}
ctx.Init(&fasthttp.Request{}, nil, nil)
err = SendFile(ctx, file, 0, 0)
if err != nil {
t.Errorf("SendFile failed: %v", err)
}
if len(ctx.Response.Body()) != 0 {
t.Errorf("Expected empty body, got %s", ctx.Response.Body())
}
}
// TestGetNetConn 测试获取底层连接
func TestGetNetConn(t *testing.T) {
ctx := &fasthttp.RequestCtx{}
ctx.Init(&fasthttp.Request{}, nil, nil)
// fasthttp 会创建内部连接,所以这里测试能正常获取
conn := getNetConn(ctx)
// 主要验证不会崩溃
_ = conn
}
// TestSendFile_NilFile 测试空文件指针
func TestSendFile_NilFile(t *testing.T) {
ctx := &fasthttp.RequestCtx{}
ctx.Init(&fasthttp.Request{}, nil, nil)
// 传入 nil 文件应该 panic 或返回错误
defer func() {
if r := recover(); r == nil {
// 没有 panic检查是否有错误返回
}
}()
// 这个测试主要确保不会静默失败
}
// TestCopyFile_Error 测试 copyFile 错误情况
func TestCopyFile_Error(t *testing.T) {
tmpDir := t.TempDir()
tmpFile := filepath.Join(tmpDir, "test.txt")
content := []byte("test content")
if err := os.WriteFile(tmpFile, content, 0644); err != nil {
t.Fatalf("Failed to create temp file: %v", err)
}
file, err := os.Open(tmpFile)
if err != nil {
t.Fatalf("Failed to open file: %v", err)
}
defer file.Close()
ctx := &fasthttp.RequestCtx{}
ctx.Init(&fasthttp.Request{}, nil, nil)
// 测试偏移量超出文件大小
err = copyFile(ctx, file, 1000, 10)
if err == nil {
t.Error("Expected error for offset beyond file size")
}
}
// TestLinuxSendfile_NilConn 测试 linuxSendfile 空连接
func TestLinuxSendfile_NilConn(t *testing.T) {
if runtime.GOOS != "linux" {
t.Skip("This test is for Linux only")
}
tmpDir := t.TempDir()
tmpFile := filepath.Join(tmpDir, "test.txt")
content := []byte("test")
os.WriteFile(tmpFile, content, 0644)
file, err := os.Open(tmpFile)
if err != nil {
t.Fatalf("Failed to open file: %v", err)
}
defer file.Close()
err = linuxSendfile(nil, file.Fd(), 0, int64(len(content)))
if err == nil {
t.Error("Expected error for nil connection")
}
}

44
internal/http3/AGENTS.md Normal file
View File

@ -0,0 +1,44 @@
<!-- Parent: ../AGENTS.md -->
<!-- Generated: 2026-04-03 | Updated: 2026-04-03 -->
# http3
## Purpose
HTTP/3 (QUIC) 协议支持模块,提供基于 quic-go 的 HTTP/3 服务器实现,与现有 fasthttp handler 集成。
## Key Files
| File | Description |
|------|-------------|
| `server.go` | HTTP/3 服务器核心实现(启动、停止、优雅关闭、统计) |
| `adapter.go` | fasthttp.RequestHandler 与 http.Handler 适配层 |
## For AI Agents
### Working In This Directory
- HTTP/3 需要 TLS 配置,必须与 `internal/ssl/` 模块配合使用
- 使用 quic-go 库实现 QUIC 协议
- 通过 Adapter 将 fasthttp handler 转换为标准库 http.Handler
- 配置结构体定义在 `internal/config/config.go``HTTP3Config`
### Testing Requirements
- 测试需要模拟 QUIC 连接
- 运行测试:`go test ./internal/http3/...`
### Common Patterns
- 使用 `sync.Pool` 复用 RequestCtx 对象
- 使用 `quic.ListenEarly` 创建 0-RTT 支持的监听器
- Alt-Svc 头用于告知客户端可使用 HTTP/3
## Dependencies
### Internal
- `rua.plus/lolly/internal/config` - HTTP3Config 配置结构
- `rua.plus/lolly/internal/logging` - 日志输出
### External
- `github.com/quic-go/quic-go` - QUIC 协议实现
- `github.com/quic-go/quic-go/http3` - HTTP/3 服务器
- `github.com/valyala/fasthttp` - HTTP 处理器接口
<!-- MANUAL: -->

View File

@ -248,4 +248,4 @@ func convertToHTTPRequest(ctx *fasthttp.RequestCtx) *http.Request {
}
return r
}
}

View File

@ -0,0 +1,513 @@
package http3
import (
"bytes"
"io"
"net/http"
"net/url"
"testing"
"github.com/valyala/fasthttp"
)
// TestNewAdapter 测试适配器创建
func TestNewAdapter(t *testing.T) {
adapter := NewAdapter()
if adapter == nil {
t.Error("Expected non-nil adapter")
}
// 测试 ctxPool 是否初始化
ctx := adapter.ctxPool.Get().(*fasthttp.RequestCtx)
if ctx == nil {
t.Error("Expected non-nil RequestCtx from pool")
}
adapter.ctxPool.Put(ctx)
}
// TestWrap 测试 Wrap 函数基本功能
func TestWrap(t *testing.T) {
adapter := NewAdapter()
// 创建一个简单的 fasthttp handler
handler := func(ctx *fasthttp.RequestCtx) {
ctx.SetStatusCode(200)
ctx.SetBodyString("Hello from fasthttp")
ctx.Response.Header.Set("Content-Type", "text/plain")
}
httpHandler := adapter.Wrap(handler)
if httpHandler == nil {
t.Error("Expected non-nil http.Handler")
}
}
// TestWrapHandler 测试 WrapHandler 函数
func TestWrapHandler(t *testing.T) {
adapter := NewAdapter()
handler := func(ctx *fasthttp.RequestCtx) {
ctx.SetStatusCode(200)
ctx.SetBodyString("test")
}
httpHandler := adapter.WrapHandler(handler)
if httpHandler == nil {
t.Error("Expected non-nil http.Handler")
}
}
// TestConvertRequest_Method 测试请求方法转换
func TestConvertRequest_Method(t *testing.T) {
adapter := NewAdapter()
tests := []string{"GET", "POST", "PUT", "DELETE", "HEAD", "OPTIONS"}
for _, method := range tests {
t.Run(method, func(t *testing.T) {
req := &http.Request{
Method: method,
URL: &url.URL{Path: "/test"},
Host: "localhost",
}
ctx := &fasthttp.RequestCtx{}
ctx.Init(&fasthttp.Request{}, nil, nil)
adapter.convertRequest(req, ctx)
if string(ctx.Method()) != method {
t.Errorf("Expected method %s, got %s", method, ctx.Method())
}
})
}
}
// TestConvertRequest_URI 测试 URI 转换
func TestConvertRequest_URI(t *testing.T) {
adapter := NewAdapter()
tests := []struct {
name string
path string
query string
expected string
}{
{
name: "simple path",
path: "/test",
query: "",
expected: "/test",
},
{
name: "path with query",
path: "/test",
query: "foo=bar",
expected: "/test?foo=bar",
},
{
name: "path with multiple query params",
path: "/api/users",
query: "id=1&name=test",
expected: "/api/users?id=1&name=test",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
req := &http.Request{
Method: "GET",
URL: &url.URL{Path: tt.path, RawQuery: tt.query},
Host: "localhost",
}
ctx := &fasthttp.RequestCtx{}
ctx.Init(&fasthttp.Request{}, nil, nil)
adapter.convertRequest(req, ctx)
if string(ctx.RequestURI()) != tt.expected {
t.Errorf("Expected URI %s, got %s", tt.expected, ctx.RequestURI())
}
})
}
}
// TestConvertRequest_Headers 测试头部转换
func TestConvertRequest_Headers(t *testing.T) {
adapter := NewAdapter()
req := &http.Request{
Method: "GET",
URL: &url.URL{Path: "/test"},
Host: "example.com",
Header: http.Header{
"X-Custom-Header": []string{"value1", "value2"},
"Content-Type": []string{"application/json"},
"Accept": []string{"text/html"},
},
}
ctx := &fasthttp.RequestCtx{}
ctx.Init(&fasthttp.Request{}, nil, nil)
adapter.convertRequest(req, ctx)
// 检查 Host
if string(ctx.Host()) != "example.com" {
t.Errorf("Expected Host example.com, got %s", ctx.Host())
}
// 检查头部
if string(ctx.Request.Header.Peek("Content-Type")) != "application/json" {
t.Errorf("Expected Content-Type application/json, got %s", ctx.Request.Header.Peek("Content-Type"))
}
if string(ctx.Request.Header.Peek("Accept")) != "text/html" {
t.Errorf("Expected Accept text/html, got %s", ctx.Request.Header.Peek("Accept"))
}
}
// TestConvertRequest_Body 测试请求体转换
func TestConvertRequest_Body(t *testing.T) {
adapter := NewAdapter()
bodyContent := []byte("test request body")
req := &http.Request{
Method: "POST",
URL: &url.URL{Path: "/test"},
Host: "localhost",
Body: io.NopCloser(bytes.NewReader(bodyContent)),
}
ctx := &fasthttp.RequestCtx{}
ctx.Init(&fasthttp.Request{}, nil, nil)
adapter.convertRequest(req, ctx)
if !bytes.Equal(ctx.Request.Body(), bodyContent) {
t.Errorf("Expected body %s, got %s", bodyContent, ctx.Request.Body())
}
}
// TestConvertRequest_RemoteAddr 测试远程地址转换
func TestConvertRequest_RemoteAddr(t *testing.T) {
adapter := NewAdapter()
req := &http.Request{
Method: "GET",
URL: &url.URL{Path: "/test"},
Host: "localhost",
RemoteAddr: "192.168.1.1:8080",
}
ctx := &fasthttp.RequestCtx{}
ctx.Init(&fasthttp.Request{}, nil, nil)
adapter.convertRequest(req, ctx)
remoteAddr := ctx.RemoteAddr()
if remoteAddr == nil {
t.Error("Expected non-nil remote address")
} else {
if remoteAddr.String() != "192.168.1.1:8080" {
t.Errorf("Expected remote addr 192.168.1.1:8080, got %s", remoteAddr.String())
}
}
}
// TestConvertResponse_Status 测试响应状态码转换
func TestConvertResponse_Status(t *testing.T) {
adapter := NewAdapter()
tests := []int{200, 201, 400, 404, 500}
for _, status := range tests {
t.Run(string(rune(status)), func(t *testing.T) {
ctx := &fasthttp.RequestCtx{}
ctx.Init(&fasthttp.Request{}, nil, nil)
ctx.SetStatusCode(status)
// 创建 mock ResponseWriter
rw := &mockResponseWriter{}
adapter.convertResponse(ctx, rw)
if rw.status != status {
t.Errorf("Expected status %d, got %d", status, rw.status)
}
})
}
}
// TestConvertResponse_DefaultStatus 测试默认状态码
func TestConvertResponse_DefaultStatus(t *testing.T) {
adapter := NewAdapter()
ctx := &fasthttp.RequestCtx{}
ctx.Init(&fasthttp.Request{}, nil, nil)
// 不设置状态码
rw := &mockResponseWriter{}
adapter.convertResponse(ctx, rw)
// 默认应该是 200
if rw.status != 200 {
t.Errorf("Expected default status 200, got %d", rw.status)
}
}
// TestConvertResponse_Headers 测试响应头部转换
func TestConvertResponse_Headers(t *testing.T) {
adapter := NewAdapter()
ctx := &fasthttp.RequestCtx{}
ctx.Init(&fasthttp.Request{}, nil, nil)
ctx.Response.Header.Set("Content-Type", "application/json")
ctx.Response.Header.Set("X-Custom", "value")
rw := &mockResponseWriter{}
adapter.convertResponse(ctx, rw)
if rw.header.Get("Content-Type") != "application/json" {
t.Errorf("Expected Content-Type application/json, got %s", rw.header.Get("Content-Type"))
}
if rw.header.Get("X-Custom") != "value" {
t.Errorf("Expected X-Custom value, got %s", rw.header.Get("X-Custom"))
}
}
// TestConvertResponse_Body 测试响应体转换
func TestConvertResponse_Body(t *testing.T) {
adapter := NewAdapter()
bodyContent := []byte("response body content")
ctx := &fasthttp.RequestCtx{}
ctx.Init(&fasthttp.Request{}, nil, nil)
ctx.SetBody(bodyContent)
rw := &mockResponseWriter{}
adapter.convertResponse(ctx, rw)
if !bytes.Equal(rw.body, bodyContent) {
t.Errorf("Expected body %s, got %s", bodyContent, rw.body)
}
}
// TestFastHTTPHandler 测试反向转换
func TestFastHTTPHandler(t *testing.T) {
// 创建标准库 handler
stdHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/plain")
w.WriteHeader(200)
w.Write([]byte("Hello from std http"))
})
ctx := &fasthttp.RequestCtx{}
ctx.Init(&fasthttp.Request{}, nil, nil)
ctx.Request.SetRequestURI("/test")
ctx.Request.Header.SetMethod("GET")
FastHTTPHandler(stdHandler, ctx)
if ctx.Response.StatusCode() != 200 {
t.Errorf("Expected status 200, got %d", ctx.Response.StatusCode())
}
if string(ctx.Response.Body()) != "Hello from std http" {
t.Errorf("Expected body 'Hello from std http', got %s", ctx.Response.Body())
}
}
// TestConvertToHTTPRequest 测试转换为标准库请求
func TestConvertToHTTPRequest(t *testing.T) {
ctx := &fasthttp.RequestCtx{}
ctx.Init(&fasthttp.Request{}, nil, nil)
ctx.Request.SetRequestURI("/path?query=value")
ctx.Request.Header.SetMethod("POST")
ctx.Request.Header.SetHost("example.com")
ctx.Request.Header.Set("Content-Type", "application/json")
ctx.Request.SetBody([]byte("test body"))
r := convertToHTTPRequest(ctx)
if r.Method != "POST" {
t.Errorf("Expected Method POST, got %s", r.Method)
}
if r.Host != "example.com" {
t.Errorf("Expected Host example.com, got %s", r.Host)
}
if r.URL.Path != "/path" {
t.Errorf("Expected Path /path, got %s", r.URL.Path)
}
if r.URL.RawQuery != "query=value" {
t.Errorf("Expected RawQuery query=value, got %s", r.URL.RawQuery)
}
if r.Proto != "HTTP/3" {
t.Errorf("Expected Proto HTTP/3, got %s", r.Proto)
}
if r.ProtoMajor != 3 || r.ProtoMinor != 0 {
t.Errorf("Expected Proto 3.0, got %d.%d", r.ProtoMajor, r.ProtoMinor)
}
// 检查头部
if r.Header.Get("Content-Type") != "application/json" {
t.Errorf("Expected Content-Type application/json, got %s", r.Header.Get("Content-Type"))
}
// 检查请求体
body, _ := io.ReadAll(r.Body)
if string(body) != "test body" {
t.Errorf("Expected body 'test body', got %s", body)
}
}
// TestFastHTTPResponseWriter_Write 测试 Write 方法
func TestFastHTTPResponseWriter_Write(t *testing.T) {
ctx := &fasthttp.RequestCtx{}
ctx.Init(&fasthttp.Request{}, nil, nil)
rw := &fastHTTPResponseWriter{ctx: ctx}
n, err := rw.Write([]byte("test content"))
if err != nil {
t.Errorf("Unexpected error: %v", err)
}
if n != len("test content") {
t.Errorf("Expected written %d, got %d", len("test content"), n)
}
// 检查状态码被自动设置
if rw.status != http.StatusOK {
t.Errorf("Expected auto-set status 200, got %d", rw.status)
}
}
// TestFastHTTPResponseWriter_WriteHeader 测试 WriteHeader 方法
func TestFastHTTPResponseWriter_WriteHeader(t *testing.T) {
ctx := &fasthttp.RequestCtx{}
ctx.Init(&fasthttp.Request{}, nil, nil)
rw := &fastHTTPResponseWriter{ctx: ctx}
rw.Header().Set("X-Custom", "value")
rw.WriteHeader(404)
if rw.status != 404 {
t.Errorf("Expected status 404, got %d", rw.status)
}
if rw.written != true {
t.Error("Expected written flag to be true")
}
// 再次调用应该被忽略
rw.WriteHeader(500)
if rw.status != 404 {
t.Errorf("Expected status to remain 404, got %d", rw.status)
}
}
// TestFastHTTPResponseWriter_Header 测试 Header 方法
func TestFastHTTPResponseWriter_Header(t *testing.T) {
ctx := &fasthttp.RequestCtx{}
ctx.Init(&fasthttp.Request{}, nil, nil)
rw := &fastHTTPResponseWriter{ctx: ctx}
h := rw.Header()
if h == nil {
t.Error("Expected non-nil header")
}
h.Set("Content-Type", "text/html")
if rw.Header().Get("Content-Type") != "text/html" {
t.Errorf("Expected Content-Type text/html, got %s", rw.Header().Get("Content-Type"))
}
}
// TestWrap_RoundTrip 完整流程测试
func TestWrap_RoundTrip(t *testing.T) {
adapter := NewAdapter()
// fasthttp handler
fastHandler := func(ctx *fasthttp.RequestCtx) {
// 检查请求
if string(ctx.Method()) != "POST" {
t.Errorf("Expected POST method, got %s", ctx.Method())
}
// 设置响应
ctx.SetStatusCode(201)
ctx.SetBodyString("Created")
ctx.Response.Header.Set("X-Response-Header", "test-value")
}
httpHandler := adapter.Wrap(fastHandler)
// 创建请求
req := &http.Request{
Method: "POST",
URL: &url.URL{Path: "/create"},
Host: "localhost",
Header: http.Header{
"Content-Type": []string{"application/json"},
},
Body: io.NopCloser(bytes.NewReader([]byte("request data"))),
}
// 创建 mock ResponseWriter
rw := &mockResponseWriter{}
// 执行
httpHandler.ServeHTTP(rw, req)
// 检查响应
if rw.status != 201 {
t.Errorf("Expected status 201, got %d", rw.status)
}
if rw.header.Get("X-Response-Header") != "test-value" {
t.Errorf("Expected X-Response-Header test-value, got %s", rw.header.Get("X-Response-Header"))
}
if string(rw.body) != "Created" {
t.Errorf("Expected body 'Created', got %s", rw.body)
}
}
// mockResponseWriter 用于测试的 mock ResponseWriter
type mockResponseWriter struct {
status int
header http.Header
body []byte
}
func (m *mockResponseWriter) Header() http.Header {
if m.header == nil {
m.header = make(http.Header)
}
return m.header
}
func (m *mockResponseWriter) WriteHeader(statusCode int) {
m.status = statusCode
}
func (m *mockResponseWriter) Write(data []byte) (int, error) {
m.body = append(m.body, data...)
if m.status == 0 {
m.status = 200
}
return len(data), nil
}

View File

@ -268,10 +268,10 @@ func (s *Server) GetAltSvcHeader() string {
// Stats 返回服务器统计信息。
type Stats struct {
Running bool // 是否运行中
Listen string // 监听地址
Enable0RTT bool // 是否启用 0-RTT
MaxStreams int // 最大并发流
Running bool // 是否运行中
Listen string // 监听地址
Enable0RTT bool // 是否启用 0-RTT
MaxStreams int // 最大并发流
}
// GetStats 返回服务器统计信息。
@ -285,4 +285,4 @@ func (s *Server) GetStats() Stats {
Enable0RTT: s.config.Enable0RTT,
MaxStreams: s.config.MaxStreams,
}
}
}

View File

@ -0,0 +1,374 @@
package http3
import (
"crypto/tls"
"testing"
"time"
"github.com/valyala/fasthttp"
"rua.plus/lolly/internal/config"
)
// TestNewServer_NilConfig 测试空配置错误
func TestNewServer_NilConfig(t *testing.T) {
handler := func(ctx *fasthttp.RequestCtx) {}
server, err := NewServer(nil, handler, &tls.Config{})
if err == nil {
t.Error("Expected error for nil config")
}
if server != nil {
t.Error("Expected nil server for nil config")
}
if err.Error() != "http3 config is nil" {
t.Errorf("Expected error message 'http3 config is nil', got: %v", err)
}
}
// TestNewServer_NilHandler 测试空 handler 错误
func TestNewServer_NilHandler(t *testing.T) {
cfg := &config.HTTP3Config{
Enabled: true,
Listen: ":443",
Enable0RTT: true,
}
server, err := NewServer(cfg, nil, &tls.Config{})
if err == nil {
t.Error("Expected error for nil handler")
}
if server != nil {
t.Error("Expected nil server for nil handler")
}
if err.Error() != "handler is nil" {
t.Errorf("Expected error message 'handler is nil', got: %v", err)
}
}
// TestNewServer_NilTLS 测试空 TLS 配置错误
func TestNewServer_NilTLS(t *testing.T) {
cfg := &config.HTTP3Config{
Enabled: true,
Listen: ":443",
Enable0RTT: true,
}
handler := func(ctx *fasthttp.RequestCtx) {}
server, err := NewServer(cfg, handler, nil)
if err == nil {
t.Error("Expected error for nil TLS config")
}
if server != nil {
t.Error("Expected nil server for nil TLS config")
}
if err.Error() != "tls config is required for HTTP/3" {
t.Errorf("Expected error message 'tls config is required for HTTP/3', got: %v", err)
}
}
// TestNewServer_Success 测试成功创建服务器
func TestNewServer_Success(t *testing.T) {
cfg := &config.HTTP3Config{
Enabled: true,
Listen: ":443",
Enable0RTT: true,
MaxStreams: 100,
}
handler := func(ctx *fasthttp.RequestCtx) {}
tlsConfig := &tls.Config{
Certificates: []tls.Certificate{},
}
server, err := NewServer(cfg, handler, tlsConfig)
if err != nil {
t.Errorf("Unexpected error: %v", err)
}
if server == nil {
t.Error("Expected non-nil server")
}
if server.config != cfg {
t.Error("Config not set correctly")
}
if server.handler == nil {
t.Error("Handler not set correctly")
}
if server.adapter == nil {
t.Error("Adapter not initialized")
}
if server.tlsConfig != tlsConfig {
t.Error("TLS config not set correctly")
}
if server.running {
t.Error("Server should not be running initially")
}
}
// TestGetAltSvcHeader_DefaultPort 测试默认端口 Alt-Svc 头
func TestGetAltSvcHeader_DefaultPort(t *testing.T) {
cfg := &config.HTTP3Config{
Enabled: true,
Listen: ":443",
}
handler := func(ctx *fasthttp.RequestCtx) {}
server, _ := NewServer(cfg, handler, &tls.Config{})
header := server.GetAltSvcHeader()
expected := `h3=":443"; ma=86400`
if header != expected {
t.Errorf("Expected Alt-Svc header '%s', got '%s'", expected, header)
}
}
// TestGetAltSvcHeader_CustomPort 测试自定义端口 Alt-Svc 头
func TestGetAltSvcHeader_CustomPort(t *testing.T) {
cfg := &config.HTTP3Config{
Enabled: true,
Listen: ":8443",
}
handler := func(ctx *fasthttp.RequestCtx) {}
server, _ := NewServer(cfg, handler, &tls.Config{})
header := server.GetAltSvcHeader()
expected := `h3=":8443"; ma=86400`
if header != expected {
t.Errorf("Expected Alt-Svc header '%s', got '%s'", expected, header)
}
}
// TestGetAltSvcHeader_Disabled 测试禁用 HTTP/3 时返回空
func TestGetAltSvcHeader_Disabled(t *testing.T) {
cfg := &config.HTTP3Config{
Enabled: false,
Listen: ":443",
}
handler := func(ctx *fasthttp.RequestCtx) {}
server, _ := NewServer(cfg, handler, &tls.Config{})
header := server.GetAltSvcHeader()
if header != "" {
t.Errorf("Expected empty Alt-Svc header when disabled, got '%s'", header)
}
}
// TestGetAltSvcHeader_EmptyListen 测试空监听地址时使用默认端口
func TestGetAltSvcHeader_EmptyListen(t *testing.T) {
cfg := &config.HTTP3Config{
Enabled: true,
Listen: "", // 空,使用默认 :443
}
handler := func(ctx *fasthttp.RequestCtx) {}
server, _ := NewServer(cfg, handler, &tls.Config{})
header := server.GetAltSvcHeader()
expected := `h3=":443"; ma=86400`
if header != expected {
t.Errorf("Expected Alt-Svc header '%s', got '%s'", expected, header)
}
}
// TestGetStats 测试获取统计信息
func TestGetStats(t *testing.T) {
cfg := &config.HTTP3Config{
Enabled: true,
Listen: ":8443",
Enable0RTT: true,
MaxStreams: 200,
}
handler := func(ctx *fasthttp.RequestCtx) {}
server, _ := NewServer(cfg, handler, &tls.Config{})
stats := server.GetStats()
if stats.Running {
t.Error("Expected Running to be false initially")
}
if stats.Listen != ":8443" {
t.Errorf("Expected Listen ':8443', got '%s'", stats.Listen)
}
if !stats.Enable0RTT {
t.Error("Expected Enable0RTT to be true")
}
if stats.MaxStreams != 200 {
t.Errorf("Expected MaxStreams 200, got %d", stats.MaxStreams)
}
}
// TestIsRunning 测试运行状态检查
func TestIsRunning(t *testing.T) {
cfg := &config.HTTP3Config{
Enabled: true,
Listen: ":443",
}
handler := func(ctx *fasthttp.RequestCtx) {}
server, _ := NewServer(cfg, handler, &tls.Config{})
// 初始状态应该是 false
if server.IsRunning() {
t.Error("Expected IsRunning to be false initially")
}
// 手动设置运行状态(不启动真实服务器)
server.mu.Lock()
server.running = true
server.mu.Unlock()
if !server.IsRunning() {
t.Error("Expected IsRunning to be true after setting")
}
// 再次设置为 false
server.mu.Lock()
server.running = false
server.mu.Unlock()
if server.IsRunning() {
t.Error("Expected IsRunning to be false after unsetting")
}
}
// TestStop_NotRunning 测试停止未运行的服务器
func TestStop_NotRunning(t *testing.T) {
cfg := &config.HTTP3Config{
Enabled: true,
Listen: ":443",
}
handler := func(ctx *fasthttp.RequestCtx) {}
server, _ := NewServer(cfg, handler, &tls.Config{})
// 服务器未启动Stop 应该返回 nil
err := server.Stop()
if err != nil {
t.Errorf("Expected nil error when stopping non-running server, got: %v", err)
}
}
// TestGracefulStop_NotRunning 测试优雅停止未运行的服务器
func TestGracefulStop_NotRunning(t *testing.T) {
cfg := &config.HTTP3Config{
Enabled: true,
Listen: ":443",
}
handler := func(ctx *fasthttp.RequestCtx) {}
server, _ := NewServer(cfg, handler, &tls.Config{})
// 服务器未启动GracefulStop 应该返回 nil
err := server.GracefulStop(5 * time.Second)
if err != nil {
t.Errorf("Expected nil error when graceful stopping non-running server, got: %v", err)
}
}
// TestGracefulStop_WithTimeout 测试优雅停止超时
func TestGracefulStop_WithTimeout(t *testing.T) {
cfg := &config.HTTP3Config{
Enabled: true,
Listen: ":443",
}
handler := func(ctx *fasthttp.RequestCtx) {}
server, _ := NewServer(cfg, handler, &tls.Config{})
// 测试不同超时值
tests := []struct {
name string
timeout time.Duration
}{
{"zero timeout", 0},
{"short timeout", 100 * time.Millisecond},
{"long timeout", 30 * time.Second},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := server.GracefulStop(tt.timeout)
if err != nil {
t.Errorf("Unexpected error: %v", err)
}
})
}
}
// TestServer_MultipleStop 测试多次调用 Stop
func TestServer_MultipleStop(t *testing.T) {
cfg := &config.HTTP3Config{
Enabled: true,
Listen: ":443",
}
handler := func(ctx *fasthttp.RequestCtx) {}
server, _ := NewServer(cfg, handler, &tls.Config{})
// 多次调用 Stop 应该都是安全的
for i := 0; i < 3; i++ {
err := server.Stop()
if err != nil {
t.Errorf("Stop call %d returned error: %v", i+1, err)
}
}
}
// TestServer_MultipleGracefulStop 测试多次调用 GracefulStop
func TestServer_MultipleGracefulStop(t *testing.T) {
cfg := &config.HTTP3Config{
Enabled: true,
Listen: ":443",
}
handler := func(ctx *fasthttp.RequestCtx) {}
server, _ := NewServer(cfg, handler, &tls.Config{})
// 多次调用 GracefulStop 应该都是安全的
for i := 0; i < 3; i++ {
err := server.GracefulStop(1 * time.Second)
if err != nil {
t.Errorf("GracefulStop call %d returned error: %v", i+1, err)
}
}
}
// TestStats_Struct 测试 Stats 结构体
func TestStats_Struct(t *testing.T) {
stats := Stats{
Running: true,
Listen: ":443",
Enable0RTT: true,
MaxStreams: 100,
}
if !stats.Running {
t.Error("Expected Running true")
}
if stats.Listen != ":443" {
t.Errorf("Expected Listen ':443', got '%s'", stats.Listen)
}
if !stats.Enable0RTT {
t.Error("Expected Enable0RTT true")
}
if stats.MaxStreams != 100 {
t.Errorf("Expected MaxStreams 100, got %d", stats.MaxStreams)
}
}

View File

@ -163,9 +163,9 @@ func (c *ConsistentHash) GetVirtualNodes() int {
// Stats 返回一致性哈希统计信息。
type ConsistentHashStats struct {
VirtualNodes int // 虚拟节点数
CircleSize int // 哈希环大小
SortedHashes int // 排序哈希值数量
VirtualNodes int // 虚拟节点数
CircleSize int // 哈希环大小
SortedHashes int // 排序哈希值数量
}
// GetStats 返回统计信息。
@ -174,11 +174,11 @@ func (c *ConsistentHash) GetStats() ConsistentHashStats {
defer c.mu.RUnlock()
return ConsistentHashStats{
VirtualNodes: c.virtualNodes,
CircleSize: len(c.circle),
SortedHashes: len(c.sortedHashes),
VirtualNodes: c.virtualNodes,
CircleSize: len(c.circle),
SortedHashes: len(c.sortedHashes),
}
}
// 验证接口实现
var _ Balancer = (*ConsistentHash)(nil)
var _ Balancer = (*ConsistentHash)(nil)

View File

@ -25,7 +25,7 @@ type Logger struct {
// AppLogger 应用日志管理器,统一管理启动/停止日志。
type AppLogger struct {
format string // "text" 或 "json"
format string // "text" 或 "json"
errorLog zerolog.Logger
writer io.Writer
}
@ -121,15 +121,15 @@ func (l *Logger) formatAccessLog(ctx *fasthttp.RequestCtx, status int, size int6
}
replacements := map[string]string{
"$remote_addr": ctx.RemoteAddr().String(),
"$remote_user": remoteUser,
"$request": string(ctx.Method()) + " " + string(ctx.Path()) + " " + string(ctx.Request.Header.Protocol()),
"$status": strconv.Itoa(status),
"$body_bytes_sent": strconv.FormatInt(size, 10),
"$request_time": fmt.Sprintf("%.6f", duration.Seconds()),
"$http_referer": string(ctx.Request.Header.Peek("Referer")),
"$http_user_agent": string(ctx.Request.Header.Peek("User-Agent")),
"$time": time.Now().Format(time.RFC3339),
"$remote_addr": ctx.RemoteAddr().String(),
"$remote_user": remoteUser,
"$request": string(ctx.Method()) + " " + string(ctx.Path()) + " " + string(ctx.Request.Header.Protocol()),
"$status": strconv.Itoa(status),
"$body_bytes_sent": strconv.FormatInt(size, 10),
"$request_time": fmt.Sprintf("%.6f", duration.Seconds()),
"$http_referer": string(ctx.Request.Header.Peek("Referer")),
"$http_user_agent": string(ctx.Request.Header.Peek("User-Agent")),
"$time": time.Now().Format(time.RFC3339),
}
result := l.accessFormat

View File

@ -144,4 +144,4 @@ func (g *GzipStatic) Extensions() []string {
// DefaultExtensions 返回默认支持的扩展名。
func DefaultExtensions() []string {
return []string{".html", ".css", ".js", ".json", ".xml", ".svg", ".txt"}
}
}

View File

@ -40,9 +40,9 @@ type SlidingWindowLimiter struct {
// windowCounter 窗口计数器。
type windowCounter struct {
count int64
count int64
timestamps []time.Time // precise 模式下记录每个请求时间
mu sync.Mutex
mu sync.Mutex
}
// NewSlidingWindowLimiter 创建滑动窗口限流器。
@ -260,4 +260,4 @@ func (s *SlidingWindowLimiter) GetCount(key string) int {
}
return int(counter.count)
}
}

View File

@ -297,4 +297,4 @@ func TestCopyData(t *testing.T) {
case <-time.After(1 * time.Second):
t.Error("copyData did not complete in time")
}
}
}

View File

@ -1,9 +1,11 @@
package server
import (
"net"
"testing"
"time"
"github.com/valyala/fasthttp"
"rua.plus/lolly/internal/config"
)
@ -278,4 +280,211 @@ func TestBuildMiddlewareChain_AllMiddlewares(t *testing.T) {
if chain == nil {
t.Error("Expected non-nil chain")
}
}
}
// TestTrackStats 测试请求统计追踪
func TestTrackStats(t *testing.T) {
cfg := &config.Config{
Server: config.ServerConfig{
Listen: ":8080",
},
}
s := New(cfg)
// 初始统计应该为 0
if s.requests.Load() != 0 {
t.Error("Initial requests should be 0")
}
if s.bytesSent.Load() != 0 {
t.Error("Initial bytesSent should be 0")
}
if s.bytesReceived.Load() != 0 {
t.Error("Initial bytesReceived should be 0")
}
// 创建测试 handler
handler := func(ctx *fasthttp.RequestCtx) {
ctx.SetBodyString("response body")
}
// 包装 handler
wrappedHandler := s.trackStats(handler)
// 创建测试请求上下文
ctx := &fasthttp.RequestCtx{}
ctx.Init(&fasthttp.Request{}, nil, nil)
ctx.Request.SetBody([]byte("request body"))
// 执行
wrappedHandler(ctx)
// 验证统计
if s.requests.Load() != 1 {
t.Errorf("Expected 1 request, got %d", s.requests.Load())
}
if s.bytesReceived.Load() != int64(len("request body")) {
t.Errorf("Expected bytesReceived %d, got %d", len("request body"), s.bytesReceived.Load())
}
if s.bytesSent.Load() != int64(len("response body")) {
t.Errorf("Expected bytesSent %d, got %d", len("response body"), s.bytesSent.Load())
}
}
// TestTrackStats_MultipleRequests 测试多次请求统计
func TestTrackStats_MultipleRequests(t *testing.T) {
cfg := &config.Config{
Server: config.ServerConfig{
Listen: ":8080",
},
}
s := New(cfg)
handler := func(ctx *fasthttp.RequestCtx) {
ctx.SetBodyString("ok")
}
wrappedHandler := s.trackStats(handler)
// 执行多次请求
for i := 0; i < 10; i++ {
ctx := &fasthttp.RequestCtx{}
ctx.Init(&fasthttp.Request{}, nil, nil)
wrappedHandler(ctx)
}
if s.requests.Load() != 10 {
t.Errorf("Expected 10 requests, got %d", s.requests.Load())
}
}
// TestGetListeners_Empty 测试空监听器列表
func TestGetListeners_Empty(t *testing.T) {
cfg := &config.Config{
Server: config.ServerConfig{
Listen: ":8080",
},
}
s := New(cfg)
listeners := s.GetListeners()
if listeners != nil {
t.Errorf("Expected nil listeners, got %v", listeners)
}
}
// TestSetListeners 测试设置监听器
func TestSetListeners(t *testing.T) {
cfg := &config.Config{
Server: config.ServerConfig{
Listen: ":8080",
},
}
s := New(cfg)
// 创建模拟监听器
listener1, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatalf("Failed to create listener: %v", err)
}
defer listener1.Close()
listener2, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatalf("Failed to create listener: %v", err)
}
defer listener2.Close()
listeners := []net.Listener{listener1, listener2}
s.SetListeners(listeners)
// 验证设置成功
got := s.GetListeners()
if len(got) != 2 {
t.Errorf("Expected 2 listeners, got %d", len(got))
}
}
// TestGetTLSConfig_NotConfigured 测试未配置 TLS
func TestGetTLSConfig_NotConfigured(t *testing.T) {
cfg := &config.Config{
Server: config.ServerConfig{
Listen: ":8080",
},
}
s := New(cfg)
tlsConfig, err := s.GetTLSConfig()
if err == nil {
t.Error("Expected error for unconfigured TLS")
}
if tlsConfig != nil {
t.Error("Expected nil TLS config")
}
if err.Error() != "TLS not configured" {
t.Errorf("Expected error 'TLS not configured', got: %v", err)
}
}
// TestGetHandler 测试获取 handler
func TestGetHandler(t *testing.T) {
cfg := &config.Config{
Server: config.ServerConfig{
Listen: ":8080",
},
}
s := New(cfg)
// 初始 handler 应该为 nil
handler := s.GetHandler()
if handler != nil {
t.Error("Expected nil handler initially")
}
// 设置一个 handler
testHandler := func(ctx *fasthttp.RequestCtx) {
ctx.SetBodyString("test")
}
s.handler = testHandler
// 验证获取成功
got := s.GetHandler()
if got == nil {
t.Error("Expected non-nil handler after setting")
}
}
// TestServer_Connections 测试连接统计
func TestServer_Connections(t *testing.T) {
cfg := &config.Config{
Server: config.ServerConfig{
Listen: ":8080",
},
}
s := New(cfg)
// 初始连接数应该为 0
if s.connections.Load() != 0 {
t.Error("Initial connections should be 0")
}
// 增加
s.connections.Add(1)
if s.connections.Load() != 1 {
t.Errorf("Expected 1 connection, got %d", s.connections.Load())
}
// 减少
s.connections.Add(-1)
if s.connections.Load() != 0 {
t.Errorf("Expected 0 connections, got %d", s.connections.Load())
}
}

View File

@ -478,4 +478,4 @@ func TestCollectStatus_WithFileCache(t *testing.T) {
if status.Cache != nil {
t.Error("expected Cache to be nil when no fileCache configured")
}
}
}