Files
gochat/internal/auth/refresh_store_test.go_BAK
T
2026-06-04 15:44:48 +08:00

352 lines
8.6 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package auth
import (
"context"
"testing"
"time"
"github.com/alicebob/miniredis/v2"
"github.com/redis/go-redis/v9"
"github.com/gochat/gochat/internal/config"
)
// setupTestStore 创建 miniredis 服务器和 RefreshTokenStore,用于测试。
// 返回 store、miniredis server 和 redis.Client,测试结束后需调用 cleanup。
func setupTestStore(t *testing.T) (*RefreshTokenStore, *miniredis.Miniredis, *redis.Client) {
t.Helper()
mr := miniredis.RunT(t)
rdb := redis.NewClient(&redis.Options{
Addr: mr.Addr(),
})
cfg := &config.JWTConfig{
Secret: "test-secret-key-min-32-chars!!",
ExpiryHours: 24,
RefreshExpiryHours: 7 * 24, // 7天
AccessExpiryMinutes: 15,
}
store := NewRefreshTokenStore(rdb, cfg)
t.Cleanup(func() {
rdb.Close()
mr.Close()
})
return store, mr, rdb
}
// ========== Store 测试 ==========
// TestRefreshTokenStore_Store 正常存储 refresh token
func TestRefreshTokenStore_Store(t *testing.T) {
store, mr, _ := setupTestStore(t)
ctx := context.Background()
userID := uint(100)
token := "refresh-token-abc123"
err := store.Store(ctx, userID, token)
if err != nil {
t.Fatalf("Store 返回意外错误: %v", err)
}
// 验证 Redis 中确实存储了正确的值
key := "gochat:refresh_token:100"
val, err := mr.Get(key)
if err != nil {
t.Fatalf("miniredis.Get 返回错误: %v", err)
}
if val != token {
t.Errorf("存储的值不匹配: got %q, want %q", val, token)
}
// 验证 TTL 设置正确(7天 = 168小时)
ttl := mr.TTL(key)
expectedTTL := time.Duration(7*24) * time.Hour
// TTL 允许一定误差(因为 miniredis 模拟时间可能有微小偏差)
if ttl < expectedTTL-time.Minute || ttl > expectedTTL+time.Minute {
t.Errorf("TTL 不在预期范围内: got %v, want 约 %v", ttl, expectedTTL)
}
}
// TestRefreshTokenStore_Store_覆盖旧token Store 同一 userID 会覆盖旧值
func TestRefreshTokenStore_Store_覆盖旧token(t *testing.T) {
store, _, _ := setupTestStore(t)
ctx := context.Background()
userID := uint(200)
// 第一次存储
err := store.Store(ctx, userID, "old-token")
if err != nil {
t.Fatalf("第一次 Store 返回意外错误: %v", err)
}
// 第二次存储(覆盖)
err = store.Store(ctx, userID, "new-token")
if err != nil {
t.Fatalf("第二次 Store 返回意外错误: %v", err)
}
// 验证 Validate 只匹配新 token
ok, err := store.Validate(ctx, userID, "new-token")
if err != nil {
t.Fatalf("Validate 返回意外错误: %v", err)
}
if !ok {
t.Error("新 token 应验证成功")
}
ok, err = store.Validate(ctx, userID, "old-token")
if err != nil {
t.Fatalf("Validate 返回意外错误: %v", err)
}
if ok {
t.Error("旧 token 应验证失败")
}
}
// ========== Validate 测试 ==========
// TestRefreshTokenStore Validate_匹配 验证匹配的 refresh token
func TestRefreshTokenStore_Validate_匹配(t *testing.T) {
store, _, _ := setupTestStore(t)
ctx := context.Background()
userID := uint(300)
token := "valid-refresh-token"
err := store.Store(ctx, userID, token)
if err != nil {
t.Fatalf("Store 返回意外错误: %v", err)
}
ok, err := store.Validate(ctx, userID, token)
if err != nil {
t.Fatalf("Validate 返回意外错误: %v", err)
}
if !ok {
t.Error("token 匹配时应返回 true")
}
}
// TestRefreshTokenStore Validate_不匹配 验证不匹配的 refresh token
func TestRefreshTokenStore_Validate_不匹配(t *testing.T) {
store, _, _ := setupTestStore(t)
ctx := context.Background()
userID := uint(400)
err := store.Store(ctx, userID, "correct-token")
if err != nil {
t.Fatalf("Store 返回意外错误: %v", err)
}
ok, err := store.Validate(ctx, userID, "wrong-token")
if err != nil {
t.Fatalf("Validate 返回意外错误: %v", err)
}
if ok {
t.Error("不匹配的 token 应返回 false")
}
}
// TestRefreshTokenStore Validate_不存在 验证不存在(已过期/已撤销)的 token
func TestRefreshTokenStore_Validate_不存在(t *testing.T) {
store, _, _ := setupTestStore(t)
ctx := context.Background()
// 没有存储任何 token,直接验证
userID := uint(500)
ok, err := store.Validate(ctx, userID, "any-token")
if err != nil {
t.Fatalf("Validate 返回意外错误: %v", err)
}
if ok {
t.Error("不存在的 token 应返回 false(而非错误)")
}
}
// ========== Revoke 测试 ==========
// TestRefreshTokenStore_Revoke 正常撤销 refresh token
func TestRefreshTokenStore_Revoke(t *testing.T) {
store, mr, _ := setupTestStore(t)
ctx := context.Background()
userID := uint(600)
token := "to-be-revoked"
err := store.Store(ctx, userID, token)
if err != nil {
t.Fatalf("Store 返回意外错误: %v", err)
}
err = store.Revoke(ctx, userID)
if err != nil {
t.Fatalf("Revoke 返回意外错误: %v", err)
}
// 验证 Redis 中 key 已被删除
key := "gochat:refresh_token:600"
exists := mr.Exists(key)
if exists {
t.Error("Revoke 后 key 应不存在")
}
// 验证 Validate 返回 false
ok, err := store.Validate(ctx, userID, token)
if err != nil {
t.Fatalf("Validate 返回意外错误: %v", err)
}
if ok {
t.Error("撤销后 Validate 应返回 false")
}
}
// TestRefreshTokenStore_Revoke_不存在key 撤销不存在的 key 不会报错
func TestRefreshTokenStore_Revoke_不存在key(t *testing.T) {
store, _, _ := setupTestStore(t)
ctx := context.Background()
userID := uint(700)
err := store.Revoke(ctx, userID)
if err != nil {
t.Fatalf("撤销不存在的 key 不应返回错误: %v", err)
}
}
// ========== Rotate 测试 ==========
// TestRefreshTokenStore_Rotate 正常旋转 token(旧 token → 新 token
func TestRefreshTokenStore_Rotate(t *testing.T) {
store, _, _ := setupTestStore(t)
ctx := context.Background()
userID := uint(800)
oldToken := "old-refresh-token"
newToken := "new-refresh-token"
// 先存储旧 token
err := store.Store(ctx, userID, oldToken)
if err != nil {
t.Fatalf("Store 返回意外错误: %v", err)
}
// 旋转:替换为新 token
err = store.Rotate(ctx, userID, newToken)
if err != nil {
t.Fatalf("Rotate 返回意外错误: %v", err)
}
// 旧 token 应不再有效
ok, err := store.Validate(ctx, userID, oldToken)
if err != nil {
t.Fatalf("Validate 返回意外错误: %v", err)
}
if ok {
t.Error("旋转后旧 token 应验证失败")
}
// 新 token 应有效
ok, err = store.Validate(ctx, userID, newToken)
if err != nil {
t.Fatalf("Validate 返回意外错误: %v", err)
}
if !ok {
t.Error("旋转后新 token 应验证成功")
}
}
// TestRefreshTokenStore_Rotate_连续旋转 多次连续旋转 token
func TestRefreshTokenStore_Rotate_连续旋转(t *testing.T) {
store, _, _ := setupTestStore(t)
ctx := context.Background()
userID := uint(900)
tokens := []string{"token-v1", "token-v2", "token-v3"}
// 存储初始 token
err := store.Store(ctx, userID, tokens[0])
if err != nil {
t.Fatalf("Store 返回意外错误: %v", err)
}
// 第一次旋转
err = store.Rotate(ctx, userID, tokens[1])
if err != nil {
t.Fatalf("第一次 Rotate 返回意外错误: %v", err)
}
// 第二次旋转
err = store.Rotate(ctx, userID, tokens[2])
if err != nil {
t.Fatalf("第二次 Rotate 返回意外错误: %v", err)
}
// 只有最新 token 有效
for i, tok := range tokens {
ok, err := store.Validate(ctx, userID, tok)
if err != nil {
t.Fatalf("Validate(token-%d) 返回意外错误: %v", i+1, err)
}
if i == 2 && !ok {
t.Error("最新 token (token-v3) 应验证成功")
}
if i < 2 && ok {
t.Errorf("旧 token (token-v%d) 应验证失败", i+1)
}
}
}
// ========== 异常路径测试 ==========
// TestRefreshTokenStore_Redis断连 Redis 连接断开时的错误处理
func TestRefreshTokenStore_Redis断连(t *testing.T) {
mr := miniredis.RunT(t)
rdb := redis.NewClient(&redis.Options{
Addr: mr.Addr(),
})
cfg := &config.JWTConfig{
Secret: "test-secret-key-min-32-chars!!",
RefreshExpiryHours: 168,
}
store := NewRefreshTokenStore(rdb, cfg)
ctx := context.Background()
// 先关闭 miniredis 模拟断连
mr.Close()
userID := uint(1000)
// Store 应返回错误
err := store.Store(ctx, userID, "some-token")
if err == nil {
t.Error("Redis 断连时 Store 应返回错误")
}
// Validate 应返回错误
_, err = store.Validate(ctx, userID, "some-token")
if err == nil {
t.Error("Redis 断连时 Validate 应返回错误")
}
// Revoke 应返回错误
err = store.Revoke(ctx, userID)
if err == nil {
t.Error("Redis 断连时 Revoke 应返回错误")
}
// Rotate 应返回错误
err = store.Rotate(ctx, userID, "new-token")
if err == nil {
t.Error("Redis 断连时 Rotate 应返回错误")
}
rdb.Close()
}