- 添加 6 个卡 Gateway 接口测试(查询状态、流量、实名、获取链接、停机、复机) - 添加 7 个设备 Gateway 接口测试(查询信息、卡槽、限速、WiFi、切卡、重启、恢复出厂) - 每个接口测试包含成功场景和权限校验场景 - 更新测试环境初始化,添加 Gateway 客户端 mock 支持 - 所有 13 个接口测试通过
423 lines
11 KiB
Go
423 lines
11 KiB
Go
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)
|
||
}
|