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 cookie_value: Some(vec![AcquiredCookie {
116 name: "TestCookie".to_string(),
117 value: "cookie-value-123".to_string(),
118 }]),
119 }),
120 };
121
122 repo.save("vault.example.com".to_string(), config.clone())
124 .await
125 .unwrap();
126
127 let retrieved = repo
129 .get("vault.example.com".to_string())
130 .await
131 .unwrap()
132 .unwrap();
133
134 if let BootstrapConfig::SsoCookieVendor(vendor_config) = retrieved.bootstrap {
135 assert_eq!(vendor_config.cookie_name, Some("TestCookie".to_string()));
136 assert_eq!(vendor_config.cookie_value.as_ref().unwrap().len(), 1);
137 assert_eq!(
138 vendor_config.cookie_value.as_ref().unwrap()[0].value,
139 "cookie-value-123"
140 );
141 } else {
142 panic!("Expected SsoCookieVendor");
143 }
144 }
145
146 #[tokio::test]
147 async fn repository_overwrite() {
148 let repo = InMemoryRepository::default();
149
150 let config1 = ServerCommunicationConfig {
152 bootstrap: BootstrapConfig::Direct,
153 };
154 repo.save("vault.example.com".to_string(), config1)
155 .await
156 .unwrap();
157
158 let config2 = ServerCommunicationConfig {
160 bootstrap: BootstrapConfig::SsoCookieVendor(SsoCookieVendorConfig {
161 idp_login_url: Some("https://example.com".to_string()),
162 cookie_name: Some("Cookie".to_string()),
163 cookie_domain: Some("example.com".to_string()),
164 cookie_value: None,
165 }),
166 };
167 repo.save("vault.example.com".to_string(), config2)
168 .await
169 .unwrap();
170
171 let retrieved = repo
173 .get("vault.example.com".to_string())
174 .await
175 .unwrap()
176 .unwrap();
177 assert!(matches!(
178 retrieved.bootstrap,
179 BootstrapConfig::SsoCookieVendor(_)
180 ));
181 }
182
183 #[tokio::test]
184 async fn repository_multiple_hostnames() {
185 let repo = InMemoryRepository::default();
186
187 let config1 = ServerCommunicationConfig {
188 bootstrap: BootstrapConfig::Direct,
189 };
190 let config2 = ServerCommunicationConfig {
191 bootstrap: BootstrapConfig::SsoCookieVendor(SsoCookieVendorConfig {
192 idp_login_url: Some("https://example.com".to_string()),
193 cookie_name: Some("Cookie".to_string()),
194 cookie_domain: Some("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}