bitwarden_server_communication_config/
repository.rs1use bitwarden_error::bitwarden_error;
2use thiserror::Error;
3
4use crate::ServerCommunicationConfig;
5
6#[derive(Debug, Error, Clone, PartialEq, Eq)]
8#[bitwarden_error(flat)]
9pub enum ServerCommunicationConfigRepositoryError {
10 #[error("Failed to get configuration: {0}")]
12 GetError(String),
13
14 #[error("Failed to save configuration: {0}")]
16 SaveError(String),
17}
18
19pub trait ServerCommunicationConfigRepository: Send + Sync {
24 type GetError: std::fmt::Debug + Send + Sync + 'static;
26 type SaveError: std::fmt::Debug + Send + Sync + 'static;
28
29 fn get(
41 &self,
42 hostname: String,
43 ) -> impl std::future::Future<Output = Result<Option<ServerCommunicationConfig>, Self::GetError>>;
44
45 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 #[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 repo.save("vault.example.com".to_string(), config.clone())
125 .await
126 .unwrap();
127
128 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 let config1 = ServerCommunicationConfig {
153 bootstrap: BootstrapConfig::Direct,
154 };
155 repo.save("vault.example.com".to_string(), config1)
156 .await
157 .unwrap();
158
159 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 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 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 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}