package integration import ( "bytes" "context" "encoding/json" "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" 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" ) // testEnv 测试环境 type testEnv struct { db *gorm.DB redisClient *redis.Client app *fiber.App accountService *accountService.Service postgresCleanup func() redisCleanup func() } // setupTestEnv 设置测试环境 func setupTestEnv(t *testing.T) *testEnv { t.Helper() ctx := context.Background() // 启动 PostgreSQL 容器 pgContainer, err := testcontainers_postgres.Run(ctx, "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.Run(ctx, "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) accountRoleStore := postgresStore.NewAccountRoleStore(db) // 初始化 Service accService := accountService.New(accountStore, roleStore, accountRoleStore) // 初始化 Handler accountHandler := admin.NewAccountHandler(accService) // 创建 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()}) }, }) // 注册路由 services := &bootstrap.Handlers{ Account: accountHandler, } routes.RegisterRoutes(app, services) return &testEnv{ db: db, redisClient: redisClient, app: app, accountService: accService, 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) } }, } } // teardownTestEnv 清理测试环境 func (e *testEnv) teardown() { if e.postgresCleanup != nil { e.postgresCleanup() } if e.redisCleanup != nil { e.redisCleanup() } } // createTestAccount 创建测试账号并返回,用于设置测试上下文 func createTestAccount(t *testing.T, db *gorm.DB, account *model.Account) *model.Account { t.Helper() err := db.Create(account).Error require.NoError(t, err) return account } // TestAccountAPI_Create 测试创建账号 API func TestAccountAPI_Create(t *testing.T) { env := setupTestEnv(t) defer env.teardown() // 创建一个测试用的中间件来设置用户上下文 testUserID := uint(1) env.app.Use(func(c *fiber.Ctx) error { ctx := middleware.SetUserContext(c.UserContext(), testUserID, constants.UserTypeRoot, 0) c.SetUserContext(ctx) return c.Next() }) // 创建一个 root 账号作为创建者 rootAccount := &model.Account{ Username: "root", Phone: "13800000000", Password: "hashedpassword", UserType: constants.UserTypeRoot, Status: constants.StatusEnabled, } createTestAccount(t, env.db, rootAccount) t.Run("成功创建平台账号", func(t *testing.T) { reqBody := model.CreateAccountRequest{ Username: "platform_user", Phone: "13800000001", Password: "Password123", UserType: constants.UserTypePlatform, ParentID: &rootAccount.ID, } jsonBody, _ := json.Marshal(reqBody) req := httptest.NewRequest("POST", "/api/admin/accounts", bytes.NewReader(jsonBody)) req.Header.Set("Content-Type", "application/json") resp, err := env.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 count int64 env.db.Model(&model.Account{}).Where("username = ?", "platform_user").Count(&count) assert.Equal(t, int64(1), count) }) t.Run("用户名重复时返回错误", func(t *testing.T) { // 先创建一个账号 existingAccount := &model.Account{ Username: "existing_user", Phone: "13800000002", Password: "hashedpassword", UserType: constants.UserTypePlatform, ParentID: &rootAccount.ID, Status: constants.StatusEnabled, } createTestAccount(t, env.db, existingAccount) // 尝试创建同名账号 reqBody := model.CreateAccountRequest{ Username: "existing_user", Phone: "13800000003", Password: "Password123", UserType: constants.UserTypePlatform, ParentID: &rootAccount.ID, } jsonBody, _ := json.Marshal(reqBody) req := httptest.NewRequest("POST", "/api/admin/accounts", bytes.NewReader(jsonBody)) req.Header.Set("Content-Type", "application/json") resp, err := env.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.CodeUsernameExists, result.Code) }) t.Run("非root用户缺少parent_id时返回错误", func(t *testing.T) { reqBody := model.CreateAccountRequest{ Username: "no_parent_user", Phone: "13800000004", Password: "Password123", UserType: constants.UserTypePlatform, // 没有提供 ParentID } jsonBody, _ := json.Marshal(reqBody) req := httptest.NewRequest("POST", "/api/admin/accounts", bytes.NewReader(jsonBody)) req.Header.Set("Content-Type", "application/json") resp, err := env.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.CodeParentIDRequired, result.Code) }) } // TestAccountAPI_Get 测试获取账号详情 API func TestAccountAPI_Get(t *testing.T) { env := setupTestEnv(t) defer env.teardown() // 添加测试中间件 testUserID := uint(1) env.app.Use(func(c *fiber.Ctx) error { ctx := middleware.SetUserContext(c.UserContext(), testUserID, constants.UserTypeRoot, 0) c.SetUserContext(ctx) return c.Next() }) // 创建测试账号 testAccount := &model.Account{ Username: "test_user", Phone: "13800000010", Password: "hashedpassword", UserType: constants.UserTypePlatform, Status: constants.StatusEnabled, } createTestAccount(t, env.db, testAccount) t.Run("成功获取账号详情", func(t *testing.T) { req := httptest.NewRequest("GET", fmt.Sprintf("/api/admin/accounts/%d", testAccount.ID), nil) resp, err := env.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) }) 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) var result response.Response err = json.NewDecoder(resp.Body).Decode(&result) require.NoError(t, err) assert.Equal(t, errors.CodeAccountNotFound, result.Code) }) t.Run("无效ID返回错误", func(t *testing.T) { req := httptest.NewRequest("GET", "/api/admin/accounts/invalid", nil) resp, err := env.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) }) } // TestAccountAPI_Update 测试更新账号 API func TestAccountAPI_Update(t *testing.T) { env := setupTestEnv(t) defer env.teardown() // 添加测试中间件 testUserID := uint(1) env.app.Use(func(c *fiber.Ctx) error { ctx := middleware.SetUserContext(c.UserContext(), testUserID, constants.UserTypeRoot, 0) c.SetUserContext(ctx) return c.Next() }) // 创建测试账号 testAccount := &model.Account{ Username: "update_test", Phone: "13800000020", Password: "hashedpassword", UserType: constants.UserTypePlatform, Status: constants.StatusEnabled, } createTestAccount(t, env.db, testAccount) t.Run("成功更新账号", func(t *testing.T) { newUsername := "updated_user" reqBody := model.UpdateAccountRequest{ Username: &newUsername, } jsonBody, _ := json.Marshal(reqBody) req := httptest.NewRequest("PUT", fmt.Sprintf("/api/admin/accounts/%d", testAccount.ID), bytes.NewReader(jsonBody)) req.Header.Set("Content-Type", "application/json") resp, err := env.app.Test(req) require.NoError(t, err) assert.Equal(t, fiber.StatusOK, resp.StatusCode) // 验证数据库已更新 var updated model.Account env.db.First(&updated, testAccount.ID) assert.Equal(t, newUsername, updated.Username) }) } // TestAccountAPI_Delete 测试删除账号 API func TestAccountAPI_Delete(t *testing.T) { env := setupTestEnv(t) defer env.teardown() // 添加测试中间件 testUserID := uint(1) env.app.Use(func(c *fiber.Ctx) error { ctx := middleware.SetUserContext(c.UserContext(), testUserID, constants.UserTypeRoot, 0) c.SetUserContext(ctx) return c.Next() }) t.Run("成功软删除账号", func(t *testing.T) { // 创建测试账号 testAccount := &model.Account{ Username: "delete_test", Phone: "13800000030", Password: "hashedpassword", UserType: constants.UserTypePlatform, Status: constants.StatusEnabled, } createTestAccount(t, env.db, testAccount) req := httptest.NewRequest("DELETE", fmt.Sprintf("/api/admin/accounts/%d", testAccount.ID), nil) resp, err := env.app.Test(req) require.NoError(t, err) assert.Equal(t, fiber.StatusOK, resp.StatusCode) // 验证账号已软删除 var deleted model.Account err = env.db.Unscoped().First(&deleted, testAccount.ID).Error require.NoError(t, err) assert.NotNil(t, deleted.DeletedAt) }) } // TestAccountAPI_List 测试账号列表 API func TestAccountAPI_List(t *testing.T) { env := setupTestEnv(t) defer env.teardown() // 添加测试中间件 testUserID := uint(1) env.app.Use(func(c *fiber.Ctx) error { ctx := middleware.SetUserContext(c.UserContext(), testUserID, constants.UserTypeRoot, 0) c.SetUserContext(ctx) return c.Next() }) // 创建多个测试账号 for i := 1; i <= 5; i++ { account := &model.Account{ Username: fmt.Sprintf("list_test_%d", i), Phone: fmt.Sprintf("1380000004%d", i), Password: "hashedpassword", UserType: constants.UserTypePlatform, Status: constants.StatusEnabled, } createTestAccount(t, env.db, 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) var result response.Response err = json.NewDecoder(resp.Body).Decode(&result) require.NoError(t, err) assert.Equal(t, 0, result.Code) }) t.Run("分页功能正常", func(t *testing.T) { req := httptest.NewRequest("GET", "/api/admin/accounts?page=1&page_size=2", nil) resp, err := env.app.Test(req) require.NoError(t, err) assert.Equal(t, fiber.StatusOK, resp.StatusCode) }) } // TestAccountAPI_AssignRoles 测试分配角色 API func TestAccountAPI_AssignRoles(t *testing.T) { env := setupTestEnv(t) defer env.teardown() // 添加测试中间件 testUserID := uint(1) env.app.Use(func(c *fiber.Ctx) error { ctx := middleware.SetUserContext(c.UserContext(), testUserID, constants.UserTypeRoot, 0) c.SetUserContext(ctx) return c.Next() }) // 创建测试账号 testAccount := &model.Account{ Username: "role_test", Phone: "13800000050", Password: "hashedpassword", UserType: constants.UserTypePlatform, Status: constants.StatusEnabled, } createTestAccount(t, env.db, testAccount) // 创建测试角色 testRole := &model.Role{ RoleName: "测试角色", RoleType: constants.RoleTypeSuper, Status: constants.StatusEnabled, } env.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/accounts/%d/roles", testAccount.ID), bytes.NewReader(jsonBody)) req.Header.Set("Content-Type", "application/json") resp, err := env.app.Test(req) require.NoError(t, err) assert.Equal(t, fiber.StatusOK, resp.StatusCode) // 验证关联已创建 var count int64 env.db.Model(&model.AccountRole{}).Where("account_id = ? AND role_id = ?", testAccount.ID, testRole.ID).Count(&count) assert.Equal(t, int64(1), count) }) } // TestAccountAPI_GetRoles 测试获取账号角色 API func TestAccountAPI_GetRoles(t *testing.T) { env := setupTestEnv(t) defer env.teardown() // 添加测试中间件 testUserID := uint(1) env.app.Use(func(c *fiber.Ctx) error { ctx := middleware.SetUserContext(c.UserContext(), testUserID, constants.UserTypeRoot, 0) c.SetUserContext(ctx) return c.Next() }) // 创建测试账号 testAccount := &model.Account{ Username: "get_roles_test", Phone: "13800000060", Password: "hashedpassword", UserType: constants.UserTypePlatform, Status: constants.StatusEnabled, } createTestAccount(t, env.db, testAccount) // 创建并分配角色 testRole := &model.Role{ RoleName: "获取角色测试", RoleType: constants.RoleTypeSuper, Status: constants.StatusEnabled, } env.db.Create(testRole) accountRole := &model.AccountRole{ AccountID: testAccount.ID, RoleID: testRole.ID, Status: constants.StatusEnabled, Creator: 1, Updater: 1, } env.db.Create(accountRole) t.Run("成功获取账号角色", func(t *testing.T) { req := httptest.NewRequest("GET", fmt.Sprintf("/api/admin/accounts/%d/roles", testAccount.ID), nil) resp, err := env.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) }) } // TestAccountAPI_RemoveRole 测试移除角色 API func TestAccountAPI_RemoveRole(t *testing.T) { env := setupTestEnv(t) defer env.teardown() // 添加测试中间件 testUserID := uint(1) env.app.Use(func(c *fiber.Ctx) error { ctx := middleware.SetUserContext(c.UserContext(), testUserID, constants.UserTypeRoot, 0) c.SetUserContext(ctx) return c.Next() }) // 创建测试账号 testAccount := &model.Account{ Username: "remove_role_test", Phone: "13800000070", Password: "hashedpassword", UserType: constants.UserTypePlatform, Status: constants.StatusEnabled, } createTestAccount(t, env.db, testAccount) // 创建并分配角色 testRole := &model.Role{ RoleName: "移除角色测试", RoleType: constants.RoleTypeSuper, Status: constants.StatusEnabled, } env.db.Create(testRole) accountRole := &model.AccountRole{ AccountID: testAccount.ID, RoleID: testRole.ID, Status: constants.StatusEnabled, Creator: 1, Updater: 1, } env.db.Create(accountRole) t.Run("成功移除角色", func(t *testing.T) { req := httptest.NewRequest("DELETE", fmt.Sprintf("/api/admin/accounts/%d/roles/%d", testAccount.ID, testRole.ID), nil) resp, err := env.app.Test(req) require.NoError(t, err) assert.Equal(t, fiber.StatusOK, resp.StatusCode) // 验证关联已软删除 var ar model.AccountRole err = env.db.Unscoped().Where("account_id = ? AND role_id = ?", testAccount.ID, testRole.ID).First(&ar).Error require.NoError(t, err) assert.NotNil(t, ar.DeletedAt) }) }