package auth import ( "backend/config" "backend/internal/dto" "backend/internal/model" "backend/utils" "path/filepath" "strings" "testing" "gorm.io/driver/sqlite" "gorm.io/gorm" "gorm.io/gorm/logger" ) // newTestService 构造基于临时 SQLite 的 AuthService(每次测试独立库,互不干扰) // Logan.Li func newTestService(t *testing.T) (*AuthService, *gorm.DB) { t.Helper() dbPath := filepath.Join(t.TempDir(), "test.sqlite3") db, err := gorm.Open(sqlite.Open(dbPath+"?_journal_mode=WAL&_busy_timeout=5000"), &gorm.Config{ Logger: logger.Default.LogMode(logger.Silent), }) if err != nil { t.Fatalf("打开测试库失败: %v", err) } if err := db.AutoMigrate(&model.User{}, &model.RefreshToken{}); err != nil { t.Fatalf("迁移测试库失败: %v", err) } user := model.User{ Username: "tester", Password: utils.HashPassword("pass1234"), Role: "admin", IsActive: true, } if err := db.Create(&user).Error; err != nil { t.Fatalf("创建测试用户失败: %v", err) } return NewAuthService(db, &config.Config{JWTSecret: "test-secret"}), db } // login 走一次真实登录,拿到令牌对 // Logan.Li func login(t *testing.T, svc *AuthService) *LoginResult { t.Helper() res, err := svc.Login(dto.LoginRequest{Username: "tester", Password: "pass1234"}, "127.0.0.1", "go-test") if err != nil { t.Fatalf("登录失败: %v", err) } if res.AccessToken == "" || res.RefreshToken == "" { t.Fatalf("登录未返回完整令牌对: %+v", res) } return res } // TestLoginIssuesPair 登录必须同时返回访问令牌与刷新令牌,且库里不存明文 // Logan.Li func TestLoginIssuesPair(t *testing.T) { svc, db := newTestService(t) res := login(t, svc) if !strings.HasPrefix(res.RefreshToken, refreshTokenPrefix) { t.Fatalf("刷新令牌前缀不符: %s", res.RefreshToken) } var row model.RefreshToken if err := db.First(&row).Error; err != nil { t.Fatalf("刷新令牌未入库: %v", err) } if row.TokenHash == res.RefreshToken || strings.Contains(row.TokenHash, res.RefreshToken) { t.Fatalf("库内不应存明文令牌: %s", row.TokenHash) } if row.TokenHash != hashRefreshToken(res.RefreshToken) { t.Fatalf("库内摘要与令牌不匹配") } if !row.ExpiresAt.After(row.CreatedAt) { t.Fatalf("到期时间应晚于创建时间") } } // TestRefreshRotates 刷新必须换出新令牌对并撤销旧令牌,旧访问令牌仍可用至过期 // Logan.Li func TestRefreshRotates(t *testing.T) { svc, db := newTestService(t) first := login(t, svc) second, err := svc.Refresh(first.RefreshToken, "127.0.0.1", "go-test") if err != nil { t.Fatalf("刷新失败: %v", err) } if second.RefreshToken == first.RefreshToken { t.Fatalf("刷新后令牌未轮换") } var old model.RefreshToken if err := db.Where("token_hash = ?", hashRefreshToken(first.RefreshToken)).First(&old).Error; err != nil { t.Fatalf("旧令牌记录丢失: %v", err) } if old.RevokedAt == nil { t.Fatalf("旧令牌应被撤销") } if old.RotatedTo != hashRefreshToken(second.RefreshToken) { t.Fatalf("轮换链未记录新令牌摘要") } // 新令牌仍可继续刷新 // Logan.Li if _, err := svc.Refresh(second.RefreshToken, "127.0.0.1", "go-test"); err != nil { t.Fatalf("新令牌刷新失败: %v", err) } } // TestReplayRevokesAll 重放已撤销的令牌 → 撤销该用户全部会话(防令牌泄漏) // Logan.Li func TestReplayRevokesAll(t *testing.T) { svc, db := newTestService(t) first := login(t, svc) second, err := svc.Refresh(first.RefreshToken, "127.0.0.1", "go-test") if err != nil { t.Fatalf("刷新失败: %v", err) } // 用已撤销的旧令牌再刷新一次 // Logan.Li if _, err := svc.Refresh(first.RefreshToken, "127.0.0.1", "go-test"); err == nil { t.Fatalf("重放旧令牌本应失败") } var active int64 db.Model(&model.RefreshToken{}).Where("revoked_at IS NULL").Count(&active) if active != 0 { t.Fatalf("重放后应撤销全部令牌,仍有 %d 条有效", active) } if _, err := svc.Refresh(second.RefreshToken, "127.0.0.1", "go-test"); err == nil { t.Fatalf("重放后新令牌也应失效") } } // TestRevokeOnLogout 登出后刷新令牌立即失效,且重复登出不报错 // Logan.Li func TestRevokeOnLogout(t *testing.T) { svc, _ := newTestService(t) res := login(t, svc) svc.Revoke(res.RefreshToken) svc.Revoke(res.RefreshToken) // 幂等 if _, err := svc.Refresh(res.RefreshToken, "127.0.0.1", "go-test"); err == nil { t.Fatalf("登出后刷新令牌仍可用") } } // TestRefreshRejectsInactive 账号被禁用后刷新失败 // Logan.Li func TestRefreshRejectsInactive(t *testing.T) { svc, db := newTestService(t) res := login(t, svc) db.Model(&model.User{}).Where("username = ?", "tester").Update("is_active", false) if _, err := svc.Refresh(res.RefreshToken, "127.0.0.1", "go-test"); err == nil { t.Fatalf("禁用账号本应刷新失败") } } // TestChangePasswordRevokesRefresh 改密后刷新令牌全部撤销,避免绕过失效基线 // Logan.Li func TestChangePasswordRevokesRefresh(t *testing.T) { svc, _ := newTestService(t) res := login(t, svc) if err := svc.ChangePassword(res.User.ID, dto.ChangePasswordRequest{ OldPassword: "pass1234", NewPassword: "pass5678", }); err != nil { t.Fatalf("改密失败: %v", err) } if _, err := svc.Refresh(res.RefreshToken, "127.0.0.1", "go-test"); err == nil { t.Fatalf("改密后刷新令牌本应失效") } }