Skip to main content

openlark_core/
http.rs

1use std::{collections::HashSet, marker::PhantomData};
2
3use reqwest::RequestBuilder;
4use tracing::debug;
5use tracing::{info_span, Instrument};
6
7use crate::{
8    api::ApiResponseTrait,
9    api::{ApiRequest, Response},
10    auth::app_ticket::apply_app_ticket,
11    config::Config,
12    constants::*,
13    error::CoreError,
14    req_option::RequestOption,
15    req_translator::ReqTranslator,
16    response_handler::ImprovedResponseHandler,
17    SDKResult,
18};
19
20/// HTTP 传输层
21///
22/// 负责处理 API 请求的发送和响应接收,支持多种认证令牌类型
23pub struct Transport<T> {
24    phantom_data: PhantomData<T>,
25}
26
27impl<T> Default for Transport<T> {
28    fn default() -> Self {
29        Self::new()
30    }
31}
32
33impl<T> Transport<T> {
34    /// 创建新的 Transport 实例
35    pub fn new() -> Self {
36        Self {
37            phantom_data: PhantomData,
38        }
39    }
40}
41
42impl<T: ApiResponseTrait + std::fmt::Debug + for<'de> serde::Deserialize<'de>> Transport<T> {
43    /// 发送 API 请求
44    ///
45    /// # 参数
46    /// - `req`: API 请求对象
47    /// - `config`: 客户端配置
48    /// - `option`: 请求选项(可选)
49    ///
50    /// # 返回
51    /// API 响应结果
52    pub async fn request<R: Send>(
53        req: ApiRequest<R>,
54        config: &Config,
55        option: Option<RequestOption>,
56    ) -> Result<Response<T>, CoreError> {
57        // Create span for HTTP request tracing
58        let span = info_span!(
59            "http_request",
60            method = %req.method(),
61            path = %req.api_path(),
62            app_id = %config.app_id,
63            duration_ms = tracing::field::Empty,
64            status = tracing::field::Empty,
65        );
66
67        async move {
68            let start_time = std::time::Instant::now();
69            let option = option.unwrap_or_default();
70
71            let mut token_types = req.supported_access_token_types();
72            if token_types.is_empty() {
73                token_types = vec![AccessTokenType::None];
74            }
75
76            let result: Result<_, _> = async {
77                validate_token_type(&token_types, &option)?;
78                let access_token_type =
79                    determine_token_type(&token_types, &option, config.enable_token_cache);
80                validate(config, &option, access_token_type)?;
81
82                Self::do_request(req, access_token_type, config, option).await
83            }
84            .await;
85
86            // Record metrics in current span
87            let current_span = tracing::Span::current();
88            let duration_ms = start_time.elapsed().as_millis() as u64;
89            current_span.record("duration_ms", duration_ms);
90
91            match &result {
92                Ok(response) => {
93                    current_span.record(
94                        "status",
95                        if response.is_success() {
96                            "success"
97                        } else {
98                            "api_error"
99                        },
100                    );
101                }
102                Err(_) => {
103                    current_span.record("status", "error");
104                }
105            }
106
107            result
108        }
109        .instrument(span)
110        .await
111    }
112
113    async fn do_request<R: Send>(
114        mut http_req: ApiRequest<R>,
115        access_token_type: AccessTokenType,
116        config: &Config,
117        option: RequestOption,
118    ) -> SDKResult<Response<T>> {
119        let req =
120            ReqTranslator::translate(&mut http_req, access_token_type, config, &option).await?;
121        debug!(
122            method = %http_req.method(),
123            path = %http_req.api_path(),
124            "Sending request"
125        );
126        let resp = Self::do_send(req, http_req.to_bytes(), !http_req.file().is_empty()).await?;
127        debug!(
128            success = resp.is_success(),
129            code = resp.raw_response.code,
130            msg = %resp.raw_response.msg,
131            "Received response"
132        );
133
134        if !resp.is_success() && resp.raw_response.code == ERR_CODE_APP_TICKET_INVALID {
135            apply_app_ticket(config).await?;
136        }
137
138        Ok(resp)
139    }
140
141    /// 执行 HTTP 请求
142    pub async fn do_send(
143        raw_request: RequestBuilder,
144        body: Vec<u8>,
145        multi_part: bool,
146    ) -> SDKResult<Response<T>> {
147        // Create span for network request tracing
148        let span = info_span!(
149            "http_send",
150            multi_part = multi_part,
151            body_size = body.len(),
152            response_code = tracing::field::Empty,
153            response_size = tracing::field::Empty,
154        );
155
156        async move {
157            let future = if multi_part {
158                raw_request.send()
159            } else {
160                raw_request.body(body).send()
161            };
162
163            match future.await {
164                Ok(response) => {
165                    let status_code = response.status();
166                    tracing::Span::current().record("response_code", status_code.as_u16());
167
168                    // 使用改进的响应处理器,单次解析而非双重解析
169                    ImprovedResponseHandler::handle_response(response).await
170                }
171                Err(err) => {
172                    debug!("Request error: {err:?}");
173                    tracing::Span::current().record("response_code", 0_u16); // Indicate network error
174                    Err(err.into())
175                }
176            }
177        }
178        .instrument(span)
179        .await
180    }
181}
182
183fn validate_token_type(
184    access_token_types: &[AccessTokenType],
185    option: &RequestOption,
186) -> Result<(), CoreError> {
187    // 未指定可用 token 类型时,不做额外校验。
188    // 旧实现误将“非空”作为提前返回条件,并在空列表时访问 [0] 导致 panic。
189    if access_token_types.is_empty() {
190        return Ok(());
191    }
192
193    let access_token_type = access_token_types[0];
194
195    if access_token_type == AccessTokenType::Tenant && option.user_access_token.is_some() {
196        return Err(crate::error::validation_error(
197            "access_token_type",
198            "tenant token type not match user access token",
199        ));
200    }
201
202    if access_token_type == AccessTokenType::App && option.tenant_access_token.is_some() {
203        return Err(crate::error::validation_error(
204            "access_token_type",
205            "user token type not match tenant access token",
206        ));
207    }
208
209    Ok(())
210}
211
212fn determine_token_type(
213    access_token_types: &[AccessTokenType],
214    option: &RequestOption,
215    enable_token_cache: bool,
216) -> AccessTokenType {
217    if !enable_token_cache {
218        if option.user_access_token.is_some() {
219            return AccessTokenType::User;
220        }
221        if option.tenant_access_token.is_some() {
222            return AccessTokenType::Tenant;
223        }
224        if option.app_access_token.is_some() {
225            return AccessTokenType::App;
226        }
227
228        return AccessTokenType::None;
229    }
230
231    // 缓存开启但未指定 token 类型时,退回到“按显式传入的 token”推断,避免空列表 panic。
232    if access_token_types.is_empty() {
233        if option.user_access_token.is_some() {
234            return AccessTokenType::User;
235        }
236        if option.tenant_access_token.is_some() || option.tenant_key.is_some() {
237            return AccessTokenType::Tenant;
238        }
239        if option.app_access_token.is_some() {
240            return AccessTokenType::App;
241        }
242        return AccessTokenType::None;
243    }
244
245    let mut accessible_token_type_set: HashSet<AccessTokenType> = HashSet::new();
246    let mut access_token_type = access_token_types[0];
247
248    for t in access_token_types {
249        if *t == AccessTokenType::Tenant {
250            access_token_type = *t; // 默认值
251        }
252        accessible_token_type_set.insert(*t);
253    }
254
255    if option.tenant_key.is_some() && accessible_token_type_set.contains(&AccessTokenType::Tenant) {
256        access_token_type = AccessTokenType::Tenant;
257    }
258
259    if option.user_access_token.is_some()
260        && accessible_token_type_set.contains(&AccessTokenType::User)
261    {
262        access_token_type = AccessTokenType::User;
263    }
264
265    access_token_type
266}
267
268fn validate(
269    config: &Config,
270    option: &RequestOption,
271    access_token_type: AccessTokenType,
272) -> Result<(), CoreError> {
273    if config.app_id.is_empty() {
274        return Err(crate::error::validation_error("app_id", "AppId is empty"));
275    }
276
277    if config.app_secret.is_empty() {
278        return Err(crate::error::validation_error(
279            "app_secret",
280            "AppSecret is empty",
281        ));
282    }
283
284    if !config.enable_token_cache {
285        if access_token_type == AccessTokenType::None {
286            return Ok(());
287        }
288        if option.user_access_token.is_none()
289            && option.tenant_access_token.is_none()
290            && option.app_access_token.is_none()
291        {
292            return Err(crate::error::validation_error(
293                "access_token",
294                "accessToken is empty",
295            ));
296        }
297    }
298
299    if config.app_type == AppType::Marketplace
300        && access_token_type == AccessTokenType::Tenant
301        && option.tenant_key.is_none()
302    {
303        return Err(crate::error::validation_error(
304            "access_token",
305            "accessToken is empty",
306        ));
307    }
308
309    if access_token_type == AccessTokenType::User && option.user_access_token.is_none() {
310        return Err(crate::error::validation_error(
311            "user_access_token",
312            "user access token is empty",
313        ));
314    }
315
316    if option.header.contains_key(HTTP_HEADER_KEY_REQUEST_ID) {
317        return Err(crate::error::validation_error(
318            "header",
319            format!("use {HTTP_HEADER_KEY_REQUEST_ID} as header key is not allowed"),
320        ));
321    }
322    if option.header.contains_key(HTTP_HEADER_REQUEST_ID) {
323        return Err(crate::error::validation_error(
324            "header",
325            format!("use {HTTP_HEADER_REQUEST_ID} as header key is not allowed"),
326        ));
327    }
328
329    Ok(())
330}
331
332#[cfg(test)]
333#[allow(clippy::field_reassign_with_default)]
334mod test {
335    use std::collections::HashMap;
336
337    use crate::{
338        config::Config,
339        constants::{AccessTokenType, AppType, HTTP_HEADER_KEY_REQUEST_ID, HTTP_HEADER_REQUEST_ID},
340        http::{determine_token_type, validate, validate_token_type},
341        req_option::RequestOption,
342    };
343
344    fn create_test_config() -> Config {
345        Config::builder()
346            .app_id("test_app_id")
347            .app_secret("test_app_secret")
348            .build()
349    }
350
351    fn create_test_config_marketplace() -> Config {
352        Config::builder()
353            .app_id("test_app_id")
354            .app_secret("test_app_secret")
355            .app_type(AppType::Marketplace)
356            .build()
357    }
358
359    #[test]
360    fn test_validate_token_type_empty_list_no_panic() {
361        let empty_types: Vec<AccessTokenType> = vec![];
362        let option = RequestOption::default();
363
364        // 空列表不应 panic,且不做额外校验。
365        let result = validate_token_type(&empty_types, &option);
366        assert!(result.is_ok());
367    }
368
369    #[test]
370    fn test_validate_token_type_non_empty_list_returns_ok() {
371        let types = vec![AccessTokenType::User, AccessTokenType::Tenant];
372        let option = RequestOption::default();
373
374        // 列表非空时应进行校验(当前仅对 Tenant/App 的 token 冲突做约束)
375        let result = validate_token_type(&types, &option);
376        assert!(result.is_ok());
377    }
378
379    #[test]
380    fn test_validate_token_type_tenant_with_user_token() {
381        let types = vec![AccessTokenType::Tenant];
382        let option = RequestOption {
383            user_access_token: Some("user_token".to_string()),
384            ..Default::default()
385        };
386
387        let result = validate_token_type(&types, &option);
388        assert!(result.is_err());
389    }
390
391    #[test]
392    fn test_validate_token_type_app_with_tenant_token() {
393        let types = vec![AccessTokenType::App];
394        let option = RequestOption {
395            tenant_access_token: Some("tenant_token".to_string()),
396            ..Default::default()
397        };
398
399        let result = validate_token_type(&types, &option);
400        assert!(result.is_err());
401    }
402
403    #[test]
404    fn test_validate_token_type_valid_combinations() {
405        let types = vec![AccessTokenType::User];
406        let mut option = RequestOption::default();
407        option.user_access_token = Some("user_token".to_string());
408
409        let result = validate_token_type(&types, &option);
410        assert!(result.is_ok());
411    }
412
413    #[test]
414    fn test_determine_token_type_no_cache_user() {
415        let types = vec![AccessTokenType::User, AccessTokenType::Tenant];
416        let mut option = RequestOption::default();
417        option.user_access_token = Some("user_token".to_string());
418
419        let token_type = determine_token_type(&types, &option, false);
420        assert_eq!(token_type, AccessTokenType::User);
421    }
422
423    #[test]
424    fn test_determine_token_type_no_cache_tenant() {
425        let types = vec![AccessTokenType::User, AccessTokenType::Tenant];
426        let option = RequestOption {
427            tenant_access_token: Some("tenant_token".to_string()),
428            ..Default::default()
429        };
430
431        let token_type = determine_token_type(&types, &option, false);
432        assert_eq!(token_type, AccessTokenType::Tenant);
433    }
434
435    #[test]
436    fn test_determine_token_type_no_cache_app() {
437        let types = vec![AccessTokenType::App, AccessTokenType::Tenant];
438        let option = RequestOption {
439            app_access_token: Some("app_token".to_string()),
440            ..Default::default()
441        };
442
443        let token_type = determine_token_type(&types, &option, false);
444        assert_eq!(token_type, AccessTokenType::App);
445    }
446
447    #[test]
448    fn test_determine_token_type_no_cache_none() {
449        let types = vec![AccessTokenType::None];
450        let option = RequestOption::default();
451
452        let token_type = determine_token_type(&types, &option, false);
453        assert_eq!(token_type, AccessTokenType::None);
454    }
455
456    #[test]
457    fn test_determine_token_type_with_cache_defaults_to_tenant() {
458        let types = vec![AccessTokenType::User, AccessTokenType::Tenant];
459        let option = RequestOption::default();
460
461        let token_type = determine_token_type(&types, &option, true);
462        assert_eq!(token_type, AccessTokenType::Tenant);
463    }
464
465    #[test]
466    fn test_determine_token_type_with_cache_tenant_key() {
467        let types = vec![AccessTokenType::User, AccessTokenType::Tenant];
468        let mut option = RequestOption::default();
469        option.tenant_key = Some("tenant_key".to_string());
470
471        let token_type = determine_token_type(&types, &option, true);
472        assert_eq!(token_type, AccessTokenType::Tenant);
473    }
474
475    #[test]
476    fn test_determine_token_type_with_cache_user_access_token() {
477        let types = vec![AccessTokenType::User, AccessTokenType::Tenant];
478        let mut option = RequestOption::default();
479        option.user_access_token = Some("user_token".to_string());
480
481        let token_type = determine_token_type(&types, &option, true);
482        assert_eq!(token_type, AccessTokenType::User);
483    }
484
485    #[test]
486    fn test_validate_empty_app_id() {
487        let config = Config::builder()
488            .app_id("")
489            .app_secret("test_secret")
490            .build();
491        let option = RequestOption::default();
492
493        let result = validate(&config, &option, AccessTokenType::None);
494        assert!(matches!(
495            result,
496            Err(crate::error::CoreError::Validation { .. })
497        ));
498    }
499
500    #[test]
501    fn test_validate_empty_app_secret() {
502        let config = Config::builder().app_id("test_id").app_secret("").build();
503        let option = RequestOption::default();
504
505        let result = validate(&config, &option, AccessTokenType::None);
506        assert!(matches!(
507            result,
508            Err(crate::error::CoreError::Validation { .. })
509        ));
510    }
511
512    #[test]
513    fn test_validate_no_cache_missing_access_tokens() {
514        let config = Config::builder()
515            .app_id("test_app_id")
516            .app_secret("test_app_secret")
517            .enable_token_cache(false)
518            .build();
519        let option = RequestOption::default();
520
521        let result = validate(&config, &option, AccessTokenType::User);
522        assert!(matches!(
523            result,
524            Err(crate::error::CoreError::Validation { .. })
525        ));
526    }
527
528    #[test]
529    fn test_validate_no_cache_with_tokens() {
530        let config = Config::builder()
531            .app_id("test_app_id")
532            .app_secret("test_app_secret")
533            .enable_token_cache(false)
534            .build();
535        let mut option = RequestOption::default();
536        option.user_access_token = Some("token".to_string());
537
538        let result = validate(&config, &option, AccessTokenType::User);
539        assert!(result.is_ok());
540    }
541
542    #[test]
543    fn test_validate_marketplace_tenant_no_key() {
544        let config = create_test_config_marketplace();
545        let option = RequestOption::default();
546
547        let result = validate(&config, &option, AccessTokenType::Tenant);
548        assert!(matches!(
549            result,
550            Err(crate::error::CoreError::Validation { .. })
551        ));
552    }
553
554    #[test]
555    fn test_validate_marketplace_tenant_with_key() {
556        let config = create_test_config_marketplace();
557        let mut option = RequestOption::default();
558        option.tenant_key = Some("tenant_key".to_string());
559
560        let result = validate(&config, &option, AccessTokenType::Tenant);
561        assert!(result.is_ok());
562    }
563
564    #[test]
565    fn test_validate_user_token_empty() {
566        let config = create_test_config();
567        let option = RequestOption::default();
568
569        let result = validate(&config, &option, AccessTokenType::User);
570        assert!(matches!(
571            result,
572            Err(crate::error::CoreError::Validation { .. })
573        ));
574    }
575
576    #[test]
577    fn test_validate_user_token_present() {
578        let config = create_test_config();
579        let mut option = RequestOption::default();
580        option.user_access_token = Some("user_token".to_string());
581
582        let result = validate(&config, &option, AccessTokenType::User);
583        assert!(result.is_ok());
584    }
585
586    #[test]
587    fn test_validate_forbidden_header_key_request_id() {
588        let config = create_test_config();
589        let mut option = RequestOption::default();
590        let mut header = HashMap::new();
591        header.insert(HTTP_HEADER_KEY_REQUEST_ID.to_string(), "test".to_string());
592        option.header = header;
593
594        let result = validate(&config, &option, AccessTokenType::None);
595        assert!(matches!(
596            result,
597            Err(crate::error::CoreError::Validation { .. })
598        ));
599    }
600
601    #[test]
602    fn test_validate_forbidden_header_request_id() {
603        let config = create_test_config();
604        let mut option = RequestOption::default();
605        let mut header = HashMap::new();
606        header.insert(HTTP_HEADER_REQUEST_ID.to_string(), "test".to_string());
607        option.header = header;
608
609        let result = validate(&config, &option, AccessTokenType::None);
610        assert!(matches!(
611            result,
612            Err(crate::error::CoreError::Validation { .. })
613        ));
614    }
615
616    #[test]
617    fn test_validate_valid_config() {
618        let config = create_test_config();
619        let option = RequestOption::default();
620
621        let result = validate(&config, &option, AccessTokenType::None);
622        assert!(result.is_ok());
623    }
624
625    #[test]
626    fn test_validate_no_cache_none_token_type() {
627        let config = Config::builder()
628            .app_id("test_app_id")
629            .app_secret("test_app_secret")
630            .enable_token_cache(false)
631            .build();
632        let option = RequestOption::default();
633
634        let result = validate(&config, &option, AccessTokenType::None);
635        assert!(result.is_ok());
636    }
637
638    #[test]
639    fn test_determine_token_type_first_is_tenant() {
640        let types = vec![AccessTokenType::Tenant, AccessTokenType::User];
641        let option = RequestOption::default();
642
643        let token_type = determine_token_type(&types, &option, true);
644        assert_eq!(token_type, AccessTokenType::Tenant);
645    }
646
647    #[test]
648    fn test_determine_token_type_no_tenant_in_list() {
649        let types = vec![AccessTokenType::User, AccessTokenType::App];
650        let option = RequestOption::default();
651
652        let token_type = determine_token_type(&types, &option, true);
653        // Should use first type when no Tenant type is available
654        assert_eq!(token_type, AccessTokenType::User);
655    }
656
657    #[test]
658    fn test_validate_token_type_edge_case_single_element() {
659        let types = vec![AccessTokenType::None];
660        let mut option = RequestOption::default();
661        option.user_access_token = Some("user_token".to_string());
662
663        let result = validate_token_type(&types, &option);
664        assert!(result.is_ok());
665    }
666
667    #[test]
668    fn test_decode_file_name_whitespace_handling() {
669        let raw = " attachment ; filename=\"test.txt\" ; filename*=UTF-8''spaced%20file.txt ";
670        let file_name = crate::content_disposition::extract_filename(raw).unwrap();
671        assert_eq!(file_name, "spaced%20file.txt");
672    }
673
674    #[test]
675    fn test_decode_file_name_no_equals() {
676        let raw = "attachment; filename*UTF-8''invalid.txt";
677        let file_name = crate::content_disposition::extract_filename(raw);
678        assert!(file_name.is_none());
679    }
680
681    // Additional comprehensive tests for better coverage
682
683    #[test]
684    fn test_validate_token_type_non_empty_list_ok() {
685        let types = vec![AccessTokenType::User, AccessTokenType::Tenant];
686        let option = RequestOption::default();
687
688        let result = validate_token_type(&types, &option);
689        assert!(result.is_ok());
690    }
691
692    #[test]
693    fn test_determine_token_type_priority_with_multiple_tokens() {
694        let types = vec![
695            AccessTokenType::User,
696            AccessTokenType::Tenant,
697            AccessTokenType::App,
698        ];
699        let mut option = RequestOption::default();
700        option.user_access_token = Some("user_token".to_string());
701        option.tenant_key = Some("tenant_key".to_string());
702
703        // User token should take priority when present
704        let token_type = determine_token_type(&types, &option, true);
705        assert_eq!(token_type, AccessTokenType::User);
706    }
707
708    #[test]
709    fn test_determine_token_type_tenant_key_without_tenant_type() {
710        let types = vec![AccessTokenType::User, AccessTokenType::App];
711        let mut option = RequestOption::default();
712        option.tenant_key = Some("tenant_key".to_string());
713
714        // Should default to first type when Tenant not available
715        let token_type = determine_token_type(&types, &option, true);
716        assert_eq!(token_type, AccessTokenType::User);
717    }
718
719    #[test]
720    fn test_determine_token_type_user_token_without_user_type() {
721        let types = vec![AccessTokenType::Tenant, AccessTokenType::App];
722        let mut option = RequestOption::default();
723        option.user_access_token = Some("user_token".to_string());
724
725        // Should use Tenant when User type not available
726        let token_type = determine_token_type(&types, &option, true);
727        assert_eq!(token_type, AccessTokenType::Tenant);
728    }
729
730    #[test]
731    fn test_determine_token_type_cache_disabled_fallback_priority() {
732        let types = vec![
733            AccessTokenType::User,
734            AccessTokenType::Tenant,
735            AccessTokenType::App,
736        ];
737        let mut option = RequestOption::default();
738        option.tenant_access_token = Some("tenant_token".to_string());
739        option.app_access_token = Some("app_token".to_string());
740
741        // Tenant should be chosen over App when cache disabled
742        let token_type = determine_token_type(&types, &option, false);
743        assert_eq!(token_type, AccessTokenType::Tenant);
744    }
745
746    #[test]
747    fn test_determine_token_type_cache_disabled_all_empty() {
748        let types = vec![AccessTokenType::User, AccessTokenType::Tenant];
749        let option = RequestOption::default();
750
751        // Should return None when no tokens provided and cache disabled
752        let token_type = determine_token_type(&types, &option, false);
753        assert_eq!(token_type, AccessTokenType::None);
754    }
755
756    #[test]
757    fn test_validate_config_with_all_required_fields() {
758        let config = Config::builder()
759            .app_id("valid_app_id")
760            .app_secret("valid_app_secret")
761            .enable_token_cache(true)
762            .build();
763        let option = RequestOption::default();
764
765        let result = validate(&config, &option, AccessTokenType::Tenant);
766        assert!(result.is_ok());
767    }
768
769    #[test]
770    fn test_validate_marketplace_app_with_valid_tenant_key() {
771        let config = Config::builder()
772            .app_id("marketplace_app")
773            .app_secret("marketplace_secret")
774            .app_type(AppType::Marketplace)
775            .build();
776        let mut option = RequestOption::default();
777        option.tenant_key = Some("valid_tenant_key".to_string());
778
779        let result = validate(&config, &option, AccessTokenType::Tenant);
780        assert!(result.is_ok());
781    }
782
783    #[test]
784    fn test_validate_marketplace_app_type_with_non_tenant_token() {
785        let config = Config::builder()
786            .app_id("marketplace_app")
787            .app_secret("marketplace_secret")
788            .app_type(AppType::Marketplace)
789            .build();
790        let mut option = RequestOption::default();
791        option.user_access_token = Some("user_token".to_string());
792
793        // User token type needs user_access_token to be present
794        let result = validate(&config, &option, AccessTokenType::User);
795        assert!(result.is_ok());
796
797        // App token type should pass without additional requirements for marketplace
798        let result = validate(&config, &RequestOption::default(), AccessTokenType::App);
799        assert!(result.is_ok());
800
801        // None token type should also pass
802        let result = validate(&config, &RequestOption::default(), AccessTokenType::None);
803        assert!(result.is_ok());
804    }
805
806    #[test]
807    fn test_validate_no_cache_with_multiple_token_types() {
808        let config = Config::builder()
809            .app_id("test_app")
810            .app_secret("test_secret")
811            .enable_token_cache(false)
812            .build();
813        let mut option = RequestOption::default();
814        option.user_access_token = Some("user_token".to_string());
815        option.tenant_access_token = Some("tenant_token".to_string());
816        option.app_access_token = Some("app_token".to_string());
817
818        // Should validate successfully with any token present
819        let result = validate(&config, &option, AccessTokenType::User);
820        assert!(result.is_ok());
821    }
822
823    #[test]
824    fn test_validate_user_token_type_with_empty_user_token() {
825        let config = create_test_config();
826        let mut option = RequestOption::default();
827        option.tenant_access_token = Some("tenant_token".to_string()); // Other tokens present
828
829        // Should fail when User token type but user_access_token is empty
830        let result = validate(&config, &option, AccessTokenType::User);
831        assert!(matches!(
832            result,
833            Err(crate::error::CoreError::Validation { .. })
834        ));
835        if let Err(crate::error::CoreError::Validation { message, .. }) = result {
836            assert!(message.contains("user access token is empty"));
837        }
838    }
839
840    #[test]
841    fn test_validate_forbidden_headers_custom_values() {
842        let config = create_test_config();
843        let mut option = RequestOption::default();
844        let mut header = HashMap::new();
845        header.insert("X-Request-Id".to_string(), "custom_id".to_string());
846        header.insert("Custom-Header".to_string(), "value".to_string());
847        option.header = header;
848
849        let result = validate(&config, &option, AccessTokenType::None);
850        assert!(matches!(
851            result,
852            Err(crate::error::CoreError::Validation { .. })
853        ));
854    }
855
856    #[test]
857    fn test_validate_forbidden_headers_request_id_variation() {
858        let config = create_test_config();
859        let mut option = RequestOption::default();
860        let mut header = HashMap::new();
861        header.insert("Request-Id".to_string(), "another_id".to_string());
862        option.header = header;
863
864        let result = validate(&config, &option, AccessTokenType::None);
865        assert!(matches!(
866            result,
867            Err(crate::error::CoreError::Validation { .. })
868        ));
869    }
870
871    #[test]
872    fn test_validate_allowed_custom_headers() {
873        let config = create_test_config();
874        let mut option = RequestOption::default();
875        let mut header = HashMap::new();
876        header.insert("Authorization".to_string(), "Bearer token".to_string());
877        header.insert("Content-Type".to_string(), "application/json".to_string());
878        header.insert("Custom-App-Header".to_string(), "value".to_string());
879        option.header = header;
880
881        let result = validate(&config, &option, AccessTokenType::None);
882        assert!(result.is_ok());
883    }
884
885    #[test]
886    fn test_decode_file_name_missing_utf8_prefix() {
887        let raw = "attachment; filename*=''missing_utf8.txt";
888        let file_name = crate::content_disposition::extract_filename(raw);
889        assert_eq!(file_name, Some("missing_utf8.txt".to_string()));
890    }
891
892    #[test]
893    fn test_decode_file_name_malformed_filename_star() {
894        let raw = "attachment; filename*=UTF-8";
895        let file_name = crate::content_disposition::extract_filename(raw);
896        assert_eq!(file_name, None);
897    }
898
899    #[test]
900    fn test_decode_file_name_multiple_filename_star_entries() {
901        let raw = "attachment; filename*=UTF-8''first.txt; filename*=UTF-8''second.txt";
902        let file_name = crate::content_disposition::extract_filename(raw).unwrap();
903        // Should return the first match
904        assert_eq!(file_name, "first.txt");
905    }
906
907    #[test]
908    fn test_decode_file_name_special_characters() {
909        let raw = "attachment; filename*=UTF-8''special%20%21%40%23.txt";
910        let file_name = crate::content_disposition::extract_filename(raw).unwrap();
911        assert_eq!(file_name, "special%20%21%40%23.txt");
912    }
913
914    #[test]
915    fn test_decode_file_name_empty_filename() {
916        let raw = "attachment; filename*=UTF-8''";
917        let file_name = crate::content_disposition::extract_filename(raw).unwrap();
918        assert_eq!(file_name, "");
919    }
920
921    #[test]
922    fn test_determine_token_type_empty_types_list_no_panic() {
923        let types: Vec<AccessTokenType> = vec![];
924        let option = RequestOption::default();
925
926        let token_type = determine_token_type(&types, &option, true);
927        assert_eq!(token_type, AccessTokenType::None);
928    }
929
930    #[test]
931    fn test_determine_token_type_empty_types_list_no_cache() {
932        // When cache is disabled, empty types list works fine
933        let types: Vec<AccessTokenType> = vec![];
934        let option = RequestOption::default();
935
936        // This works because cache-disabled path returns None without accessing types[0]
937        let token_type = determine_token_type(&types, &option, false);
938        assert_eq!(token_type, AccessTokenType::None);
939    }
940
941    #[test]
942    fn test_determine_token_type_single_app_type() {
943        let types = vec![AccessTokenType::App];
944        let option = RequestOption::default();
945
946        let token_type = determine_token_type(&types, &option, true);
947        assert_eq!(token_type, AccessTokenType::App);
948    }
949
950    #[test]
951    fn test_determine_token_type_single_none_type() {
952        let types = vec![AccessTokenType::None];
953        let option = RequestOption::default();
954
955        let token_type = determine_token_type(&types, &option, true);
956        assert_eq!(token_type, AccessTokenType::None);
957    }
958
959    #[test]
960    fn test_validate_with_cache_enabled_various_token_types() {
961        let config = Config::builder()
962            .app_id("test_app")
963            .app_secret("test_secret")
964            .enable_token_cache(true)
965            .build();
966        let option = RequestOption::default();
967
968        // With cache enabled, should validate OK for all token types
969        assert!(validate(&config, &option, AccessTokenType::None).is_ok());
970        assert!(validate(&config, &option, AccessTokenType::App).is_ok());
971        assert!(validate(&config, &option, AccessTokenType::Tenant).is_ok());
972    }
973
974    #[test]
975    fn test_validate_self_build_app_type_with_tenant_token() {
976        let config = Config::builder()
977            .app_id("self_build_app")
978            .app_secret("self_build_secret")
979            .app_type(AppType::SelfBuild)
980            .build();
981        let option = RequestOption::default();
982
983        // Self-build apps should validate OK without tenant_key
984        let result = validate(&config, &option, AccessTokenType::Tenant);
985        assert!(result.is_ok());
986    }
987
988    // Test edge cases for the validate_token_type logic paths
989
990    #[test]
991    fn test_validate_token_type_with_mismatched_tokens_simulation() {
992        // types 与 option 中显式 token 类型不匹配时,应返回错误(避免静默放过错误 token)。
993        let types = vec![AccessTokenType::Tenant];
994        let mut option = RequestOption::default();
995        option.user_access_token = Some("user_token".to_string()); // Mismatch!
996
997        let result = validate_token_type(&types, &option);
998        assert!(result.is_err());
999    }
1000
1001    #[test]
1002    fn test_validate_comprehensive_error_messages() {
1003        let config_empty_id = Config::builder().app_id("").app_secret("secret").build();
1004
1005        let config_empty_secret = Config::builder().app_id("app_id").app_secret("").build();
1006
1007        let option = RequestOption::default();
1008
1009        // Test specific error messages
1010        if let Err(crate::error::CoreError::Validation { message, .. }) =
1011            validate(&config_empty_id, &option, AccessTokenType::None)
1012        {
1013            assert_eq!(message, "AppId is empty");
1014        } else {
1015            panic!("Expected IllegalParamError for empty app_id");
1016        }
1017
1018        if let Err(crate::error::CoreError::Validation { message, .. }) =
1019            validate(&config_empty_secret, &option, AccessTokenType::None)
1020        {
1021            assert_eq!(message, "AppSecret is empty");
1022        } else {
1023            panic!("Expected IllegalParamError for empty app_secret");
1024        }
1025    }
1026}