Skip to main content

openlark_client/
config.rs

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