lolly/internal/variable/builtin_coverage_test.go
xfy f1dd8cbeb0 fix: complete Stage 1 deep review fixes
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
2026-06-17 09:48:43 +08:00

491 lines
14 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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)
}