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