滑动续期、网络错误归类、API Key 激活乐观锁与生效提示
This commit is contained in:
@@ -7,6 +7,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"oci-portal/internal/model"
|
||||
@@ -523,3 +524,124 @@ func TestSessionCleanup(t *testing.T) {
|
||||
t.Errorf("after cleanup rows = %+v, want only alive", rows)
|
||||
}
|
||||
}
|
||||
|
||||
// signShortToken 用服务同款密钥手工签指定 TTL 的令牌,构造临近过期态。
|
||||
func signShortToken(t *testing.T, auth *AuthService, username string, ver uint, jti string, ttl time.Duration) string {
|
||||
t.Helper()
|
||||
claims := &authClaims{
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
Subject: username,
|
||||
ID: jti,
|
||||
IssuedAt: jwt.NewNumericDate(time.Now()),
|
||||
ExpiresAt: jwt.NewNumericDate(time.Now().Add(ttl)),
|
||||
},
|
||||
Ver: ver,
|
||||
}
|
||||
tok, err := jwt.NewWithClaims(jwt.SigningMethodHS256, claims).SignedString(auth.jwtSecret)
|
||||
if err != nil {
|
||||
t.Fatalf("sign short token: %v", err)
|
||||
}
|
||||
return tok
|
||||
}
|
||||
|
||||
func renewalFixture(t *testing.T) (*AuthService, string, TokenProof) {
|
||||
t.Helper()
|
||||
auth := newTestAuth(t)
|
||||
if err := auth.EnsureAdmin("admin", "pass123"); err != nil {
|
||||
t.Fatalf("EnsureAdmin: %v", err)
|
||||
}
|
||||
ctx := context.Background()
|
||||
loginTok, _, err := auth.Login(ctx, "admin", "pass123", "",
|
||||
SessionMeta{ClientIP: "127.0.0.1", UserAgent: "t"})
|
||||
if err != nil {
|
||||
t.Fatalf("Login: %v", err)
|
||||
}
|
||||
_, proof, err := auth.ParseTokenProof(ctx, loginTok)
|
||||
if err != nil {
|
||||
t.Fatalf("ParseTokenProof: %v", err)
|
||||
}
|
||||
return auth, loginTok, proof
|
||||
}
|
||||
|
||||
func setSessionExpiry(t *testing.T, auth *AuthService, jti string, expires time.Time) model.UserSession {
|
||||
t.Helper()
|
||||
if err := auth.db.Model(&model.UserSession{}).Where("token_id = ?", jti).
|
||||
UpdateColumn("expires_at", expires).Error; err != nil {
|
||||
t.Fatalf("set session expiry: %v", err)
|
||||
}
|
||||
var row model.UserSession
|
||||
if err := auth.db.Where("token_id = ?", jti).First(&row).Error; err != nil {
|
||||
t.Fatalf("find session row: %v", err)
|
||||
}
|
||||
return row
|
||||
}
|
||||
|
||||
func TestMaybeRenewEligibility(t *testing.T) {
|
||||
auth, loginTok, _ := renewalFixture(t)
|
||||
tests := []struct {
|
||||
name string
|
||||
token string
|
||||
}{
|
||||
{name: "剩余时间高于阈值", token: loginTok},
|
||||
{name: "非法令牌", token: "not.a.token"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if _, _, ok := auth.MaybeRenew(context.Background(), tt.token); ok {
|
||||
t.Error("MaybeRenew ok = true, want false")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMaybeRenewExtendsSession(t *testing.T) {
|
||||
auth, _, proof := renewalFixture(t)
|
||||
before := setSessionExpiry(t, auth, proof.Jti, time.Now().Add(time.Hour))
|
||||
short := signShortToken(t, auth, "admin", proof.Ver, proof.Jti, time.Hour)
|
||||
renewed, expires, ok := auth.MaybeRenew(context.Background(), short)
|
||||
if !ok {
|
||||
t.Fatal("剩余 1h 的令牌应换发")
|
||||
}
|
||||
_, newProof, err := auth.ParseTokenProof(context.Background(), renewed)
|
||||
if err != nil {
|
||||
t.Fatalf("ParseTokenProof(renewed): %v", err)
|
||||
}
|
||||
if newProof.Jti != proof.Jti || newProof.Ver != proof.Ver {
|
||||
t.Errorf("proof = %+v, want %+v", newProof, proof)
|
||||
}
|
||||
var after model.UserSession
|
||||
if err := auth.db.Where("token_id = ?", proof.Jti).First(&after).Error; err != nil {
|
||||
t.Fatalf("reload session row: %v", err)
|
||||
}
|
||||
if delta := after.ExpiresAt.Sub(before.ExpiresAt); delta < 22*time.Hour {
|
||||
t.Errorf("session expiry delta = %v, want >= 22h", delta)
|
||||
}
|
||||
if gap := after.ExpiresAt.Sub(expires); gap < -time.Second || gap > time.Second {
|
||||
t.Errorf("session expiry = %v, token expiry = %v", after.ExpiresAt, expires)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMaybeRenewedTokenFollowsSessionRevocation(t *testing.T) {
|
||||
auth, loginTok, proof := renewalFixture(t)
|
||||
short := signShortToken(t, auth, "admin", proof.Ver, proof.Jti, time.Hour)
|
||||
renewed, _, ok := auth.MaybeRenew(context.Background(), short)
|
||||
if !ok {
|
||||
t.Fatal("剩余 1h 的令牌应换发")
|
||||
}
|
||||
auth.Logout(context.Background(), loginTok)
|
||||
if _, err := auth.ParseToken(context.Background(), renewed); err == nil {
|
||||
t.Error("会话撤销后换发令牌仍有效, want 失效")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMaybeRenewUpdateFailureDoesNotIssueToken(t *testing.T) {
|
||||
auth, _, proof := renewalFixture(t)
|
||||
short := signShortToken(t, auth, "admin", proof.Ver, proof.Jti, time.Hour)
|
||||
if err := auth.db.Migrator().DropTable(&model.UserSession{}); err != nil {
|
||||
t.Fatalf("drop sessions table: %v", err)
|
||||
}
|
||||
token, expires, ok := auth.MaybeRenew(context.Background(), short)
|
||||
if ok || token != "" || !expires.IsZero() {
|
||||
t.Errorf("MaybeRenew = (%q, %v, %v), want empty result", token, expires, ok)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user