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