open_lark/core/
app_ticket_manager.rs

1use serde::{Deserialize, Serialize};
2
3use crate::core::{
4    cache::QuickCache,
5    config::Config,
6    constants::{APPLY_APP_TICKET_PATH, APP_TICKET_KEY_PREFIX},
7    SDKResult,
8};
9
10#[derive(Debug)]
11pub struct AppTicketManager {
12    pub cache: QuickCache<String>,
13}
14
15impl Default for AppTicketManager {
16    fn default() -> Self {
17        Self::new()
18    }
19}
20
21impl AppTicketManager {
22    pub fn new() -> Self {
23        Self {
24            cache: QuickCache::new(),
25        }
26    }
27
28    pub fn set(&mut self, app_id: &str, value: &str, expire_time: i32) {
29        let key = app_ticket_key(app_id);
30        self.cache.set(&key, value.to_string(), expire_time);
31    }
32
33    pub async fn get(&self, config: &Config) -> Option<String> {
34        let key = app_ticket_key(&config.app_id);
35        match self.cache.get(&key) {
36            None => None,
37            Some(ticket) => {
38                if ticket.is_empty() {
39                    apply_app_ticket(config).await.ok();
40                }
41
42                Some(ticket)
43            }
44        }
45    }
46}
47
48fn app_ticket_key(app_id: &str) -> String {
49    format!("{APP_TICKET_KEY_PREFIX}-{app_id}")
50}
51
52pub async fn apply_app_ticket(config: &Config) -> SDKResult<()> {
53    let url = format!("{}{}", config.base_url, APPLY_APP_TICKET_PATH);
54
55    let body = ResendAppTicketReq {
56        app_id: config.app_id.clone(),
57        app_secret: config.app_secret.clone(),
58    };
59
60    let _response = config.http_client.post(&url).json(&body).send().await?;
61
62    Ok(())
63}
64
65#[derive(Serialize, Deserialize)]
66struct ResendAppTicketReq {
67    app_id: String,
68    app_secret: String,
69}
70
71// #[derive(Serialize, Deserialize)]
72// struct ResendAppTicketResp {
73//     #[serde(skip)]
74//     api_resp:
75//     #[serde(flatten)]
76//     code_error: ErrorResponse,
77// }
78
79#[cfg(test)]
80mod tests {
81    use super::*;
82    use crate::core::config::Config;
83    use std::time::Duration;
84
85    fn create_test_config() -> Config {
86        Config::builder()
87            .app_id("test_app_id")
88            .app_secret("test_app_secret")
89            .base_url("https://test.api.com")
90            .build()
91    }
92
93    #[test]
94    fn test_app_ticket_manager_creation() {
95        let manager = AppTicketManager::new();
96
97        // Check that manager is created properly
98        assert!(format!("{:?}", manager).contains("AppTicketManager"));
99    }
100
101    #[test]
102    fn test_app_ticket_manager_default() {
103        let manager = AppTicketManager::default();
104
105        // Test default implementation
106        assert!(format!("{:?}", manager).contains("AppTicketManager"));
107    }
108
109    #[test]
110    fn test_app_ticket_key_generation() {
111        let app_id = "test_app_123";
112        let key = app_ticket_key(app_id);
113
114        assert!(key.contains("test_app_123"));
115        assert!(key.starts_with("app_ticket"));
116    }
117
118    #[test]
119    fn test_set_and_get_ticket() {
120        let mut manager = AppTicketManager::new();
121        let app_id = "test_app";
122        let ticket_value = "test_ticket_value";
123
124        // Set ticket
125        manager.set(app_id, ticket_value, 60);
126
127        // Check cache directly
128        let key = app_ticket_key(app_id);
129        let cached_ticket = manager.cache.get(&key);
130        assert_eq!(cached_ticket, Some(ticket_value.to_string()));
131    }
132
133    #[test]
134    fn test_set_ticket_with_different_app_ids() {
135        let mut manager = AppTicketManager::new();
136
137        // Set tickets for different apps
138        manager.set("app1", "ticket1", 60);
139        manager.set("app2", "ticket2", 60);
140
141        // Verify both are stored separately
142        let key1 = app_ticket_key("app1");
143        let key2 = app_ticket_key("app2");
144
145        assert_eq!(manager.cache.get(&key1), Some("ticket1".to_string()));
146        assert_eq!(manager.cache.get(&key2), Some("ticket2".to_string()));
147    }
148
149    #[test]
150    fn test_ticket_expiration() {
151        let mut manager = AppTicketManager::new();
152        let app_id = "test_app";
153
154        // Set ticket with very short expiration
155        manager.set(app_id, "short_lived_ticket", 1);
156
157        // Verify it exists initially
158        let key = app_ticket_key(app_id);
159        assert!(manager.cache.get(&key).is_some());
160
161        // Wait for expiration
162        std::thread::sleep(Duration::from_secs(2));
163
164        // Should be expired and removed
165        assert!(manager.cache.get(&key).is_none());
166    }
167
168    #[test]
169    fn test_overwrite_existing_ticket() {
170        let mut manager = AppTicketManager::new();
171        let app_id = "test_app";
172
173        // Set initial ticket
174        manager.set(app_id, "initial_ticket", 60);
175
176        // Overwrite with new ticket
177        manager.set(app_id, "updated_ticket", 60);
178
179        // Verify only new ticket exists
180        let key = app_ticket_key(app_id);
181        assert_eq!(manager.cache.get(&key), Some("updated_ticket".to_string()));
182    }
183
184    #[test]
185    fn test_empty_app_id() {
186        let mut manager = AppTicketManager::new();
187
188        // Test with empty app_id
189        manager.set("", "ticket", 60);
190
191        let key = app_ticket_key("");
192        assert!(manager.cache.get(&key).is_some());
193    }
194
195    #[test]
196    fn test_special_characters_in_app_id() {
197        let mut manager = AppTicketManager::new();
198        let special_app_id = "app-id_with.special@chars";
199
200        manager.set(special_app_id, "special_ticket", 60);
201
202        let key = app_ticket_key(special_app_id);
203        assert_eq!(manager.cache.get(&key), Some("special_ticket".to_string()));
204    }
205
206    #[test]
207    fn test_zero_expiration_time() {
208        let mut manager = AppTicketManager::new();
209        let app_id = "test_app";
210
211        // Set ticket with zero expiration (should expire immediately or very soon)
212        manager.set(app_id, "zero_exp_ticket", 0);
213
214        // Sleep briefly to allow expiration
215        std::thread::sleep(Duration::from_millis(10));
216
217        let key = app_ticket_key(app_id);
218        // May or may not be present depending on timing, but shouldn't crash
219        let _ = manager.cache.get(&key);
220    }
221
222    #[test]
223    fn test_very_long_ticket_value() {
224        let mut manager = AppTicketManager::new();
225        let app_id = "test_app";
226        let long_ticket = "a".repeat(10000); // 10KB ticket
227
228        manager.set(app_id, &long_ticket, 60);
229
230        let key = app_ticket_key(app_id);
231        assert_eq!(manager.cache.get(&key), Some(long_ticket));
232    }
233
234    #[tokio::test]
235    async fn test_get_with_cached_ticket() {
236        let mut manager = AppTicketManager::new();
237        let config = create_test_config();
238
239        // Pre-populate cache with ticket
240        manager.set(&config.app_id, "cached_ticket", 60);
241
242        let result = manager.get(&config).await;
243        assert_eq!(result, Some("cached_ticket".to_string()));
244    }
245
246    #[tokio::test]
247    async fn test_get_with_empty_ticket() {
248        let mut manager = AppTicketManager::new();
249        let config = create_test_config();
250
251        // Set empty ticket in cache
252        manager.set(&config.app_id, "", 60);
253
254        // This will try to apply for new ticket, but return the empty one
255        let result = manager.get(&config).await;
256        assert_eq!(result, Some("".to_string()));
257    }
258
259    #[tokio::test]
260    async fn test_get_without_cached_ticket() {
261        let manager = AppTicketManager::new();
262        let config = create_test_config();
263
264        // No ticket in cache
265        let result = manager.get(&config).await;
266        assert_eq!(result, None);
267    }
268
269    #[test]
270    fn test_resend_app_ticket_req_serialization() {
271        let req = ResendAppTicketReq {
272            app_id: "test_app".to_string(),
273            app_secret: "test_secret".to_string(),
274        };
275
276        // Test JSON serialization
277        let json = serde_json::to_string(&req).unwrap();
278        assert!(json.contains("test_app"));
279        assert!(json.contains("test_secret"));
280
281        // Test JSON deserialization
282        let deserialized: ResendAppTicketReq = serde_json::from_str(&json).unwrap();
283        assert_eq!(deserialized.app_id, "test_app");
284        assert_eq!(deserialized.app_secret, "test_secret");
285    }
286
287    #[test]
288    fn test_multiple_managers_independence() {
289        let mut manager1 = AppTicketManager::new();
290        let mut manager2 = AppTicketManager::new();
291
292        manager1.set("app1", "ticket1", 60);
293        manager2.set("app2", "ticket2", 60);
294
295        // Each manager should only have its own ticket
296        let key1 = app_ticket_key("app1");
297        let key2 = app_ticket_key("app2");
298
299        assert_eq!(manager1.cache.get(&key1), Some("ticket1".to_string()));
300        assert_eq!(manager1.cache.get(&key2), None);
301
302        assert_eq!(manager2.cache.get(&key2), Some("ticket2".to_string()));
303        assert_eq!(manager2.cache.get(&key1), None);
304    }
305
306    #[test]
307    fn test_app_ticket_key_format() {
308        let test_cases = vec![
309            ("simple_app", "app_ticket-simple_app"),
310            ("app-with-dashes", "app_ticket-app-with-dashes"),
311            ("app_with_underscores", "app_ticket-app_with_underscores"),
312            ("123numeric", "app_ticket-123numeric"),
313        ];
314
315        for (app_id, _expected_suffix) in test_cases {
316            let key = app_ticket_key(app_id);
317            assert!(key.ends_with(&format!("-{}", app_id)));
318            // Verify it starts with the expected prefix
319            assert!(key.starts_with("app_ticket"));
320        }
321    }
322}