package integ import ( "bytes" "context" "encoding/json" "fmt" "net/http" "net/http/httptest" "sync" "sync/atomic" "testing" "time" "github.com/break/junhong_cmp_fiber/internal/bootstrap" "github.com/break/junhong_cmp_fiber/internal/gateway" "github.com/break/junhong_cmp_fiber/internal/model" "github.com/break/junhong_cmp_fiber/internal/routes" "github.com/break/junhong_cmp_fiber/pkg/auth" "github.com/break/junhong_cmp_fiber/pkg/config" "github.com/break/junhong_cmp_fiber/pkg/constants" "github.com/break/junhong_cmp_fiber/pkg/errors" "github.com/break/junhong_cmp_fiber/pkg/middleware" "github.com/break/junhong_cmp_fiber/tests/testutils" "github.com/gofiber/fiber/v2" "github.com/redis/go-redis/v9" "github.com/stretchr/testify/require" "go.uber.org/zap" "golang.org/x/crypto/bcrypt" "gorm.io/gorm" ) // IntegrationTestEnv 集成测试环境 // 封装集成测试所需的所有依赖,提供统一的测试环境设置 type IntegrationTestEnv struct { TX *gorm.DB // 自动回滚的数据库事务 Redis *redis.Client // 全局 Redis 连接 Logger *zap.Logger // 测试用日志记录器 TokenManager *auth.TokenManager // Token 管理器 App *fiber.App // 配置好的 Fiber 应用实例 Handlers *bootstrap.Handlers Middlewares *bootstrap.Middlewares t *testing.T superAdmin *model.Account currentToken string } // NewIntegrationTestEnv 创建集成测试环境 // // 自动完成以下初始化: // - 创建独立的数据库事务(测试结束后自动回滚) // - 获取全局 Redis 连接并清理测试键 // - 创建 Logger 和 TokenManager // - 通过 Bootstrap 初始化所有 Handlers 和 Middlewares // - 配置 Fiber App 并注册路由 // // 用法: // // func TestXxx(t *testing.T) { // env := testutils.NewIntegrationTestEnv(t) // // env.App 已配置好,可直接发送请求 // // env.TX 是独立事务,测试结束后自动回滚 // } var configOnce sync.Once func NewIntegrationTestEnv(t *testing.T) *IntegrationTestEnv { t.Helper() configOnce.Do(func() { _, _ = config.Load() }) tx := testutils.NewTestTransaction(t) rdb := testutils.GetTestRedis(t) testutils.CleanTestRedisKeys(t, rdb) logger, _ := zap.NewDevelopment() tokenManager := auth.NewTokenManager(rdb, 24*time.Hour, 7*24*time.Hour) gatewayClient := createMockGatewayClient() deps := &bootstrap.Dependencies{ DB: tx, Redis: rdb, Logger: logger, TokenManager: tokenManager, GatewayClient: gatewayClient, } result, err := bootstrap.Bootstrap(deps) require.NoError(t, err, "Bootstrap 初始化失败") app := fiber.New(fiber.Config{ ErrorHandler: errors.SafeErrorHandler(logger), }) routes.RegisterRoutes(app, result.Handlers, result.Middlewares) env := &IntegrationTestEnv{ TX: tx, Redis: rdb, Logger: logger, TokenManager: tokenManager, App: app, Handlers: result.Handlers, Middlewares: result.Middlewares, t: t, } return env } func createMockGatewayClient() *gateway.Client { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") resp := gateway.GatewayResponse{ Code: 200, Msg: "success", TraceID: "test-trace-id", Data: json.RawMessage(`{}`), } json.NewEncoder(w).Encode(resp) })) client := gateway.NewClient(server.URL, "test-app-id", "test-app-secret") return client } // AsSuperAdmin 设置当前请求使用超级管理员身份 // 返回 IntegrationTestEnv 以支持链式调用 // // 用法: // // resp, err := env.AsSuperAdmin().Request("GET", "/api/admin/roles", nil) func (e *IntegrationTestEnv) AsSuperAdmin() *IntegrationTestEnv { e.t.Helper() if e.superAdmin == nil { e.superAdmin = e.ensureSuperAdmin() } e.currentToken = e.generateToken(e.superAdmin) return e } // AsUser 设置当前请求使用指定用户身份 // 返回 IntegrationTestEnv 以支持链式调用 // // 用法: // // account := e.CreateTestAccount(...) // resp, err := env.AsUser(account).Request("GET", "/api/admin/shops", nil) func (e *IntegrationTestEnv) AsUser(account *model.Account) *IntegrationTestEnv { e.t.Helper() token := e.generateToken(account) e.currentToken = token return e } // Request 发送 HTTP 请求 // 自动添加 Authorization header(如果已设置用户身份) // // 用法: // // resp, err := env.AsSuperAdmin().Request("GET", "/api/admin/roles", nil) // resp, err := env.Request("POST", "/api/admin/login", loginBody) func (e *IntegrationTestEnv) Request(method, path string, body []byte) (*http.Response, error) { e.t.Helper() var req *http.Request if body != nil { req = httptest.NewRequest(method, path, bytes.NewReader(body)) req.Header.Set("Content-Type", "application/json") } else { req = httptest.NewRequest(method, path, nil) } if e.currentToken != "" { req.Header.Set("Authorization", "Bearer "+e.currentToken) } return e.App.Test(req, -1) } // RequestWithHeaders 发送带自定义 Headers 的 HTTP 请求 func (e *IntegrationTestEnv) RequestWithHeaders(method, path string, body []byte, headers map[string]string) (*http.Response, error) { e.t.Helper() var req *http.Request if body != nil { req = httptest.NewRequest(method, path, bytes.NewReader(body)) } else { req = httptest.NewRequest(method, path, nil) } for k, v := range headers { req.Header.Set(k, v) } if body != nil && req.Header.Get("Content-Type") == "" { req.Header.Set("Content-Type", "application/json") } if e.currentToken != "" && req.Header.Get("Authorization") == "" { req.Header.Set("Authorization", "Bearer "+e.currentToken) } return e.App.Test(req, -1) } // ClearAuth 清除当前认证状态 func (e *IntegrationTestEnv) ClearAuth() *IntegrationTestEnv { e.currentToken = "" return e } // ensureSuperAdmin 确保超级管理员账号存在 func (e *IntegrationTestEnv) ensureSuperAdmin() *model.Account { e.t.Helper() var existing model.Account err := e.TX.Where("user_type = ?", constants.UserTypeSuperAdmin).First(&existing).Error if err == nil { return &existing } return e.CreateTestAccount("superadmin", "password123", constants.UserTypeSuperAdmin, nil, nil) } // generateToken 为账号生成访问 Token func (e *IntegrationTestEnv) generateToken(account *model.Account) string { e.t.Helper() ctx := context.Background() var shopID, enterpriseID uint if account.ShopID != nil { shopID = *account.ShopID } if account.EnterpriseID != nil { enterpriseID = *account.EnterpriseID } tokenInfo := &auth.TokenInfo{ UserID: account.ID, UserType: account.UserType, ShopID: shopID, EnterpriseID: enterpriseID, Username: account.Username, Device: "test", IP: "127.0.0.1", } accessToken, _, err := e.TokenManager.GenerateTokenPair(ctx, tokenInfo) require.NoError(e.t, err, "生成 Token 失败") return accessToken } var ( usernameCounter uint64 phoneCounter uint64 shopCodeCounter uint64 ) // CreateTestAccount 创建测试账号 func (e *IntegrationTestEnv) CreateTestAccount(username, password string, userType int, shopID, enterpriseID *uint) *model.Account { e.t.Helper() hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) require.NoError(e.t, err) counter := atomic.AddUint64(&usernameCounter, 1) uniqueUsername := fmt.Sprintf("%s_%d", username, counter) uniquePhone := fmt.Sprintf("138%08d", atomic.AddUint64(&phoneCounter, 1)) account := &model.Account{ BaseModel: model.BaseModel{ Creator: 1, Updater: 1, }, Username: uniqueUsername, Phone: uniquePhone, Password: string(hashedPassword), UserType: userType, ShopID: shopID, EnterpriseID: enterpriseID, Status: 1, } err = e.TX.Create(account).Error require.NoError(e.t, err, "创建测试账号失败") return account } // CreateTestShop 创建测试商户 func (e *IntegrationTestEnv) CreateTestShop(name string, level int, parentID *uint) *model.Shop { e.t.Helper() counter := atomic.AddUint64(&shopCodeCounter, 1) uniqueCode := fmt.Sprintf("SHOP_%d_%d", time.Now().UnixNano()%10000, counter) uniqueName := fmt.Sprintf("%s_%d", name, counter) shop := &model.Shop{ BaseModel: model.BaseModel{ Creator: 1, Updater: 1, }, ShopName: uniqueName, ShopCode: uniqueCode, Level: level, ParentID: parentID, Status: 1, } err := e.TX.Create(shop).Error require.NoError(e.t, err, "创建测试商户失败") return shop } // CreateTestEnterprise 创建测试企业 func (e *IntegrationTestEnv) CreateTestEnterprise(name string, ownerShopID *uint) *model.Enterprise { e.t.Helper() counter := atomic.AddUint64(&shopCodeCounter, 1) uniqueCode := fmt.Sprintf("ENT_%d_%d", time.Now().UnixNano()%10000, counter) uniqueName := fmt.Sprintf("%s_%d", name, counter) enterprise := &model.Enterprise{ BaseModel: model.BaseModel{ Creator: 1, Updater: 1, }, EnterpriseName: uniqueName, EnterpriseCode: uniqueCode, OwnerShopID: ownerShopID, Status: 1, } err := e.TX.Create(enterprise).Error require.NoError(e.t, err, "创建测试企业失败") return enterprise } // CreateTestRole 创建测试角色 func (e *IntegrationTestEnv) CreateTestRole(name string, roleType int) *model.Role { e.t.Helper() counter := atomic.AddUint64(&usernameCounter, 1) uniqueName := fmt.Sprintf("%s_%d", name, counter) role := &model.Role{ BaseModel: model.BaseModel{ Creator: 1, Updater: 1, }, RoleName: uniqueName, RoleType: roleType, Status: constants.StatusEnabled, } err := e.TX.Create(role).Error require.NoError(e.t, err, "创建测试角色失败") return role } // CreateTestPermission 创建测试权限 func (e *IntegrationTestEnv) CreateTestPermission(name, code string, permType int) *model.Permission { e.t.Helper() counter := atomic.AddUint64(&usernameCounter, 1) uniqueName := fmt.Sprintf("%s_%d", name, counter) uniqueCode := fmt.Sprintf("%s_%d", code, counter) permission := &model.Permission{ BaseModel: model.BaseModel{ Creator: 1, Updater: 1, }, PermName: uniqueName, PermCode: uniqueCode, PermType: permType, Status: constants.StatusEnabled, } err := e.TX.Create(permission).Error require.NoError(e.t, err, "创建测试权限失败") return permission } // SetUserContext 设置用户上下文(用于直接调用 Service 层测试) func (e *IntegrationTestEnv) SetUserContext(ctx context.Context, userID uint, userType int, shopID uint) context.Context { return middleware.SetUserContext(ctx, middleware.NewSimpleUserContext(userID, userType, shopID)) } // GetSuperAdminContext 获取超级管理员上下文 func (e *IntegrationTestEnv) GetSuperAdminContext() context.Context { if e.superAdmin == nil { e.superAdmin = e.ensureSuperAdmin() } return e.SetUserContext(context.Background(), e.superAdmin.ID, constants.UserTypeSuperAdmin, 0) } // RawDB 获取跳过数据权限过滤的数据库连接 // 用于测试中验证数据是否正确写入,不受 GORM Callback 影响 // // 用法: // // var count int64 // env.RawDB().Model(&model.Role{}).Where("role_name = ?", name).Count(&count) func (e *IntegrationTestEnv) RawDB() *gorm.DB { ctx := e.GetSuperAdminContext() return e.TX.WithContext(ctx) }