Skip to main content

openlark_core/
http.rs

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