Skip to main content

sz_rust_auth_facade/
oauth.rs

1//! OAuth2 模块 — 对齐 Laravel Socialite
2//!
3//! 提供 OAuth2 客户端抽象,对齐 Laravel Socialite `ProviderInterface` 的核心 API。
4//! 支持通用 OAuth2 提供商(QQ、微信、GitHub、Google 等)。
5//!
6//! ## Laravel Socialite 对齐
7//!
8//! ### 核心 API 映射
9//!
10//! | Laravel Socialite 方法 | Rust 方法 | 说明 |
11//! |-----------------------|-----------|------|
12//! | `Socialite::driver('qq')->redirect()` | [`OAuth2Provider::redirect_url`] | 生成授权 URL |
13//! | `Socialite::driver('qq')->user()` | [`OAuth2Provider::user_from_token`] | 用授权码换取用户信息 |
14//! | `$user->getId()` | [`SocialiteUser::id`] | 第三方用户 ID |
15//! | `$user->getNickname()` | [`SocialiteUser::nickname`] | 用户昵称 |
16//! | `$user->getName()` | [`SocialiteUser::name`] | 用户姓名 |
17//! | `$user->getEmail()` | [`SocialiteUser::email`] | 邮箱 |
18//! | `$user->getAvatar()` | [`SocialiteUser::avatar`] | 头像 URL |
19//! | `$user->token` | [`SocialiteUser::access_token`] | 访问令牌 |
20//! | `$user->refreshToken` | [`SocialiteUser::refresh_token`] | 刷新令牌 |
21//! | `$user->expiresIn` | [`SocialiteUser::expires_in`] | 令牌过期时间 |
22//!
23//! ### Laravel 行为对齐
24//!
25//! - **配置驱动**:Laravel 通过 `config/services.php` 配置 `client_id`/`client_secret`/`redirect`。
26//!   Rust 通过 [`OAuth2Config`] builder 表达相同配置项。
27//! - **state 参数**:Laravel 自动生成 CSRF 防护的 state 参数。Rust 由调用方传入 state
28//!   (对齐 Laravel `Socialite::driver('qq')->withState($state)->redirect()`)。
29//! - **scopes**:Laravel 支持 `scopes()` 设置多个 scope。Rust 通过 [`OAuth2Config::with_scopes`]
30//!   或 [`OAuth2Config::with_scope`] 累加。
31//! - **额外参数**:Laravel 支持 `with()` 追加查询参数。Rust 通过
32//!   [`OAuth2Config::with_extra_param`] / [`OAuth2Config::with_extra_params`]。
33//!
34//! ## 架构说明
35//!
36//! - **OAuth2Provider trait**:对齐 Laravel `ProviderInterface`,业务方实现具体提供商逻辑
37//! - **GenericOAuth2Provider**:通用 OAuth2 实现,接收 [`OAuth2Config`] 和
38//!   [`OAuth2HttpTransport`],支持任意标准 OAuth2 提供商
39//! - **OAuth2HttpTransport trait**:HTTP 传输抽象,解耦 OAuth2 客户端与具体 HTTP 库。
40//!   与 `notify::HttpTransport` 分离,因为 OAuth2 token 交换需要返回响应体(解析 access_token)
41//! - **MemoryOAuth2HttpTransport**:内存 HTTP 传输实现,支持预置 mock 响应,用于测试
42
43use parking_lot::Mutex;
44use std::collections::VecDeque;
45use std::sync::Arc;
46use thiserror::Error;
47
48// ============================================================================
49// 错误类型
50// ============================================================================
51
52/// OAuth2 错误 — 对齐 Laravel Socialite 异常体系
53#[derive(Debug, Error)]
54pub enum OAuth2Error {
55    /// 缺少必填字段(client_id / client_secret / redirect_url / auth_url / token_url 等)
56    #[error("OAuth2 字段缺失: {0}")]
57    MissingField(String),
58    /// 授权失败(授权码为空、state 不匹配等)
59    #[error("OAuth2 授权失败: {0}")]
60    AuthFailed(String),
61    /// token 交换失败(HTTP 错误、响应缺少 access_token 等)
62    #[error("OAuth2 token 交换失败: {0}")]
63    TokenExchangeFailed(String),
64    /// 获取用户信息失败(HTTP 错误、响应解析失败等)
65    #[error("OAuth2 获取用户信息失败: {0}")]
66    UserInfoFailed(String),
67    /// HTTP 传输失败(网络错误、连接超时等)
68    #[error("OAuth2 HTTP 传输失败: {0}")]
69    HttpTransport(String),
70    /// 序列化/反序列化失败
71    #[error("OAuth2 序列化失败: {0}")]
72    Serialize(String),
73}
74
75// ============================================================================
76// OAuth2Config
77// ============================================================================
78
79/// OAuth2 配置 — 对齐 Laravel Socialite `config/services.php`
80///
81/// 通过 builder 模式构建,必填字段在 [`OAuth2Config::new`] 中提供,
82/// 可选字段通过 `with_*` 链式方法追加。
83///
84/// # PHP 对齐
85///
86/// ```php
87/// // config/services.php
88/// 'qq' => [
89///     'client_id' => env('QQ_CLIENT_ID'),
90///     'client_secret' => env('QQ_CLIENT_SECRET'),
91///     'redirect' => env('QQ_REDIRECT_URL'),
92/// ],
93/// ```
94///
95/// # Rust 用法
96///
97/// ```ignore
98/// use sz_rust_auth_facade::oauth::OAuth2Config;
99///
100/// let config = OAuth2Config::new(
101///     "100123456",
102///     "secretabc",
103///     "https://example.com/oauth/qq/callback",
104///     "https://graph.qq.com/oauth2.0/authorize",
105///     "https://graph.qq.com/oauth2.0/token",
106/// )
107/// .with_user_url("https://graph.qq.com/user/get_user_info")
108/// .with_scope("get_user_info");
109/// ```
110#[derive(Debug, Clone)]
111pub struct OAuth2Config {
112    /// 客户端 ID(必填)
113    pub client_id: String,
114    /// 客户端密钥(必填)
115    pub client_secret: String,
116    /// 回调 URL(必填)
117    pub redirect_url: String,
118    /// 授权服务器 authorize 端点(必填,如 `https://graph.qq.com/oauth2.0/authorize`)
119    pub auth_url: String,
120    /// 授权服务器 token 端点(必填,如 `https://graph.qq.com/oauth2.0/token`)
121    pub token_url: String,
122    /// 资源服务器用户信息端点(可选,如 `https://graph.qq.com/user/get_user_info`)
123    pub user_url: Option<String>,
124    /// 默认 scopes(可选)
125    pub scopes: Vec<String>,
126    /// 额外参数(可选)
127    pub extra_params: Vec<(String, String)>,
128}
129
130impl OAuth2Config {
131    /// 创建 OAuth2 配置
132    ///
133    /// # 参数
134    ///
135    /// - `client_id`: 客户端 ID
136    /// - `client_secret`: 客户端密钥
137    /// - `redirect_url`: 回调 URL
138    /// - `auth_url`: 授权服务器 authorize 端点
139    /// - `token_url`: 授权服务器 token 端点
140    #[allow(clippy::too_many_arguments)]
141    pub fn new(
142        client_id: impl Into<String>,
143        client_secret: impl Into<String>,
144        redirect_url: impl Into<String>,
145        auth_url: impl Into<String>,
146        token_url: impl Into<String>,
147    ) -> Self {
148        Self {
149            client_id: client_id.into(),
150            client_secret: client_secret.into(),
151            redirect_url: redirect_url.into(),
152            auth_url: auth_url.into(),
153            token_url: token_url.into(),
154            user_url: None,
155            scopes: Vec::new(),
156            extra_params: Vec::new(),
157        }
158    }
159
160    /// 设置用户信息端点
161    pub fn with_user_url(mut self, user_url: impl Into<String>) -> Self {
162        self.user_url = Some(user_url.into());
163        self
164    }
165
166    /// 设置 scopes 列表(覆盖原有)
167    pub fn with_scopes(mut self, scopes: Vec<String>) -> Self {
168        self.scopes = scopes;
169        self
170    }
171
172    /// 追加单个 scope
173    pub fn with_scope(mut self, scope: impl Into<String>) -> Self {
174        self.scopes.push(scope.into());
175        self
176    }
177
178    /// 设置额外参数列表(覆盖原有)
179    pub fn with_extra_params(mut self, params: Vec<(String, String)>) -> Self {
180        self.extra_params = params;
181        self
182    }
183
184    /// 追加单个额外参数
185    pub fn with_extra_param(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
186        self.extra_params.push((key.into(), value.into()));
187        self
188    }
189
190    /// 校验必填字段
191    ///
192    /// 必填字段:`client_id` / `client_secret` / `redirect_url` / `auth_url` / `token_url`。
193    /// 任一为空字符串则返回 [`OAuth2Error::MissingField`]。
194    pub fn validate(&self) -> Result<(), OAuth2Error> {
195        if self.client_id.is_empty() {
196            return Err(OAuth2Error::MissingField("client_id".into()));
197        }
198        if self.client_secret.is_empty() {
199            return Err(OAuth2Error::MissingField("client_secret".into()));
200        }
201        if self.redirect_url.is_empty() {
202            return Err(OAuth2Error::MissingField("redirect_url".into()));
203        }
204        if self.auth_url.is_empty() {
205            return Err(OAuth2Error::MissingField("auth_url".into()));
206        }
207        if self.token_url.is_empty() {
208            return Err(OAuth2Error::MissingField("token_url".into()));
209        }
210        Ok(())
211    }
212}
213
214// ============================================================================
215// SocialiteUser
216// ============================================================================
217
218/// OAuth2 用户信息 — 对齐 Laravel Socialite `User`
219///
220/// 表示从 OAuth2 提供商获取的用户信息,包含标准字段和原始响应数据。
221///
222/// # PHP 对齐
223///
224/// ```php
225/// $user = Socialite::driver('qq')->user();
226/// $user->getId();        // 第三方用户 ID
227/// $user->getNickname();  // 昵称
228/// $user->getName();      // 姓名
229/// $user->getEmail();     // 邮箱
230/// $user->getAvatar();    // 头像
231/// $user->token;          // access_token
232/// $user->refreshToken;   // refresh_token
233/// $user->expiresIn;      // 过期秒数
234/// $user->user;           // 原始响应(raw)
235/// ```
236#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
237pub struct SocialiteUser {
238    /// 第三方平台用户 ID
239    pub id: String,
240    /// 用户昵称
241    pub nickname: Option<String>,
242    /// 用户姓名
243    pub name: Option<String>,
244    /// 邮箱
245    pub email: Option<String>,
246    /// 头像 URL
247    pub avatar: Option<String>,
248    /// 原始响应数据(JSON)
249    pub raw: serde_json::Value,
250    /// 访问令牌
251    #[serde(skip_serializing)]
252    pub access_token: Option<String>,
253    /// 刷新令牌
254    #[serde(skip_serializing)]
255    pub refresh_token: Option<String>,
256    /// 令牌过期时间(Unix 秒)
257    pub expires_in: Option<i64>,
258}
259
260// ============================================================================
261// OAuth2Provider trait
262// ============================================================================
263
264/// OAuth2 提供商 trait — 对齐 Laravel Socialite `ProviderInterface`
265///
266/// 业务方实现此 trait 以对接具体 OAuth2 提供商(QQ / 微信 / GitHub / Google 等)。
267/// 框架内置 [`GenericOAuth2Provider`] 通用实现,满足标准 OAuth2 协议的提供商可直接使用。
268///
269/// # 线程安全
270///
271/// 实现者必须保证 `Send + Sync`,因为 Provider 通常作为单例在多线程下使用。
272pub trait OAuth2Provider: Send + Sync {
273    /// 生成授权 URL(对齐 `Socialite::driver('qq')->redirect()`)
274    ///
275    /// # 参数
276    ///
277    /// - `state`: CSRF 防护的 state 参数(由调用方生成并暂存到 session)
278    ///
279    /// # 返回
280    ///
281    /// 完整的授权 URL,包含 `client_id` / `redirect_uri` / `response_type=code` /
282    /// `state` / `scope` 等查询参数。
283    fn redirect_url(&self, state: &str) -> String;
284
285    /// 用授权码换取访问令牌并获取用户信息(对齐 `Socialite::driver('qq')->user()`)
286    ///
287    /// # 参数
288    ///
289    /// - `code`: 授权服务器回调时携带的授权码
290    ///
291    /// # 返回
292    ///
293    /// 成功返回 [`SocialiteUser`],失败返回 [`OAuth2Error`]。
294    fn user_from_token(&self, code: &str) -> Result<SocialiteUser, OAuth2Error>;
295}
296
297// ============================================================================
298// OAuth2HttpTransport trait(HTTP 传输抽象)
299// ============================================================================
300
301/// OAuth2 HTTP 传输 trait — 用于解耦 OAuth2 客户端与具体 HTTP 库
302///
303/// 与 `notify::HttpTransport` 分离的原因:OAuth2 token 交换需要返回响应体
304/// (解析 `access_token`),而 `notify::HttpTransport::post_json` 仅返回 `Result<(), _>`。
305///
306/// 业务方实现此 trait 注入 reqwest / hyper / etc.,即可让 [`GenericOAuth2Provider`]
307/// 投入生产。
308///
309/// # 线程安全
310///
311/// 实现者必须保证 `Send + Sync`,因为 Provider 通常作为单例在多线程下使用。
312pub trait OAuth2HttpTransport: Send + Sync {
313    /// 发送 POST 请求,Content-Type: application/json
314    ///
315    /// # 参数
316    ///
317    /// - `url`: 目标 URL
318    /// - `body`: 请求体(JSON 字符串)
319    ///
320    /// # 返回
321    ///
322    /// 成功返回响应体字符串,失败返回 [`OAuth2Error`]。
323    fn post_json(&self, url: &str, body: &str) -> Result<String, OAuth2Error>;
324}
325
326// ============================================================================
327// MemoryOAuth2HttpTransport(测试/开发用 HTTP 传输实现)
328// ============================================================================
329
330/// 内存 HTTP 传输 — 用于测试和开发环境
331///
332/// 不实际发送 HTTP 请求,而是:
333/// - 将请求暂存到内部 Vec,供测试断言使用
334/// - 从预置响应队列中依次返回 mock 响应
335///
336/// # 用法
337///
338/// ```ignore
339/// use sz_rust_auth_facade::oauth::MemoryOAuth2HttpTransport;
340///
341/// let transport = MemoryOAuth2HttpTransport::new();
342/// // 预置 mock 响应(按调用顺序消费)
343/// transport.push_response(r#"{"access_token":"token123"}"#);
344/// transport.push_response(r#"{"id":"1","nickname":"test"}"#);
345///
346/// let resp = transport.post_json("https://example.com/token", "{}").unwrap();
347/// assert_eq!(resp, r#"{"access_token":"token123"}"#);
348/// ```
349#[derive(Debug, Default)]
350pub struct MemoryOAuth2HttpTransport {
351    /// 已"发送"的 HTTP 请求列表(url, body)
352    requests: Mutex<Vec<(String, String)>>,
353    /// 预置的 mock 响应队列(FIFO)
354    responses: Mutex<VecDeque<String>>,
355}
356
357impl MemoryOAuth2HttpTransport {
358    /// 创建新的内存 HTTP 传输
359    pub fn new() -> Self {
360        Self::default()
361    }
362
363    /// 预置 mock 响应(追加到队列末尾,按调用顺序消费)
364    pub fn push_response(&self, response: impl Into<String>) {
365        self.responses.lock().push_back(response.into());
366    }
367
368    /// 获取已发送请求数量
369    pub fn count(&self) -> usize {
370        self.requests.lock().len()
371    }
372
373    /// 获取所有已发送请求(快照)
374    pub fn all(&self) -> Vec<(String, String)> {
375        self.requests.lock().clone()
376    }
377
378    /// 获取最后发送的请求
379    pub fn last(&self) -> Option<(String, String)> {
380        self.requests.lock().last().cloned()
381    }
382
383    /// 清空已发送请求和预置响应
384    pub fn clear(&self) {
385        self.requests.lock().clear();
386        self.responses.lock().clear();
387    }
388}
389
390impl OAuth2HttpTransport for MemoryOAuth2HttpTransport {
391    fn post_json(&self, url: &str, body: &str) -> Result<String, OAuth2Error> {
392        self.requests
393            .lock()
394            .push((url.to_string(), body.to_string()));
395        let mut responses = self.responses.lock();
396        match responses.pop_front() {
397            Some(resp) => Ok(resp),
398            None => Ok(String::new()),
399        }
400    }
401}
402
403// ============================================================================
404// GenericOAuth2Provider
405// ============================================================================
406
407/// 通用 OAuth2 提供商 — 对齐 Laravel Socialite `GenericProvider`
408///
409/// 接收 [`OAuth2Config`] 和 [`OAuth2HttpTransport`],支持任意标准 OAuth2 提供商。
410///
411/// # 工作流程
412///
413/// 1. [`GenericOAuth2Provider::redirect_url`]:构造授权 URL,引导用户跳转到授权服务器
414/// 2. 授权服务器回调,携带 `code` 和 `state`
415/// 3. [`GenericOAuth2Provider::user_from_token`]:
416///    - POST `token_url` 换取 `access_token`(grant_type=authorization_code)
417///    - 如果配置了 `user_url`,POST 用户信息端点获取用户资料
418///    - 返回 [`SocialiteUser`]
419///
420/// # 用法
421///
422/// ```ignore
423/// use std::sync::Arc;
424/// use sz_rust_auth_facade::oauth::{
425///     GenericOAuth2Provider, MemoryOAuth2HttpTransport, OAuth2Config, OAuth2Provider,
426/// };
427///
428/// let config = OAuth2Config::new(
429///     "client_id",
430///     "client_secret",
431///     "https://example.com/callback",
432///     "https://provider.com/oauth2.0/authorize",
433///     "https://provider.com/oauth2.0/token",
434/// )
435/// .with_user_url("https://provider.com/user/info");
436///
437/// let transport = Arc::new(MemoryOAuth2HttpTransport::new());
438/// let provider = GenericOAuth2Provider::new(config, transport);
439///
440/// let url = provider.redirect_url("random_state");
441/// let user = provider.user_from_token("auth_code").unwrap();
442/// ```
443pub struct GenericOAuth2Provider {
444    /// OAuth2 配置
445    config: OAuth2Config,
446    /// HTTP 传输实现
447    transport: Arc<dyn OAuth2HttpTransport>,
448}
449
450impl GenericOAuth2Provider {
451    /// 创建通用 OAuth2 提供商
452    ///
453    /// # 参数
454    ///
455    /// - `config`: OAuth2 配置
456    /// - `transport`: HTTP 传输实现(业务方注入 reqwest / hyper / etc.)
457    pub fn new(config: OAuth2Config, transport: Arc<dyn OAuth2HttpTransport>) -> Self {
458        Self { config, transport }
459    }
460
461    /// 构造授权 URL
462    ///
463    /// 拼接 `auth_url` 与查询参数:
464    /// - `client_id`
465    /// - `redirect_uri`
466    /// - `response_type=code`
467    /// - `state`
468    /// - `scope`(如果配置了 scopes,以空格连接)
469    /// - 额外参数(如果配置了 extra_params)
470    ///
471    /// 如果 `auth_url` 已包含查询字符串,则用 `&` 追加,否则用 `?` 起始。
472    fn build_redirect_url(&self, state: &str) -> String {
473        let mut params: Vec<(String, String)> = vec![
474            ("client_id".into(), self.config.client_id.clone()),
475            ("redirect_uri".into(), self.config.redirect_url.clone()),
476            ("response_type".into(), "code".into()),
477            ("state".into(), state.to_string()),
478        ];
479
480        if !self.config.scopes.is_empty() {
481            params.push(("scope".into(), self.config.scopes.join(" ")));
482        }
483
484        for (key, value) in &self.config.extra_params {
485            params.push((key.clone(), value.clone()));
486        }
487
488        let query = params
489            .iter()
490            .map(|(key, value)| format!("{}={}", percent_encode(key), percent_encode(value)))
491            .collect::<Vec<_>>()
492            .join("&");
493
494        let separator = if self.config.auth_url.contains('?') {
495            "&"
496        } else {
497            "?"
498        };
499        format!("{}{}{}", self.config.auth_url, separator, query)
500    }
501
502    /// 用授权码换取访问令牌
503    ///
504    /// POST `token_url`,body 为 JSON:
505    /// ```json
506    /// {
507    ///   "grant_type": "authorization_code",
508    ///   "code": "<code>",
509    ///   "client_id": "<client_id>",
510    ///   "client_secret": "<client_secret>",
511    ///   "redirect_uri": "<redirect_url>"
512    /// }
513    /// ```
514    ///
515    /// 成功返回解析后的 JSON(含 `access_token` / `refresh_token` / `expires_in` 等)。
516    fn exchange_token(&self, code: &str) -> Result<serde_json::Value, OAuth2Error> {
517        let body = serde_json::json!({
518            "grant_type": "authorization_code",
519            "code": code,
520            "client_id": self.config.client_id,
521            "client_secret": self.config.client_secret,
522            "redirect_uri": self.config.redirect_url,
523        });
524        let body_str =
525            serde_json::to_string(&body).map_err(|err| OAuth2Error::Serialize(err.to_string()))?;
526
527        let response = self
528            .transport
529            .post_json(&self.config.token_url, &body_str)
530            .map_err(|err| OAuth2Error::HttpTransport(err.to_string()))?;
531
532        if response.is_empty() {
533            return Ok(serde_json::Value::Null);
534        }
535
536        serde_json::from_str(&response)
537            .map_err(|err| OAuth2Error::TokenExchangeFailed(format!("解析 token 响应失败: {err}")))
538    }
539
540    /// 获取用户信息
541    ///
542    /// POST `user_url`,body 为 JSON(含 `access_token` 和 `openid`)。
543    /// 成功返回解析后的 JSON。
544    fn fetch_user_info(
545        &self,
546        access_token: &str,
547        token_json: &serde_json::Value,
548    ) -> Result<serde_json::Value, OAuth2Error> {
549        let user_url = self
550            .config
551            .user_url
552            .as_ref()
553            .ok_or_else(|| OAuth2Error::UserInfoFailed("user_url 未配置".into()))?;
554
555        let body = serde_json::json!({
556            "access_token": access_token,
557            "openid": token_json.get("openid").cloned().unwrap_or(serde_json::Value::Null),
558        });
559        let body_str =
560            serde_json::to_string(&body).map_err(|err| OAuth2Error::Serialize(err.to_string()))?;
561
562        let response = self
563            .transport
564            .post_json(user_url, &body_str)
565            .map_err(|err| OAuth2Error::HttpTransport(err.to_string()))?;
566
567        if response.is_empty() {
568            return Ok(serde_json::Value::Null);
569        }
570
571        serde_json::from_str(&response)
572            .map_err(|err| OAuth2Error::UserInfoFailed(format!("解析用户信息响应失败: {err}")))
573    }
574
575    /// 从用户信息 JSON 中提取 [`SocialiteUser`] 的标准字段
576    ///
577    /// 兼容多种字段命名(对齐 Laravel Socialite 的字段映射逻辑):
578    /// - `id` / `openid` / `user_id` → `id`
579    /// - `nickname` / `nick_name` → `nickname`
580    /// - `name` / `username` → `name`
581    /// - `email` → `email`
582    /// - `avatar` / `figureurl_qq_1` / `figureurl` / `headimgurl` → `avatar`
583    fn extract_user_fields(user_json: &serde_json::Value) -> SocialiteUser {
584        let id = user_json
585            .get("id")
586            .or_else(|| user_json.get("openid"))
587            .or_else(|| user_json.get("user_id"))
588            .and_then(extract_string)
589            .unwrap_or_default();
590
591        let nickname = user_json
592            .get("nickname")
593            .or_else(|| user_json.get("nick_name"))
594            .and_then(extract_string);
595
596        let name = user_json
597            .get("name")
598            .or_else(|| user_json.get("username"))
599            .and_then(extract_string);
600
601        let email = user_json.get("email").and_then(extract_string);
602
603        let avatar = user_json
604            .get("avatar")
605            .or_else(|| user_json.get("figureurl_qq_1"))
606            .or_else(|| user_json.get("figureurl"))
607            .or_else(|| user_json.get("headimgurl"))
608            .and_then(extract_string);
609
610        SocialiteUser {
611            id,
612            nickname,
613            name,
614            email,
615            avatar,
616            raw: user_json.clone(),
617            access_token: None,
618            refresh_token: None,
619            expires_in: None,
620        }
621    }
622}
623
624impl OAuth2Provider for GenericOAuth2Provider {
625    fn redirect_url(&self, state: &str) -> String {
626        self.build_redirect_url(state)
627    }
628
629    fn user_from_token(&self, code: &str) -> Result<SocialiteUser, OAuth2Error> {
630        // 1. 校验配置必填字段
631        self.config.validate()?;
632
633        // 2. 校验授权码非空
634        if code.is_empty() {
635            return Err(OAuth2Error::AuthFailed("授权码不能为空".into()));
636        }
637
638        // 3. 用授权码换取访问令牌
639        let token_json = self.exchange_token(code)?;
640
641        // 4. 提取 access_token
642        let access_token = token_json
643            .get("access_token")
644            .and_then(|value| value.as_str())
645            .ok_or_else(|| {
646                OAuth2Error::TokenExchangeFailed(format!(
647                    "token 响应缺少 access_token 字段: {token_json}"
648                ))
649            })?
650            .to_string();
651
652        let refresh_token = token_json
653            .get("refresh_token")
654            .and_then(|value| value.as_str())
655            .map(|value| value.to_string());
656
657        let expires_in = token_json
658            .get("expires_in")
659            .and_then(|value| value.as_i64());
660
661        // 5. 如果配置了 user_url,获取用户信息;否则仅返回 token 信息
662        let mut user = if self.config.user_url.is_some() {
663            let user_json = self.fetch_user_info(&access_token, &token_json)?;
664            Self::extract_user_fields(&user_json)
665        } else {
666            SocialiteUser::default()
667        };
668
669        user.access_token = Some(access_token);
670        user.refresh_token = refresh_token;
671        user.expires_in = expires_in;
672
673        Ok(user)
674    }
675}
676
677// ============================================================================
678// 辅助函数
679// ============================================================================
680
681/// 从 JSON 值中提取字符串
682///
683/// 支持字符串和整数类型(整数转为十进制字符串),其他类型返回 `None`。
684fn extract_string(value: &serde_json::Value) -> Option<String> {
685    match value {
686        serde_json::Value::String(string) => Some(string.clone()),
687        serde_json::Value::Number(number) => number.as_i64().map(|number| number.to_string()),
688        _ => None,
689    }
690}
691
692/// 简易百分号编码 — 用于 URL 查询参数
693///
694/// 对齐 RFC 3986 的 unreserved 字符集(`A-Za-z0-9-._~`)保持原样,
695/// 其余字符编码为 `%XX` 形式(UTF-8 字节)。
696fn percent_encode(input: &str) -> String {
697    let mut output = String::with_capacity(input.len());
698    for byte in input.as_bytes() {
699        if matches!(byte, b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'.' | b'_' | b'~') {
700            output.push(*byte as char);
701        } else {
702            output.push_str(&format!("%{byte:02X}"));
703        }
704    }
705    output
706}
707
708// ============================================================================
709// 单元测试
710// ============================================================================
711
712#[cfg(test)]
713mod tests {
714    use super::*;
715
716    // ------------------------------------------------------------------------
717    // OAuth2Config 测试
718    // ------------------------------------------------------------------------
719
720    /// 测试 OAuth2Config builder 模式(所有可选字段)
721    #[test]
722    fn test_oauth2_config_builder() {
723        let config = OAuth2Config::new(
724            "client123",
725            "secret456",
726            "https://example.com/callback",
727            "https://provider.com/authorize",
728            "https://provider.com/token",
729        )
730        .with_user_url("https://provider.com/user/info")
731        .with_scopes(vec!["scope1".into(), "scope2".into()])
732        .with_extra_param("foo", "bar");
733
734        assert_eq!(config.client_id, "client123");
735        assert_eq!(config.client_secret, "secret456");
736        assert_eq!(config.redirect_url, "https://example.com/callback");
737        assert_eq!(config.auth_url, "https://provider.com/authorize");
738        assert_eq!(config.token_url, "https://provider.com/token");
739        assert_eq!(
740            config.user_url.as_deref(),
741            Some("https://provider.com/user/info")
742        );
743        assert_eq!(config.scopes, vec!["scope1", "scope2"]);
744        assert_eq!(config.extra_params, vec![("foo".into(), "bar".into())]);
745    }
746
747    /// 测试 OAuth2Config 最小配置(仅必填字段)
748    #[test]
749    fn test_oauth2_config_minimal() {
750        let config = OAuth2Config::new(
751            "client123",
752            "secret456",
753            "https://example.com/callback",
754            "https://provider.com/authorize",
755            "https://provider.com/token",
756        );
757
758        assert_eq!(config.client_id, "client123");
759        assert_eq!(config.client_secret, "secret456");
760        assert_eq!(config.redirect_url, "https://example.com/callback");
761        assert_eq!(config.auth_url, "https://provider.com/authorize");
762        assert_eq!(config.token_url, "https://provider.com/token");
763        assert!(config.user_url.is_none());
764        assert!(config.scopes.is_empty());
765        assert!(config.extra_params.is_empty());
766
767        // 最小配置应通过校验
768        assert!(config.validate().is_ok());
769    }
770
771    /// 测试 OAuth2Config::with_scope 追加多个 scope
772    #[test]
773    fn test_oauth2_config_with_scope_chained() {
774        let config = OAuth2Config::new(
775            "id",
776            "secret",
777            "https://example.com/callback",
778            "https://provider.com/authorize",
779            "https://provider.com/token",
780        )
781        .with_scope("get_user_info")
782        .with_scope("get_unionid");
783
784        assert_eq!(config.scopes, vec!["get_user_info", "get_unionid"]);
785    }
786
787    /// 测试 OAuth2Config::with_extra_params 覆盖
788    #[test]
789    fn test_oauth2_config_with_extra_params() {
790        let config = OAuth2Config::new(
791            "id",
792            "secret",
793            "https://example.com/callback",
794            "https://provider.com/authorize",
795            "https://provider.com/token",
796        )
797        .with_extra_param("a", "1")
798        .with_extra_param("b", "2")
799        .with_extra_params(vec![("x".into(), "10".into())]);
800
801        assert_eq!(config.extra_params, vec![("x".into(), "10".into())]);
802    }
803
804    /// 测试 OAuth2Config::validate 检测空字段
805    #[test]
806    fn test_oauth2_config_validate_empty_fields() {
807        // client_id 为空
808        let config = OAuth2Config::new(
809            "",
810            "secret",
811            "https://example.com/callback",
812            "https://provider.com/authorize",
813            "https://provider.com/token",
814        );
815        let err = config.validate().unwrap_err();
816        assert!(matches!(err, OAuth2Error::MissingField(field) if field == "client_id"));
817
818        // client_secret 为空
819        let config = OAuth2Config::new(
820            "id",
821            "",
822            "https://example.com/callback",
823            "https://provider.com/authorize",
824            "https://provider.com/token",
825        );
826        let err = config.validate().unwrap_err();
827        assert!(matches!(err, OAuth2Error::MissingField(field) if field == "client_secret"));
828
829        // redirect_url 为空
830        let config = OAuth2Config::new(
831            "id",
832            "secret",
833            "",
834            "https://provider.com/authorize",
835            "https://provider.com/token",
836        );
837        let err = config.validate().unwrap_err();
838        assert!(matches!(err, OAuth2Error::MissingField(field) if field == "redirect_url"));
839
840        // auth_url 为空
841        let config = OAuth2Config::new(
842            "id",
843            "secret",
844            "https://example.com/callback",
845            "",
846            "https://provider.com/token",
847        );
848        let err = config.validate().unwrap_err();
849        assert!(matches!(err, OAuth2Error::MissingField(field) if field == "auth_url"));
850
851        // token_url 为空
852        let config = OAuth2Config::new(
853            "id",
854            "secret",
855            "https://example.com/callback",
856            "https://provider.com/authorize",
857            "",
858        );
859        let err = config.validate().unwrap_err();
860        assert!(matches!(err, OAuth2Error::MissingField(field) if field == "token_url"));
861    }
862
863    // ------------------------------------------------------------------------
864    // SocialiteUser 测试
865    // ------------------------------------------------------------------------
866
867    /// 测试 SocialiteUser 默认值
868    #[test]
869    fn test_socialite_user_default() {
870        let user = SocialiteUser::default();
871        assert!(user.id.is_empty());
872        assert!(user.nickname.is_none());
873        assert!(user.name.is_none());
874        assert!(user.email.is_none());
875        assert!(user.avatar.is_none());
876        assert!(user.raw.is_null());
877        assert!(user.access_token.is_none());
878        assert!(user.refresh_token.is_none());
879        assert!(user.expires_in.is_none());
880    }
881
882    /// 测试 SocialiteUser 序列化/反序列化
883    #[test]
884    fn test_socialite_user_serialize_deserialize() {
885        let user = SocialiteUser {
886            id: "123".into(),
887            nickname: Some("tester".into()),
888            name: Some("Test User".into()),
889            email: Some("test@example.com".into()),
890            avatar: Some("https://example.com/avatar.png".into()),
891            raw: serde_json::json!({"key": "value"}),
892            access_token: Some("token123".into()),
893            refresh_token: Some("refresh456".into()),
894            expires_in: Some(3600),
895        };
896
897        let json = serde_json::to_string(&user).expect("序列化失败");
898
899        // P0-SEC-01 安全修复:access_token / refresh_token 不应出现在序列化输出中
900        // 防止令牌通过 API 响应泄漏(对齐 MerchantUser.password 的 skip_serializing 策略)
901        assert!(
902            !json.contains("access_token"),
903            "access_token 不应出现在序列化 JSON 中(安全脱敏要求): {json}"
904        );
905        assert!(
906            !json.contains("refresh_token"),
907            "refresh_token 不应出现在序列化 JSON 中(安全脱敏要求): {json}"
908        );
909
910        let parsed: SocialiteUser = serde_json::from_str(&json).expect("反序列化失败");
911
912        assert_eq!(parsed.id, "123");
913        assert_eq!(parsed.nickname.as_deref(), Some("tester"));
914        assert_eq!(parsed.name.as_deref(), Some("Test User"));
915        assert_eq!(parsed.email.as_deref(), Some("test@example.com"));
916        assert_eq!(
917            parsed.avatar.as_deref(),
918            Some("https://example.com/avatar.png")
919        );
920        // 反序列化后 token 字段为 None(序列化时未包含,反序列化用 #[serde(default)])
921        assert_eq!(parsed.access_token, None);
922        assert_eq!(parsed.refresh_token, None);
923        assert_eq!(parsed.expires_in, Some(3600));
924    }
925
926    // ------------------------------------------------------------------------
927    // redirect_url 测试
928    // ------------------------------------------------------------------------
929
930    /// 测试 redirect_url 包含必填查询参数
931    #[test]
932    fn test_redirect_url_contains_required_params() {
933        let config = OAuth2Config::new(
934            "client123",
935            "secret456",
936            "https://example.com/callback",
937            "https://provider.com/oauth2.0/authorize",
938            "https://provider.com/oauth2.0/token",
939        );
940        let provider =
941            GenericOAuth2Provider::new(config, Arc::new(MemoryOAuth2HttpTransport::new()));
942
943        let url = provider.redirect_url("random_state_abc");
944
945        assert!(url.starts_with("https://provider.com/oauth2.0/authorize?"));
946        assert!(url.contains("client_id=client123"));
947        assert!(url.contains("redirect_uri=https%3A%2F%2Fexample.com%2Fcallback"));
948        assert!(url.contains("response_type=code"));
949        assert!(url.contains("state=random_state_abc"));
950        // 未配置 scopes 时不应包含 scope
951        assert!(!url.contains("scope="));
952    }
953
954    /// 测试 redirect_url 包含 scopes(空格连接,百分号编码为 %20)
955    #[test]
956    fn test_redirect_url_with_scopes() {
957        let config = OAuth2Config::new(
958            "client123",
959            "secret456",
960            "https://example.com/callback",
961            "https://provider.com/oauth2.0/authorize",
962            "https://provider.com/oauth2.0/token",
963        )
964        .with_scopes(vec!["get_user_info".into(), "get_unionid".into()]);
965        let provider =
966            GenericOAuth2Provider::new(config, Arc::new(MemoryOAuth2HttpTransport::new()));
967
968        let url = provider.redirect_url("state123");
969
970        // scope 以空格连接,空格编码为 %20
971        assert!(url.contains("scope=get_user_info%20get_unionid"));
972    }
973
974    /// 测试 redirect_url 包含额外参数
975    #[test]
976    fn test_redirect_url_with_extra_params() {
977        let config = OAuth2Config::new(
978            "client123",
979            "secret456",
980            "https://example.com/callback",
981            "https://provider.com/oauth2.0/authorize",
982            "https://provider.com/oauth2.0/token",
983        )
984        .with_extra_param("foo", "bar")
985        .with_extra_param("display", "mobile");
986        let provider =
987            GenericOAuth2Provider::new(config, Arc::new(MemoryOAuth2HttpTransport::new()));
988
989        let url = provider.redirect_url("state123");
990
991        assert!(url.contains("foo=bar"));
992        assert!(url.contains("display=mobile"));
993    }
994
995    /// 测试 redirect_url 在已有查询字符串的 auth_url 上追加参数
996    #[test]
997    fn test_redirect_url_with_existing_query() {
998        let config = OAuth2Config::new(
999            "client123",
1000            "secret456",
1001            "https://example.com/callback",
1002            "https://provider.com/authorize?foo=bar",
1003            "https://provider.com/token",
1004        );
1005        let provider =
1006            GenericOAuth2Provider::new(config, Arc::new(MemoryOAuth2HttpTransport::new()));
1007
1008        let url = provider.redirect_url("state123");
1009
1010        // 已有查询字符串时应使用 & 追加
1011        assert!(url.contains("?foo=bar&"));
1012        assert!(url.contains("client_id=client123"));
1013    }
1014
1015    // ------------------------------------------------------------------------
1016    // MemoryOAuth2HttpTransport 测试
1017    // ------------------------------------------------------------------------
1018
1019    /// 测试 MemoryOAuth2HttpTransport 记录请求并返回预置响应
1020    #[test]
1021    fn test_memory_oauth2_http_transport_post_json() {
1022        let transport = MemoryOAuth2HttpTransport::new();
1023        transport.push_response(r#"{"access_token":"token123"}"#);
1024
1025        let response = transport
1026            .post_json("https://example.com/token", r#"{"code":"abc"}"#)
1027            .expect("post_json 失败");
1028
1029        assert_eq!(response, r#"{"access_token":"token123"}"#);
1030        assert_eq!(transport.count(), 1);
1031
1032        let (url, body) = transport.last().expect("应有请求记录");
1033        assert_eq!(url, "https://example.com/token");
1034        assert_eq!(body, r#"{"code":"abc"}"#);
1035    }
1036
1037    /// 测试 MemoryOAuth2HttpTransport 响应队列按顺序消费
1038    #[test]
1039    fn test_memory_oauth2_http_transport_response_queue() {
1040        let transport = MemoryOAuth2HttpTransport::new();
1041        transport.push_response("resp1");
1042        transport.push_response("resp2");
1043
1044        let resp1 = transport
1045            .post_json("url1", "body1")
1046            .expect("第一次调用失败");
1047        let resp2 = transport
1048            .post_json("url2", "body2")
1049            .expect("第二次调用失败");
1050
1051        assert_eq!(resp1, "resp1");
1052        assert_eq!(resp2, "resp2");
1053        assert_eq!(transport.count(), 2);
1054    }
1055
1056    /// 测试 MemoryOAuth2HttpTransport 响应耗尽后返回空字符串
1057    #[test]
1058    fn test_memory_oauth2_http_transport_empty_response() {
1059        let transport = MemoryOAuth2HttpTransport::new();
1060        // 不预置响应
1061        let response = transport
1062            .post_json("url", "body")
1063            .expect("post_json 不应失败");
1064        assert_eq!(response, "");
1065    }
1066
1067    /// 测试 MemoryOAuth2HttpTransport clear
1068    #[test]
1069    fn test_memory_oauth2_http_transport_clear() {
1070        let transport = MemoryOAuth2HttpTransport::new();
1071        transport.push_response("resp");
1072        transport.post_json("url", "body").expect("调用失败");
1073        assert_eq!(transport.count(), 1);
1074
1075        transport.clear();
1076        assert_eq!(transport.count(), 0);
1077        // clear 后响应队列也清空,返回空字符串
1078        let response = transport
1079            .post_json("url", "body")
1080            .expect("post_json 不应失败");
1081        assert_eq!(response, "");
1082    }
1083
1084    // ------------------------------------------------------------------------
1085    // GenericOAuth2Provider::user_from_token 测试
1086    // ------------------------------------------------------------------------
1087
1088    /// 测试 GenericOAuth2Provider::user_from_token 完整流程
1089    ///
1090    /// 使用 MemoryOAuth2HttpTransport mock token 响应和用户信息响应。
1091    #[test]
1092    fn test_generic_oauth2_provider_user_from_token() {
1093        let transport = Arc::new(MemoryOAuth2HttpTransport::new());
1094        // 预置 mock 响应:token 响应 + 用户信息响应
1095        transport.push_response(r#"{"access_token":"token123","refresh_token":"refresh456","expires_in":3600,"openid":"openid_abc"}"#);
1096        transport.push_response(
1097            r#"{"id":"12345","nickname":"test_user","name":"Test","email":"test@example.com","avatar":"https://example.com/avatar.png"}"#,
1098        );
1099
1100        let config = OAuth2Config::new(
1101            "client123",
1102            "secret456",
1103            "https://example.com/callback",
1104            "https://provider.com/authorize",
1105            "https://provider.com/token",
1106        )
1107        .with_user_url("https://provider.com/user/info");
1108        let provider = GenericOAuth2Provider::new(config, transport.clone());
1109
1110        let user = provider
1111            .user_from_token("auth_code_abc")
1112            .expect("user_from_token 失败");
1113
1114        // 验证 token 字段
1115        assert_eq!(user.access_token.as_deref(), Some("token123"));
1116        assert_eq!(user.refresh_token.as_deref(), Some("refresh456"));
1117        assert_eq!(user.expires_in, Some(3600));
1118
1119        // 验证用户信息字段
1120        assert_eq!(user.id, "12345");
1121        assert_eq!(user.nickname.as_deref(), Some("test_user"));
1122        assert_eq!(user.name.as_deref(), Some("Test"));
1123        assert_eq!(user.email.as_deref(), Some("test@example.com"));
1124        assert_eq!(
1125            user.avatar.as_deref(),
1126            Some("https://example.com/avatar.png")
1127        );
1128
1129        // 验证原始响应数据
1130        assert_eq!(user.raw["id"], "12345");
1131        assert_eq!(user.raw["nickname"], "test_user");
1132
1133        // 验证 HTTP 请求次数(token + user info)
1134        assert_eq!(transport.count(), 2);
1135    }
1136
1137    /// 测试 GenericOAuth2Provider::user_from_token 无 user_url 时仅返回 token
1138    #[test]
1139    fn test_generic_oauth2_provider_user_from_token_no_user_url() {
1140        let transport = Arc::new(MemoryOAuth2HttpTransport::new());
1141        transport.push_response(r#"{"access_token":"token123","expires_in":7200}"#);
1142
1143        let config = OAuth2Config::new(
1144            "client123",
1145            "secret456",
1146            "https://example.com/callback",
1147            "https://provider.com/authorize",
1148            "https://provider.com/token",
1149        );
1150        // 不配置 user_url
1151        let provider = GenericOAuth2Provider::new(config, transport.clone());
1152
1153        let user = provider
1154            .user_from_token("auth_code")
1155            .expect("user_from_token 失败");
1156
1157        assert_eq!(user.access_token.as_deref(), Some("token123"));
1158        assert_eq!(user.expires_in, Some(7200));
1159        assert!(user.refresh_token.is_none());
1160        // 无 user_url 时不获取用户信息,id 为空
1161        assert!(user.id.is_empty());
1162        // 仅一次 HTTP 请求(token 交换)
1163        assert_eq!(transport.count(), 1);
1164    }
1165
1166    /// 测试 GenericOAuth2Provider::user_from_token 授权码为空时返回错误
1167    #[test]
1168    fn test_generic_oauth2_provider_missing_code() {
1169        let config = OAuth2Config::new(
1170            "client123",
1171            "secret456",
1172            "https://example.com/callback",
1173            "https://provider.com/authorize",
1174            "https://provider.com/token",
1175        );
1176        let provider =
1177            GenericOAuth2Provider::new(config, Arc::new(MemoryOAuth2HttpTransport::new()));
1178
1179        let err = provider.user_from_token("").unwrap_err();
1180        assert!(matches!(err, OAuth2Error::AuthFailed(msg) if msg.contains("授权码")));
1181    }
1182
1183    /// 测试 GenericOAuth2Provider 配置缺少必填字段时返回错误
1184    #[test]
1185    fn test_oauth2_provider_missing_config_fields() {
1186        // client_id 为空
1187        let config = OAuth2Config::new(
1188            "",
1189            "secret456",
1190            "https://example.com/callback",
1191            "https://provider.com/authorize",
1192            "https://provider.com/token",
1193        );
1194        let provider = GenericOAuth2Provider::new(config, Arc::new(MemoryHttpTransport));
1195
1196        let err = provider.user_from_token("code").unwrap_err();
1197        assert!(matches!(err, OAuth2Error::MissingField(field) if field == "client_id"));
1198
1199        // token_url 为空
1200        let config = OAuth2Config::new(
1201            "client123",
1202            "secret456",
1203            "https://example.com/callback",
1204            "https://provider.com/authorize",
1205            "",
1206        );
1207        let provider = GenericOAuth2Provider::new(config, Arc::new(MemoryHttpTransport));
1208
1209        let err = provider.user_from_token("code").unwrap_err();
1210        assert!(matches!(err, OAuth2Error::MissingField(field) if field == "token_url"));
1211    }
1212
1213    /// 测试 token 响应缺少 access_token 时返回 TokenExchangeFailed
1214    #[test]
1215    fn test_generic_oauth2_provider_token_response_missing_access_token() {
1216        let transport = MemoryOAuth2HttpTransport::new();
1217        transport.push_response(r#"{"error":"invalid_grant"}"#);
1218
1219        let config = OAuth2Config::new(
1220            "client123",
1221            "secret456",
1222            "https://example.com/callback",
1223            "https://provider.com/authorize",
1224            "https://provider.com/token",
1225        );
1226        let provider = GenericOAuth2Provider::new(config, Arc::new(transport));
1227
1228        let err = provider.user_from_token("code").unwrap_err();
1229        assert!(matches!(err, OAuth2Error::TokenExchangeFailed(_)));
1230    }
1231
1232    /// 测试 token 响应非 JSON 时返回 TokenExchangeFailed
1233    #[test]
1234    fn test_generic_oauth2_provider_token_response_invalid_json() {
1235        let transport = MemoryOAuth2HttpTransport::new();
1236        transport.push_response("not a json");
1237
1238        let config = OAuth2Config::new(
1239            "client123",
1240            "secret456",
1241            "https://example.com/callback",
1242            "https://provider.com/authorize",
1243            "https://provider.com/token",
1244        );
1245        let provider = GenericOAuth2Provider::new(config, Arc::new(transport));
1246
1247        let err = provider.user_from_token("code").unwrap_err();
1248        assert!(matches!(err, OAuth2Error::TokenExchangeFailed(_)));
1249    }
1250
1251    /// 测试用户信息字段兼容多种命名(openid / figureurl_qq_1 等)
1252    #[test]
1253    fn test_generic_oauth2_provider_user_info_field_aliases() {
1254        let transport = MemoryOAuth2HttpTransport::new();
1255        transport.push_response(r#"{"access_token":"token123","openid":"openid_abc"}"#);
1256        transport.push_response(
1257            r#"{"openid":"qq_12345","nickname":"qq_user","figureurl_qq_1":"https://qzapp.qlogo.cn/1.png"}"#,
1258        );
1259
1260        let config = OAuth2Config::new(
1261            "client123",
1262            "secret456",
1263            "https://example.com/callback",
1264            "https://provider.com/authorize",
1265            "https://provider.com/token",
1266        )
1267        .with_user_url("https://provider.com/user/info");
1268        let provider = GenericOAuth2Provider::new(config, Arc::new(transport));
1269
1270        let user = provider
1271            .user_from_token("code")
1272            .expect("user_from_token 失败");
1273
1274        // openid 作为 id
1275        assert_eq!(user.id, "qq_12345");
1276        assert_eq!(user.nickname.as_deref(), Some("qq_user"));
1277        // figureurl_qq_1 作为 avatar
1278        assert_eq!(user.avatar.as_deref(), Some("https://qzapp.qlogo.cn/1.png"));
1279    }
1280
1281    /// 测试用户 ID 为整数类型时正确转为字符串
1282    #[test]
1283    fn test_generic_oauth2_provider_user_id_integer() {
1284        let transport = MemoryOAuth2HttpTransport::new();
1285        transport.push_response(r#"{"access_token":"token123"}"#);
1286        transport.push_response(r#"{"id":12345,"nickname":"github_user"}"#);
1287
1288        let config = OAuth2Config::new(
1289            "client123",
1290            "secret456",
1291            "https://example.com/callback",
1292            "https://provider.com/authorize",
1293            "https://provider.com/token",
1294        )
1295        .with_user_url("https://provider.com/user/info");
1296        let provider = GenericOAuth2Provider::new(config, Arc::new(transport));
1297
1298        let user = provider
1299            .user_from_token("code")
1300            .expect("user_from_token 失败");
1301
1302        assert_eq!(user.id, "12345");
1303        assert_eq!(user.nickname.as_deref(), Some("github_user"));
1304    }
1305
1306    /// 测试 HTTP 传输失败时返回 HttpTransport 错误
1307    #[test]
1308    fn test_generic_oauth2_provider_http_transport_failure() {
1309        let config = OAuth2Config::new(
1310            "client123",
1311            "secret456",
1312            "https://example.com/callback",
1313            "https://provider.com/authorize",
1314            "https://provider.com/token",
1315        );
1316        let provider = GenericOAuth2Provider::new(config, Arc::new(FailingTransport));
1317
1318        let err = provider.user_from_token("code").unwrap_err();
1319        assert!(matches!(err, OAuth2Error::HttpTransport(_)));
1320    }
1321
1322    /// 测试 percent_encode 函数
1323    #[test]
1324    fn test_percent_encode() {
1325        // unreserved 字符保持原样
1326        assert_eq!(percent_encode("abcXYZ09-._~"), "abcXYZ09-._~");
1327        // 空格编码为 %20
1328        assert_eq!(percent_encode("a b"), "a%20b");
1329        // 斜杠编码为 %2F
1330        assert_eq!(percent_encode("/"), "%2F");
1331        // 冒号编码为 %3A
1332        assert_eq!(percent_encode(":"), "%3A");
1333        // URL 编码
1334        assert_eq!(
1335            percent_encode("https://example.com/path"),
1336            "https%3A%2F%2Fexample.com%2Fpath"
1337        );
1338        // 中文字符(UTF-8 编码)
1339        assert_eq!(percent_encode("中"), "%E4%B8%AD");
1340    }
1341
1342    /// 测试 extract_string 函数
1343    #[test]
1344    fn test_extract_string() {
1345        // 字符串
1346        assert_eq!(
1347            extract_string(&serde_json::json!("hello")),
1348            Some("hello".into())
1349        );
1350        // 整数
1351        assert_eq!(
1352            extract_string(&serde_json::json!(12345)),
1353            Some("12345".into())
1354        );
1355        // 浮点数(不支持,返回 None)
1356        assert_eq!(extract_string(&serde_json::json!(1.5)), None);
1357        // 布尔值(不支持,返回 None)
1358        assert_eq!(extract_string(&serde_json::json!(true)), None);
1359        // null(不支持,返回 None)
1360        assert_eq!(extract_string(&serde_json::Value::Null), None);
1361        // 对象(不支持,返回 None)
1362        assert_eq!(extract_string(&serde_json::json!({"a": 1})), None);
1363    }
1364
1365    // ------------------------------------------------------------------------
1366    // 测试辅助类型
1367    // ------------------------------------------------------------------------
1368
1369    /// 始终失败的 HTTP 传输(用于测试错误路径)
1370    struct FailingTransport;
1371
1372    impl OAuth2HttpTransport for FailingTransport {
1373        fn post_json(&self, _url: &str, _body: &str) -> Result<String, OAuth2Error> {
1374            Err(OAuth2Error::HttpTransport("connection refused".into()))
1375        }
1376    }
1377
1378    /// 空的 HTTP 传输(用于仅需校验、不实际发送的测试)
1379    struct MemoryHttpTransport;
1380
1381    impl OAuth2HttpTransport for MemoryHttpTransport {
1382        fn post_json(&self, _url: &str, _body: &str) -> Result<String, OAuth2Error> {
1383            Ok(String::new())
1384        }
1385    }
1386}