lolly/internal/lua/api_var_test.go
xfy f123018f2d feat(lua): 增强变量 API 支持数值类型返回和测试覆盖
- 新增 getVariableLua 方法返回 Lua 类型而非字符串
- request_length 等变量返回数值类型而非字符串
- luaVarNewIndex 支持 nil 值转换为空字符串
- 添加 api_var 全面的单元测试覆盖
- 添加 api_ctx 上下文 API 测试
- 添加 api_socket_tcp TCP socket 测试

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-16 18:12:16 +08:00

679 lines
17 KiB
Go
Raw Permalink 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.

// Package lua 提供 ngx.var API 测试
package lua
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/valyala/fasthttp"
)
// TestNgxVarAPI 测试 ngx.var API 基础功能
func TestNgxVarAPI(t *testing.T) {
// 创建 fasthttp 请求上下文
var req fasthttp.Request
req.Header.SetMethod("GET")
req.Header.SetRequestURI("/test/path?foo=bar&baz=qux")
req.Header.Set("Host", "example.com")
req.Header.Set("User-Agent", "TestAgent")
ctx := &fasthttp.RequestCtx{}
ctx.Init(&req, nil, nil)
engine, err := NewEngine(DefaultConfig())
require.NoError(t, err)
defer engine.Close()
// 创建协程
coro, err := engine.NewCoroutine(ctx)
require.NoError(t, err)
defer coro.Close()
// 设置沙箱(这会注册 ngx API
err = coro.SetupSandbox()
require.NoError(t, err)
// 测试 ngx.var 存在
err = coro.Execute(`
if type(ngx) ~= "table" then
error("ngx is not a table")
end
if type(ngx.var) ~= "table" then
error("ngx.var is not a table")
end
`)
assert.NoError(t, err)
}
// TestNgxVarReadBuiltin 测试读取内置变量
func TestNgxVarReadBuiltin(t *testing.T) {
// 创建 fasthttp 请求上下文
var req fasthttp.Request
req.Header.SetMethod("POST")
req.Header.SetRequestURI("/api/test?name=value")
req.Header.Set("Host", "test.example.com")
req.Header.Set("User-Agent", "Mozilla/5.0")
req.Header.Set("Accept", "application/json")
ctx := &fasthttp.RequestCtx{}
ctx.Init(&req, nil, nil)
engine, err := NewEngine(DefaultConfig())
require.NoError(t, err)
defer engine.Close()
coro, err := engine.NewCoroutine(ctx)
require.NoError(t, err)
defer coro.Close()
err = coro.SetupSandbox()
require.NoError(t, err)
// 测试读取内置变量
err = coro.Execute(`
local method = ngx.var.request_method
if method ~= "POST" then
error("request_method should be POST, got: " .. tostring(method))
end
local uri = ngx.var.uri
if uri ~= "/api/test" then
error("uri should be /api/test, got: " .. tostring(uri))
end
local host = ngx.var.http_host
if host ~= "test.example.com" then
error("http_host should be test.example.com, got: " .. tostring(host))
end
local userAgent = ngx.var.http_user_agent
if userAgent ~= "Mozilla/5.0" then
error("http_user_agent should be Mozilla/5.0, got: " .. tostring(userAgent))
end
`)
assert.NoError(t, err)
}
// TestNgxVarReadQueryArgs 测试读取查询参数
func TestNgxVarReadQueryArgs(t *testing.T) {
var req fasthttp.Request
req.Header.SetMethod("GET")
req.Header.SetRequestURI("/search?q=lua&page=1")
ctx := &fasthttp.RequestCtx{}
ctx.Init(&req, nil, nil)
engine, err := NewEngine(DefaultConfig())
require.NoError(t, err)
defer engine.Close()
coro, err := engine.NewCoroutine(ctx)
require.NoError(t, err)
defer coro.Close()
err = coro.SetupSandbox()
require.NoError(t, err)
// 测试读取查询参数
err = coro.Execute(`
local q = ngx.var.arg_q
if q ~= "lua" then
error("arg_q should be 'lua', got: " .. tostring(q))
end
local page = ngx.var.arg_page
if page ~= "1" then
error("arg_page should be '1', got: " .. tostring(page))
end
local queryString = ngx.var.query_string
if type(queryString) ~= "string" then
error("query_string should be a string")
end
`)
assert.NoError(t, err)
}
// TestNgxVarWriteCustom 测试设置自定义变量
func TestNgxVarWriteCustom(t *testing.T) {
var req fasthttp.Request
req.Header.SetMethod("GET")
req.Header.SetRequestURI("/test")
ctx := &fasthttp.RequestCtx{}
ctx.Init(&req, nil, nil)
engine, err := NewEngine(DefaultConfig())
require.NoError(t, err)
defer engine.Close()
coro, err := engine.NewCoroutine(ctx)
require.NoError(t, err)
defer coro.Close()
err = coro.SetupSandbox()
require.NoError(t, err)
// 测试设置和读取自定义变量
err = coro.Execute(`
-- 设置自定义变量
ngx.var.my_custom_var = "hello world"
ngx.var.another_var = "12345"
-- 读取自定义变量
local val1 = ngx.var.my_custom_var
if val1 ~= "hello world" then
error("my_custom_var should be 'hello world', got: " .. tostring(val1))
end
local val2 = ngx.var.another_var
if val2 ~= "12345" then
error("another_var should be '12345', got: " .. tostring(val2))
end
-- 覆盖已存在的变量
ngx.var.my_custom_var = "updated"
local val3 = ngx.var.my_custom_var
if val3 ~= "updated" then
error("my_custom_var should be 'updated', got: " .. tostring(val3))
end
`)
assert.NoError(t, err)
}
// TestNgxVarIndexAccess 测试索引访问方式 ngx.var[key]
func TestNgxVarIndexAccess(t *testing.T) {
var req fasthttp.Request
req.Header.SetMethod("GET")
req.Header.SetRequestURI("/test")
ctx := &fasthttp.RequestCtx{}
ctx.Init(&req, nil, nil)
engine, err := NewEngine(DefaultConfig())
require.NoError(t, err)
defer engine.Close()
coro, err := engine.NewCoroutine(ctx)
require.NoError(t, err)
defer coro.Close()
err = coro.SetupSandbox()
require.NoError(t, err)
// 测试索引访问
err = coro.Execute(`
-- 使用索引方式设置变量
ngx.var["dynamic_key"] = "dynamic_value"
-- 使用索引方式读取变量
local val = ngx.var["dynamic_key"]
if val ~= "dynamic_value" then
error("dynamic_key should be 'dynamic_value', got: " .. tostring(val))
end
-- 混合访问方式
ngx.var.mixed = "mixed_value"
local mixed = ngx.var["mixed"]
if mixed ~= "mixed_value" then
error("mixed should be 'mixed_value', got: " .. tostring(mixed))
end
`)
assert.NoError(t, err)
}
// TestNgxVarNilRequestCtx 测试无请求上下文的情况
func TestNgxVarNilRequestCtx(t *testing.T) {
engine, err := NewEngine(DefaultConfig())
require.NoError(t, err)
defer engine.Close()
// 创建没有请求上下文的协程
coro, err := engine.NewCoroutine(nil)
require.NoError(t, err)
defer coro.Close()
err = coro.SetupSandbox()
require.NoError(t, err)
// 测试在无请求上下文时访问 ngx.var
err = coro.Execute(`
-- 应该返回空字符串或 nil
local method = ngx.var.request_method
-- 可以接受空字符串或 nil
-- 但自定义变量仍然可以设置
ngx.var.test = "value"
local val = ngx.var.test
if val ~= "value" then
error("custom var should be settable even without request ctx")
end
`)
assert.NoError(t, err)
}
// TestNgxVarUndefined 测试未定义变量
func TestNgxVarUndefined(t *testing.T) {
var req fasthttp.Request
req.Header.SetMethod("GET")
req.Header.SetRequestURI("/test")
ctx := &fasthttp.RequestCtx{}
ctx.Init(&req, nil, nil)
engine, err := NewEngine(DefaultConfig())
require.NoError(t, err)
defer engine.Close()
coro, err := engine.NewCoroutine(ctx)
require.NoError(t, err)
defer coro.Close()
err = coro.SetupSandbox()
require.NoError(t, err)
// 测试读取未定义变量
err = coro.Execute(`
local undefined = ngx.var.undefined_var_name
if undefined ~= nil then
error("undefined var should be nil, got: " .. tostring(undefined))
end
`)
assert.NoError(t, err)
}
// TestNgxVarAdditionalBuiltinVars 测试其他内置变量
func TestNgxVarAdditionalBuiltinVars(t *testing.T) {
var req fasthttp.Request
req.Header.SetMethod("DELETE")
req.Header.SetRequestURI("/api/users?id=123&name=test")
req.Header.Set("Host", "api.example.com")
req.Header.Set("User-Agent", "TestClient/1.0")
req.Header.Set("Accept", "application/json")
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer token123")
ctx := &fasthttp.RequestCtx{}
ctx.Init(&req, nil, nil)
engine, err := NewEngine(DefaultConfig())
require.NoError(t, err)
defer engine.Close()
coro, err := engine.NewCoroutine(ctx)
require.NoError(t, err)
defer coro.Close()
err = coro.SetupSandbox()
require.NoError(t, err)
// 测试其他内置变量
err = coro.Execute(`
-- URI 相关变量
local request_uri = ngx.var.request_uri
if request_uri ~= "/api/users?id=123&name=test" then
error("request_uri mismatch, got: " .. tostring(request_uri))
end
local uri = ngx.var.uri
if uri ~= "/api/users" then
error("urishould be /api/users, got: " .. tostring(uri))
end
local document_uri = ngx.var.document_uri
if document_uri ~= "/api/users" then
error("document_uri should be /api/users, got: " .. tostring(document_uri))
end
-- 查询字符串
local query_string = ngx.var.query_string
if query_string ~= "id=123&name=test" then
error("query_string mismatch, got: " .. tostring(query_string))
end
local args = ngx.var.args
if args ~= "id=123&name=test" then
error("args should match query_string, got: " .. tostring(args))
end
-- 请求头
local accept = ngx.var.http_accept
if accept ~= "application/json" then
error("http_accept mismatch, got: " .. tostring(accept))
end
local contentType = ngx.var.http_content_type
if contentType ~= "application/json" then
error("http_content_type mismatch, got: " .. tostring(contentType))
end
local authorization = ngx.var.http_authorization
if authorization ~= "Bearer token123" then
error("http_authorization mismatch, got: " .. tostring(authorization))
end
-- 内置变量 map
local vars = {
"request_method", "request_uri", "uri", "document_uri",
"query_string", "args", "http_host", "http_user_agent",
"http_accept", "http_content_type"
}
for _, v in ipairs(vars) do
local val = ngx.var[v]
if type(val) ~= "string" then
error("var " .. v .. " should be string, got: " .. type(val))
end
end
`)
assert.NoError(t, err)
}
// TestNgxVarDynamicArgsAccess 测试动态参数访问
func TestNgxVarDynamicArgsAccess(t *testing.T) {
var req fasthttp.Request
req.Header.SetMethod("GET")
req.Header.SetRequestURI("/search?keyword=lua&category=programming&limit=10")
ctx := &fasthttp.RequestCtx{}
ctx.Init(&req, nil, nil)
engine, err := NewEngine(DefaultConfig())
require.NoError(t, err)
defer engine.Close()
coro, err := engine.NewCoroutine(ctx)
require.NoError(t, err)
defer coro.Close()
err = coro.SetupSandbox()
require.NoError(t, err)
// 测试动态参数访问
err = coro.Execute(`
-- 直接通过 arg_ 访问
local keyword = ngx.var.arg_keyword
if keyword ~= "lua" then
error("arg_keyword should be 'lua', got: " .. tostring(keyword))
end
local category = ngx.var.arg_category
if category ~= "programming" then
error("arg_category should be 'programming', got: " .. tostring(category))
end
local limit = ngx.var.arg_limit
if limit ~= "10" then
error("arg_limit should be '10', got: " .. tostring(limit))
end
-- 使用动态键访问
local keys = {"keyword", "category", "limit"}
for i, k in ipairs(keys) do
local val = ngx.var["arg_" .. k]
if type(val) ~= "string" then
error("dynamic arg access should return string")
end
end
`)
assert.NoError(t, err)
}
// TestNgxVarGoAPI 测试 Go 层 API 调用
func TestNgxVarGoAPI(t *testing.T) {
var req fasthttp.Request
req.Header.SetMethod("GET")
req.Header.SetRequestURI("/test")
ctx := &fasthttp.RequestCtx{}
ctx.Init(&req, nil, nil)
// 直接创建 API 实例并测试 Go 层 API
api := newNgxVarAPI(ctx)
require.NotNil(t, api)
// 测试 SetVariable
api.SetVariable("go_set_var", "value_from_go")
value, ok := api.GetVariable("go_set_var")
assert.True(t, ok)
assert.Equal(t, "value_from_go", value)
// 测试 GetVariable 不存在的变量
value, ok = api.GetVariable("nonexistent")
assert.False(t, ok)
assert.Equal(t, "", value)
// 测试覆盖Go 设置Go 读取验证
api.SetVariable("cross_lang", "from_go")
val, ok := api.GetVariable("cross_lang")
assert.True(t, ok)
assert.Equal(t, "from_go", val)
// 测试覆盖:直接设置 storeGo 读取验证
api.store["cross_lang2"] = "from_lua"
value, ok = api.GetVariable("cross_lang2")
assert.True(t, ok)
assert.Equal(t, "from_lua", value)
}
// TestNgxVarRequestMethodAccess 测试各种请求方法
func TestNgxVarRequestMethodAccess(t *testing.T) {
methods := []string{"GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"}
for _, method := range methods {
t.Run(method, func(t *testing.T) {
var req fasthttp.Request
req.Header.SetMethod(method)
req.Header.SetRequestURI("/test")
ctx := &fasthttp.RequestCtx{}
ctx.Init(&req, nil, nil)
engine, err := NewEngine(DefaultConfig())
require.NoError(t, err)
defer engine.Close()
coro, err := engine.NewCoroutine(ctx)
require.NoError(t, err)
defer coro.Close()
err = coro.SetupSandbox()
require.NoError(t, err)
err = coro.Execute(`
local method = ngx.var.request_method
if method ~= "` + method + `" then
error("request_method should be '` + method + `', got: " .. tostring(method))
end
`)
assert.NoError(t, err)
})
}
}
// TestNgxVarMixedAccessPatterns 测试混合访问模式
func TestNgxVarMixedAccessPatterns(t *testing.T) {
var req fasthttp.Request
req.Header.SetMethod("GET")
req.Header.SetRequestURI("/test")
ctx := &fasthttp.RequestCtx{}
ctx.Init(&req, nil, nil)
engine, err := NewEngine(DefaultConfig())
require.NoError(t, err)
defer engine.Close()
coro, err := engine.NewCoroutine(ctx)
require.NoError(t, err)
defer coro.Close()
err = coro.SetupSandbox()
require.NoError(t, err)
// 测试混合访问模式
err = coro.Execute(`
-- 点号访问设置
ngx.var.test1 = "value1"
-- 索引访问读取
local val1 = ngx.var["test1"]
if val1 ~= "value1" then
error("mixed access 1 failed")
end
-- 索引访问设置
ngx.var["test2"] = "value2"
-- 点号访问读取
local val2 = ngx.var.test2
if val2 ~= "value2" then
error("mixed access 2 failed")
end
-- 循环访问
for i = 1, 3 do
ngx.var["dynamic_" .. i] = "val_" .. i
end
for i = 1, 3 do
local v = ngx.var["dynamic_" .. i]
if v ~= "val_" .. i then
error("dynamic loop failed for i=" .. i)
end
end
`)
assert.NoError(t, err)
}
// TestNgxVarSpecialHeaders 测试特殊请求头
func TestNgxVarSpecialHeaders(t *testing.T) {
var req fasthttp.Request
req.Header.SetMethod("GET")
req.Header.SetRequestURI("/test")
req.Header.Set("Accept-Encoding", "gzip, deflate, br")
req.Header.Set("Accept-Language", "en-US,en;q=0.9")
req.Header.Set("Connection", "keep-alive")
req.Header.Set("Referer", "https://example.com")
ctx := &fasthttp.RequestCtx{}
ctx.Init(&req, nil, nil)
engine, err := NewEngine(DefaultConfig())
require.NoError(t, err)
defer engine.Close()
coro, err := engine.NewCoroutine(ctx)
require.NoError(t, err)
defer coro.Close()
err = coro.SetupSandbox()
require.NoError(t, err)
err = coro.Execute(`
-- 测试带连字符的头
local acceptEncoding = ngx.var.http_accept_encoding
if acceptEncoding ~= "gzip, deflate, br" then
error("http_accept_encoding mismatch")
end
local acceptLanguage = ngx.var.http_accept_language
if acceptLanguage ~= "en-US,en;q=0.9" then
error("http_accept_language mismatch")
end
local connection = ngx.var.http_connection
if connection ~= "keep-alive" then
error("http_connection mismatch")
end
local referer = ngx.var.http_referer
if referer ~= "https://example.com" then
error("http_referer mismatch")
end
-- 测试也可以通过下划线访问
local enc2 = ngx.var["http_accept_encoding"]
if enc2 ~= acceptEncoding then
error("http_accept_encoding via index mismatch")
end
`)
assert.NoError(t, err)
}
// TestNgxVarEmptyAndNil 测试空值和 nil 处理
func TestNgxVarEmptyAndNil(t *testing.T) {
var req fasthttp.Request
req.Header.SetMethod("GET")
req.Header.SetRequestURI("/")
ctx := &fasthttp.RequestCtx{}
ctx.Init(&req, nil, nil)
engine, err := NewEngine(DefaultConfig())
require.NoError(t, err)
defer engine.Close()
coro, err := engine.NewCoroutine(ctx)
require.NoError(t, err)
defer coro.Close()
err = coro.SetupSandbox()
require.NoError(t, err)
err = coro.Execute(`
-- 未设置的参数应该返回空字符串或 nil
local empty = ngx.var.arg_nonexistent
-- arg_ 对不存在的参数通常返回空字符串
-- 自定义变量设为空字符串
ngx.var.empty_string = ""
local val = ngx.var.empty_string
if val ~= "" then
error("empty_string should be empty")
end
-- 覆盖为空值
ngx.var.test = "value"
ngx.var.test = nil -- Lua 的 nil 在 __newindex 中会被转换
-- 实现中 nil 会被转换为空字符串
`)
assert.NoError(t, err)
}
// TestNgxVarRequestBodyAccess 测试请求体相关变量
func TestNgxVarRequestBodyAccess(t *testing.T) {
var req fasthttp.Request
req.Header.SetMethod("POST")
req.Header.SetRequestURI("/upload")
req.Header.SetContentType("application/octet-stream")
req.SetBody([]byte("test body content"))
ctx := &fasthttp.RequestCtx{}
ctx.Init(&req, nil, nil)
engine, err := NewEngine(DefaultConfig())
require.NoError(t, err)
defer engine.Close()
coro, err := engine.NewCoroutine(ctx)
require.NoError(t, err)
defer coro.Close()
err = coro.SetupSandbox()
require.NoError(t, err)
err = coro.Execute(`
-- 测试请求长度
local length = ngx.var.request_length
if type(length) ~= "number" then
error("request_length should be a number")
end
-- 测试内容类型
local contentType = ngx.var.http_content_type
if contentType ~= "application/octet-stream" then
error("content_type mismatch")
end
`)
assert.NoError(t, err)
}