feat: 实现设备管理和设备导入功能,修复测试问题
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 5m30s
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 5m30s
主要变更: - 实现设备管理模块(创建、查询、列表、更新状态、删除) - 实现设备批量导入功能(CSV 解析、ICCID 绑定、异步任务处理) - 添加设备-SIM 卡绑定约束(部分唯一索引防止并发问题) - 修复 fee_rate 数据库字段类型(numeric -> bigint) - 修复测试数据隔离问题(基于增量断言) - 修复集成测试中间件顺序问题 - 清理无用测试文件(PersonalCustomer、Email 相关) - 归档 enterprise-card-authorization 变更
This commit is contained in:
@@ -37,7 +37,7 @@ import (
|
||||
// testEnv 测试环境
|
||||
type testEnv struct {
|
||||
tx *gorm.DB
|
||||
rdb *redis.Client
|
||||
rdb *redis.Client
|
||||
app *fiber.App
|
||||
accountService *accountService.Service
|
||||
postgresCleanup func()
|
||||
@@ -121,12 +121,19 @@ func setupTestEnv(t *testing.T) *testEnv {
|
||||
services := &bootstrap.Handlers{
|
||||
Account: accountHandler,
|
||||
}
|
||||
middlewares := &bootstrap.Middlewares{}
|
||||
middlewares := &bootstrap.Middlewares{
|
||||
AdminAuth: func(c *fiber.Ctx) error {
|
||||
return c.Next()
|
||||
},
|
||||
H5Auth: func(c *fiber.Ctx) error {
|
||||
return c.Next()
|
||||
},
|
||||
}
|
||||
routes.RegisterRoutes(app, services, middlewares)
|
||||
|
||||
return &testEnv{
|
||||
tx: tx,
|
||||
rdb: rdb,
|
||||
rdb: rdb,
|
||||
app: app,
|
||||
accountService: accService,
|
||||
postgresCleanup: func() {
|
||||
|
||||
@@ -34,7 +34,7 @@ import (
|
||||
// regressionTestEnv 回归测试环境
|
||||
type regressionTestEnv struct {
|
||||
tx *gorm.DB
|
||||
rdb *redis.Client
|
||||
rdb *redis.Client
|
||||
app *fiber.App
|
||||
postgresCleanup func()
|
||||
redisCleanup func()
|
||||
@@ -132,13 +132,21 @@ func setupRegressionTestEnv(t *testing.T) *regressionTestEnv {
|
||||
Role: roleHandler,
|
||||
Permission: permHandler,
|
||||
}
|
||||
middlewares := &bootstrap.Middlewares{}
|
||||
// 提供一个空操作的 AdminAuth 中间件,避免 nil panic
|
||||
middlewares := &bootstrap.Middlewares{
|
||||
AdminAuth: func(c *fiber.Ctx) error {
|
||||
return c.Next()
|
||||
},
|
||||
H5Auth: func(c *fiber.Ctx) error {
|
||||
return c.Next()
|
||||
},
|
||||
}
|
||||
routes.RegisterRoutes(app, services, middlewares)
|
||||
|
||||
return ®ressionTestEnv{
|
||||
tx: tx,
|
||||
tx: tx,
|
||||
rdb: rdb,
|
||||
app: app,
|
||||
app: app,
|
||||
postgresCleanup: func() {
|
||||
if err := pgContainer.Terminate(ctx); err != nil {
|
||||
t.Logf("终止 PostgreSQL 容器失败: %v", err)
|
||||
|
||||
333
tests/integration/device_test.go
Normal file
333
tests/integration/device_test.go
Normal file
@@ -0,0 +1,333 @@
|
||||
package integration
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/internal/bootstrap"
|
||||
internalMiddleware "github.com/break/junhong_cmp_fiber/internal/middleware"
|
||||
"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/queue"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/response"
|
||||
"github.com/break/junhong_cmp_fiber/tests/testutil"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/redis/go-redis/v9"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.uber.org/zap"
|
||||
"gorm.io/driver/postgres"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/logger"
|
||||
)
|
||||
|
||||
type deviceTestEnv struct {
|
||||
db *gorm.DB
|
||||
rdb *redis.Client
|
||||
tokenManager *auth.TokenManager
|
||||
app *fiber.App
|
||||
adminToken string
|
||||
t *testing.T
|
||||
}
|
||||
|
||||
func setupDeviceTestEnv(t *testing.T) *deviceTestEnv {
|
||||
t.Helper()
|
||||
|
||||
t.Setenv("CONFIG_ENV", "dev")
|
||||
t.Setenv("CONFIG_PATH", "../../configs/config.dev.yaml")
|
||||
cfg, err := config.Load()
|
||||
require.NoError(t, err)
|
||||
err = config.Set(cfg)
|
||||
require.NoError(t, err)
|
||||
|
||||
zapLogger, _ := zap.NewDevelopment()
|
||||
|
||||
dsn := "host=cxd.whcxd.cn port=16159 user=erp_pgsql password=erp_2025 dbname=junhong_cmp_test sslmode=disable TimeZone=Asia/Shanghai"
|
||||
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{
|
||||
Logger: logger.Default.LogMode(logger.Silent),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
rdb := redis.NewClient(&redis.Options{
|
||||
Addr: "cxd.whcxd.cn:16299",
|
||||
Password: "cpNbWtAaqgo1YJmbMp3h",
|
||||
DB: 15,
|
||||
})
|
||||
|
||||
ctx := context.Background()
|
||||
err = rdb.Ping(ctx).Err()
|
||||
require.NoError(t, err)
|
||||
|
||||
testPrefix := fmt.Sprintf("test:%s:", t.Name())
|
||||
keys, _ := rdb.Keys(ctx, testPrefix+"*").Result()
|
||||
if len(keys) > 0 {
|
||||
rdb.Del(ctx, keys...)
|
||||
}
|
||||
|
||||
tokenManager := auth.NewTokenManager(rdb, 24*time.Hour, 7*24*time.Hour)
|
||||
superAdmin := testutil.CreateSuperAdmin(t, db)
|
||||
adminToken, _ := testutil.GenerateTestToken(t, rdb, superAdmin, "web")
|
||||
|
||||
queueClient := queue.NewClient(rdb, zapLogger)
|
||||
|
||||
deps := &bootstrap.Dependencies{
|
||||
DB: db,
|
||||
Redis: rdb,
|
||||
Logger: zapLogger,
|
||||
TokenManager: tokenManager,
|
||||
QueueClient: queueClient,
|
||||
}
|
||||
|
||||
result, err := bootstrap.Bootstrap(deps)
|
||||
require.NoError(t, err)
|
||||
|
||||
app := fiber.New(fiber.Config{
|
||||
ErrorHandler: internalMiddleware.ErrorHandler(zapLogger),
|
||||
})
|
||||
|
||||
routes.RegisterRoutes(app, result.Handlers, result.Middlewares)
|
||||
|
||||
return &deviceTestEnv{
|
||||
db: db,
|
||||
rdb: rdb,
|
||||
tokenManager: tokenManager,
|
||||
app: app,
|
||||
adminToken: adminToken,
|
||||
t: t,
|
||||
}
|
||||
}
|
||||
|
||||
func (e *deviceTestEnv) teardown() {
|
||||
// 清理测试数据
|
||||
e.db.Exec("DELETE FROM tb_device WHERE device_no LIKE 'TEST%'")
|
||||
e.db.Exec("DELETE FROM tb_device_sim_binding WHERE device_id IN (SELECT id FROM tb_device WHERE device_no LIKE 'TEST%')")
|
||||
e.db.Exec("DELETE FROM tb_device_import_task WHERE task_no LIKE 'TEST%'")
|
||||
|
||||
ctx := context.Background()
|
||||
testPrefix := fmt.Sprintf("test:%s:", e.t.Name())
|
||||
keys, _ := e.rdb.Keys(ctx, testPrefix+"*").Result()
|
||||
if len(keys) > 0 {
|
||||
e.rdb.Del(ctx, keys...)
|
||||
}
|
||||
|
||||
e.rdb.Close()
|
||||
}
|
||||
|
||||
func TestDevice_List(t *testing.T) {
|
||||
env := setupDeviceTestEnv(t)
|
||||
defer env.teardown()
|
||||
|
||||
// 创建测试设备
|
||||
devices := []*model.Device{
|
||||
{DeviceNo: "TEST_DEVICE_001", DeviceName: "测试设备1", DeviceType: "router", MaxSimSlots: 4, Status: constants.DeviceStatusInStock},
|
||||
{DeviceNo: "TEST_DEVICE_002", DeviceName: "测试设备2", DeviceType: "router", MaxSimSlots: 2, Status: constants.DeviceStatusInStock},
|
||||
{DeviceNo: "TEST_DEVICE_003", DeviceName: "测试设备3", DeviceType: "mifi", MaxSimSlots: 1, Status: constants.DeviceStatusDistributed},
|
||||
}
|
||||
for _, device := range devices {
|
||||
require.NoError(t, env.db.Create(device).Error)
|
||||
}
|
||||
|
||||
t.Run("获取设备列表-无过滤", func(t *testing.T) {
|
||||
req := httptest.NewRequest("GET", "/api/admin/devices?page=1&page_size=20", nil)
|
||||
req.Header.Set("Authorization", "Bearer "+env.adminToken)
|
||||
|
||||
resp, err := env.app.Test(req, -1)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
assert.Equal(t, 200, resp.StatusCode)
|
||||
|
||||
var result response.Response
|
||||
err = json.NewDecoder(resp.Body).Decode(&result)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 0, result.Code)
|
||||
})
|
||||
|
||||
t.Run("获取设备列表-按设备类型过滤", func(t *testing.T) {
|
||||
req := httptest.NewRequest("GET", "/api/admin/devices?device_type=router", nil)
|
||||
req.Header.Set("Authorization", "Bearer "+env.adminToken)
|
||||
|
||||
resp, err := env.app.Test(req, -1)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
assert.Equal(t, 200, resp.StatusCode)
|
||||
|
||||
var result response.Response
|
||||
err = json.NewDecoder(resp.Body).Decode(&result)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 0, result.Code)
|
||||
})
|
||||
|
||||
t.Run("获取设备列表-按状态过滤", func(t *testing.T) {
|
||||
req := httptest.NewRequest("GET", fmt.Sprintf("/api/admin/devices?status=%d", constants.DeviceStatusInStock), nil)
|
||||
req.Header.Set("Authorization", "Bearer "+env.adminToken)
|
||||
|
||||
resp, err := env.app.Test(req, -1)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
assert.Equal(t, 200, resp.StatusCode)
|
||||
|
||||
var result response.Response
|
||||
err = json.NewDecoder(resp.Body).Decode(&result)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 0, result.Code)
|
||||
})
|
||||
|
||||
t.Run("未认证请求应返回错误", func(t *testing.T) {
|
||||
req := httptest.NewRequest("GET", "/api/admin/devices", nil)
|
||||
|
||||
resp, err := env.app.Test(req, -1)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
var result response.Response
|
||||
err = json.NewDecoder(resp.Body).Decode(&result)
|
||||
require.NoError(t, err)
|
||||
assert.NotEqual(t, 0, result.Code, "未认证请求应返回错误码")
|
||||
})
|
||||
}
|
||||
|
||||
func TestDevice_GetByID(t *testing.T) {
|
||||
env := setupDeviceTestEnv(t)
|
||||
defer env.teardown()
|
||||
|
||||
// 创建测试设备
|
||||
device := &model.Device{
|
||||
DeviceNo: "TEST_DEVICE_GET_001",
|
||||
DeviceName: "测试设备详情",
|
||||
DeviceType: "router",
|
||||
MaxSimSlots: 4,
|
||||
Status: constants.DeviceStatusInStock,
|
||||
}
|
||||
require.NoError(t, env.db.Create(device).Error)
|
||||
|
||||
t.Run("获取设备详情-成功", func(t *testing.T) {
|
||||
url := fmt.Sprintf("/api/admin/devices/%d", device.ID)
|
||||
req := httptest.NewRequest("GET", url, nil)
|
||||
req.Header.Set("Authorization", "Bearer "+env.adminToken)
|
||||
|
||||
resp, err := env.app.Test(req, -1)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
assert.Equal(t, 200, resp.StatusCode)
|
||||
|
||||
var result response.Response
|
||||
err = json.NewDecoder(resp.Body).Decode(&result)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 0, result.Code)
|
||||
|
||||
// 验证返回数据
|
||||
dataMap, ok := result.Data.(map[string]interface{})
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, "TEST_DEVICE_GET_001", dataMap["device_no"])
|
||||
})
|
||||
|
||||
t.Run("获取不存在的设备-应返回错误", func(t *testing.T) {
|
||||
req := httptest.NewRequest("GET", "/api/admin/devices/999999", nil)
|
||||
req.Header.Set("Authorization", "Bearer "+env.adminToken)
|
||||
|
||||
resp, err := env.app.Test(req, -1)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
var result response.Response
|
||||
err = json.NewDecoder(resp.Body).Decode(&result)
|
||||
require.NoError(t, err)
|
||||
assert.NotEqual(t, 0, result.Code, "不存在的设备应返回错误码")
|
||||
})
|
||||
}
|
||||
|
||||
func TestDevice_Delete(t *testing.T) {
|
||||
env := setupDeviceTestEnv(t)
|
||||
defer env.teardown()
|
||||
|
||||
// 创建测试设备
|
||||
device := &model.Device{
|
||||
DeviceNo: "TEST_DEVICE_DEL_001",
|
||||
DeviceName: "测试删除设备",
|
||||
DeviceType: "router",
|
||||
MaxSimSlots: 4,
|
||||
Status: constants.DeviceStatusInStock,
|
||||
}
|
||||
require.NoError(t, env.db.Create(device).Error)
|
||||
|
||||
t.Run("删除设备-成功", func(t *testing.T) {
|
||||
url := fmt.Sprintf("/api/admin/devices/%d", device.ID)
|
||||
req := httptest.NewRequest("DELETE", url, nil)
|
||||
req.Header.Set("Authorization", "Bearer "+env.adminToken)
|
||||
|
||||
resp, err := env.app.Test(req, -1)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
assert.Equal(t, 200, resp.StatusCode)
|
||||
|
||||
var result response.Response
|
||||
err = json.NewDecoder(resp.Body).Decode(&result)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 0, result.Code)
|
||||
|
||||
// 验证设备已被软删除
|
||||
var deletedDevice model.Device
|
||||
err = env.db.Unscoped().First(&deletedDevice, device.ID).Error
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, deletedDevice.DeletedAt)
|
||||
})
|
||||
}
|
||||
|
||||
func TestDeviceImport_TaskList(t *testing.T) {
|
||||
env := setupDeviceTestEnv(t)
|
||||
defer env.teardown()
|
||||
|
||||
// 创建测试导入任务
|
||||
task := &model.DeviceImportTask{
|
||||
TaskNo: "TEST_DEVICE_IMPORT_001",
|
||||
Status: model.ImportTaskStatusCompleted,
|
||||
BatchNo: "TEST_BATCH_001",
|
||||
TotalCount: 100,
|
||||
}
|
||||
require.NoError(t, env.db.Create(task).Error)
|
||||
|
||||
t.Run("获取导入任务列表", func(t *testing.T) {
|
||||
req := httptest.NewRequest("GET", "/api/admin/devices/import-tasks?page=1&page_size=20", nil)
|
||||
req.Header.Set("Authorization", "Bearer "+env.adminToken)
|
||||
|
||||
resp, err := env.app.Test(req, -1)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
assert.Equal(t, 200, resp.StatusCode)
|
||||
|
||||
var result response.Response
|
||||
err = json.NewDecoder(resp.Body).Decode(&result)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 0, result.Code)
|
||||
})
|
||||
|
||||
t.Run("获取导入任务详情", func(t *testing.T) {
|
||||
url := fmt.Sprintf("/api/admin/devices/import-tasks/%d", task.ID)
|
||||
req := httptest.NewRequest("GET", url, nil)
|
||||
req.Header.Set("Authorization", "Bearer "+env.adminToken)
|
||||
|
||||
resp, err := env.app.Test(req, -1)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
assert.Equal(t, 200, resp.StatusCode)
|
||||
|
||||
var result response.Response
|
||||
err = json.NewDecoder(resp.Body).Decode(&result)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 0, result.Code)
|
||||
})
|
||||
}
|
||||
@@ -17,6 +17,7 @@ import (
|
||||
testcontainers_postgres "github.com/testcontainers/testcontainers-go/modules/postgres"
|
||||
testcontainers_redis "github.com/testcontainers/testcontainers-go/modules/redis"
|
||||
"github.com/testcontainers/testcontainers-go/wait"
|
||||
"go.uber.org/zap"
|
||||
"gorm.io/driver/postgres"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/logger"
|
||||
@@ -37,7 +38,7 @@ import (
|
||||
// permTestEnv 权限测试环境
|
||||
type permTestEnv struct {
|
||||
tx *gorm.DB
|
||||
rdb *redis.Client
|
||||
rdb *redis.Client
|
||||
app *fiber.App
|
||||
permissionService *permissionService.Service
|
||||
cleanup func()
|
||||
@@ -105,23 +106,28 @@ func setupPermTestEnv(t *testing.T) *permTestEnv {
|
||||
// 初始化 Handler
|
||||
permHandler := admin.NewPermissionHandler(permSvc)
|
||||
|
||||
// 创建 Fiber App
|
||||
app := fiber.New(fiber.Config{
|
||||
ErrorHandler: func(c *fiber.Ctx, err error) error {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
|
||||
},
|
||||
ErrorHandler: errors.SafeErrorHandler(zap.NewNop()),
|
||||
})
|
||||
|
||||
app.Use(func(c *fiber.Ctx) error {
|
||||
ctx := middleware.SetUserContext(c.UserContext(), middleware.NewSimpleUserContext(1, constants.UserTypeSuperAdmin, 0))
|
||||
c.SetUserContext(ctx)
|
||||
return c.Next()
|
||||
})
|
||||
|
||||
// 注册路由
|
||||
services := &bootstrap.Handlers{
|
||||
Permission: permHandler,
|
||||
}
|
||||
middlewares := &bootstrap.Middlewares{}
|
||||
middlewares := &bootstrap.Middlewares{
|
||||
AdminAuth: func(c *fiber.Ctx) error { return c.Next() },
|
||||
H5Auth: func(c *fiber.Ctx) error { return c.Next() },
|
||||
}
|
||||
routes.RegisterRoutes(app, services, middlewares)
|
||||
|
||||
return &permTestEnv{
|
||||
tx: tx,
|
||||
rdb: rdb,
|
||||
rdb: rdb,
|
||||
app: app,
|
||||
permissionService: permSvc,
|
||||
cleanup: func() {
|
||||
@@ -140,14 +146,6 @@ func TestPermissionAPI_Create(t *testing.T) {
|
||||
env := setupPermTestEnv(t)
|
||||
defer env.cleanup()
|
||||
|
||||
// 添加测试中间件
|
||||
testUserID := uint(1)
|
||||
env.app.Use(func(c *fiber.Ctx) error {
|
||||
ctx := middleware.SetUserContext(c.UserContext(), middleware.NewSimpleUserContext(testUserID, constants.UserTypeSuperAdmin, 0))
|
||||
c.SetUserContext(ctx)
|
||||
return c.Next()
|
||||
})
|
||||
|
||||
t.Run("成功创建权限", func(t *testing.T) {
|
||||
reqBody := dto.CreatePermissionRequest{
|
||||
PermName: "用户管理",
|
||||
@@ -206,7 +204,6 @@ func TestPermissionAPI_Create(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("创建子权限", func(t *testing.T) {
|
||||
// 先创建父权限
|
||||
parentPerm := &model.Permission{
|
||||
PermName: "系统管理",
|
||||
PermCode: "system:manage",
|
||||
@@ -215,10 +212,9 @@ func TestPermissionAPI_Create(t *testing.T) {
|
||||
}
|
||||
env.tx.Create(parentPerm)
|
||||
|
||||
// 创建子权限
|
||||
reqBody := dto.CreatePermissionRequest{
|
||||
PermName: "用户列表",
|
||||
PermCode: "system:user:list",
|
||||
PermCode: "user:list",
|
||||
PermType: constants.PermissionTypeButton,
|
||||
ParentID: &parentPerm.ID,
|
||||
}
|
||||
@@ -231,10 +227,10 @@ func TestPermissionAPI_Create(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, fiber.StatusOK, resp.StatusCode)
|
||||
|
||||
// 验证父权限ID已设置
|
||||
var child model.Permission
|
||||
env.tx.Where("perm_code = ?", "system:user:list").First(&child)
|
||||
assert.NotNil(t, child.ParentID)
|
||||
err = env.tx.Where("perm_code = ?", "user:list").First(&child).Error
|
||||
require.NoError(t, err, "子权限应该已创建")
|
||||
require.NotNil(t, child.ParentID, "子权限的 ParentID 应该已设置")
|
||||
assert.Equal(t, parentPerm.ID, *child.ParentID)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -48,7 +48,10 @@ func TestPlatformAccountAPI_ListPlatformAccounts(t *testing.T) {
|
||||
})
|
||||
|
||||
services := &bootstrap.Handlers{Account: accountHandler}
|
||||
middlewares := &bootstrap.Middlewares{}
|
||||
middlewares := &bootstrap.Middlewares{
|
||||
AdminAuth: func(c *fiber.Ctx) error { return c.Next() },
|
||||
H5Auth: func(c *fiber.Ctx) error { return c.Next() },
|
||||
}
|
||||
routes.RegisterRoutes(app, services, middlewares)
|
||||
|
||||
superAdmin := &model.Account{
|
||||
@@ -137,7 +140,10 @@ func TestPlatformAccountAPI_UpdatePassword(t *testing.T) {
|
||||
})
|
||||
|
||||
services := &bootstrap.Handlers{Account: accountHandler}
|
||||
middlewares := &bootstrap.Middlewares{}
|
||||
middlewares := &bootstrap.Middlewares{
|
||||
AdminAuth: func(c *fiber.Ctx) error { return c.Next() },
|
||||
H5Auth: func(c *fiber.Ctx) error { return c.Next() },
|
||||
}
|
||||
routes.RegisterRoutes(app, services, middlewares)
|
||||
|
||||
testAccount := &model.Account{
|
||||
@@ -212,7 +218,10 @@ func TestPlatformAccountAPI_UpdateStatus(t *testing.T) {
|
||||
})
|
||||
|
||||
services := &bootstrap.Handlers{Account: accountHandler}
|
||||
middlewares := &bootstrap.Middlewares{}
|
||||
middlewares := &bootstrap.Middlewares{
|
||||
AdminAuth: func(c *fiber.Ctx) error { return c.Next() },
|
||||
H5Auth: func(c *fiber.Ctx) error { return c.Next() },
|
||||
}
|
||||
routes.RegisterRoutes(app, services, middlewares)
|
||||
|
||||
testAccount := &model.Account{
|
||||
@@ -282,7 +291,10 @@ func TestPlatformAccountAPI_AssignRoles(t *testing.T) {
|
||||
})
|
||||
|
||||
services := &bootstrap.Handlers{Account: accountHandler}
|
||||
middlewares := &bootstrap.Middlewares{}
|
||||
middlewares := &bootstrap.Middlewares{
|
||||
AdminAuth: func(c *fiber.Ctx) error { return c.Next() },
|
||||
H5Auth: func(c *fiber.Ctx) error { return c.Next() },
|
||||
}
|
||||
routes.RegisterRoutes(app, services, middlewares)
|
||||
|
||||
superAdmin := &model.Account{
|
||||
|
||||
@@ -37,7 +37,7 @@ import (
|
||||
// roleTestEnv 角色测试环境
|
||||
type roleTestEnv struct {
|
||||
tx *gorm.DB
|
||||
rdb *redis.Client
|
||||
rdb *redis.Client
|
||||
app *fiber.App
|
||||
roleService *roleService.Service
|
||||
postgresCleanup func()
|
||||
@@ -121,12 +121,15 @@ func setupRoleTestEnv(t *testing.T) *roleTestEnv {
|
||||
services := &bootstrap.Handlers{
|
||||
Role: roleHandler,
|
||||
}
|
||||
middlewares := &bootstrap.Middlewares{}
|
||||
middlewares := &bootstrap.Middlewares{
|
||||
AdminAuth: func(c *fiber.Ctx) error { return c.Next() },
|
||||
H5Auth: func(c *fiber.Ctx) error { return c.Next() },
|
||||
}
|
||||
routes.RegisterRoutes(app, services, middlewares)
|
||||
|
||||
return &roleTestEnv{
|
||||
tx: tx,
|
||||
rdb: rdb,
|
||||
rdb: rdb,
|
||||
app: app,
|
||||
roleService: roleSvc,
|
||||
postgresCleanup: func() {
|
||||
|
||||
@@ -2,7 +2,9 @@ package unit
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
@@ -321,13 +323,15 @@ func TestEnterpriseService_List(t *testing.T) {
|
||||
t.Run("查询企业列表-按名称筛选", func(t *testing.T) {
|
||||
ctx := createEnterpriseTestContext(1)
|
||||
|
||||
ts := time.Now().UnixNano()
|
||||
searchKey := fmt.Sprintf("列表测试企业_%d", ts)
|
||||
for i := 0; i < 3; i++ {
|
||||
createReq := &dto.CreateEnterpriseReq{
|
||||
EnterpriseName: "列表测试企业",
|
||||
EnterpriseCode: "ENT_LIST_" + string(rune('A'+i)),
|
||||
EnterpriseName: fmt.Sprintf("%s_%d", searchKey, i),
|
||||
EnterpriseCode: fmt.Sprintf("ENT_LIST_%d_%d", ts, i),
|
||||
ContactName: "联系人",
|
||||
ContactPhone: "1380000007" + string(rune('0'+i)),
|
||||
LoginPhone: "1390000007" + string(rune('0'+i)),
|
||||
ContactPhone: fmt.Sprintf("138%08d", ts%100000000+int64(i)),
|
||||
LoginPhone: fmt.Sprintf("139%08d", ts%100000000+int64(i)),
|
||||
Password: "Test123456",
|
||||
}
|
||||
_, err := service.Create(ctx, createReq)
|
||||
@@ -337,7 +341,7 @@ func TestEnterpriseService_List(t *testing.T) {
|
||||
req := &dto.EnterpriseListReq{
|
||||
Page: 1,
|
||||
PageSize: 20,
|
||||
EnterpriseName: "列表测试",
|
||||
EnterpriseName: searchKey,
|
||||
}
|
||||
|
||||
result, err := service.List(ctx, req)
|
||||
|
||||
@@ -30,73 +30,70 @@ func TestPermissionPlatformFilter_List(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
ctx = middleware.SetUserContext(ctx, middleware.NewSimpleUserContext(1, constants.UserTypeSuperAdmin, 0))
|
||||
|
||||
// 创建不同 platform 的权限
|
||||
baseReq := &dto.PermissionListRequest{Page: 1, PageSize: 1000}
|
||||
_, existingTotal, err := service.List(ctx, baseReq)
|
||||
require.NoError(t, err)
|
||||
|
||||
allReq := &dto.PermissionListRequest{Page: 1, PageSize: 1000, Platform: constants.PlatformAll}
|
||||
_, existingAllTotal, err := service.List(ctx, allReq)
|
||||
require.NoError(t, err)
|
||||
|
||||
webReq := &dto.PermissionListRequest{Page: 1, PageSize: 1000, Platform: constants.PlatformWeb}
|
||||
_, existingWebTotal, err := service.List(ctx, webReq)
|
||||
require.NoError(t, err)
|
||||
|
||||
h5Req := &dto.PermissionListRequest{Page: 1, PageSize: 1000, Platform: constants.PlatformH5}
|
||||
_, existingH5Total, err := service.List(ctx, h5Req)
|
||||
require.NoError(t, err)
|
||||
|
||||
permissions := []*model.Permission{
|
||||
{PermName: "全端菜单", PermCode: "menu:all", PermType: constants.PermissionTypeMenu, Platform: constants.PlatformAll, Status: constants.StatusEnabled},
|
||||
{PermName: "Web菜单", PermCode: "menu:web", PermType: constants.PermissionTypeMenu, Platform: constants.PlatformWeb, Status: constants.StatusEnabled},
|
||||
{PermName: "H5菜单", PermCode: "menu:h5", PermType: constants.PermissionTypeMenu, Platform: constants.PlatformH5, Status: constants.StatusEnabled},
|
||||
{PermName: "Web按钮", PermCode: "button:web", PermType: constants.PermissionTypeButton, Platform: constants.PlatformWeb, Status: constants.StatusEnabled},
|
||||
{PermName: "H5按钮", PermCode: "button:h5", PermType: constants.PermissionTypeButton, Platform: constants.PlatformH5, Status: constants.StatusEnabled},
|
||||
{PermName: "全端菜单_test", PermCode: "menu:all:test", PermType: constants.PermissionTypeMenu, Platform: constants.PlatformAll, Status: constants.StatusEnabled},
|
||||
{PermName: "Web菜单_test", PermCode: "menu:web:test", PermType: constants.PermissionTypeMenu, Platform: constants.PlatformWeb, Status: constants.StatusEnabled},
|
||||
{PermName: "H5菜单_test", PermCode: "menu:h5:test", PermType: constants.PermissionTypeMenu, Platform: constants.PlatformH5, Status: constants.StatusEnabled},
|
||||
{PermName: "Web按钮_test", PermCode: "button:web:test", PermType: constants.PermissionTypeButton, Platform: constants.PlatformWeb, Status: constants.StatusEnabled},
|
||||
{PermName: "H5按钮_test", PermCode: "button:h5:test", PermType: constants.PermissionTypeButton, Platform: constants.PlatformH5, Status: constants.StatusEnabled},
|
||||
}
|
||||
for _, perm := range permissions {
|
||||
require.NoError(t, tx.Create(perm).Error)
|
||||
}
|
||||
|
||||
// 测试查询全部权限(不过滤)
|
||||
t.Run("查询全部权限", func(t *testing.T) {
|
||||
req := &dto.PermissionListRequest{
|
||||
Page: 1,
|
||||
PageSize: 10,
|
||||
}
|
||||
perms, total, err := service.List(ctx, req)
|
||||
req := &dto.PermissionListRequest{Page: 1, PageSize: 1000}
|
||||
_, total, err := service.List(ctx, req)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, int64(5), total)
|
||||
assert.Len(t, perms, 5)
|
||||
assert.Equal(t, existingTotal+5, total)
|
||||
})
|
||||
|
||||
// 测试只查询 all 权限
|
||||
t.Run("只查询all端口权限", func(t *testing.T) {
|
||||
req := &dto.PermissionListRequest{
|
||||
Page: 1,
|
||||
PageSize: 10,
|
||||
Platform: constants.PlatformAll,
|
||||
}
|
||||
req := &dto.PermissionListRequest{Page: 1, PageSize: 1000, Platform: constants.PlatformAll}
|
||||
perms, total, err := service.List(ctx, req)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, int64(1), total)
|
||||
assert.Len(t, perms, 1)
|
||||
assert.Equal(t, "全端菜单", perms[0].PermName)
|
||||
assert.Equal(t, existingAllTotal+1, total)
|
||||
found := false
|
||||
for _, perm := range perms {
|
||||
if perm.PermName == "全端菜单_test" {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
assert.True(t, found, "应包含测试创建的全端菜单权限")
|
||||
})
|
||||
|
||||
// 测试只查询 web 权限
|
||||
t.Run("只查询web端口权限", func(t *testing.T) {
|
||||
req := &dto.PermissionListRequest{
|
||||
Page: 1,
|
||||
PageSize: 10,
|
||||
Platform: constants.PlatformWeb,
|
||||
}
|
||||
req := &dto.PermissionListRequest{Page: 1, PageSize: 1000, Platform: constants.PlatformWeb}
|
||||
perms, total, err := service.List(ctx, req)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, int64(2), total)
|
||||
assert.Len(t, perms, 2)
|
||||
// 验证都是 web 端口的权限
|
||||
assert.Equal(t, existingWebTotal+2, total)
|
||||
for _, perm := range perms {
|
||||
assert.Equal(t, constants.PlatformWeb, perm.Platform)
|
||||
}
|
||||
})
|
||||
|
||||
// 测试只查询 h5 权限
|
||||
t.Run("只查询h5端口权限", func(t *testing.T) {
|
||||
req := &dto.PermissionListRequest{
|
||||
Page: 1,
|
||||
PageSize: 10,
|
||||
Platform: constants.PlatformH5,
|
||||
}
|
||||
req := &dto.PermissionListRequest{Page: 1, PageSize: 1000, Platform: constants.PlatformH5}
|
||||
perms, total, err := service.List(ctx, req)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, int64(2), total)
|
||||
assert.Len(t, perms, 2)
|
||||
// 验证都是 h5 端口的权限
|
||||
assert.Equal(t, existingH5Total+2, total)
|
||||
for _, perm := range perms {
|
||||
assert.Equal(t, constants.PlatformH5, perm.Platform)
|
||||
}
|
||||
@@ -184,10 +181,13 @@ func TestPermissionPlatformFilter_Tree(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
ctx = middleware.SetUserContext(ctx, middleware.NewSimpleUserContext(1, constants.UserTypeSuperAdmin, 0))
|
||||
|
||||
// 创建层级权限
|
||||
existingTree, err := service.GetTree(ctx, nil)
|
||||
require.NoError(t, err)
|
||||
existingCount := len(existingTree)
|
||||
|
||||
parent := &model.Permission{
|
||||
PermName: "系统管理",
|
||||
PermCode: "system:manage",
|
||||
PermName: "系统管理_tree_test",
|
||||
PermCode: "system:manage:tree_test",
|
||||
PermType: constants.PermissionTypeMenu,
|
||||
Platform: constants.PlatformWeb,
|
||||
Status: constants.StatusEnabled,
|
||||
@@ -195,8 +195,8 @@ func TestPermissionPlatformFilter_Tree(t *testing.T) {
|
||||
require.NoError(t, tx.Create(parent).Error)
|
||||
|
||||
child := &model.Permission{
|
||||
PermName: "用户管理",
|
||||
PermCode: "user:manage",
|
||||
PermName: "用户管理_tree_test",
|
||||
PermCode: "user:manage:tree_test",
|
||||
PermType: constants.PermissionTypeMenu,
|
||||
Platform: constants.PlatformWeb,
|
||||
ParentID: &parent.ID,
|
||||
@@ -204,19 +204,22 @@ func TestPermissionPlatformFilter_Tree(t *testing.T) {
|
||||
}
|
||||
require.NoError(t, tx.Create(child).Error)
|
||||
|
||||
// 获取权限树
|
||||
tree, err := service.GetTree(ctx, nil)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, tree, 1)
|
||||
assert.Len(t, tree, existingCount+1)
|
||||
|
||||
// 验证父节点
|
||||
root := tree[0]
|
||||
assert.Equal(t, "系统管理", root.PermName)
|
||||
assert.Equal(t, constants.PlatformWeb, root.Platform)
|
||||
var testRoot *dto.PermissionTreeNode
|
||||
for _, node := range tree {
|
||||
if node.PermName == "系统管理_tree_test" {
|
||||
testRoot = node
|
||||
break
|
||||
}
|
||||
}
|
||||
require.NotNil(t, testRoot, "应包含测试创建的父节点")
|
||||
assert.Equal(t, constants.PlatformWeb, testRoot.Platform)
|
||||
|
||||
// 验证子节点
|
||||
require.Len(t, root.Children, 1)
|
||||
childNode := root.Children[0]
|
||||
assert.Equal(t, "用户管理", childNode.PermName)
|
||||
require.Len(t, testRoot.Children, 1)
|
||||
childNode := testRoot.Children[0]
|
||||
assert.Equal(t, "用户管理_tree_test", childNode.PermName)
|
||||
assert.Equal(t, constants.PlatformWeb, childNode.Platform)
|
||||
}
|
||||
|
||||
@@ -1,328 +0,0 @@
|
||||
package unit
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/internal/model"
|
||||
"github.com/break/junhong_cmp_fiber/internal/store/postgres"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
||||
"github.com/break/junhong_cmp_fiber/tests/testutils"
|
||||
)
|
||||
|
||||
// TestPersonalCustomerStore_Create 测试创建个人客户
|
||||
func TestPersonalCustomerStore_Create(t *testing.T) {
|
||||
tx := testutils.NewTestTransaction(t)
|
||||
rdb := testutils.GetTestRedis(t)
|
||||
testutils.CleanTestRedisKeys(t, rdb)
|
||||
|
||||
store := postgres.NewPersonalCustomerStore(tx, rdb)
|
||||
ctx := context.Background()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
customer *model.PersonalCustomer
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "创建基本个人客户",
|
||||
customer: &model.PersonalCustomer{
|
||||
WxOpenID: "wx_openid_test_a",
|
||||
WxUnionID: "wx_unionid_test_a",
|
||||
Nickname: "测试用户A",
|
||||
Status: constants.StatusEnabled,
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "创建带微信信息的个人客户",
|
||||
customer: &model.PersonalCustomer{
|
||||
WxOpenID: "wx_openid_123456",
|
||||
WxUnionID: "wx_unionid_abcdef",
|
||||
Nickname: "测试用户B",
|
||||
AvatarURL: "https://example.com/avatar.jpg",
|
||||
Status: constants.StatusEnabled,
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := store.Create(ctx, tt.customer)
|
||||
if tt.wantErr {
|
||||
assert.Error(t, err)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
assert.NotZero(t, tt.customer.ID)
|
||||
assert.NotZero(t, tt.customer.CreatedAt)
|
||||
assert.NotZero(t, tt.customer.UpdatedAt)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestPersonalCustomerStore_GetByID 测试根据 ID 查询个人客户
|
||||
func TestPersonalCustomerStore_GetByID(t *testing.T) {
|
||||
tx := testutils.NewTestTransaction(t)
|
||||
rdb := testutils.GetTestRedis(t)
|
||||
testutils.CleanTestRedisKeys(t, rdb)
|
||||
|
||||
store := postgres.NewPersonalCustomerStore(tx, rdb)
|
||||
ctx := context.Background()
|
||||
|
||||
// 创建测试客户
|
||||
customer := &model.PersonalCustomer{
|
||||
WxOpenID: "wx_openid_test_getbyid",
|
||||
WxUnionID: "wx_unionid_test_getbyid",
|
||||
Nickname: "测试客户",
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
err := store.Create(ctx, customer)
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Run("查询存在的客户", func(t *testing.T) {
|
||||
found, err := store.GetByID(ctx, customer.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, customer.WxOpenID, found.WxOpenID)
|
||||
assert.Equal(t, customer.Nickname, found.Nickname)
|
||||
})
|
||||
|
||||
t.Run("查询不存在的客户", func(t *testing.T) {
|
||||
_, err := store.GetByID(ctx, 99999)
|
||||
assert.Error(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
// TestPersonalCustomerStore_GetByPhone 测试根据手机号查询
|
||||
func TestPersonalCustomerStore_GetByPhone(t *testing.T) {
|
||||
tx := testutils.NewTestTransaction(t)
|
||||
rdb := testutils.GetTestRedis(t)
|
||||
testutils.CleanTestRedisKeys(t, rdb)
|
||||
|
||||
store := postgres.NewPersonalCustomerStore(tx, rdb)
|
||||
ctx := context.Background()
|
||||
|
||||
// 创建测试客户
|
||||
customer := &model.PersonalCustomer{
|
||||
WxOpenID: "wx_openid_test_phone",
|
||||
WxUnionID: "wx_unionid_test_phone",
|
||||
Nickname: "测试客户",
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
err := store.Create(ctx, customer)
|
||||
require.NoError(t, err)
|
||||
|
||||
// 创建手机号绑定记录
|
||||
customerPhone := &model.PersonalCustomerPhone{
|
||||
CustomerID: customer.ID,
|
||||
Phone: "13800000001",
|
||||
IsPrimary: true,
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
err = tx.Create(customerPhone).Error
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Run("根据手机号查询", func(t *testing.T) {
|
||||
found, err := store.GetByPhone(ctx, "13800000001")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, customer.ID, found.ID)
|
||||
assert.Equal(t, customer.Nickname, found.Nickname)
|
||||
})
|
||||
|
||||
t.Run("查询不存在的手机号", func(t *testing.T) {
|
||||
_, err := store.GetByPhone(ctx, "99900000000")
|
||||
assert.Error(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
// TestPersonalCustomerStore_GetByWxOpenID 测试根据微信 OpenID 查询
|
||||
func TestPersonalCustomerStore_GetByWxOpenID(t *testing.T) {
|
||||
tx := testutils.NewTestTransaction(t)
|
||||
rdb := testutils.GetTestRedis(t)
|
||||
testutils.CleanTestRedisKeys(t, rdb)
|
||||
|
||||
store := postgres.NewPersonalCustomerStore(tx, rdb)
|
||||
ctx := context.Background()
|
||||
|
||||
// 创建测试客户
|
||||
customer := &model.PersonalCustomer{
|
||||
WxOpenID: "wx_openid_unique",
|
||||
WxUnionID: "wx_unionid_unique",
|
||||
Nickname: "测试客户",
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
err := store.Create(ctx, customer)
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Run("根据微信OpenID查询", func(t *testing.T) {
|
||||
found, err := store.GetByWxOpenID(ctx, "wx_openid_unique")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, customer.ID, found.ID)
|
||||
assert.Equal(t, customer.WxOpenID, found.WxOpenID)
|
||||
})
|
||||
|
||||
t.Run("查询不存在的OpenID", func(t *testing.T) {
|
||||
_, err := store.GetByWxOpenID(ctx, "nonexistent_openid")
|
||||
assert.Error(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
// TestPersonalCustomerStore_Update 测试更新个人客户
|
||||
func TestPersonalCustomerStore_Update(t *testing.T) {
|
||||
tx := testutils.NewTestTransaction(t)
|
||||
rdb := testutils.GetTestRedis(t)
|
||||
testutils.CleanTestRedisKeys(t, rdb)
|
||||
|
||||
store := postgres.NewPersonalCustomerStore(tx, rdb)
|
||||
ctx := context.Background()
|
||||
|
||||
// 创建测试客户
|
||||
customer := &model.PersonalCustomer{
|
||||
WxOpenID: "wx_openid_test_update",
|
||||
WxUnionID: "wx_unionid_test_update",
|
||||
Nickname: "原昵称",
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
err := store.Create(ctx, customer)
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Run("更新客户信息", func(t *testing.T) {
|
||||
customer.Nickname = "新昵称"
|
||||
customer.AvatarURL = "https://example.com/new_avatar.jpg"
|
||||
|
||||
err := store.Update(ctx, customer)
|
||||
require.NoError(t, err)
|
||||
|
||||
// 验证更新
|
||||
found, err := store.GetByID(ctx, customer.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "新昵称", found.Nickname)
|
||||
assert.Equal(t, "https://example.com/new_avatar.jpg", found.AvatarURL)
|
||||
})
|
||||
|
||||
t.Run("绑定微信信息", func(t *testing.T) {
|
||||
customer.WxOpenID = "wx_openid_new"
|
||||
customer.WxUnionID = "wx_unionid_new"
|
||||
err := store.Update(ctx, customer)
|
||||
require.NoError(t, err)
|
||||
|
||||
found, err := store.GetByID(ctx, customer.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "wx_openid_new", found.WxOpenID)
|
||||
assert.Equal(t, "wx_unionid_new", found.WxUnionID)
|
||||
})
|
||||
|
||||
t.Run("更新客户状态", func(t *testing.T) {
|
||||
customer.Status = constants.StatusDisabled
|
||||
err := store.Update(ctx, customer)
|
||||
require.NoError(t, err)
|
||||
|
||||
found, err := store.GetByID(ctx, customer.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, constants.StatusDisabled, found.Status)
|
||||
})
|
||||
}
|
||||
|
||||
// TestPersonalCustomerStore_Delete 测试软删除个人客户
|
||||
func TestPersonalCustomerStore_Delete(t *testing.T) {
|
||||
tx := testutils.NewTestTransaction(t)
|
||||
rdb := testutils.GetTestRedis(t)
|
||||
testutils.CleanTestRedisKeys(t, rdb)
|
||||
|
||||
store := postgres.NewPersonalCustomerStore(tx, rdb)
|
||||
ctx := context.Background()
|
||||
|
||||
// 创建测试客户
|
||||
customer := &model.PersonalCustomer{
|
||||
WxOpenID: "wx_openid_test_delete",
|
||||
WxUnionID: "wx_unionid_test_delete",
|
||||
Nickname: "待删除客户",
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
err := store.Create(ctx, customer)
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Run("软删除客户", func(t *testing.T) {
|
||||
err := store.Delete(ctx, customer.ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
// 验证已被软删除
|
||||
_, err = store.GetByID(ctx, customer.ID)
|
||||
assert.Error(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
// TestPersonalCustomerStore_List 测试查询客户列表
|
||||
func TestPersonalCustomerStore_List(t *testing.T) {
|
||||
tx := testutils.NewTestTransaction(t)
|
||||
rdb := testutils.GetTestRedis(t)
|
||||
testutils.CleanTestRedisKeys(t, rdb)
|
||||
|
||||
store := postgres.NewPersonalCustomerStore(tx, rdb)
|
||||
ctx := context.Background()
|
||||
|
||||
// 创建多个测试客户
|
||||
for i := 1; i <= 5; i++ {
|
||||
customer := &model.PersonalCustomer{
|
||||
WxOpenID: testutils.GenerateUsername("wx_openid_list_", i),
|
||||
WxUnionID: testutils.GenerateUsername("wx_unionid_list_", i),
|
||||
Nickname: testutils.GenerateUsername("客户", i),
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
err := store.Create(ctx, customer)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
t.Run("分页查询", func(t *testing.T) {
|
||||
customers, total, err := store.List(ctx, nil, nil)
|
||||
require.NoError(t, err)
|
||||
assert.GreaterOrEqual(t, len(customers), 5)
|
||||
assert.GreaterOrEqual(t, total, int64(5))
|
||||
})
|
||||
|
||||
t.Run("带过滤条件查询", func(t *testing.T) {
|
||||
filters := map[string]interface{}{
|
||||
"status": constants.StatusEnabled,
|
||||
}
|
||||
customers, _, err := store.List(ctx, nil, filters)
|
||||
require.NoError(t, err)
|
||||
for _, c := range customers {
|
||||
assert.Equal(t, constants.StatusEnabled, c.Status)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestPersonalCustomerStore_UniqueConstraints 测试唯一约束
|
||||
func TestPersonalCustomerStore_UniqueConstraints(t *testing.T) {
|
||||
tx := testutils.NewTestTransaction(t)
|
||||
rdb := testutils.GetTestRedis(t)
|
||||
testutils.CleanTestRedisKeys(t, rdb)
|
||||
|
||||
store := postgres.NewPersonalCustomerStore(tx, rdb)
|
||||
ctx := context.Background()
|
||||
|
||||
// 创建测试客户
|
||||
customer := &model.PersonalCustomer{
|
||||
WxOpenID: "wx_openid_unique_test",
|
||||
WxUnionID: "wx_unionid_unique_test",
|
||||
Nickname: "唯一测试客户",
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
err := store.Create(ctx, customer)
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Run("重复微信OpenID应失败", func(t *testing.T) {
|
||||
duplicate := &model.PersonalCustomer{
|
||||
WxOpenID: "wx_openid_unique_test", // 重复
|
||||
WxUnionID: "wx_unionid_different",
|
||||
Nickname: "另一个客户",
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
err := store.Create(ctx, duplicate)
|
||||
assert.Error(t, err)
|
||||
})
|
||||
}
|
||||
@@ -1,555 +0,0 @@
|
||||
package unit
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/bytedance/sonic"
|
||||
"github.com/hibiken/asynq"
|
||||
"github.com/redis/go-redis/v9"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
||||
)
|
||||
|
||||
// TestQueueClientEnqueue 测试任务入队
|
||||
func TestQueueClientEnqueue(t *testing.T) {
|
||||
rdb := redis.NewClient(&redis.Options{
|
||||
Addr: "localhost:6379",
|
||||
})
|
||||
defer func() { _ = rdb.Close() }()
|
||||
|
||||
ctx := context.Background()
|
||||
rdb.FlushDB(ctx)
|
||||
|
||||
client := asynq.NewClient(asynq.RedisClientOpt{
|
||||
Addr: "localhost:6379",
|
||||
})
|
||||
defer func() { _ = client.Close() }()
|
||||
|
||||
payload := map[string]string{
|
||||
"request_id": "test-001",
|
||||
"to": "test@example.com",
|
||||
}
|
||||
|
||||
payloadBytes, err := sonic.Marshal(payload)
|
||||
require.NoError(t, err)
|
||||
|
||||
task := asynq.NewTask(constants.TaskTypeEmailSend, payloadBytes)
|
||||
info, err := client.Enqueue(task)
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.NotEmpty(t, info.ID)
|
||||
assert.Equal(t, constants.QueueDefault, info.Queue)
|
||||
}
|
||||
|
||||
// TestQueueClientEnqueueWithOptions 测试带选项的任务入队
|
||||
func TestQueueClientEnqueueWithOptions(t *testing.T) {
|
||||
rdb := redis.NewClient(&redis.Options{
|
||||
Addr: "localhost:6379",
|
||||
})
|
||||
defer func() { _ = rdb.Close() }()
|
||||
|
||||
ctx := context.Background()
|
||||
rdb.FlushDB(ctx)
|
||||
|
||||
client := asynq.NewClient(asynq.RedisClientOpt{
|
||||
Addr: "localhost:6379",
|
||||
})
|
||||
defer func() { _ = client.Close() }()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
opts []asynq.Option
|
||||
verify func(*testing.T, *asynq.TaskInfo)
|
||||
}{
|
||||
{
|
||||
name: "Custom Queue",
|
||||
opts: []asynq.Option{
|
||||
asynq.Queue(constants.QueueCritical),
|
||||
},
|
||||
verify: func(t *testing.T, info *asynq.TaskInfo) {
|
||||
assert.Equal(t, constants.QueueCritical, info.Queue)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Custom Retry",
|
||||
opts: []asynq.Option{
|
||||
asynq.MaxRetry(3),
|
||||
},
|
||||
verify: func(t *testing.T, info *asynq.TaskInfo) {
|
||||
assert.Equal(t, 3, info.MaxRetry)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Custom Timeout",
|
||||
opts: []asynq.Option{
|
||||
asynq.Timeout(5 * time.Minute),
|
||||
},
|
||||
verify: func(t *testing.T, info *asynq.TaskInfo) {
|
||||
assert.Equal(t, 5*time.Minute, info.Timeout)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Delayed Task",
|
||||
opts: []asynq.Option{
|
||||
asynq.ProcessIn(10 * time.Second),
|
||||
},
|
||||
verify: func(t *testing.T, info *asynq.TaskInfo) {
|
||||
assert.True(t, info.NextProcessAt.After(time.Now()))
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Combined Options",
|
||||
opts: []asynq.Option{
|
||||
asynq.Queue(constants.QueueCritical),
|
||||
asynq.MaxRetry(5),
|
||||
asynq.Timeout(10 * time.Minute),
|
||||
},
|
||||
verify: func(t *testing.T, info *asynq.TaskInfo) {
|
||||
assert.Equal(t, constants.QueueCritical, info.Queue)
|
||||
assert.Equal(t, 5, info.MaxRetry)
|
||||
assert.Equal(t, 10*time.Minute, info.Timeout)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
payload := map[string]string{
|
||||
"request_id": "test-" + tt.name,
|
||||
}
|
||||
|
||||
payloadBytes, err := sonic.Marshal(payload)
|
||||
require.NoError(t, err)
|
||||
|
||||
task := asynq.NewTask(constants.TaskTypeEmailSend, payloadBytes)
|
||||
info, err := client.Enqueue(task, tt.opts...)
|
||||
|
||||
require.NoError(t, err)
|
||||
tt.verify(t, info)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestQueueClientTaskUniqueness 测试任务唯一性
|
||||
func TestQueueClientTaskUniqueness(t *testing.T) {
|
||||
rdb := redis.NewClient(&redis.Options{
|
||||
Addr: "localhost:6379",
|
||||
})
|
||||
defer func() { _ = rdb.Close() }()
|
||||
|
||||
ctx := context.Background()
|
||||
rdb.FlushDB(ctx)
|
||||
|
||||
client := asynq.NewClient(asynq.RedisClientOpt{
|
||||
Addr: "localhost:6379",
|
||||
})
|
||||
defer func() { _ = client.Close() }()
|
||||
|
||||
payload := map[string]string{
|
||||
"request_id": "unique-001",
|
||||
"to": "test@example.com",
|
||||
}
|
||||
|
||||
payloadBytes, err := sonic.Marshal(payload)
|
||||
require.NoError(t, err)
|
||||
|
||||
// 第一次提交
|
||||
task1 := asynq.NewTask(constants.TaskTypeEmailSend, payloadBytes)
|
||||
info1, err := client.Enqueue(task1,
|
||||
asynq.TaskID("unique-task-001"),
|
||||
asynq.Unique(1*time.Hour),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, info1)
|
||||
|
||||
// 第二次提交(重复)
|
||||
task2 := asynq.NewTask(constants.TaskTypeEmailSend, payloadBytes)
|
||||
info2, err := client.Enqueue(task2,
|
||||
asynq.TaskID("unique-task-001"),
|
||||
asynq.Unique(1*time.Hour),
|
||||
)
|
||||
|
||||
// 应该返回错误(任务已存在)
|
||||
assert.Error(t, err)
|
||||
assert.Nil(t, info2)
|
||||
}
|
||||
|
||||
// TestQueuePriorityWeights 测试队列优先级权重
|
||||
func TestQueuePriorityWeights(t *testing.T) {
|
||||
queues := map[string]int{
|
||||
constants.QueueCritical: 6,
|
||||
constants.QueueDefault: 3,
|
||||
constants.QueueLow: 1,
|
||||
}
|
||||
|
||||
// 验证权重总和
|
||||
totalWeight := 0
|
||||
for _, weight := range queues {
|
||||
totalWeight += weight
|
||||
}
|
||||
assert.Equal(t, 10, totalWeight)
|
||||
|
||||
// 验证权重比例
|
||||
assert.Equal(t, 0.6, float64(queues[constants.QueueCritical])/float64(totalWeight))
|
||||
assert.Equal(t, 0.3, float64(queues[constants.QueueDefault])/float64(totalWeight))
|
||||
assert.Equal(t, 0.1, float64(queues[constants.QueueLow])/float64(totalWeight))
|
||||
}
|
||||
|
||||
// TestTaskPayloadSizeLimit 测试任务载荷大小限制
|
||||
func TestTaskPayloadSizeLimit(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
payloadSize int
|
||||
shouldError bool
|
||||
}{
|
||||
{
|
||||
name: "Small Payload (1KB)",
|
||||
payloadSize: 1024,
|
||||
shouldError: false,
|
||||
},
|
||||
{
|
||||
name: "Medium Payload (100KB)",
|
||||
payloadSize: 100 * 1024,
|
||||
shouldError: false,
|
||||
},
|
||||
{
|
||||
name: "Large Payload (1MB)",
|
||||
payloadSize: 1024 * 1024,
|
||||
shouldError: false,
|
||||
},
|
||||
// Redis 默认支持最大 512MB,但实际应用中不建议超过 1MB
|
||||
}
|
||||
|
||||
rdb := redis.NewClient(&redis.Options{
|
||||
Addr: "localhost:6379",
|
||||
})
|
||||
defer func() { _ = rdb.Close() }()
|
||||
|
||||
ctx := context.Background()
|
||||
rdb.FlushDB(ctx)
|
||||
|
||||
client := asynq.NewClient(asynq.RedisClientOpt{
|
||||
Addr: "localhost:6379",
|
||||
})
|
||||
defer func() { _ = client.Close() }()
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// 创建指定大小的载荷
|
||||
largeData := make([]byte, tt.payloadSize)
|
||||
for i := range largeData {
|
||||
largeData[i] = byte(i % 256)
|
||||
}
|
||||
|
||||
payload := map[string]interface{}{
|
||||
"request_id": "size-test-001",
|
||||
"data": largeData,
|
||||
}
|
||||
|
||||
payloadBytes, err := sonic.Marshal(payload)
|
||||
require.NoError(t, err)
|
||||
|
||||
task := asynq.NewTask(constants.TaskTypeDataSync, payloadBytes)
|
||||
info, err := client.Enqueue(task)
|
||||
|
||||
if tt.shouldError {
|
||||
assert.Error(t, err)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, info)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestTaskScheduling 测试任务调度
|
||||
func TestTaskScheduling(t *testing.T) {
|
||||
rdb := redis.NewClient(&redis.Options{
|
||||
Addr: "localhost:6379",
|
||||
})
|
||||
defer func() { _ = rdb.Close() }()
|
||||
|
||||
ctx := context.Background()
|
||||
rdb.FlushDB(ctx)
|
||||
|
||||
client := asynq.NewClient(asynq.RedisClientOpt{
|
||||
Addr: "localhost:6379",
|
||||
})
|
||||
defer func() { _ = client.Close() }()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
scheduleOpt asynq.Option
|
||||
expectedTime time.Time
|
||||
}{
|
||||
{
|
||||
name: "Process In 5 Seconds",
|
||||
scheduleOpt: asynq.ProcessIn(5 * time.Second),
|
||||
expectedTime: time.Now().Add(5 * time.Second),
|
||||
},
|
||||
{
|
||||
name: "Process At Specific Time",
|
||||
scheduleOpt: asynq.ProcessAt(time.Now().Add(10 * time.Second)),
|
||||
expectedTime: time.Now().Add(10 * time.Second),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
payload := map[string]string{
|
||||
"request_id": "schedule-test-" + tt.name,
|
||||
}
|
||||
|
||||
payloadBytes, err := sonic.Marshal(payload)
|
||||
require.NoError(t, err)
|
||||
|
||||
task := asynq.NewTask(constants.TaskTypeEmailSend, payloadBytes)
|
||||
info, err := client.Enqueue(task, tt.scheduleOpt)
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.True(t, info.NextProcessAt.After(time.Now()))
|
||||
// 允许 1 秒的误差
|
||||
assert.WithinDuration(t, tt.expectedTime, info.NextProcessAt, 1*time.Second)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestQueueInspectorStats 测试队列统计
|
||||
func TestQueueInspectorStats(t *testing.T) {
|
||||
rdb := redis.NewClient(&redis.Options{
|
||||
Addr: "localhost:6379",
|
||||
})
|
||||
defer func() { _ = rdb.Close() }()
|
||||
|
||||
ctx := context.Background()
|
||||
rdb.FlushDB(ctx)
|
||||
|
||||
client := asynq.NewClient(asynq.RedisClientOpt{
|
||||
Addr: "localhost:6379",
|
||||
})
|
||||
defer func() { _ = client.Close() }()
|
||||
|
||||
// 提交一些任务
|
||||
for i := 0; i < 5; i++ {
|
||||
payload := map[string]string{
|
||||
"request_id": "stats-test-" + string(rune(i)),
|
||||
}
|
||||
|
||||
payloadBytes, err := sonic.Marshal(payload)
|
||||
require.NoError(t, err)
|
||||
|
||||
task := asynq.NewTask(constants.TaskTypeEmailSend, payloadBytes)
|
||||
_, err = client.Enqueue(task)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
// 使用 Inspector 查询统计
|
||||
inspector := asynq.NewInspector(asynq.RedisClientOpt{
|
||||
Addr: "localhost:6379",
|
||||
})
|
||||
defer func() { _ = inspector.Close() }()
|
||||
|
||||
info, err := inspector.GetQueueInfo(constants.QueueDefault)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, 5, info.Pending)
|
||||
assert.Equal(t, 0, info.Active)
|
||||
assert.Equal(t, 0, info.Completed)
|
||||
}
|
||||
|
||||
// TestTaskRetention 测试任务保留策略
|
||||
func TestTaskRetention(t *testing.T) {
|
||||
rdb := redis.NewClient(&redis.Options{
|
||||
Addr: "localhost:6379",
|
||||
})
|
||||
defer func() { _ = rdb.Close() }()
|
||||
|
||||
ctx := context.Background()
|
||||
rdb.FlushDB(ctx)
|
||||
|
||||
client := asynq.NewClient(asynq.RedisClientOpt{
|
||||
Addr: "localhost:6379",
|
||||
})
|
||||
defer func() { _ = client.Close() }()
|
||||
|
||||
payload := map[string]string{
|
||||
"request_id": "retention-test-001",
|
||||
}
|
||||
|
||||
payloadBytes, err := sonic.Marshal(payload)
|
||||
require.NoError(t, err)
|
||||
|
||||
// 提交任务并设置保留时间
|
||||
task := asynq.NewTask(constants.TaskTypeEmailSend, payloadBytes)
|
||||
info, err := client.Enqueue(task,
|
||||
asynq.Retention(24*time.Hour), // 保留 24 小时
|
||||
)
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, info)
|
||||
}
|
||||
|
||||
// TestQueueDraining 测试队列暂停和恢复
|
||||
func TestQueueDraining(t *testing.T) {
|
||||
rdb := redis.NewClient(&redis.Options{
|
||||
Addr: "localhost:6379",
|
||||
})
|
||||
defer func() { _ = rdb.Close() }()
|
||||
|
||||
ctx := context.Background()
|
||||
rdb.FlushDB(ctx)
|
||||
|
||||
inspector := asynq.NewInspector(asynq.RedisClientOpt{
|
||||
Addr: "localhost:6379",
|
||||
})
|
||||
defer func() { _ = inspector.Close() }()
|
||||
|
||||
// 暂停队列
|
||||
err := inspector.PauseQueue(constants.QueueDefault)
|
||||
require.NoError(t, err)
|
||||
|
||||
// 检查队列是否已暂停
|
||||
info, err := inspector.GetQueueInfo(constants.QueueDefault)
|
||||
require.NoError(t, err)
|
||||
assert.True(t, info.Paused)
|
||||
|
||||
// 恢复队列
|
||||
err = inspector.UnpauseQueue(constants.QueueDefault)
|
||||
require.NoError(t, err)
|
||||
|
||||
// 检查队列是否已恢复
|
||||
info, err = inspector.GetQueueInfo(constants.QueueDefault)
|
||||
require.NoError(t, err)
|
||||
assert.False(t, info.Paused)
|
||||
}
|
||||
|
||||
// TestTaskCancellation 测试任务取消
|
||||
func TestTaskCancellation(t *testing.T) {
|
||||
rdb := redis.NewClient(&redis.Options{
|
||||
Addr: "localhost:6379",
|
||||
})
|
||||
defer func() { _ = rdb.Close() }()
|
||||
|
||||
ctx := context.Background()
|
||||
rdb.FlushDB(ctx)
|
||||
|
||||
client := asynq.NewClient(asynq.RedisClientOpt{
|
||||
Addr: "localhost:6379",
|
||||
})
|
||||
defer func() { _ = client.Close() }()
|
||||
|
||||
payload := map[string]string{
|
||||
"request_id": "cancel-test-001",
|
||||
}
|
||||
|
||||
payloadBytes, err := sonic.Marshal(payload)
|
||||
require.NoError(t, err)
|
||||
|
||||
// 提交任务
|
||||
task := asynq.NewTask(constants.TaskTypeEmailSend, payloadBytes)
|
||||
info, err := client.Enqueue(task)
|
||||
require.NoError(t, err)
|
||||
|
||||
// 取消任务
|
||||
inspector := asynq.NewInspector(asynq.RedisClientOpt{
|
||||
Addr: "localhost:6379",
|
||||
})
|
||||
defer func() { _ = inspector.Close() }()
|
||||
|
||||
err = inspector.DeleteTask(constants.QueueDefault, info.ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
// 验证任务已删除
|
||||
queueInfo, err := inspector.GetQueueInfo(constants.QueueDefault)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 0, queueInfo.Pending)
|
||||
}
|
||||
|
||||
// TestBatchTaskEnqueue 测试批量任务入队
|
||||
func TestBatchTaskEnqueue(t *testing.T) {
|
||||
rdb := redis.NewClient(&redis.Options{
|
||||
Addr: "localhost:6379",
|
||||
})
|
||||
defer func() { _ = rdb.Close() }()
|
||||
|
||||
ctx := context.Background()
|
||||
rdb.FlushDB(ctx)
|
||||
|
||||
client := asynq.NewClient(asynq.RedisClientOpt{
|
||||
Addr: "localhost:6379",
|
||||
})
|
||||
defer func() { _ = client.Close() }()
|
||||
|
||||
// 批量创建任务
|
||||
batchSize := 100
|
||||
for i := 0; i < batchSize; i++ {
|
||||
payload := map[string]string{
|
||||
"request_id": "batch-" + string(rune(i)),
|
||||
}
|
||||
|
||||
payloadBytes, err := sonic.Marshal(payload)
|
||||
require.NoError(t, err)
|
||||
|
||||
task := asynq.NewTask(constants.TaskTypeEmailSend, payloadBytes)
|
||||
_, err = client.Enqueue(task)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
// 验证任务数量
|
||||
inspector := asynq.NewInspector(asynq.RedisClientOpt{
|
||||
Addr: "localhost:6379",
|
||||
})
|
||||
defer func() { _ = inspector.Close() }()
|
||||
|
||||
info, err := inspector.GetQueueInfo(constants.QueueDefault)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, batchSize, info.Pending)
|
||||
}
|
||||
|
||||
// TestTaskGrouping 测试任务分组
|
||||
func TestTaskGrouping(t *testing.T) {
|
||||
rdb := redis.NewClient(&redis.Options{
|
||||
Addr: "localhost:6379",
|
||||
})
|
||||
defer func() { _ = rdb.Close() }()
|
||||
|
||||
ctx := context.Background()
|
||||
rdb.FlushDB(ctx)
|
||||
|
||||
client := asynq.NewClient(asynq.RedisClientOpt{
|
||||
Addr: "localhost:6379",
|
||||
})
|
||||
defer func() { _ = client.Close() }()
|
||||
|
||||
// 提交分组任务
|
||||
groupKey := "email-batch-001"
|
||||
for i := 0; i < 5; i++ {
|
||||
payload := map[string]string{
|
||||
"request_id": "group-" + string(rune(i)),
|
||||
"group": groupKey,
|
||||
}
|
||||
|
||||
payloadBytes, err := sonic.Marshal(payload)
|
||||
require.NoError(t, err)
|
||||
|
||||
task := asynq.NewTask(constants.TaskTypeEmailSend, payloadBytes)
|
||||
_, err = client.Enqueue(task,
|
||||
asynq.Group(groupKey),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
// 验证任务已按组提交
|
||||
inspector := asynq.NewInspector(asynq.RedisClientOpt{
|
||||
Addr: "localhost:6379",
|
||||
})
|
||||
defer func() { _ = inspector.Close() }()
|
||||
|
||||
info, err := inspector.GetQueueInfo(constants.QueueDefault)
|
||||
require.NoError(t, err)
|
||||
assert.GreaterOrEqual(t, info.Pending, 5)
|
||||
}
|
||||
@@ -179,5 +179,5 @@ func TestRoleAssignmentLimit_SuperAdmin(t *testing.T) {
|
||||
// 尝试为超级管理员分配角色(应该失败)
|
||||
_, err := service.AssignRoles(ctx, superAdmin.ID, []uint{role.ID})
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "不需要分配角色")
|
||||
assert.Contains(t, err.Error(), "超级管理员不允许分配角色")
|
||||
}
|
||||
|
||||
@@ -1,390 +0,0 @@
|
||||
package unit
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/bytedance/sonic"
|
||||
"github.com/hibiken/asynq"
|
||||
"github.com/redis/go-redis/v9"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
||||
)
|
||||
|
||||
// MockEmailPayload 邮件任务载荷(测试用)
|
||||
type MockEmailPayload struct {
|
||||
RequestID string `json:"request_id"`
|
||||
To string `json:"to"`
|
||||
Subject string `json:"subject"`
|
||||
Body string `json:"body"`
|
||||
CC []string `json:"cc,omitempty"`
|
||||
}
|
||||
|
||||
// TestHandlerIdempotency 测试处理器幂等性逻辑
|
||||
func TestHandlerIdempotency(t *testing.T) {
|
||||
rdb := redis.NewClient(&redis.Options{
|
||||
Addr: "localhost:6379",
|
||||
})
|
||||
defer rdb.Close()
|
||||
|
||||
ctx := context.Background()
|
||||
rdb.FlushDB(ctx)
|
||||
|
||||
requestID := "test-req-001"
|
||||
lockKey := constants.RedisTaskLockKey(requestID)
|
||||
|
||||
// 测试场景1: 第一次执行(未加锁)
|
||||
t.Run("First Execution - Should Acquire Lock", func(t *testing.T) {
|
||||
result, err := rdb.SetNX(ctx, lockKey, "1", 24*time.Hour).Result()
|
||||
require.NoError(t, err)
|
||||
assert.True(t, result, "第一次执行应该成功获取锁")
|
||||
})
|
||||
|
||||
// 测试场景2: 重复执行(已加锁)
|
||||
t.Run("Duplicate Execution - Should Skip", func(t *testing.T) {
|
||||
result, err := rdb.SetNX(ctx, lockKey, "1", 24*time.Hour).Result()
|
||||
require.NoError(t, err)
|
||||
assert.False(t, result, "重复执行应该跳过(锁已存在)")
|
||||
})
|
||||
|
||||
// 清理
|
||||
rdb.Del(ctx, lockKey)
|
||||
}
|
||||
|
||||
// TestHandlerErrorHandling 测试处理器错误处理
|
||||
func TestHandlerErrorHandling(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
payload MockEmailPayload
|
||||
shouldError bool
|
||||
errorMsg string
|
||||
}{
|
||||
{
|
||||
name: "Valid Payload",
|
||||
payload: MockEmailPayload{
|
||||
RequestID: "valid-001",
|
||||
To: "test@example.com",
|
||||
Subject: "Test",
|
||||
Body: "Test Body",
|
||||
},
|
||||
shouldError: false,
|
||||
},
|
||||
{
|
||||
name: "Missing RequestID",
|
||||
payload: MockEmailPayload{
|
||||
RequestID: "",
|
||||
To: "test@example.com",
|
||||
Subject: "Test",
|
||||
Body: "Test Body",
|
||||
},
|
||||
shouldError: true,
|
||||
errorMsg: "request_id 不能为空",
|
||||
},
|
||||
{
|
||||
name: "Missing To",
|
||||
payload: MockEmailPayload{
|
||||
RequestID: "test-002",
|
||||
To: "",
|
||||
Subject: "Test",
|
||||
Body: "Test Body",
|
||||
},
|
||||
shouldError: true,
|
||||
errorMsg: "收件人不能为空",
|
||||
},
|
||||
{
|
||||
name: "Invalid Email Format",
|
||||
payload: MockEmailPayload{
|
||||
RequestID: "test-003",
|
||||
To: "invalid-email",
|
||||
Subject: "Test",
|
||||
Body: "Test Body",
|
||||
},
|
||||
shouldError: true,
|
||||
errorMsg: "邮箱格式无效",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// 验证载荷
|
||||
err := validateEmailPayload(&tt.payload)
|
||||
|
||||
if tt.shouldError {
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), tt.errorMsg)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// validateEmailPayload 验证邮件载荷(模拟实际处理器中的验证逻辑)
|
||||
func validateEmailPayload(payload *MockEmailPayload) error {
|
||||
if payload.RequestID == "" {
|
||||
return asynq.SkipRetry // 参数错误不重试
|
||||
}
|
||||
if payload.To == "" {
|
||||
return asynq.SkipRetry
|
||||
}
|
||||
// 简单的邮箱格式验证
|
||||
if payload.To != "" && !contains(payload.To, "@") {
|
||||
return asynq.SkipRetry
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func contains(s, substr string) bool {
|
||||
for i := 0; i < len(s)-len(substr)+1; i++ {
|
||||
if s[i:i+len(substr)] == substr {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// TestHandlerRetryLogic 测试重试逻辑
|
||||
func TestHandlerRetryLogic(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
error error
|
||||
shouldRetry bool
|
||||
}{
|
||||
{
|
||||
name: "Retryable Error - Network Issue",
|
||||
error: assert.AnError,
|
||||
shouldRetry: true,
|
||||
},
|
||||
{
|
||||
name: "Non-Retryable Error - Invalid Params",
|
||||
error: asynq.SkipRetry,
|
||||
shouldRetry: false,
|
||||
},
|
||||
{
|
||||
name: "No Error",
|
||||
error: nil,
|
||||
shouldRetry: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
shouldRetry := tt.error != nil && tt.error != asynq.SkipRetry
|
||||
assert.Equal(t, tt.shouldRetry, shouldRetry)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestPayloadDeserialization 测试载荷反序列化
|
||||
func TestPayloadDeserialization(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
jsonPayload string
|
||||
expectError bool
|
||||
}{
|
||||
{
|
||||
name: "Valid JSON",
|
||||
jsonPayload: `{"request_id":"test-001","to":"test@example.com","subject":"Test","body":"Body"}`,
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "Invalid JSON",
|
||||
jsonPayload: `{invalid json}`,
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
name: "Empty JSON",
|
||||
jsonPayload: `{}`,
|
||||
expectError: false, // JSON 解析成功,但验证会失败
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
var payload MockEmailPayload
|
||||
err := sonic.Unmarshal([]byte(tt.jsonPayload), &payload)
|
||||
|
||||
if tt.expectError {
|
||||
require.Error(t, err)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestTaskStatusTransition 测试任务状态转换
|
||||
func TestTaskStatusTransition(t *testing.T) {
|
||||
rdb := redis.NewClient(&redis.Options{
|
||||
Addr: "localhost:6379",
|
||||
})
|
||||
defer rdb.Close()
|
||||
|
||||
ctx := context.Background()
|
||||
rdb.FlushDB(ctx)
|
||||
|
||||
taskID := "task-transition-001"
|
||||
statusKey := constants.RedisTaskStatusKey(taskID)
|
||||
|
||||
// 状态转换序列
|
||||
transitions := []struct {
|
||||
status string
|
||||
valid bool
|
||||
}{
|
||||
{"pending", true},
|
||||
{"processing", true},
|
||||
{"completed", true},
|
||||
{"failed", false}, // completed 后不应该转到 failed
|
||||
}
|
||||
|
||||
currentStatus := ""
|
||||
for _, tr := range transitions {
|
||||
t.Run("Transition to "+tr.status, func(t *testing.T) {
|
||||
// 检查状态转换是否合法
|
||||
if isValidTransition(currentStatus, tr.status) == tr.valid {
|
||||
err := rdb.Set(ctx, statusKey, tr.status, 7*24*time.Hour).Err()
|
||||
require.NoError(t, err)
|
||||
currentStatus = tr.status
|
||||
} else {
|
||||
// 不合法的转换应该被拒绝
|
||||
assert.False(t, tr.valid)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// isValidTransition 检查状态转换是否合法
|
||||
func isValidTransition(from, to string) bool {
|
||||
validTransitions := map[string][]string{
|
||||
"": {"pending"},
|
||||
"pending": {"processing", "failed"},
|
||||
"processing": {"completed", "failed"},
|
||||
"completed": {}, // 终态
|
||||
"failed": {}, // 终态
|
||||
}
|
||||
|
||||
allowed, exists := validTransitions[from]
|
||||
if !exists {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, valid := range allowed {
|
||||
if valid == to {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// TestConcurrentTaskExecution 测试并发任务执行
|
||||
func TestConcurrentTaskExecution(t *testing.T) {
|
||||
rdb := redis.NewClient(&redis.Options{
|
||||
Addr: "localhost:6379",
|
||||
})
|
||||
defer rdb.Close()
|
||||
|
||||
ctx := context.Background()
|
||||
rdb.FlushDB(ctx)
|
||||
|
||||
// 模拟多个并发任务尝试获取同一个锁
|
||||
requestID := "concurrent-test-001"
|
||||
lockKey := constants.RedisTaskLockKey(requestID)
|
||||
|
||||
concurrency := 10
|
||||
successCount := 0
|
||||
|
||||
done := make(chan bool, concurrency)
|
||||
|
||||
// 并发执行
|
||||
for i := 0; i < concurrency; i++ {
|
||||
go func() {
|
||||
result, err := rdb.SetNX(ctx, lockKey, "1", 24*time.Hour).Result()
|
||||
if err == nil && result {
|
||||
successCount++
|
||||
}
|
||||
done <- true
|
||||
}()
|
||||
}
|
||||
|
||||
// 等待所有 goroutine 完成
|
||||
for i := 0; i < concurrency; i++ {
|
||||
<-done
|
||||
}
|
||||
|
||||
// 验证只有一个成功获取锁
|
||||
assert.Equal(t, 1, successCount, "只有一个任务应该成功获取锁")
|
||||
}
|
||||
|
||||
// TestTaskTimeout 测试任务超时处理
|
||||
func TestTaskTimeout(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
taskDuration time.Duration
|
||||
timeout time.Duration
|
||||
shouldTimeout bool
|
||||
}{
|
||||
{
|
||||
name: "Normal Execution",
|
||||
taskDuration: 100 * time.Millisecond,
|
||||
timeout: 1 * time.Second,
|
||||
shouldTimeout: false,
|
||||
},
|
||||
{
|
||||
name: "Timeout Execution",
|
||||
taskDuration: 2 * time.Second,
|
||||
timeout: 500 * time.Millisecond,
|
||||
shouldTimeout: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), tt.timeout)
|
||||
defer cancel()
|
||||
|
||||
// 模拟任务执行
|
||||
done := make(chan bool)
|
||||
go func() {
|
||||
time.Sleep(tt.taskDuration)
|
||||
done <- true
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-done:
|
||||
assert.False(t, tt.shouldTimeout, "任务应该正常完成")
|
||||
case <-ctx.Done():
|
||||
assert.True(t, tt.shouldTimeout, "任务应该超时")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestLockExpiration 测试锁过期机制
|
||||
func TestLockExpiration(t *testing.T) {
|
||||
rdb := redis.NewClient(&redis.Options{
|
||||
Addr: "localhost:6379",
|
||||
})
|
||||
defer rdb.Close()
|
||||
|
||||
ctx := context.Background()
|
||||
rdb.FlushDB(ctx)
|
||||
|
||||
requestID := "expiration-test-001"
|
||||
lockKey := constants.RedisTaskLockKey(requestID)
|
||||
|
||||
// 设置短 TTL 的锁
|
||||
result, err := rdb.SetNX(ctx, lockKey, "1", 100*time.Millisecond).Result()
|
||||
require.NoError(t, err)
|
||||
assert.True(t, result)
|
||||
|
||||
// 等待锁过期
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
|
||||
// 验证锁已过期,可以重新获取
|
||||
result, err = rdb.SetNX(ctx, lockKey, "1", 24*time.Hour).Result()
|
||||
require.NoError(t, err)
|
||||
assert.True(t, result, "锁过期后应该可以重新获取")
|
||||
}
|
||||
Reference in New Issue
Block a user