Skip to main content

openlark_client/
config.rs

1//! OpenLark Client 配置管理
2//!
3//! 提供灵活的配置系统,支持环境变量、验证和默认值
4
5use crate::Result;
6use std::time::Duration;
7
8use openlark_core::config::Config as CoreConfig;
9use openlark_core::constants::AppType;
10
11/// Check if the base_url points to a known Lark/Feishu domain
12fn is_known_base_url(url: &str) -> bool {
13    let allowed_suffixes = ["feishu.cn", "larksuite.com", "larkoffice.com"];
14    // Parse URL, extract host, check suffix
15    if let Ok(parsed) = url::Url::parse(url)
16        && let Some(host) = parsed.host_str()
17    {
18        return allowed_suffixes
19            .iter()
20            .any(|suffix| host == *suffix || host.ends_with(&format!(".{suffix}")));
21    }
22    false
23}
24
25/// 🔧 OpenLark客户端配置
26///
27/// 支持从环境变量自动加载配置
28///
29/// # 环境变量
30/// - `OPENLARK_APP_ID`: 应用ID(必需)
31/// - `OPENLARK_APP_SECRET`: 应用密钥(必需)
32/// - `OPENLARK_APP_TYPE`: 应用类型(可选:self_build / marketplace,默认 self_build)
33/// - `OPENLARK_BASE_URL`: API基础URL(可选,默认:`https://open.feishu.cn`,国际版 Lark 使用 `https://open.larksuite.com`)
34/// - `OPENLARK_ENABLE_TOKEN_CACHE`: 是否允许自动获取 token(可选,默认 true)
35///
36/// # 示例
37/// ```rust,no_run
38/// use openlark_client::Config;
39///
40/// // 从环境变量创建配置
41/// let config = Config::from_env();
42///
43/// // 手动构建配置
44/// let config = Config::builder()
45///     .app_id("your_app_id")
46///     .app_secret("your_app_secret")
47///     .base_url("https://open.feishu.cn")  // 默认值,国际版 Lark 使用 https://open.larksuite.com
48///     .build();
49/// ```
50#[derive(Clone)]
51pub struct Config {
52    /// 🆔 飞书应用ID
53    pub app_id: String,
54    /// 🔑 飞书应用密钥
55    pub app_secret: String,
56    /// 🏷️ 应用类型(自建 / 商店)
57    pub app_type: AppType,
58    /// 🔐 是否允许 SDK 自动获取 token(通过 openlark-core 的 TokenProvider)
59    pub enable_token_cache: bool,
60    /// 🌐 API基础URL
61    pub base_url: String,
62    /// 🔓 是否允许自定义 base_url 域名
63    pub allow_custom_base_url: bool,
64    /// ⏱️ 请求超时时间
65    pub timeout: Duration,
66    /// 🔄 默认重试次数
67    pub retry_count: u32,
68    /// 📝 是否启用日志记录
69    pub enable_log: bool,
70    /// 📋 自定义HTTP headers
71    pub headers: std::collections::HashMap<String, String>,
72    /// 响应体最大大小限制(字节),默认 100MB
73    pub max_response_size: u64,
74    /// 🔧 底层 core 配置(按需生成)
75    #[doc(hidden)]
76    pub(crate) core_config: Option<CoreConfig>,
77}
78
79impl std::fmt::Debug for Config {
80    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
81        f.debug_struct("Config")
82            .field("app_id", &self.app_id)
83            .field("app_secret", &"***")
84            .field("app_type", &self.app_type)
85            .field("enable_token_cache", &self.enable_token_cache)
86            .field("base_url", &self.base_url)
87            .field("timeout", &self.timeout)
88            .field("retry_count", &self.retry_count)
89            .field("enable_log", &self.enable_log)
90            .field("headers", &format!("{} headers", self.headers.len()))
91            .field("max_response_size", &self.max_response_size)
92            .finish()
93    }
94}
95
96impl Default for Config {
97    fn default() -> Self {
98        Self {
99            app_id: String::new(),
100            app_secret: String::new(),
101            app_type: AppType::SelfBuild,
102            enable_token_cache: true,
103            base_url: "https://open.feishu.cn".to_string(),
104            allow_custom_base_url: false,
105            timeout: Duration::from_secs(30),
106            retry_count: 3,
107            enable_log: true,
108            headers: std::collections::HashMap::new(),
109            max_response_size: 100 * 1024 * 1024, // 100MB
110            core_config: None,
111        }
112    }
113}
114
115impl Config {
116    /// 🆕 创建新的配置实例
117    pub fn new() -> Self {
118        Self::default()
119    }
120
121    /// 🌍 从环境变量创建配置
122    ///
123    /// # 环境变量
124    /// - `OPENLARK_APP_ID`: 应用ID(必需)
125    /// - `OPENLARK_APP_SECRET`: 应用密钥(必需)
126    /// - `OPENLARK_APP_TYPE`: 应用类型(可选:self_build / marketplace)
127    /// - `OPENLARK_BASE_URL`: API基础URL(可选,默认 `https://open.feishu.cn`,国际版 Lark 使用 `https://open.larksuite.com`)
128    /// - `OPENLARK_ENABLE_TOKEN_CACHE`: 是否允许自动获取 token(可选)
129    ///
130    /// # 返回值
131    /// 返回配置实例,环境变量缺失时使用默认值
132    pub fn from_env() -> Self {
133        let mut config = Self::default();
134        config.load_from_env();
135        config
136    }
137
138    /// 📥 从环境变量加载配置到当前实例
139    ///
140    /// 只设置存在且非空的环境变量
141    pub fn load_from_env(&mut self) {
142        for (key, value) in std::env::vars() {
143            self.apply_env_var(&key, &value);
144        }
145    }
146
147    fn apply_env_var(&mut self, key: &str, value: &str) {
148        match key {
149            "OPENLARK_APP_ID" if !value.is_empty() => self.app_id = value.to_string(),
150            "OPENLARK_APP_SECRET" if !value.is_empty() => self.app_secret = value.to_string(),
151            "OPENLARK_APP_TYPE" => {
152                let v = value.trim().to_lowercase();
153                match v.as_str() {
154                    "self_build" | "selfbuild" | "self" => self.app_type = AppType::SelfBuild,
155                    "marketplace" | "store" => self.app_type = AppType::Marketplace,
156                    _ => {}
157                }
158            }
159            "OPENLARK_BASE_URL" if !value.is_empty() => self.base_url = value.to_string(),
160            "OPENLARK_ENABLE_TOKEN_CACHE" => {
161                let s = value.trim().to_lowercase();
162                if !s.is_empty() {
163                    self.enable_token_cache = !(s.starts_with('f') || s == "0");
164                }
165            }
166            "OPENLARK_TIMEOUT" => {
167                if let Ok(timeout_secs) = value.parse::<u64>() {
168                    self.timeout = Duration::from_secs(timeout_secs);
169                }
170            }
171            "OPENLARK_RETRY_COUNT" => {
172                if let Ok(retry_count) = value.parse::<u32>() {
173                    self.retry_count = retry_count;
174                }
175            }
176            "OPENLARK_MAX_RESPONSE_SIZE" => {
177                if let Ok(size) = value.parse::<u64>() {
178                    self.max_response_size = size;
179                }
180            }
181            // 日志开关(默认启用,只有设置为"false"时才禁用)
182            "OPENLARK_ENABLE_LOG" => {
183                self.enable_log = !value.to_lowercase().starts_with('f');
184            }
185            _ => {}
186        }
187    }
188
189    /// ✅ 验证配置的有效性
190    ///
191    /// # 验证规则
192    /// - app_id和app_secret不能为空
193    /// - base_url必须是有效的HTTP/HTTPS URL
194    /// - timeout必须大于0
195    /// - retry_count不能超过合理的范围
196    ///
197    /// # 错误
198    /// 返回验证失败的详细错误信息
199    pub fn validate(&self) -> Result<()> {
200        if self.app_id.is_empty() {
201            return Err(crate::error::validation_error("app_id", "app_id不能为空"));
202        }
203
204        if self.app_secret.is_empty() {
205            return Err(crate::error::validation_error(
206                "app_secret",
207                "app_secret不能为空",
208            ));
209        }
210
211        if self.base_url.is_empty() {
212            return Err(crate::error::validation_error(
213                "base_url",
214                "base_url不能为空",
215            ));
216        }
217
218        // 验证URL格式
219        if !self.base_url.starts_with("http://") && !self.base_url.starts_with("https://") {
220            return Err(crate::error::validation_error(
221                "base_url",
222                "base_url必须以http://或https://开头",
223            ));
224        }
225
226        // Check base_url domain against whitelist
227        if !self.allow_custom_base_url && !is_known_base_url(&self.base_url) {
228            tracing::warn!(
229                "base_url '{}' is not a known Feishu/Lark domain.
230                If this is intentional, set allow_custom_base_url(true) in config.",
231                self.base_url
232            );
233            return Err(crate::error::validation_error(
234                "base_url",
235                "base_url 域名不在白名单中,已知域名: *.feishu.cn, *.larksuite.com, *.larkoffice.com。如需使用自定义域名,请设置 allow_custom_base_url(true)",
236            ));
237        }
238
239        // 验证超时时间
240        if self.timeout.is_zero() {
241            return Err(crate::error::validation_error(
242                "timeout",
243                "timeout必须大于0",
244            ));
245        }
246
247        // 验证重试次数
248        if self.retry_count > 10 {
249            return Err(crate::error::validation_error(
250                "retry_count",
251                "retry_count不能超过10",
252            ));
253        }
254
255        Ok(())
256    }
257
258    /// 🏗️ 创建配置构建器
259    pub fn builder() -> ConfigBuilder {
260        ConfigBuilder::new()
261    }
262
263    /// 🔧 添加自定义HTTP header
264    pub fn add_header<K, V>(&mut self, key: K, value: V)
265    where
266        K: Into<String>,
267        V: Into<String>,
268    {
269        self.headers.insert(key.into(), value.into());
270    }
271
272    /// 🧹 清除所有自定义headers
273    pub fn clear_headers(&mut self) {
274        self.headers.clear();
275    }
276
277    /// 🔍 检查配置是否完整(可用于API调用)
278    pub fn is_complete(&self) -> bool {
279        !self.app_id.is_empty() && !self.app_secret.is_empty()
280    }
281
282    /// 📋 获取配置摘要(不包含敏感信息)
283    pub fn summary(&self) -> ConfigSummary {
284        ConfigSummary {
285            app_id: self.app_id.clone(),
286            app_secret_set: !self.app_secret.is_empty(),
287            app_type: self.app_type,
288            enable_token_cache: self.enable_token_cache,
289            base_url: self.base_url.clone(),
290            allow_custom_base_url: self.allow_custom_base_url,
291            timeout: self.timeout,
292            retry_count: self.retry_count,
293            enable_log: self.enable_log,
294            header_count: self.headers.len(),
295            max_response_size: self.max_response_size,
296        }
297    }
298
299    /// 🔄 更新配置,只更新非空字段
300    pub fn update_with(&mut self, other: &Config) {
301        if !other.app_id.is_empty() {
302            self.app_id = other.app_id.clone();
303        }
304        if !other.app_secret.is_empty() {
305            self.app_secret = other.app_secret.clone();
306        }
307        if other.app_type != AppType::SelfBuild {
308            self.app_type = other.app_type;
309        }
310        if other.enable_token_cache != self.enable_token_cache {
311            self.enable_token_cache = other.enable_token_cache;
312        }
313        if !other.base_url.is_empty() {
314            self.base_url = other.base_url.clone();
315        }
316        if !other.timeout.is_zero() {
317            self.timeout = other.timeout;
318        }
319        if other.retry_count != 3 {
320            self.retry_count = other.retry_count;
321        }
322        if other.max_response_size != 100 * 1024 * 1024 {
323            self.max_response_size = other.max_response_size;
324        }
325        if other.enable_log != self.enable_log {
326            self.enable_log = other.enable_log;
327        }
328        // 合并headers
329        for (key, value) in &other.headers {
330            self.headers.insert(key.clone(), value.clone());
331        }
332    }
333
334    /// 🔧 构建底层 core 配置(不含 TokenProvider)
335    pub fn build_core_config(&self) -> CoreConfig {
336        CoreConfig::builder()
337            .app_id(self.app_id.clone())
338            .app_secret(self.app_secret.clone())
339            .base_url(self.base_url.clone())
340            .app_type(self.app_type)
341            .enable_token_cache(self.enable_token_cache)
342            .req_timeout(self.timeout)
343            .max_response_size(self.max_response_size)
344            .header(self.headers.clone())
345            .build()
346    }
347
348    /// 🔧 构建带有默认 TokenProvider 的 core 配置
349    #[cfg(feature = "auth")]
350    pub fn build_core_config_with_token_provider(&self) -> CoreConfig {
351        use openlark_auth::AuthTokenProvider;
352        let base_config = self.build_core_config();
353        let provider = AuthTokenProvider::new(base_config.clone());
354        base_config.with_token_provider(provider)
355    }
356
357    /// 🔧 获取或构建 core 配置
358    pub fn get_or_build_core_config(&self) -> CoreConfig {
359        if let Some(ref core_config) = self.core_config {
360            return core_config.clone();
361        }
362        self.build_core_config()
363    }
364
365    /// 🔧 获取或构建带有 TokenProvider 的 core 配置
366    #[cfg(feature = "auth")]
367    pub fn get_or_build_core_config_with_token_provider(&self) -> CoreConfig {
368        if let Some(ref core_config) = self.core_config {
369            return core_config.clone();
370        }
371        self.build_core_config_with_token_provider()
372    }
373}
374
375/// 🏗️ 配置构建器 - 提供流畅的API
376///
377/// # 示例
378/// ```rust,no_run
379/// use openlark_client::Config;
380/// use std::time::Duration;
381///
382/// let config = Config::builder()
383///     .app_id("your_app_id")
384///     .app_secret("your_app_secret")
385///     .base_url("https://open.feishu.cn")
386///     .timeout(Duration::from_secs(60))
387///     .retry_count(5)
388///     .enable_log(true)
389///     .add_header("X-Custom-Header", "value")
390///     .build();
391/// ```
392#[derive(Debug, Clone)]
393pub struct ConfigBuilder {
394    config: Config,
395}
396
397impl ConfigBuilder {
398    /// 🆕 创建新的构建器
399    pub fn new() -> Self {
400        Self {
401            config: Config::default(),
402        }
403    }
404
405    /// 🆔 设置应用ID
406    pub fn app_id<S: Into<String>>(mut self, app_id: S) -> Self {
407        self.config.app_id = app_id.into();
408        self
409    }
410
411    /// 🔑 设置应用密钥
412    pub fn app_secret<S: Into<String>>(mut self, app_secret: S) -> Self {
413        self.config.app_secret = app_secret.into();
414        self
415    }
416
417    /// 🏷️ 设置应用类型(自建 / 商店)
418    pub fn app_type(mut self, app_type: AppType) -> Self {
419        self.config.app_type = app_type;
420        self
421    }
422
423    /// 🔐 设置是否允许自动获取 token(默认 true)
424    pub fn enable_token_cache(mut self, enable: bool) -> Self {
425        self.config.enable_token_cache = enable;
426        self
427    }
428
429    /// 🌐 设置API基础URL
430    pub fn base_url<S: Into<String>>(mut self, base_url: S) -> Self {
431        self.config.base_url = base_url.into();
432        self
433    }
434
435    /// ⏱️ 设置请求超时时间
436    pub fn timeout(mut self, timeout: Duration) -> Self {
437        self.config.timeout = timeout;
438        self
439    }
440
441    /// 🔄 设置重试次数
442    pub fn retry_count(mut self, count: u32) -> Self {
443        self.config.retry_count = count;
444        self
445    }
446
447    /// 📝 设置是否启用日志
448    pub fn enable_log(mut self, enable: bool) -> Self {
449        self.config.enable_log = enable;
450        self
451    }
452
453    /// 设置响应体最大大小限制(字节),默认 100MB
454    pub fn max_response_size(mut self, size: u64) -> Self {
455        self.config.max_response_size = size;
456        self
457    }
458
459    /// 🔧 添加自定义HTTP header
460    pub fn add_header<K, V>(mut self, key: K, value: V) -> Self
461    where
462        K: Into<String>,
463        V: Into<String>,
464    {
465        self.config.add_header(key, value);
466        self
467    }
468
469    /// 🌍 从环境变量加载配置
470    pub fn from_env(mut self) -> Self {
471        self.config.load_from_env();
472        self
473    }
474
475    /// 🔨 构建最终的配置实例
476    ///
477    /// # 错误
478    /// 如果配置验证失败,返回相应的错误
479    pub fn build(self) -> Result<Config> {
480        self.config.validate()?;
481        Ok(self.config)
482    }
483
484    /// 🔨 构建配置实例(跳过验证)
485    ///
486    /// 注意:使用此方法时配置可能无效,后续使用时需要验证
487    pub fn build_unvalidated(self) -> Config {
488        self.config
489    }
490}
491
492impl Default for ConfigBuilder {
493    fn default() -> Self {
494        Self::new()
495    }
496}
497
498/// 📋 配置摘要(不包含敏感信息)
499#[derive(Debug, Clone)]
500pub struct ConfigSummary {
501    /// 🆔 应用ID
502    pub app_id: String,
503    /// 🔑 应用密钥是否已设置
504    pub app_secret_set: bool,
505    /// 🏷️ 应用类型
506    pub app_type: AppType,
507    /// 🔐 是否允许自动获取 token
508    pub enable_token_cache: bool,
509    /// 🌐 API基础URL
510    pub base_url: String,
511    /// 🔓 是否允许自定义 base_url 域名
512    pub allow_custom_base_url: bool,
513    /// ⏱️ 请求超时时间
514    pub timeout: Duration,
515    /// 🔄 重试次数
516    pub retry_count: u32,
517    /// 📝 是否启用日志
518    pub enable_log: bool,
519    /// 📋 自定义headers数量
520    pub header_count: usize,
521    /// 响应体最大大小限制
522    pub max_response_size: u64,
523}
524
525impl ConfigSummary {
526    /// 📋 获取友好的配置描述
527    pub fn friendly_description(&self) -> String {
528        format!(
529            "应用ID: {}, 基础URL: {}, 超时: {:?}, 重试: {}, 日志: {}, Headers: {}, 最大响应: {}",
530            self.app_id,
531            self.base_url,
532            self.timeout,
533            self.retry_count,
534            if self.enable_log { "启用" } else { "禁用" },
535            self.header_count,
536            self.max_response_size
537        )
538    }
539}
540
541impl std::fmt::Display for ConfigSummary {
542    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
543        write!(
544            f,
545            "Config {{ app_id: {}, app_secret_set: {}, base_url: {}, timeout: {:?}, retry_count: {}, enable_log: {}, header_count: {}, max_response_size: {} }}",
546            self.app_id,
547            self.app_secret_set,
548            self.base_url,
549            self.timeout,
550            self.retry_count,
551            self.enable_log,
552            self.header_count,
553            self.max_response_size
554        )
555    }
556}
557
558/// 从环境变量创建配置的便利方法
559impl From<std::env::Vars> for Config {
560    fn from(env_vars: std::env::Vars) -> Self {
561        let mut config = Config::default();
562        for (key, value) in env_vars {
563            config.apply_env_var(&key, &value);
564        }
565        config
566    }
567}
568
569#[cfg(test)]
570#[allow(unused_imports)]
571mod tests {
572    use super::*;
573    use std::time::Duration;
574
575    #[test]
576    fn test_config_default() {
577        let config = Config::default();
578        assert_eq!(config.app_id, "");
579        assert_eq!(config.app_secret, "");
580        assert_eq!(config.base_url, "https://open.feishu.cn");
581        assert_eq!(config.timeout, Duration::from_secs(30));
582        assert_eq!(config.retry_count, 3);
583        assert!(config.enable_log);
584        assert!(config.headers.is_empty());
585    }
586
587    #[test]
588    fn test_config_builder() {
589        let config = Config::builder()
590            .app_id("test_app_id")
591            .app_secret("test_app_secret")
592            .base_url("https://test.feishu.cn")
593            .timeout(Duration::from_secs(60))
594            .retry_count(5)
595            .enable_log(false)
596            .build();
597
598        assert!(config.is_ok());
599        let config = config.unwrap();
600        assert_eq!(config.app_id, "test_app_id");
601        assert_eq!(config.app_secret, "test_app_secret");
602        assert_eq!(config.base_url, "https://test.feishu.cn");
603        assert_eq!(config.timeout, Duration::from_secs(60));
604        assert_eq!(config.retry_count, 5);
605        assert!(!config.enable_log);
606    }
607
608    #[test]
609    fn test_config_from_env() {
610        crate::test_utils::with_env_vars(
611            &[
612                ("OPENLARK_APP_ID", Some("test_app_id")),
613                ("OPENLARK_APP_SECRET", Some("test_app_secret")),
614                ("OPENLARK_APP_TYPE", Some("marketplace")),
615                ("OPENLARK_BASE_URL", Some("https://test.feishu.cn")),
616                ("OPENLARK_ENABLE_TOKEN_CACHE", Some("false")),
617                ("OPENLARK_TIMEOUT", Some("60")),
618                ("OPENLARK_RETRY_COUNT", Some("5")),
619                ("OPENLARK_ENABLE_LOG", Some("false")),
620            ],
621            || {
622                let config = Config::from_env();
623                assert_eq!(config.app_id, "test_app_id");
624                assert_eq!(config.app_secret, "test_app_secret");
625                assert_eq!(config.app_type, AppType::Marketplace);
626                assert_eq!(config.base_url, "https://test.feishu.cn");
627                assert!(!config.enable_token_cache);
628                assert_eq!(config.timeout, Duration::from_secs(60));
629                assert_eq!(config.retry_count, 5);
630                assert!(!config.enable_log);
631            },
632        );
633    }
634
635    #[test]
636    fn test_config_validation() {
637        // 有效配置
638        let config = Config {
639            app_id: "test_app_id".to_string(),
640            app_secret: "test_app_secret".to_string(),
641            app_type: AppType::SelfBuild,
642            enable_token_cache: true,
643            base_url: "https://open.feishu.cn".to_string(),
644            allow_custom_base_url: false,
645            timeout: Duration::from_secs(30),
646            retry_count: 3,
647            enable_log: true,
648            headers: std::collections::HashMap::new(),
649            max_response_size: 100 * 1024 * 1024,
650            core_config: None,
651        };
652        assert!(config.validate().is_ok());
653
654        // 无效配置 - 空app_id
655        let invalid_config = Config {
656            app_id: String::new(),
657            ..config.clone()
658        };
659        assert!(invalid_config.validate().is_err());
660
661        // 无效配置 - 空app_secret
662        let invalid_config = Config {
663            app_secret: String::new(),
664            ..config
665        };
666        assert!(invalid_config.validate().is_err());
667    }
668
669    #[test]
670    fn test_config_headers() {
671        let mut config = Config::default();
672
673        // 添加header
674        config.add_header("X-Custom-Header", "custom-value");
675        assert_eq!(config.headers.len(), 1);
676        assert_eq!(
677            config.headers.get("X-Custom-Header"),
678            Some(&"custom-value".to_string())
679        );
680
681        // 清除headers
682        config.clear_headers();
683        assert!(config.headers.is_empty());
684    }
685
686    #[test]
687    fn test_config_update_with() {
688        let mut base_config = Config::default();
689        let update_config = Config {
690            app_id: "updated_app_id".to_string(),
691            app_secret: "updated_app_secret".to_string(),
692            timeout: Duration::from_secs(60),
693            ..Default::default()
694        };
695
696        base_config.update_with(&update_config);
697        assert_eq!(base_config.app_id, "updated_app_id");
698        assert_eq!(base_config.app_secret, "updated_app_secret");
699        assert_eq!(base_config.timeout, Duration::from_secs(60));
700        // 其他字段保持默认值
701        assert_eq!(base_config.base_url, "https://open.feishu.cn");
702    }
703
704    #[test]
705    fn test_config_summary() {
706        let config = Config {
707            app_id: "test_app_id".to_string(),
708            app_secret: "test_app_secret".to_string(),
709            app_type: AppType::SelfBuild,
710            enable_token_cache: true,
711            base_url: "https://open.feishu.cn".to_string(),
712            allow_custom_base_url: false,
713            timeout: Duration::from_secs(30),
714            retry_count: 3,
715            enable_log: true,
716            headers: std::collections::HashMap::new(),
717            max_response_size: 100 * 1024 * 1024,
718            core_config: None,
719        };
720
721        let summary = config.summary();
722        assert_eq!(summary.app_id, "test_app_id");
723        assert!(summary.app_secret_set);
724        assert_eq!(summary.base_url, "https://open.feishu.cn");
725        assert_eq!(summary.timeout, Duration::from_secs(30));
726        assert_eq!(summary.retry_count, 3);
727        assert!(summary.enable_log);
728        assert_eq!(summary.header_count, 0);
729    }
730
731    #[test]
732    fn test_config_is_complete() {
733        let mut config = Config::default();
734        assert!(!config.is_complete());
735
736        config.app_id = "test_app_id".to_string();
737        assert!(!config.is_complete());
738
739        config.app_secret = "test_app_secret".to_string();
740        assert!(config.is_complete());
741    }
742}
743
744#[test]
745fn test_config_validation_known_base_url() {
746    // 测试已知的 Feishu/Lark 域名
747    let known_urls = vec![
748        "https://open.feishu.cn",
749        "https://api.feishu.cn",
750        "https://custom.feishu.cn",
751        "https://open.larksuite.com",
752        "https://api.larksuite.com",
753        "https://custom.larksuite.com",
754        "https://open.larkoffice.com",
755        "https://custom.larkoffice.com",
756    ];
757
758    for url in known_urls {
759        let config = Config {
760            app_id: "test_app_id".to_string(),
761            app_secret: "test_app_secret".to_string(),
762            app_type: AppType::SelfBuild,
763            enable_token_cache: true,
764            base_url: url.to_string(),
765            allow_custom_base_url: false,
766            timeout: Duration::from_secs(30),
767            retry_count: 3,
768            enable_log: true,
769            headers: std::collections::HashMap::new(),
770            max_response_size: 100 * 1024 * 1024,
771            core_config: None,
772        };
773        assert!(config.validate().is_ok(), "URL {url} should be valid");
774    }
775}
776
777#[test]
778fn test_config_validation_unknown_base_url_rejected() {
779    // 测试未知域名被拒绝
780    let unknown_urls = vec![
781        "https://evil.com",
782        "https://malicious-site.com",
783        "https://example.com",
784        "https://not-feishu.cn",
785        "https://fake-larksuite.com",
786    ];
787
788    for url in unknown_urls {
789        let config = Config {
790            app_id: "test_app_id".to_string(),
791            app_secret: "test_app_secret".to_string(),
792            app_type: AppType::SelfBuild,
793            enable_token_cache: true,
794            base_url: url.to_string(),
795            allow_custom_base_url: false,
796            timeout: Duration::from_secs(30),
797            retry_count: 3,
798            enable_log: true,
799            headers: std::collections::HashMap::new(),
800            max_response_size: 100 * 1024 * 1024,
801            core_config: None,
802        };
803        assert!(config.validate().is_err(), "URL {url} should be rejected");
804    }
805}
806
807#[test]
808fn test_config_validation_custom_base_url_allowed() {
809    // 测试 allow_custom_base_url 允许使用自定义域名
810    let config = Config {
811        app_id: "test_app_id".to_string(),
812        app_secret: "test_app_secret".to_string(),
813        app_type: AppType::SelfBuild,
814        enable_token_cache: true,
815        base_url: "https://open.feishu.cn".to_string(),
816        allow_custom_base_url: true,
817        timeout: Duration::from_secs(30),
818        retry_count: 3,
819        enable_log: true,
820        headers: std::collections::HashMap::new(),
821        max_response_size: 100 * 1024 * 1024,
822        core_config: None,
823    };
824    assert!(config.validate().is_ok());
825}