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!("PLACEHOLDER_JWT_FOR_APP_{}_AT_{}", self.app_id, claims.iat);
55
56 Ok(jwt_payload)
57 }
58}
59
60#[derive(Debug, Serialize, Deserialize)]
62struct JwtClaims {
63 iat: i64,
65 exp: i64,
67 iss: String,
69}
70
71#[derive(Debug, Clone)]
73pub struct InstallationToken {
74 token: String,
75 expires_at: SystemTime,
76}
77
78impl InstallationToken {
79 pub fn new(token: String, expires_at: SystemTime) -> Self {
81 Self { token, expires_at }
82 }
83
84 pub fn token(&self) -> &str {
86 &self.token
87 }
88
89 pub fn is_expired(&self) -> bool {
91 SystemTime::now() >= self.expires_at
92 }
93
94 pub fn is_expiring_soon(&self) -> bool {
96 if let Ok(duration) = self.expires_at.duration_since(SystemTime::now()) {
97 duration < Duration::from_secs(300) } else {
99 true }
101 }
102}
103
104pub struct InstallationTokenManager {
106 auth: GithubAppAuth,
107 cached_token: Option<InstallationToken>,
108}
109
110impl InstallationTokenManager {
111 pub fn new(auth: GithubAppAuth) -> Self {
113 Self {
114 auth,
115 cached_token: None,
116 }
117 }
118
119 pub async fn get_token(&mut self, installation_id: i64) -> GithubResult<String> {
127 if let Some(ref token) = self.cached_token {
129 if !token.is_expiring_soon() {
130 return Ok(token.token().to_string());
131 }
132 }
133
134 self.refresh_token(installation_id).await
136 }
137
138 async fn refresh_token(&mut self, installation_id: i64) -> GithubResult<String> {
140 let _jwt = self.auth.generate_jwt()?;
142
143 let token_value = format!("ghs_installation_token_for_{}", installation_id);
150 let expires_at = SystemTime::now() + Duration::from_secs(3600); let token = InstallationToken::new(token_value.clone(), expires_at);
153 self.cached_token = Some(token);
154
155 Ok(token_value)
156 }
157
158 pub fn clear_cache(&mut self) {
160 self.cached_token = None;
161 }
162}
163
164#[cfg(test)]
165mod tests {
166 use super::*;
167
168 #[test]
169 fn test_github_app_auth_new() {
170 let auth = GithubAppAuth::new(12345, "private-key".to_string());
171 assert_eq!(auth.app_id(), 12345);
172 assert_eq!(auth.private_key(), "private-key");
173 }
174
175 #[test]
176 fn test_generate_jwt() {
177 let auth = GithubAppAuth::new(12345, "private-key".to_string());
178 let jwt = auth.generate_jwt();
179 assert!(jwt.is_ok());
180 let jwt_str = jwt.unwrap();
181 assert!(jwt_str.contains("12345"));
182 }
183
184 #[test]
185 fn test_installation_token_is_expired() {
186 let expired_time = SystemTime::now() - Duration::from_secs(60);
187 let token = InstallationToken::new("token".to_string(), expired_time);
188 assert!(token.is_expired());
189 }
190
191 #[test]
192 fn test_installation_token_not_expired() {
193 let future_time = SystemTime::now() + Duration::from_secs(3600);
194 let token = InstallationToken::new("token".to_string(), future_time);
195 assert!(!token.is_expired());
196 }
197
198 #[test]
199 fn test_installation_token_is_expiring_soon() {
200 let soon_time = SystemTime::now() + Duration::from_secs(120); let token = InstallationToken::new("token".to_string(), soon_time);
202 assert!(token.is_expiring_soon());
203 }
204
205 #[test]
206 fn test_installation_token_not_expiring_soon() {
207 let future_time = SystemTime::now() + Duration::from_secs(3600); let token = InstallationToken::new("token".to_string(), future_time);
209 assert!(!token.is_expiring_soon());
210 }
211
212 #[tokio::test]
213 async fn test_installation_token_manager() {
214 let auth = GithubAppAuth::new(12345, "private-key".to_string());
215 let mut manager = InstallationTokenManager::new(auth);
216
217 let token = manager.get_token(67890).await;
218 assert!(token.is_ok());
219 assert!(token.unwrap().contains("67890"));
220 }
221
222 #[tokio::test]
223 async fn test_installation_token_manager_caching() {
224 let auth = GithubAppAuth::new(12345, "private-key".to_string());
225 let mut manager = InstallationTokenManager::new(auth);
226
227 let token1 = manager.get_token(67890).await.unwrap();
229
230 let token2 = manager.get_token(67890).await.unwrap();
232
233 assert_eq!(token1, token2);
234 }
235
236 #[tokio::test]
237 async fn test_installation_token_manager_clear_cache() {
238 let auth = GithubAppAuth::new(12345, "private-key".to_string());
239 let mut manager = InstallationTokenManager::new(auth);
240
241 let _token1 = manager.get_token(67890).await.unwrap();
243
244 manager.clear_cache();
246
247 let token2 = manager.get_token(67890).await.unwrap();
249 assert!(token2.contains("67890"));
250 }
251}