diff --git a/channels-src/feishu/src/lib.rs b/channels-src/feishu/src/lib.rs index 2e7261d8..3094eaa0 100644 --- a/channels-src/feishu/src/lib.rs +++ b/channels-src/feishu/src/lib.rs @@ -206,9 +206,17 @@ struct FeishuApiResponse { data: Option, } -/// Tenant access token response. -#[derive(Debug, Default, Deserialize)] -struct TenantAccessTokenData { +/// Tenant access token response (flat format). +/// +/// Unlike most Feishu APIs that nest results under `data`, the +/// `/auth/v3/tenant_access_token/internal` endpoint returns `code`, `msg`, +/// `tenant_access_token`, and `expire` at the top level. +#[derive(Debug, Deserialize)] +struct TenantAccessTokenResponse { + #[serde(default)] + code: i32, + #[serde(default)] + msg: String, tenant_access_token: String, expire: i64, } @@ -770,9 +778,8 @@ fn obtain_tenant_token(api_base: &str) -> Result { )); } - let token_resp: FeishuApiResponse = - serde_json::from_slice(&response.body) - .map_err(|e| format!("Failed to parse token response: {}", e))?; + let token_resp: TenantAccessTokenResponse = serde_json::from_slice(&response.body) + .map_err(|e| format!("Failed to parse token response: {}", e))?; if token_resp.code != 0 { return Err(format!( @@ -781,23 +788,33 @@ fn obtain_tenant_token(api_base: &str) -> Result { )); } - let data = token_resp - .data - .ok_or_else(|| "Token response missing data".to_string())?; + if token_resp.tenant_access_token.is_empty() { + return Err("Token response missing tenant_access_token".to_string()); + } + + if token_resp.expire <= 0 { + return Err(format!( + "Token response has invalid expire value: {}", + token_resp.expire + )); + } // Cache the token with expiry. let now = channel_host::now_millis(); - let expiry = now + (data.expire as u64) * 1000; + let expiry = now.saturating_add((token_resp.expire as u64).saturating_mul(1000)); - let _ = channel_host::workspace_write(TOKEN_PATH, &data.tenant_access_token); + let _ = channel_host::workspace_write(TOKEN_PATH, &token_resp.tenant_access_token); let _ = channel_host::workspace_write(TOKEN_EXPIRY_PATH, &expiry.to_string()); channel_host::log( channel_host::LogLevel::Debug, - &format!("Tenant access token refreshed, expires in {}s", data.expire), + &format!( + "Tenant access token refreshed, expires in {}s", + token_resp.expire + ), ); - Ok(data.tenant_access_token) + Ok(token_resp.tenant_access_token) } Err(e) => Err(format!("Token exchange request failed: {}", e)), } @@ -819,3 +836,60 @@ fn json_response(status: u16, body: serde_json::Value) -> OutgoingHttpResponse { body: body_bytes, } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn parse_flat_token_response() { + let json = r#"{ + "code": 0, + "msg": "ok", + "tenant_access_token": "t-abc123", + "expire": 7200 + }"#; + let resp: TenantAccessTokenResponse = serde_json::from_str(json).unwrap(); + assert_eq!(resp.code, 0); + assert_eq!(resp.msg, "ok"); + assert_eq!(resp.tenant_access_token, "t-abc123"); + assert_eq!(resp.expire, 7200); + } + + #[test] + fn parse_token_response_rejects_missing_token() { + let json = r#"{"code": 0, "msg": "ok", "expire": 7200}"#; + let result: Result = serde_json::from_str(json); + assert!(result.is_err(), "should fail when tenant_access_token is missing"); + } + + #[test] + fn parse_token_response_rejects_missing_expire() { + let json = r#"{"code": 0, "msg": "ok", "tenant_access_token": "t-abc"}"#; + let result: Result = serde_json::from_str(json); + assert!(result.is_err(), "should fail when expire is missing"); + } + + #[test] + fn parse_token_response_defaults_code_and_msg() { + let json = r#"{"tenant_access_token": "t-abc", "expire": 3600}"#; + let resp: TenantAccessTokenResponse = serde_json::from_str(json).unwrap(); + assert_eq!(resp.code, 0); + assert_eq!(resp.msg, ""); + assert_eq!(resp.tenant_access_token, "t-abc"); + assert_eq!(resp.expire, 3600); + } + + #[test] + fn parse_token_error_response() { + let json = r#"{ + "code": 10003, + "msg": "invalid app_id", + "tenant_access_token": "", + "expire": 0 + }"#; + let resp: TenantAccessTokenResponse = serde_json::from_str(json).unwrap(); + assert_eq!(resp.code, 10003); + assert!(resp.tenant_access_token.is_empty()); + } +}