- 新增统一错误码定义和管理 (pkg/errors/codes.go) - 新增全局错误处理器和中间件 (pkg/errors/handler.go, internal/middleware/error_handler.go) - 新增错误上下文管理 (pkg/errors/context.go) - 增强 Panic 恢复中间件 (internal/middleware/recover.go) - 新增完整的单元测试和集成测试 - 新增功能文档 (docs/003-error-handling/) - 新增功能规范 (specs/003-error-handling/) - 更新 CLAUDE.md 和 README.md
259 lines
6.4 KiB
Go
259 lines
6.4 KiB
Go
package errors
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"github.com/gofiber/fiber/v2"
|
|
"github.com/valyala/fasthttp"
|
|
)
|
|
|
|
// TestFromFiberContext 测试从 Fiber Context 提取错误上下文
|
|
func TestFromFiberContext(t *testing.T) {
|
|
app := fiber.New()
|
|
|
|
tests := []struct {
|
|
name string
|
|
setupRequest func(*fasthttp.RequestCtx)
|
|
expectedMethod string
|
|
expectedPath string
|
|
hasRequestID bool
|
|
}{
|
|
{
|
|
name: "GET 请求",
|
|
setupRequest: func(ctx *fasthttp.RequestCtx) {
|
|
ctx.Request.Header.SetMethod("GET")
|
|
ctx.Request.SetRequestURI("/api/v1/users")
|
|
ctx.Request.Header.Set("X-Request-ID", "test-request-id-123")
|
|
},
|
|
expectedMethod: "GET",
|
|
expectedPath: "/api/v1/users",
|
|
hasRequestID: true,
|
|
},
|
|
{
|
|
name: "POST 请求带查询参数",
|
|
setupRequest: func(ctx *fasthttp.RequestCtx) {
|
|
ctx.Request.Header.SetMethod("POST")
|
|
ctx.Request.SetRequestURI("/api/v1/orders?status=pending")
|
|
ctx.Request.Header.Set("X-Request-ID", "post-request-456")
|
|
},
|
|
expectedMethod: "POST",
|
|
expectedPath: "/api/v1/orders",
|
|
hasRequestID: true,
|
|
},
|
|
{
|
|
name: "无 Request ID",
|
|
setupRequest: func(ctx *fasthttp.RequestCtx) {
|
|
ctx.Request.Header.SetMethod("DELETE")
|
|
ctx.Request.SetRequestURI("/api/v1/tasks/123")
|
|
},
|
|
expectedMethod: "DELETE",
|
|
expectedPath: "/api/v1/tasks/123",
|
|
hasRequestID: false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
// 创建 fasthttp 请求上下文
|
|
fctx := &fasthttp.RequestCtx{}
|
|
tt.setupRequest(fctx)
|
|
|
|
// 创建 Fiber 上下文
|
|
c := app.AcquireCtx(fctx)
|
|
defer app.ReleaseCtx(c)
|
|
|
|
// 提取错误上下文
|
|
errCtx := FromFiberContext(c)
|
|
|
|
// 验证方法
|
|
if errCtx.Method != tt.expectedMethod {
|
|
t.Errorf("Method = %q, expected %q", errCtx.Method, tt.expectedMethod)
|
|
}
|
|
|
|
// 验证路径
|
|
if errCtx.Path != tt.expectedPath {
|
|
t.Errorf("Path = %q, expected %q", errCtx.Path, tt.expectedPath)
|
|
}
|
|
|
|
// 验证 Request ID
|
|
if tt.hasRequestID && errCtx.RequestID == "" {
|
|
t.Error("Expected Request ID, but got empty string")
|
|
}
|
|
if !tt.hasRequestID && errCtx.RequestID != "" {
|
|
t.Errorf("Expected no Request ID, but got %q", errCtx.RequestID)
|
|
}
|
|
|
|
// 验证 IP 地址不为空
|
|
if errCtx.IP == "" {
|
|
t.Error("Expected IP address, but got empty string")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestErrorContextToLogFields 测试错误上下文转换为日志字段
|
|
func TestErrorContextToLogFields(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
ctx *ErrorContext
|
|
expectedFields int // 期望的字段数量
|
|
hasQuery bool
|
|
hasUserAgent bool
|
|
hasUserID bool
|
|
}{
|
|
{
|
|
name: "完整的错误上下文",
|
|
ctx: &ErrorContext{
|
|
RequestID: "test-123",
|
|
Method: "POST",
|
|
Path: "/api/v1/users",
|
|
IP: "192.168.1.100",
|
|
Query: "status=active",
|
|
UserAgent: "Mozilla/5.0",
|
|
UserID: "user-456",
|
|
},
|
|
expectedFields: 7, // request_id, method, path, ip, query, user_agent, user_id
|
|
hasQuery: true,
|
|
hasUserAgent: true,
|
|
hasUserID: true,
|
|
},
|
|
{
|
|
name: "无查询参数",
|
|
ctx: &ErrorContext{
|
|
RequestID: "test-456",
|
|
Method: "GET",
|
|
Path: "/api/v1/orders",
|
|
IP: "10.0.0.1",
|
|
Query: "",
|
|
},
|
|
expectedFields: 4, // request_id, method, path, ip
|
|
hasQuery: false,
|
|
hasUserAgent: false,
|
|
hasUserID: false,
|
|
},
|
|
{
|
|
name: "空 Request ID",
|
|
ctx: &ErrorContext{
|
|
RequestID: "",
|
|
Method: "DELETE",
|
|
Path: "/api/v1/tasks/123",
|
|
IP: "127.0.0.1",
|
|
Query: "",
|
|
},
|
|
expectedFields: 4, // request_id (空字符串), method, path, ip
|
|
hasQuery: false,
|
|
hasUserAgent: false,
|
|
hasUserID: false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
fields := tt.ctx.ToLogFields()
|
|
|
|
// 验证字段数量
|
|
if len(fields) != tt.expectedFields {
|
|
t.Errorf("Field count = %d, expected %d", len(fields), tt.expectedFields)
|
|
}
|
|
|
|
// 验证必需字段存在
|
|
if len(fields) < 4 {
|
|
t.Error("Expected at least 4 required fields (request_id, method, path, ip)")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestFromFiberContextWithUserAgent 测试带 User-Agent 的错误上下文提取
|
|
func TestFromFiberContextWithUserAgent(t *testing.T) {
|
|
app := fiber.New()
|
|
|
|
tests := []struct {
|
|
name string
|
|
method string
|
|
path string
|
|
userAgent string
|
|
expectedUserAgent bool
|
|
}{
|
|
{
|
|
name: "有 User-Agent",
|
|
method: "GET",
|
|
path: "/api/v1/users",
|
|
userAgent: "Mozilla/5.0",
|
|
expectedUserAgent: true,
|
|
},
|
|
{
|
|
name: "无 User-Agent",
|
|
method: "GET",
|
|
path: "/api/v1/users/123",
|
|
userAgent: "",
|
|
expectedUserAgent: false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
// 创建 fasthttp 请求上下文
|
|
fctx := &fasthttp.RequestCtx{}
|
|
fctx.Request.Header.SetMethod(tt.method)
|
|
fctx.Request.SetRequestURI(tt.path)
|
|
if tt.userAgent != "" {
|
|
fctx.Request.Header.Set("User-Agent", tt.userAgent)
|
|
}
|
|
|
|
// 创建 Fiber 上下文
|
|
c := app.AcquireCtx(fctx)
|
|
defer app.ReleaseCtx(c)
|
|
|
|
// 提取错误上下文
|
|
errCtx := FromFiberContext(c)
|
|
|
|
// 验证 User-Agent
|
|
if tt.expectedUserAgent && errCtx.UserAgent == "" {
|
|
t.Error("Expected User-Agent, but got empty")
|
|
}
|
|
if !tt.expectedUserAgent && errCtx.UserAgent != "" {
|
|
t.Errorf("Expected no User-Agent, but got %q", errCtx.UserAgent)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// BenchmarkFromFiberContext 基准测试错误上下文提取性能
|
|
func BenchmarkFromFiberContext(b *testing.B) {
|
|
app := fiber.New()
|
|
|
|
// 创建测试请求
|
|
fctx := &fasthttp.RequestCtx{}
|
|
fctx.Request.Header.SetMethod("POST")
|
|
fctx.Request.SetRequestURI("/api/v1/users?status=active&limit=10")
|
|
fctx.Request.Header.Set("X-Request-ID", "benchmark-request-id")
|
|
fctx.Request.SetBodyString(`{"username":"test","email":"test@example.com"}`)
|
|
|
|
c := app.AcquireCtx(fctx)
|
|
defer app.ReleaseCtx(c)
|
|
|
|
b.ResetTimer()
|
|
for i := 0; i < b.N; i++ {
|
|
_ = FromFiberContext(c)
|
|
}
|
|
}
|
|
|
|
// BenchmarkErrorContextToLogFields 基准测试日志字段转换性能
|
|
func BenchmarkErrorContextToLogFields(b *testing.B) {
|
|
ctx := &ErrorContext{
|
|
RequestID: "benchmark-123",
|
|
Method: "POST",
|
|
Path: "/api/v1/users",
|
|
IP: "192.168.1.100",
|
|
Query: "status=active&limit=10",
|
|
UserAgent: "Mozilla/5.0",
|
|
UserID: "user-456",
|
|
}
|
|
|
|
b.ResetTimer()
|
|
for i := 0; i < b.N; i++ {
|
|
_ = ctx.ToLogFields()
|
|
}
|
|
}
|