1use serde::{Deserialize, Serialize};
7
8#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct StoredToken {
11 pub access_token: String,
12 pub refresh_token: Option<String>,
13 pub expires_at: i64, pub scopes: Vec<String>,
15}
16
17impl StoredToken {
18 pub fn is_valid(&self) -> bool {
19 let now = chrono::Utc::now().timestamp();
20 self.expires_at > now + 60
22 }
23}
24
25#[derive(Debug, Clone, Default, Serialize, Deserialize)]
27pub struct ProfileConfig {
28 pub client_id: Option<String>,
29 pub client_secret: Option<String>,
30 pub base_url: Option<String>,
31 pub callback_url: Option<String>,
32 pub da_nickname: Option<String>,
33 #[serde(default = "default_use_keychain")]
34 pub use_keychain: bool,
35 #[serde(default, skip_serializing_if = "Option::is_none")]
37 pub context_hub_id: Option<String>,
38 #[serde(default, skip_serializing_if = "Option::is_none")]
40 pub context_project_id: Option<String>,
41 #[serde(default, skip_serializing_if = "Option::is_none")]
43 pub context_account_id: Option<String>,
44}
45
46fn default_use_keychain() -> bool {
47 true
48}
49
50#[derive(Debug, Clone, Default, Serialize, Deserialize)]
52pub struct ProfilesData {
53 pub active_profile: Option<String>,
54 #[serde(default)]
55 pub profiles: std::collections::HashMap<String, ProfileConfig>,
56}
57
58#[cfg(test)]
59mod tests {
60 use super::*;
61
62 #[test]
65 fn test_stored_token_serialization() {
66 let token = StoredToken {
67 access_token: "test_access_token".to_string(),
68 refresh_token: Some("test_refresh_token".to_string()),
69 expires_at: 1700000000,
70 scopes: vec!["data:read".to_string(), "data:write".to_string()],
71 };
72
73 let json = serde_json::to_string(&token).unwrap();
74 assert!(json.contains("test_access_token"));
75 assert!(json.contains("test_refresh_token"));
76 assert!(json.contains("1700000000"));
77 assert!(json.contains("data:read"));
78 }
79
80 #[test]
81 fn test_stored_token_deserialization() {
82 let json = r#"{
83 "access_token": "my_token",
84 "refresh_token": "my_refresh",
85 "expires_at": 1700000000,
86 "scopes": ["scope1", "scope2"]
87 }"#;
88
89 let token: StoredToken = serde_json::from_str(json).unwrap();
90 assert_eq!(token.access_token, "my_token");
91 assert_eq!(token.refresh_token, Some("my_refresh".to_string()));
92 assert_eq!(token.expires_at, 1700000000);
93 assert_eq!(token.scopes, vec!["scope1", "scope2"]);
94 }
95
96 #[test]
97 fn test_stored_token_without_refresh_token() {
98 let json = r#"{
99 "access_token": "my_token",
100 "refresh_token": null,
101 "expires_at": 1700000000,
102 "scopes": []
103 }"#;
104
105 let token: StoredToken = serde_json::from_str(json).unwrap();
106 assert_eq!(token.refresh_token, None);
107 assert!(token.scopes.is_empty());
108 }
109
110 #[test]
111 fn test_stored_token_is_valid_future_expiry() {
112 let future_timestamp = chrono::Utc::now().timestamp() + 3600; let token = StoredToken {
114 access_token: "token".to_string(),
115 refresh_token: None,
116 expires_at: future_timestamp,
117 scopes: vec![],
118 };
119 assert!(token.is_valid());
120 }
121
122 #[test]
123 fn test_stored_token_is_valid_past_expiry() {
124 let past_timestamp = chrono::Utc::now().timestamp() - 3600; let token = StoredToken {
126 access_token: "token".to_string(),
127 refresh_token: None,
128 expires_at: past_timestamp,
129 scopes: vec![],
130 };
131 assert!(!token.is_valid());
132 }
133
134 #[test]
135 fn test_stored_token_is_valid_buffer() {
136 let soon_timestamp = chrono::Utc::now().timestamp() + 30;
138 let token = StoredToken {
139 access_token: "token".to_string(),
140 refresh_token: None,
141 expires_at: soon_timestamp,
142 scopes: vec![],
143 };
144 assert!(!token.is_valid());
145 }
146
147 #[test]
148 fn test_stored_token_is_valid_just_outside_buffer() {
149 let timestamp = chrono::Utc::now().timestamp() + 120;
151 let token = StoredToken {
152 access_token: "token".to_string(),
153 refresh_token: None,
154 expires_at: timestamp,
155 scopes: vec![],
156 };
157 assert!(token.is_valid());
158 }
159
160 #[test]
163 fn test_profile_config_default() {
164 let config = ProfileConfig::default();
165 assert!(config.client_id.is_none());
166 assert!(config.client_secret.is_none());
167 assert!(config.base_url.is_none());
168 assert!(config.callback_url.is_none());
169 assert!(config.da_nickname.is_none());
170 assert!(!config.use_keychain);
173 assert!(config.context_hub_id.is_none());
174 assert!(config.context_project_id.is_none());
175 assert!(config.context_account_id.is_none());
176 }
177
178 #[test]
179 fn test_profile_config_serialization() {
180 let config = ProfileConfig {
181 client_id: Some("my_client_id".to_string()),
182 client_secret: Some("my_secret".to_string()),
183 base_url: Some("https://custom.api.com".to_string()),
184 callback_url: Some("http://localhost:3000/callback".to_string()),
185 da_nickname: Some("my-nickname".to_string()),
186 use_keychain: true,
187 context_hub_id: None,
188 context_project_id: None,
189 context_account_id: None,
190 };
191
192 let json = serde_json::to_string(&config).unwrap();
193 assert!(json.contains("my_client_id"));
194 assert!(json.contains("my_secret"));
195 assert!(json.contains("https://custom.api.com"));
196 assert!(json.contains("my-nickname"));
197 }
198
199 #[test]
200 fn test_profile_config_deserialization() {
201 let json = r#"{
202 "client_id": "test_id",
203 "client_secret": "test_secret",
204 "base_url": "https://api.example.com",
205 "use_keychain": false
206 }"#;
207
208 let config: ProfileConfig = serde_json::from_str(json).unwrap();
209 assert_eq!(config.client_id, Some("test_id".to_string()));
210 assert_eq!(config.client_secret, Some("test_secret".to_string()));
211 assert_eq!(config.base_url, Some("https://api.example.com".to_string()));
212 assert!(!config.use_keychain);
213 }
214
215 #[test]
216 fn test_profile_config_deserialization_defaults() {
217 let json = r#"{"client_id": "test"}"#;
219 let config: ProfileConfig = serde_json::from_str(json).unwrap();
220 assert!(config.use_keychain);
221 }
222
223 #[test]
226 fn test_profiles_data_default() {
227 let data = ProfilesData::default();
228 assert!(data.active_profile.is_none());
229 assert!(data.profiles.is_empty());
230 }
231
232 #[test]
233 fn test_profiles_data_serialization() {
234 let mut profiles = std::collections::HashMap::new();
235 profiles.insert(
236 "default".to_string(),
237 ProfileConfig {
238 client_id: Some("id1".to_string()),
239 ..ProfileConfig::default()
240 },
241 );
242 profiles.insert(
243 "production".to_string(),
244 ProfileConfig {
245 client_id: Some("id2".to_string()),
246 ..ProfileConfig::default()
247 },
248 );
249
250 let data = ProfilesData {
251 active_profile: Some("default".to_string()),
252 profiles,
253 };
254
255 let json = serde_json::to_string(&data).unwrap();
256 assert!(json.contains("default"));
257 assert!(json.contains("production"));
258 assert!(json.contains("id1"));
259 assert!(json.contains("id2"));
260 }
261
262 #[test]
263 fn test_profiles_data_deserialization() {
264 let json = r#"{
265 "active_profile": "dev",
266 "profiles": {
267 "dev": {"client_id": "dev_id"},
268 "prod": {"client_id": "prod_id"}
269 }
270 }"#;
271
272 let data: ProfilesData = serde_json::from_str(json).unwrap();
273 assert_eq!(data.active_profile, Some("dev".to_string()));
274 assert_eq!(data.profiles.len(), 2);
275 assert_eq!(
276 data.profiles.get("dev").unwrap().client_id,
277 Some("dev_id".to_string())
278 );
279 }
280
281 #[test]
282 fn test_profiles_data_empty_profiles() {
283 let json = r#"{"active_profile": null}"#;
284 let data: ProfilesData = serde_json::from_str(json).unwrap();
285 assert!(data.active_profile.is_none());
286 assert!(data.profiles.is_empty());
287 }
288
289 #[test]
290 fn test_stored_token_roundtrip() {
291 let original = StoredToken {
292 access_token: "access123".to_string(),
293 refresh_token: Some("refresh456".to_string()),
294 expires_at: 1700000000,
295 scopes: vec!["read".to_string(), "write".to_string()],
296 };
297
298 let json = serde_json::to_string(&original).unwrap();
299 let deserialized: StoredToken = serde_json::from_str(&json).unwrap();
300
301 assert_eq!(original.access_token, deserialized.access_token);
302 assert_eq!(original.refresh_token, deserialized.refresh_token);
303 assert_eq!(original.expires_at, deserialized.expires_at);
304 assert_eq!(original.scopes, deserialized.scopes);
305 }
306
307 #[test]
308 fn test_profile_config_roundtrip() {
309 let original = ProfileConfig {
310 client_id: Some("client".to_string()),
311 client_secret: Some("secret".to_string()),
312 base_url: Some("https://api.com".to_string()),
313 callback_url: Some("http://localhost/callback".to_string()),
314 da_nickname: Some("nickname".to_string()),
315 use_keychain: false,
316 context_hub_id: Some("b.hub-123".to_string()),
317 context_project_id: Some("b.proj-456".to_string()),
318 context_account_id: Some("acc-789".to_string()),
319 };
320
321 let json = serde_json::to_string(&original).unwrap();
322 let deserialized: ProfileConfig = serde_json::from_str(&json).unwrap();
323
324 assert_eq!(original.client_id, deserialized.client_id);
325 assert_eq!(original.client_secret, deserialized.client_secret);
326 assert_eq!(original.base_url, deserialized.base_url);
327 assert_eq!(original.callback_url, deserialized.callback_url);
328 assert_eq!(original.da_nickname, deserialized.da_nickname);
329 assert_eq!(original.use_keychain, deserialized.use_keychain);
330 assert_eq!(original.context_hub_id, deserialized.context_hub_id);
331 assert_eq!(original.context_project_id, deserialized.context_project_id);
332 assert_eq!(original.context_account_id, deserialized.context_account_id);
333 }
334
335 #[test]
338 fn test_profile_config_context_fields_default_none() {
339 let config = ProfileConfig::default();
340 assert!(config.context_hub_id.is_none());
341 assert!(config.context_project_id.is_none());
342 assert!(config.context_account_id.is_none());
343 }
344
345 #[test]
346 fn test_profile_config_context_fields_deserialization() {
347 let json = r#"{
348 "client_id": "test",
349 "context_hub_id": "b.hub-123",
350 "context_project_id": "b.proj-456",
351 "context_account_id": "acc-789"
352 }"#;
353 let config: ProfileConfig = serde_json::from_str(json).unwrap();
354 assert_eq!(config.context_hub_id.unwrap(), "b.hub-123");
355 assert_eq!(config.context_project_id.unwrap(), "b.proj-456");
356 assert_eq!(config.context_account_id.unwrap(), "acc-789");
357 }
358
359 #[test]
360 fn test_profile_config_context_fields_skip_none_serialization() {
361 let config = ProfileConfig::default();
362 let json = serde_json::to_string(&config).unwrap();
363 assert!(!json.contains("context_hub_id"));
365 assert!(!json.contains("context_project_id"));
366 assert!(!json.contains("context_account_id"));
367 }
368}