package integration import ( "encoding/json" "fmt" "net/http" "testing" "time" "github.com/break/junhong_cmp_fiber/internal/model" "github.com/break/junhong_cmp_fiber/internal/model/dto" accountSvc "github.com/break/junhong_cmp_fiber/internal/service/account" accountAuditSvc "github.com/break/junhong_cmp_fiber/internal/service/account_audit" "github.com/break/junhong_cmp_fiber/internal/store/postgres" "github.com/break/junhong_cmp_fiber/pkg/constants" "github.com/break/junhong_cmp_fiber/pkg/response" "github.com/break/junhong_cmp_fiber/tests/testutils/integ" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) // extractAccountID 从响应 data 中提取账号 ID // gorm.Model 的 ID 字段在 JSON 中序列化为大写 "ID" func extractAccountID(t *testing.T, data map[string]interface{}) uint { t.Helper() idVal := data["ID"] if idVal == nil { idVal = data["id"] } require.NotNil(t, idVal, "响应应包含 ID 字段") return uint(idVal.(float64)) } // TestAccountAudit 账号操作审计日志集成测试 // 验证所有账号管理操作都被正确记录到审计日志 func TestAccountAudit(t *testing.T) { env := integ.NewIntegrationTestEnv(t) // 13.2 - 创建账号时记录审计日志 t.Run("创建账号时记录审计日志", func(t *testing.T) { shop := env.CreateTestShop("测试店铺", 1, nil) // 创建账号 reqBody := dto.CreateAccountRequest{ Username: fmt.Sprintf("test_%d", time.Now().UnixNano()), Phone: fmt.Sprintf("138%08d", time.Now().UnixNano()%100000000), Password: "Password123", UserType: constants.UserTypeAgent, ShopID: &shop.ID, } jsonBody, err := json.Marshal(reqBody) require.NoError(t, err) resp, err := env.AsSuperAdmin().Request("POST", "/api/admin/accounts", jsonBody) require.NoError(t, err) defer resp.Body.Close() require.Equal(t, http.StatusOK, resp.StatusCode) // 解析响应获取账号 ID var result response.Response err = json.NewDecoder(resp.Body).Decode(&result) require.NoError(t, err) require.Equal(t, 0, result.Code, "创建账号应该成功,响应: %+v", result) require.NotNil(t, result.Data, "响应 data 不应为 nil") data, ok := result.Data.(map[string]interface{}) require.True(t, ok, "响应 data 应为 map,实际: %T", result.Data) accountID := extractAccountID(t, data) // 等待异步日志写入 time.Sleep(200 * time.Millisecond) // 验证审计日志 var log model.AccountOperationLog err = env.RawDB().Where("target_account_id = ? AND operation_type = ?", accountID, "create"). First(&log).Error require.NoError(t, err, "应该存在创建操作的审计日志") // 验证日志字段 assert.Equal(t, "create", log.OperationType) assert.NotNil(t, log.AfterData, "创建操作应有 after_data") assert.Nil(t, log.BeforeData, "创建操作不应有 before_data") assert.NotNil(t, log.TargetUsername) assert.Equal(t, reqBody.Username, *log.TargetUsername) // 验证 after_data 包含账号信息 afterData := log.AfterData assert.Equal(t, reqBody.Username, afterData["username"]) assert.Equal(t, reqBody.Phone, afterData["phone"]) }) // 13.3 - 更新账号时记录 before_data 和 after_data t.Run("更新账号时记录before_data和after_data", func(t *testing.T) { shop := env.CreateTestShop("测试店铺", 1, nil) account := env.CreateTestAccount("agent_update", "password123", constants.UserTypeAgent, &shop.ID, nil) // 记录原始数据 originalUsername := account.Username // 更新账号 newUsername := fmt.Sprintf("updated_%d", time.Now().UnixNano()) reqBody := dto.UpdateAccountRequest{ Username: &newUsername, } jsonBody, err := json.Marshal(reqBody) require.NoError(t, err) resp, err := env.AsSuperAdmin().Request("PUT", fmt.Sprintf("/api/admin/accounts/%d", account.ID), jsonBody) require.NoError(t, err) defer resp.Body.Close() require.Equal(t, http.StatusOK, resp.StatusCode) // 等待异步日志写入 time.Sleep(200 * time.Millisecond) // 验证审计日志 var log model.AccountOperationLog err = env.RawDB().Where("target_account_id = ? AND operation_type = ?", account.ID, "update"). Order("created_at DESC").First(&log).Error require.NoError(t, err, "应该存在更新操作的审计日志") // 验证 before_data assert.NotNil(t, log.BeforeData, "更新操作应有 before_data") beforeData := log.BeforeData assert.Equal(t, originalUsername, beforeData["username"]) // 验证 after_data assert.NotNil(t, log.AfterData, "更新操作应有 after_data") afterData := log.AfterData assert.Equal(t, newUsername, afterData["username"]) }) // 13.4 - 删除账号时记录审计日志 t.Run("删除账号时记录审计日志", func(t *testing.T) { shop := env.CreateTestShop("测试店铺", 1, nil) account := env.CreateTestAccount("agent_delete", "password123", constants.UserTypeAgent, &shop.ID, nil) // 删除账号 resp, err := env.AsSuperAdmin().Request("DELETE", fmt.Sprintf("/api/admin/accounts/%d", account.ID), nil) require.NoError(t, err) defer resp.Body.Close() require.Equal(t, http.StatusOK, resp.StatusCode) // 等待异步日志写入 time.Sleep(200 * time.Millisecond) // 验证审计日志 var log model.AccountOperationLog err = env.RawDB().Where("target_account_id = ? AND operation_type = ?", account.ID, "delete"). First(&log).Error require.NoError(t, err, "应该存在删除操作的审计日志") assert.Equal(t, "delete", log.OperationType) assert.NotNil(t, log.BeforeData, "删除操作应有 before_data") assert.Nil(t, log.AfterData, "删除操作不应有 after_data") }) // 13.5 - 分配角色时记录审计日志 t.Run("分配角色时记录审计日志", func(t *testing.T) { shop := env.CreateTestShop("测试店铺", 1, nil) account := env.CreateTestAccount("agent_roles", "password123", constants.UserTypeAgent, &shop.ID, nil) // 代理账号使用 RoleTypeCustomer (2) 类型的角色 role := env.CreateTestRole("测试角色", constants.RoleTypeCustomer) // 分配角色 reqBody := dto.AssignRolesRequest{ RoleIDs: []uint{role.ID}, } jsonBody, err := json.Marshal(reqBody) require.NoError(t, err) resp, err := env.AsSuperAdmin().Request("POST", fmt.Sprintf("/api/admin/accounts/%d/roles", account.ID), jsonBody) require.NoError(t, err) defer resp.Body.Close() require.Equal(t, http.StatusOK, resp.StatusCode) // 等待异步日志写入 time.Sleep(200 * time.Millisecond) // 验证审计日志 var log model.AccountOperationLog err = env.RawDB().Where("target_account_id = ? AND operation_type = ?", account.ID, "assign_roles"). First(&log).Error require.NoError(t, err, "应该存在分配角色操作的审计日志") assert.Equal(t, "assign_roles", log.OperationType) assert.NotNil(t, log.AfterData, "分配角色操作应有 after_data") afterData := log.AfterData roleIDs, ok := afterData["role_ids"].([]interface{}) require.True(t, ok, "after_data 应包含 role_ids 数组") assert.Contains(t, roleIDs, float64(role.ID)) }) // 13.6 - 移除角色时记录审计日志 // 由于路由参数名与 Handler 不匹配(路由用 :account_id,Handler 用 c.Params("id")), // 此测试创建独立的 AccountService 实例直接调用 RemoveRole 方法来验证审计日志 t.Run("移除角色时记录审计日志", func(t *testing.T) { shop := env.CreateTestShop("测试店铺", 1, nil) account := env.CreateTestAccount("agent_remove_role", "password123", constants.UserTypeAgent, &shop.ID, nil) role := env.CreateTestRole("测试角色", constants.RoleTypeCustomer) // 先分配角色 assignReqBody := dto.AssignRolesRequest{ RoleIDs: []uint{role.ID}, } jsonBody, err := json.Marshal(assignReqBody) require.NoError(t, err) resp, err := env.AsSuperAdmin().Request("POST", fmt.Sprintf("/api/admin/accounts/%d/roles", account.ID), jsonBody) require.NoError(t, err) resp.Body.Close() require.Equal(t, http.StatusOK, resp.StatusCode) time.Sleep(200 * time.Millisecond) // 创建独立的 Service 实例来测试 RemoveRole accountStore := postgres.NewAccountStore(env.TX, env.Redis) roleStore := postgres.NewRoleStore(env.TX) accountRoleStore := postgres.NewAccountRoleStore(env.TX, env.Redis) shopStore := postgres.NewShopStore(env.TX, env.Redis) enterpriseStore := postgres.NewEnterpriseStore(env.TX, env.Redis) auditLogStore := postgres.NewAccountOperationLogStore(env.TX) auditService := accountAuditSvc.NewService(auditLogStore) accountService := accountSvc.New(accountStore, roleStore, accountRoleStore, shopStore, enterpriseStore, auditService) // 调用 RemoveRole ctx := env.GetSuperAdminContext() err = accountService.RemoveRole(ctx, account.ID, role.ID) require.NoError(t, err) // 等待异步日志写入 time.Sleep(200 * time.Millisecond) // 验证审计日志 var log model.AccountOperationLog err = env.RawDB().Where("target_account_id = ? AND operation_type = ?", account.ID, "remove_role"). Order("created_at DESC").First(&log).Error require.NoError(t, err, "应该存在移除角色操作的审计日志") assert.Equal(t, "remove_role", log.OperationType) assert.NotNil(t, log.AfterData, "移除角色操作应有 after_data") afterData := log.AfterData assert.Equal(t, float64(role.ID), afterData["removed_role_id"]) }) // 13.7 - 审计日志包含完整的操作上下文 t.Run("审计日志包含完整的操作上下文", func(t *testing.T) { shop := env.CreateTestShop("测试店铺", 1, nil) // 创建账号 reqBody := dto.CreateAccountRequest{ Username: fmt.Sprintf("test_ctx_%d", time.Now().UnixNano()), Phone: fmt.Sprintf("138%08d", time.Now().UnixNano()%100000000), Password: "Password123", UserType: constants.UserTypeAgent, ShopID: &shop.ID, } jsonBody, err := json.Marshal(reqBody) require.NoError(t, err) resp, err := env.AsSuperAdmin().Request("POST", "/api/admin/accounts", jsonBody) require.NoError(t, err) defer resp.Body.Close() require.Equal(t, http.StatusOK, resp.StatusCode) // 解析响应 var result response.Response err = json.NewDecoder(resp.Body).Decode(&result) require.NoError(t, err) data, ok := result.Data.(map[string]interface{}) require.True(t, ok) accountID := extractAccountID(t, data) // 等待异步日志写入 time.Sleep(200 * time.Millisecond) // 验证审计日志包含所有上下文 var log model.AccountOperationLog err = env.RawDB().Where("target_account_id = ?", accountID).First(&log).Error require.NoError(t, err) // 验证操作人信息 assert.NotZero(t, log.OperatorID, "应有操作人ID") assert.NotZero(t, log.OperatorType, "应有操作人类型") assert.NotEmpty(t, log.OperatorName, "应有操作人用户名") // 验证目标账号信息 assert.NotNil(t, log.TargetAccountID, "应有目标账号ID") assert.Equal(t, accountID, *log.TargetAccountID) assert.NotNil(t, log.TargetUsername, "应有目标账号用户名") assert.NotNil(t, log.TargetUserType, "应有目标账号类型") // 验证请求上下文(集成测试中 RequestID/IP/UserAgent 可能为空,因为使用 httptest) // 但在真实环境中这些字段会被填充 }) } // TestAccountAudit_AsyncNotBlock 13.8 - 审计日志写入失败不影响业务操作 // 使用独立环境避免与其他测试的异步 goroutine 冲突 func TestAccountAudit_AsyncNotBlock(t *testing.T) { env := integ.NewIntegrationTestEnv(t) shop := env.CreateTestShop("测试店铺", 1, nil) reqBody := dto.CreateAccountRequest{ Username: fmt.Sprintf("test_async_%d", time.Now().UnixNano()), Phone: fmt.Sprintf("138%08d", time.Now().UnixNano()%100000000), Password: "Password123", UserType: constants.UserTypeAgent, ShopID: &shop.ID, } jsonBody, err := json.Marshal(reqBody) require.NoError(t, err) resp, err := env.AsSuperAdmin().Request("POST", "/api/admin/accounts", jsonBody) require.NoError(t, err) defer resp.Body.Close() require.Equal(t, http.StatusOK, resp.StatusCode) var result response.Response err = json.NewDecoder(resp.Body).Decode(&result) require.NoError(t, err) assert.Equal(t, 0, result.Code, "业务操作应该成功,不受审计日志影响") assert.NotNil(t, result.Data, "应返回创建的账号数据") data, ok := result.Data.(map[string]interface{}) require.True(t, ok) accountID := extractAccountID(t, data) time.Sleep(200 * time.Millisecond) var account model.Account err = env.RawDB().First(&account, accountID).Error require.NoError(t, err, "账号应该被成功创建到数据库") assert.Equal(t, reqBody.Username, account.Username) } // TestAccountAudit_OperationTypes 13.9 - 验证操作类型正确性 // 使用独立环境避免与其他测试的异步 goroutine 冲突 func TestAccountAudit_OperationTypes(t *testing.T) { env := integ.NewIntegrationTestEnv(t) shop := env.CreateTestShop("测试店铺", 1, nil) createReqBody := dto.CreateAccountRequest{ Username: fmt.Sprintf("test_optype_%d", time.Now().UnixNano()), Phone: fmt.Sprintf("138%08d", time.Now().UnixNano()%100000000), Password: "Password123", UserType: constants.UserTypeAgent, ShopID: &shop.ID, } jsonBody, err := json.Marshal(createReqBody) require.NoError(t, err) resp, err := env.AsSuperAdmin().Request("POST", "/api/admin/accounts", jsonBody) require.NoError(t, err) var result response.Response err = json.NewDecoder(resp.Body).Decode(&result) require.NoError(t, err) resp.Body.Close() data, ok := result.Data.(map[string]interface{}) require.True(t, ok) accountID := extractAccountID(t, data) time.Sleep(200 * time.Millisecond) newUsername := fmt.Sprintf("updated_optype_%d", time.Now().UnixNano()) updateReqBody := dto.UpdateAccountRequest{ Username: &newUsername, } jsonBody, err = json.Marshal(updateReqBody) require.NoError(t, err) resp, err = env.AsSuperAdmin().Request("PUT", fmt.Sprintf("/api/admin/accounts/%d", accountID), jsonBody) require.NoError(t, err) resp.Body.Close() time.Sleep(200 * time.Millisecond) var logs []model.AccountOperationLog err = env.RawDB().Where("target_account_id = ?", accountID). Order("created_at ASC").Find(&logs).Error require.NoError(t, err) require.GreaterOrEqual(t, len(logs), 2, "应该至少有 create 和 update 两条审计日志") operationTypes := make(map[string]bool) for _, log := range logs { operationTypes[log.OperationType] = true } assert.True(t, operationTypes["create"], "应该有 create 类型的审计日志") assert.True(t, operationTypes["update"], "应该有 update 类型的审计日志") }