Files
junhong_cmp_fiber/tests/integration/api_regression_test.go
huang 1b9080e3ab 实现角色权限体系重构
本次提交完成了角色权限体系的重构,主要包括:

1. 数据库迁移
   - 添加 tb_permission.platform 字段(all/web/h5)
   - 更新 tb_role.role_type 注释(1=平台角色,2=客户角色)

2. GORM 模型更新
   - Permission 模型添加 Platform 字段
   - Role 模型更新 RoleType 注释

3. 常量定义
   - 新增角色类型常量(RoleTypePlatform, RoleTypeCustomer)
   - 新增权限端口常量(PlatformAll, PlatformWeb, PlatformH5)
   - 添加角色类型与用户类型匹配规则函数

4. Store 层实现
   - Permission Store 支持按 platform 过滤
   - Account Role Store 添加 CountByAccountID 方法

5. Service 层实现
   - 角色分配支持类型匹配校验
   - 角色分配支持数量限制(超级管理员0个,平台用户无限制,代理/企业1个)
   - Permission Service 支持 platform 过滤

6. 权限校验中间件
   - 实现 RequirePermission、RequireAnyPermission、RequireAllPermissions
   - 支持 platform 字段过滤
   - 支持跳过超级管理员检查

7. 测试用例
   - 角色类型匹配规则单元测试
   - 角色分配数量限制单元测试
   - 权限 platform 过滤单元测试
   - 权限校验中间件集成测试(占位)

8. 代码清理
   - 删除过时的 subordinate 测试文件
   - 移除 Account.ParentID 相关引用
   - 更新 DTO 验证规则

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-10 09:51:52 +08:00

