tencentcloud_hunyuan_sdk/
lib.rs

1//! TencentCloud Hunyuan SDK for Rust
2//!
3//! Async client for Hunyuan (v2023-09-01) with TC3-HMAC-SHA256 signing.
4//!
5//! Features:
6//! - Async HTTP via `reqwest`
7//! - TC3 signing
8//! - Typed helper for `ChatCompletions`
9//!
10//! Debug logging can be enabled with `ClientBuilder::debug(true)` or by setting the
11//! environment variable `TENCENTCLOUD_SDK_DEBUG=true`. Sensitive values are masked
12//! in logs, but request/response bodies may still contain sensitive data.
13//!
14//! Quick start example is available in the README and under `examples/chat.rs`.
15pub mod client;
16pub mod models;
17
18#[cfg(test)]
19mod tests {
20    use crate::client::{Client, ClientBuilder, Credential, Region};
21    use crate::models::{ChatCompletionsRequest, Message};
22    use time::OffsetDateTime;
23
24    #[test]
25    fn test_region_as_str() {
26        assert_eq!(Region::ApBeijing.as_str(), "ap-beijing");
27        assert_eq!(Region::ApGuangzhou.as_str(), "ap-guangzhou");
28        assert_eq!(
29            Region::Custom("custom-region".to_string()).as_str(),
30            "custom-region"
31        );
32    }
33
34    #[test]
35    fn test_credential_creation() {
36        let cred = Credential {
37            secret_id: "test_id".to_string(),
38            secret_key: "test_key".to_string(),
39            token: None,
40        };
41        assert_eq!(cred.secret_id, "test_id");
42        assert_eq!(cred.secret_key, "test_key");
43        assert!(cred.token.is_none());
44
45        let cred_with_token = Credential {
46            secret_id: "test_id".to_string(),
47            secret_key: "test_key".to_string(),
48            token: Some("test_token".to_string()),
49        };
50        assert_eq!(cred_with_token.token, Some("test_token".to_string()));
51    }
52
53    #[test]
54    fn test_client_builder_defaults() {
55        let client = ClientBuilder::new()
56            .credential(Credential {
57                secret_id: "test_id".to_string(),
58                secret_key: "test_key".to_string(),
59                token: None,
60            })
61            .build();
62
63        assert_eq!(client.region().as_str(), "ap-guangzhou");
64        assert_eq!(client.endpoint(), "hunyuan.tencentcloudapi.com");
65        // Note: debug state depends on environment, so we don't test it here
66    }
67
68    #[test]
69    fn test_client_builder_custom_values() {
70        let client = ClientBuilder::new()
71            .credential(Credential {
72                secret_id: "test_id".to_string(),
73                secret_key: "test_key".to_string(),
74                token: None,
75            })
76            .region(Region::ApBeijing)
77            .endpoint("custom.endpoint.com")
78            .debug(true)
79            .build();
80
81        assert_eq!(client.region().as_str(), "ap-beijing");
82        assert_eq!(client.endpoint(), "custom.endpoint.com");
83        assert!(client.debug());
84    }
85
86    #[test]
87    fn test_client_builder_custom_region() {
88        let client = ClientBuilder::new()
89            .credential(Credential {
90                secret_id: "test_id".to_string(),
91                secret_key: "test_key".to_string(),
92                token: None,
93            })
94            .region(Region::Custom("us-west-1".to_string()))
95            .build();
96
97        assert_eq!(client.region().as_str(), "us-west-1");
98    }
99
100    #[test]
101    fn test_client_builder_with_token() {
102        let client = ClientBuilder::new()
103            .credential(Credential {
104                secret_id: "test_id".to_string(),
105                secret_key: "test_key".to_string(),
106                token: Some("session_token".to_string()),
107            })
108            .build();
109
110        assert_eq!(client.credential().token, Some("session_token".to_string()));
111    }
112
113    #[test]
114    fn test_client_builder_env_debug() {
115        // Test with environment variable set
116        std::env::set_var("TENCENTCLOUD_SDK_DEBUG", "true");
117        let _client = ClientBuilder::new()
118            .credential(Credential {
119                secret_id: "test_id".to_string(),
120                secret_key: "test_key".to_string(),
121                token: None,
122            })
123            .build();
124        // Note: debug state depends on environment, so we don't test it here
125
126        // Test with environment variable not set
127        std::env::remove_var("TENCENTCLOUD_SDK_DEBUG");
128        let _client = ClientBuilder::new()
129            .credential(Credential {
130                secret_id: "test_id".to_string(),
131                secret_key: "test_key".to_string(),
132                token: None,
133            })
134            .build();
135        // Note: debug state depends on environment, so we don't test it here
136    }
137
138    #[test]
139    fn test_client_builder_override_env_debug() {
140        std::env::set_var("TENCENTCLOUD_SDK_DEBUG", "true");
141
142        // Override environment variable
143        let client = ClientBuilder::new()
144            .credential(Credential {
145                secret_id: "test_id".to_string(),
146                secret_key: "test_key".to_string(),
147                token: None,
148            })
149            .debug(false)
150            .build();
151        assert!(!client.debug());
152
153        std::env::remove_var("TENCENTCLOUD_SDK_DEBUG");
154    }
155
156    #[test]
157    fn test_client_builder_panic_no_credential() {
158        let result = std::panic::catch_unwind(|| {
159            ClientBuilder::new().build();
160        });
161        assert!(result.is_err());
162    }
163
164    #[test]
165    fn test_client_builder_methods() {
166        let builder = ClientBuilder::new();
167        assert!(!builder.has_http());
168        assert!(!builder.has_credential());
169        assert!(!builder.has_region());
170        assert!(!builder.has_endpoint());
171        assert!(!builder.has_debug());
172    }
173
174    #[test]
175    fn test_client_builder_fluent_interface() {
176        let builder = ClientBuilder::new()
177            .http(reqwest::Client::new())
178            .credential(Credential {
179                secret_id: "test_id".to_string(),
180                secret_key: "test_key".to_string(),
181                token: None,
182            })
183            .region(Region::ApBeijing)
184            .endpoint("test.endpoint.com")
185            .debug(true);
186
187        assert!(builder.has_http());
188        assert!(builder.has_credential());
189        assert!(builder.has_region());
190        assert!(builder.has_endpoint());
191        assert!(builder.has_debug());
192    }
193
194    #[test]
195    fn test_client_clone() {
196        let client = ClientBuilder::new()
197            .credential(Credential {
198                secret_id: "test_id".to_string(),
199                secret_key: "test_key".to_string(),
200                token: None,
201            })
202            .build();
203
204        let cloned = client.clone();
205        assert_eq!(cloned.region().as_str(), client.region().as_str());
206        assert_eq!(cloned.endpoint(), client.endpoint());
207        assert_eq!(cloned.debug(), client.debug());
208    }
209
210    #[test]
211    fn test_client_builder_method() {
212        let client = Client::builder()
213            .credential(Credential {
214                secret_id: "test_id".to_string(),
215                secret_key: "test_key".to_string(),
216                token: None,
217            })
218            .build();
219
220        assert_eq!(client.region().as_str(), "ap-guangzhou");
221    }
222
223    #[test]
224    fn test_tc3_sign_components() {
225        let client = ClientBuilder::new()
226            .credential(Credential {
227                secret_id: "test_id".to_string(),
228                secret_key: "test_key".to_string(),
229                token: None,
230            })
231            .build();
232
233        let timestamp = OffsetDateTime::now_utc().unix_timestamp();
234        let (signature, credential_scope) = client.tc3_sign(
235            "POST",
236            "/",
237            "",
238            "content-type:application/json; charset=utf-8\nhost:hunyuan.tencentcloudapi.com\n",
239            "content-type;host",
240            "test_payload_hash",
241            timestamp,
242        );
243
244        assert!(!signature.is_empty());
245        assert!(!credential_scope.is_empty());
246        assert!(credential_scope.contains("hunyuan"));
247        assert!(credential_scope.contains("tc3_request"));
248    }
249
250    #[test]
251    fn test_build_headers() {
252        let client = ClientBuilder::new()
253            .credential(Credential {
254                secret_id: "test_id".to_string(),
255                secret_key: "test_key".to_string(),
256                token: None,
257            })
258            .build();
259
260        let timestamp = OffsetDateTime::now_utc().unix_timestamp();
261        let headers = client.build_headers("TestAction", "test_body", timestamp);
262
263        assert_eq!(headers.get("Host").unwrap(), "hunyuan.tencentcloudapi.com");
264        assert_eq!(
265            headers.get("Content-Type").unwrap(),
266            "application/json; charset=utf-8"
267        );
268        assert_eq!(headers.get("X-TC-Action").unwrap(), "TestAction");
269        assert_eq!(headers.get("X-TC-Version").unwrap(), "2023-09-01");
270        assert_eq!(headers.get("X-TC-Region").unwrap(), "ap-guangzhou");
271        assert_eq!(
272            headers.get("X-TC-Timestamp").unwrap(),
273            &timestamp.to_string()
274        );
275        assert!(headers.get("X-TC-Token").is_none());
276    }
277
278    #[test]
279    fn test_build_headers_with_token() {
280        let client = ClientBuilder::new()
281            .credential(Credential {
282                secret_id: "test_id".to_string(),
283                secret_key: "test_key".to_string(),
284                token: Some("test_token".to_string()),
285            })
286            .build();
287
288        let timestamp = OffsetDateTime::now_utc().unix_timestamp();
289        let headers = client.build_headers("TestAction", "test_body", timestamp);
290
291        assert_eq!(headers.get("X-TC-Token").unwrap(), "test_token");
292    }
293
294    #[test]
295    fn test_models_creation() {
296        let message = Message {
297            role: "user".to_string(),
298            content: "Hello, world!".to_string(),
299        };
300
301        assert_eq!(message.role, "user");
302        assert_eq!(message.content, "Hello, world!");
303
304        let request = ChatCompletionsRequest {
305            model: Some("hunyuan-pro".to_string()),
306            messages: vec![message],
307            temperature: Some(0.7),
308            top_p: Some(0.9),
309            stream: Some(false),
310        };
311
312        assert_eq!(request.model, Some("hunyuan-pro".to_string()));
313        assert_eq!(request.messages.len(), 1);
314        assert_eq!(request.temperature, Some(0.7));
315        assert_eq!(request.top_p, Some(0.9));
316        assert_eq!(request.stream, Some(false));
317    }
318
319    #[test]
320    fn test_serde_serialization() {
321        let message = Message {
322            role: "user".to_string(),
323            content: "Test message".to_string(),
324        };
325
326        let json = serde_json::to_string(&message).unwrap();
327        let deserialized: Message = serde_json::from_str(&json).unwrap();
328
329        assert_eq!(deserialized.role, message.role);
330        assert_eq!(deserialized.content, message.content);
331    }
332
333    #[test]
334    fn test_serde_serialization_with_optional_fields() {
335        let request = ChatCompletionsRequest {
336            model: Some("hunyuan-pro".to_string()),
337            messages: vec![Message {
338                role: "user".to_string(),
339                content: "Test".to_string(),
340            }],
341            temperature: None,
342            top_p: None,
343            stream: None,
344        };
345
346        let json = serde_json::to_string(&request).unwrap();
347        let deserialized: ChatCompletionsRequest = serde_json::from_str(&json).unwrap();
348
349        assert_eq!(deserialized.temperature, None);
350        assert_eq!(deserialized.top_p, None);
351        assert_eq!(deserialized.stream, None);
352    }
353}
354
355pub use client::{Client, ClientBuilder, Credential, Region};