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 values: Mutex<HashMap<String, serde_json::Value>>,
12}
13
14impl FakeOAuthCredentialStore {
15 pub fn new() -> Self {
16 Self::default()
17 }
18
19 pub fn with_credential(self, key: &str, credential: OAuthCredential) -> Self {
20 self.values.lock().unwrap().insert(key.to_string(), serde_json::to_value(credential).unwrap());
21 self
22 }
23
24 pub fn with_value(self, key: &str, value: serde_json::Value) -> Self {
25 self.values.lock().unwrap().insert(key.to_string(), value);
26 self
27 }
28}
29
30#[async_trait]
31impl OAuthCredentialStorage for FakeOAuthCredentialStore {
32 async fn load(&self, key: &str) -> Result<Option<serde_json::Value>, OAuthError> {
33 Ok(self.values.lock().unwrap().get(key).cloned())
34 }
35
36 async fn save(&self, key: &str, value: serde_json::Value) -> Result<(), OAuthError> {
37 self.values.lock().unwrap().insert(key.to_string(), value);
38 Ok(())
39 }
40
41 async fn delete(&self, key: &str) -> Result<(), OAuthError> {
42 self.values.lock().unwrap().remove(key);
43 Ok(())
44 }
45
46 fn contains(&self, key: &str) -> bool {
47 self.values.lock().unwrap().contains_key(key)
48 }
49}
50
51#[cfg(test)]
52mod tests {
53 use super::*;
54
55 #[tokio::test]
56 async fn load_returns_none_when_empty() {
57 let store = FakeOAuthCredentialStore::new();
58 let result = store.load_credential("unknown").await;
59 assert!(result.unwrap().is_none());
60 }
61
62 #[tokio::test]
63 async fn save_then_load_round_trips() {
64 let store = FakeOAuthCredentialStore::new();
65 let cred = OAuthCredential {
66 client_id: "client_1".to_string(),
67 access_token: "tok_abc".to_string(),
68 refresh_token: Some("ref_xyz".to_string()),
69 expires_at: Some(9_999_999_999_999),
70 };
71
72 store.save_credential("my-server", cred.clone()).await.unwrap();
73
74 let loaded = store.load_credential("my-server").await.unwrap().expect("should find saved credential");
75 assert_eq!(loaded.client_id, "client_1");
76 assert_eq!(loaded.access_token, "tok_abc");
77 assert_eq!(loaded.refresh_token.as_deref(), Some("ref_xyz"));
78 }
79
80 #[tokio::test]
81 async fn delete_removes_credential() {
82 let store = FakeOAuthCredentialStore::new();
83 let cred = OAuthCredential {
84 client_id: "c".to_string(),
85 access_token: "t".to_string(),
86 refresh_token: None,
87 expires_at: None,
88 };
89 store.save_credential("x", cred).await.unwrap();
90 assert!(store.contains("x"));
91
92 store.delete("x").await.unwrap();
93 assert!(!store.contains("x"));
94 }
95
96 #[tokio::test]
97 async fn secrets_round_trip_and_delete() {
98 let store = FakeOAuthCredentialStore::new().with_value("mcp:slack", serde_json::json!({"a": 1}));
99
100 assert_eq!(store.load("mcp:slack").await.unwrap(), Some(serde_json::json!({"a": 1})));
101
102 store.save("mcp:slack", serde_json::json!({"a": 2})).await.unwrap();
103 assert_eq!(store.load("mcp:slack").await.unwrap(), Some(serde_json::json!({"a": 2})));
104
105 store.delete("mcp:slack").await.unwrap();
106 assert!(store.load("mcp:slack").await.unwrap().is_none());
107 }
108
109 #[test]
110 fn has_value_reflects_state() {
111 let store = FakeOAuthCredentialStore::new().with_credential(
112 "present",
113 OAuthCredential {
114 client_id: "c".to_string(),
115 access_token: "t".to_string(),
116 refresh_token: None,
117 expires_at: None,
118 },
119 );
120
121 assert!(store.contains("present"));
122 assert!(!store.contains("absent"));
123 }
124}