399 lines
12 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package integration
import (
"context"
"fmt"
"net/http/httptest"
"testing"
"time"
"github.com/gofiber/fiber/v2"
"github.com/redis/go-redis/v9"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/testcontainers/testcontainers-go"
testcontainers_postgres "github.com/testcontainers/testcontainers-go/modules/postgres"
testcontainers_redis "github.com/testcontainers/testcontainers-go/modules/redis"
"github.com/testcontainers/testcontainers-go/wait"
"gorm.io/driver/postgres"
"gorm.io/gorm"
"gorm.io/gorm/logger"
"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"
permissionService "github.com/break/junhong_cmp_fiber/internal/service/permission"
roleService "github.com/break/junhong_cmp_fiber/internal/service/role"
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/middleware"
)
// regressionTestEnv 回归测试环境
type regressionTestEnv struct {
db *gorm.DB
redisClient *redis.Client
app *fiber.App
postgresCleanup func()
redisCleanup func()
}
// setupRegressionTestEnv 设置回归测试环境
func setupRegressionTestEnv(t *testing.T) *regressionTestEnv {
t.Helper()
ctx := context.Background()
// 启动 PostgreSQL 容器
pgContainer, err := testcontainers_postgres.RunContainer(ctx,
testcontainers.WithImage("postgres:14-alpine"),
testcontainers_postgres.WithDatabase("testdb"),
testcontainers_postgres.WithUsername("postgres"),
testcontainers_postgres.WithPassword("password"),
testcontainers.WithWaitStrategy(
wait.ForLog("database system is ready to accept connections").
WithOccurrence(2).
WithStartupTimeout(30*time.Second),
),
)
require.NoError(t, err, "启动 PostgreSQL 容器失败")
pgConnStr, err := pgContainer.ConnectionString(ctx, "sslmode=disable")
require.NoError(t, err)
// 启动 Redis 容器
redisContainer, err := testcontainers_redis.RunContainer(ctx,
testcontainers.WithImage("redis:6-alpine"),
)
require.NoError(t, err, "启动 Redis 容器失败")
redisHost, err := redisContainer.Host(ctx)
require.NoError(t, err)
redisPort, err := redisContainer.MappedPort(ctx, "6379")
require.NoError(t, err)
// 连接数据库
db, err := gorm.Open(postgres.Open(pgConnStr), &gorm.Config{
Logger: logger.Default.LogMode(logger.Silent),
})
require.NoError(t, err)
// 自动迁移
err = db.AutoMigrate(
&model.Account{},
&model.Role{},
&model.Permission{},
&model.AccountRole{},
&model.RolePermission{},
)
require.NoError(t, err)
// 连接 Redis
redisClient := redis.NewClient(&redis.Options{
Addr: fmt.Sprintf("%s:%s", redisHost, redisPort.Port()),
})
// 初始化所有 Store
accountStore := postgresStore.NewAccountStore(db, redisClient)
roleStore := postgresStore.NewRoleStore(db)
permStore := postgresStore.NewPermissionStore(db)
accountRoleStore := postgresStore.NewAccountRoleStore(db)
rolePermStore := postgresStore.NewRolePermissionStore(db)
// 初始化所有 Service
accService := accountService.New(accountStore, roleStore, accountRoleStore)
roleSvc := roleService.New(roleStore, permStore, rolePermStore)
permSvc := permissionService.New(permStore)
// 初始化所有 Handler
accountHandler := admin.NewAccountHandler(accService)
roleHandler := admin.NewRoleHandler(roleSvc)
permHandler := admin.NewPermissionHandler(permSvc)
// 创建 Fiber App
app := fiber.New(fiber.Config{
ErrorHandler: func(c *fiber.Ctx, err error) error {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
},
})
// 添加测试中间件设置用户上下文
app.Use(func(c *fiber.Ctx) error {
ctx := middleware.SetUserContext(c.UserContext(), 1, constants.UserTypeSuperAdmin, 0)
c.SetUserContext(ctx)
return c.Next()
})
// 注册所有路由
services := &bootstrap.Handlers{
Account: accountHandler,
Role: roleHandler,
Permission: permHandler,
}
routes.RegisterRoutes(app, services)
return &regressionTestEnv{
db: db,
redisClient: redisClient,
app: app,
postgresCleanup: func() {
if err := pgContainer.Terminate(ctx); err != nil {
t.Logf("终止 PostgreSQL 容器失败: %v", err)
}
},
redisCleanup: func() {
if err := redisContainer.Terminate(ctx); err != nil {
t.Logf("终止 Redis 容器失败: %v", err)
}
},
}
}
// TestAPIRegression_AllEndpointsAccessible 测试所有 API 端点在重构后仍可访问
func TestAPIRegression_AllEndpointsAccessible(t *testing.T) {
env := setupRegressionTestEnv(t)
defer env.postgresCleanup()
defer env.redisCleanup()
// 定义所有需要测试的端点
endpoints := []struct {
method string
path string
name string
}{
// Health endpoints
{"GET", "/health", "Health check"},
{"GET", "/health/ready", "Readiness check"},
// Account endpoints
{"GET", "/api/admin/accounts", "List accounts"},
{"GET", "/api/admin/accounts/1", "Get account"},
// Role endpoints
{"GET", "/api/admin/roles", "List roles"},
{"GET", "/api/admin/roles/1", "Get role"},
// Permission endpoints
{"GET", "/api/admin/permissions", "List permissions"},
{"GET", "/api/admin/permissions/1", "Get permission"},
{"GET", "/api/admin/permissions/tree", "Get permission tree"},
}
for _, ep := range endpoints {
t.Run(ep.name, func(t *testing.T) {
req := httptest.NewRequest(ep.method, ep.path, nil)
resp, err := env.app.Test(req)
require.NoError(t, err)
// 验证端点可访问(状态码不是 404 或 500
assert.NotEqual(t, fiber.StatusNotFound, resp.StatusCode,
"端点 %s %s 应该存在", ep.method, ep.path)
assert.NotEqual(t, fiber.StatusInternalServerError, resp.StatusCode,
"端点 %s %s 不应该返回 500 错误", ep.method, ep.path)
})
}
}
// TestAPIRegression_RouteModularization 测试路由模块化后功能正常
func TestAPIRegression_RouteModularization(t *testing.T) {
env := setupRegressionTestEnv(t)
defer env.postgresCleanup()
defer env.redisCleanup()
t.Run("账号模块路由正常", func(t *testing.T) {
// 创建测试数据
account := &model.Account{
Username: "regression_test",
Phone: "13800000300",
Password: "hashedpassword",
UserType: constants.UserTypePlatform,
Status: constants.StatusEnabled,
}
env.db.Create(account)
// 测试获取账号
req := httptest.NewRequest("GET", fmt.Sprintf("/api/admin/accounts/%d", account.ID), nil)
resp, err := env.app.Test(req)
require.NoError(t, err)
assert.Equal(t, fiber.StatusOK, resp.StatusCode)
// 测试获取角色列表
req = httptest.NewRequest("GET", fmt.Sprintf("/api/admin/accounts/%d/roles", account.ID), nil)
resp, err = env.app.Test(req)
require.NoError(t, err)
assert.Equal(t, fiber.StatusOK, resp.StatusCode)
})
t.Run("角色模块路由正常", func(t *testing.T) {
// 创建测试数据
role := &model.Role{
RoleName: "回归测试角色",
RoleType: constants.RoleTypePlatform,
Status: constants.StatusEnabled,
}
env.db.Create(role)
// 测试获取角色
req := httptest.NewRequest("GET", fmt.Sprintf("/api/admin/roles/%d", role.ID), nil)
resp, err := env.app.Test(req)
require.NoError(t, err)
assert.Equal(t, fiber.StatusOK, resp.StatusCode)
// 测试获取权限列表
req = httptest.NewRequest("GET", fmt.Sprintf("/api/admin/roles/%d/permissions", role.ID), nil)
resp, err = env.app.Test(req)
require.NoError(t, err)
assert.Equal(t, fiber.StatusOK, resp.StatusCode)
})
t.Run("权限模块路由正常", func(t *testing.T) {
// 创建测试数据
perm := &model.Permission{
PermName: "回归测试权限",
PermCode: "regression:test:perm",
PermType: constants.PermissionTypeMenu,
Status: constants.StatusEnabled,
}
env.db.Create(perm)
// 测试获取权限
req := httptest.NewRequest("GET", fmt.Sprintf("/api/admin/permissions/%d", perm.ID), nil)
resp, err := env.app.Test(req)
require.NoError(t, err)
assert.Equal(t, fiber.StatusOK, resp.StatusCode)
// 测试获取权限树
req = httptest.NewRequest("GET", "/api/admin/permissions/tree", nil)
resp, err = env.app.Test(req)
require.NoError(t, err)
assert.Equal(t, fiber.StatusOK, resp.StatusCode)
})
}
// TestAPIRegression_ErrorHandling 测试错误处理在重构后仍正常
func TestAPIRegression_ErrorHandling(t *testing.T) {
env := setupRegressionTestEnv(t)
defer env.postgresCleanup()
defer env.redisCleanup()
t.Run("资源不存在返回正确错误码", func(t *testing.T) {
// 账号不存在
req := httptest.NewRequest("GET", "/api/admin/accounts/99999", nil)
resp, err := env.app.Test(req)
require.NoError(t, err)
// 应该返回业务错误,不是 404
assert.NotEqual(t, fiber.StatusNotFound, resp.StatusCode)
// 角色不存在
req = httptest.NewRequest("GET", "/api/admin/roles/99999", nil)
resp, err = env.app.Test(req)
require.NoError(t, err)
assert.NotEqual(t, fiber.StatusNotFound, resp.StatusCode)
// 权限不存在
req = httptest.NewRequest("GET", "/api/admin/permissions/99999", nil)
resp, err = env.app.Test(req)
require.NoError(t, err)
assert.NotEqual(t, fiber.StatusNotFound, resp.StatusCode)
})
t.Run("无效参数返回正确错误码", func(t *testing.T) {
// 无效账号 ID
req := httptest.NewRequest("GET", "/api/admin/accounts/invalid", nil)
resp, err := env.app.Test(req)
require.NoError(t, err)
assert.NotEqual(t, fiber.StatusInternalServerError, resp.StatusCode)
})
}
// TestAPIRegression_Pagination 测试分页功能在重构后仍正常
func TestAPIRegression_Pagination(t *testing.T) {
env := setupRegressionTestEnv(t)
defer env.postgresCleanup()
defer env.redisCleanup()
// 创建测试数据
for i := 1; i <= 25; i++ {
account := &model.Account{
Username: fmt.Sprintf("pagination_test_%d", i),
Phone: fmt.Sprintf("138000004%02d", i),
Password: "hashedpassword",
UserType: constants.UserTypePlatform,
Status: constants.StatusEnabled,
}
env.db.Create(account)
}
t.Run("分页参数正常工作", func(t *testing.T) {
// 第一页
req := httptest.NewRequest("GET", "/api/admin/accounts?page=1&page_size=10", nil)
resp, err := env.app.Test(req)
require.NoError(t, err)
assert.Equal(t, fiber.StatusOK, resp.StatusCode)
// 第二页
req = httptest.NewRequest("GET", "/api/admin/accounts?page=2&page_size=10", nil)
resp, err = env.app.Test(req)
require.NoError(t, err)
assert.Equal(t, fiber.StatusOK, resp.StatusCode)
})
t.Run("默认分页参数工作", func(t *testing.T) {
req := httptest.NewRequest("GET", "/api/admin/accounts", nil)
resp, err := env.app.Test(req)
require.NoError(t, err)
assert.Equal(t, fiber.StatusOK, resp.StatusCode)
})
}
// TestAPIRegression_ResponseFormat 测试响应格式在重构后保持一致
func TestAPIRegression_ResponseFormat(t *testing.T) {
env := setupRegressionTestEnv(t)
defer env.postgresCleanup()
defer env.redisCleanup()
t.Run("成功响应包含正确字段", func(t *testing.T) {
req := httptest.NewRequest("GET", "/api/admin/accounts", nil)
resp, err := env.app.Test(req)
require.NoError(t, err)
assert.Equal(t, fiber.StatusOK, resp.StatusCode)
// 响应应该是 JSON
assert.Contains(t, resp.Header.Get("Content-Type"), "application/json")
})
t.Run("健康检查端点响应正常", func(t *testing.T) {
req := httptest.NewRequest("GET", "/health", nil)
resp, err := env.app.Test(req)
require.NoError(t, err)
assert.Equal(t, fiber.StatusOK, resp.StatusCode)
})
}
// TestAPIRegression_ServicesIntegration 测试服务集成在重构后仍正常
func TestAPIRegression_ServicesIntegration(t *testing.T) {
env := setupRegressionTestEnv(t)
defer env.postgresCleanup()
defer env.redisCleanup()
t.Run("Services 容器正确初始化", func(t *testing.T) {
// 验证所有模块路由都已注册
endpoints := []string{
"/health",
"/api/admin/accounts",
"/api/admin/roles",
"/api/admin/permissions",
}
for _, ep := range endpoints {
req := httptest.NewRequest("GET", ep, nil)
resp, err := env.app.Test(req)
require.NoError(t, err)
assert.NotEqual(t, fiber.StatusNotFound, resp.StatusCode,
"端点 %s 应该已注册", ep)
}
})
}