highlevel_api/auth/
refresh.rs1use std::collections::HashMap;
2use std::sync::Arc;
3
4use tokio::sync::Mutex;
5
6use super::token::TokenData;
7use crate::error::Result;
8
9pub async fn auto_refresh_token<F, Fut>(token: &mut TokenData, refresh: F) -> Result<()>
14where
15 F: FnOnce() -> Fut,
16 Fut: std::future::Future<Output = Result<TokenData>>,
17{
18 if !token.is_expired() {
19 return Ok(());
20 }
21 let new_token = refresh().await?;
22 *token = new_token;
23 Ok(())
24}
25
26#[derive(Clone)]
30pub struct TokenRefresher {
31 locks: Arc<Mutex<HashMap<String, Arc<Mutex<()>>>>>,
32}
33
34impl TokenRefresher {
35 pub fn new() -> Self {
36 Self {
37 locks: Arc::new(Mutex::new(HashMap::new())),
38 }
39 }
40
41 pub async fn ensure_valid<F, Fut>(
47 &self,
48 key: &str,
49 token: &mut TokenData,
50 refresh: F,
51 ) -> Result<()>
52 where
53 F: FnOnce() -> Fut + Send,
54 Fut: std::future::Future<Output = Result<TokenData>> + Send,
55 {
56 if !token.is_expired() {
58 return Ok(());
59 }
60
61 let lock = {
63 let mut map = self.locks.lock().await;
64 map.entry(key.to_string())
65 .or_insert_with(|| Arc::new(Mutex::new(())))
66 .clone()
67 };
68
69 let _guard = lock.lock().await;
70
71 if !token.is_expired() {
74 return Ok(());
75 }
76
77 let new_token = refresh().await?;
78 *token = new_token;
79 Ok(())
80 }
81}
82
83impl Default for TokenRefresher {
84 fn default() -> Self {
85 Self::new()
86 }
87}
88
89#[cfg(test)]
90mod tests {
91 use super::*;
92
93 #[tokio::test]
94 async fn unused_token_does_not_refresh() {
95 let mut token = TokenData {
96 access_token: "tok".into(),
97 refresh_token: None,
98 token_type: "Bearer".into(),
99 expires_in: 3600,
100 scope: None,
101 user_type: None,
102 company_id: None,
103 location_id: None,
104 issued_at: None,
105 };
106
107 let call_count = Arc::new(std::sync::atomic::AtomicU32::new(0));
108 let replacement = token.clone();
109 let cc = call_count.clone();
110 auto_refresh_token(&mut token, move || {
111 cc.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
112 let t = replacement.clone();
113 async { Ok(t) }
114 })
115 .await
116 .unwrap();
117
118 assert_eq!(call_count.load(std::sync::atomic::Ordering::SeqCst), 0);
119 }
120
121 #[tokio::test]
122 async fn expired_token_triggers_refresh() {
123 let mut token = TokenData {
124 access_token: "tok".into(),
125 refresh_token: None,
126 token_type: "Bearer".into(),
127 expires_in: 3600,
128 scope: None,
129 user_type: None,
130 company_id: None,
131 location_id: None,
132 issued_at: Some(1),
133 };
134
135 let call_count = Arc::new(std::sync::atomic::AtomicU32::new(0));
136 let replacement = TokenData {
137 access_token: "new_tok".into(),
138 refresh_token: None,
139 token_type: "Bearer".into(),
140 expires_in: 3600,
141 scope: None,
142 user_type: None,
143 company_id: None,
144 location_id: None,
145 issued_at: None,
146 };
147 let cc = call_count.clone();
148 auto_refresh_token(&mut token, move || {
149 cc.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
150 let t = replacement.clone();
151 async { Ok(t) }
152 })
153 .await
154 .unwrap();
155
156 assert_eq!(call_count.load(std::sync::atomic::Ordering::SeqCst), 1);
157 assert_eq!(token.access_token, "new_tok");
158 }
159
160 #[tokio::test]
161 async fn single_flight_refreshes_once() {
162 let refresher = TokenRefresher::new();
163
164 let shared = Arc::new(tokio::sync::Mutex::new(TokenData {
166 access_token: "tok".into(),
167 refresh_token: None,
168 token_type: "Bearer".into(),
169 expires_in: 3600,
170 scope: None,
171 user_type: None,
172 company_id: None,
173 location_id: None,
174 issued_at: Some(1),
175 }));
176
177 let call_count = Arc::new(std::sync::atomic::AtomicU32::new(0));
178
179 let mut tasks = vec![];
180 for _ in 0..5 {
181 let r = refresher.clone();
182 let cc = call_count.clone();
183 let shared = shared.clone();
184 tasks.push(tokio::spawn(async move {
185 let mut token = shared.lock().await;
186 r.ensure_valid("loc_1", &mut *token, || {
187 cc.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
188 async move {
189 Ok(TokenData {
190 access_token: "refreshed".into(),
191 refresh_token: None,
192 token_type: "Bearer".into(),
193 expires_in: 3600,
194 scope: None,
195 user_type: None,
196 company_id: None,
197 location_id: None,
198 issued_at: None,
199 })
200 }
201 })
202 .await
203 .unwrap();
204 token.access_token.clone()
205 }));
206 }
207
208 for task in tasks {
209 assert_eq!(task.await.unwrap(), "refreshed");
210 }
211 assert_eq!(call_count.load(std::sync::atomic::Ordering::SeqCst), 1);
212 }
213}