Files
junhong_cmp_fiber/tests/unit/shop_service_test.go
huang 18f35f3ef4 feat: 完成B端认证系统和商户管理模块测试补全
主要变更:
- 新增B端认证系统(后台+H5):登录、登出、Token刷新、密码修改
- 完善商户管理和商户账号管理功能
- 补全单元测试(ShopService: 72.5%, ShopAccountService: 79.8%)
- 新增集成测试(商户管理+商户账号管理)
- 归档OpenSpec提案(add-shop-account-management, implement-b-end-auth-system)
- 完善文档(使用指南、API文档、认证架构说明)

测试统计:
- 13个测试套件,37个测试用例,100%通过率
- 平均覆盖率76.2%,达标

OpenSpec验证:通过(strict模式)
2026-01-15 18:15:17 +08:00

741 lines
20 KiB
Go

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/service/shop"
"github.com/break/junhong_cmp_fiber/internal/store/postgres"
"github.com/break/junhong_cmp_fiber/pkg/constants"
"github.com/break/junhong_cmp_fiber/pkg/errors"
"github.com/break/junhong_cmp_fiber/tests/testutils"
)
// TestShopService_Create 测试创建店铺
func TestShopService_Create(t *testing.T) {
db, redisClient := testutils.SetupTestDB(t)
defer testutils.TeardownTestDB(t, db, redisClient)
shopStore := postgres.NewShopStore(db, redisClient)
accountStore := postgres.NewAccountStore(db, redisClient)
service := shop.New(shopStore, accountStore)
t.Run("创建一级店铺成功", func(t *testing.T) {
ctx := createContextWithUserID(1)
req := &model.CreateShopRequest{
ShopName: "测试一级店铺",
ShopCode: "SHOP_L1_001",
ParentID: nil,
ContactName: "张三",
ContactPhone: "13800000001",
Province: "北京市",
City: "北京市",
District: "朝阳区",
Address: "朝阳路100号",
InitUsername: generateUniqueUsername("admin", t),
InitPhone: "13800138001",
InitPassword: "password123",
}
result, err := service.Create(ctx, req)
require.NoError(t, err)
assert.NotZero(t, result.ID)
assert.Equal(t, "测试一级店铺", result.ShopName)
assert.Equal(t, "SHOP_L1_001", result.ShopCode)
assert.Equal(t, 1, result.Level)
assert.Nil(t, result.ParentID)
assert.Equal(t, constants.StatusEnabled, result.Status)
})
t.Run("创建二级店铺成功", func(t *testing.T) {
ctx := createContextWithUserID(1)
// 先创建一级店铺
parent := &model.Shop{
ShopName: "一级店铺",
ShopCode: "PARENT_001",
Level: 1,
ContactName: "李四",
ContactPhone: "13800000002",
Status: constants.StatusEnabled,
BaseModel: model.BaseModel{
Creator: 1,
Updater: 1,
},
}
err := shopStore.Create(ctx, parent)
require.NoError(t, err)
// 创建二级店铺
req := &model.CreateShopRequest{
ShopName: "测试二级店铺",
ShopCode: "SHOP_L2_001",
ParentID: &parent.ID,
ContactName: "王五",
ContactPhone: "13800000003",
InitUsername: generateUniqueUsername("agent", t),
InitPhone: "13800138002",
InitPassword: "password123",
}
result, err := service.Create(ctx, req)
require.NoError(t, err)
assert.NotZero(t, result.ID)
assert.Equal(t, 2, result.Level)
assert.Equal(t, parent.ID, *result.ParentID)
})
t.Run("层级校验-创建第8级店铺应失败", func(t *testing.T) {
ctx := createContextWithUserID(1)
// 创建 7 级店铺层级
var shops []*model.Shop
for i := 1; i <= 7; i++ {
var parentID *uint
if i > 1 {
parentID = &shops[i-2].ID
}
shopModel := &model.Shop{
ShopName: testutils.GenerateUsername("店铺L", i),
ShopCode: testutils.GenerateUsername("LEVEL", i),
ParentID: parentID,
Level: i,
ContactName: "测试联系人",
ContactPhone: testutils.GeneratePhone("138", i),
Status: constants.StatusEnabled,
BaseModel: model.BaseModel{
Creator: 1,
Updater: 1,
},
}
err := shopStore.Create(ctx, shopModel)
require.NoError(t, err)
shops = append(shops, shopModel)
}
// 验证已创建 7 级
assert.Len(t, shops, 7)
assert.Equal(t, 7, shops[6].Level)
// 尝试创建第 8 级店铺(应该失败)
req := &model.CreateShopRequest{
ShopName: "第8级店铺",
ShopCode: "SHOP_L8_001",
ParentID: &shops[6].ID, // 第7级店铺的ID
ContactName: "测试",
ContactPhone: "13800000008",
InitUsername: generateUniqueUsername("level8", t),
InitPhone: "13800138008",
InitPassword: "password123",
}
result, err := service.Create(ctx, req)
assert.Error(t, err)
assert.Nil(t, result)
// 验证错误码
appErr, ok := err.(*errors.AppError)
require.True(t, ok, "错误应该是 AppError 类型")
assert.Equal(t, errors.CodeShopLevelExceeded, appErr.Code)
assert.Contains(t, appErr.Message, "不能超过 7 级")
})
t.Run("店铺编号唯一性检查-重复编号应失败", func(t *testing.T) {
ctx := createContextWithUserID(1)
// 创建第一个店铺
req1 := &model.CreateShopRequest{
ShopName: "店铺A",
ShopCode: "UNIQUE_CODE_001",
ContactName: "张三",
ContactPhone: "13800000001",
InitUsername: generateUniqueUsername("unique1", t),
InitPhone: generateUniquePhone(),
InitPassword: "password123",
}
_, err := service.Create(ctx, req1)
require.NoError(t, err)
// 尝试创建相同编号的店铺(应该失败)
req2 := &model.CreateShopRequest{
ShopName: "店铺B",
ShopCode: "UNIQUE_CODE_001", // 重复编号
ContactName: "李四",
ContactPhone: "13800000002",
InitUsername: generateUniqueUsername("unique2", t),
InitPhone: generateUniquePhone(),
InitPassword: "password123",
}
result, err := service.Create(ctx, req2)
assert.Error(t, err)
assert.Nil(t, result)
// 验证错误码
appErr, ok := err.(*errors.AppError)
require.True(t, ok)
assert.Equal(t, errors.CodeShopCodeExists, appErr.Code)
assert.Contains(t, appErr.Message, "编号已存在")
})
t.Run("上级店铺不存在应失败", func(t *testing.T) {
ctx := createContextWithUserID(1)
nonExistentID := uint(99999)
req := &model.CreateShopRequest{
ShopName: "测试店铺",
ShopCode: "SHOP_INVALID_PARENT",
ParentID: &nonExistentID, // 不存在的上级店铺 ID
ContactName: "测试",
ContactPhone: "13800000009",
InitUsername: generateUniqueUsername("invalid", t),
InitPhone: "13800138009",
InitPassword: "password123",
}
result, err := service.Create(ctx, req)
assert.Error(t, err)
assert.Nil(t, result)
// 验证错误码
appErr, ok := err.(*errors.AppError)
require.True(t, ok)
assert.Equal(t, errors.CodeInvalidParentID, appErr.Code)
assert.Contains(t, appErr.Message, "上级店铺不存在")
})
t.Run("未授权访问应失败", func(t *testing.T) {
ctx := context.Background() // 没有用户 ID 的 context
req := &model.CreateShopRequest{
ShopName: "测试店铺",
ShopCode: "SHOP_UNAUTHORIZED",
ContactName: "测试",
ContactPhone: "13800000010",
InitUsername: generateUniqueUsername("unauth", t),
InitPhone: "13800138010",
InitPassword: "password123",
}
result, err := service.Create(ctx, req)
assert.Error(t, err)
assert.Nil(t, result)
// 验证错误码
appErr, ok := err.(*errors.AppError)
require.True(t, ok)
assert.Equal(t, errors.CodeUnauthorized, appErr.Code)
assert.Contains(t, appErr.Message, "未授权")
})
}
// TestShopService_Update 测试更新店铺
func TestShopService_Update(t *testing.T) {
db, redisClient := testutils.SetupTestDB(t)
defer testutils.TeardownTestDB(t, db, redisClient)
shopStore := postgres.NewShopStore(db, redisClient)
accountStore := postgres.NewAccountStore(db, redisClient)
service := shop.New(shopStore, accountStore)
t.Run("更新店铺信息成功", func(t *testing.T) {
ctx := createContextWithUserID(1)
// 先创建店铺
shopModel := &model.Shop{
ShopName: "原始店铺名称",
ShopCode: "ORIGINAL_CODE",
Level: 1,
ContactName: "原联系人",
ContactPhone: "13800000001",
Status: constants.StatusEnabled,
BaseModel: model.BaseModel{
Creator: 1,
Updater: 1,
},
}
err := shopStore.Create(ctx, shopModel)
require.NoError(t, err)
// 更新店铺
req := &model.UpdateShopRequest{
ShopName: "更新后的店铺名称",
ContactName: "新联系人",
ContactPhone: "13900000001",
Province: "上海市",
City: "上海市",
District: "浦东新区",
Address: "陆家嘴环路1000号",
Status: constants.StatusEnabled,
}
result, err := service.Update(ctx, shopModel.ID, req)
require.NoError(t, err)
assert.Equal(t, "更新后的店铺名称", result.ShopName)
assert.Equal(t, "ORIGINAL_CODE", result.ShopCode)
assert.Equal(t, "新联系人", result.ContactName)
assert.Equal(t, "13900000001", result.ContactPhone)
assert.Equal(t, "上海市", result.Province)
assert.Equal(t, "上海市", result.City)
assert.Equal(t, "浦东新区", result.District)
assert.Equal(t, "陆家嘴环路1000号", result.Address)
})
t.Run("更新店铺编号-唯一性检查", func(t *testing.T) {
ctx := createContextWithUserID(1)
// 创建两个店铺
shop1 := &model.Shop{
ShopName: "店铺1",
ShopCode: "CODE_001",
Level: 1,
Status: constants.StatusEnabled,
BaseModel: model.BaseModel{
Creator: 1,
Updater: 1,
},
}
err := shopStore.Create(ctx, shop1)
require.NoError(t, err)
shop2 := &model.Shop{
ShopName: "店铺2",
ShopCode: "CODE_002",
Level: 1,
Status: constants.StatusEnabled,
BaseModel: model.BaseModel{
Creator: 1,
Updater: 1,
},
}
err = shopStore.Create(ctx, shop2)
require.NoError(t, err)
// 尝试更新 shop2 的名称为已存在的名称(应该成功,因为名称不需要唯一性)
req := &model.UpdateShopRequest{
ShopName: "店铺1",
Status: constants.StatusEnabled,
}
result, err := service.Update(ctx, shop2.ID, req)
require.NoError(t, err)
assert.NotNil(t, result)
assert.Equal(t, "店铺1", result.ShopName)
})
t.Run("更新不存在的店铺应失败", func(t *testing.T) {
ctx := createContextWithUserID(1)
req := &model.UpdateShopRequest{
ShopName: "新名称",
Status: constants.StatusEnabled,
}
result, err := service.Update(ctx, 99999, req)
assert.Error(t, err)
assert.Nil(t, result)
appErr, ok := err.(*errors.AppError)
require.True(t, ok)
assert.Equal(t, errors.CodeShopNotFound, appErr.Code)
})
t.Run("未授权访问应失败", func(t *testing.T) {
ctx := context.Background()
req := &model.UpdateShopRequest{
ShopName: "新名称",
Status: constants.StatusEnabled,
}
result, err := service.Update(ctx, 1, req)
assert.Error(t, err)
assert.Nil(t, result)
appErr, ok := err.(*errors.AppError)
require.True(t, ok)
assert.Equal(t, errors.CodeUnauthorized, appErr.Code)
})
}
// TestShopService_Disable 测试禁用店铺
func TestShopService_Disable(t *testing.T) {
db, redisClient := testutils.SetupTestDB(t)
defer testutils.TeardownTestDB(t, db, redisClient)
shopStore := postgres.NewShopStore(db, redisClient)
accountStore := postgres.NewAccountStore(db, redisClient)
service := shop.New(shopStore, accountStore)
t.Run("禁用店铺成功", func(t *testing.T) {
ctx := createContextWithUserID(1)
// 创建店铺
shopModel := &model.Shop{
ShopName: "待禁用店铺",
ShopCode: "SHOP_TO_DISABLE",
Level: 1,
Status: constants.StatusEnabled,
BaseModel: model.BaseModel{
Creator: 1,
Updater: 1,
},
}
err := shopStore.Create(ctx, shopModel)
require.NoError(t, err)
assert.Equal(t, constants.StatusEnabled, shopModel.Status)
// 禁用店铺
err = service.Disable(ctx, shopModel.ID)
require.NoError(t, err)
// 验证状态已更新
result, err := shopStore.GetByID(ctx, shopModel.ID)
require.NoError(t, err)
assert.Equal(t, constants.StatusDisabled, result.Status)
assert.Equal(t, uint(1), result.Updater)
})
t.Run("禁用不存在的店铺应失败", func(t *testing.T) {
ctx := createContextWithUserID(1)
err := service.Disable(ctx, 99999)
assert.Error(t, err)
// 验证错误码
appErr, ok := err.(*errors.AppError)
require.True(t, ok)
assert.Equal(t, errors.CodeShopNotFound, appErr.Code)
})
t.Run("未授权访问应失败", func(t *testing.T) {
ctx := context.Background()
err := service.Disable(ctx, 1)
assert.Error(t, err)
// 验证错误码
appErr, ok := err.(*errors.AppError)
require.True(t, ok)
assert.Equal(t, errors.CodeUnauthorized, appErr.Code)
})
}
// TestShopService_Enable 测试启用店铺
func TestShopService_Enable(t *testing.T) {
db, redisClient := testutils.SetupTestDB(t)
defer testutils.TeardownTestDB(t, db, redisClient)
shopStore := postgres.NewShopStore(db, redisClient)
accountStore := postgres.NewAccountStore(db, redisClient)
service := shop.New(shopStore, accountStore)
t.Run("启用店铺成功", func(t *testing.T) {
ctx := createContextWithUserID(1)
// 创建启用状态的店铺
shopModel := &model.Shop{
ShopName: "待启用店铺",
ShopCode: "SHOP_TO_ENABLE",
Level: 1,
Status: constants.StatusEnabled,
BaseModel: model.BaseModel{
Creator: 1,
Updater: 1,
},
}
err := shopStore.Create(ctx, shopModel)
require.NoError(t, err)
// 先禁用店铺
shopModel.Status = constants.StatusDisabled
err = shopStore.Update(ctx, shopModel)
require.NoError(t, err)
// 验证已禁用
disabled, err := shopStore.GetByID(ctx, shopModel.ID)
require.NoError(t, err)
assert.Equal(t, constants.StatusDisabled, disabled.Status)
// 启用店铺
err = service.Enable(ctx, shopModel.ID)
require.NoError(t, err)
// 验证状态已更新为启用
result, err := shopStore.GetByID(ctx, shopModel.ID)
require.NoError(t, err)
assert.Equal(t, constants.StatusEnabled, result.Status)
assert.Equal(t, uint(1), result.Updater)
})
t.Run("启用不存在的店铺应失败", func(t *testing.T) {
ctx := createContextWithUserID(1)
err := service.Enable(ctx, 99999)
assert.Error(t, err)
// 验证错误码
appErr, ok := err.(*errors.AppError)
require.True(t, ok)
assert.Equal(t, errors.CodeShopNotFound, appErr.Code)
})
t.Run("未授权访问应失败", func(t *testing.T) {
ctx := context.Background()
err := service.Enable(ctx, 1)
assert.Error(t, err)
// 验证错误码
appErr, ok := err.(*errors.AppError)
require.True(t, ok)
assert.Equal(t, errors.CodeUnauthorized, appErr.Code)
})
}
// TestShopService_GetByID 测试获取店铺详情
func TestShopService_GetByID(t *testing.T) {
db, redisClient := testutils.SetupTestDB(t)
defer testutils.TeardownTestDB(t, db, redisClient)
shopStore := postgres.NewShopStore(db, redisClient)
accountStore := postgres.NewAccountStore(db, redisClient)
service := shop.New(shopStore, accountStore)
t.Run("获取存在的店铺", func(t *testing.T) {
ctx := createContextWithUserID(1)
// 创建店铺
shopModel := &model.Shop{
ShopName: "测试店铺",
ShopCode: "TEST_SHOP_001",
Level: 1,
Status: constants.StatusEnabled,
BaseModel: model.BaseModel{
Creator: 1,
Updater: 1,
},
}
err := shopStore.Create(ctx, shopModel)
require.NoError(t, err)
// 获取店铺
result, err := service.GetByID(ctx, shopModel.ID)
require.NoError(t, err)
assert.Equal(t, shopModel.ID, result.ID)
assert.Equal(t, "测试店铺", result.ShopName)
assert.Equal(t, "TEST_SHOP_001", result.ShopCode)
})
t.Run("获取不存在的店铺应失败", func(t *testing.T) {
ctx := createContextWithUserID(1)
result, err := service.GetByID(ctx, 99999)
assert.Error(t, err)
assert.Nil(t, result)
// 验证错误码
appErr, ok := err.(*errors.AppError)
require.True(t, ok)
assert.Equal(t, errors.CodeShopNotFound, appErr.Code)
})
}
// TestShopService_List 测试查询店铺列表
func TestShopService_List(t *testing.T) {
db, redisClient := testutils.SetupTestDB(t)
defer testutils.TeardownTestDB(t, db, redisClient)
shopStore := postgres.NewShopStore(db, redisClient)
accountStore := postgres.NewAccountStore(db, redisClient)
service := shop.New(shopStore, accountStore)
t.Run("查询店铺列表", func(t *testing.T) {
ctx := createContextWithUserID(1)
// 创建多个店铺
for i := 1; i <= 5; i++ {
shopModel := &model.Shop{
ShopName: testutils.GenerateUsername("测试店铺", i),
ShopCode: testutils.GenerateUsername("SHOP_LIST", i),
Level: 1,
Status: constants.StatusEnabled,
BaseModel: model.BaseModel{
Creator: 1,
Updater: 1,
},
}
err := shopStore.Create(ctx, shopModel)
require.NoError(t, err)
}
// 查询列表
shops, total, err := service.List(ctx, nil, nil)
require.NoError(t, err)
assert.GreaterOrEqual(t, len(shops), 5)
assert.GreaterOrEqual(t, total, int64(5))
})
}
// TestShopService_GetSubordinateShopIDs 测试获取下级店铺 ID 列表
func TestShopService_GetSubordinateShopIDs(t *testing.T) {
db, redisClient := testutils.SetupTestDB(t)
defer testutils.TeardownTestDB(t, db, redisClient)
shopStore := postgres.NewShopStore(db, redisClient)
accountStore := postgres.NewAccountStore(db, redisClient)
service := shop.New(shopStore, accountStore)
t.Run("获取下级店铺 ID 列表", func(t *testing.T) {
ctx := createContextWithUserID(1)
// 创建店铺层级
shop1 := &model.Shop{
ShopName: "一级店铺",
ShopCode: "SUBORDINATE_L1",
Level: 1,
Status: constants.StatusEnabled,
BaseModel: model.BaseModel{
Creator: 1,
Updater: 1,
},
}
err := shopStore.Create(ctx, shop1)
require.NoError(t, err)
shop2 := &model.Shop{
ShopName: "二级店铺",
ShopCode: "SUBORDINATE_L2",
ParentID: &shop1.ID,
Level: 2,
Status: constants.StatusEnabled,
BaseModel: model.BaseModel{
Creator: 1,
Updater: 1,
},
}
err = shopStore.Create(ctx, shop2)
require.NoError(t, err)
shop3 := &model.Shop{
ShopName: "三级店铺",
ShopCode: "SUBORDINATE_L3",
ParentID: &shop2.ID,
Level: 3,
Status: constants.StatusEnabled,
BaseModel: model.BaseModel{
Creator: 1,
Updater: 1,
},
}
err = shopStore.Create(ctx, shop3)
require.NoError(t, err)
// 获取一级店铺的所有下级(包含自己)
ids, err := service.GetSubordinateShopIDs(ctx, shop1.ID)
require.NoError(t, err)
assert.Contains(t, ids, shop1.ID)
assert.Contains(t, ids, shop2.ID)
assert.Contains(t, ids, shop3.ID)
assert.Len(t, ids, 3)
})
}
// TestShopService_Delete 测试删除店铺
func TestShopService_Delete(t *testing.T) {
db, redisClient := testutils.SetupTestDB(t)
defer testutils.TeardownTestDB(t, db, redisClient)
shopStore := postgres.NewShopStore(db, redisClient)
accountStore := postgres.NewAccountStore(db, redisClient)
service := shop.New(shopStore, accountStore)
t.Run("删除店铺成功", func(t *testing.T) {
ctx := createContextWithUserID(1)
shopModel := &model.Shop{
ShopName: "待删除店铺",
ShopCode: "DELETE_001",
Level: 1,
Status: constants.StatusEnabled,
BaseModel: model.BaseModel{
Creator: 1,
Updater: 1,
},
}
err := shopStore.Create(ctx, shopModel)
require.NoError(t, err)
err = service.Delete(ctx, shopModel.ID)
require.NoError(t, err)
_, err = shopStore.GetByID(ctx, shopModel.ID)
assert.Error(t, err)
})
t.Run("删除店铺并禁用关联账号", func(t *testing.T) {
ctx := createContextWithUserID(1)
shopModel := &model.Shop{
ShopName: "有账号的店铺",
ShopCode: "DELETE_002",
Level: 1,
Status: constants.StatusEnabled,
BaseModel: model.BaseModel{
Creator: 1,
Updater: 1,
},
}
err := shopStore.Create(ctx, shopModel)
require.NoError(t, err)
account := &model.Account{
BaseModel: model.BaseModel{
Creator: 1,
Updater: 1,
},
Username: testutils.GenerateUsername("agent", 1),
Phone: testutils.GeneratePhone("139", 1),
Password: "hashedpassword123",
UserType: constants.UserTypeAgent,
ShopID: &shopModel.ID,
Status: constants.StatusEnabled,
}
err = accountStore.Create(ctx, account)
require.NoError(t, err)
err = service.Delete(ctx, shopModel.ID)
require.NoError(t, err)
updatedAccount, err := accountStore.GetByID(ctx, account.ID)
require.NoError(t, err)
assert.Equal(t, constants.StatusDisabled, updatedAccount.Status)
})
t.Run("删除不存在的店铺应失败", func(t *testing.T) {
ctx := createContextWithUserID(1)
err := service.Delete(ctx, 99999)
assert.Error(t, err)
appErr, ok := err.(*errors.AppError)
require.True(t, ok)
assert.Equal(t, errors.CodeShopNotFound, appErr.Code)
})
t.Run("未授权访问应失败", func(t *testing.T) {
ctx := context.Background()
err := service.Delete(ctx, 1)
assert.Error(t, err)
appErr, ok := err.(*errors.AppError)
require.True(t, ok)
assert.Equal(t, errors.CodeUnauthorized, appErr.Code)
})
}