feat(account): 实现平台账号管理功能
- 新增平台账号列表查询接口(自动筛选超级管理员和平台用户) - 新增密码修改和状态切换专用接口 - 增强角色分配功能,支持空数组清空所有角色 - 新增超级管理员保护机制,禁止分配角色 - 新增完整的集成测试和OpenSpec规范文档
This commit is contained in:
359
tests/integration/platform_account_test.go
Normal file
359
tests/integration/platform_account_test.go
Normal file
@@ -0,0 +1,359 @@
|
||||
package integration
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/internal/bootstrap"
|
||||
"github.com/break/junhong_cmp_fiber/internal/handler/admin"
|
||||
"github.com/break/junhong_cmp_fiber/internal/model"
|
||||
"github.com/break/junhong_cmp_fiber/internal/routes"
|
||||
accountService "github.com/break/junhong_cmp_fiber/internal/service/account"
|
||||
postgresStore "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/pkg/middleware"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/response"
|
||||
"github.com/break/junhong_cmp_fiber/tests/testutils"
|
||||
)
|
||||
|
||||
func TestPlatformAccountAPI_ListPlatformAccounts(t *testing.T) {
|
||||
db, redisClient := testutils.SetupTestDB(t)
|
||||
defer testutils.TeardownTestDB(t, db, redisClient)
|
||||
|
||||
accountStore := postgresStore.NewAccountStore(db, redisClient)
|
||||
roleStore := postgresStore.NewRoleStore(db)
|
||||
accountRoleStore := postgresStore.NewAccountRoleStore(db)
|
||||
accService := accountService.New(accountStore, roleStore, accountRoleStore)
|
||||
accountHandler := admin.NewAccountHandler(accService)
|
||||
|
||||
app := fiber.New(fiber.Config{
|
||||
ErrorHandler: errors.SafeErrorHandler(nil),
|
||||
})
|
||||
|
||||
testUserID := uint(1)
|
||||
app.Use(func(c *fiber.Ctx) error {
|
||||
ctx := middleware.SetUserContext(c.UserContext(), middleware.NewSimpleUserContext(testUserID, constants.UserTypeSuperAdmin, 0))
|
||||
c.SetUserContext(ctx)
|
||||
return c.Next()
|
||||
})
|
||||
|
||||
services := &bootstrap.Handlers{Account: accountHandler}
|
||||
middlewares := &bootstrap.Middlewares{}
|
||||
routes.RegisterRoutes(app, services, middlewares)
|
||||
|
||||
superAdmin := &model.Account{
|
||||
Username: testutils.GenerateUsername("super_admin", 1),
|
||||
Phone: testutils.GeneratePhone("138", 1),
|
||||
Password: "hashedpassword",
|
||||
UserType: constants.UserTypeSuperAdmin,
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
db.Create(superAdmin)
|
||||
|
||||
platformUser := &model.Account{
|
||||
Username: testutils.GenerateUsername("platform_user", 2),
|
||||
Phone: testutils.GeneratePhone("138", 2),
|
||||
Password: "hashedpassword",
|
||||
UserType: constants.UserTypePlatform,
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
db.Create(platformUser)
|
||||
|
||||
agentUser := &model.Account{
|
||||
Username: testutils.GenerateUsername("agent_user", 3),
|
||||
Phone: testutils.GeneratePhone("138", 3),
|
||||
Password: "hashedpassword",
|
||||
UserType: constants.UserTypeAgent,
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
db.Create(agentUser)
|
||||
|
||||
t.Run("列表只返回平台账号和超级管理员", func(t *testing.T) {
|
||||
req := httptest.NewRequest("GET", "/api/admin/platform-accounts?page=1&page_size=20", nil)
|
||||
resp, err := app.Test(req)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, fiber.StatusOK, resp.StatusCode)
|
||||
|
||||
var result response.Response
|
||||
err = json.NewDecoder(resp.Body).Decode(&result)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 0, result.Code)
|
||||
|
||||
data := result.Data.(map[string]interface{})
|
||||
items := data["items"].([]interface{})
|
||||
assert.GreaterOrEqual(t, len(items), 2)
|
||||
|
||||
var count int64
|
||||
db.Model(&model.Account{}).Where("user_type IN ?", []int{1, 2}).Count(&count)
|
||||
assert.GreaterOrEqual(t, count, int64(2))
|
||||
})
|
||||
|
||||
t.Run("按用户名筛选", func(t *testing.T) {
|
||||
req := httptest.NewRequest("GET", "/api/admin/platform-accounts?username=platform_user", nil)
|
||||
resp, err := app.Test(req)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, fiber.StatusOK, resp.StatusCode)
|
||||
|
||||
var result response.Response
|
||||
err = json.NewDecoder(resp.Body).Decode(&result)
|
||||
require.NoError(t, err)
|
||||
|
||||
data := result.Data.(map[string]interface{})
|
||||
items := data["items"].([]interface{})
|
||||
assert.GreaterOrEqual(t, len(items), 1)
|
||||
})
|
||||
}
|
||||
|
||||
func TestPlatformAccountAPI_UpdatePassword(t *testing.T) {
|
||||
db, redisClient := testutils.SetupTestDB(t)
|
||||
defer testutils.TeardownTestDB(t, db, redisClient)
|
||||
|
||||
accountStore := postgresStore.NewAccountStore(db, redisClient)
|
||||
roleStore := postgresStore.NewRoleStore(db)
|
||||
accountRoleStore := postgresStore.NewAccountRoleStore(db)
|
||||
accService := accountService.New(accountStore, roleStore, accountRoleStore)
|
||||
accountHandler := admin.NewAccountHandler(accService)
|
||||
|
||||
app := fiber.New(fiber.Config{
|
||||
ErrorHandler: errors.SafeErrorHandler(nil),
|
||||
})
|
||||
|
||||
testUserID := uint(1)
|
||||
app.Use(func(c *fiber.Ctx) error {
|
||||
ctx := middleware.SetUserContext(c.UserContext(), middleware.NewSimpleUserContext(testUserID, constants.UserTypeSuperAdmin, 0))
|
||||
c.SetUserContext(ctx)
|
||||
return c.Next()
|
||||
})
|
||||
|
||||
services := &bootstrap.Handlers{Account: accountHandler}
|
||||
middlewares := &bootstrap.Middlewares{}
|
||||
routes.RegisterRoutes(app, services, middlewares)
|
||||
|
||||
testAccount := &model.Account{
|
||||
Username: testutils.GenerateUsername("pwd_test", 10),
|
||||
Phone: testutils.GeneratePhone("139", 10),
|
||||
Password: "old_hashed_password",
|
||||
UserType: constants.UserTypePlatform,
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
db.Create(testAccount)
|
||||
|
||||
t.Run("成功修改密码", func(t *testing.T) {
|
||||
reqBody := model.UpdatePasswordRequest{
|
||||
NewPassword: "NewPassword@123",
|
||||
}
|
||||
jsonBody, _ := json.Marshal(reqBody)
|
||||
req := httptest.NewRequest("PUT", fmt.Sprintf("/api/admin/platform-accounts/%d/password", testAccount.ID), bytes.NewReader(jsonBody))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp, err := app.Test(req)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, fiber.StatusOK, resp.StatusCode)
|
||||
|
||||
var result response.Response
|
||||
err = json.NewDecoder(resp.Body).Decode(&result)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 0, result.Code)
|
||||
|
||||
var updated model.Account
|
||||
db.First(&updated, testAccount.ID)
|
||||
assert.NotEqual(t, "old_hashed_password", updated.Password)
|
||||
})
|
||||
|
||||
t.Run("账号不存在返回错误", func(t *testing.T) {
|
||||
reqBody := model.UpdatePasswordRequest{
|
||||
NewPassword: "NewPassword@123",
|
||||
}
|
||||
jsonBody, _ := json.Marshal(reqBody)
|
||||
req := httptest.NewRequest("PUT", "/api/admin/platform-accounts/99999/password", bytes.NewReader(jsonBody))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp, err := app.Test(req)
|
||||
require.NoError(t, err)
|
||||
|
||||
var result response.Response
|
||||
err = json.NewDecoder(resp.Body).Decode(&result)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, errors.CodeAccountNotFound, result.Code)
|
||||
})
|
||||
}
|
||||
|
||||
func TestPlatformAccountAPI_UpdateStatus(t *testing.T) {
|
||||
db, redisClient := testutils.SetupTestDB(t)
|
||||
defer testutils.TeardownTestDB(t, db, redisClient)
|
||||
|
||||
accountStore := postgresStore.NewAccountStore(db, redisClient)
|
||||
roleStore := postgresStore.NewRoleStore(db)
|
||||
accountRoleStore := postgresStore.NewAccountRoleStore(db)
|
||||
accService := accountService.New(accountStore, roleStore, accountRoleStore)
|
||||
accountHandler := admin.NewAccountHandler(accService)
|
||||
|
||||
app := fiber.New(fiber.Config{
|
||||
ErrorHandler: errors.SafeErrorHandler(nil),
|
||||
})
|
||||
|
||||
testUserID := uint(1)
|
||||
app.Use(func(c *fiber.Ctx) error {
|
||||
ctx := middleware.SetUserContext(c.UserContext(), middleware.NewSimpleUserContext(testUserID, constants.UserTypeSuperAdmin, 0))
|
||||
c.SetUserContext(ctx)
|
||||
return c.Next()
|
||||
})
|
||||
|
||||
services := &bootstrap.Handlers{Account: accountHandler}
|
||||
middlewares := &bootstrap.Middlewares{}
|
||||
routes.RegisterRoutes(app, services, middlewares)
|
||||
|
||||
testAccount := &model.Account{
|
||||
Username: testutils.GenerateUsername("status_test", 20),
|
||||
Phone: testutils.GeneratePhone("137", 20),
|
||||
Password: "hashedpassword",
|
||||
UserType: constants.UserTypePlatform,
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
db.Create(testAccount)
|
||||
|
||||
t.Run("成功禁用账号", func(t *testing.T) {
|
||||
reqBody := model.UpdateStatusRequest{
|
||||
Status: constants.StatusDisabled,
|
||||
}
|
||||
jsonBody, _ := json.Marshal(reqBody)
|
||||
req := httptest.NewRequest("PUT", fmt.Sprintf("/api/admin/platform-accounts/%d/status", testAccount.ID), bytes.NewReader(jsonBody))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp, err := app.Test(req)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, fiber.StatusOK, resp.StatusCode)
|
||||
|
||||
var updated model.Account
|
||||
db.First(&updated, testAccount.ID)
|
||||
assert.Equal(t, constants.StatusDisabled, updated.Status)
|
||||
})
|
||||
|
||||
t.Run("成功启用账号", func(t *testing.T) {
|
||||
reqBody := model.UpdateStatusRequest{
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
jsonBody, _ := json.Marshal(reqBody)
|
||||
req := httptest.NewRequest("PUT", fmt.Sprintf("/api/admin/platform-accounts/%d/status", testAccount.ID), bytes.NewReader(jsonBody))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp, err := app.Test(req)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, fiber.StatusOK, resp.StatusCode)
|
||||
|
||||
var updated model.Account
|
||||
db.First(&updated, testAccount.ID)
|
||||
assert.Equal(t, constants.StatusEnabled, updated.Status)
|
||||
})
|
||||
}
|
||||
|
||||
func TestPlatformAccountAPI_AssignRoles(t *testing.T) {
|
||||
db, redisClient := testutils.SetupTestDB(t)
|
||||
defer testutils.TeardownTestDB(t, db, redisClient)
|
||||
|
||||
accountStore := postgresStore.NewAccountStore(db, redisClient)
|
||||
roleStore := postgresStore.NewRoleStore(db)
|
||||
accountRoleStore := postgresStore.NewAccountRoleStore(db)
|
||||
accService := accountService.New(accountStore, roleStore, accountRoleStore)
|
||||
accountHandler := admin.NewAccountHandler(accService)
|
||||
|
||||
app := fiber.New(fiber.Config{
|
||||
ErrorHandler: errors.SafeErrorHandler(nil),
|
||||
})
|
||||
|
||||
testUserID := uint(1)
|
||||
app.Use(func(c *fiber.Ctx) error {
|
||||
ctx := middleware.SetUserContext(c.UserContext(), middleware.NewSimpleUserContext(testUserID, constants.UserTypeSuperAdmin, 0))
|
||||
c.SetUserContext(ctx)
|
||||
return c.Next()
|
||||
})
|
||||
|
||||
services := &bootstrap.Handlers{Account: accountHandler}
|
||||
middlewares := &bootstrap.Middlewares{}
|
||||
routes.RegisterRoutes(app, services, middlewares)
|
||||
|
||||
superAdmin := &model.Account{
|
||||
Username: testutils.GenerateUsername("super_admin_role", 30),
|
||||
Phone: testutils.GeneratePhone("136", 30),
|
||||
Password: "hashedpassword",
|
||||
UserType: constants.UserTypeSuperAdmin,
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
db.Create(superAdmin)
|
||||
|
||||
platformUser := &model.Account{
|
||||
Username: testutils.GenerateUsername("platform_user_role", 31),
|
||||
Phone: testutils.GeneratePhone("136", 31),
|
||||
Password: "hashedpassword",
|
||||
UserType: constants.UserTypePlatform,
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
db.Create(platformUser)
|
||||
|
||||
testRole := &model.Role{
|
||||
RoleName: testutils.GenerateUsername("测试角色", 30),
|
||||
RoleType: constants.RoleTypePlatform,
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
db.Create(testRole)
|
||||
|
||||
t.Run("超级管理员禁止分配角色", func(t *testing.T) {
|
||||
reqBody := model.AssignRolesRequest{
|
||||
RoleIDs: []uint{testRole.ID},
|
||||
}
|
||||
jsonBody, _ := json.Marshal(reqBody)
|
||||
req := httptest.NewRequest("POST", fmt.Sprintf("/api/admin/platform-accounts/%d/roles", superAdmin.ID), bytes.NewReader(jsonBody))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp, err := app.Test(req)
|
||||
require.NoError(t, err)
|
||||
|
||||
var result response.Response
|
||||
err = json.NewDecoder(resp.Body).Decode(&result)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, errors.CodeInvalidParam, result.Code)
|
||||
assert.Contains(t, result.Message, "超级管理员不允许分配角色")
|
||||
})
|
||||
|
||||
t.Run("平台用户成功分配角色", func(t *testing.T) {
|
||||
reqBody := model.AssignRolesRequest{
|
||||
RoleIDs: []uint{testRole.ID},
|
||||
}
|
||||
jsonBody, _ := json.Marshal(reqBody)
|
||||
req := httptest.NewRequest("POST", fmt.Sprintf("/api/admin/platform-accounts/%d/roles", platformUser.ID), bytes.NewReader(jsonBody))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp, err := app.Test(req)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, fiber.StatusOK, resp.StatusCode)
|
||||
|
||||
var count int64
|
||||
db.Model(&model.AccountRole{}).Where("account_id = ? AND role_id = ?", platformUser.ID, testRole.ID).Count(&count)
|
||||
assert.Equal(t, int64(1), count)
|
||||
})
|
||||
|
||||
t.Run("空数组清空所有角色", func(t *testing.T) {
|
||||
reqBody := model.AssignRolesRequest{
|
||||
RoleIDs: []uint{},
|
||||
}
|
||||
jsonBody, _ := json.Marshal(reqBody)
|
||||
req := httptest.NewRequest("POST", fmt.Sprintf("/api/admin/platform-accounts/%d/roles", platformUser.ID), bytes.NewReader(jsonBody))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp, err := app.Test(req)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, fiber.StatusOK, resp.StatusCode)
|
||||
|
||||
var count int64
|
||||
db.Model(&model.AccountRole{}).Where("account_id = ?", platformUser.ID).Count(&count)
|
||||
assert.Equal(t, int64(0), count)
|
||||
})
|
||||
}
|
||||
@@ -14,7 +14,7 @@ import (
|
||||
"github.com/break/junhong_cmp_fiber/internal/model"
|
||||
)
|
||||
|
||||
// SetupTestDB 设置测试数据库和 Redis
|
||||
// SetupTestDB 设置测试数据库和 Redis(使用事务)
|
||||
func SetupTestDB(t *testing.T) (*gorm.DB, *redis.Client) {
|
||||
t.Helper()
|
||||
|
||||
@@ -42,11 +42,15 @@ func SetupTestDB(t *testing.T) (*gorm.DB, *redis.Client) {
|
||||
t.Fatalf("数据库迁移失败: %v", err)
|
||||
}
|
||||
|
||||
// 连接测试 Redis(使用远程 Redis)
|
||||
txDB := db.Begin()
|
||||
if txDB.Error != nil {
|
||||
t.Fatalf("开启事务失败: %v", txDB.Error)
|
||||
}
|
||||
|
||||
redisClient := redis.NewClient(&redis.Options{
|
||||
Addr: "cxd.whcxd.cn:16299",
|
||||
Password: "cpNbWtAaqgo1YJmbMp3h",
|
||||
DB: 15, // 使用测试数据库
|
||||
DB: 15,
|
||||
})
|
||||
|
||||
ctx := context.Background()
|
||||
@@ -54,35 +58,28 @@ func SetupTestDB(t *testing.T) (*gorm.DB, *redis.Client) {
|
||||
t.Skipf("跳过测试:无法连接 Redis: %v", err)
|
||||
}
|
||||
|
||||
// 清空 Redis 测试数据库
|
||||
redisClient.FlushDB(ctx)
|
||||
testPrefix := fmt.Sprintf("test:%s:", t.Name())
|
||||
keys, _ := redisClient.Keys(ctx, testPrefix+"*").Result()
|
||||
if len(keys) > 0 {
|
||||
redisClient.Del(ctx, keys...)
|
||||
}
|
||||
|
||||
return db, redisClient
|
||||
return txDB, redisClient
|
||||
}
|
||||
|
||||
// TeardownTestDB 清理测试数据库
|
||||
// TeardownTestDB 清理测试数据库(回滚事务)
|
||||
func TeardownTestDB(t *testing.T, db *gorm.DB, redisClient *redis.Client) {
|
||||
t.Helper()
|
||||
|
||||
// 清空测试数据
|
||||
ctx := context.Background()
|
||||
db.Exec("TRUNCATE TABLE tb_account_role CASCADE")
|
||||
db.Exec("TRUNCATE TABLE tb_role_permission CASCADE")
|
||||
db.Exec("TRUNCATE TABLE tb_account CASCADE")
|
||||
db.Exec("TRUNCATE TABLE tb_role CASCADE")
|
||||
db.Exec("TRUNCATE TABLE tb_permission CASCADE")
|
||||
db.Exec("TRUNCATE TABLE tb_shop CASCADE")
|
||||
db.Exec("TRUNCATE TABLE tb_enterprise CASCADE")
|
||||
db.Exec("TRUNCATE TABLE tb_personal_customer CASCADE")
|
||||
|
||||
// 清空 Redis
|
||||
redisClient.FlushDB(ctx)
|
||||
|
||||
// 关闭连接
|
||||
sqlDB, _ := db.DB()
|
||||
if sqlDB != nil {
|
||||
_ = sqlDB.Close()
|
||||
testPrefix := fmt.Sprintf("test:%s:", t.Name())
|
||||
keys, _ := redisClient.Keys(ctx, testPrefix+"*").Result()
|
||||
if len(keys) > 0 {
|
||||
redisClient.Del(ctx, keys...)
|
||||
}
|
||||
|
||||
db.Rollback()
|
||||
|
||||
_ = redisClient.Close()
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user