feat: 实现设备管理和设备导入功能,修复测试问题
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:
2026-01-26 18:05:12 +08:00
parent fdcff33058
commit ce0783f96e
68 changed files with 6400 additions and 1482 deletions

View File

@@ -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() {

View File

@@ -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 &regressionTestEnv{
tx: tx,
tx: tx,
rdb: rdb,
app: app,
app: app,
postgresCleanup: func() {
if err := pgContainer.Terminate(ctx); err != nil {
t.Logf("终止 PostgreSQL 容器失败: %v", err)

View 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)
})
}

View File

@@ -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)
})
}

View File

@@ -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{

View File

@@ -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() {

View File

@@ -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)

View File

@@ -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)
}

View File

@@ -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)
})
}

View File

@@ -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)
}

View File

@@ -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(), "超级管理员不允许分配角色")
}

View File

@@ -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, "锁过期后应该可以重新获取")
}