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 + 'static {
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 let repo = InMemoryRepository::default();
108 let config = ServerCommunicationConfig {
109 bootstrap: BootstrapConfig::SsoCookieVendor(SsoCookieVendorConfig {
110 idp_login_url: "https://example.com/login".to_string(),
111 cookie_name: "TestCookie".to_string(),
112 cookie_domain: "example.com".to_string(),
113 cookie_value: Some("cookie-value-123".to_string()),
114 }),
115 };
116
117 repo.save("vault.example.com".to_string(), config.clone())
119 .await
120 .unwrap();
121
122 let retrieved = repo
124 .get("vault.example.com".to_string())
125 .await
126 .unwrap()
127 .unwrap();
128
129 if let BootstrapConfig::SsoCookieVendor(vendor_config) = retrieved.bootstrap {
130 assert_eq!(vendor_config.cookie_name, "TestCookie");
131 assert_eq!(
132 vendor_config.cookie_value,
133 Some("cookie-value-123".to_string())
134 );
135 } else {
136 panic!("Expected SsoCookieVendor");
137 }
138 }
139
140 #[tokio::test]
141 async fn repository_overwrite() {
142 let repo = InMemoryRepository::default();
143
144 let config1 = ServerCommunicationConfig {
146 bootstrap: BootstrapConfig::Direct,
147 };
148 repo.save("vault.example.com".to_string(), config1)
149 .await
150 .unwrap();
151
152 let config2 = ServerCommunicationConfig {
154 bootstrap: BootstrapConfig::SsoCookieVendor(SsoCookieVendorConfig {
155 idp_login_url: "https://example.com".to_string(),
156 cookie_name: "Cookie".to_string(),
157 cookie_domain: "example.com".to_string(),
158 cookie_value: None,
159 }),
160 };
161 repo.save("vault.example.com".to_string(), config2)
162 .await
163 .unwrap();
164
165 let retrieved = repo
167 .get("vault.example.com".to_string())
168 .await
169 .unwrap()
170 .unwrap();
171 assert!(matches!(
172 retrieved.bootstrap,
173 BootstrapConfig::SsoCookieVendor(_)
174 ));
175 }
176
177 #[tokio::test]
178 async fn repository_multiple_hostnames() {
179 let repo = InMemoryRepository::default();
180
181 let config1 = ServerCommunicationConfig {
182 bootstrap: BootstrapConfig::Direct,
183 };
184 let config2 = ServerCommunicationConfig {
185 bootstrap: BootstrapConfig::SsoCookieVendor(SsoCookieVendorConfig {
186 idp_login_url: "https://example.com".to_string(),
187 cookie_name: "Cookie".to_string(),
188 cookie_domain: "example.com".to_string(),
189 cookie_value: None,
190 }),
191 };
192
193 repo.save("vault1.example.com".to_string(), config1)
195 .await
196 .unwrap();
197 repo.save("vault2.example.com".to_string(), config2)
198 .await
199 .unwrap();
200
201 let retrieved1 = repo
203 .get("vault1.example.com".to_string())
204 .await
205 .unwrap()
206 .unwrap();
207 assert!(matches!(retrieved1.bootstrap, BootstrapConfig::Direct));
208
209 let retrieved2 = repo
210 .get("vault2.example.com".to_string())
211 .await
212 .unwrap()
213 .unwrap();
214 assert!(matches!(
215 retrieved2.bootstrap,
216 BootstrapConfig::SsoCookieVendor(_)
217 ));
218 }
219}