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() }