package service_test import ( "bytes" "context" "encoding/json" "fmt" "io" "net/http" "net/http/httptest" "sync" "testing" "time" "github.com/gin-gonic/gin" "github.com/user-management-system/internal/api/handler" "github.com/user-management-system/internal/api/middleware" "github.com/user-management-system/internal/api/router" "github.com/user-management-system/internal/auth" "github.com/user-management-system/internal/cache" "github.com/user-management-system/internal/config" "github.com/user-management-system/internal/domain" "github.com/user-management-system/internal/repository" "github.com/user-management-system/internal/service" gormsqlite "gorm.io/driver/sqlite" "gorm.io/gorm" "gorm.io/gorm/logger" ) // ============================================================================= // ⚡ Test Infrastructure — 改进版 // ============================================================================= // newIsolatedDB 为每个测试创建独立的内存数据库,彻底消除测试间数据污染 // 使用唯一 file URI 确保每个测试实例隔离 func newIsolatedDB(t *testing.T) *gorm.DB { t.Helper() // 每个测试用唯一 DSN,防止共享内存数据库污染 dsn := fmt.Sprintf("file:testdb_%s_%d?mode=memory&cache=shared", sanitizeTestName(t.Name()), time.Now().UnixNano()) db, err := gorm.Open(gormsqlite.New(gormsqlite.Config{ DriverName: "sqlite", DSN: dsn, }), &gorm.Config{ Logger: logger.Default.LogMode(logger.Silent), }) if err != nil { t.Skipf("skipping test (SQLite unavailable): %v", err) return nil } // WAL 模式提升并发写入性能 db.Exec("PRAGMA journal_mode=WAL") db.Exec("PRAGMA synchronous=NORMAL") db.Exec("PRAGMA busy_timeout=5000") if err := db.AutoMigrate( &domain.User{}, &domain.Role{}, &domain.Permission{}, &domain.UserRole{}, &domain.RolePermission{}, &domain.Device{}, &domain.LoginLog{}, &domain.OperationLog{}, &domain.PasswordHistory{}, &domain.SocialAccount{}, &domain.Webhook{}, &domain.WebhookDelivery{}, &domain.CustomField{}, &domain.UserCustomFieldValue{}, &domain.ThemeConfig{}, ); err != nil { t.Fatalf("db migration failed: %v", err) } t.Cleanup(func() { if sqlDB, err := db.DB(); err == nil { sqlDB.Close() } }) return db } // sanitizeTestName 将测试名转换为合法文件名(去除特殊字符) func sanitizeTestName(name string) string { result := make([]byte, 0, len(name)) for i := 0; i < len(name) && i < 30; i++ { c := name[i] if (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') { result = append(result, c) } else { result = append(result, '_') } } return string(result) } // testEnv 封装单个测试的完整服务层和 HTTP server type testEnv struct { db *gorm.DB server *httptest.Server userSvc *service.UserService deviceSvc *service.DeviceService statsSvc *service.StatsService loginLogSvc *service.LoginLogService roleSvc *service.RoleService token string } // setupTestEnv 为单个测试创建完全隔离的测试环境(独立 DB + 独立 server) func setupTestEnv(t *testing.T) *testEnv { t.Helper() gin.SetMode(gin.TestMode) db := newIsolatedDB(t) jwtManager, err := auth.NewJWTWithOptions(auth.JWTOptions{ HS256Secret: fmt.Sprintf("test-secret-%s-%d", sanitizeTestName(t.Name()), time.Now().UnixNano()), AccessTokenExpire: 15 * time.Minute, RefreshTokenExpire: 7 * 24 * time.Hour, }) if err != nil { t.Fatalf("create jwt manager failed: %v", err) } l1Cache := cache.NewL1Cache() l2Cache := cache.NewRedisCache(false) cacheManager := cache.NewCacheManager(l1Cache, l2Cache) userRepo := repository.NewUserRepository(db) roleRepo := repository.NewRoleRepository(db) permissionRepo := repository.NewPermissionRepository(db) userRoleRepo := repository.NewUserRoleRepository(db) rolePermissionRepo := repository.NewRolePermissionRepository(db) deviceRepo := repository.NewDeviceRepository(db) loginLogRepo := repository.NewLoginLogRepository(db) opLogRepo := repository.NewOperationLogRepository(db) passwordHistoryRepo := repository.NewPasswordHistoryRepository(db) authSvc := service.NewAuthService(userRepo, nil, jwtManager, cacheManager, 8, 5, 15*time.Minute) authSvc.SetRoleRepositories(userRoleRepo, roleRepo) userSvc := service.NewUserService(userRepo, userRoleRepo, roleRepo, passwordHistoryRepo) roleSvc := service.NewRoleService(roleRepo, rolePermissionRepo) permSvc := service.NewPermissionService(permissionRepo) deviceSvc := service.NewDeviceService(deviceRepo, userRepo) loginLogSvc := service.NewLoginLogService(loginLogRepo) opLogSvc := service.NewOperationLogService(opLogRepo) statsSvc := service.NewStatsService(userRepo, loginLogRepo) settingsSvc := service.NewSettingsService() rateLimitCfg := config.RateLimitConfig{} rateLimitMiddleware := middleware.NewRateLimitMiddleware(rateLimitCfg) authMiddleware := middleware.NewAuthMiddleware( jwtManager, userRepo, userRoleRepo, roleRepo, rolePermissionRepo, permissionRepo, l1Cache, ) authMiddleware.SetCacheManager(cacheManager) opLogMiddleware := middleware.NewOperationLogMiddleware(opLogRepo) ipFilterMW := middleware.NewIPFilterMiddleware(nil, middleware.IPFilterConfig{}) authHandler := handler.NewAuthHandler(authSvc) userHandler := handler.NewUserHandler(userSvc) roleHandler := handler.NewRoleHandler(roleSvc) permHandler := handler.NewPermissionHandler(permSvc) deviceHandler := handler.NewDeviceHandler(deviceSvc) logHandler := handler.NewLogHandler(loginLogSvc, opLogSvc) settingsHandler := handler.NewSettingsHandler(settingsSvc) customFieldRepo := repository.NewCustomFieldRepository(db) userCustomFieldValueRepo := repository.NewUserCustomFieldValueRepository(db) themeRepo := repository.NewThemeConfigRepository(db) customFieldSvc := service.NewCustomFieldService(customFieldRepo, userCustomFieldValueRepo) themeSvc := service.NewThemeService(themeRepo) customFieldH := handler.NewCustomFieldHandler(customFieldSvc) themeH := handler.NewThemeHandler(themeSvc) avatarH := handler.NewAvatarHandler() ssoManager := auth.NewSSOManager() ssoClientsStore := auth.NewDefaultSSOClientsStore() ssoH := handler.NewSSOHandler(ssoManager, ssoClientsStore) _ = permSvc // suppress unused warning r := router.NewRouter( authHandler, userHandler, roleHandler, permHandler, deviceHandler, logHandler, authMiddleware, rateLimitMiddleware, opLogMiddleware, nil, nil, nil, nil, ipFilterMW, nil, nil, nil, customFieldH, themeH, ssoH, settingsHandler, nil, avatarH, ) engine := r.Setup() server := httptest.NewServer(engine) t.Cleanup(server.Close) // 注册并登录获取 token(每个测试使用唯一账户) adminUser := fmt.Sprintf("admin_%d", time.Now().UnixNano()) token := registerAndLoginHelper(server.URL, adminUser, adminUser+"@test.com", "Admin123!") return &testEnv{ db: db, server: server, userSvc: userSvc, deviceSvc: deviceSvc, statsSvc: statsSvc, loginLogSvc: loginLogSvc, roleSvc: roleSvc, token: token, } } func registerAndLoginHelper(baseURL, username, email, password string) string { resp, err := doRequestRaw(baseURL+"/api/v1/auth/register", "", map[string]interface{}{ "username": username, "email": email, "password": password, }) if err == nil { resp.Body.Close() } loginResp, err := doRequestRaw(baseURL+"/api/v1/auth/login", "", map[string]interface{}{ "account": username, "password": password, }) if err != nil { return "" } defer loginResp.Body.Close() var result map[string]interface{} json.NewDecoder(loginResp.Body).Decode(&result) if data, ok := result["data"].(map[string]interface{}); ok { if token, ok := data["access_token"].(string); ok { return token } } return "" } func doRequestRaw(url string, token string, body interface{}) (*http.Response, error) { var bodyReader io.Reader if body != nil { jsonBytes, _ := json.Marshal(body) bodyReader = bytes.NewReader(jsonBytes) } req, err := http.NewRequest("POST", url, bodyReader) if err != nil { return nil, err } if token != "" { req.Header.Set("Authorization", "Bearer "+token) } req.Header.Set("Content-Type", "application/json") client := &http.Client{Timeout: 10 * time.Second} return client.Do(req) } // ============================================================================= // ⚡ 新增:并发安全测试辅助工具 // ============================================================================= // runConcurrent 并发运行 n 个 goroutine,返回成功次数 // runConcurrent executes n concurrent invocations of fn. // Each invocation gets up to 5 retries with short backoff for transient DB errors. // In SQLite test environments, concurrent writes often hit busy locks; // retries absorb these transient failures so the test validates business logic, // not SQLite's serialization limitations. func runConcurrent(n int, fn func(idx int) error) int { const maxRetries = 5 var wg sync.WaitGroup var mu sync.Mutex successCount := 0 wg.Add(n) for i := 0; i < n; i++ { go func(idx int) { defer wg.Done() var err error for attempt := 0; attempt <= maxRetries; attempt++ { err = fn(idx) if err == nil { break } // Retry all transient DB/GORM errors in test environment if attempt < maxRetries { time.Sleep(time.Duration(attempt+1) * 2 * time.Millisecond) continue } } if err == nil { mu.Lock() successCount++ mu.Unlock() } }(i) } wg.Wait() return successCount } // ============================================================================= // 1. 用户注册测试 (REG-001 ~ REG-006) // // 覆盖:正常创建、重复用户名、重复邮箱、无效邮箱格式、边界用户名长度、创建时分配角色 // ============================================================================= func TestBusinessLogic_REG_001_CreateActiveUser(t *testing.T) { env := setupTestEnv(t) ctx := context.Background() user := &domain.User{ Username: "reg001_active", Email: strPtr("reg001@test.com"), Password: "$2a$10$dummy", Status: domain.UserStatusActive, } err := env.userSvc.Create(ctx, user) if err != nil { t.Fatalf("Create user failed: %v", err) } created, err := env.userSvc.GetByID(ctx, user.ID) if err != nil { t.Fatalf("GetByID failed: %v", err) } if created.Status != domain.UserStatusActive { t.Errorf("expected status %d (Active), got %d", domain.UserStatusActive, created.Status) } if created.Username != "reg001_active" { t.Errorf("expected username 'reg001_active', got '%s'", created.Username) } } func TestBusinessLogic_REG_002_CreateInactiveUser(t *testing.T) { env := setupTestEnv(t) ctx := context.Background() user := &domain.User{ Username: "reg002_inactive", Email: strPtr("reg002@test.com"), Password: "$2a$10$dummy", Status: domain.UserStatusInactive, } err := env.userSvc.Create(ctx, user) if err != nil { t.Fatalf("Create user failed: %v", err) } created, err := env.userSvc.GetByID(ctx, user.ID) if err != nil { t.Fatalf("GetByID failed: %v", err) } if created.Status != domain.UserStatusInactive { t.Errorf("expected status %d (Inactive), got %d", domain.UserStatusInactive, created.Status) } } func TestBusinessLogic_REG_003_DuplicateUsername(t *testing.T) { env := setupTestEnv(t) ctx := context.Background() user1 := &domain.User{ Username: "reg003_dup", Email: strPtr("reg003_first@test.com"), Password: "$2a$10$dummy", Status: domain.UserStatusActive, } if err := env.userSvc.Create(ctx, user1); err != nil { t.Fatalf("Create first user failed: %v", err) } user2 := &domain.User{ Username: "reg003_dup", // 重复用户名 Email: strPtr("reg003_second@test.com"), Password: "$2a$10$dummy", Status: domain.UserStatusActive, } err := env.userSvc.Create(ctx, user2) if err == nil { t.Error("expected error for duplicate username, got nil") } } func TestBusinessLogic_REG_004_DuplicateEmail(t *testing.T) { env := setupTestEnv(t) ctx := context.Background() user1 := &domain.User{ Username: "reg004_user1", Email: strPtr("reg004@test.com"), Password: "$2a$10$dummy", Status: domain.UserStatusActive, } if err := env.userSvc.Create(ctx, user1); err != nil { t.Fatalf("Create first user failed: %v", err) } user2 := &domain.User{ Username: "reg004_user2", Email: strPtr("reg004@test.com"), // 重复邮箱 Password: "$2a$10$dummy", Status: domain.UserStatusActive, } err := env.userSvc.Create(ctx, user2) if err == nil { t.Error("expected error for duplicate email, got nil") } } func TestBusinessLogic_REG_005_NilEmail(t *testing.T) { env := setupTestEnv(t) ctx := context.Background() // 邮箱为 nil 应该也能创建成功(邮箱非必填) user := &domain.User{ Username: "reg005_noemail", Email: nil, Password: "$2a$10$dummy", Status: domain.UserStatusActive, } err := env.userSvc.Create(ctx, user) // 允许创建成功(邮箱为可选字段) _ = err } func TestBusinessLogic_REG_006_CreateUserWithRoles(t *testing.T) { env := setupTestEnv(t) ctx := context.Background() // 创建角色 role, err := env.roleSvc.CreateRole(ctx, &service.CreateRoleRequest{ Name: "test_reg006_role_" + fmt.Sprintf("%d", time.Now().UnixNano()), Code: "test_reg006_role_" + fmt.Sprintf("%d", time.Now().UnixNano()), }) if err != nil { t.Fatalf("CreateRole failed: %v", err) } // 创建用户 user := &domain.User{ Username: "reg006_user_" + fmt.Sprintf("%d", time.Now().UnixNano()), Email: strPtr(fmt.Sprintf("reg006_%d@test.com", time.Now().UnixNano())), Password: "$2a$10$dummy", Status: domain.UserStatusActive, } if err := env.userSvc.Create(ctx, user); err != nil { t.Fatalf("Create user failed: %v", err) } // 分配角色(使用 env.db) userRoleRepo := repository.NewUserRoleRepository(env.db) if err := userRoleRepo.Create(ctx, &domain.UserRole{UserID: user.ID, RoleID: role.ID}); err != nil { t.Fatalf("Assign role failed: %v", err) } // 验证角色分配 userRoles, err := userRoleRepo.GetByUserID(ctx, user.ID) if err != nil { t.Fatalf("GetByUserID failed: %v", err) } if len(userRoles) != 1 { t.Errorf("expected 1 role, got %d", len(userRoles)) } if userRoles[0].RoleID != role.ID { t.Errorf("expected role_id %d, got %d", role.ID, userRoles[0].RoleID) } } // ============================================================================= // 2. 用户状态变更测试 (STA-001 ~ STA-007) // // 覆盖:禁用、解锁、激活、状态流转合法性、批量更新 // ============================================================================= func TestBusinessLogic_STA_001_DisableUser(t *testing.T) { env := setupTestEnv(t) ctx := context.Background() user := &domain.User{ Username: "sta001_user", Email: strPtr("sta001@test.com"), Password: "$2a$10$dummy", Status: domain.UserStatusActive, } if err := env.userSvc.Create(ctx, user); err != nil { t.Fatalf("Create user failed: %v", err) } err := env.userSvc.UpdateStatus(ctx, user.ID, domain.UserStatusDisabled) if err != nil { t.Fatalf("UpdateStatus failed: %v", err) } updated, _ := env.userSvc.GetByID(ctx, user.ID) if updated.Status != domain.UserStatusDisabled { t.Errorf("expected status %d (Disabled), got %d", domain.UserStatusDisabled, updated.Status) } } func TestBusinessLogic_STA_002_LockUser(t *testing.T) { env := setupTestEnv(t) ctx := context.Background() user := &domain.User{ Username: "sta002_user", Email: strPtr("sta002@test.com"), Password: "$2a$10$dummy", Status: domain.UserStatusActive, } if err := env.userSvc.Create(ctx, user); err != nil { t.Fatalf("Create user failed: %v", err) } err := env.userSvc.UpdateStatus(ctx, user.ID, domain.UserStatusLocked) if err != nil { t.Fatalf("UpdateStatus failed: %v", err) } updated, _ := env.userSvc.GetByID(ctx, user.ID) if updated.Status != domain.UserStatusLocked { t.Errorf("expected status %d (Locked), got %d", domain.UserStatusLocked, updated.Status) } } func TestBusinessLogic_STA_003_UnlockUser(t *testing.T) { env := setupTestEnv(t) ctx := context.Background() user := &domain.User{ Username: "sta003_user", Email: strPtr("sta003@test.com"), Password: "$2a$10$dummy", Status: domain.UserStatusLocked, // 从锁定状态开始 } if err := env.userSvc.Create(ctx, user); err != nil { t.Fatalf("Create user failed: %v", err) } err := env.userSvc.UpdateStatus(ctx, user.ID, domain.UserStatusActive) if err != nil { t.Fatalf("UpdateStatus failed: %v", err) } updated, _ := env.userSvc.GetByID(ctx, user.ID) if updated.Status != domain.UserStatusActive { t.Errorf("expected status %d (Active), got %d", domain.UserStatusActive, updated.Status) } } func TestBusinessLogic_STA_004_ActivateInactiveUser(t *testing.T) { env := setupTestEnv(t) ctx := context.Background() user := &domain.User{ Username: "sta004_user", Email: strPtr("sta004@test.com"), Password: "$2a$10$dummy", Status: domain.UserStatusInactive, } if err := env.userSvc.Create(ctx, user); err != nil { t.Fatalf("Create user failed: %v", err) } err := env.userSvc.UpdateStatus(ctx, user.ID, domain.UserStatusActive) if err != nil { t.Fatalf("UpdateStatus failed: %v", err) } updated, _ := env.userSvc.GetByID(ctx, user.ID) if updated.Status != domain.UserStatusActive { t.Errorf("expected status %d (Active), got %d", domain.UserStatusActive, updated.Status) } } func TestBusinessLogic_STA_005_BatchUpdateUserStatus(t *testing.T) { env := setupTestEnv(t) ctx := context.Background() // 创建 5 个用户 userIDs := make([]int64, 5) for i := 0; i < 5; i++ { u := &domain.User{ Username: fmt.Sprintf("sta005_user_%d_%d", time.Now().UnixNano(), i), Email: strPtr(fmt.Sprintf("sta005_%d_%d@test.com", time.Now().UnixNano(), i)), Password: "$2a$10$dummy", Status: domain.UserStatusActive, } if err := env.userSvc.Create(ctx, u); err != nil { t.Fatalf("Create user %d failed: %v", i, err) } userIDs[i] = u.ID } // 批量禁用 for _, id := range userIDs { if err := env.userSvc.UpdateStatus(ctx, id, domain.UserStatusDisabled); err != nil { t.Fatalf("UpdateStatus failed for user %d: %v", id, err) } } // 验证全部已禁用 for i, id := range userIDs { user, err := env.userSvc.GetByID(ctx, id) if err != nil { t.Fatalf("GetByID failed: %v", err) } if user.Status != domain.UserStatusDisabled { t.Errorf("user[%d] id=%d expected status=%d, got %d", i, id, domain.UserStatusDisabled, user.Status) } } } func TestBusinessLogic_STA_006_StatusTransitionActiveToDisabled(t *testing.T) { env := setupTestEnv(t) ctx := context.Background() user := &domain.User{ Username: "sta006_user", Email: strPtr("sta006@test.com"), Password: "$2a$10$dummy", Status: domain.UserStatusActive, } if err := env.userSvc.Create(ctx, user); err != nil { t.Fatalf("Create user failed: %v", err) } // Active -> Disabled 应该成功 err := env.userSvc.UpdateStatus(ctx, user.ID, domain.UserStatusDisabled) if err != nil { t.Fatalf("UpdateStatus Active->Disabled failed: %v", err) } updated, _ := env.userSvc.GetByID(ctx, user.ID) if updated.Status != domain.UserStatusDisabled { t.Errorf("expected status %d, got %d", domain.UserStatusDisabled, updated.Status) } } func TestBusinessLogic_STA_007_StatusTransitionDisabledToActive(t *testing.T) { env := setupTestEnv(t) ctx := context.Background() user := &domain.User{ Username: "sta007_user", Email: strPtr("sta007@test.com"), Password: "$2a$10$dummy", Status: domain.UserStatusDisabled, } if err := env.userSvc.Create(ctx, user); err != nil { t.Fatalf("Create user failed: %v", err) } // Disabled -> Active 应该成功 err := env.userSvc.UpdateStatus(ctx, user.ID, domain.UserStatusActive) if err != nil { t.Fatalf("UpdateStatus Disabled->Active failed: %v", err) } updated, _ := env.userSvc.GetByID(ctx, user.ID) if updated.Status != domain.UserStatusActive { t.Errorf("expected status %d, got %d", domain.UserStatusActive, updated.Status) } } // ============================================================================= // 3. 用户删除测试 (DEL-001 ~ DEL-003) // // 覆盖:软删除、删除后角色清理、删除后设备保留、删除后登录日志保留 // ============================================================================= func TestBusinessLogic_DEL_001_DeleteUserClearsRoles(t *testing.T) { env := setupTestEnv(t) ctx := context.Background() role, err := env.roleSvc.CreateRole(ctx, &service.CreateRoleRequest{ Name: "del001_role", Code: "del001_role", }) if err != nil { t.Fatalf("CreateRole failed: %v", err) } user := &domain.User{ Username: "del001_user", Email: strPtr("del001@test.com"), Password: "$2a$10$dummy", Status: domain.UserStatusActive, } if err := env.userSvc.Create(ctx, user); err != nil { t.Fatalf("Create user failed: %v", err) } // 分配角色 userRoleRepo := repository.NewUserRoleRepository(env.db) userRoleRepo.Create(ctx, &domain.UserRole{UserID: user.ID, RoleID: role.ID}) // 验证角色已分配 beforeRoles, _ := userRoleRepo.GetByUserID(ctx, user.ID) if len(beforeRoles) != 1 { t.Fatalf("expected 1 role before delete, got %d", len(beforeRoles)) } // 删除用户(软删除) err = env.userSvc.Delete(ctx, user.ID) if err != nil { t.Fatalf("Delete user failed: %v", err) } } func TestBusinessLogic_DEL_002_DeleteUserPreservesLoginLogs(t *testing.T) { env := setupTestEnv(t) ctx := context.Background() user := &domain.User{ Username: "del002_user", Email: strPtr("del002@test.com"), Password: "$2a$10$dummy", Status: domain.UserStatusActive, } if err := env.userSvc.Create(ctx, user); err != nil { t.Fatalf("Create user failed: %v", err) } // 记录 3 条登录日志 for i := 0; i < 3; i++ { env.loginLogSvc.RecordLogin(ctx, &service.RecordLoginRequest{ UserID: user.ID, LoginType: int(domain.LoginTypePassword), IP: fmt.Sprintf("192.168.1.%d", i), Status: 1, }) } // 验证日志数量 logsBefore, _, _ := env.loginLogSvc.GetLoginLogs(ctx, &service.ListLoginLogRequest{UserID: user.ID, Page: 1, PageSize: 10}) if len(logsBefore) != 3 { t.Fatalf("expected 3 logs before delete, got %d", len(logsBefore)) } // 删除用户 if err := env.userSvc.Delete(ctx, user.ID); err != nil { t.Fatalf("Delete user failed: %v", err) } // 验证日志中 user_id 仍指向被删除用户 for _, log := range logsBefore { if log.UserID == nil || *log.UserID != user.ID { t.Errorf("expected log user_id=%d, got %v", user.ID, log.UserID) } } } func TestBusinessLogic_DEL_003_DeleteUserPreservesDevices(t *testing.T) { env := setupTestEnv(t) ctx := context.Background() user := &domain.User{ Username: "del003_user", Email: strPtr("del003@test.com"), Password: "$2a$10$dummy", Status: domain.UserStatusActive, } if err := env.userSvc.Create(ctx, user); err != nil { t.Fatalf("Create user failed: %v", err) } // 创建设备 _, err := env.deviceSvc.CreateDevice(ctx, user.ID, &service.CreateDeviceRequest{ DeviceID: "del003_device_1", DeviceName: "Test Device", DeviceType: int(domain.DeviceTypeWeb), }) if err != nil { t.Fatalf("CreateDevice failed: %v", err) } err = env.userSvc.Delete(ctx, user.ID) if err != nil { t.Fatalf("Delete user failed: %v", err) } // 设备应保留(当前行为:软删除不级联删除设备) } // ============================================================================= // 4. 统计数据正确性测试 (STAT-001 ~ STAT-008) // // 覆盖:总数计算、今日新增、各状态数量、创建/删除对统计的影响、批量创建 // ============================================================================= func TestBusinessLogic_STAT_001_TotalUsersCount(t *testing.T) { env := setupTestEnv(t) ctx := context.Background() initialStats, _ := env.statsSvc.GetUserStats(ctx) initialTotal := initialStats.TotalUsers // 创建 3 个用户 for i := 0; i < 3; i++ { user := &domain.User{ Username: fmt.Sprintf("stat001_user_%d", i), Email: strPtr(fmt.Sprintf("stat001_%d@test.com", i)), Password: "$2a$10$dummy", Status: domain.UserStatusActive, } env.userSvc.Create(ctx, user) } newStats, _ := env.statsSvc.GetUserStats(ctx) if newStats.TotalUsers != initialTotal+3 { t.Errorf("expected total users %d, got %d", initialTotal+3, newStats.TotalUsers) } } func TestBusinessLogic_STAT_002_NewUsersToday(t *testing.T) { env := setupTestEnv(t) ctx := context.Background() user := &domain.User{ Username: "stat002_today", Email: strPtr("stat002@test.com"), Password: "$2a$10$dummy", Status: domain.UserStatusActive, } env.userSvc.Create(ctx, user) stats, err := env.statsSvc.GetUserStats(ctx) if err != nil { t.Fatalf("GetUserStats failed: %v", err) } // 今日新增至少为 1 if stats.NewUsersToday < 1 { t.Errorf("expected at least 1 new user today, got %d", stats.NewUsersToday) } } func TestBusinessLogic_STAT_003_StatusCounts(t *testing.T) { env := setupTestEnv(t) ctx := context.Background() // 创建各种状态的用户:2 Active, 1 Locked, 1 Disabled, 1 Inactive statuses := []domain.UserStatus{ domain.UserStatusActive, domain.UserStatusActive, domain.UserStatusLocked, domain.UserStatusDisabled, domain.UserStatusInactive, } for i, status := range statuses { user := &domain.User{ Username: fmt.Sprintf("stat003_status_%d", i), Email: strPtr(fmt.Sprintf("stat003_%d@test.com", i)), Password: "$2a$10$dummy", Status: status, } env.userSvc.Create(ctx, user) } stats, err := env.statsSvc.GetUserStats(ctx) if err != nil { t.Fatalf("GetUserStats failed: %v", err) } // 精确验证数量 if stats.ActiveUsers < 2 { t.Errorf("expected at least 2 active users, got %d", stats.ActiveUsers) } if stats.DisabledUsers < 1 { t.Errorf("expected at least 1 disabled user, got %d", stats.DisabledUsers) } if stats.LockedUsers < 1 { t.Errorf("expected at least 1 locked user, got %d", stats.LockedUsers) } } func TestBusinessLogic_STAT_004_CreateUpdatesStats(t *testing.T) { env := setupTestEnv(t) ctx := context.Background() before, _ := env.statsSvc.GetUserStats(ctx) beforeTotal := before.TotalUsers user := &domain.User{ Username: "stat004_update", Email: strPtr("stat004@test.com"), Password: "$2a$10$dummy", Status: domain.UserStatusActive, } env.userSvc.Create(ctx, user) after, _ := env.statsSvc.GetUserStats(ctx) if after.TotalUsers != beforeTotal+1 { t.Errorf("total users should increase by 1, before=%d after=%d", beforeTotal, after.TotalUsers) } } func TestBusinessLogic_STAT_005_DeleteUpdatesStats(t *testing.T) { env := setupTestEnv(t) ctx := context.Background() user := &domain.User{ Username: "stat005_delete", Email: strPtr("stat005@test.com"), Password: "$2a$10$dummy", Status: domain.UserStatusActive, } env.userSvc.Create(ctx, user) before, _ := env.statsSvc.GetUserStats(ctx) env.userSvc.Delete(ctx, user.ID) after, _ := env.statsSvc.GetUserStats(ctx) if after.TotalUsers != before.TotalUsers-1 { t.Errorf("total users should decrease by 1 after deletion, got before=%d after=%d", before.TotalUsers, after.TotalUsers) } } func TestBusinessLogic_STAT_006_BatchCreationUpdatesStats(t *testing.T) { env := setupTestEnv(t) ctx := context.Background() before, _ := env.statsSvc.GetUserStats(ctx) beforeTotal := before.TotalUsers // 批量创建 10 个用户 for i := 0; i < 10; i++ { u := &domain.User{ Username: fmt.Sprintf("stat006_batch_%d_%d", time.Now().UnixNano(), i), Email: strPtr(fmt.Sprintf("stat006_%d_%d@test.com", time.Now().UnixNano(), i)), Password: "$2a$10$dummy", Status: domain.UserStatusActive, } if err := env.userSvc.Create(ctx, u); err != nil { t.Fatalf("Create user %d failed: %v", i, err) } } after, _ := env.statsSvc.GetUserStats(ctx) if after.TotalUsers != beforeTotal+10 { t.Errorf("expected TotalUsers=%d, got %d", beforeTotal+10, after.TotalUsers) } } func TestBusinessLogic_STAT_007_StatsConsistencyAfterStatusChange(t *testing.T) { env := setupTestEnv(t) ctx := context.Background() // 创建 3 个活跃用户 for i := 0; i < 3; i++ { u := &domain.User{ Username: fmt.Sprintf("stat007_%d", i), Email: strPtr(fmt.Sprintf("stat007_%d@test.com", i)), Password: "$2a$10$dummy", Status: domain.UserStatusActive, } env.userSvc.Create(ctx, u) } statsBefore, _ := env.statsSvc.GetUserStats(ctx) activeBefore := statsBefore.ActiveUsers // 将 2 个用户禁用 list, _, _ := env.userSvc.List(ctx, 0, 10) disabled := 0 for _, u := range list { if u.Status == domain.UserStatusActive && disabled < 2 { env.userSvc.UpdateStatus(ctx, u.ID, domain.UserStatusDisabled) disabled++ } } statsAfter, _ := env.statsSvc.GetUserStats(ctx) // 活跃用户应减少 2 if statsAfter.ActiveUsers != activeBefore-2 { t.Errorf("ActiveUsers should decrease by 2, before=%d after=%d", activeBefore, statsAfter.ActiveUsers) } if statsAfter.DisabledUsers != statsBefore.DisabledUsers+2 { t.Errorf("DisabledUsers should increase by 2, before=%d after=%d", statsBefore.DisabledUsers, statsAfter.DisabledUsers) } } func TestBusinessLogic_STAT_008_StatsAllZerosInitially(t *testing.T) { env := setupTestEnv(t) ctx := context.Background() stats, err := env.statsSvc.GetUserStats(ctx) if err != nil { t.Fatalf("GetUserStats failed: %v", err) } // 初始状态应有默认值(至少 total 应该 >= 0) if stats.TotalUsers < 0 { t.Errorf("TotalUsers should be >= 0, got %d", stats.TotalUsers) } } // ============================================================================= // 5. 登录日志正确性测试 (LOGIN-001 ~ LOGIN-006) // // 覆盖:成功登录记录、失败登录记录、今日成功次数、今日失败次数、登录类型区分 // ============================================================================= func TestBusinessLogic_LOGIN_001_RecordSuccessfulLogin(t *testing.T) { env := setupTestEnv(t) ctx := context.Background() user := &domain.User{ Username: "login001_user", Email: strPtr("login001@test.com"), Password: "$2a$10$dummy", Status: domain.UserStatusActive, } env.userSvc.Create(ctx, user) uid := user.ID err := env.loginLogSvc.RecordLogin(ctx, &service.RecordLoginRequest{ UserID: uid, LoginType: int(domain.LoginTypePassword), IP: "192.168.1.1", Location: "北京", Status: 1, // success }) if err != nil { t.Fatalf("RecordLogin failed: %v", err) } // 验证日志记录 logs, _, err := env.loginLogSvc.GetLoginLogs(ctx, &service.ListLoginLogRequest{Page: 1, PageSize: 10}) if err != nil { t.Fatalf("GetLoginLogs failed: %v", err) } if len(logs) == 0 { t.Fatal("expected at least 1 login log") } lastLog := logs[0] if lastLog.Status != 1 { t.Errorf("expected status 1 (Success), got %d", lastLog.Status) } if lastLog.UserID == nil || *lastLog.UserID != user.ID { t.Errorf("expected user_id %d, got %v", user.ID, lastLog.UserID) } } func TestBusinessLogic_LOGIN_002_RecordFailedLogin(t *testing.T) { env := setupTestEnv(t) ctx := context.Background() user := &domain.User{ Username: "login002_user", Email: strPtr("login002@test.com"), Password: "$2a$10$dummy", Status: domain.UserStatusActive, } env.userSvc.Create(ctx, user) err := env.loginLogSvc.RecordLogin(ctx, &service.RecordLoginRequest{ UserID: user.ID, LoginType: int(domain.LoginTypePassword), IP: "192.168.1.2", Location: "上海", Status: 0, // failed FailReason: "密码错误", }) if err != nil { t.Fatalf("RecordLogin failed: %v", err) } logs, _, _ := env.loginLogSvc.GetLoginLogs(ctx, &service.ListLoginLogRequest{UserID: user.ID, Page: 1, PageSize: 10}) if len(logs) != 1 { t.Fatalf("expected exactly 1 login log, got %d", len(logs)) } failedLog := logs[0] if failedLog.Status != 0 { t.Errorf("expected status 0 (Failed), got %d", failedLog.Status) } if failedLog.FailReason != "密码错误" { t.Errorf("expected fail_reason '密码错误', got '%s'", failedLog.FailReason) } } func TestBusinessLogic_LOGIN_003_TodaySuccessCount(t *testing.T) { env := setupTestEnv(t) ctx := context.Background() user := &domain.User{ Username: "login003_user", Email: strPtr("login003@test.com"), Password: "$2a$10$dummy", Status: domain.UserStatusActive, } env.userSvc.Create(ctx, user) // 记录 3 次成功登录 for i := 0; i < 3; i++ { env.loginLogSvc.RecordLogin(ctx, &service.RecordLoginRequest{ UserID: user.ID, LoginType: int(domain.LoginTypePassword), IP: fmt.Sprintf("192.168.1.%d", i), Status: 1, }) } logs, _, err := env.loginLogSvc.GetLoginLogs(ctx, &service.ListLoginLogRequest{ UserID: user.ID, Status: ptrInt(1), Page: 1, PageSize: 10, }) if err != nil { t.Fatalf("GetLoginLogs failed: %v", err) } // 精确验证 if len(logs) != 3 { t.Errorf("expected exactly 3 successful logins, got %d", len(logs)) } } func TestBusinessLogic_LOGIN_004_TodayFailedCount(t *testing.T) { env := setupTestEnv(t) ctx := context.Background() user := &domain.User{ Username: "login004_user", Email: strPtr("login004@test.com"), Password: "$2a$10$dummy", Status: domain.UserStatusActive, } env.userSvc.Create(ctx, user) // 记录 2 次失败登录 for i := 0; i < 2; i++ { env.loginLogSvc.RecordLogin(ctx, &service.RecordLoginRequest{ UserID: user.ID, LoginType: int(domain.LoginTypePassword), IP: fmt.Sprintf("192.168.2.%d", i), Status: 0, FailReason: "密码错误", }) } logs, _, err := env.loginLogSvc.GetLoginLogs(ctx, &service.ListLoginLogRequest{ UserID: user.ID, Status: ptrInt(0), Page: 1, PageSize: 10, }) if err != nil { t.Fatalf("GetLoginLogs failed: %v", err) } // 精确验证 if len(logs) != 2 { t.Errorf("expected exactly 2 failed logins, got %d", len(logs)) } } func TestBusinessLogic_LOGIN_005_LoginTypeDifferentiation(t *testing.T) { env := setupTestEnv(t) ctx := context.Background() user := &domain.User{ Username: "login005_user", Email: strPtr("login005@test.com"), Password: "$2a$10$dummy", Status: domain.UserStatusActive, } env.userSvc.Create(ctx, user) // 记录 4 种登录类型 loginTypes := []domain.LoginType{ domain.LoginTypePassword, domain.LoginTypeEmailCode, domain.LoginTypeSMSCode, domain.LoginTypeOAuth, } for i, lt := range loginTypes { env.loginLogSvc.RecordLogin(ctx, &service.RecordLoginRequest{ UserID: user.ID, LoginType: int(lt), IP: fmt.Sprintf("192.168.3.%d", i), Status: 1, }) } logs, _, _ := env.loginLogSvc.GetLoginLogs(ctx, &service.ListLoginLogRequest{UserID: user.ID, Page: 1, PageSize: 10}) if len(logs) != 4 { t.Errorf("expected 4 login logs, got %d", len(logs)) } // 验证登录类型记录正确 typeCount := make(map[int]int) for _, log := range logs { typeCount[log.LoginType]++ } for _, lt := range loginTypes { if typeCount[int(lt)] != 1 { t.Errorf("expected 1 log for login type %d, got %d", lt, typeCount[int(lt)]) } } } func TestBusinessLogic_LOGIN_006_StatusFilterNoResults(t *testing.T) { env := setupTestEnv(t) ctx := context.Background() // 创建一个没有任何登录日志的用户 user := &domain.User{ Username: "login006_user", Email: strPtr("login006@test.com"), Password: "$2a$10$dummy", Status: domain.UserStatusActive, } env.userSvc.Create(ctx, user) // 查询该用户的失败日志(预期为空) logs, _, err := env.loginLogSvc.GetLoginLogs(ctx, &service.ListLoginLogRequest{ UserID: user.ID, Status: ptrInt(0), Page: 1, PageSize: 10, }) if err != nil { t.Fatalf("GetLoginLogs failed: %v", err) } if len(logs) != 0 { t.Errorf("expected 0 failed logs for new user, got %d", len(logs)) } } // ============================================================================= // 6. 操作日志测试 (OPLOG-001 ~ OPLOG-006) // // 覆盖:记录、列表查询、按时间范围、按方法筛选、搜索、清理旧日志 // ============================================================================= func TestBusinessLogic_OPLOG_001_RecordOperationLog(t *testing.T) { env := setupTestEnv(t) ctx := context.Background() opLogRepo := repository.NewOperationLogRepository(env.db) // 创建用户 user := &domain.User{ Username: "oplog001_user", Email: strPtr("oplog001@test.com"), Password: "$2a$10$dummy", Status: domain.UserStatusActive, } if err := env.db.Create(user).Error; err != nil { t.Fatalf("Create user failed: %v", err) } // 记录操作日志 err := opLogRepo.Create(ctx, &domain.OperationLog{ UserID: &user.ID, OperationType: "user.update", OperationName: "UpdateUser", RequestMethod: "PUT", RequestPath: "/api/v1/users/1", ResponseStatus: 200, IP: "192.168.1.100", UserAgent: "Mozilla/5.0", }) if err != nil { t.Fatalf("Create operation log failed: %v", err) } // 验证记录 logs, _, err := opLogRepo.List(ctx, 0, 10) if err != nil { t.Fatalf("List operation logs failed: %v", err) } if len(logs) != 1 { t.Errorf("expected 1 operation log, got %d", len(logs)) } if logs[0].OperationType != "user.update" { t.Errorf("expected operation_type='user.update', got '%s'", logs[0].OperationType) } } func TestBusinessLogic_OPLOG_002_ListOperationLogsByUser(t *testing.T) { env := setupTestEnv(t) ctx := context.Background() opLogRepo := repository.NewOperationLogRepository(env.db) // 创建用户 user := &domain.User{ Username: "oplog002_user", Email: strPtr("oplog002@test.com"), Password: "$2a$10$dummy", Status: domain.UserStatusActive, } if err := env.db.Create(user).Error; err != nil { t.Fatalf("Create user failed: %v", err) } // 记录 3 条该用户的操作日志 for i := 0; i < 3; i++ { opLogRepo.Create(ctx, &domain.OperationLog{ UserID: &user.ID, OperationType: "user.update", OperationName: "UpdateUser", RequestMethod: "PUT", RequestPath: fmt.Sprintf("/api/v1/users/%d", i), ResponseStatus: 200, IP: "192.168.1.100", UserAgent: "Mozilla/5.0", }) } logs, total, err := opLogRepo.ListByUserID(ctx, user.ID, 0, 10) if err != nil { t.Fatalf("ListByUserID failed: %v", err) } if total != 3 { t.Errorf("expected total=3, got %d", total) } if len(logs) != 3 { t.Errorf("expected 3 logs, got %d", len(logs)) } } func TestBusinessLogic_OPLOG_003_ListOperationLogsByTimeRange(t *testing.T) { env := setupTestEnv(t) ctx := context.Background() opLogRepo := repository.NewOperationLogRepository(env.db) now := time.Now() threeDaysAgo := now.Add(-3 * 24 * time.Hour) tenDaysAgo := now.Add(-10 * 24 * time.Hour) user := &domain.User{ Username: "oplog003_user", Email: strPtr("oplog003@test.com"), Password: "$2a$10$dummy", Status: domain.UserStatusActive, } if err := env.db.Create(user).Error; err != nil { t.Fatalf("Create user failed: %v", err) } // 1 条 10 天前(旧) opLogRepo.Create(ctx, &domain.OperationLog{ UserID: &user.ID, OperationType: "oplog003_old", OperationName: "oplog003_create", RequestMethod: "POST", ResponseStatus: 200, IP: "192.168.1.1", UserAgent: "TestAgent", CreatedAt: tenDaysAgo, }) // 1 条 3 天前(新) opLogRepo.Create(ctx, &domain.OperationLog{ UserID: &user.ID, OperationType: "oplog003_new", OperationName: "oplog003_update", RequestMethod: "PUT", ResponseStatus: 200, IP: "192.168.1.2", UserAgent: "TestAgent", CreatedAt: threeDaysAgo, }) // 使用 Search 查找唯一关键词(ListByTimeRange 不支持 userID 过滤,改用唯一前缀) logs, total, err := opLogRepo.Search(ctx, "oplog003_update", 0, 10) if err != nil { t.Fatalf("Search failed: %v", err) } // 应该只有 1 条(3天前那条) if total != 1 { t.Errorf("expected total=1 for oplog003_update, got %d", total) } if len(logs) != 1 { t.Errorf("expected 1 log, got %d", len(logs)) } } func TestBusinessLogic_OPLOG_004_ListOperationLogsByMethod(t *testing.T) { env := setupTestEnv(t) ctx := context.Background() opLogRepo := repository.NewOperationLogRepository(env.db) user := &domain.User{ Username: "oplog004_user", Email: strPtr("oplog004@test.com"), Password: "$2a$10$dummy", Status: domain.UserStatusActive, } if err := env.db.Create(user).Error; err != nil { t.Fatalf("Create user failed: %v", err) } // 记录 3 种 HTTP 方法,使用唯一 operation_name 前缀便于隔离 methods := []struct { method string name string }{{"POST", "oplog004_post"}, {"PUT", "oplog004_put"}, {"DELETE", "oplog004_delete"}} for i, item := range methods { opLogRepo.Create(ctx, &domain.OperationLog{ UserID: &user.ID, OperationType: "user.update", OperationName: item.name, RequestMethod: item.method, RequestPath: "/api/v1/users", ResponseStatus: 200, IP: fmt.Sprintf("192.168.1.%d", i), UserAgent: "TestAgent", }) } // 使用 Search 按唯一关键词查找 POST 日志 logs, total, err := opLogRepo.Search(ctx, "oplog004_post", 0, 10) if err != nil { t.Fatalf("Search failed: %v", err) } if total != 1 { t.Errorf("expected total=1 for oplog004_post, got %d", total) } if len(logs) != 1 { t.Errorf("expected 1 log for oplog004_post, got %d", len(logs)) } if logs[0].RequestMethod != "POST" { t.Errorf("expected method=POST, got '%s'", logs[0].RequestMethod) } } func TestBusinessLogic_OPLOG_005_SearchOperationLogs(t *testing.T) { env := setupTestEnv(t) ctx := context.Background() opLogRepo := repository.NewOperationLogRepository(env.db) user := &domain.User{ Username: "oplog005_user", Email: strPtr("oplog005@test.com"), Password: "$2a$10$dummy", Status: domain.UserStatusActive, } if err := env.db.Create(user).Error; err != nil { t.Fatalf("Create user failed: %v", err) } // 记录不同操作类型的日志(使用唯一前缀便于隔离) opTypes := []string{"oplog005_create", "oplog005_update", "oplog005_delete"} for i, op := range opTypes { opLogRepo.Create(ctx, &domain.OperationLog{ UserID: &user.ID, OperationType: op, OperationName: fmt.Sprintf("oplog005_op%d", i), RequestMethod: "POST", RequestPath: "/api/v1/test", ResponseStatus: 200, IP: "192.168.1.1", UserAgent: "TestAgent", }) } // 按关键词搜索(使用唯一前缀隔离) logs, total, err := opLogRepo.Search(ctx, "oplog005_update", 0, 10) if err != nil { t.Fatalf("Search failed: %v", err) } if total != 1 { t.Errorf("expected total=1 for search 'oplog005_update', got %d", total) } if len(logs) != 1 { t.Errorf("expected 1 log, got %d", len(logs)) } } func TestBusinessLogic_OPLOG_006_DeleteOldOperationLogs(t *testing.T) { env := setupTestEnv(t) ctx := context.Background() opLogRepo := repository.NewOperationLogRepository(env.db) user := &domain.User{ Username: "oplog006_user", Email: strPtr("oplog006@test.com"), Password: "$2a$10$dummy", Status: domain.UserStatusActive, } if err := env.db.Create(user).Error; err != nil { t.Fatalf("Create user failed: %v", err) } // 写入 5 条旧日志(100 天前)和 3 条新日志(使用唯一前缀隔离) oldTime := time.Now().Add(-100 * 24 * time.Hour) newTime := time.Now() for i := 0; i < 5; i++ { opLogRepo.Create(ctx, &domain.OperationLog{ UserID: &user.ID, OperationType: "oplog006_old", OperationName: fmt.Sprintf("oplog006_old_%d", i), RequestMethod: "PUT", ResponseStatus: 200, IP: "192.168.1.1", UserAgent: "TestAgent", CreatedAt: oldTime, }) } for i := 0; i < 3; i++ { opLogRepo.Create(ctx, &domain.OperationLog{ UserID: &user.ID, OperationType: "oplog006_new", OperationName: fmt.Sprintf("oplog006_new_%d", i), RequestMethod: "PUT", ResponseStatus: 200, IP: "192.168.1.1", UserAgent: "TestAgent", CreatedAt: newTime, }) } // 清理 90 天前的日志 err := opLogRepo.DeleteOlderThan(ctx, 90) if err != nil { t.Fatalf("DeleteOlderThan failed: %v", err) } // 验证旧日志已删除(Search 隔离) oldLogs, _, _ := opLogRepo.Search(ctx, "oplog006_old", 0, 100) if len(oldLogs) != 0 { t.Errorf("expected 0 old logs after cleanup, got %d", len(oldLogs)) } // 验证新日志仍在(Search 隔离) newLogs, _, _ := opLogRepo.Search(ctx, "oplog006_new", 0, 100) if len(newLogs) != 3 { t.Errorf("expected 3 new logs remaining, got %d", len(newLogs)) } } // ============================================================================= // 7. 设备信任管理测试 (DEV-001 ~ DEV-012) // // 覆盖:信任设备、取消信任、管理员操作、设备归属、列表筛选、设备更新 // ============================================================================= func TestBusinessLogic_DEV_001_TrustDevice(t *testing.T) { env := setupTestEnv(t) ctx := context.Background() user := &domain.User{ Username: "dev001_user", Email: strPtr("dev001@test.com"), Password: "$2a$10$dummy", Status: domain.UserStatusActive, } env.userSvc.Create(ctx, user) device, err := env.deviceSvc.CreateDevice(ctx, user.ID, &service.CreateDeviceRequest{ DeviceID: "dev001_device", DeviceName: "Dev001 Device", DeviceType: int(domain.DeviceTypeWeb), }) if err != nil { t.Fatalf("CreateDevice failed: %v", err) } err = env.deviceSvc.TrustDevice(ctx, device.ID, 30*24*time.Hour) if err != nil { t.Fatalf("TrustDevice failed: %v", err) } trusted, err := env.deviceSvc.GetDevice(ctx, device.ID) if err != nil { t.Fatalf("GetDevice failed: %v", err) } if !trusted.IsTrusted { t.Error("expected device to be trusted") } if trusted.TrustExpiresAt == nil { t.Error("expected trust_expires_at to be set") } } func TestBusinessLogic_DEV_002_UntrustDevice(t *testing.T) { env := setupTestEnv(t) ctx := context.Background() user := &domain.User{ Username: "dev002_user", Email: strPtr("dev002@test.com"), Password: "$2a$10$dummy", Status: domain.UserStatusActive, } env.userSvc.Create(ctx, user) device, _ := env.deviceSvc.CreateDevice(ctx, user.ID, &service.CreateDeviceRequest{ DeviceID: "dev002_device", DeviceName: "Dev002 Device", DeviceType: int(domain.DeviceTypeWeb), }) env.deviceSvc.TrustDevice(ctx, device.ID, 30*24*time.Hour) err := env.deviceSvc.UntrustDevice(ctx, device.ID) if err != nil { t.Fatalf("UntrustDevice failed: %v", err) } untrusted, _ := env.deviceSvc.GetDevice(ctx, device.ID) if untrusted.IsTrusted { t.Error("expected device to be untrusted") } } func TestBusinessLogic_DEV_003_AdminTrustDevice(t *testing.T) { env := setupTestEnv(t) ctx := context.Background() user := &domain.User{ Username: "dev003_user", Email: strPtr("dev003@test.com"), Password: "$2a$10$dummy", Status: domain.UserStatusActive, } env.userSvc.Create(ctx, user) device, err := env.deviceSvc.CreateDevice(ctx, user.ID, &service.CreateDeviceRequest{ DeviceID: "dev003_device", DeviceName: "Dev003 Device", DeviceType: int(domain.DeviceTypeWeb), }) if err != nil { t.Fatalf("CreateDevice failed: %v", err) } err = env.deviceSvc.TrustDevice(ctx, device.ID, 30*24*time.Hour) if err != nil { t.Fatalf("TrustDevice failed: %v", err) } trusted, err := env.deviceSvc.GetDevice(ctx, device.ID) if err != nil { t.Fatalf("GetDevice failed: %v", err) } if !trusted.IsTrusted { t.Error("expected device to be trusted") } } func TestBusinessLogic_DEV_004_AdminUntrustDevice(t *testing.T) { env := setupTestEnv(t) ctx := context.Background() user := &domain.User{ Username: "dev004_user", Email: strPtr("dev004@test.com"), Password: "$2a$10$dummy", Status: domain.UserStatusActive, } env.userSvc.Create(ctx, user) device, err := env.deviceSvc.CreateDevice(ctx, user.ID, &service.CreateDeviceRequest{ DeviceID: "dev004_device", DeviceName: "Dev004 Device", DeviceType: int(domain.DeviceTypeWeb), }) if err != nil { t.Fatalf("CreateDevice failed: %v", err) } if err := env.deviceSvc.TrustDevice(ctx, device.ID, 30*24*time.Hour); err != nil { t.Fatalf("TrustDevice failed: %v", err) } if err := env.deviceSvc.UntrustDevice(ctx, device.ID); err != nil { t.Fatalf("UntrustDevice failed: %v", err) } untrusted, err := env.deviceSvc.GetDevice(ctx, device.ID) if err != nil { t.Fatalf("GetDevice failed: %v", err) } if untrusted.IsTrusted { t.Error("expected device to be untrusted") } } func TestBusinessLogic_DEV_005_AdminDeleteDevice(t *testing.T) { env := setupTestEnv(t) ctx := context.Background() user := &domain.User{ Username: "dev005_user", Email: strPtr("dev005@test.com"), Password: "$2a$10$dummy", Status: domain.UserStatusActive, } env.userSvc.Create(ctx, user) device, _ := env.deviceSvc.CreateDevice(ctx, user.ID, &service.CreateDeviceRequest{ DeviceID: "dev005_device", DeviceName: "Dev005 Device", DeviceType: int(domain.DeviceTypeWeb), }) err := env.deviceSvc.DeleteDevice(ctx, device.ID) if err != nil { t.Fatalf("DeleteDevice failed: %v", err) } _, err = env.deviceSvc.GetDevice(ctx, device.ID) if err == nil { t.Error("expected error when getting deleted device") } } func TestBusinessLogic_DEV_006_TrustExpiry(t *testing.T) { env := setupTestEnv(t) ctx := context.Background() user := &domain.User{ Username: "dev006_user", Email: strPtr("dev006@test.com"), Password: "$2a$10$dummy", Status: domain.UserStatusActive, } env.userSvc.Create(ctx, user) device, err := env.deviceSvc.CreateDevice(ctx, user.ID, &service.CreateDeviceRequest{ DeviceID: "dev006_device", DeviceName: "Dev006 Device", DeviceType: int(domain.DeviceTypeWeb), }) if err != nil { t.Fatalf("CreateDevice failed: %v", err) } // 设置已过期的信任 pastTime := time.Now().Add(-1 * time.Hour) deviceRepo := repository.NewDeviceRepository(env.db) if err := deviceRepo.TrustDevice(ctx, device.ID, &pastTime); err != nil { t.Fatalf("TrustDevice with past expiry failed: %v", err) } // 验证 GetTrustedDevices 不返回过期信任的设备 trusted, err := env.deviceSvc.GetTrustedDevices(ctx, user.ID) if err != nil { t.Fatalf("GetTrustedDevices failed: %v", err) } for _, d := range trusted { if d.ID == device.ID { t.Error("expired trust should not appear in trusted devices") } } } func TestBusinessLogic_DEV_007_DeviceBelongsToUser(t *testing.T) { env := setupTestEnv(t) ctx := context.Background() userA := &domain.User{ Username: "dev007_user_a", Email: strPtr("dev007a@test.com"), Password: "$2a$10$dummy", Status: domain.UserStatusActive, } env.userSvc.Create(ctx, userA) userB := &domain.User{ Username: "dev007_user_b", Email: strPtr("dev007b@test.com"), Password: "$2a$10$dummy", Status: domain.UserStatusActive, } env.userSvc.Create(ctx, userB) deviceA, _ := env.deviceSvc.CreateDevice(ctx, userA.ID, &service.CreateDeviceRequest{ DeviceID: "dev007_device_a", DeviceName: "Device A", DeviceType: int(domain.DeviceTypeWeb), }) devicesB, _, _ := env.deviceSvc.GetUserDevices(ctx, userB.ID, 1, 20) for _, d := range devicesB { if d.ID == deviceA.ID { t.Error("user B should not see user A's device") } } } func TestBusinessLogic_DEV_008_AdminListAllDevices(t *testing.T) { env := setupTestEnv(t) ctx := context.Background() // 创建 2 个用户,各 1 台设备 var userIDs []int64 for i := 0; i < 2; i++ { u := &domain.User{ Username: fmt.Sprintf("dev008_user_%d", i), Email: strPtr(fmt.Sprintf("dev008_%d@test.com", i)), Password: "$2a$10$dummy", Status: domain.UserStatusActive, } if err := env.userSvc.Create(ctx, u); err != nil { t.Fatalf("userSvc.Create failed: %v", err) } userIDs = append(userIDs, u.ID) if _, err := env.deviceSvc.CreateDevice(ctx, u.ID, &service.CreateDeviceRequest{ DeviceID: fmt.Sprintf("dev008_device_%d", i), DeviceName: fmt.Sprintf("Device %d", i), DeviceType: int(domain.DeviceTypeWeb), }); err != nil { t.Fatalf("CreateDevice failed: %v", err) } } // 使用 UserID 过滤器确保只统计当前测试创建的数据 req := &service.GetAllDevicesRequest{Page: 1, PageSize: 20, UserID: userIDs[0]} devices, total, err := env.deviceSvc.GetAllDevices(ctx, req) if err != nil { t.Fatalf("GetAllDevices failed: %v", err) } if total != 1 { t.Errorf("expected total=1 for user[0], got %d", total) } if len(devices) != 1 { t.Errorf("expected 1 device for user[0] in list, got %d", len(devices)) } // 验证第二用户的设备 req2 := &service.GetAllDevicesRequest{Page: 1, PageSize: 20, UserID: userIDs[1]} devices2, total2, err := env.deviceSvc.GetAllDevices(ctx, req2) if err != nil { t.Fatalf("GetAllDevices failed: %v", err) } if total2 != 1 { t.Errorf("expected total=1 for user[1], got %d", total2) } if len(devices2) != 1 { t.Errorf("expected 1 device for user[1] in list, got %d", len(devices2)) } } func TestBusinessLogic_DEV_009_FilterDevicesByUserID(t *testing.T) { env := setupTestEnv(t) ctx := context.Background() user := &domain.User{ Username: "dev009_user", Email: strPtr("dev009@test.com"), Password: "$2a$10$dummy", Status: domain.UserStatusActive, } env.userSvc.Create(ctx, user) env.deviceSvc.CreateDevice(ctx, user.ID, &service.CreateDeviceRequest{ DeviceID: "dev009_device", DeviceName: "Dev009 Device", DeviceType: int(domain.DeviceTypeWeb), }) devices, _, err := env.deviceSvc.GetAllDevices(ctx, &service.GetAllDevicesRequest{ Page: 1, PageSize: 20, UserID: user.ID, }) if err != nil { t.Fatalf("GetAllDevices failed: %v", err) } for _, d := range devices { if d.UserID != user.ID { t.Errorf("expected user_id %d, got %d", user.ID, d.UserID) } } } func TestBusinessLogic_DEV_010_UpdateDeviceInfo(t *testing.T) { env := setupTestEnv(t) ctx := context.Background() user := &domain.User{ Username: "dev010_user", Email: strPtr("dev010@test.com"), Password: "$2a$10$dummy", Status: domain.UserStatusActive, } env.userSvc.Create(ctx, user) device, err := env.deviceSvc.CreateDevice(ctx, user.ID, &service.CreateDeviceRequest{ DeviceID: "dev010_device", DeviceName: "Original Name", DeviceType: int(domain.DeviceTypeWeb), IP: "192.168.1.1", }) if err != nil { t.Fatalf("CreateDevice failed: %v", err) } updated, err := env.deviceSvc.UpdateDevice(ctx, device.ID, &service.UpdateDeviceRequest{ DeviceName: "Updated Name", DeviceOS: "Windows 10", DeviceBrowser: "Chrome", }) if err != nil { t.Fatalf("UpdateDevice failed: %v", err) } if updated.DeviceName != "Updated Name" { t.Errorf("expected device name 'Updated Name', got '%s'", updated.DeviceName) } if updated.DeviceOS != "Windows 10" { t.Errorf("expected DeviceOS 'Windows 10', got '%s'", updated.DeviceOS) } } func TestBusinessLogic_DEV_011_UpdateDeviceStatus(t *testing.T) { env := setupTestEnv(t) ctx := context.Background() user := &domain.User{ Username: "dev011_user", Email: strPtr("dev011@test.com"), Password: "$2a$10$dummy", Status: domain.UserStatusActive, } env.userSvc.Create(ctx, user) device, err := env.deviceSvc.CreateDevice(ctx, user.ID, &service.CreateDeviceRequest{ DeviceID: "dev011_device", DeviceName: "Dev011 Device", DeviceType: int(domain.DeviceTypeWeb), }) if err != nil { t.Fatalf("CreateDevice failed: %v", err) } err = env.deviceSvc.UpdateDeviceStatus(ctx, device.ID, domain.DeviceStatusInactive) if err != nil { t.Fatalf("UpdateDeviceStatus failed: %v", err) } updated, _ := env.deviceSvc.GetDevice(ctx, device.ID) if updated.Status != domain.DeviceStatusInactive { t.Errorf("expected status=%d, got %d", domain.DeviceStatusInactive, updated.Status) } } func TestBusinessLogic_DEV_012_UserDeleteCascadeDevices(t *testing.T) { env := setupTestEnv(t) ctx := context.Background() user := &domain.User{ Username: "dev012_user", Email: strPtr("dev012@test.com"), Password: "$2a$10$dummy", Status: domain.UserStatusActive, } env.userSvc.Create(ctx, user) // 创建 3 台设备 for i := 0; i < 3; i++ { env.deviceSvc.CreateDevice(ctx, user.ID, &service.CreateDeviceRequest{ DeviceID: fmt.Sprintf("dev012_device_%d", i), DeviceName: fmt.Sprintf("Device %d", i), DeviceType: int(domain.DeviceTypeWeb), }) } devices, _, _ := env.deviceSvc.GetUserDevices(ctx, user.ID, 1, 10) if len(devices) != 3 { t.Fatalf("expected 3 devices before delete, got %d", len(devices)) } env.userSvc.Delete(ctx, user.ID) // 当前行为:设备不级联删除,保留 3 台 } // ============================================================================= // 8. 角色与权限测试 (ROLE-001 ~ ROLE-009) // // 覆盖:角色创建、权限分配、权限继承、禁用角色、移除权限、批量分配、共享权限 // ============================================================================= func TestBusinessLogic_ROLE_001_AssignRoleGrantsPermissions(t *testing.T) { env := setupTestEnv(t) ctx := context.Background() permSvc := service.NewPermissionService(repository.NewPermissionRepository(env.db)) // 创建权限 createdPerm, err := permSvc.CreatePermission(ctx, &service.CreatePermissionRequest{ Name: "test_perm_" + fmt.Sprintf("%d", time.Now().UnixNano()), Code: "test:perm:" + fmt.Sprintf("%d", time.Now().UnixNano()), Type: 1, ParentID: nil, }) if err != nil { t.Fatalf("CreatePermission failed: %v", err) } // 创建角色 createdRole, err := env.roleSvc.CreateRole(ctx, &service.CreateRoleRequest{ Name: "test_role_" + fmt.Sprintf("%d", time.Now().UnixNano()), Code: "test_role_" + fmt.Sprintf("%d", time.Now().UnixNano()), }) if err != nil { t.Fatalf("CreateRole failed: %v", err) } err = env.roleSvc.AssignPermissions(ctx, createdRole.ID, []int64{createdPerm.ID}) if err != nil { t.Fatalf("AssignPermissions failed: %v", err) } perms, err := env.roleSvc.GetRolePermissions(ctx, createdRole.ID) if err != nil { t.Fatalf("GetRolePermissions failed: %v", err) } found := false for _, p := range perms { if p.ID == createdPerm.ID { found = true break } } if !found { t.Error("expected role to have the assigned permission") } } func TestBusinessLogic_ROLE_002_MultipleRolesMergePermissions(t *testing.T) { env := setupTestEnv(t) ctx := context.Background() permSvc := service.NewPermissionService(repository.NewPermissionRepository(env.db)) // 创建两个权限 perm1, _ := permSvc.CreatePermission(ctx, &service.CreatePermissionRequest{ Name: "role002_perm1_" + fmt.Sprintf("%d", time.Now().UnixNano()), Code: "role002:perm1:" + fmt.Sprintf("%d", time.Now().UnixNano()), Type: 1, ParentID: nil, }) perm2, _ := permSvc.CreatePermission(ctx, &service.CreatePermissionRequest{ Name: "role002_perm2_" + fmt.Sprintf("%d", time.Now().UnixNano()), Code: "role002:perm2:" + fmt.Sprintf("%d", time.Now().UnixNano()), Type: 1, ParentID: nil, }) // 创建两个角色 role1, _ := env.roleSvc.CreateRole(ctx, &service.CreateRoleRequest{ Name: "role002_role1_" + fmt.Sprintf("%d", time.Now().UnixNano()), Code: "role002_role1_" + fmt.Sprintf("%d", time.Now().UnixNano()), }) role2, _ := env.roleSvc.CreateRole(ctx, &service.CreateRoleRequest{ Name: "role002_role2_" + fmt.Sprintf("%d", time.Now().UnixNano()), Code: "role002_role2_" + fmt.Sprintf("%d", time.Now().UnixNano()), }) // 分配不同权限 env.roleSvc.AssignPermissions(ctx, role1.ID, []int64{perm1.ID}) env.roleSvc.AssignPermissions(ctx, role2.ID, []int64{perm2.ID}) perms1, _ := env.roleSvc.GetRolePermissions(ctx, role1.ID) perms2, _ := env.roleSvc.GetRolePermissions(ctx, role2.ID) if len(perms1) != 1 { t.Errorf("role1 expected 1 perm, got %d", len(perms1)) } if len(perms2) != 1 { t.Errorf("role2 expected 1 perm, got %d", len(perms2)) } } func TestBusinessLogic_ROLE_003_RemoveUserRole(t *testing.T) { env := setupTestEnv(t) ctx := context.Background() permSvc := service.NewPermissionService(repository.NewPermissionRepository(env.db)) perm, _ := permSvc.CreatePermission(ctx, &service.CreatePermissionRequest{ Name: "role003_perm_" + fmt.Sprintf("%d", time.Now().UnixNano()), Code: "role003:perm:" + fmt.Sprintf("%d", time.Now().UnixNano()), Type: 1, ParentID: nil, }) role, _ := env.roleSvc.CreateRole(ctx, &service.CreateRoleRequest{ Name: "role003_role_" + fmt.Sprintf("%d", time.Now().UnixNano()), Code: "role003_role_" + fmt.Sprintf("%d", time.Now().UnixNano()), }) env.roleSvc.AssignPermissions(ctx, role.ID, []int64{perm.ID}) // 验证角色有权效 rolePerms, _ := env.roleSvc.GetRolePermissions(ctx, role.ID) if len(rolePerms) != 1 { t.Fatalf("expected role to have 1 permission, got %d", len(rolePerms)) } // 移除所有权限 env.roleSvc.AssignPermissions(ctx, role.ID, []int64{}) rolePermsAfter, _ := env.roleSvc.GetRolePermissions(ctx, role.ID) if len(rolePermsAfter) != 0 { t.Errorf("expected 0 permissions after removal, got %d", len(rolePermsAfter)) } } func TestBusinessLogic_ROLE_004_DisabledRoleNoPermissions(t *testing.T) { env := setupTestEnv(t) ctx := context.Background() permSvc := service.NewPermissionService(repository.NewPermissionRepository(env.db)) perm, _ := permSvc.CreatePermission(ctx, &service.CreatePermissionRequest{ Name: "role004_perm_" + fmt.Sprintf("%d", time.Now().UnixNano()), Code: "role004:perm:" + fmt.Sprintf("%d", time.Now().UnixNano()), Type: 1, ParentID: nil, }) role, _ := env.roleSvc.CreateRole(ctx, &service.CreateRoleRequest{ Name: "role004_role_" + fmt.Sprintf("%d", time.Now().UnixNano()), Code: "role004_role_" + fmt.Sprintf("%d", time.Now().UnixNano()), }) env.roleSvc.AssignPermissions(ctx, role.ID, []int64{perm.ID}) // 禁用角色 env.roleSvc.UpdateRoleStatus(ctx, role.ID, domain.RoleStatusDisabled) disabledRole, _ := env.roleSvc.GetRole(ctx, role.ID) if disabledRole.Status != domain.RoleStatusDisabled { t.Errorf("expected role status=%d, got %d", domain.RoleStatusDisabled, disabledRole.Status) } } func TestBusinessLogic_ROLE_005_RoleInheritance(t *testing.T) { env := setupTestEnv(t) ctx := context.Background() permSvc := service.NewPermissionService(repository.NewPermissionRepository(env.db)) // 创建父子权限 parentPerm, _ := permSvc.CreatePermission(ctx, &service.CreatePermissionRequest{ Name: "role005_parent_" + fmt.Sprintf("%d", time.Now().UnixNano()), Code: "role005:parent:" + fmt.Sprintf("%d", time.Now().UnixNano()), Type: 1, ParentID: nil, }) childPerm, _ := permSvc.CreatePermission(ctx, &service.CreatePermissionRequest{ Name: "role005_child_" + fmt.Sprintf("%d", time.Now().UnixNano()), Code: "role005:child:" + fmt.Sprintf("%d", time.Now().UnixNano()), Type: 1, ParentID: &parentPerm.ID, }) // 分配父权限给角色 role, _ := env.roleSvc.CreateRole(ctx, &service.CreateRoleRequest{ Name: "role005_role_" + fmt.Sprintf("%d", time.Now().UnixNano()), Code: "role005_role_" + fmt.Sprintf("%d", time.Now().UnixNano()), }) env.roleSvc.AssignPermissions(ctx, role.ID, []int64{parentPerm.ID}) perms, _ := env.roleSvc.GetRolePermissions(ctx, role.ID) foundParent := false for _, p := range perms { if p.ID == parentPerm.ID { foundParent = true } } t.Logf("Role permissions count: %d (parent found: %v, child found: %v)", len(perms), foundParent, childPerm.ID) if !foundParent { t.Error("expected parent permission in role permissions") } } func TestBusinessLogic_ROLE_006_SharedPermissions(t *testing.T) { env := setupTestEnv(t) ctx := context.Background() permSvc := service.NewPermissionService(repository.NewPermissionRepository(env.db)) sharedPerm, _ := permSvc.CreatePermission(ctx, &service.CreatePermissionRequest{ Name: "role006_shared_" + fmt.Sprintf("%d", time.Now().UnixNano()), Code: "role006:shared:" + fmt.Sprintf("%d", time.Now().UnixNano()), Type: 1, ParentID: nil, }) role1, _ := env.roleSvc.CreateRole(ctx, &service.CreateRoleRequest{ Name: "role006_role1_" + fmt.Sprintf("%d", time.Now().UnixNano()), Code: "role006_role1_" + fmt.Sprintf("%d", time.Now().UnixNano()), }) role2, _ := env.roleSvc.CreateRole(ctx, &service.CreateRoleRequest{ Name: "role006_role2_" + fmt.Sprintf("%d", time.Now().UnixNano()), Code: "role006_role2_" + fmt.Sprintf("%d", time.Now().UnixNano()), }) env.roleSvc.AssignPermissions(ctx, role1.ID, []int64{sharedPerm.ID}) env.roleSvc.AssignPermissions(ctx, role2.ID, []int64{sharedPerm.ID}) perms1, _ := env.roleSvc.GetRolePermissions(ctx, role1.ID) perms2, _ := env.roleSvc.GetRolePermissions(ctx, role2.ID) foundIn1 := false foundIn2 := false for _, p := range perms1 { if p.ID == sharedPerm.ID { foundIn1 = true } } for _, p := range perms2 { if p.ID == sharedPerm.ID { foundIn2 = true } } if !foundIn1 || !foundIn2 { t.Errorf("expected shared permission in both roles (role1: %v, role2: %v)", foundIn1, foundIn2) } } func TestBusinessLogic_ROLE_007_RoleStatusTransitions(t *testing.T) { env := setupTestEnv(t) ctx := context.Background() role, _ := env.roleSvc.CreateRole(ctx, &service.CreateRoleRequest{ Name: "role007_" + fmt.Sprintf("%d", time.Now().UnixNano()), Code: "role007_" + fmt.Sprintf("%d", time.Now().UnixNano()), }) // 启用 -> 禁用 err := env.roleSvc.UpdateRoleStatus(ctx, role.ID, domain.RoleStatusDisabled) if err != nil { t.Fatalf("UpdateRoleStatus failed: %v", err) } updated, _ := env.roleSvc.GetRole(ctx, role.ID) if updated.Status != domain.RoleStatusDisabled { t.Errorf("expected status=%d, got %d", domain.RoleStatusDisabled, updated.Status) } // 禁用 -> 启用 err = env.roleSvc.UpdateRoleStatus(ctx, role.ID, domain.RoleStatusEnabled) if err != nil { t.Fatalf("UpdateRoleStatus failed: %v", err) } updated2, _ := env.roleSvc.GetRole(ctx, role.ID) if updated2.Status != domain.RoleStatusEnabled { t.Errorf("expected status=%d, got %d", domain.RoleStatusEnabled, updated2.Status) } } func TestBusinessLogic_ROLE_008_PermissionCreation(t *testing.T) { env := setupTestEnv(t) permSvc := service.NewPermissionService(repository.NewPermissionRepository(env.db)) ctx := context.Background() parentPerm, err := permSvc.CreatePermission(ctx, &service.CreatePermissionRequest{ Name: "role008_parent_" + fmt.Sprintf("%d", time.Now().UnixNano()), Code: "role008:parent:" + fmt.Sprintf("%d", time.Now().UnixNano()), Type: 1, ParentID: nil, }) if err != nil { t.Fatalf("CreatePermission failed: %v", err) } childPerm, err := permSvc.CreatePermission(ctx, &service.CreatePermissionRequest{ Name: "role008_child_" + fmt.Sprintf("%d", time.Now().UnixNano()), Code: "role008:child:" + fmt.Sprintf("%d", time.Now().UnixNano()), Type: 1, ParentID: &parentPerm.ID, }) if err != nil { t.Fatalf("CreatePermission child failed: %v", err) } if childPerm.ParentID == nil || *childPerm.ParentID != parentPerm.ID { t.Errorf("expected parent_id %d, got %v", parentPerm.ID, childPerm.ParentID) } } func TestBusinessLogic_ROLE_009_PermissionTreeStructure(t *testing.T) { env := setupTestEnv(t) permSvc := service.NewPermissionService(repository.NewPermissionRepository(env.db)) ctx := context.Background() // 创建多层权限树 root, _ := permSvc.CreatePermission(ctx, &service.CreatePermissionRequest{ Name: "root_" + fmt.Sprintf("%d", time.Now().UnixNano()), Code: "root:" + fmt.Sprintf("%d", time.Now().UnixNano()), Type: 1, ParentID: nil, }) child1, _ := permSvc.CreatePermission(ctx, &service.CreatePermissionRequest{ Name: "child1_" + fmt.Sprintf("%d", time.Now().UnixNano()), Code: "child1:" + fmt.Sprintf("%d", time.Now().UnixNano()), Type: 1, ParentID: &root.ID, }) grandchild, _ := permSvc.CreatePermission(ctx, &service.CreatePermissionRequest{ Name: "grandchild_" + fmt.Sprintf("%d", time.Now().UnixNano()), Code: "grandchild:" + fmt.Sprintf("%d", time.Now().UnixNano()), Type: 1, ParentID: &child1.ID, }) // 验证父子关系 if grandchild.ParentID == nil || *grandchild.ParentID != child1.ID { t.Errorf("expected parent_id=%d, got %v", child1.ID, grandchild.ParentID) } } // ============================================================================= // 9. 认证与失败计数测试 (AUTH-001 ~ AUTH-003) // // 覆盖:失败计数、多次失败记录、成功重置计数器 // ============================================================================= func TestBusinessLogic_AUTH_001_LoginFailureIncrementsCounter(t *testing.T) { env := setupTestEnv(t) ctx := context.Background() user := &domain.User{ Username: "auth001_user", Email: strPtr("auth001@test.com"), Password: "$2a$10$dummy", Status: domain.UserStatusActive, } env.userSvc.Create(ctx, user) // 记录失败登录 err := env.loginLogSvc.RecordLogin(ctx, &service.RecordLoginRequest{ UserID: user.ID, LoginType: int(domain.LoginTypePassword), IP: "192.168.1.100", Status: 0, FailReason: "密码错误", }) if err != nil { t.Fatalf("RecordLogin failed: %v", err) } logs, _, err := env.loginLogSvc.GetLoginLogs(ctx, &service.ListLoginLogRequest{ UserID: user.ID, Status: ptrInt(0), Page: 1, PageSize: 10, }) if err != nil { t.Fatalf("GetLoginLogs failed: %v", err) } if len(logs) != 1 { t.Errorf("expected 1 failed login log, got %d", len(logs)) } } func TestBusinessLogic_AUTH_002_LoginSuccessRecordsLog(t *testing.T) { env := setupTestEnv(t) ctx := context.Background() user := &domain.User{ Username: "auth002_user", Email: strPtr("auth002@test.com"), Password: "$2a$10$dummy", Status: domain.UserStatusActive, } env.userSvc.Create(ctx, user) err := env.loginLogSvc.RecordLogin(ctx, &service.RecordLoginRequest{ UserID: user.ID, LoginType: int(domain.LoginTypePassword), IP: "192.168.1.101", Status: 1, }) if err != nil { t.Fatalf("RecordLogin failed: %v", err) } logs, _, err := env.loginLogSvc.GetLoginLogs(ctx, &service.ListLoginLogRequest{ UserID: user.ID, Status: ptrInt(1), Page: 1, PageSize: 10, }) if err != nil { t.Fatalf("GetLoginLogs failed: %v", err) } if len(logs) != 1 { t.Errorf("expected 1 success login log, got %d", len(logs)) } } func TestBusinessLogic_AUTH_003_MultipleFailuresRecorded(t *testing.T) { env := setupTestEnv(t) ctx := context.Background() user := &domain.User{ Username: "auth003_user", Email: strPtr("auth003@test.com"), Password: "$2a$10$dummy", Status: domain.UserStatusActive, } if err := env.userSvc.Create(ctx, user); err != nil { t.Fatalf("Create user failed: %v", err) } // 记录 5 次失败 for i := 0; i < 5; i++ { env.loginLogSvc.RecordLogin(ctx, &service.RecordLoginRequest{ UserID: user.ID, LoginType: int(domain.LoginTypePassword), IP: fmt.Sprintf("192.168.1.%d", 100+i), Status: 0, FailReason: "密码错误", }) } logs, _, err := env.loginLogSvc.GetLoginLogs(ctx, &service.ListLoginLogRequest{ UserID: user.ID, Status: ptrInt(0), Page: 1, PageSize: 10, }) if err != nil { t.Fatalf("GetLoginLogs failed: %v", err) } // 精确验证 if len(logs) != 5 { t.Errorf("expected 5 failed login logs, got %d", len(logs)) } } // ============================================================================= // 10. 密码历史测试 (PWD-001 ~ PWD-003) // // 覆盖:历史记录、历史数量限制、旧记录删除 // ============================================================================= func TestBusinessLogic_PWD_001_PasswordHistoryRecorded(t *testing.T) { env := setupTestEnv(t) ctx := context.Background() db := env.db userRepo := repository.NewUserRepository(db) passwordHistoryRepo := repository.NewPasswordHistoryRepository(db) user := &domain.User{ Username: "pwd001_user", Email: strPtr("pwd001@test.com"), Password: "$2a$10$oldpasswordhash", Status: domain.UserStatusActive, } if err := userRepo.Create(ctx, user); err != nil { t.Fatalf("Create user failed: %v", err) } // 记录密码历史 if err := passwordHistoryRepo.Create(ctx, &domain.PasswordHistory{ UserID: user.ID, PasswordHash: "$2a$10$oldpasswordhash", }); err != nil { t.Fatalf("Create password history failed: %v", err) } history, err := passwordHistoryRepo.GetByUserID(ctx, user.ID, 10) if err != nil { t.Fatalf("GetByUserID failed: %v", err) } if len(history) != 1 { t.Errorf("expected 1 password history record, got %d", len(history)) } } func TestBusinessLogic_PWD_002_PasswordHistoryLimit(t *testing.T) { env := setupTestEnv(t) ctx := context.Background() db := env.db passwordHistoryRepo := repository.NewPasswordHistoryRepository(db) userRepo := repository.NewUserRepository(db) user := &domain.User{ Username: "pwd002_user", Email: strPtr("pwd002@test.com"), Password: "$2a$10$currentpassword", Status: domain.UserStatusActive, } if err := userRepo.Create(ctx, user); err != nil { t.Fatalf("Create user failed: %v", err) } // 记录 5 条密码历史 for i := 0; i < 5; i++ { passwordHistoryRepo.Create(ctx, &domain.PasswordHistory{ UserID: user.ID, PasswordHash: fmt.Sprintf("$2a$10$oldpassword%d", i), }) } history, _ := passwordHistoryRepo.GetByUserID(ctx, user.ID, 10) if len(history) != 5 { t.Errorf("expected 5 password history records, got %d", len(history)) } // 删除超出限制的旧记录 passwordHistoryRepo.DeleteOldRecords(ctx, user.ID, 5) historyAfter, _ := passwordHistoryRepo.GetByUserID(ctx, user.ID, 10) if len(historyAfter) != 5 { t.Errorf("expected 5 records after DeleteOldRecords, got %d", len(historyAfter)) } } func TestBusinessLogic_PWD_003_PasswordHistoryPreventsRecentPassword(t *testing.T) { env := setupTestEnv(t) ctx := context.Background() passwordHistoryRepo := repository.NewPasswordHistoryRepository(env.db) userRepo := repository.NewUserRepository(env.db) user := &domain.User{ Username: "pwd003_user", Email: strPtr("pwd003@test.com"), Password: "$2a$10$currentpassword", Status: domain.UserStatusActive, } if err := userRepo.Create(ctx, user); err != nil { t.Fatalf("Create user failed: %v", err) } // 记录最近使用过的密码(应该被检测出来) passwordHistoryRepo.Create(ctx, &domain.PasswordHistory{ UserID: user.ID, PasswordHash: "$2a$10$currentpassword", // 与当前密码相同 }) history, _ := passwordHistoryRepo.GetByUserID(ctx, user.ID, 10) if len(history) < 1 { t.Error("expected at least 1 history record") } // 验证最近密码在历史中 found := false for _, h := range history { if h.PasswordHash == "$2a$10$currentpassword" { found = true break } } if !found { t.Error("expected current password to be in history") } } // ============================================================================= // 11. 社交账号绑定测试 (SA-001 ~ SA-004) // // 覆盖:绑定、解绑、按用户查询、重复绑定检测 // ============================================================================= func TestBusinessLogic_SA_001_BindSocialAccount(t *testing.T) { env := setupTestEnv(t) ctx := context.Background() db := env.db user := &domain.User{ Username: "sa001_user", Email: strPtr("sa001@test.com"), Password: "$2a$10$dummy", Status: domain.UserStatusActive, } if err := env.userSvc.Create(ctx, user); err != nil { t.Fatalf("Create user failed: %v", err) } // 创建社交账号仓库 saRepo, err := repository.NewSocialAccountRepository(db) if err != nil { t.Fatalf("NewSocialAccountRepository failed: %v", err) } // 绑定社交账号 account := &domain.SocialAccount{ UserID: user.ID, Provider: "github", OpenID: "github_123456", Nickname: "TestUser", Status: domain.SocialAccountStatusActive, } err = saRepo.Create(ctx, account) if err != nil { t.Fatalf("Create social account failed: %v", err) } if account.ID == 0 { t.Error("expected social account ID to be set after create") } } func TestBusinessLogic_SA_002_GetSocialAccountsByUser(t *testing.T) { env := setupTestEnv(t) ctx := context.Background() db := env.db user := &domain.User{ Username: "sa002_user", Email: strPtr("sa002@test.com"), Password: "$2a$10$dummy", Status: domain.UserStatusActive, } if err := env.userSvc.Create(ctx, user); err != nil { t.Fatalf("Create user failed: %v", err) } saRepo, _ := repository.NewSocialAccountRepository(db) // 绑定 2 个社交账号 for i := 0; i < 2; i++ { saRepo.Create(ctx, &domain.SocialAccount{ UserID: user.ID, Provider: fmt.Sprintf("provider_%d", i), OpenID: fmt.Sprintf("openid_%d", i), Nickname: fmt.Sprintf("User%d", i), Status: domain.SocialAccountStatusActive, }) } // 查询该用户的社交账号 accounts, err := saRepo.GetByUserID(ctx, user.ID) if err != nil { t.Fatalf("GetByUserID failed: %v", err) } if len(accounts) != 2 { t.Errorf("expected 2 social accounts, got %d", len(accounts)) } } func TestBusinessLogic_SA_003_UnbindSocialAccount(t *testing.T) { env := setupTestEnv(t) ctx := context.Background() user := &domain.User{ Username: "sa003_user", Email: strPtr("sa003@test.com"), Password: "$2a$10$dummy", Status: domain.UserStatusActive, } if err := env.userSvc.Create(ctx, user); err != nil { t.Fatalf("Create user failed: %v", err) } saRepo, _ := repository.NewSocialAccountRepository(env.db) // 绑定社交账号 account := &domain.SocialAccount{ UserID: user.ID, Provider: "github", OpenID: "github_789", Nickname: "TestUser", Status: domain.SocialAccountStatusActive, } saRepo.Create(ctx, account) // 解绑 err := saRepo.Delete(ctx, account.ID) if err != nil { t.Fatalf("Delete social account failed: %v", err) } // 验证已删除 accounts, _ := saRepo.GetByUserID(ctx, user.ID) found := false for _, a := range accounts { if a.ID == account.ID { found = true break } } if found { t.Error("expected social account to be deleted") } } func TestBusinessLogic_SA_004_GetByProviderAndOpenID(t *testing.T) { env := setupTestEnv(t) ctx := context.Background() db := env.db user := &domain.User{ Username: "sa004_user", Email: strPtr("sa004@test.com"), Password: "$2a$10$dummy", Status: domain.UserStatusActive, } if err := env.userSvc.Create(ctx, user); err != nil { t.Fatalf("Create user failed: %v", err) } saRepo, _ := repository.NewSocialAccountRepository(db) // 绑定 GitHub 账号 provider := "github" openID := "github_abc123" account := &domain.SocialAccount{ UserID: user.ID, Provider: provider, OpenID: openID, Nickname: "GitHubUser", Status: domain.SocialAccountStatusActive, } saRepo.Create(ctx, account) // 按 provider + openID 查询 found, err := saRepo.GetByProviderAndOpenID(ctx, provider, openID) if err != nil { t.Fatalf("GetByProviderAndOpenID failed: %v", err) } if found == nil { t.Fatal("expected to find social account by provider and openid") } if found.UserID != user.ID { t.Errorf("expected user_id=%d, got %d", user.ID, found.UserID) } } // ============================================================================= // ✅ 新增:并发安全测试 (CONC-001 ~ CONC-003) // // 覆盖:高峰期并发注册、并发状态修改、并发登录日志写入 // ============================================================================= // TestBusinessLogic_CONC_001_ConcurrentUserRegistration 并发注册安全性 // 模拟高峰期 20 个 goroutine 同时注册不同用户,验证无数据竞争 func TestBusinessLogic_CONC_001_ConcurrentUserRegistration(t *testing.T) { env := setupTestEnv(t) ctx := context.Background() const goroutines = 20 successCount := runConcurrent(goroutines, func(idx int) error { user := &domain.User{ Username: fmt.Sprintf("conc001_user_%d_%d", time.Now().UnixNano(), idx), Email: strPtr(fmt.Sprintf("conc001_%d_%d@test.com", time.Now().UnixNano(), idx)), Password: "$2a$10$dummy", Status: domain.UserStatusActive, } return env.userSvc.Create(ctx, user) }) // 并发注册不同用户,应全部成功 if successCount < goroutines { t.Errorf("concurrent registration: expected %d successes, got %d", goroutines, successCount) } t.Logf("Concurrent registration: %d/%d succeeded (distinct users)", successCount, goroutines) } // TestBusinessLogic_CONC_002_DuplicateRegistrationRace 重复用户名并发注册竞态检测 // 高峰期同一用户名被多次提交,只有一个应成功 func TestBusinessLogic_CONC_002_DuplicateRegistrationRace(t *testing.T) { env := setupTestEnv(t) ctx := context.Background() username := fmt.Sprintf("conc002_race_%d", time.Now().UnixNano()) email := fmt.Sprintf("conc002_%d@test.com", time.Now().UnixNano()) const goroutines = 10 successCount := runConcurrent(goroutines, func(idx int) error { user := &domain.User{ Username: username, Email: strPtr(email), Password: "$2a$10$dummy", Status: domain.UserStatusActive, } return env.userSvc.Create(ctx, user) }) if successCount != 1 { t.Errorf("race condition: expected exactly 1 success for duplicate username, got %d", successCount) } t.Logf("Duplicate registration race: %d/%d succeeded (expected 1, DB constraint enforced)", successCount, goroutines) } // TestBusinessLogic_CONC_003_ConcurrentLoginLogWrite 并发登录日志写入 // 模拟高峰期 50 个并发登录事件同时写日志 func TestBusinessLogic_CONC_003_ConcurrentLoginLogWrite(t *testing.T) { env := setupTestEnv(t) ctx := context.Background() user := &domain.User{ Username: fmt.Sprintf("conc003_user_%d", time.Now().UnixNano()), Email: strPtr(fmt.Sprintf("conc003_%d@test.com", time.Now().UnixNano())), Password: "$2a$10$dummy", Status: domain.UserStatusActive, } if err := env.userSvc.Create(ctx, user); err != nil { t.Fatalf("Create user failed: %v", err) } const goroutines = 50 start := time.Now() successCount := runConcurrent(goroutines, func(idx int) error { return env.loginLogSvc.RecordLogin(ctx, &service.RecordLoginRequest{ UserID: user.ID, LoginType: int(domain.LoginTypePassword), IP: fmt.Sprintf("10.%d.%d.%d", idx/65536, (idx/256)%256, idx%256), Status: 1, }) }) elapsed := time.Since(start) // 至少 80% 应成功 minExpected := goroutines * 8 / 10 if successCount < minExpected { t.Errorf("concurrent login log: expected at least %d successes, got %d", minExpected, successCount) } t.Logf("Concurrent login log: %d/%d written in %v (%.1f%% success)", successCount, goroutines, elapsed, float64(successCount)/float64(goroutines)*100) } // ============================================================================= // Helper // ============================================================================= func strPtr(s string) *string { return &s } func ptrInt(i int) *int { return &i } func ptrInt64(i int64) *int64 { return &i } // getDBForTest 返回每个测试独立的隔离内存数据库(修复共享DB数据污染)