This commit is contained in:
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("改密后刷新令牌本应失效")
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user