备份一下
This commit is contained in:
618
tests/integration/recover_test.go
Normal file
618
tests/integration/recover_test.go
Normal file
@@ -0,0 +1,618 @@
|
||||
package integration
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/internal/middleware"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/errors"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/logger"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/response"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/gofiber/fiber/v2/middleware/requestid"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// TestPanicRecovery 测试 panic 恢复功能(T052)
|
||||
func TestPanicRecovery(t *testing.T) {
|
||||
// 创建临时目录用于日志
|
||||
tempDir := t.TempDir()
|
||||
|
||||
// 初始化日志系统
|
||||
err := logger.InitLoggers("info", false,
|
||||
logger.LogRotationConfig{
|
||||
Filename: filepath.Join(tempDir, "app-panic.log"),
|
||||
MaxSize: 10,
|
||||
MaxBackups: 3,
|
||||
MaxAge: 7,
|
||||
Compress: false,
|
||||
},
|
||||
logger.LogRotationConfig{
|
||||
Filename: filepath.Join(tempDir, "access-panic.log"),
|
||||
MaxSize: 10,
|
||||
MaxBackups: 3,
|
||||
MaxAge: 7,
|
||||
Compress: false,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to initialize loggers: %v", err)
|
||||
}
|
||||
defer logger.Sync()
|
||||
|
||||
appLogger := logger.GetAppLogger()
|
||||
|
||||
// 创建应用
|
||||
app := fiber.New()
|
||||
|
||||
// 注册中间件(recover 必须第一个)
|
||||
app.Use(middleware.Recover(appLogger))
|
||||
app.Use(requestid.New(requestid.Config{
|
||||
Generator: func() string {
|
||||
return uuid.NewString()
|
||||
},
|
||||
}))
|
||||
|
||||
// 创建会 panic 的 handler
|
||||
app.Get("/panic", func(c *fiber.Ctx) error {
|
||||
panic("intentional panic for testing")
|
||||
})
|
||||
|
||||
// 创建正常的 handler
|
||||
app.Get("/ok", func(c *fiber.Ctx) error {
|
||||
return c.SendString("ok")
|
||||
})
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
path string
|
||||
shouldPanic bool
|
||||
expectedStatus int
|
||||
expectedCode int
|
||||
}{
|
||||
{
|
||||
name: "panic endpoint returns 500",
|
||||
path: "/panic",
|
||||
shouldPanic: true,
|
||||
expectedStatus: 500,
|
||||
expectedCode: errors.CodeInternalError,
|
||||
},
|
||||
{
|
||||
name: "normal endpoint works after panic",
|
||||
path: "/ok",
|
||||
shouldPanic: false,
|
||||
expectedStatus: 200,
|
||||
expectedCode: 0,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
req := httptest.NewRequest("GET", tt.path, nil)
|
||||
resp, err := app.Test(req)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to execute request: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// 验证 HTTP 状态码
|
||||
if resp.StatusCode != tt.expectedStatus {
|
||||
t.Errorf("Expected status %d, got %d", tt.expectedStatus, resp.StatusCode)
|
||||
}
|
||||
|
||||
// 解析响应
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to read response body: %v", err)
|
||||
}
|
||||
|
||||
if tt.shouldPanic {
|
||||
// panic 应该返回统一错误响应
|
||||
var response response.Response
|
||||
if err := json.Unmarshal(body, &response); err != nil {
|
||||
t.Fatalf("Failed to unmarshal response: %v", err)
|
||||
}
|
||||
|
||||
if response.Code != tt.expectedCode {
|
||||
t.Errorf("Expected code %d, got %d", tt.expectedCode, response.Code)
|
||||
}
|
||||
|
||||
if response.Data != nil {
|
||||
t.Error("Error response data should be nil")
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestPanicLogging 测试 panic 日志记录和堆栈跟踪(T053)
|
||||
func TestPanicLogging(t *testing.T) {
|
||||
// 创建临时目录用于日志
|
||||
tempDir := t.TempDir()
|
||||
appLogFile := filepath.Join(tempDir, "app-panic-log.log")
|
||||
|
||||
// 初始化日志系统
|
||||
err := logger.InitLoggers("info", false,
|
||||
logger.LogRotationConfig{
|
||||
Filename: appLogFile,
|
||||
MaxSize: 10,
|
||||
MaxBackups: 3,
|
||||
MaxAge: 7,
|
||||
Compress: false,
|
||||
},
|
||||
logger.LogRotationConfig{
|
||||
Filename: filepath.Join(tempDir, "access-panic-log.log"),
|
||||
MaxSize: 10,
|
||||
MaxBackups: 3,
|
||||
MaxAge: 7,
|
||||
Compress: false,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to initialize loggers: %v", err)
|
||||
}
|
||||
defer logger.Sync()
|
||||
|
||||
appLogger := logger.GetAppLogger()
|
||||
|
||||
// 创建应用
|
||||
app := fiber.New()
|
||||
|
||||
// 注册中间件
|
||||
app.Use(middleware.Recover(appLogger))
|
||||
app.Use(requestid.New(requestid.Config{
|
||||
Generator: func() string {
|
||||
return uuid.NewString()
|
||||
},
|
||||
}))
|
||||
|
||||
// 创建不同类型的 panic
|
||||
app.Get("/panic-string", func(c *fiber.Ctx) error {
|
||||
panic("string panic message")
|
||||
})
|
||||
|
||||
app.Get("/panic-error", func(c *fiber.Ctx) error {
|
||||
panic(fiber.NewError(500, "error panic message"))
|
||||
})
|
||||
|
||||
app.Get("/panic-struct", func(c *fiber.Ctx) error {
|
||||
panic(struct{ Message string }{"struct panic message"})
|
||||
})
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
path string
|
||||
expectedInLog []string
|
||||
unexpectedInLog []string
|
||||
}{
|
||||
{
|
||||
name: "string panic logs correctly",
|
||||
path: "/panic-string",
|
||||
expectedInLog: []string{
|
||||
"Panic 已恢复",
|
||||
"string panic message",
|
||||
"stack",
|
||||
"request_id",
|
||||
"method",
|
||||
"path",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "error panic logs correctly",
|
||||
path: "/panic-error",
|
||||
expectedInLog: []string{
|
||||
"Panic 已恢复",
|
||||
"error panic message",
|
||||
"stack",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "struct panic logs correctly",
|
||||
path: "/panic-struct",
|
||||
expectedInLog: []string{
|
||||
"Panic 已恢复",
|
||||
"stack",
|
||||
"Message",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// 执行会 panic 的请求
|
||||
req := httptest.NewRequest("GET", tt.path, nil)
|
||||
resp, err := app.Test(req)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to execute request: %v", err)
|
||||
}
|
||||
resp.Body.Close()
|
||||
|
||||
// 刷新日志缓冲区
|
||||
logger.Sync()
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
|
||||
// 读取日志内容
|
||||
logContent, err := os.ReadFile(appLogFile)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to read app log: %v", err)
|
||||
}
|
||||
|
||||
content := string(logContent)
|
||||
|
||||
// 验证日志包含预期内容
|
||||
for _, expected := range tt.expectedInLog {
|
||||
if !strings.Contains(content, expected) {
|
||||
t.Errorf("Log should contain '%s'", expected)
|
||||
}
|
||||
}
|
||||
|
||||
// 验证日志不包含意外内容
|
||||
for _, unexpected := range tt.unexpectedInLog {
|
||||
if strings.Contains(content, unexpected) {
|
||||
t.Errorf("Log should NOT contain '%s'", unexpected)
|
||||
}
|
||||
}
|
||||
|
||||
// 验证堆栈跟踪包含文件和行号
|
||||
if !strings.Contains(content, "recover_test.go") {
|
||||
t.Error("Stack trace should contain source file name")
|
||||
}
|
||||
|
||||
t.Logf("Panic log contains stack trace: %v", strings.Contains(content, "stack"))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestSubsequentRequestsAfterPanic 测试 panic 后后续请求正常处理(T054)
|
||||
func TestSubsequentRequestsAfterPanic(t *testing.T) {
|
||||
// 创建临时目录用于日志
|
||||
tempDir := t.TempDir()
|
||||
|
||||
// 初始化日志系统
|
||||
err := logger.InitLoggers("info", false,
|
||||
logger.LogRotationConfig{
|
||||
Filename: filepath.Join(tempDir, "app.log"),
|
||||
MaxSize: 10,
|
||||
MaxBackups: 3,
|
||||
MaxAge: 7,
|
||||
Compress: false,
|
||||
},
|
||||
logger.LogRotationConfig{
|
||||
Filename: filepath.Join(tempDir, "access.log"),
|
||||
MaxSize: 10,
|
||||
MaxBackups: 3,
|
||||
MaxAge: 7,
|
||||
Compress: false,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to initialize loggers: %v", err)
|
||||
}
|
||||
defer logger.Sync()
|
||||
|
||||
appLogger := logger.GetAppLogger()
|
||||
|
||||
// 创建应用
|
||||
app := fiber.New()
|
||||
|
||||
// 注册中间件
|
||||
app.Use(middleware.Recover(appLogger))
|
||||
app.Use(requestid.New(requestid.Config{
|
||||
Generator: func() string {
|
||||
return uuid.NewString()
|
||||
},
|
||||
}))
|
||||
|
||||
callCount := 0
|
||||
|
||||
app.Get("/test", func(c *fiber.Ctx) error {
|
||||
callCount++
|
||||
// 第 1、3、5 次调用会 panic
|
||||
if callCount%2 == 1 {
|
||||
panic("test panic")
|
||||
}
|
||||
// 第 2、4、6 次调用正常返回
|
||||
return c.JSON(fiber.Map{
|
||||
"call_count": callCount,
|
||||
"status": "ok",
|
||||
})
|
||||
})
|
||||
|
||||
// 执行多次请求,验证 panic 不影响后续请求
|
||||
for i := 1; i <= 6; i++ {
|
||||
req := httptest.NewRequest("GET", "/test", nil)
|
||||
resp, err := app.Test(req)
|
||||
if err != nil {
|
||||
t.Fatalf("Request %d failed: %v", i, err)
|
||||
}
|
||||
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
resp.Body.Close()
|
||||
|
||||
if i%2 == 1 {
|
||||
// 奇数次应该返回 500
|
||||
if resp.StatusCode != 500 {
|
||||
t.Errorf("Request %d: expected status 500, got %d", i, resp.StatusCode)
|
||||
}
|
||||
} else {
|
||||
// 偶数次应该返回 200
|
||||
if resp.StatusCode != 200 {
|
||||
t.Errorf("Request %d: expected status 200, got %d", i, resp.StatusCode)
|
||||
}
|
||||
|
||||
// 验证响应内容
|
||||
var response map[string]any
|
||||
if err := json.Unmarshal(body, &response); err != nil {
|
||||
t.Fatalf("Request %d: failed to unmarshal response: %v", i, err)
|
||||
}
|
||||
|
||||
if status, ok := response["status"].(string); !ok || status != "ok" {
|
||||
t.Errorf("Request %d: expected status 'ok', got %v", i, response["status"])
|
||||
}
|
||||
}
|
||||
|
||||
t.Logf("Request %d completed: status=%d", i, resp.StatusCode)
|
||||
}
|
||||
|
||||
// 验证所有 6 次调用都执行了
|
||||
if callCount != 6 {
|
||||
t.Errorf("Expected 6 calls, got %d", callCount)
|
||||
}
|
||||
}
|
||||
|
||||
// TestPanicWithRequestID 测试 panic 日志包含 Request ID(T053)
|
||||
func TestPanicWithRequestID(t *testing.T) {
|
||||
// 创建临时目录用于日志
|
||||
tempDir := t.TempDir()
|
||||
appLogFile := filepath.Join(tempDir, "app-panic-reqid.log")
|
||||
|
||||
// 初始化日志系统
|
||||
err := logger.InitLoggers("info", false,
|
||||
logger.LogRotationConfig{
|
||||
Filename: appLogFile,
|
||||
MaxSize: 10,
|
||||
MaxBackups: 3,
|
||||
MaxAge: 7,
|
||||
Compress: false,
|
||||
},
|
||||
logger.LogRotationConfig{
|
||||
Filename: filepath.Join(tempDir, "access-panic-reqid.log"),
|
||||
MaxSize: 10,
|
||||
MaxBackups: 3,
|
||||
MaxAge: 7,
|
||||
Compress: false,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to initialize loggers: %v", err)
|
||||
}
|
||||
defer logger.Sync()
|
||||
|
||||
appLogger := logger.GetAppLogger()
|
||||
|
||||
// 创建应用
|
||||
app := fiber.New()
|
||||
|
||||
// 注册中间件(顺序重要)
|
||||
app.Use(middleware.Recover(appLogger))
|
||||
app.Use(requestid.New(requestid.Config{
|
||||
Generator: func() string {
|
||||
return uuid.NewString()
|
||||
},
|
||||
}))
|
||||
|
||||
app.Get("/panic", func(c *fiber.Ctx) error {
|
||||
panic("test panic with request id")
|
||||
})
|
||||
|
||||
// 执行请求
|
||||
req := httptest.NewRequest("GET", "/panic", nil)
|
||||
resp, err := app.Test(req)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to execute request: %v", err)
|
||||
}
|
||||
resp.Body.Close()
|
||||
|
||||
// 获取 Request ID
|
||||
requestID := resp.Header.Get("X-Request-ID")
|
||||
if requestID == "" {
|
||||
t.Error("X-Request-ID header should be set even after panic")
|
||||
}
|
||||
|
||||
// 刷新日志缓冲区
|
||||
logger.Sync()
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
|
||||
// 读取日志内容
|
||||
logContent, err := os.ReadFile(appLogFile)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to read app log: %v", err)
|
||||
}
|
||||
|
||||
content := string(logContent)
|
||||
|
||||
// 验证日志包含 Request ID
|
||||
if !strings.Contains(content, requestID) {
|
||||
t.Errorf("Panic log should contain request ID '%s'", requestID)
|
||||
}
|
||||
|
||||
// 验证日志包含关键字段
|
||||
requiredFields := []string{
|
||||
"request_id",
|
||||
"method",
|
||||
"path",
|
||||
"panic",
|
||||
"stack",
|
||||
}
|
||||
|
||||
for _, field := range requiredFields {
|
||||
if !strings.Contains(content, field) {
|
||||
t.Errorf("Panic log should contain field '%s'", field)
|
||||
}
|
||||
}
|
||||
|
||||
t.Logf("Panic log successfully includes Request ID: %s", requestID)
|
||||
}
|
||||
|
||||
// TestConcurrentPanics 测试并发 panic 处理(T054)
|
||||
func TestConcurrentPanics(t *testing.T) {
|
||||
// 创建临时目录用于日志
|
||||
tempDir := t.TempDir()
|
||||
|
||||
// 初始化日志系统
|
||||
err := logger.InitLoggers("info", false,
|
||||
logger.LogRotationConfig{
|
||||
Filename: filepath.Join(tempDir, "app.log"),
|
||||
MaxSize: 10,
|
||||
MaxBackups: 3,
|
||||
MaxAge: 7,
|
||||
Compress: false,
|
||||
},
|
||||
logger.LogRotationConfig{
|
||||
Filename: filepath.Join(tempDir, "access.log"),
|
||||
MaxSize: 10,
|
||||
MaxBackups: 3,
|
||||
MaxAge: 7,
|
||||
Compress: false,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to initialize loggers: %v", err)
|
||||
}
|
||||
defer logger.Sync()
|
||||
|
||||
appLogger := logger.GetAppLogger()
|
||||
|
||||
// 创建应用
|
||||
app := fiber.New()
|
||||
|
||||
// 注册中间件
|
||||
app.Use(middleware.Recover(appLogger))
|
||||
app.Use(requestid.New(requestid.Config{
|
||||
Generator: func() string {
|
||||
return uuid.NewString()
|
||||
},
|
||||
}))
|
||||
|
||||
app.Get("/panic", func(c *fiber.Ctx) error {
|
||||
panic("concurrent panic test")
|
||||
})
|
||||
|
||||
// 并发发送多个会 panic 的请求
|
||||
const numRequests = 20
|
||||
errors := make(chan error, numRequests)
|
||||
statuses := make(chan int, numRequests)
|
||||
|
||||
for i := 0; i < numRequests; i++ {
|
||||
go func() {
|
||||
req := httptest.NewRequest("GET", "/panic", nil)
|
||||
resp, err := app.Test(req)
|
||||
if err != nil {
|
||||
errors <- err
|
||||
statuses <- 0
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
statuses <- resp.StatusCode
|
||||
errors <- nil
|
||||
}()
|
||||
}
|
||||
|
||||
// 收集所有结果
|
||||
for i := 0; i < numRequests; i++ {
|
||||
if err := <-errors; err != nil {
|
||||
t.Fatalf("Request failed: %v", err)
|
||||
}
|
||||
status := <-statuses
|
||||
if status != 500 {
|
||||
t.Errorf("Expected status 500, got %d", status)
|
||||
}
|
||||
}
|
||||
|
||||
t.Logf("Successfully handled %d concurrent panics", numRequests)
|
||||
}
|
||||
|
||||
// TestRecoverMiddlewareOrder 测试 Recover 中间件必须在第一个(T052)
|
||||
func TestRecoverMiddlewareOrder(t *testing.T) {
|
||||
// 创建临时目录用于日志
|
||||
tempDir := t.TempDir()
|
||||
|
||||
// 初始化日志系统
|
||||
err := logger.InitLoggers("info", false,
|
||||
logger.LogRotationConfig{
|
||||
Filename: filepath.Join(tempDir, "app.log"),
|
||||
MaxSize: 10,
|
||||
MaxBackups: 3,
|
||||
MaxAge: 7,
|
||||
Compress: false,
|
||||
},
|
||||
logger.LogRotationConfig{
|
||||
Filename: filepath.Join(tempDir, "access.log"),
|
||||
MaxSize: 10,
|
||||
MaxBackups: 3,
|
||||
MaxAge: 7,
|
||||
Compress: false,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to initialize loggers: %v", err)
|
||||
}
|
||||
defer logger.Sync()
|
||||
|
||||
appLogger := logger.GetAppLogger()
|
||||
|
||||
// 创建应用
|
||||
app := fiber.New()
|
||||
|
||||
// 正确的顺序:Recover → RequestID → Logger
|
||||
app.Use(middleware.Recover(appLogger))
|
||||
app.Use(requestid.New(requestid.Config{
|
||||
Generator: func() string {
|
||||
return uuid.NewString()
|
||||
},
|
||||
}))
|
||||
app.Use(logger.Middleware())
|
||||
|
||||
app.Get("/panic", func(c *fiber.Ctx) error {
|
||||
panic("test panic")
|
||||
})
|
||||
|
||||
// 执行请求
|
||||
req := httptest.NewRequest("GET", "/panic", nil)
|
||||
resp, err := app.Test(req)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to execute request: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// 验证请求被正确处理(返回 500 而不是崩溃)
|
||||
if resp.StatusCode != 500 {
|
||||
t.Errorf("Expected status 500, got %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
// 验证仍然有 Request ID(说明 RequestID 中间件在 Recover 之后执行)
|
||||
requestID := resp.Header.Get("X-Request-ID")
|
||||
if requestID == "" {
|
||||
t.Error("X-Request-ID should be set even after panic")
|
||||
}
|
||||
|
||||
// 解析响应,验证返回了统一错误格式
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
var response response.Response
|
||||
if err := json.Unmarshal(body, &response); err != nil {
|
||||
t.Fatalf("Failed to unmarshal response: %v", err)
|
||||
}
|
||||
|
||||
if response.Code != errors.CodeInternalError {
|
||||
t.Errorf("Expected code %d, got %d", errors.CodeInternalError, response.Code)
|
||||
}
|
||||
|
||||
t.Logf("Recover middleware correctly placed first, handled panic gracefully")
|
||||
}
|
||||
Reference in New Issue
Block a user