test(auth): 刷新令牌服务单元测试(签发/轮换/重放/登出/禁用/改密)
CI / backend (push) Waiting to run

This commit is contained in:
Logan.Li committed 2026-11-03 01:17:02 +08:00
1 parent a01b89f942
commit 93c7a045e8
1 file changed
+170
@@ -0,0 +1,170 @@
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("改密后刷新令牌本应失效")
}
}