meritocrab_github/
auth.rs1use crate::error::{GithubError, GithubResult};
2use serde::{Deserialize, Serialize};
3use std::time::{Duration, SystemTime, UNIX_EPOCH};
4
5#[derive(Clone)]
7pub struct GithubAppAuth {
8 app_id: i64,
9 private_key: String,
10}
11
12impl GithubAppAuth {
13 pub fn new(app_id: i64, private_key: String) -> Self {
15 Self {
16 app_id,
17 private_key,
18 }
19 }
20
21 pub fn app_id(&self) -> i64 {
23 self.app_id
24 }
25
26 pub fn private_key(&self) -> &str {
28 &self.private_key
29 }
30
31 pub fn generate_jwt(&self) -> GithubResult<String> {
41 let now = SystemTime::now()
42 .duration_since(UNIX_EPOCH)
43 .map_err(|e| GithubError::AuthError(format!("System time error: {}", e)))?
44 .as_secs() as i64;
45
46 let claims = JwtClaims {
47 iat: now,
48 exp: now + 600, iss: self.app_id.to_string(),
50 };
51
52 let jwt_payload = format!(
55 "PLACEHOLDER_JWT_FOR_APP_{}_AT_{}",
56 self.app_id, claims.iat
57 );
58
59 Ok(jwt_payload)
60 }
61}
62
63#[derive(Debug, Serialize, Deserialize)]
65struct JwtClaims {
66 iat: i64,
68 exp: i64,
70 iss: String,
72}
73
74#[derive(Debug, Clone)]
76pub struct InstallationToken {
77 token: String,
78 expires_at: SystemTime,
79}
80
81impl InstallationToken {
82 pub fn new(token: String, expires_at: SystemTime) -> Self {
84 Self {
85 token,
86 expires_at,
87 }
88 }
89
90 pub fn token(&self) -> &str {
92 &self.token
93 }
94
95 pub fn is_expired(&self) -> bool {
97 SystemTime::now() >= self.expires_at
98 }
99
100 pub fn is_expiring_soon(&self) -> bool {
102 if let Ok(duration) = self.expires_at.duration_since(SystemTime::now()) {
103 duration < Duration::from_secs(300) } else {
105 true }
107 }
108}
109
110pub struct InstallationTokenManager {
112 auth: GithubAppAuth,
113 cached_token: Option<InstallationToken>,
114}
115
116impl InstallationTokenManager {
117 pub fn new(auth: GithubAppAuth) -> Self {
119 Self {
120 auth,
121 cached_token: None,
122 }
123 }
124
125 pub async fn get_token(&mut self, installation_id: i64) -> GithubResult<String> {
133 if let Some(ref token) = self.cached_token {
135 if !token.is_expiring_soon() {
136 return Ok(token.token().to_string());
137 }
138 }
139
140 self.refresh_token(installation_id).await
142 }
143
144 async fn refresh_token(&mut self, installation_id: i64) -> GithubResult<String> {
146 let _jwt = self.auth.generate_jwt()?;
148
149 let token_value = format!("ghs_installation_token_for_{}", installation_id);
156 let expires_at = SystemTime::now() + Duration::from_secs(3600); let token = InstallationToken::new(token_value.clone(), expires_at);
159 self.cached_token = Some(token);
160
161 Ok(token_value)
162 }
163
164 pub fn clear_cache(&mut self) {
166 self.cached_token = None;
167 }
168}
169
170#[cfg(test)]
171mod tests {
172 use super::*;
173
174 #[test]
175 fn test_github_app_auth_new() {
176 let auth = GithubAppAuth::new(12345, "private-key".to_string());
177 assert_eq!(auth.app_id(), 12345);
178 assert_eq!(auth.private_key(), "private-key");
179 }
180
181 #[test]
182 fn test_generate_jwt() {
183 let auth = GithubAppAuth::new(12345, "private-key".to_string());
184 let jwt = auth.generate_jwt();
185 assert!(jwt.is_ok());
186 let jwt_str = jwt.unwrap();
187 assert!(jwt_str.contains("12345"));
188 }
189
190 #[test]
191 fn test_installation_token_is_expired() {
192 let expired_time = SystemTime::now() - Duration::from_secs(60);
193 let token = InstallationToken::new("token".to_string(), expired_time);
194 assert!(token.is_expired());
195 }
196
197 #[test]
198 fn test_installation_token_not_expired() {
199 let future_time = SystemTime::now() + Duration::from_secs(3600);
200 let token = InstallationToken::new("token".to_string(), future_time);
201 assert!(!token.is_expired());
202 }
203
204 #[test]
205 fn test_installation_token_is_expiring_soon() {
206 let soon_time = SystemTime::now() + Duration::from_secs(120); let token = InstallationToken::new("token".to_string(), soon_time);
208 assert!(token.is_expiring_soon());
209 }
210
211 #[test]
212 fn test_installation_token_not_expiring_soon() {
213 let future_time = SystemTime::now() + Duration::from_secs(3600); let token = InstallationToken::new("token".to_string(), future_time);
215 assert!(!token.is_expiring_soon());
216 }
217
218 #[tokio::test]
219 async fn test_installation_token_manager() {
220 let auth = GithubAppAuth::new(12345, "private-key".to_string());
221 let mut manager = InstallationTokenManager::new(auth);
222
223 let token = manager.get_token(67890).await;
224 assert!(token.is_ok());
225 assert!(token.unwrap().contains("67890"));
226 }
227
228 #[tokio::test]
229 async fn test_installation_token_manager_caching() {
230 let auth = GithubAppAuth::new(12345, "private-key".to_string());
231 let mut manager = InstallationTokenManager::new(auth);
232
233 let token1 = manager.get_token(67890).await.unwrap();
235
236 let token2 = manager.get_token(67890).await.unwrap();
238
239 assert_eq!(token1, token2);
240 }
241
242 #[tokio::test]
243 async fn test_installation_token_manager_clear_cache() {
244 let auth = GithubAppAuth::new(12345, "private-key".to_string());
245 let mut manager = InstallationTokenManager::new(auth);
246
247 let _token1 = manager.get_token(67890).await.unwrap();
249
250 manager.clear_cache();
252
253 let token2 = manager.get_token(67890).await.unwrap();
255 assert!(token2.contains("67890"));
256 }
257}