2898 lines
82 KiB
Go
2898 lines
82 KiB
Go
|
|
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数据污染)
|