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 domain: String,
43 ) -> impl std::future::Future<Output = Result<Option<ServerCommunicationConfig>, Self::GetError>>
44 + Send;
45
46 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 #[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 repo.save("vault.example.com".to_string(), config.clone())
122 .await
123 .unwrap();
124
125 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 let config1 = ServerCommunicationConfig {
150 bootstrap: BootstrapConfig::Direct,
151 };
152 repo.save("vault.example.com".to_string(), config1)
153 .await
154 .unwrap();
155
156 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 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 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 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}