feat: 实现统一错误处理系统 (003-error-handling)
- 新增统一错误码定义和管理 (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
This commit is contained in:
258
pkg/errors/context_test.go
Normal file
258
pkg/errors/context_test.go
Normal file
@@ -0,0 +1,258 @@
|
||||
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()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user