lolly/internal/integration/variable_test.go
xfy b5c3c0954f
Some checks failed
CI / Test (push) Failing after 23s
CI / Build (push) Has been skipped
fix: address repository review issues
Security:
- Enforce client_max_body_size in HTTP/2 and HTTP/3 adapters before buffering
- Use trusted-proxy-aware client IP extraction for access control and rate limiting
- Include Host and Vary headers in proxy cache key to prevent cache poisoning
- Harden static file path traversal check with filepath.Clean + prefix validation
- Honor proxy_ssl config for WebSocket upstream TLS

Proxy/handler bugs:
- Decrement WebSocket connection count immediately on return (not deferred in retry loop)
- Remove ineffective headersPool
- Coalesce concurrent background cache refreshes with singleflight

Dev/build:
- Fix Makefile run and test-config targets
- Remove broken wget healthcheck from docker-compose
- Add missing integration build tags
- Fix go vet warnings in tests

CI:
- Run golangci-lint and integration tests in Gitea Actions

Refs: docs/superpowers/plans/2026-06-17-fix-review-issues.md
2026-06-17 11:17:53 +08:00

336 lines
8.8 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.

//go:build integration
// variable_integration_test.go - 变量系统集成测试
//
// 测试变量系统与日志、代理、重写的端到端集成
//
// 作者xfy
package integration
import (
"bytes"
"strings"
"testing"
"time"
"github.com/valyala/fasthttp"
"rua.plus/lolly/internal/config"
"rua.plus/lolly/internal/logging"
"rua.plus/lolly/internal/middleware/rewrite"
"rua.plus/lolly/internal/variable"
)
// TestVariableEndToEndInLogging 测试变量在访问日志中的端到端使用
func TestVariableEndToEndInLogging(t *testing.T) {
// 创建内存日志输出
var buf bytes.Buffer
cfg := &config.LoggingConfig{
Access: config.AccessLogConfig{
// 使用完整的 nginx 风格格式
Format: "$remote_addr - $remote_user [$time_local] \"$request_method $request_uri $scheme\" $status $body_bytes_sent \"$http_user_agent\"",
},
}
logger := logging.New(cfg)
// 模拟请求
ctx := &fasthttp.RequestCtx{}
ctx.Request.SetRequestURI("/api/users?page=1")
ctx.Request.Header.SetMethod("GET")
ctx.Request.Header.SetHost("example.com")
ctx.Request.Header.Set("User-Agent", "TestAgent/1.0")
// 记录访问日志
logger.LogAccess(ctx, 200, 1024, 50*time.Millisecond)
// 验证输出(这里主要验证不 panic
_ = buf.String()
t.Log("Logging with variables completed successfully")
}
// TestVariableInProxyHeaders 测试代理请求头中的变量
func TestVariableInProxyHeaders(t *testing.T) {
// 创建请求
ctx := &fasthttp.RequestCtx{}
ctx.Request.SetRequestURI("/api/test")
ctx.Request.Header.SetMethod("POST")
ctx.Request.Header.SetHost("api.example.com")
ctx.Request.Header.Set("X-Custom-Header", "original")
// 测试变量展开
vc := variable.NewContext(ctx)
defer variable.ReleaseContext(vc)
// 模拟代理配置中的 header 设置
tests := []struct {
template string
contains []string
}{
{"X-Forwarded-Host: $host", []string{"api.example.com"}},
{"X-Real-IP: $remote_addr", []string{"0.0.0.0"}},
{"X-Request-ID: $request_id", []string{"-"}}, // 未设置时为 -
}
for _, tt := range tests {
result := vc.Expand(tt.template)
for _, expected := range tt.contains {
if !strings.Contains(result, expected) {
t.Errorf("Expand(%q) = %q, expected to contain %q", tt.template, result, expected)
}
}
}
}
// TestVariableInRewriteRules 测试重写规则中的变量
func TestVariableInRewriteRules(t *testing.T) {
// 创建带有变量的重写规则
rules := []config.RewriteRule{
{
Pattern: "^/api/(.*)$",
Replacement: "/v1/$1",
Flag: "break",
},
{
Pattern: "^/redirect/(.*)$",
Replacement: "/new/$1",
Flag: "redirect",
},
}
mw, err := rewrite.New(rules)
if err != nil {
t.Fatalf("failed to create rewrite middleware: %v", err)
}
// 测试第一个规则
t.Run("rewrite_with_capture", func(t *testing.T) {
ctx := &fasthttp.RequestCtx{}
ctx.Request.SetRequestURI("/api/users")
ctx.Request.Header.SetMethod("GET")
var capturedPath string
next := func(c *fasthttp.RequestCtx) {
capturedPath = string(c.Path())
}
handler := mw.Process(next)
handler(ctx)
if capturedPath != "/v1/users" {
t.Errorf("expected path '/v1/users', got %q", capturedPath)
}
})
// 测试重定向规则
t.Run("redirect_with_capture", func(t *testing.T) {
ctx := &fasthttp.RequestCtx{}
ctx.Request.SetRequestURI("/redirect/old-page")
ctx.Request.Header.SetMethod("GET")
handler := mw.Process(func(_ *fasthttp.RequestCtx) {
t.Error("should not reach next handler for redirect")
})
handler(ctx)
// 验证重定向状态码
if ctx.Response.StatusCode() != 302 {
t.Errorf("expected status 302, got %d", ctx.Response.StatusCode())
}
})
}
// TestVariableCompatibilityWithNginx 测试与 nginx 变量格式的兼容性
func TestVariableCompatibilityWithNginx(t *testing.T) {
ctx := &fasthttp.RequestCtx{}
ctx.Request.SetRequestURI("/test/path?foo=bar&baz=qux")
ctx.Request.Header.SetMethod("POST")
ctx.Request.Header.SetHost("example.com")
ctx.Request.Header.Set("User-Agent", "TestAgent/1.0")
ctx.Request.Header.Set("Referer", "http://referrer.com")
// 设置响应信息
variable.SetResponseInfoInContext(ctx, 201, 2048, 100000000) // 100ms
vc := variable.NewContext(ctx)
defer variable.ReleaseContext(vc)
// 设置 HTTP 头变量logging 中会自动设置这些)
vc.Set("http_referer", "http://referrer.com")
vc.Set("http_user_agent", "TestAgent/1.0")
vc.Set("remote_user", "-")
// 测试 nginx 风格的组合日志格式
logFormat := "$remote_addr - $remote_user [$time_local] \"$request_method $request_uri $scheme\" $status $body_bytes_sent \"$http_referer\" \"$http_user_agent\""
result := vc.Expand(logFormat)
// 验证结果包含期望的部分
expectedParts := []string{
"POST", // method
"/test/path", // path
"foo=bar", // query string
"http", // scheme
"201", // status
"2048", // body_bytes_sent
"http://referrer.com", // referer
"TestAgent/1.0", // user agent
}
for _, part := range expectedParts {
if !strings.Contains(result, part) {
t.Errorf("log output missing %q: got %q", part, result)
}
}
t.Logf("Generated log: %s", result)
}
// TestVariablePerformance 测试变量展开性能
func TestVariablePerformance(t *testing.T) {
ctx := &fasthttp.RequestCtx{}
ctx.Request.SetRequestURI("/api/v1/users/123?active=true")
ctx.Request.Header.SetMethod("GET")
ctx.Request.Header.SetHost("api.example.com")
vc := variable.NewContext(ctx)
defer variable.ReleaseContext(vc)
// 常见日志格式模板
template := "$remote_addr - $remote_user [$time_local] \"$request_method $request_uri $scheme\" $status $body_bytes_sent \"$http_user_agent\""
// 执行多次展开
start := time.Now()
iterations := 10000
for range iterations {
_ = vc.Expand(template)
}
elapsed := time.Since(start)
// 计算平均时间
avg := elapsed / time.Duration(iterations)
t.Logf("Average expansion time: %v (iterations: %d)", avg, iterations)
// 验证性能在合理范围内。
// race 检测会显著放大执行时间,因此阈值放宽到 50μs。
threshold := 50 * time.Microsecond
if avg > threshold {
t.Errorf("average time %v exceeds %v", avg, threshold)
}
}
// TestVariableEdgeCases 测试变量的边界情况
func TestVariableEdgeCases(t *testing.T) {
tests := []struct {
name string
setup func(*fasthttp.RequestCtx)
template string
expected string
}{
{
name: "empty_template",
setup: func(ctx *fasthttp.RequestCtx) {
ctx.Request.SetRequestURI("/test")
},
template: "",
expected: "",
},
{
name: "no_variables",
setup: func(ctx *fasthttp.RequestCtx) {
ctx.Request.SetRequestURI("/test")
},
template: "static text without variables",
expected: "static text without variables",
},
{
name: "undefined_variable",
setup: func(ctx *fasthttp.RequestCtx) {
ctx.Request.SetRequestURI("/test")
},
template: "$undefined_var",
expected: "$undefined_var", // 保持原样
},
{
name: "mixed_defined_undefined",
setup: func(ctx *fasthttp.RequestCtx) {
ctx.Request.SetRequestURI("/test")
ctx.Request.Header.SetHost("example.com")
},
template: "$host-$undefined",
expected: "example.com-$undefined",
},
{
name: "special_characters_in_value",
setup: func(ctx *fasthttp.RequestCtx) {
ctx.Request.SetRequestURI("/test%20path")
ctx.Request.Header.SetHost("example.com")
},
template: "$uri",
expected: "/test path", // fasthttp Path() 返回解码后的路径
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ctx := &fasthttp.RequestCtx{}
tt.setup(ctx)
vc := variable.NewContext(ctx)
defer variable.ReleaseContext(vc)
result := vc.Expand(tt.template)
if result != tt.expected {
t.Errorf("Expand(%q) = %q, want %q", tt.template, result, tt.expected)
}
})
}
}
// TestVariableAllBuiltins 测试所有内置变量
func TestVariableAllBuiltins(t *testing.T) {
ctx := &fasthttp.RequestCtx{}
ctx.Request.SetRequestURI("/test/path?foo=bar")
ctx.Request.Header.SetMethod("GET")
ctx.Request.Header.SetHost("example.com")
// 设置响应信息
variable.SetResponseInfoInContext(ctx, 200, 1024, 50000000) // 50ms
vc := variable.NewContext(ctx)
defer variable.ReleaseContext(vc)
// 测试所有内置变量
builtinVars := []string{
"host",
"remote_addr",
"remote_port",
"request_uri",
"uri",
"args",
"request_method",
"scheme",
"server_name",
"server_port",
"status",
"body_bytes_sent",
"request_time",
"time_local",
"time_iso8601",
"request_id",
}
for _, varName := range builtinVars {
t.Run(varName, func(t *testing.T) {
value, ok := vc.Get(varName)
if !ok {
t.Errorf("builtin variable %q not found", varName)
return
}
if value == "" && varName != "args" {
t.Logf("Warning: %q is empty", varName)
}
t.Logf("%s = %q", varName, value)
})
}
}