Batch 1 - Crash/data corruption: - Add nil/empty config guards in server and app - Protect against fasthttp buffer mutation in logging - Ensure mimeutil falls back before caching empty MIME - Make proxy health checker and resolver tolerate nil cfg - Prevent Lua EMERG/ALERT/CRIT log levels from killing process Batch 2 - Concurrency/race conditions: - Add focused race tests for compression pool, ssl/ocsp, server proxies slice, Lua timer cancel, and TCP socket connect - Fix Lua timer active race and socket ConnectAsync race Batch 3 - Resource leaks/functional damage: - Track and stop rate limiter cleanup goroutines on shutdown - Add tests for proxy connection count leaks, server pool deadlock, Linux sendfile integrity, WebSocket frame data loss, stream UDP stop deadlock, and upstream name mismatch - Fix stream ListenTCP/upstream lookup and UDP shutdown Batch 4 - High severity technical debt: - Add nil guard in security headers middleware - Validate sliding window divisor and add tests - Protect static handler fields with existing RWMutex - Avoid mutating live HostClient.Addr on DNS updates - Fix slow start Start/Stop and resolver restart - Initialize variable fallback context maps - Add tests for request_id/time_local and logging file handle close Also: - Relax integration variable performance threshold under race detector - Update benchmark plan docs for new stream ListenTCP signature Verification: - make test: pass - go test -race -count=1 ./internal/...: pass - make lint: 0 issues - make build: success
491 lines
14 KiB
Go
491 lines
14 KiB
Go
// builtin_coverage_test.go - 补充未覆盖函数的测试
|
||
//
|
||
// 测试覆盖:
|
||
// - formatRequestTime: 请求时间格式化
|
||
// - SetGlobalVariables: 全局变量设置
|
||
// - EphemeralGet: 未覆盖的分支
|
||
// - GetSSLClientVerify: TLS 状态分支
|
||
// - init: 内置变量注册验证
|
||
//
|
||
// 作者:xfy
|
||
package variable
|
||
|
||
import (
|
||
"sync"
|
||
"testing"
|
||
|
||
"github.com/stretchr/testify/assert"
|
||
"github.com/stretchr/testify/require"
|
||
"github.com/valyala/fasthttp"
|
||
)
|
||
|
||
// TestFormatRequestTime 测试请求处理时间格式化
|
||
func TestFormatRequestTime(t *testing.T) {
|
||
tests := []struct {
|
||
name string
|
||
ns int64
|
||
expected string
|
||
}{
|
||
{"零值", 0, "0.000"},
|
||
{"1毫秒", 1_000_000, "0.001"},
|
||
{"15毫秒", 15_000_000, "0.015"},
|
||
{"100毫秒", 100_000_000, "0.100"},
|
||
{"1秒", 1_000_000_000, "1.000"},
|
||
{"1.234秒", 1_234_000_000, "1.234"},
|
||
{"微小值", 1, "0.000"},
|
||
{"大值", 60_000_000_000, "60.000"},
|
||
}
|
||
|
||
for _, tt := range tests {
|
||
t.Run(tt.name, func(t *testing.T) {
|
||
result := formatRequestTime(tt.ns)
|
||
assert.Equal(t, tt.expected, result)
|
||
})
|
||
}
|
||
}
|
||
|
||
// TestFormatRequestTime_ViaBuiltinGetter 通过内置变量 getter 间接调用 formatRequestTime
|
||
func TestFormatRequestTime_ViaBuiltinGetter(t *testing.T) {
|
||
ctx := &fasthttp.RequestCtx{}
|
||
|
||
builtin := GetBuiltin(VarRequestTime)
|
||
require.NotNil(t, builtin)
|
||
require.NotNil(t, builtin.Getter)
|
||
|
||
tests := []struct {
|
||
name string
|
||
ns int64
|
||
expected string
|
||
}{
|
||
{"通过 getter 零值", 0, "0.000"},
|
||
{"通过 getter 15毫秒", 15_000_000, "0.015"},
|
||
}
|
||
|
||
for _, tt := range tests {
|
||
t.Run(tt.name, func(t *testing.T) {
|
||
ctx.SetUserValue(VarRequestTime, tt.ns)
|
||
result := builtin.Getter(ctx)
|
||
assert.Equal(t, tt.expected, result)
|
||
})
|
||
}
|
||
}
|
||
|
||
// TestSetGlobalVariables 测试设置全局自定义变量
|
||
func TestSetGlobalVariables(t *testing.T) {
|
||
t.Run("正常设置", func(t *testing.T) {
|
||
SetGlobalVariables(map[string]string{
|
||
"app_name": "lolly",
|
||
"environment": "production",
|
||
})
|
||
|
||
v, ok := GetGlobalVariable("app_name")
|
||
assert.True(t, ok)
|
||
assert.Equal(t, "lolly", v)
|
||
|
||
v, ok = GetGlobalVariable("environment")
|
||
assert.True(t, ok)
|
||
assert.Equal(t, "production", v)
|
||
})
|
||
|
||
t.Run("空配置", func(t *testing.T) {
|
||
SetGlobalVariables(map[string]string{})
|
||
|
||
// 之前设置的变量应该被清空
|
||
_, ok := GetGlobalVariable("app_name")
|
||
assert.False(t, ok)
|
||
})
|
||
|
||
t.Run("覆盖已有变量", func(t *testing.T) {
|
||
SetGlobalVariables(map[string]string{
|
||
"version": "1.0",
|
||
})
|
||
|
||
SetGlobalVariables(map[string]string{
|
||
"version": "2.0",
|
||
})
|
||
|
||
v, ok := GetGlobalVariable("version")
|
||
assert.True(t, ok)
|
||
assert.Equal(t, "2.0", v)
|
||
})
|
||
|
||
// 清理
|
||
SetGlobalVariables(nil)
|
||
}
|
||
|
||
// TestSetGlobalVariables_VariableExpansion 测试全局变量在展开中的使用
|
||
func TestSetGlobalVariables_VariableExpansion(t *testing.T) {
|
||
SetGlobalVariables(map[string]string{
|
||
"app_name": "lolly",
|
||
"env": "test",
|
||
})
|
||
|
||
ctx := &fasthttp.RequestCtx{}
|
||
ctx.Request.Header.SetHost("example.com")
|
||
vc := NewContext(ctx)
|
||
defer ReleaseContext(vc)
|
||
|
||
result := vc.Expand("$app_name-$env")
|
||
assert.Equal(t, "lolly-test", result)
|
||
|
||
// 清理
|
||
SetGlobalVariables(nil)
|
||
}
|
||
|
||
// TestEphemeralGet_GlobalVariables 测试 EphemeralGet 获取全局变量
|
||
func TestEphemeralGet_GlobalVariables(t *testing.T) {
|
||
SetGlobalVariables(map[string]string{
|
||
"global_key": "global_value",
|
||
})
|
||
|
||
ctx := &fasthttp.RequestCtx{}
|
||
vc := NewContext(ctx)
|
||
defer ReleaseContext(vc)
|
||
|
||
result := vc.EphemeralGet("global_key")
|
||
assert.Equal(t, []byte("global_value"), result)
|
||
|
||
// 清理
|
||
SetGlobalVariables(nil)
|
||
}
|
||
|
||
// TestEphemeralGet_GlobalVariablesFallback 测试全局变量不存在时 EphemeralGet 的行为
|
||
func TestEphemeralGet_GlobalVariablesFallback(t *testing.T) {
|
||
SetGlobalVariables(map[string]string{})
|
||
|
||
ctx := &fasthttp.RequestCtx{}
|
||
vc := NewContext(ctx)
|
||
defer ReleaseContext(vc)
|
||
|
||
result := vc.EphemeralGet("nonexistent_global")
|
||
assert.Nil(t, result)
|
||
|
||
// 清理
|
||
SetGlobalVariables(nil)
|
||
}
|
||
|
||
// TestEphemeralGet_ServerName 测试 EphemeralGet 获取 server_name
|
||
func TestEphemeralGet_ServerName(t *testing.T) {
|
||
ctx := &fasthttp.RequestCtx{}
|
||
vc := NewContext(ctx)
|
||
defer ReleaseContext(vc)
|
||
|
||
// 未设置时走 builtin getter
|
||
result := vc.EphemeralGet(VarServerName)
|
||
assert.Equal(t, []byte("-"), result)
|
||
}
|
||
|
||
// TestEphemeralGet_ServerNameSet 测试 EphemeralGet 获取设置后的 server_name
|
||
func TestEphemeralGet_ServerNameSet(t *testing.T) {
|
||
ctx := &fasthttp.RequestCtx{}
|
||
vc := NewContext(ctx)
|
||
defer ReleaseContext(vc)
|
||
|
||
vc.SetServerName("my-server")
|
||
result := vc.EphemeralGet(VarServerName)
|
||
assert.Equal(t, []byte("my-server"), result)
|
||
}
|
||
|
||
// TestEphemeralGet_UpstreamConnectAndHeaderTime 测试上游连接时间和头部时间的 EphemeralGet
|
||
func TestEphemeralGet_UpstreamConnectAndHeaderTime(t *testing.T) {
|
||
ctx := &fasthttp.RequestCtx{}
|
||
vc := NewContext(ctx)
|
||
defer ReleaseContext(vc)
|
||
|
||
// 未设置时返回 "-"
|
||
assert.Equal(t, []byte("-"), vc.EphemeralGet(VarUpstreamConnectTime))
|
||
assert.Equal(t, []byte("-"), vc.EphemeralGet(VarUpstreamHeaderTime))
|
||
|
||
// 设置后返回正确值
|
||
vc.SetUpstreamVars("http://backend:8080", 200, 0.123, 0.005, 0.010)
|
||
|
||
result := vc.EphemeralGet(VarUpstreamConnectTime)
|
||
assert.Equal(t, []byte("0.005"), result)
|
||
|
||
result = vc.EphemeralGet(VarUpstreamHeaderTime)
|
||
assert.Equal(t, []byte("0.010"), result)
|
||
}
|
||
|
||
// TestEphemeralGet_ResponseInfoViaUserValue 测试通过 UserValue 获取响应信息的 EphemeralGet
|
||
func TestEphemeralGet_ResponseInfoViaUserValue(t *testing.T) {
|
||
ctx := &fasthttp.RequestCtx{}
|
||
|
||
// 通过 SetResponseInfoInContext 设置(而非 SetResponseInfo)
|
||
SetResponseInfoInContext(ctx, 500, 2048, 30_000_000)
|
||
|
||
vc := NewContext(ctx)
|
||
defer ReleaseContext(vc)
|
||
|
||
assert.Equal(t, []byte("500"), vc.EphemeralGet(VarStatus))
|
||
assert.Equal(t, []byte("2048"), vc.EphemeralGet(VarBodyBytesSent))
|
||
assert.Equal(t, []byte("0.030"), vc.EphemeralGet(VarRequestTime))
|
||
}
|
||
|
||
// TestEphemeralGet_BodyBytesSentDefault 测试 body_bytes_sent 默认值的 EphemeralGet
|
||
func TestEphemeralGet_BodyBytesSentDefault(t *testing.T) {
|
||
ctx := &fasthttp.RequestCtx{}
|
||
vc := NewContext(ctx)
|
||
defer ReleaseContext(vc)
|
||
|
||
// 未设置时返回 "0"
|
||
result := vc.EphemeralGet(VarBodyBytesSent)
|
||
assert.Equal(t, []byte("0"), result)
|
||
}
|
||
|
||
// TestEphemeralGet_RequestTimeDefault 测试 request_time 默认值的 EphemeralGet
|
||
func TestEphemeralGet_RequestTimeDefault(t *testing.T) {
|
||
ctx := &fasthttp.RequestCtx{}
|
||
vc := NewContext(ctx)
|
||
defer ReleaseContext(vc)
|
||
|
||
// 未设置时返回 "0.000"
|
||
result := vc.EphemeralGet(VarRequestTime)
|
||
assert.Equal(t, []byte("0.000"), result)
|
||
}
|
||
|
||
// TestEphemeralGet_ConcurrentAccess 测试 EphemeralGet 的并发安全性
|
||
func TestEphemeralGet_ConcurrentAccess(t *testing.T) {
|
||
SetGlobalVariables(map[string]string{
|
||
"shared": "value",
|
||
})
|
||
|
||
var wg sync.WaitGroup
|
||
for i := range 10 {
|
||
wg.Add(1)
|
||
go func(id int) {
|
||
defer wg.Done()
|
||
ctx := &fasthttp.RequestCtx{}
|
||
vc := NewContext(ctx)
|
||
defer ReleaseContext(vc)
|
||
|
||
vc.Set("local", "val")
|
||
result := vc.EphemeralGet("shared")
|
||
assert.Equal(t, []byte("value"), result, "goroutine %d", id)
|
||
}(i)
|
||
}
|
||
wg.Wait()
|
||
|
||
// 清理
|
||
SetGlobalVariables(nil)
|
||
}
|
||
|
||
// TestGetSSLClientVerify_TLSWithUserValue 测试 TLS 连接下设置 UserValue 的场景
|
||
func TestGetSSLClientVerify_TLSWithUserValue(t *testing.T) {
|
||
ctx := &fasthttp.RequestCtx{}
|
||
// 模拟 TLS 连接
|
||
ctx.SetUserValue("tls_connection", true)
|
||
// 设置验证结果为 SUCCESS
|
||
ctx.SetUserValue(VarSSLClientVerify, "SUCCESS")
|
||
|
||
// 由于 fasthttp.RequestCtx 默认 IsTLS() 返回 false
|
||
// 无法直接模拟 TLS 连接,所以测试 UserValue 被正确读取的场景
|
||
// 这个测试验证了当 IsTLS() 为 true 时的逻辑路径
|
||
// 在非 TLS 环境下,即使设置了 UserValue 也会返回 NONE
|
||
result := GetSSLClientVerify(ctx)
|
||
assert.Equal(t, "NONE", result)
|
||
}
|
||
|
||
// TestGetSSLClientVerify_TLSWithPeerCertPresent 测试 TLS 连接下 peer cert 存在的场景
|
||
func TestGetSSLClientVerify_TLSWithPeerCertPresent(t *testing.T) {
|
||
ctx := &fasthttp.RequestCtx{}
|
||
// 设置证书存在标志(模拟 mTLS 场景)
|
||
ctx.SetUserValue("tls_peer_cert_present", true)
|
||
|
||
// 非 TLS 连接,即使设置了 peer_cert_present 也返回 NONE
|
||
result := GetSSLClientVerify(ctx)
|
||
assert.Equal(t, "NONE", result)
|
||
}
|
||
|
||
// TestGetSSLClientVerify_InvalidUserValueType 测试 UserValue 类型不正确的场景
|
||
func TestGetSSLClientVerify_InvalidUserValueType(t *testing.T) {
|
||
ctx := &fasthttp.RequestCtx{}
|
||
// 设置非 string 类型的 UserValue
|
||
ctx.SetUserValue(VarSSLClientVerify, 12345)
|
||
|
||
// 非 TLS 连接直接返回 NONE,不会检查 UserValue 类型
|
||
result := GetSSLClientVerify(ctx)
|
||
assert.Equal(t, "NONE", result)
|
||
}
|
||
|
||
// TestInit_BuiltinVariablesRegistered 测试 init 函数注册了所有内置变量
|
||
func TestInit_BuiltinVariablesRegistered(t *testing.T) {
|
||
expectedVars := []string{
|
||
VarHost,
|
||
VarRemoteAddr,
|
||
VarRemotePort,
|
||
VarRequestURI,
|
||
VarURI,
|
||
VarArgs,
|
||
VarRequestMethod,
|
||
VarScheme,
|
||
VarServerName,
|
||
VarServerPort,
|
||
VarStatus,
|
||
VarBodyBytesSent,
|
||
VarRequestTime,
|
||
VarTimeLocal,
|
||
VarTimeISO8601,
|
||
VarRequestID,
|
||
}
|
||
|
||
for _, name := range expectedVars {
|
||
t.Run(name, func(t *testing.T) {
|
||
builtin := GetBuiltin(name)
|
||
require.NotNil(t, builtin, "内置变量 %s 应该已注册", name)
|
||
assert.Equal(t, name, builtin.Name)
|
||
assert.NotEmpty(t, builtin.Description)
|
||
assert.NotNil(t, builtin.Getter, "内置变量 %s 应该有 Getter", name)
|
||
})
|
||
}
|
||
}
|
||
|
||
// TestInit_UpstreamVariablesInContext 测试上游变量通过 Context 字段获取
|
||
// 上游变量不是通过 RegisterBuiltin 注册的,而是通过 Context 结构体字段直接访问
|
||
func TestInit_UpstreamVariablesInContext(t *testing.T) {
|
||
ctx := &fasthttp.RequestCtx{}
|
||
vc := NewContext(ctx)
|
||
defer ReleaseContext(vc)
|
||
|
||
upstreamVars := []string{
|
||
VarUpstreamAddr,
|
||
VarUpstreamStatus,
|
||
VarUpstreamResponseTime,
|
||
VarUpstreamConnectTime,
|
||
VarUpstreamHeaderTime,
|
||
}
|
||
|
||
vc.SetUpstreamVars("http://backend:8080", 200, 0.123, 0.005, 0.010)
|
||
|
||
for _, name := range upstreamVars {
|
||
t.Run(name, func(t *testing.T) {
|
||
v, ok := vc.Get(name)
|
||
require.True(t, ok, "上游变量 %s 应该可以通过 Get 获取", name)
|
||
assert.NotEqual(t, "", v, "上游变量 %s 不应为空", name)
|
||
})
|
||
}
|
||
}
|
||
|
||
// TestInit_SSLVariablesRegistered 测试 init 注册了 SSL 变量
|
||
func TestInit_SSLVariablesRegistered(t *testing.T) {
|
||
sslVars := []string{
|
||
VarSSLClientVerify,
|
||
VarSSLClientSerial,
|
||
VarSSLClientSubject,
|
||
VarSSLClientIssuer,
|
||
VarSSLClientFingerprint,
|
||
VarSSLClientNotBefore,
|
||
VarSSLClientNotAfter,
|
||
VarSSLClientDNS,
|
||
VarSSLClientEmail,
|
||
}
|
||
|
||
for _, name := range sslVars {
|
||
t.Run(name, func(t *testing.T) {
|
||
builtin := GetBuiltin(name)
|
||
require.NotNil(t, builtin, "SSL 变量 %s 应该已注册", name)
|
||
assert.NotEmpty(t, builtin.Description)
|
||
})
|
||
}
|
||
}
|
||
|
||
// TestInit_HostGetterBytes 测试 host 变量有 GetterBytes
|
||
func TestInit_HostGetterBytes(t *testing.T) {
|
||
builtin := GetBuiltin(VarHost)
|
||
require.NotNil(t, builtin)
|
||
assert.NotNil(t, builtin.GetterBytes)
|
||
|
||
ctx := &fasthttp.RequestCtx{}
|
||
ctx.Request.Header.SetHost("test.example.com")
|
||
|
||
result := builtin.GetterBytes(ctx)
|
||
assert.Equal(t, []byte("test.example.com"), result)
|
||
}
|
||
|
||
// TestInit_URIGetterBytes 测试 URI 变量有 GetterBytes
|
||
func TestInit_URIGetterBytes(t *testing.T) {
|
||
builtin := GetBuiltin(VarURI)
|
||
require.NotNil(t, builtin)
|
||
assert.NotNil(t, builtin.GetterBytes)
|
||
|
||
ctx := &fasthttp.RequestCtx{}
|
||
ctx.Request.SetRequestURI("/path/to/resource")
|
||
|
||
result := builtin.GetterBytes(ctx)
|
||
assert.Equal(t, []byte("/path/to/resource"), result)
|
||
}
|
||
|
||
// TestInit_RequestURIGetterBytes 测试 request_uri 变量有 GetterBytes
|
||
func TestInit_RequestURIGetterBytes(t *testing.T) {
|
||
builtin := GetBuiltin(VarRequestURI)
|
||
require.NotNil(t, builtin)
|
||
assert.NotNil(t, builtin.GetterBytes)
|
||
|
||
ctx := &fasthttp.RequestCtx{}
|
||
ctx.Request.SetRequestURI("/path?query=1")
|
||
|
||
result := builtin.GetterBytes(ctx)
|
||
assert.Equal(t, []byte("/path?query=1"), result)
|
||
}
|
||
|
||
// TestInit_ArgsGetterBytes 测试 args 变量有 GetterBytes
|
||
func TestInit_ArgsGetterBytes(t *testing.T) {
|
||
builtin := GetBuiltin(VarArgs)
|
||
require.NotNil(t, builtin)
|
||
assert.NotNil(t, builtin.GetterBytes)
|
||
|
||
ctx := &fasthttp.RequestCtx{}
|
||
ctx.Request.SetRequestURI("/test?key=val")
|
||
|
||
result := builtin.GetterBytes(ctx)
|
||
assert.Equal(t, []byte("key=val"), result)
|
||
}
|
||
|
||
// TestRequestID_GeneratedOnce 验证 request_id 在一次请求中只生成一次并存储在 UserValue。
|
||
func TestRequestID_GeneratedOnce(t *testing.T) {
|
||
ctx := &fasthttp.RequestCtx{}
|
||
vc := NewContext(ctx)
|
||
defer ReleaseContext(vc)
|
||
|
||
id1, ok1 := vc.Get(VarRequestID)
|
||
id2, ok2 := vc.Get(VarRequestID)
|
||
|
||
require.True(t, ok1, "第一次获取 request_id 应该成功")
|
||
require.True(t, ok2, "第二次获取 request_id 应该成功")
|
||
assert.Equal(t, id1, id2, "同一请求的 request_id 应该相同")
|
||
assert.NotEmpty(t, id1, "request_id 不应为空")
|
||
|
||
// 验证已存储在 UserValue 中
|
||
uv := ctx.UserValue(VarRequestID)
|
||
require.NotNil(t, uv)
|
||
assert.Equal(t, id1, uv)
|
||
}
|
||
|
||
// TestTimeLocal_Format 验证 time_local 使用 -0700 时区格式。
|
||
func TestTimeLocal_Format(t *testing.T) {
|
||
ctx := &fasthttp.RequestCtx{}
|
||
vc := NewContext(ctx)
|
||
defer ReleaseContext(vc)
|
||
|
||
value, ok := vc.Get(VarTimeLocal)
|
||
require.True(t, ok, "time_local 应该存在")
|
||
assert.NotEmpty(t, value, "time_local 不应为空")
|
||
|
||
// 格式示例:16/Jun/2026:10:23:42 +0000
|
||
// 必须包含时区偏移(+/-HHMM)
|
||
assert.Regexp(t, `\d{2}/[A-Za-z]{3}/\d{4}:\d{2}:\d{2}:\d{2} [+-]\d{4}`, value,
|
||
"time_local 格式应为 02/Jan/2006:15:04:05 -0700")
|
||
}
|
||
|
||
// TestInit_MethodGetterBytes 测试 request_method 变量有 GetterBytes
|
||
func TestInit_MethodGetterBytes(t *testing.T) {
|
||
builtin := GetBuiltin(VarRequestMethod)
|
||
require.NotNil(t, builtin)
|
||
assert.NotNil(t, builtin.GetterBytes)
|
||
|
||
ctx := &fasthttp.RequestCtx{}
|
||
ctx.Request.Header.SetMethod("POST")
|
||
|
||
result := builtin.GetterBytes(ctx)
|
||
assert.Equal(t, []byte("POST"), result)
|
||
}
|