Files
junhong_cmp_fiber/tests/testutils/integ/integration.go
huang 23eb0307bb
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 5m30s
feat: 实现门店套餐分配功能并统一测试基础设施
新增功能:
- 门店套餐分配管理(shop_package_allocation):支持门店套餐库存管理
- 门店套餐系列分配管理(shop_series_allocation):支持套餐系列分配和佣金层级设置
- 我的套餐查询(my_package):支持门店查询自己的套餐分配情况

测试改进:
- 统一集成测试基础设施,新增 testutils.NewIntegrationTestEnv
- 重构所有集成测试使用新的测试环境设置
- 移除旧的测试辅助函数和冗余测试文件
- 新增 test_helpers_test.go 统一任务测试辅助

技术细节:
- 新增数据库迁移 000025_create_shop_allocation_tables
- 新增 3 个 Handler、Service、Store 和对应的单元测试
- 更新 OpenAPI 文档和文档生成器
- 测试覆盖率:Service 层 > 90%

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-28 10:45:16 +08:00

402 lines
11 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package integ
import (
"bytes"
"context"
"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/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)
deps := &bootstrap.Dependencies{
DB: tx,
Redis: rdb,
Logger: logger,
TokenManager: tokenManager,
}
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
}
// 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)
}