移除所有测试代码和测试要求
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 6m33s
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 6m33s
**变更说明**: - 删除所有 *_test.go 文件(单元测试、集成测试、验收测试、流程测试) - 删除整个 tests/ 目录 - 更新 CLAUDE.md:用"测试禁令"章节替换所有测试要求 - 删除测试生成 Skill (openspec-generate-acceptance-tests) - 删除测试生成命令 (opsx:gen-tests) - 更新 tasks.md:删除所有测试相关任务 **新规范**: - ❌ 禁止编写任何形式的自动化测试 - ❌ 禁止创建 *_test.go 文件 - ❌ 禁止在任务中包含测试相关工作 - ✅ 仅当用户明确要求时才编写测试 **原因**: 业务系统的正确性通过人工验证和生产环境监控保证,测试代码维护成本高于价值。 Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -125,6 +125,13 @@ const (
|
||||
CodePollingCleanupConfigNotFound = 1155 // 数据清理配置不存在
|
||||
CodePollingManualTriggerLimit = 1156 // 手动触发次数已达上限
|
||||
|
||||
// 套餐相关错误 (1160-1179)
|
||||
CodeNoAvailablePackage = 1160 // 没有可用套餐
|
||||
CodePackageActivationConflict = 1161 // 套餐正在激活中
|
||||
CodeNoMainPackage = 1162 // 必须有主套餐才能购买加油包
|
||||
CodeRealnameRequired = 1163 // 设备/卡必须先完成实名认证才能购买套餐
|
||||
CodeMixedOrderForbidden = 1164 // 同订单不能同时购买正式套餐和加油包
|
||||
|
||||
// 服务端错误 (2000-2999) -> 5xx HTTP 状态码
|
||||
CodeInternalError = 2001 // 内部服务器错误
|
||||
CodeDatabaseError = 2002 // 数据库错误
|
||||
@@ -230,6 +237,11 @@ var allErrorCodes = []int{
|
||||
CodePollingAlertRuleNotFound,
|
||||
CodePollingCleanupConfigNotFound,
|
||||
CodePollingManualTriggerLimit,
|
||||
CodeNoAvailablePackage,
|
||||
CodePackageActivationConflict,
|
||||
CodeNoMainPackage,
|
||||
CodeRealnameRequired,
|
||||
CodeMixedOrderForbidden,
|
||||
CodeInternalError,
|
||||
CodeDatabaseError,
|
||||
CodeRedisError,
|
||||
@@ -333,6 +345,11 @@ var errorMessages = map[int]string{
|
||||
CodePollingAlertRuleNotFound: "告警规则不存在",
|
||||
CodePollingCleanupConfigNotFound: "数据清理配置不存在",
|
||||
CodePollingManualTriggerLimit: "手动触发次数已达上限",
|
||||
CodeNoAvailablePackage: "没有可用套餐",
|
||||
CodePackageActivationConflict: "套餐正在激活中,请稍后重试",
|
||||
CodeNoMainPackage: "必须有主套餐才能购买加油包",
|
||||
CodeRealnameRequired: "设备/卡必须先完成实名认证才能购买套餐",
|
||||
CodeMixedOrderForbidden: "同订单不能同时购买正式套餐和加油包",
|
||||
CodeInvalidCredentials: "用户名或密码错误",
|
||||
CodeAccountLocked: "账号已锁定",
|
||||
CodePasswordExpired: "密码已过期",
|
||||
|
||||
@@ -1,219 +0,0 @@
|
||||
package errors
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
// TestGetHTTPStatus 测试错误码到 HTTP 状态码的映射
|
||||
func TestGetHTTPStatus(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
code int
|
||||
expected int
|
||||
}{
|
||||
// 成功
|
||||
{"成功", CodeSuccess, fiber.StatusOK},
|
||||
|
||||
// 客户端错误 (1xxx -> 4xx)
|
||||
{"参数验证失败", CodeInvalidParam, fiber.StatusBadRequest},
|
||||
{"缺失认证令牌", CodeMissingToken, fiber.StatusUnauthorized},
|
||||
{"无效令牌", CodeInvalidToken, fiber.StatusUnauthorized},
|
||||
{"未授权访问", CodeUnauthorized, fiber.StatusUnauthorized},
|
||||
{"禁止访问", CodeForbidden, fiber.StatusForbidden},
|
||||
{"资源未找到", CodeNotFound, fiber.StatusNotFound},
|
||||
{"资源冲突", CodeConflict, fiber.StatusConflict},
|
||||
{"请求过多", CodeTooManyRequests, fiber.StatusTooManyRequests},
|
||||
{"请求体过大", CodeRequestTooLarge, fiber.StatusBadRequest},
|
||||
|
||||
// 服务端错误 (2xxx -> 5xx)
|
||||
{"内部服务器错误", CodeInternalError, fiber.StatusInternalServerError},
|
||||
{"数据库错误", CodeDatabaseError, fiber.StatusInternalServerError},
|
||||
{"缓存服务错误", CodeRedisError, fiber.StatusInternalServerError},
|
||||
{"服务不可用", CodeServiceUnavailable, fiber.StatusServiceUnavailable},
|
||||
{"请求超时", CodeTimeout, fiber.StatusGatewayTimeout},
|
||||
{"任务队列错误", CodeTaskQueueError, fiber.StatusInternalServerError},
|
||||
|
||||
// 未知错误码
|
||||
{"未知错误码", 9999, fiber.StatusInternalServerError},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := GetHTTPStatus(tt.code)
|
||||
if result != tt.expected {
|
||||
t.Errorf("GetHTTPStatus(%d) = %d, expected %d", tt.code, result, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestGetMessage 测试错误码到错误消息的映射
|
||||
func TestGetMessage(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
code int
|
||||
expected string
|
||||
}{
|
||||
// 成功
|
||||
{"成功", CodeSuccess, "成功"},
|
||||
|
||||
// 客户端错误
|
||||
{"参数验证失败", CodeInvalidParam, "参数验证失败"},
|
||||
{"缺失认证令牌", CodeMissingToken, "缺失认证令牌"},
|
||||
{"无效令牌", CodeInvalidToken, "无效或过期的令牌"},
|
||||
{"未授权访问", CodeUnauthorized, "未授权访问"},
|
||||
{"禁止访问", CodeForbidden, "禁止访问"},
|
||||
{"资源未找到", CodeNotFound, "资源未找到"},
|
||||
{"资源冲突", CodeConflict, "资源冲突"},
|
||||
{"请求过多", CodeTooManyRequests, "请求过多,请稍后重试"},
|
||||
{"请求体过大", CodeRequestTooLarge, "请求体过大"},
|
||||
|
||||
// 服务端错误
|
||||
{"内部服务器错误", CodeInternalError, "内部服务器错误"},
|
||||
{"数据库错误", CodeDatabaseError, "数据库错误"},
|
||||
{"缓存服务错误", CodeRedisError, "缓存服务错误"},
|
||||
{"服务不可用", CodeServiceUnavailable, "服务暂时不可用"},
|
||||
{"请求超时", CodeTimeout, "请求超时"},
|
||||
{"任务队列错误", CodeTaskQueueError, "任务队列错误"},
|
||||
|
||||
// 未知错误码
|
||||
{"未知错误码", 9999, "请求处理失败"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := GetMessage(tt.code, "zh-CN")
|
||||
if result != tt.expected {
|
||||
t.Errorf("GetMessage(%d, \"zh-CN\") = %q, expected %q", tt.code, result, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestGetLogLevel 测试错误码到日志级别的映射
|
||||
func TestGetLogLevel(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
code int
|
||||
expected string
|
||||
}{
|
||||
// 成功 (不记录日志)
|
||||
{"成功", CodeSuccess, "info"},
|
||||
|
||||
// 客户端错误 (Warn 级别)
|
||||
{"参数验证失败", CodeInvalidParam, "warn"},
|
||||
{"缺失认证令牌", CodeMissingToken, "warn"},
|
||||
{"无效令牌", CodeInvalidToken, "warn"},
|
||||
{"未授权访问", CodeUnauthorized, "warn"},
|
||||
{"禁止访问", CodeForbidden, "warn"},
|
||||
{"资源未找到", CodeNotFound, "warn"},
|
||||
{"资源冲突", CodeConflict, "warn"},
|
||||
{"请求过多", CodeTooManyRequests, "warn"},
|
||||
{"请求体过大", CodeRequestTooLarge, "warn"},
|
||||
|
||||
// 服务端错误 (Error 级别)
|
||||
{"内部服务器错误", CodeInternalError, "error"},
|
||||
{"数据库错误", CodeDatabaseError, "error"},
|
||||
{"缓存服务错误", CodeRedisError, "error"},
|
||||
{"服务不可用", CodeServiceUnavailable, "error"},
|
||||
{"请求超时", CodeTimeout, "error"},
|
||||
{"任务队列错误", CodeTaskQueueError, "error"},
|
||||
|
||||
// 未知错误码 (Error 级别)
|
||||
{"未知错误码", 9999, "error"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := GetLogLevel(tt.code)
|
||||
if result != tt.expected {
|
||||
t.Errorf("GetLogLevel(%d) = %q, expected %q", tt.code, result, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAllCodesHaveMessages(t *testing.T) {
|
||||
var missing []int
|
||||
for _, code := range allErrorCodes {
|
||||
if _, ok := errorMessages[code]; !ok {
|
||||
missing = append(missing, code)
|
||||
}
|
||||
}
|
||||
if len(missing) > 0 {
|
||||
t.Errorf("以下错误码缺少映射消息: %v", missing)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNoOrphanMessages(t *testing.T) {
|
||||
codeSet := make(map[int]bool)
|
||||
for _, code := range allErrorCodes {
|
||||
codeSet[code] = true
|
||||
}
|
||||
|
||||
var orphan []int
|
||||
for code := range errorMessages {
|
||||
if !codeSet[code] {
|
||||
orphan = append(orphan, code)
|
||||
}
|
||||
}
|
||||
if len(orphan) > 0 {
|
||||
t.Errorf("以下错误码在 errorMessages 中存在但未在 allErrorCodes 中注册: %v", orphan)
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkGetHTTPStatus 基准测试 HTTP 状态码映射性能
|
||||
func BenchmarkGetHTTPStatus(b *testing.B) {
|
||||
codes := []int{
|
||||
CodeSuccess,
|
||||
CodeInvalidParam,
|
||||
CodeMissingToken,
|
||||
CodeInternalError,
|
||||
CodeDatabaseError,
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
for _, code := range codes {
|
||||
GetHTTPStatus(code)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkGetMessage 基准测试错误消息获取性能
|
||||
func BenchmarkGetMessage(b *testing.B) {
|
||||
codes := []int{
|
||||
CodeSuccess,
|
||||
CodeInvalidParam,
|
||||
CodeMissingToken,
|
||||
CodeInternalError,
|
||||
CodeDatabaseError,
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
for _, code := range codes {
|
||||
GetMessage(code, "zh-CN")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkGetLogLevel 基准测试日志级别映射性能
|
||||
func BenchmarkGetLogLevel(b *testing.B) {
|
||||
codes := []int{
|
||||
CodeSuccess,
|
||||
CodeInvalidParam,
|
||||
CodeMissingToken,
|
||||
CodeInternalError,
|
||||
CodeDatabaseError,
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
for _, code := range codes {
|
||||
GetLogLevel(code)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,258 +0,0 @@
|
||||
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()
|
||||
}
|
||||
}
|
||||
@@ -1,348 +0,0 @@
|
||||
package errors
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// TestSafeErrorHandler 测试 SafeErrorHandler 基本功能
|
||||
func TestSafeErrorHandler(t *testing.T) {
|
||||
logger, _ := zap.NewProduction()
|
||||
defer func() { _ = logger.Sync() }()
|
||||
handler := SafeErrorHandler(logger)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
err error
|
||||
expectedStatus int
|
||||
expectedCode int
|
||||
}{
|
||||
{
|
||||
name: "AppError 参数验证失败",
|
||||
err: New(CodeInvalidParam, "用户名不能为空"),
|
||||
expectedStatus: 400,
|
||||
expectedCode: CodeInvalidParam,
|
||||
},
|
||||
{
|
||||
name: "AppError 缺失令牌",
|
||||
err: New(CodeMissingToken, ""),
|
||||
expectedStatus: 401,
|
||||
expectedCode: CodeMissingToken,
|
||||
},
|
||||
{
|
||||
name: "AppError 资源未找到",
|
||||
err: New(CodeNotFound, "用户不存在"),
|
||||
expectedStatus: 404,
|
||||
expectedCode: CodeNotFound,
|
||||
},
|
||||
{
|
||||
name: "AppError 数据库错误",
|
||||
err: New(CodeDatabaseError, "连接失败"),
|
||||
expectedStatus: 500,
|
||||
expectedCode: CodeDatabaseError,
|
||||
},
|
||||
{
|
||||
name: "fiber.Error 400",
|
||||
err: fiber.NewError(400, "Bad Request"),
|
||||
expectedStatus: 400,
|
||||
expectedCode: CodeInvalidParam,
|
||||
},
|
||||
{
|
||||
name: "fiber.Error 404",
|
||||
err: fiber.NewError(404, "Not Found"),
|
||||
expectedStatus: 404,
|
||||
expectedCode: CodeNotFound,
|
||||
},
|
||||
{
|
||||
name: "标准 error",
|
||||
err: errors.New("standard error"),
|
||||
expectedStatus: 500,
|
||||
expectedCode: CodeInternalError,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
app := fiber.New(fiber.Config{
|
||||
ErrorHandler: handler,
|
||||
})
|
||||
|
||||
app.Get("/test", func(c *fiber.Ctx) error {
|
||||
return tt.err
|
||||
})
|
||||
|
||||
// 不实际发起 HTTP 请求,仅验证 handler 不会 panic
|
||||
// 实际的集成测试在 tests/integration/ 中进行
|
||||
if handler == nil {
|
||||
t.Error("SafeErrorHandler returned nil")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestAppErrorMethods 测试 AppError 的方法
|
||||
func TestAppErrorMethods(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
err *AppError
|
||||
expectedError string
|
||||
expectedCode int
|
||||
}{
|
||||
{
|
||||
name: "基本 AppError",
|
||||
err: New(CodeInvalidParam, "参数错误"),
|
||||
expectedError: "参数错误",
|
||||
expectedCode: CodeInvalidParam,
|
||||
},
|
||||
{
|
||||
name: "空消息使用默认",
|
||||
err: New(CodeDatabaseError, ""),
|
||||
expectedError: "数据库错误",
|
||||
expectedCode: CodeDatabaseError,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// 测试 Error() 方法
|
||||
if tt.err.Error() != tt.expectedError {
|
||||
t.Errorf("Error() = %q, expected %q", tt.err.Error(), tt.expectedError)
|
||||
}
|
||||
|
||||
// 测试 Code 字段
|
||||
if tt.err.Code != tt.expectedCode {
|
||||
t.Errorf("Code = %d, expected %d", tt.err.Code, tt.expectedCode)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestAppErrorUnwrap 测试错误链支持
|
||||
func TestAppErrorUnwrap(t *testing.T) {
|
||||
originalErr := errors.New("database connection failed")
|
||||
appErr := Wrap(CodeDatabaseError, originalErr)
|
||||
|
||||
// 测试 Unwrap
|
||||
unwrapped := appErr.Unwrap()
|
||||
if unwrapped != originalErr {
|
||||
t.Errorf("Unwrap() = %v, expected %v", unwrapped, originalErr)
|
||||
}
|
||||
|
||||
// 测试 errors.Is
|
||||
if !errors.Is(appErr, originalErr) {
|
||||
t.Error("errors.Is failed to identify wrapped error")
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkSafeErrorHandler 基准测试错误处理性能
|
||||
func BenchmarkSafeErrorHandler(b *testing.B) {
|
||||
logger, _ := zap.NewProduction()
|
||||
defer func() { _ = logger.Sync() }()
|
||||
_ = SafeErrorHandler(logger) // 避免未使用变量警告
|
||||
|
||||
testErrors := []error{
|
||||
New(CodeInvalidParam, "参数错误"),
|
||||
New(CodeDatabaseError, "数据库错误"),
|
||||
fiber.NewError(404, "Not Found"),
|
||||
errors.New("standard error"),
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
err := testErrors[i%len(testErrors)]
|
||||
_ = err // 避免未使用变量警告
|
||||
// 注意:这里无法直接调用 handler,因为它需要 Fiber Context
|
||||
// 实际性能测试应该在集成测试中进行
|
||||
}
|
||||
}
|
||||
|
||||
// TestNewWithValidation 测试创建 AppError 时的参数验证
|
||||
func TestNewWithValidation(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
code int
|
||||
message string
|
||||
expectPanic bool
|
||||
}{
|
||||
{
|
||||
name: "有效的错误码和消息",
|
||||
code: CodeInvalidParam,
|
||||
message: "自定义消息",
|
||||
expectPanic: false,
|
||||
},
|
||||
{
|
||||
name: "有效的错误码,空消息",
|
||||
code: CodeDatabaseError,
|
||||
message: "",
|
||||
expectPanic: false,
|
||||
},
|
||||
{
|
||||
name: "未知错误码",
|
||||
code: 9999,
|
||||
message: "未知错误",
|
||||
expectPanic: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
defer func() {
|
||||
r := recover()
|
||||
if (r != nil) != tt.expectPanic {
|
||||
t.Errorf("New() panic = %v, expectPanic = %v", r != nil, tt.expectPanic)
|
||||
}
|
||||
}()
|
||||
|
||||
err := New(tt.code, tt.message)
|
||||
if err == nil {
|
||||
t.Error("New() returned nil")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestWrapError 测试包装错误功能
|
||||
func TestWrapError(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
originalErr error
|
||||
code int
|
||||
message string
|
||||
expectedMessage string
|
||||
}{
|
||||
{
|
||||
name: "包装标准错误",
|
||||
originalErr: errors.New("connection timeout"),
|
||||
code: CodeTimeout,
|
||||
message: "",
|
||||
expectedMessage: "请求超时: connection timeout",
|
||||
},
|
||||
{
|
||||
name: "包装带自定义消息",
|
||||
originalErr: errors.New("SQL error"),
|
||||
code: CodeDatabaseError,
|
||||
message: "用户表查询失败",
|
||||
expectedMessage: "用户表查询失败: SQL error",
|
||||
},
|
||||
{
|
||||
name: "包装 nil 错误",
|
||||
originalErr: nil,
|
||||
code: CodeInternalError,
|
||||
message: "意外的 nil 错误",
|
||||
expectedMessage: "意外的 nil 错误",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
var err *AppError
|
||||
if tt.message == "" {
|
||||
err = Wrap(tt.code, tt.originalErr)
|
||||
} else {
|
||||
err = Wrap(tt.code, tt.originalErr, tt.message)
|
||||
}
|
||||
|
||||
if err.Error() != tt.expectedMessage {
|
||||
t.Errorf("Wrap().Error() = %q, expected %q", err.Error(), tt.expectedMessage)
|
||||
}
|
||||
|
||||
if err.Code != tt.code {
|
||||
t.Errorf("Wrap().Code = %d, expected %d", err.Code, tt.code)
|
||||
}
|
||||
|
||||
if tt.originalErr != nil {
|
||||
unwrapped := err.Unwrap()
|
||||
if unwrapped != tt.originalErr {
|
||||
t.Errorf("Wrap().Unwrap() = %v, expected %v", unwrapped, tt.originalErr)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestErrorMessageSanitization 测试错误消息脱敏
|
||||
func TestErrorMessageSanitization(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
code int
|
||||
message string
|
||||
shouldBeSanitized bool
|
||||
expectedForClient string
|
||||
}{
|
||||
{
|
||||
name: "客户端错误保留消息",
|
||||
code: CodeInvalidParam,
|
||||
message: "用户名长度必须在 3-20 之间",
|
||||
shouldBeSanitized: false,
|
||||
expectedForClient: "用户名长度必须在 3-20 之间",
|
||||
},
|
||||
{
|
||||
name: "服务端错误脱敏",
|
||||
code: CodeDatabaseError,
|
||||
message: "pq: relation 'users' does not exist",
|
||||
shouldBeSanitized: true,
|
||||
expectedForClient: "数据库错误", // 应该返回通用消息
|
||||
},
|
||||
{
|
||||
name: "内部错误脱敏",
|
||||
code: CodeInternalError,
|
||||
message: "panic: runtime error: invalid memory address",
|
||||
shouldBeSanitized: true,
|
||||
expectedForClient: "内部服务器错误",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// 这个测试逻辑应该在 handler.go 的 handleError 中实现
|
||||
// 这里仅验证逻辑概念
|
||||
|
||||
var clientMessage string
|
||||
if tt.shouldBeSanitized {
|
||||
// 服务端错误使用默认消息
|
||||
clientMessage = GetMessage(tt.code, "zh-CN")
|
||||
} else {
|
||||
// 客户端错误保留原始消息
|
||||
clientMessage = tt.message
|
||||
}
|
||||
|
||||
if clientMessage != tt.expectedForClient {
|
||||
t.Errorf("Client message = %q, expected %q", clientMessage, tt.expectedForClient)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestConcurrentErrorHandling 测试并发场景下的错误处理
|
||||
func TestConcurrentErrorHandling(t *testing.T) {
|
||||
logger, _ := zap.NewProduction()
|
||||
defer func() { _ = logger.Sync() }()
|
||||
handler := SafeErrorHandler(logger)
|
||||
if handler == nil {
|
||||
t.Fatal("SafeErrorHandler returned nil")
|
||||
}
|
||||
|
||||
// 并发创建错误
|
||||
errChan := make(chan error, 100)
|
||||
for i := 0; i < 100; i++ {
|
||||
go func(idx int) {
|
||||
code := CodeInvalidParam
|
||||
if idx%2 == 0 {
|
||||
code = CodeDatabaseError
|
||||
}
|
||||
errChan <- New(code, fmt.Sprintf("错误 #%d", idx))
|
||||
}(i)
|
||||
}
|
||||
|
||||
// 验证所有错误都能正确创建
|
||||
for i := 0; i < 100; i++ {
|
||||
err := <-errChan
|
||||
if err == nil {
|
||||
t.Errorf("Goroutine %d returned nil error", i)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user