备份一下

This commit is contained in:
2025-11-11 15:53:01 +08:00
parent e98dd4d725
commit 39c5b524a9
10 changed files with 4295 additions and 63 deletions

View 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 IDT053
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")
}