Skip to main content

aether_auth/
fake.rs

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}