Files
junhong_cmp_fiber/tests/testutils/integ/integration.go
huang 2ae585225b test(integration): 添加 Gateway 接口集成测试
- 添加 6 个卡 Gateway 接口测试(查询状态、流量、实名、获取链接、停机、复机)
- 添加 7 个设备 Gateway 接口测试(查询信息、卡槽、限速、WiFi、切卡、重启、恢复出厂)
- 每个接口测试包含成功场景和权限校验场景
- 更新测试环境初始化,添加 Gateway 客户端 mock 支持
- 所有 13 个接口测试通过
2026-02-02 17:44:24 +08:00

423 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"
"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)
}