主要修复: - errcheck: defer Close 使用 //nolint:errcheck,类型断言改为 ok 检查 - govet fieldalignment: 调整结构体字段顺序优化内存布局 - revive unused-parameter: 将未使用参数改为 _ - exhaustive: 添加缺失的 switch case 或 default - goconst: 提取重复字符串为常量 (accessAllow, accessDeny 等) - staticcheck SA9003: 修复空分支逻辑 - gofmt: 运行 gofmt -w 格式化 - nolintlint: 修复 nolint 注释格式 其他改进: - 更新 .golangci.yml 配置,启用更严格的检查 - 移除未使用的代码和导入 - 简化测试辅助函数调用 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
548 lines
14 KiB
Go
548 lines
14 KiB
Go
// Package lua 提供 ngx.req API 测试
|
||
package lua
|
||
|
||
import (
|
||
"testing"
|
||
|
||
"github.com/stretchr/testify/assert"
|
||
"github.com/stretchr/testify/require"
|
||
"github.com/valyala/fasthttp"
|
||
glua "github.com/yuin/gopher-lua"
|
||
"rua.plus/lolly/internal/testutil"
|
||
)
|
||
|
||
// 创建测试用的 fasthttp.RequestCtx
|
||
func createTestRequestCtx(method, uri string, headers map[string]string, body []byte) *fasthttp.RequestCtx {
|
||
ctx := testutil.NewRequestCtxWithHeader(method, uri, headers)
|
||
|
||
// 设置请求体
|
||
if len(body) > 0 {
|
||
ctx.Request.SetBody(body)
|
||
}
|
||
|
||
return ctx
|
||
}
|
||
|
||
// TestNgxReqGetMethod 测试 ngx.req.get_method()
|
||
func TestNgxReqGetMethod(t *testing.T) {
|
||
engine, err := NewEngine(DefaultConfig())
|
||
require.NoError(t, err)
|
||
defer engine.Close()
|
||
|
||
// 创建测试请求
|
||
reqCtx := createTestRequestCtx("POST", "/test", nil, nil)
|
||
|
||
// 创建协程
|
||
coro, err := engine.NewCoroutine(reqCtx)
|
||
require.NoError(t, err)
|
||
defer coro.Close()
|
||
|
||
// 设置沙箱(这会自动注册 ngx API)
|
||
err = coro.SetupSandbox()
|
||
require.NoError(t, err)
|
||
|
||
// 测试获取请求方法
|
||
err = coro.Execute(`
|
||
local method = ngx.req.get_method()
|
||
if method ~= "POST" then
|
||
error("expected POST, got " .. tostring(method))
|
||
end
|
||
`)
|
||
assert.NoError(t, err)
|
||
}
|
||
|
||
// TestNgxReqGetURI 测试 ngx.req.get_uri()
|
||
func TestNgxReqGetURI(t *testing.T) {
|
||
engine, err := NewEngine(DefaultConfig())
|
||
require.NoError(t, err)
|
||
defer engine.Close()
|
||
|
||
reqCtx := createTestRequestCtx("GET", "/path/to/resource?key=value", nil, nil)
|
||
|
||
coro, err := engine.NewCoroutine(reqCtx)
|
||
require.NoError(t, err)
|
||
defer coro.Close()
|
||
|
||
err = coro.SetupSandbox()
|
||
require.NoError(t, err)
|
||
|
||
err = coro.Execute(`
|
||
local uri = ngx.req.get_uri()
|
||
if uri ~= "/path/to/resource" then
|
||
error("expected /path/to/resource, got " .. tostring(uri))
|
||
end
|
||
`)
|
||
assert.NoError(t, err)
|
||
}
|
||
|
||
// TestNgxReqSetURI 测试 ngx.req.set_uri()
|
||
func TestNgxReqSetURI(t *testing.T) {
|
||
engine, err := NewEngine(DefaultConfig())
|
||
require.NoError(t, err)
|
||
defer engine.Close()
|
||
|
||
// 测试设置 URI(不带 jump)
|
||
reqCtx := createTestRequestCtx("GET", "/original", nil, nil)
|
||
coro, err := engine.NewCoroutine(reqCtx)
|
||
require.NoError(t, err)
|
||
|
||
err = coro.SetupSandbox()
|
||
require.NoError(t, err)
|
||
|
||
err = coro.Execute(`
|
||
ngx.req.set_uri("/new/path")
|
||
`)
|
||
assert.NoError(t, err)
|
||
coro.Close()
|
||
|
||
// 验证 URI 已修改
|
||
assert.Equal(t, "/new/path", string(reqCtx.URI().Path()))
|
||
|
||
// 测试设置 URI(带 jump)
|
||
reqCtx2 := createTestRequestCtx("GET", "/original", nil, nil)
|
||
coro2, err := engine.NewCoroutine(reqCtx2)
|
||
require.NoError(t, err)
|
||
|
||
err = coro2.SetupSandbox()
|
||
require.NoError(t, err)
|
||
|
||
err = coro2.Execute(`
|
||
ngx.req.set_uri("/redirect/path", true)
|
||
`)
|
||
assert.NoError(t, err)
|
||
coro2.Close()
|
||
|
||
assert.Equal(t, "/redirect/path", string(reqCtx2.URI().Path()))
|
||
// 验证 jump 标记已设置
|
||
jumpFlag := reqCtx2.UserValue("_ngx_req_internal_jump")
|
||
assert.Equal(t, true, jumpFlag)
|
||
}
|
||
|
||
// TestNgxReqGetURIArgs 测试 ngx.req.get_uri_args()
|
||
func TestNgxReqGetURIArgs(t *testing.T) {
|
||
engine, err := NewEngine(DefaultConfig())
|
||
require.NoError(t, err)
|
||
defer engine.Close()
|
||
|
||
reqCtx := createTestRequestCtx("GET", "/test?foo=bar&baz=qux&arr=1&arr=2", nil, nil)
|
||
|
||
coro, err := engine.NewCoroutine(reqCtx)
|
||
require.NoError(t, err)
|
||
defer coro.Close()
|
||
|
||
err = coro.SetupSandbox()
|
||
require.NoError(t, err)
|
||
|
||
err = coro.Execute(`
|
||
local args = ngx.req.get_uri_args()
|
||
|
||
if args.foo ~= "bar" then
|
||
error("expected foo=bar, got " .. tostring(args.foo))
|
||
end
|
||
|
||
if args.baz ~= "qux" then
|
||
error("expected baz=qux, got " .. tostring(args.baz))
|
||
end
|
||
|
||
-- 多值参数应该返回数组
|
||
if type(args.arr) ~= "table" then
|
||
error("expected arr to be table, got " .. type(args.arr))
|
||
end
|
||
|
||
if args.arr[1] ~= "1" or args.arr[2] ~= "2" then
|
||
error("expected arr = {1, 2}")
|
||
end
|
||
`)
|
||
assert.NoError(t, err)
|
||
}
|
||
|
||
// TestNgxReqSetURIArgs 测试 ngx.req.set_uri_args()
|
||
func TestNgxReqSetURIArgs(t *testing.T) {
|
||
engine, err := NewEngine(DefaultConfig())
|
||
require.NoError(t, err)
|
||
defer engine.Close()
|
||
|
||
reqCtx := createTestRequestCtx("GET", "/test", nil, nil)
|
||
|
||
coro, err := engine.NewCoroutine(reqCtx)
|
||
require.NoError(t, err)
|
||
defer coro.Close()
|
||
|
||
err = coro.SetupSandbox()
|
||
require.NoError(t, err)
|
||
|
||
// 测试使用 table 设置参数
|
||
err = coro.Execute(`
|
||
ngx.req.set_uri_args({ key = "value", num = 123 })
|
||
`)
|
||
assert.NoError(t, err)
|
||
|
||
queryStr := string(reqCtx.URI().QueryString())
|
||
assert.Contains(t, queryStr, "key=value")
|
||
assert.Contains(t, queryStr, "num=123")
|
||
|
||
// 测试使用字符串设置参数
|
||
reqCtx2 := createTestRequestCtx("GET", "/test", nil, nil)
|
||
coro2, err := engine.NewCoroutine(reqCtx2)
|
||
require.NoError(t, err)
|
||
defer coro2.Close()
|
||
|
||
err = coro2.SetupSandbox()
|
||
require.NoError(t, err)
|
||
|
||
err = coro2.Execute(`
|
||
ngx.req.set_uri_args("foo=bar&baz=qux")
|
||
`)
|
||
assert.NoError(t, err)
|
||
|
||
assert.Equal(t, "foo=bar&baz=qux", string(reqCtx2.URI().QueryString()))
|
||
}
|
||
|
||
// TestNgxReqGetHeaders 测试 ngx.req.get_headers()
|
||
func TestNgxReqGetHeaders(t *testing.T) {
|
||
engine, err := NewEngine(DefaultConfig())
|
||
require.NoError(t, err)
|
||
defer engine.Close()
|
||
|
||
reqCtx := createTestRequestCtx("GET", "/test", map[string]string{
|
||
"Host": "example.com",
|
||
"X-Custom": "custom-value",
|
||
"Content-Type": "application/json",
|
||
}, nil)
|
||
|
||
coro, err := engine.NewCoroutine(reqCtx)
|
||
require.NoError(t, err)
|
||
defer coro.Close()
|
||
|
||
err = coro.SetupSandbox()
|
||
require.NoError(t, err)
|
||
|
||
err = coro.Execute(`
|
||
local headers = ngx.req.get_headers()
|
||
|
||
if headers.Host ~= "example.com" then
|
||
error("expected Host=example.com, got " .. tostring(headers.Host))
|
||
end
|
||
|
||
if headers["X-Custom"] ~= "custom-value" then
|
||
error("expected X-Custom=custom-value")
|
||
end
|
||
|
||
if headers["Content-Type"] ~= "application/json" then
|
||
error("expected Content-Type=application/json")
|
||
end
|
||
`)
|
||
assert.NoError(t, err)
|
||
}
|
||
|
||
// TestNgxReqSetHeader 测试 ngx.req.set_header()
|
||
func TestNgxReqSetHeader(t *testing.T) {
|
||
engine, err := NewEngine(DefaultConfig())
|
||
require.NoError(t, err)
|
||
defer engine.Close()
|
||
|
||
// 测试设置请求头
|
||
reqCtx := createTestRequestCtx("GET", "/test", nil, nil)
|
||
coro, err := engine.NewCoroutine(reqCtx)
|
||
require.NoError(t, err)
|
||
|
||
err = coro.SetupSandbox()
|
||
require.NoError(t, err)
|
||
|
||
err = coro.Execute(`
|
||
ngx.req.set_header("X-Custom-Header", "custom-value")
|
||
`)
|
||
assert.NoError(t, err)
|
||
coro.Close()
|
||
|
||
assert.Equal(t, "custom-value", string(reqCtx.Request.Header.Peek("X-Custom-Header")))
|
||
|
||
// 测试使用 nil 清除请求头
|
||
reqCtx2 := createTestRequestCtx("GET", "/test", map[string]string{
|
||
"X-Custom-Header": "custom-value",
|
||
}, nil)
|
||
coro2, err := engine.NewCoroutine(reqCtx2)
|
||
require.NoError(t, err)
|
||
|
||
err = coro2.SetupSandbox()
|
||
require.NoError(t, err)
|
||
|
||
err = coro2.Execute(`
|
||
ngx.req.set_header("X-Custom-Header", nil)
|
||
`)
|
||
assert.NoError(t, err)
|
||
coro2.Close()
|
||
|
||
assert.Equal(t, "", string(reqCtx2.Request.Header.Peek("X-Custom-Header")))
|
||
}
|
||
|
||
// TestNgxReqClearHeader 测试 ngx.req.clear_header()
|
||
func TestNgxReqClearHeader(t *testing.T) {
|
||
engine, err := NewEngine(DefaultConfig())
|
||
require.NoError(t, err)
|
||
defer engine.Close()
|
||
|
||
reqCtx := createTestRequestCtx("GET", "/test", map[string]string{
|
||
"X-To-Clear": "value",
|
||
}, nil)
|
||
|
||
coro, err := engine.NewCoroutine(reqCtx)
|
||
require.NoError(t, err)
|
||
defer coro.Close()
|
||
|
||
err = coro.SetupSandbox()
|
||
require.NoError(t, err)
|
||
|
||
// 先验证头存在
|
||
assert.Equal(t, "value", string(reqCtx.Request.Header.Peek("X-To-Clear")))
|
||
|
||
// 清除头
|
||
err = coro.Execute(`
|
||
ngx.req.clear_header("X-To-Clear")
|
||
`)
|
||
assert.NoError(t, err)
|
||
|
||
assert.Equal(t, "", string(reqCtx.Request.Header.Peek("X-To-Clear")))
|
||
}
|
||
|
||
// TestNgxReqGetBodyData 测试 ngx.req.get_body_data()
|
||
func TestNgxReqGetBodyData(t *testing.T) {
|
||
engine, err := NewEngine(DefaultConfig())
|
||
require.NoError(t, err)
|
||
defer engine.Close()
|
||
|
||
reqCtx := createTestRequestCtx("POST", "/test", nil, []byte("test body data"))
|
||
|
||
coro, err := engine.NewCoroutine(reqCtx)
|
||
require.NoError(t, err)
|
||
defer coro.Close()
|
||
|
||
err = coro.SetupSandbox()
|
||
require.NoError(t, err)
|
||
|
||
err = coro.Execute(`
|
||
local body = ngx.req.get_body_data()
|
||
if body ~= "test body data" then
|
||
error("expected 'test body data', got " .. tostring(body))
|
||
end
|
||
`)
|
||
assert.NoError(t, err)
|
||
}
|
||
|
||
// TestNgxReqGetBodyDataEmpty 测试 ngx.req.get_body_data() 空请求体
|
||
func TestNgxReqGetBodyDataEmpty(t *testing.T) {
|
||
engine, err := NewEngine(DefaultConfig())
|
||
require.NoError(t, err)
|
||
defer engine.Close()
|
||
|
||
reqCtx := createTestRequestCtx("GET", "/test", nil, nil)
|
||
|
||
coro, err := engine.NewCoroutine(reqCtx)
|
||
require.NoError(t, err)
|
||
defer coro.Close()
|
||
|
||
err = coro.SetupSandbox()
|
||
require.NoError(t, err)
|
||
|
||
err = coro.Execute(`
|
||
local body = ngx.req.get_body_data()
|
||
if body ~= nil then
|
||
error("expected nil for empty body, got " .. tostring(body))
|
||
end
|
||
`)
|
||
assert.NoError(t, err)
|
||
}
|
||
|
||
// TestNgxReqReadBody 测试 ngx.req.read_body()
|
||
func TestNgxReqReadBody(t *testing.T) {
|
||
engine, err := NewEngine(DefaultConfig())
|
||
require.NoError(t, err)
|
||
defer engine.Close()
|
||
|
||
reqCtx := createTestRequestCtx("POST", "/test", map[string]string{
|
||
"Content-Length": "14",
|
||
}, []byte("test body data"))
|
||
|
||
coro, err := engine.NewCoroutine(reqCtx)
|
||
require.NoError(t, err)
|
||
defer coro.Close()
|
||
|
||
err = coro.SetupSandbox()
|
||
require.NoError(t, err)
|
||
|
||
// read_body 应该成功执行
|
||
err = coro.Execute(`
|
||
ngx.req.read_body()
|
||
`)
|
||
assert.NoError(t, err)
|
||
|
||
// 验证请求体仍可访问
|
||
body := reqCtx.Request.Body()
|
||
assert.Equal(t, "test body data", string(body))
|
||
}
|
||
|
||
// TestNgxReqAPIIntegration 测试 ngx.req API 集成场景
|
||
func TestNgxReqAPIIntegration(t *testing.T) {
|
||
engine, err := NewEngine(DefaultConfig())
|
||
require.NoError(t, err)
|
||
defer engine.Close()
|
||
|
||
reqCtx := createTestRequestCtx("POST", "/api/users?limit=10&offset=20", map[string]string{
|
||
"Content-Type": "application/json",
|
||
"X-API-Key": "secret123",
|
||
}, []byte(`{"name":"test"}`))
|
||
|
||
coro, err := engine.NewCoroutine(reqCtx)
|
||
require.NoError(t, err)
|
||
defer coro.Close()
|
||
|
||
err = coro.SetupSandbox()
|
||
require.NoError(t, err)
|
||
|
||
// 复杂场景:获取各种请求信息并修改
|
||
err = coro.Execute(`
|
||
-- 获取请求信息
|
||
local method = ngx.req.get_method()
|
||
local uri = ngx.req.get_uri()
|
||
local args = ngx.req.get_uri_args()
|
||
local headers = ngx.req.get_headers()
|
||
|
||
-- 验证获取的信息
|
||
if method ~= "POST" then
|
||
error("method should be POST")
|
||
end
|
||
|
||
if uri ~= "/api/users" then
|
||
error("uri should be /api/users, got " .. tostring(uri))
|
||
end
|
||
|
||
if args.limit ~= "10" or args.offset ~= "20" then
|
||
error("args incorrect")
|
||
end
|
||
|
||
-- 注意:fasthttp 会标准化 header 名称,所以需要使用实际的 key
|
||
if headers["Content-Type"] ~= "application/json" and headers["content-type"] ~= "application/json" then
|
||
error("Content-Type header incorrect: " .. tostring(headers["Content-Type"]))
|
||
end
|
||
|
||
-- 修改请求
|
||
ngx.req.set_header("X-Request-ID", "req-12345")
|
||
ngx.req.set_uri("/api/v2/users")
|
||
`)
|
||
assert.NoError(t, err)
|
||
|
||
// 在 Go 层验证修改
|
||
assert.Equal(t, "/api/v2/users", string(reqCtx.URI().Path()))
|
||
assert.Equal(t, "req-12345", string(reqCtx.Request.Header.Peek("X-Request-ID")))
|
||
}
|
||
|
||
// TestNgxReqMetrics 测试 ngx.req API 性能指标
|
||
func TestNgxReqMetrics(t *testing.T) {
|
||
reqCtx := createTestRequestCtx("GET", "/test?a=1&b=2", nil, nil)
|
||
api := newNgxReqAPI(reqCtx)
|
||
|
||
L := glua.NewState()
|
||
defer L.Close()
|
||
|
||
// 创建 ngx 表
|
||
ngx := L.NewTable()
|
||
|
||
// 注册 API
|
||
RegisterNgxReqAPI(L, api, ngx)
|
||
|
||
// 将 ngx 设置到全局
|
||
L.SetGlobal("ngx", ngx)
|
||
|
||
// 调用各种 API
|
||
L.DoString(`
|
||
ngx.req.get_method()
|
||
ngx.req.get_uri()
|
||
ngx.req.get_uri_args()
|
||
`)
|
||
|
||
// 验证指标
|
||
metrics := api.GetMetrics()
|
||
assert.Greater(t, metrics.DirectCallCount, uint64(0), "应该有直接层调用")
|
||
assert.Greater(t, metrics.CompatibleCallCount, uint64(0), "应该有兼容层调用")
|
||
|
||
// 验证平均延迟
|
||
directAvg := api.GetDirectLayerAvgNs()
|
||
compatibleAvg := api.GetCompatibleLayerAvgNs()
|
||
assert.GreaterOrEqual(t, directAvg, float64(0))
|
||
assert.GreaterOrEqual(t, compatibleAvg, float64(0))
|
||
|
||
// 验证性能比率
|
||
ratio := api.GetPerformanceRatio()
|
||
assert.GreaterOrEqual(t, ratio, float64(0))
|
||
|
||
// 重置指标
|
||
api.ResetMetrics()
|
||
metrics = api.GetMetrics()
|
||
assert.Equal(t, uint64(0), metrics.DirectCallCount)
|
||
assert.Equal(t, uint64(0), metrics.CompatibleCallCount)
|
||
}
|
||
|
||
// TestNgxReqGetHeadersWithMaxHeaders 测试 ngx.req.get_headers(max_headers)
|
||
func TestNgxReqGetHeadersWithMaxHeaders(t *testing.T) {
|
||
engine, err := NewEngine(DefaultConfig())
|
||
require.NoError(t, err)
|
||
defer engine.Close()
|
||
|
||
// 创建带有多个头的请求
|
||
reqCtx := &fasthttp.RequestCtx{}
|
||
reqCtx.Request.Header.SetMethod("GET")
|
||
reqCtx.Request.SetRequestURI("/test")
|
||
reqCtx.Request.Header.Set("Header1", "value1")
|
||
reqCtx.Request.Header.Set("Header2", "value2")
|
||
reqCtx.Request.Header.Set("Header3", "value3")
|
||
|
||
coro, err := engine.NewCoroutine(reqCtx)
|
||
require.NoError(t, err)
|
||
defer coro.Close()
|
||
|
||
err = coro.SetupSandbox()
|
||
require.NoError(t, err)
|
||
|
||
// 测试限制头数
|
||
err = coro.Execute(`
|
||
local headers = ngx.req.get_headers(2)
|
||
local count = 0
|
||
for k, v in pairs(headers) do
|
||
count = count + 1
|
||
end
|
||
-- 应该最多返回 2 个头
|
||
if count > 2 then
|
||
error("expected at most 2 headers, got " .. count)
|
||
end
|
||
`)
|
||
assert.NoError(t, err)
|
||
}
|
||
|
||
// TestNgxReqSetURIArgsWithArray 测试 ngx.req.set_uri_args() 使用数组值
|
||
func TestNgxReqSetURIArgsWithArray(t *testing.T) {
|
||
engine, err := NewEngine(DefaultConfig())
|
||
require.NoError(t, err)
|
||
defer engine.Close()
|
||
|
||
reqCtx := createTestRequestCtx("GET", "/test", nil, nil)
|
||
|
||
coro, err := engine.NewCoroutine(reqCtx)
|
||
require.NoError(t, err)
|
||
defer coro.Close()
|
||
|
||
err = coro.SetupSandbox()
|
||
require.NoError(t, err)
|
||
|
||
// 测试使用包含数组的 table
|
||
err = coro.Execute(`
|
||
ngx.req.set_uri_args({ tags = { "a", "b", "c" }, page = 1 })
|
||
`)
|
||
assert.NoError(t, err)
|
||
|
||
queryStr := string(reqCtx.URI().QueryString())
|
||
assert.Contains(t, queryStr, "tags=a")
|
||
assert.Contains(t, queryStr, "tags=b")
|
||
assert.Contains(t, queryStr, "tags=c")
|
||
assert.Contains(t, queryStr, "page=1")
|
||
}
|