lolly/internal/lua/api_log_test.go
xfy e646cc5d05 refactor(test): 提取 testutil 包统一测试辅助函数
- 新增 NewRequestCtx 和 NewRequestCtxWithHeader 辅助函数
- 简化各测试文件中 RequestCtx 创建代码
- 减少测试代码重复,提高可维护性

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-13 13:15:20 +08:00

397 lines
10 KiB
Go

// Package lua 提供 ngx.log API 测试
package lua
import (
"bytes"
"testing"
"github.com/rs/zerolog"
"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"
)
// mockRequestCtxForLog 创建模拟的 RequestCtx
func mockRequestCtxForLog() *fasthttp.RequestCtx {
return testutil.NewRequestCtx("GET", "/")
}
// TestNgxLogLevelConstants 测试日志级别常量
func TestNgxLogLevelConstants(t *testing.T) {
// 验证日志级别常量值
assert.Equal(t, 0, LogStderr)
assert.Equal(t, 1, LogEmerg)
assert.Equal(t, 2, LogAlert)
assert.Equal(t, 3, LogCrit)
assert.Equal(t, 4, LogErr)
assert.Equal(t, 5, LogWarn)
assert.Equal(t, 6, LogNotice)
assert.Equal(t, 7, LogInfo)
assert.Equal(t, 8, LogDebug)
}
// TestNgxHTTPStatusConstants 测试 HTTP 状态码常量
func TestNgxHTTPStatusConstants(t *testing.T) {
// 验证常用 HTTP 状态码
assert.Equal(t, 200, HTTPOK)
assert.Equal(t, 201, HTTPCreated)
assert.Equal(t, 301, HTTPMovedPermanently)
assert.Equal(t, 302, HTTPFound)
assert.Equal(t, 303, HTTPSeeOther)
assert.Equal(t, 307, HTTPTemporaryRedirect)
assert.Equal(t, 308, HTTPPermanentRedirect)
assert.Equal(t, 400, HTTPBadRequest)
assert.Equal(t, 404, HTTPNotFound)
assert.Equal(t, 500, HTTPInternalServerError)
}
// TestNgxLogAPIRegistration 测试 API 注册
func TestNgxLogAPIRegistration(t *testing.T) {
engine, err := NewEngine(DefaultConfig())
require.NoError(t, err)
defer engine.Close()
// 创建测试请求上下文
ctx := mockRequestCtxForLog()
luaCtx := NewContext(engine, ctx)
// 创建 zerolog logger
var buf bytes.Buffer
logger := zerolog.New(&buf)
// 创建 API 实例
api := newNgxLogAPI(ctx, luaCtx, &logger)
// 在 Lua 状态机中注册 API
L := engine.L
RegisterNgxLogAPI(L, api)
// 验证 ngx 表已创建
ngx := L.GetGlobal("ngx")
require.NotEqual(t, nil, ngx)
// 验证日志级别常量已注册
ngxTable := ngx.(*glua.LTable)
assert.Equal(t, glua.LNumber(LogStderr), ngxTable.RawGetString("STDERR"))
assert.Equal(t, glua.LNumber(LogEmerg), ngxTable.RawGetString("EMERG"))
assert.Equal(t, glua.LNumber(LogErr), ngxTable.RawGetString("ERR"))
assert.Equal(t, glua.LNumber(LogWarn), ngxTable.RawGetString("WARN"))
assert.Equal(t, glua.LNumber(LogInfo), ngxTable.RawGetString("INFO"))
assert.Equal(t, glua.LNumber(LogDebug), ngxTable.RawGetString("DEBUG"))
// 验证 HTTP 状态码常量已注册
assert.Equal(t, glua.LNumber(HTTPOK), ngxTable.RawGetString("HTTP_OK"))
assert.Equal(t, glua.LNumber(HTTPNotFound), ngxTable.RawGetString("HTTP_NOT_FOUND"))
assert.Equal(t, glua.LNumber(HTTPInternalServerError), ngxTable.RawGetString("HTTP_INTERNAL_SERVER_ERROR"))
// 验证函数已注册
assert.NotEqual(t, glua.LNil, ngxTable.RawGetString("log"))
assert.NotEqual(t, glua.LNil, ngxTable.RawGetString("say"))
assert.NotEqual(t, glua.LNil, ngxTable.RawGetString("print"))
assert.NotEqual(t, glua.LNil, ngxTable.RawGetString("flush"))
assert.NotEqual(t, glua.LNil, ngxTable.RawGetString("exit"))
assert.NotEqual(t, glua.LNil, ngxTable.RawGetString("redirect"))
}
// TestNgxLogAPILog 测试 ngx.log API
func TestNgxLogAPILog(t *testing.T) {
engine, err := NewEngine(DefaultConfig())
require.NoError(t, err)
defer engine.Close()
ctx := mockRequestCtxForLog()
luaCtx := NewContext(engine, ctx)
var buf bytes.Buffer
logger := zerolog.New(&buf)
api := newNgxLogAPI(ctx, luaCtx, &logger)
L := engine.L
RegisterNgxLogAPI(L, api)
// 测试日志输出
err = L.DoString(`
ngx.log(ngx.INFO, "test message")
`)
// 日志调用不应返回错误
assert.NoError(t, err)
// 验证日志内容
logOutput := buf.String()
assert.Contains(t, logOutput, "test message")
}
// TestNgxLogAPISay 测试 ngx.say API
func TestNgxLogAPISay(t *testing.T) {
engine, err := NewEngine(DefaultConfig())
require.NoError(t, err)
defer engine.Close()
ctx := mockRequestCtxForLog()
luaCtx := NewContext(engine, ctx)
var buf bytes.Buffer
logger := zerolog.New(&buf)
api := newNgxLogAPI(ctx, luaCtx, &logger)
L := engine.L
RegisterNgxLogAPI(L, api)
// 测试 say 输出
err = L.DoString(`
ngx.say("Hello, ")
ngx.say("World!")
`)
require.NoError(t, err)
// 验证输出缓冲包含换行符
assert.Equal(t, "Hello, \nWorld!\n", string(luaCtx.OutputBuffer))
}
// TestNgxLogAPIPrint 测试 ngx.print API
func TestNgxLogAPIPrint(t *testing.T) {
engine, err := NewEngine(DefaultConfig())
require.NoError(t, err)
defer engine.Close()
ctx := mockRequestCtxForLog()
luaCtx := NewContext(engine, ctx)
var buf bytes.Buffer
logger := zerolog.New(&buf)
api := newNgxLogAPI(ctx, luaCtx, &logger)
L := engine.L
RegisterNgxLogAPI(L, api)
// 测试 print 输出
err = L.DoString(`
ngx.print("Hello")
ngx.print(", ")
ngx.print("World")
`)
require.NoError(t, err)
// 验证输出缓冲不包含换行符
assert.Equal(t, "Hello, World", string(luaCtx.OutputBuffer))
}
// TestNgxLogAPIFlush 测试 ngx.flush API
func TestNgxLogAPIFlush(t *testing.T) {
engine, err := NewEngine(DefaultConfig())
require.NoError(t, err)
defer engine.Close()
ctx := mockRequestCtxForLog()
luaCtx := NewContext(engine, ctx)
var buf bytes.Buffer
logger := zerolog.New(&buf)
api := newNgxLogAPI(ctx, luaCtx, &logger)
L := engine.L
RegisterNgxLogAPI(L, api)
// 测试 flush
err = L.DoString(`
ngx.print("before flush")
local ok = ngx.flush()
assert(ok == true, "flush should return true")
`)
require.NoError(t, err)
}
// TestNgxLogAPIExit 测试 ngx.exit API
func TestNgxLogAPIExit(t *testing.T) {
engine, err := NewEngine(DefaultConfig())
require.NoError(t, err)
defer engine.Close()
ctx := mockRequestCtxForLog()
luaCtx := NewContext(engine, ctx)
var buf bytes.Buffer
logger := zerolog.New(&buf)
api := newNgxLogAPI(ctx, luaCtx, &logger)
L := engine.L
RegisterNgxLogAPI(L, api)
// 测试 exit - 应该抛出错误
err = L.DoString(`
ngx.exit(ngx.HTTP_OK)
`)
// exit 应该返回错误
assert.Error(t, err)
assert.Contains(t, err.Error(), "ngx.exit")
// 验证状态码已设置
assert.Equal(t, 200, ctx.Response.StatusCode())
}
// TestNgxLogAPIRedirect 测试 ngx.redirect API
func TestNgxLogAPIRedirect(t *testing.T) {
engine, err := NewEngine(DefaultConfig())
require.NoError(t, err)
defer engine.Close()
ctx := mockRequestCtxForLog()
luaCtx := NewContext(engine, ctx)
var buf bytes.Buffer
logger := zerolog.New(&buf)
api := newNgxLogAPI(ctx, luaCtx, &logger)
L := engine.L
RegisterNgxLogAPI(L, api)
// 测试 redirect - 默认状态码 302
err = L.DoString(`
ngx.redirect("/new/path")
`)
// redirect 应该返回错误以终止执行
assert.Error(t, err)
assert.Contains(t, err.Error(), "ngx.redirect")
// 验证重定向头和状态码
assert.Equal(t, 302, ctx.Response.StatusCode())
assert.Equal(t, "/new/path", string(ctx.Response.Header.Peek("Location")))
}
// TestNgxLogAPIRedirectWithStatus 测试带状态码的 redirect
func TestNgxLogAPIRedirectWithStatus(t *testing.T) {
engine, err := NewEngine(DefaultConfig())
require.NoError(t, err)
defer engine.Close()
ctx := mockRequestCtxForLog()
luaCtx := NewContext(engine, ctx)
var buf bytes.Buffer
logger := zerolog.New(&buf)
api := newNgxLogAPI(ctx, luaCtx, &logger)
L := engine.L
RegisterNgxLogAPI(L, api)
// 测试 redirect - 301
err = L.DoString(`
ngx.redirect("/permanent/path", ngx.HTTP_MOVED_PERMANENTLY)
`)
assert.Error(t, err)
// 验证状态码
assert.Equal(t, 301, ctx.Response.StatusCode())
}
// TestNgxLogAPIRedirectInvalidStatus 测试无效状态码的 redirect
func TestNgxLogAPIRedirectInvalidStatus(t *testing.T) {
engine, err := NewEngine(DefaultConfig())
require.NoError(t, err)
defer engine.Close()
ctx := mockRequestCtxForLog()
luaCtx := NewContext(engine, ctx)
var buf bytes.Buffer
logger := zerolog.New(&buf)
api := newNgxLogAPI(ctx, luaCtx, &logger)
L := engine.L
RegisterNgxLogAPI(L, api)
// 测试 redirect - 无效状态码 (400)
err = L.DoString(`
ngx.redirect("/path", 400)
`)
// 应该返回参数错误
assert.Error(t, err)
}
// TestNgxLogAPILogLevels 测试不同日志级别
func TestNgxLogAPILogLevels(t *testing.T) {
testCases := []struct {
level int
expected zerolog.Level
}{
{LogEmerg, zerolog.FatalLevel},
{LogAlert, zerolog.FatalLevel},
{LogCrit, zerolog.FatalLevel},
{LogErr, zerolog.ErrorLevel},
{LogWarn, zerolog.WarnLevel},
{LogNotice, zerolog.InfoLevel},
{LogInfo, zerolog.InfoLevel},
{LogDebug, zerolog.DebugLevel},
{999, zerolog.InfoLevel}, // 未知级别默认为 Info
}
for _, tc := range testCases {
result := LogLevelToZerolog(tc.level)
assert.Equal(t, tc.expected, result, "level %d should map to %v", tc.level, tc.expected)
}
}
// TestNgxLogAPIWithoutLogger 测试无 logger 的情况
func TestNgxLogAPIWithoutLogger(t *testing.T) {
engine, err := NewEngine(DefaultConfig())
require.NoError(t, err)
defer engine.Close()
ctx := mockRequestCtxForLog()
luaCtx := NewContext(engine, ctx)
// 创建无 logger 的 API
api := newNgxLogAPI(ctx, luaCtx, nil)
L := engine.L
RegisterNgxLogAPI(L, api)
// 测试日志 - 不应 panic
err = L.DoString(`
ngx.log(ngx.INFO, "message without logger")
`)
assert.NoError(t, err)
}
// TestNgxLogAPIIntegration 集成测试
func TestNgxLogAPIIntegration(t *testing.T) {
engine, err := NewEngine(DefaultConfig())
require.NoError(t, err)
defer engine.Close()
ctx := mockRequestCtxForLog()
luaCtx := NewContext(engine, ctx)
var buf bytes.Buffer
logger := zerolog.New(&buf)
api := newNgxLogAPI(ctx, luaCtx, &logger)
L := engine.L
RegisterNgxLogAPI(L, api)
// 综合测试
err = L.DoString(`
-- 记录日志
ngx.log(ngx.INFO, "Starting request")
-- 输出内容
ngx.say("Line 1")
ngx.print("Line 2")
ngx.say("")
-- 使用常量
ngx.say("HTTP OK: " .. ngx.HTTP_OK)
ngx.say("HTTP NOT FOUND: " .. ngx.HTTP_NOT_FOUND)
`)
require.NoError(t, err)
// 验证输出
output := string(luaCtx.OutputBuffer)
assert.Contains(t, output, "Line 1")
assert.Contains(t, output, "Line 2")
assert.Contains(t, output, "HTTP OK: 200")
assert.Contains(t, output, "HTTP NOT FOUND: 404")
}