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

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