diff --git a/internal/lua/api_log.go b/internal/lua/api_log.go new file mode 100644 index 0000000..1a9f7f1 --- /dev/null +++ b/internal/lua/api_log.go @@ -0,0 +1,340 @@ +// Package lua 提供 ngx.log 和输出控制 API 实现 +package lua + +import ( + "strconv" + "strings" + + "github.com/rs/zerolog" + "github.com/valyala/fasthttp" + glua "github.com/yuin/gopher-lua" +) + +// 日志级别常量(与 OpenResty/ngx_lua 兼容) +const ( + LogStderr = 0 + LogEmerg = 1 + LogAlert = 2 + LogCrit = 3 + LogErr = 4 + LogWarn = 5 + LogNotice = 6 + LogInfo = 7 + LogDebug = 8 +) + +// HTTP 状态码常量 +const ( + HTTPContinue = 100 + HTTPSwitchingProtocols = 101 + HTTPOK = 200 + HTTPCreated = 201 + HTTPAccepted = 202 + HTTPNoContent = 204 + HTTPPartialContent = 206 + HTTPMovedPermanently = 301 + HTTPFound = 302 + HTTPSeeOther = 303 + HTTPNotModified = 304 + HTTPTemporaryRedirect = 307 + HTTPPermanentRedirect = 308 + HTTPBadRequest = 400 + HTTPUnauthorized = 401 + HTTPForbidden = 403 + HTTPNotFound = 404 + HTTPMethodNotAllowed = 405 + HTTPRequestTimeout = 408 + HTTPConflict = 409 + HTTPGone = 410 + HTTPLengthRequired = 411 + HTTPPayloadTooLarge = 413 + HTTPURITooLong = 414 + HTTPUnsupportedMedia = 415 + HTTPRangeNotSatisfiable = 416 + HTTPTooManyRequests = 429 + HTTPInternalServerError = 500 + HTTPNotImplemented = 501 + HTTPBadGateway = 502 + HTTPServiceUnavailable = 503 + HTTPGatewayTimeout = 504 + HTTPHTTPVersionNotSupported = 505 +) + +// ngxLogAPI ngx.log 和输出控制 API 实现 +type ngxLogAPI struct { + // 请求上下文 + ctx *fasthttp.RequestCtx + + // Lua 上下文(用于访问输出缓冲等) + luaCtx *LuaContext + + // 日志记录器 + logger *zerolog.Logger +} + +// newNgxLogAPI 创建 ngx.log API 实例 +func newNgxLogAPI(ctx *fasthttp.RequestCtx, luaCtx *LuaContext, logger *zerolog.Logger) *ngxLogAPI { + return &ngxLogAPI{ + ctx: ctx, + luaCtx: luaCtx, + logger: logger, + } +} + +// RegisterNgxLogAPI 在 Lua 状态机中注册 ngx.log 和输出控制 API +func RegisterNgxLogAPI(L *glua.LState, api *ngxLogAPI) { + // 获取或创建 ngx 表 + var ngx *glua.LTable + existingNgx := L.GetGlobal("ngx") + if existingNgx != nil && existingNgx.Type() == glua.LTTable { + ngx = existingNgx.(*glua.LTable) + } else { + ngx = L.NewTable() + } + + // 注册日志级别常量 + ngx.RawSetString("STDERR", glua.LNumber(LogStderr)) + ngx.RawSetString("EMERG", glua.LNumber(LogEmerg)) + ngx.RawSetString("ALERT", glua.LNumber(LogAlert)) + ngx.RawSetString("CRIT", glua.LNumber(LogCrit)) + ngx.RawSetString("ERR", glua.LNumber(LogErr)) + ngx.RawSetString("WARN", glua.LNumber(LogWarn)) + ngx.RawSetString("NOTICE", glua.LNumber(LogNotice)) + ngx.RawSetString("INFO", glua.LNumber(LogInfo)) + ngx.RawSetString("DEBUG", glua.LNumber(LogDebug)) + + // 注册 HTTP 状态码常量 + ngx.RawSetString("HTTP_CONTINUE", glua.LNumber(HTTPContinue)) + ngx.RawSetString("HTTP_SWITCHING_PROTOCOLS", glua.LNumber(HTTPSwitchingProtocols)) + ngx.RawSetString("HTTP_OK", glua.LNumber(HTTPOK)) + ngx.RawSetString("HTTP_CREATED", glua.LNumber(HTTPCreated)) + ngx.RawSetString("HTTP_ACCEPTED", glua.LNumber(HTTPAccepted)) + ngx.RawSetString("HTTP_NO_CONTENT", glua.LNumber(HTTPNoContent)) + ngx.RawSetString("HTTP_PARTIAL_CONTENT", glua.LNumber(HTTPPartialContent)) + ngx.RawSetString("HTTP_MOVED_PERMANENTLY", glua.LNumber(HTTPMovedPermanently)) + ngx.RawSetString("HTTP_FOUND", glua.LNumber(HTTPFound)) + ngx.RawSetString("HTTP_SEE_OTHER", glua.LNumber(HTTPSeeOther)) + ngx.RawSetString("HTTP_NOT_MODIFIED", glua.LNumber(HTTPNotModified)) + ngx.RawSetString("HTTP_TEMPORARY_REDIRECT", glua.LNumber(HTTPTemporaryRedirect)) + ngx.RawSetString("HTTP_PERMANENT_REDIRECT", glua.LNumber(HTTPPermanentRedirect)) + ngx.RawSetString("HTTP_BAD_REQUEST", glua.LNumber(HTTPBadRequest)) + ngx.RawSetString("HTTP_UNAUTHORIZED", glua.LNumber(HTTPUnauthorized)) + ngx.RawSetString("HTTP_FORBIDDEN", glua.LNumber(HTTPForbidden)) + ngx.RawSetString("HTTP_NOT_FOUND", glua.LNumber(HTTPNotFound)) + ngx.RawSetString("HTTP_METHOD_NOT_ALLOWED", glua.LNumber(HTTPMethodNotAllowed)) + ngx.RawSetString("HTTP_REQUEST_TIMEOUT", glua.LNumber(HTTPRequestTimeout)) + ngx.RawSetString("HTTP_CONFLICT", glua.LNumber(HTTPConflict)) + ngx.RawSetString("HTTP_GONE", glua.LNumber(HTTPGone)) + ngx.RawSetString("HTTP_LENGTH_REQUIRED", glua.LNumber(HTTPLengthRequired)) + ngx.RawSetString("HTTP_PAYLOAD_TOO_LARGE", glua.LNumber(HTTPPayloadTooLarge)) + ngx.RawSetString("HTTP_URI_TOO_LONG", glua.LNumber(HTTPURITooLong)) + ngx.RawSetString("HTTP_UNSUPPORTED_MEDIA_TYPE", glua.LNumber(HTTPUnsupportedMedia)) + ngx.RawSetString("HTTP_RANGE_NOT_SATISFIABLE", glua.LNumber(HTTPRangeNotSatisfiable)) + ngx.RawSetString("HTTP_TOO_MANY_REQUESTS", glua.LNumber(HTTPTooManyRequests)) + ngx.RawSetString("HTTP_INTERNAL_SERVER_ERROR", glua.LNumber(HTTPInternalServerError)) + ngx.RawSetString("HTTP_NOT_IMPLEMENTED", glua.LNumber(HTTPNotImplemented)) + ngx.RawSetString("HTTP_BAD_GATEWAY", glua.LNumber(HTTPBadGateway)) + ngx.RawSetString("HTTP_SERVICE_UNAVAILABLE", glua.LNumber(HTTPServiceUnavailable)) + ngx.RawSetString("HTTP_GATEWAY_TIMEOUT", glua.LNumber(HTTPGatewayTimeout)) + ngx.RawSetString("HTTP_VERSION_NOT_SUPPORTED", glua.LNumber(HTTPHTTPVersionNotSupported)) + + // 特殊常量 + ngx.RawSetString("ERROR", glua.LNumber(-1)) + ngx.RawSetString("OK", glua.LNumber(0)) + ngx.RawSetString("AGAIN", glua.LNumber(-2)) + ngx.RawSetString("DONE", glua.LNumber(-4)) + ngx.RawSetString("DECLINED", glua.LNumber(-5)) + + // 注册 ngx.log 函数 + ngx.RawSetString("log", L.NewFunction(api.luaLog)) + + // 注册输出控制函数 + ngx.RawSetString("say", L.NewFunction(api.luaSay)) + ngx.RawSetString("print", L.NewFunction(api.luaPrint)) + ngx.RawSetString("flush", L.NewFunction(api.luaFlush)) + ngx.RawSetString("exit", L.NewFunction(api.luaExit)) + ngx.RawSetString("redirect", L.NewFunction(api.luaRedirect)) + + // 注册 ngx 全局变量 + L.SetGlobal("ngx", ngx) +} + +// luaLog 实现 ngx.log(level, ...) - 日志输出 +// Lua 调用: ngx.log(ngx.ERR, "error message") +// 返回: 无 +func (api *ngxLogAPI) luaLog(L *glua.LState) int { + // 获取日志级别 + level := L.CheckInt(1) + + // 收集所有参数并拼接 + var parts []string + n := L.GetTop() + for i := 2; i <= n; i++ { + parts = append(parts, L.ToString(i)) + } + msg := strings.Join(parts, " ") + + // 根据级别映射到 zerolog + if api.logger != nil { + switch level { + case LogEmerg, LogAlert, LogCrit: + api.logger.Fatal().Msg(msg) + case LogErr: + api.logger.Error().Msg(msg) + case LogWarn: + api.logger.Warn().Msg(msg) + case LogNotice: + api.logger.Info().Msg(msg) + case LogInfo: + api.logger.Info().Msg(msg) + case LogDebug: + api.logger.Debug().Msg(msg) + default: + api.logger.Info().Msg(msg) + } + } + + return 0 +} + +// luaSay 实现 ngx.say(...) - 输出内容并附加换行符 +// Lua 调用: ngx.say("hello", "world") +// 返回: 无 +func (api *ngxLogAPI) luaSay(L *glua.LState) int { + // 收集所有参数 + var parts []string + n := L.GetTop() + for i := 1; i <= n; i++ { + parts = append(parts, L.ToString(i)) + } + msg := strings.Join(parts, "") + "\n" + + // 写入到 LuaContext 的输出缓冲 + if api.luaCtx != nil { + api.luaCtx.Write([]byte(msg)) + } else if api.ctx != nil { + // 直接写入响应 + _, _ = api.ctx.Write([]byte(msg)) + } + + return 0 +} + +// luaPrint 实现 ngx.print(...) - 输出内容不附加换行符 +// Lua 调用: ngx.print("hello", "world") +// 返回: 无 +func (api *ngxLogAPI) luaPrint(L *glua.LState) int { + // 收集所有参数 + var parts []string + n := L.GetTop() + for i := 1; i <= n; i++ { + parts = append(parts, L.ToString(i)) + } + msg := strings.Join(parts, "") + + // 写入到 LuaContext 的输出缓冲 + if api.luaCtx != nil { + api.luaCtx.Write([]byte(msg)) + } else if api.ctx != nil { + // 直接写入响应 + _, _ = api.ctx.Write([]byte(msg)) + } + + return 0 +} + +// luaFlush 实现 ngx.flush(wait?) - 刷新输出缓冲区 +// Lua 调用: ngx.flush() 或 ngx.flush(true) +// 返回: 1 (boolean,表示是否成功) +func (api *ngxLogAPI) luaFlush(L *glua.LState) int { + // 可选的 wait 参数 + wait := false + if L.GetTop() >= 1 { + wait = L.ToBool(1) + } + + // 刷新输出缓冲 + if api.luaCtx != nil { + api.luaCtx.FlushOutput() + } + + // fasthttp 没有显式的 flush 方法,数据会自动发送 + // wait 参数在此实现中被忽略(阻塞式 flush) + _ = wait + + L.Push(glua.LTrue) + return 1 +} + +// luaExit 实现 ngx.exit(status) - 结束请求处理 +// Lua 调用: ngx.exit(ngx.HTTP_OK) 或 ngx.exit(200) +// 返回: 无(抛出错误以终止执行) +func (api *ngxLogAPI) luaExit(L *glua.LState) int { + status := L.CheckInt(1) + + // 设置退出状态 + if api.luaCtx != nil { + api.luaCtx.Exit(status) + } else if api.ctx != nil { + api.ctx.SetStatusCode(status) + } + + // 抛出错误以终止 Lua 执行 + L.RaiseError("%s", "ngx.exit: "+strconv.Itoa(status)) + return 0 +} + +// luaRedirect 实现 ngx.redirect(uri, status?) - HTTP 重定向 +// Lua 调用: ngx.redirect("/new/path") 或 ngx.redirect("/new/path", 301) +// 返回: 无(抛出错误以终止执行) +func (api *ngxLogAPI) luaRedirect(L *glua.LState) int { + uri := L.CheckString(1) + + // 默认状态码为 302 (HTTPFound) + status := HTTPFound + if L.GetTop() >= 2 { + status = L.CheckInt(2) + } + + // 验证重定向状态码 + if status != HTTPMovedPermanently && + status != HTTPFound && + status != HTTPSeeOther && + status != HTTPTemporaryRedirect && + status != HTTPPermanentRedirect { + L.ArgError(2, "invalid redirect status code") + return 0 + } + + // 设置重定向头 + if api.ctx != nil { + api.ctx.Response.Header.Set("Location", uri) + api.ctx.SetStatusCode(status) + } + + if api.luaCtx != nil { + api.luaCtx.Exited = true + } + + // 抛出错误以终止 Lua 执行 + L.RaiseError("%s", "ngx.redirect: "+uri) + return 0 +} + +// LogLevelToZerolog 将 ngx 日志级别转换为 zerolog 级别 +func LogLevelToZerolog(level int) zerolog.Level { + switch level { + case LogEmerg, LogAlert, LogCrit: + return zerolog.FatalLevel + case LogErr: + return zerolog.ErrorLevel + case LogWarn: + return zerolog.WarnLevel + case LogNotice, LogInfo: + return zerolog.InfoLevel + case LogDebug: + return zerolog.DebugLevel + default: + return zerolog.InfoLevel + } +} diff --git a/internal/lua/api_log_test.go b/internal/lua/api_log_test.go new file mode 100644 index 0000000..7daa07b --- /dev/null +++ b/internal/lua/api_log_test.go @@ -0,0 +1,395 @@ +// 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" +) + +// mockRequestCtxForLog 创建模拟的 RequestCtx +func mockRequestCtxForLog() *fasthttp.RequestCtx { + ctx := &fasthttp.RequestCtx{} + return ctx +} + +// 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") +}