package middleware import ( "fmt" "net/http" "net/http/httptest" "strings" "testing" "time" "github.com/gin-gonic/gin" jwtlib "github.com/golang-jwt/jwt/v5" "gorm.io/driver/sqlite" "gorm.io/gorm" "kefu-cloud/server/internal/model" ) // setupRenewEnv 初始化测试用 JWT(1 小时)与内存数据库(含可用租户/账号)。 func setupRenewEnv(t *testing.T) { t.Helper() InitJWT("renew-test-secret", time.Hour) // 每个测试独立命名空间,避免共享内存库的命名冲突 dsn := fmt.Sprintf("file:kefu_renew_%d?mode=memory&cache=shared", time.Now().UnixNano()) db, err := gorm.Open(sqlite.Open(dsn), &gorm.Config{}) if err != nil { t.Fatalf("打开测试数据库失败: %v", err) } if err := model.Migrate(db); err != nil { t.Fatalf("迁移测试数据库失败: %v", err) } model.DB = db t.Cleanup(func() { model.DB = nil }) tenant := model.Tenant{Name: "续期测试租户", Status: "normal"} if err := db.Create(&tenant).Error; err != nil { t.Fatalf("创建租户失败: %v", err) } user := model.User{TenantID: tenant.ID, Role: "admin", Username: "renew_admin", PasswordHash: "x", Nickname: "续期管理员", Status: "online"} if err := db.Create(&user).Error; err != nil { t.Fatalf("创建用户失败: %v", err) } } // signToken 按指定签发时间生成令牌(jwtExpire 由 setupRenewEnv 设为 1h)。 func signToken(userID, tenantID uint, role string, issuedAt time.Time) string { claims := Claims{ UserID: userID, TenantID: tenantID, Role: role, RegisteredClaims: jwtlib.RegisteredClaims{ IssuedAt: jwtlib.NewNumericDate(issuedAt), ExpiresAt: jwtlib.NewNumericDate(issuedAt.Add(jwtExpire)), }, } token := jwtlib.NewWithClaims(jwtlib.SigningMethodHS256, claims) signed, _ := token.SignedString(jwtSecret) return signed } func newRenewRouter() *gin.Engine { gin.SetMode(gin.TestMode) r := gin.New() r.Use(AuthRequired()) r.GET("/ping", func(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"code": 0}) }) return r } func TestAuthRequiredSlidesTokenWhenHalfExpired(t *testing.T) { setupRenewEnv(t) r := newRenewRouter() // jwtExpire=1h,签发于 40 分钟前 → 已过半寿命,应滑动续期 oldToken := signToken(1, 1, "admin", time.Now().Add(-40*time.Minute)) rec := httptest.NewRecorder() req := httptest.NewRequest(http.MethodGet, "/ping", nil) req.Header.Set("Authorization", "Bearer "+oldToken) r.ServeHTTP(rec, req) if rec.Code != http.StatusOK { t.Fatalf("status=%d body=%s", rec.Code, rec.Body.String()) } renewed := rec.Header().Get("X-Renewed-Token") if renewed == "" { t.Fatal("超过半寿命的令牌应返回 X-Renewed-Token") } // 新令牌应是全新签发(IssuedAt ≈ 现在),可正常解析且未过期 claims, err := ParseToken(renewed) if err != nil { t.Fatalf("续期令牌应可解析: %v", err) } if claims.UserID != 1 || claims.Role != "admin" { t.Fatalf("续期令牌身份不符: %+v", claims) } if time.Since(claims.IssuedAt.Time) > time.Minute { t.Fatalf("续期令牌应为新签发: issuedAt=%v", claims.IssuedAt.Time) } } func TestAuthRequiredDoesNotRenewFreshToken(t *testing.T) { setupRenewEnv(t) r := newRenewRouter() freshToken := signToken(1, 1, "admin", time.Now()) rec := httptest.NewRecorder() req := httptest.NewRequest(http.MethodGet, "/ping", nil) req.Header.Set("Authorization", "Bearer "+freshToken) r.ServeHTTP(rec, req) if rec.Code != http.StatusOK { t.Fatalf("status=%d body=%s", rec.Code, rec.Body.String()) } if renewed := rec.Header().Get("X-Renewed-Token"); renewed != "" { t.Fatal("新鲜令牌不应触发续期") } } func TestParseTokenRejectsGarbage(t *testing.T) { InitJWT("parse-test-secret", time.Hour) if _, err := ParseToken("not-a-token"); err == nil { t.Fatal("无效令牌应解析失败") } if _, err := ParseToken(strings.Repeat("x", 64)); err == nil { t.Fatal("垃圾令牌应解析失败") } }