// 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.GetLStateForTest() defer engine.PutLStateForTest(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.GetLStateForTest() 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.GetLStateForTest() 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.GetLStateForTest() 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.GetLStateForTest() 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.GetLStateForTest() 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.GetLStateForTest() 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.GetLStateForTest() 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.GetLStateForTest() 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.GetLStateForTest() 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.GetLStateForTest() 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") }