Skip to main content

bitwarden_server_communication_config/
repository.rs

1use bitwarden_error::bitwarden_error;
2use thiserror::Error;
3
4use crate::ServerCommunicationConfig;
5
6/// Repository errors for configuration storage operations
7#[derive(Debug, Error, Clone, PartialEq, Eq)]
8#[bitwarden_error(flat)]
9pub enum ServerCommunicationConfigRepositoryError {
10    /// Error occurred while retrieving configuration
11    #[error("Failed to get configuration: {0}")]
12    GetError(String),
13
14    /// Error occurred while saving configuration
15    #[error("Failed to save configuration: {0}")]
16    SaveError(String),
17}
18
19/// Repository for storing server communication configuration
20///
21/// This trait abstracts storage to allow TypeScript implementations via State Provider
22/// in WASM contexts, while also supporting in-memory implementations for testing.
23pub trait ServerCommunicationConfigRepository: Send + Sync {
24    /// Error type returned by `get()` operations
25    type GetError: std::fmt::Debug + Send + Sync + 'static;
26    /// Error type returned by `save()` operations
27    type SaveError: std::fmt::Debug + Send + Sync + 'static;
28
29    /// Retrieves configuration for a domain
30    ///
31    /// # Arguments
32    ///
33    /// * `domain` - The server domain (e.g., "vault.amazon.com")
34    ///
35    /// # Returns
36    ///
37    /// - `Ok(Some(config))` - Configuration exists for this domain
38    /// - `Ok(None)` - No configuration exists (not an error)
39    /// - `Err(e)` - Storage operation failed
40    fn get(
41        &self,
42        domain: String,
43    ) -> impl std::future::Future<Output = Result<Option<ServerCommunicationConfig>, Self::GetError>>
44    + Send;
45
46    /// Saves configuration for a domain
47    ///
48    /// Overwrites any existing configuration for this domain.
49    ///
50    /// # Arguments
51    ///
52    /// * `domain` - The server domain (e.g., "vault.amazon.com")
53    /// * `config` - The configuration to store
54    ///
55    /// # Returns
56    ///
57    /// - `Ok(())` - Configuration saved successfully
58    /// - `Err(e)` - Storage operation failed
59    fn save(
60        &self,
61        domain: String,
62        config: ServerCommunicationConfig,
63    ) -> impl std::future::Future<Output = Result<(), Self::SaveError>> + Send;
64}
65
66#[cfg(test)]
67mod tests {
68    use std::{collections::HashMap, sync::Arc};
69
70    use tokio::sync::RwLock;
71
72    use super::*;
73    use crate::{BootstrapConfig, SsoCookieVendorConfig};
74
75    /// In-memory implementation of the repository for testing
76    #[derive(Default, Clone)]
77    struct InMemoryRepository {
78        storage: Arc<RwLock<HashMap<String, ServerCommunicationConfig>>>,
79    }
80
81    impl ServerCommunicationConfigRepository for InMemoryRepository {
82        type GetError = ();
83        type SaveError = ();
84
85        async fn get(&self, domain: String) -> Result<Option<ServerCommunicationConfig>, ()> {
86            Ok(self.storage.read().await.get(&domain).cloned())
87        }
88
89        async fn save(&self, domain: String, config: ServerCommunicationConfig) -> Result<(), ()> {
90            self.storage.write().await.insert(domain, config);
91            Ok(())
92        }
93    }
94
95    #[tokio::test]
96    async fn repository_get_none() {
97        let repo = InMemoryRepository::default();
98        let result = repo.get("vault.example.com".to_string()).await.unwrap();
99        assert!(result.is_none());
100    }
101
102    #[tokio::test]
103    async fn repository_save_and_get() {
104        use crate::AcquiredCookie;
105
106        let repo = InMemoryRepository::default();
107        let config = ServerCommunicationConfig {
108            bootstrap: BootstrapConfig::SsoCookieVendor(SsoCookieVendorConfig {
109                idp_login_url: Some("https://example.com/login".to_string()),
110                cookie_name: "TestCookie".to_string(),
111                cookie_domain: "example.com".to_string(),
112                vault_url: "https://vault.example.com".to_string(),
113                cookie_value: Some(vec![AcquiredCookie {
114                    name: "TestCookie".to_string(),
115                    value: "cookie-value-123".to_string(),
116                }]),
117            }),
118        };
119
120        // Save
121        repo.save("vault.example.com".to_string(), config.clone())
122            .await
123            .unwrap();
124
125        // Get
126        let retrieved = repo
127            .get("vault.example.com".to_string())
128            .await
129            .unwrap()
130            .unwrap();
131
132        if let BootstrapConfig::SsoCookieVendor(vendor_config) = retrieved.bootstrap {
133            assert_eq!(vendor_config.cookie_name, "TestCookie");
134            assert_eq!(vendor_config.cookie_value.as_ref().unwrap().len(), 1);
135            assert_eq!(
136                vendor_config.cookie_value.as_ref().unwrap()[0].value,
137                "cookie-value-123"
138            );
139        } else {
140            panic!("Expected SsoCookieVendor");
141        }
142    }
143
144    #[tokio::test]
145    async fn repository_overwrite() {
146        let repo = InMemoryRepository::default();
147
148        // Save first config
149        let config1 = ServerCommunicationConfig {
150            bootstrap: BootstrapConfig::Direct,
151        };
152        repo.save("vault.example.com".to_string(), config1)
153            .await
154            .unwrap();
155
156        // Overwrite with second config
157        let config2 = ServerCommunicationConfig {
158            bootstrap: BootstrapConfig::SsoCookieVendor(SsoCookieVendorConfig {
159                idp_login_url: Some("https://example.com".to_string()),
160                cookie_name: "Cookie".to_string(),
161                cookie_domain: "example.com".to_string(),
162                vault_url: "https://vault.example.com".to_string(),
163                cookie_value: None,
164            }),
165        };
166        repo.save("vault.example.com".to_string(), config2)
167            .await
168            .unwrap();
169
170        // Verify second config is retrieved
171        let retrieved = repo
172            .get("vault.example.com".to_string())
173            .await
174            .unwrap()
175            .unwrap();
176        assert!(matches!(
177            retrieved.bootstrap,
178            BootstrapConfig::SsoCookieVendor(_)
179        ));
180    }
181
182    #[tokio::test]
183    async fn repository_multiple_domains() {
184        let repo = InMemoryRepository::default();
185
186        let config1 = ServerCommunicationConfig {
187            bootstrap: BootstrapConfig::Direct,
188        };
189        let config2 = ServerCommunicationConfig {
190            bootstrap: BootstrapConfig::SsoCookieVendor(SsoCookieVendorConfig {
191                idp_login_url: Some("https://example.com".to_string()),
192                cookie_name: "Cookie".to_string(),
193                cookie_domain: "example.com".to_string(),
194                vault_url: "https://vault.example.com".to_string(),
195                cookie_value: None,
196            }),
197        };
198
199        // Save different configs for different domains
200        repo.save("vault1.example.com".to_string(), config1)
201            .await
202            .unwrap();
203        repo.save("vault2.example.com".to_string(), config2)
204            .await
205            .unwrap();
206
207        // Verify each domain has its own config
208        let retrieved1 = repo
209            .get("vault1.example.com".to_string())
210            .await
211            .unwrap()
212            .unwrap();
213        assert!(matches!(retrieved1.bootstrap, BootstrapConfig::Direct));
214
215        let retrieved2 = repo
216            .get("vault2.example.com".to_string())
217            .await
218            .unwrap()
219            .unwrap();
220        assert!(matches!(
221            retrieved2.bootstrap,
222            BootstrapConfig::SsoCookieVendor(_)
223        ));
224    }
225}