openlark 0.15.0

飞书开放平台 Rust SDK - 企业级高覆盖率 API 客户端,极简依赖一条命令
Documentation
use std::time::Duration;

use openlark_auth::auth::auth::v3::*;
use openlark_core::{
    api::responses::{RawResponse, Response},
    config::Config,
    error::{timeout_error, CoreError, ErrorTrait},
};

fn create_test_config(base_url: &str, timeout_ms: u64) -> Config {
    Config::builder()
        .app_id("test_app_id")
        .app_secret("test_app_secret")
        .base_url(base_url)
        .req_timeout(Duration::from_millis(timeout_ms))
        .build()
}

fn macro_required_check(value: &str) -> openlark_core::SDKResult<()> {
    openlark_core::validate_required!(value, "字段不能为空");
    Ok(())
}

#[cfg(test)]
mod validation_tests {
    use super::*;

    #[test]
    fn test_validate_required_empty_string() {
        assert!(openlark_core::Validatable::is_empty_trimmed(&""));
    }

    #[test]
    fn test_validate_required_whitespace_only() {
        assert!(openlark_core::Validatable::is_empty_trimmed(&"   \t\n"));
    }

    #[test]
    fn test_validate_required_non_empty_after_trim() {
        assert!(!openlark_core::Validatable::is_empty_trimmed(&"  app_123  "));
    }

    #[test]
    fn test_validate_required_special_characters_valid() {
        assert!(!openlark_core::Validatable::is_empty_trimmed(&"!@#$%^&*()"));
    }

    #[test]
    fn test_validate_required_unicode_valid() {
        assert!(!openlark_core::Validatable::is_empty_trimmed(&"应用ID-测试"));
    }

    #[test]
    fn test_validate_required_macro_empty_returns_validation_error() {
        let result = macro_required_check("");
        assert!(matches!(result, Err(CoreError::Validation { .. })));
        let err = result.expect_err("应返回验证错误");
        assert_eq!(err.to_string(), "验证错误 general: 字段不能为空");
    }

    #[test]
    fn test_validate_required_macro_whitespace_not_trimmed() {
        let result = macro_required_check("   ");
        assert!(result.is_ok());
    }

    #[test]
    fn test_api_error_response_parsing() {
        let raw: RawResponse = serde_json::from_str(
            r#"{
                "code": 99991663,
                "msg": "invalid app credentials",
                "request_id": "req_123"
            }"#,
        )
        .expect("错误响应 JSON 应可解析");

        assert_eq!(raw.code, 99991663);
        assert_eq!(raw.msg, "invalid app credentials");
        assert_eq!(raw.request_id.as_deref(), Some("req_123"));
        assert!(!raw.is_success());
    }

    #[test]
    fn test_api_error_response_into_result_returns_api_error() {
        let response = Response::<serde_json::Value>::new(
            None,
            RawResponse {
                code: 400,
                msg: "bad request".to_string(),
                request_id: Some("req_bad".to_string()),
                data: None,
                error: None,
            },
        );

        let result = response.into_result();
        assert!(matches!(result, Err(CoreError::Api(_))));

        let err = result.expect_err("应返回 API 错误");
        assert!(err.to_string().contains("bad request"));
    }

    #[test]
    fn test_success_response_without_data_returns_api_error() {
        let response = Response::<serde_json::Value>::new(
            None,
            RawResponse {
                code: 0,
                msg: "ok".to_string(),
                request_id: Some("req_empty_data".to_string()),
                data: None,
                error: None,
            },
        );

        let result = response.into_result();
        assert!(matches!(result, Err(CoreError::Api(_))));
        let err = result.expect_err("应返回 API 错误");
        assert!(err.to_string().contains("响应数据为空"));
    }

    #[tokio::test]
    async fn test_app_access_token_network_error_handling() {
        let config = create_test_config("http://nonexistent.invalid", 300);
        let service = AuthServiceV3::new(config);

        let result = service
            .app_access_token()
            .app_id("valid_app_id")
            .app_secret("valid_app_secret")
            .execute()
            .await;

        assert!(result.is_err());
    }

    #[tokio::test]
    async fn test_whitespace_app_id_not_blocked_by_macro_and_fails_on_network() {
        let config = create_test_config("http://nonexistent.invalid", 300);
        let service = AuthServiceV3::new(config);

        let result = service
            .app_access_token()
            .app_id("   ")
            .app_secret("valid_app_secret")
            .execute()
            .await;

        assert!(result.is_err());
    }

    #[tokio::test]
    async fn test_empty_app_id_returns_validation_error_before_network() {
        let config = create_test_config("http://127.0.0.1:1", 300);
        let service = AuthServiceV3::new(config);

        let result = service
            .app_access_token()
            .app_id("")
            .app_secret("valid_app_secret")
            .execute()
            .await;

        assert!(matches!(result, Err(CoreError::Validation { .. })));
        let err = result.expect_err("应返回验证错误");
        assert!(err.to_string().contains("应用ID不能为空"));
    }

    #[tokio::test]
    async fn test_empty_app_secret_returns_validation_error_before_network() {
        let config = create_test_config("http://127.0.0.1:1", 300);
        let service = AuthServiceV3::new(config);

        let result = service
            .app_access_token()
            .app_id("valid_app_id")
            .app_secret("")
            .execute()
            .await;

        assert!(matches!(result, Err(CoreError::Validation { .. })));
        let err = result.expect_err("应返回验证错误");
        assert!(err.to_string().contains("应用密钥不能为空"));
    }

    #[test]
    fn test_timeout_error_handling() {
        let err = timeout_error(Duration::from_millis(50), Some("auth/token".to_string()));
        assert!(matches!(err, CoreError::Timeout { .. }));
        assert!(err.to_string().contains("auth/token"));
    }

    #[test]
    fn test_network_error_is_retryable() {
        let err = openlark_core::error::network_error("network down");
        assert!(err.is_retryable());
        assert_eq!(err.user_message(), Some("网络连接异常,请稍后重试"));
    }
}