1use std::collections::HashMap;
2use std::sync::Mutex;
3
4use async_trait::async_trait;
5
6use crate::credential::{OAuthCredential, OAuthCredentialStorage};
7use crate::error::OAuthError;
8
9#[derive(Default)]
10pub struct FakeOAuthCredentialStore {
11 credentials: Mutex<HashMap<String, OAuthCredential>>,
12}
13
14impl FakeOAuthCredentialStore {
15 pub fn new() -> Self {
16 Self { credentials: Mutex::new(HashMap::new()) }
17 }
18
19 pub fn with_credential(self, key: &str, credential: OAuthCredential) -> Self {
20 self.credentials.lock().unwrap().insert(key.to_string(), credential);
21 self
22 }
23}
24
25#[async_trait]
26impl OAuthCredentialStorage for FakeOAuthCredentialStore {
27 async fn load_credential(&self, server_id: &str) -> Result<Option<OAuthCredential>, OAuthError> {
28 Ok(self.credentials.lock().unwrap().get(server_id).cloned())
29 }
30
31 async fn save_credential(&self, key: &str, credential: OAuthCredential) -> Result<(), OAuthError> {
32 self.credentials.lock().unwrap().insert(key.to_string(), credential);
33 Ok(())
34 }
35
36 async fn delete_credential(&self, key: &str) -> Result<(), OAuthError> {
37 self.credentials.lock().unwrap().remove(key);
38 Ok(())
39 }
40
41 fn has_credential(&self, key: &str) -> bool {
42 self.credentials.lock().unwrap().contains_key(key)
43 }
44}
45
46#[cfg(test)]
47mod tests {
48 use super::*;
49
50 #[tokio::test]
51 async fn load_returns_none_when_empty() {
52 let store = FakeOAuthCredentialStore::new();
53 let result = store.load_credential("unknown").await;
54 assert!(result.unwrap().is_none());
55 }
56
57 #[tokio::test]
58 async fn save_then_load_round_trips() {
59 let store = FakeOAuthCredentialStore::new();
60 let cred = OAuthCredential {
61 client_id: "client_1".to_string(),
62 access_token: "tok_abc".to_string(),
63 refresh_token: Some("ref_xyz".to_string()),
64 expires_at: Some(9_999_999_999_999),
65 };
66
67 store.save_credential("my-server", cred.clone()).await.unwrap();
68
69 let loaded = store.load_credential("my-server").await.unwrap().expect("should find saved credential");
70 assert_eq!(loaded.client_id, "client_1");
71 assert_eq!(loaded.access_token, "tok_abc");
72 assert_eq!(loaded.refresh_token.as_deref(), Some("ref_xyz"));
73 }
74
75 #[tokio::test]
76 async fn delete_removes_credential() {
77 let store = FakeOAuthCredentialStore::new();
78 let cred = OAuthCredential {
79 client_id: "c".to_string(),
80 access_token: "t".to_string(),
81 refresh_token: None,
82 expires_at: None,
83 };
84 store.save_credential("x", cred).await.unwrap();
85 assert!(store.has_credential("x"));
86
87 store.delete_credential("x").await.unwrap();
88 assert!(!store.has_credential("x"));
89 }
90
91 #[test]
92 fn has_credential_reflects_state() {
93 let store = FakeOAuthCredentialStore::new().with_credential(
94 "present",
95 OAuthCredential {
96 client_id: "c".to_string(),
97 access_token: "t".to_string(),
98 refresh_token: None,
99 expires_at: None,
100 },
101 );
102
103 assert!(store.has_credential("present"));
104 assert!(!store.has_credential("absent"));
105 }
106}