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